How CallFlags Control Smart Contract Method Invocation Permissions and Scope in Neo

CallFlags are a byte-sized bit-mask enum that defines what operations a Neo smart contract may perform, with the runtime verifying that the caller's flags contain the required flags declared on the callee method before allowing execution to proceed.

In the neo-project/neo repository, CallFlags serve as the fundamental permission mechanism governing smart contract execution. These flags determine whether a contract can read or write storage, invoke other contracts, or emit notifications. Understanding how CallFlags control smart contract method invocation permissions and scope is essential for developing secure Neo smart contracts and analyzing their behavior.

Understanding the CallFlags Bit-Mask Enum

Core Flag Definitions

The CallFlags enum is defined in src/Neo/SmartContract/CallFlags.cs as a [Flags] byte enumeration:

[Flags]
public enum CallFlags : byte
{
    None          = 0,
    ReadStates    = 0b00000001,   // contract may read storage
    WriteStates   = 0b00000010,   // contract may write storage
    AllowCall     = 0b00000100,   // contract may invoke other contracts
    AllowNotify   = 0b00001000,   // contract may emit notifications
    States        = ReadStates | WriteStates,
    ReadOnly      = ReadStates | AllowCall,
    All           = States | AllowCall | AllowNotify
}

Composite Flag Constants

The enum provides composite values to simplify common permission sets. States combines ReadStates and WriteStates, permitting both reading and writing to storage. ReadOnly combines ReadStates with AllowCall, allowing storage reads and inter-contract calls but explicitly prohibiting storage writes. All grants every capability, serving as the default context for unrestricted execution.

Where CallFlags Are Stored and Propagated

ExecutionContextState Storage

Each execution context stores its current flags in ExecutionContextState, defined in src/Neo/SmartContract/ExecutionContextState.cs:

public CallFlags CallFlags { get; set; } = CallFlags.All;

This property defaults to CallFlags.All, meaning scripts can perform any operation unless explicitly restricted.

Loading Scripts with Custom Flags

When loading a script, callers can override the default flags using the configureState parameter:

engine.LoadScript(scriptBytes, configureState: p => p.CallFlags = CallFlags.None);

This pattern creates a sandboxed environment where the loaded script cannot perform any stateful operations, useful for verification scripts that must not alter global state.

Declaring Required CallFlags on Native Contracts

ContractMethodAttribute Usage

Native contract methods declare their required permissions using ContractMethodAttribute. For example, in src/Neo/SmartContract/Native/Treasury.cs, the Verify method requires only read access:

[ContractMethod(CpuFee = 1 << 5, RequiredCallFlags = CallFlags.ReadStates)]
private bool Verify(ApplicationEngine engine) => CheckCommittee(engine);

This declaration ensures that callers must possess the ReadStates flag to invoke this method.

ContractMethodMetadata Runtime Reflection

The runtime extracts these requirements through ContractMethodMetadata, defined in src/Neo/SmartContract/Native/ContractMethodMetadata.cs:

public CallFlags RequiredCallFlags { get; }

This property is populated from the ContractMethodAttribute during contract initialization, making the permission requirements available for runtime validation.

Runtime Permission Verification

The Permission Check in NativeContract.Invoke

When a native contract method is invoked, the Invoke method in src/Neo/SmartContract/Native/NativeContract.cs performs the critical permission check:

if (!state.CallFlags.HasFlag(method.RequiredCallFlags))
    throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}.");

This validation ensures that the caller's current flags contain all bits required by the callee. If the check fails, the transaction aborts with an InvalidOperationException, preventing unauthorized state modifications or external calls.

Inter-Contract Call Flag Propagation

When contracts invoke other contracts via ApplicationEngine.CallContract, the caller explicitly specifies the flags for the new execution context:

engine.CallContract(targetHash, method, CallFlags.ReadOnly, args);

The engine creates a new execution context with the supplied CallFlags, allowing contracts to deliberately restrict the capabilities of contracts they invoke. This propagation mechanism enables fine-grained control over permission delegation across contract boundaries.

Practical Code Examples

Read-Only Contract Calls

The following example demonstrates invoking a contract method that only requires storage read permissions:

// Engine setup (test environment)
var engine = ApplicationEngine.Create(
    TriggerType.Application, null, snapshotCache, null, ProtocolSettings.Default);

// Call a contract method that only reads storage, passing ReadOnly flag.
engine.CallContract(contractHash, "balanceOf",
    CallFlags.ReadOnly, new VM.Types.Array { accountHash });

