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 toCallFlags.Allfor unrestricted access. - Native contract methods declare required permissions via
ContractMethodAttribute.RequiredCallFlags, whichContractMethodMetadataexposes to the runtime. - The
NativeContract.Invokemethod enforces permissions by verifyingstate.CallFlags.HasFlag(method.RequiredCallFlags), throwingInvalidOperationExceptionfor violations. - Inter-contract calls propagate flags explicitly through
ApplicationEngine.CallContract, allowing callers to restrict callee capabilities. - Practical patterns include using
CallFlags.ReadOnlyfor safe queries,CallFlags.Nonefor sandboxing, and explicitAllowNotifyfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →