Matador
API Reference

Interpreter Architecture

How the interpreter evaluates policies, isolates state, and enforces lifecycle rules.

The Matador Permission Interpreter executes compiled policies on-chain. It validates bytecode before dispatch, evaluates policy functions, and isolates state by installation.

Execution Model

Lifecycle enforcement enters through the reserved main() function (function ID 0). Persistent writes are staged during evaluation and committed only after it returns true.

Preflight checks the policy structure; execution checks initialization and lifecycle context before evaluating main(). State loads use the installation namespace. Persistent stores accumulate in a write set, which is flushed only on success. A failed check or false result reverts the call.

Function Execution

Each function has an operand stack and 16 registers for arguments and locals. The interpreter advances a program counter through the function body, applying arithmetic, branches, state access, and function calls as instructions require.

ResourceLimit
Operand stack16 words per function
Arguments8 per function
Locals16 per function
Function-call depth16

Preflight validates instruction boundaries, branch targets, function calls, and stack/local limits before execution. Public typed reads select a public read entry and check its declared return type before dispatch.

Bytecode Layout

The current callable format places metadata before the instruction section.

SectionSizePurpose
Header48 bytesFormat version, section offsets, counts, and phase-aware flag
Function table32 bytes per entrySelectors, types, limits, flags, and body offsets
State declarations104 bytes per entryDeclaration identity, type, storage kind, and fixed seed
InstructionsVariableFunction bodies

Preflight rejects gaps or overlaps in function bodies and requires the instruction section to end at the bytecode boundary. The current format does not accept a separate data section.

See Callable Bytecode Notes for entry metadata and format validation, and the Opcode Reference for instruction encodings.

Authorization and Phases

Every nonempty policy declares public authorize() -> bool. The adapter authenticates the authorizer set, invokes authorize(), then runs lifecycle execution. Signature formats remain the adapter's responsibility.

InstructionValue
EXEC_ACCOUNTControlled smart account or Safe
EXEC_AUTHORIZER_COUNTNumber of authenticated authorizers
EXEC_AUTHORIZED_BYWhether an address belongs to the authenticated set
EXEC_PHASE1 for pre, 2 for post; unknown values revert

The authorize() call graph is read-only. Preflight rejects external reads, phase access, and BLOCK_TIMESTAMP throughout that graph, including reachable helpers and branches that would be dead at runtime. The reserved main() entry can read context.phase through EXEC_PHASE.

Block Time

BLOCK_TIMESTAMP (0xb5) reads the current block.timestamp when execution reaches it and pushes a uint256. It is allowed in main(), pre/post execution, and public typed reads. Timestamp use alone does not make a policy phase-aware or require transient storage.

It is forbidden throughout authorize() because Kernel runs authorization during ERC-4337 validation, where canonical-mempool rules prohibit TIMESTAMP. A timestamp condition governs execution time; it does not set ERC-4337 validAfter or validUntil. See Callable Bytecode Notes for opcode compatibility and timing considerations.

Declared State

State belongs to a policy installation. Replacing or reinstalling identical bytecode receives a fresh namespace rather than recovering the previous installation's values.

State kindLoad / store instructionsScope
DurablePERSIST_LOAD_DECL / PERSIST_STORE_DECLInstallation namespace
Operation-scopedTRANSIENT_LOAD_DECL / TRANSIENT_STORE_DECLInstallation, operation ID, and exact policy bytes

Storage Ownership

Call modePhysical storage ownerNamespace authority
delegatecallAdapterAdapter
Direct callInterpreterDirect msg.sender

A direct caller can manage only its own partition and cannot read adapter storage. The namespace authority supplies a nonzero stateNamespaceId. Production adapters issue a fresh value on every successful install or replacement and retain their generation counter after removal.

The keys below use immutable hash-domain identifiers. The v2 suffixes do not identify supported policy generations.

persist = H("matador.persist.decl.v2", namespaceAuthority, subject,
            stateNamespaceId, declKey)
transient = H("matador.transient.value.decl.v2", namespaceAuthority, subject,
              operationId, stateNamespaceId, policyCodeHash, declKey)

policyCodeHash binds transient pre/post state to the exact enforced bytes; durable state is independent of that hash. Physical storage ownership comes from the EVM call context and is not another encoded key field.

Initialization

Each state entry includes a 32-byte fixed seed. Transient seeds must be zero.

Allocate a Namespace

The adapter assigns a fresh stateNamespaceId for the installation.

Initialize Seeds

Before activating the binding, the trusted adapter delegatecalls initializePolicyState. It writes nonzero persistent seeds and sets a separate initialization marker. Zero-only policies need no materialization write.

Enforce Initialization

Enforcement and typed reads require the marker for policies with nonzero seeds. A seeded value later set to zero remains initialized.

Declaration Validation

The language validator, semantic compiler pass, final encoder, and runtime preflight each enforce a 64-entry limit across persistent and transient state. Preflight checks the limit before allocating the declaration array and rejects duplicate nameHash or declKey values across all kinds and types.

Every state load and store also validates the runtime word:

TypeAccepted value
bool0 or 1
addressFits in uint160
uint256, bytes32Full 256-bit word

Corrupt stored values revert on load; the interpreter never truncates or reinterprets them.

Adapter Requirements

Adapters supply trusted subject and stateNamespaceId arguments from their own installation records. These arguments remain outside policy-visible ExecutionContext.

Deployment boundary

Deploy the interpreter implementation directly, without a generic proxy. Every delegatecall context is treated as a trusted adapter namespace. Custom adapters must load the subject and namespace from their installation record; accepting caller-selected values breaks state isolation.

Transient Storage Support

Preflight derives phase/transient capability from bytecode and rejects a mismatch with the phase-aware header flag.

EIP-1153 required

Transient state uses TSTORE and TLOAD. Deployments with transient storage disabled reject phase-aware bytecode before execution.

Safe Lifecycle Limit

A Safe can run only one phase-aware lifecycle per top-level transaction. Its active-operation marker remains until EIP-1153 clears transient storage at transaction end. A second relayed or multicall execution reverts, preventing an early Safe-origin post callback from suppressing the official post check.

See Contract Interfaces for adapter APIs and lifecycle requirements.

Security Invariants

Alongside the format and state checks above, execution enforces these rules:

  • Canonical authorizers: EXEC_AUTHORIZED_BY rejects words above uint160 before address conversion, so dirty high bits cannot alias an authenticated address.
  • Checked arithmetic: ADD, SUB, and MUL revert on overflow or underflow; DIV and MOD revert on division by zero using interpreter custom errors.
  • Lifecycle integrity: Transient reads require initialized data for the same operation ID. Durable writes in phase-aware bytecode require post-guarded paths.
  • Invalid instructions: Unknown opcodes and malformed encodings revert.

On this page