Because CallFlags.ReadOnly includes ReadStates and AllowCall, this satisfies native methods requiring only ReadStates while preventing any storage modifications.

Attempting Writes Without Proper Flags

Attempting to call a state-modifying method without WriteStates results in an exception:

engine.CallContract(contractHash, "transfer",
    CallFlags.ReadOnly, new VM.Types.Array { from, to, amount });

If the transfer method declares RequiredCallFlags = CallFlags.States (which includes WriteStates), the runtime throws:


InvalidOperationException: Cannot call this method with the flag ReadOnly.

This prevents unauthorized state modifications even if the contract logic is otherwise valid.

Sandboxing with CallFlags.None

To create a completely restricted execution environment, set CallFlags.None when loading a script:

engine.LoadScript(scriptBytes, configureState: p => p.CallFlags = CallFlags.None);

Any subsequent native contract call will fail because None does not contain required flags (e.g., ReadStates). This is useful for sandboxing or for verification scripts that must not alter state.

Emitting Events with AllowNotify

To permit a contract to emit notifications, explicitly include the AllowNotify flag:

engine.CallContract(contractHash, "emitEvent",
    CallFlags.AllowNotify, new VM.Types.Array { "hello" });

Only contracts that declare RequiredCallFlags that include AllowNotify (or All) will accept this call.

Key Source Files Reference

File Role Link
CallFlags.cs Defines the flag enum used throughout the VM. CallFlags.cs
ExecutionContextState.cs Holds the per‑call CallFlags value (defaults to All). ExecutionContextState.cs
ContractMethodAttribute.cs Attribute used on native contract methods to declare required flags. ContractMethodAttribute.cs
ContractMethodMetadata.cs Reads the attribute and exposes RequiredCallFlags for runtime checks. ContractMethodMetadata.cs
NativeContract.cs Core dispatcher that verifies state.CallFlags against method.RequiredCallFlags. NativeContract.cs – Invoke
UT_InteropService.cs (unit test) Demonstrates successful and failing calls based on flag mismatches. UT_InteropService.cs

Summary

  • CallFlags are a byte-sized bit-mask enum that defines what operations a Neo smart contract may perform during execution.
  • Each execution context stores its current flags in ExecutionContextState.CallFlags, defaulting to CallFlags.All for unrestricted access.
  • Native contract methods declare required permissions via ContractMethodAttribute.RequiredCallFlags, which ContractMethodMetadata exposes to the runtime.
  • The NativeContract.Invoke method enforces permissions by verifying state.CallFlags.HasFlag(method.RequiredCallFlags), throwing InvalidOperationException for violations.
  • Inter-contract calls propagate flags explicitly through ApplicationEngine.CallContract, allowing callers to restrict callee capabilities.
  • Practical patterns include using CallFlags.ReadOnly for safe queries, CallFlags.None for sandboxing, and explicit AllowNotify for event emission.

Frequently Asked Questions

What happens if a contract tries to call a method without the required CallFlags?

The runtime throws an InvalidOperationException with a message indicating that the method cannot be called with the current flag set. This occurs in NativeContract.Invoke after checking state.CallFlags.HasFlag(method.RequiredCallFlags), immediately aborting the transaction and preventing unauthorized operations.

Can a contract reduce its own permissions before calling another contract?

Yes. When invoking another contract via ApplicationEngine.CallContract, the caller explicitly specifies the CallFlags for the new execution context. By passing restrictive flags such as CallFlags.ReadOnly or CallFlags.None, a contract can sandbox the callee, ensuring it cannot modify state or invoke further contracts beyond the granted permissions.

What is the difference between CallFlags.States and CallFlags.ReadOnly?

CallFlags.States combines ReadStates and WriteStates, permitting both reading and writing to storage. CallFlags.ReadOnly combines ReadStates with AllowCall, allowing storage reads and inter-contract calls but explicitly prohibiting storage writes. Use States for state-modifying operations and ReadOnly for safe queries that may need to invoke other contracts.

How do I completely restrict a script from performing any state operations?

Load the script with CallFlags.None by providing a configuration lambda to LoadScript: engine.LoadScript(scriptBytes, configureState: p => p.CallFlags = CallFlags.None);. This sets the execution context's flags to zero, removing all permissions including ReadStates, WriteStates, AllowCall, and AllowNotify. Any attempt to invoke native contract methods will fail because the context lacks the required flags.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →