Matador Docs
API Reference

Contract Interfaces

Technical reference for the Matador smart contract ecosystem.

This reference documents the core interfaces, data structures, and errors used by the on-chain components of Matador.

IPermissionInterpreter

The IPermissionInterpreter is the heart of the system. It executes the compiled bytecode to validate transactions.

contracts/src/interfaces/IPermissionInterpreter.sol
interface IPermissionInterpreter {
    function enforce(
        bytes memory context,
        address subject,
        bytes32 stateNamespaceId,
        ExecutionContext calldata exec
    ) external;

    function preflightPolicy(bytes calldata context)
        external
        view
        returns (
            bytes32 policyCodeHash,
            bool phaseAware,
            bool hasTransient,
            bool hasAuthorize
        );

    function callBool(
        bytes memory context,
        bytes4 selector,
        bytes memory args,
        address subject,
        bytes32 stateNamespaceId,
        ExecutionContext calldata exec
    ) external view returns (bool);

    function callUint256(
        bytes memory context,
        bytes4 selector,
        bytes memory args,
        address subject,
        bytes32 stateNamespaceId,
        ExecutionContext calldata exec
    ) external view returns (uint256);

    function callBytes32(
        bytes memory context,
        bytes4 selector,
        bytes memory args,
        address subject,
        bytes32 stateNamespaceId,
        ExecutionContext calldata exec
    ) external view returns (bytes32);

    function callAddress(
        bytes memory context,
        bytes4 selector,
        bytes memory args,
        address subject,
        bytes32 stateNamespaceId,
        ExecutionContext calldata exec
    ) external view returns (address);
}

ExecutionContext

The ExecutionContext struct contains authenticated operation and lifecycle facts supplied by the adapter. Ambient chain values are interpreter-owned capabilities and are read lazily only when their opcode executes; callers do not populate them in this struct.

PropertyTypeDescription
accountaddressThe smart account or Safe whose authority and state are being exercised.
authorizersaddress[]Identities authenticated by the adapter for this operation. Policies inspect membership through authorizedBy(address).
targetaddressThe destination contract address.
valueuint256The native value (Wei) sent with the call.
databytesThe transaction calldata.
selectorbytes4The function selector from data, or 0x00000000 when unavailable.
nonceuint256The nonce of the smart account.
phaseuint8Lifecycle phase for enforce(): 1 for pre, 2 for post. Typed public reads may pass 0 because read entries cannot access context.phase.
operationIdbytes32Stable id shared by matching pre/post calls. Required for transient state.

Methods

enforce

Executes the reserved fn main() -> bool lifecycle entry in a mutable context. Successful execution requires main() to return canonical true; persistence writes are flushed only on that successful path.

  • State namespace: Requires a nonzero authority-issued stateNamespaceId. Production adapters load it from their installation record; a direct caller manages namespaces only within its own msg.sender partition. The namespace is outside ExecutionContext and policy bytecode.
  • Reverts: If any condition in the policy fails.
  • Gas: Costs vary based on the policy complexity and state access.

preflightPolicy

Fully validates callable bytecode without executing it and returns the exact policyCodeHash, validated phase-aware capability, and transient-state capability, plus the mandatory authorize() capability. Adapters call preflight before activating a policy. The configured EIP-1153 support gate is part of preflight.

Typed Read Callables

Executes a public read callable selected by its canonical function selector. These APIs are for query surfaces such as validate() or remainingDailySpend().

Read Callables

Public callable functions are read-only in the initial implementation. Use enforce for the write-capable main() lifecycle path.

IPermissionInterpreter.ExecutionContext memory exec = IPermissionInterpreter.ExecutionContext({
    account: address(this),
    authorizers: authenticatedAuthorizers,
    target: target,
    value: value,
    data: data,
    selector: bytes4(0),
    nonce: 0,
    phase: 0,
    operationId: bytes32(0)
});

// The account wrapper loads the active state namespace and delegatecalls the
// interpreter in the same storage context used by enforcement.
bool ok = account.callPermissionBool(
    permissionId,
    bytes4(keccak256("validate()")),
    "",
    exec
);

uint256 nav = account.callPermissionUint256(
    permissionId,
    bytes4(keccak256("nav(address)")),
    abi.encode(vault),
    exec
);

State-aware reads require the adapter

A direct interpreter call uses the interpreter's physical storage and direct msg.sender as namespaceAuthority; it cannot read state written under an account or module's delegatecall storage. Use account wrappers for persistence-aware reads. A custom adapter must load subject and stateNamespaceId from its trusted installation record rather than accept either from policy bytecode or ExecutionContext. Physical storage ownership is implicit in EVM call context and is not a separate encoded key field. Do not place the interpreter behind a generic proxy: every delegatecall context is classified as a trusted adapter namespace.

Safe policy rotation

Safe GuardManager does not notify the guard when it is enabled or disabled. Disabling/re-enabling therefore preserves the current policy generation; setPolicy creates the new generation. For a phase-aware policy, disable the guard in a policy-approved Safe transaction, rotate or clear policy while unguarded, then re-enable it. The cached guard completes post on the disabling transaction. clearPolicy reverts with SafeGuardStillEnabled while the guard remains enabled.

If an exactly empty binding is enabled accidentally, the guard permits only a zero-value ordinary Safe call with canonical ABI encoding to either setPolicy(bytes) on the guard or setGuard(address(0)) on that same Safe. This is the first-install/recovery path; it performs no policy enforcement or transient writes. Delegatecall, value, arbitrary targets, a nonzero guard, malformed/noncanonical calldata, and any partial policy metadata still fail closed.

The Safe-scoped active marker intentionally remains until EIP-1153 clears it at top-level transaction end. A given Safe can complete only one phase-aware lifecycle in that transaction; relayer/multicall attempts to execute another fail with SafeOperationAlreadyActive. This keeps early Safe-origin post calls fail-closed because the guard cannot authenticate them as the official post callback.

Callable Bytecode Notes

Callable bytecode starts with its header and a fixed-size function table before the instruction section. The reserved main() lifecycle entry has function id 0 and selector 0x00000000; public entries are sorted by canonical selector and carry return type, argument count, argument type tags, local count, stack bound, flags, and body offsets.

Every execution path runs preflight before dispatch. Preflight rejects malformed headers, unsupported versions, duplicate public selectors, non-public selectors, overlapping or gapped function bodies, invalid branch targets, invalid CALL_FN targets, unsupported type tags, local/stack limit violations, and read entries that can reach write-classified behavior.

Every nonempty policy also contains public authorize() -> bool. Adapters run it after authenticating the authorizer set and before lifecycle enforcement. Preflight rejects authorize call graphs that reach external reads, phase access, or BLOCK_TIMESTAMP, including transitively reachable helpers and instructions in branches that would be dead at runtime.

BLOCK_TIMESTAMP (0xb5) has no immediate bytes and pushes the current EVM block.timestamp as one uint256. It is read only when that instruction is reached. The opcode is valid in main(), pre/post lifecycle paths, and ordinary public reads, but is forbidden throughout the authorize() call graph. It is a contract-only capability in this release: the source compiler does not yet expose context.timestamp syntax.

Execution time, not validation time

A timestamp condition can allow validation and later deny execution if the block time changes before execution. It does not populate ERC-4337 validAfter or validUntil, and it does not make time-dependent authorize() logic canonical-mempool safe. Block producers also have bounded discretion over timestamps, so policies should include an appropriate time tolerance rather than treating the value as a precise wall clock.

Callable bytecode includes declared-state metadata for durable @persist and operation-scoped @transient variables. The compiler sets the phase-aware header flag when bytecode uses context.phase, declares transient state, or contains transient load/store opcodes. Runtime preflight rejects header mismatches.

The compiler and preflight accept at most 64 combined persist/transient state declarations. Preflight also rejects duplicate nameHash or declKey values across all persist/transient kinds and types. Every declared state load and store validates canonical runtime representation: bool is 0/1, address fits uint160, and uint256/bytes32 accept a full word. Already-corrupt state fails closed on load.

State keys use a namespaceAuthority: the adapter/storage owner under delegatecall, or direct msg.sender for direct interpreter calls. Durable state is keyed by its versioned domain separator, namespace authority, trusted subject, adapter-issued state namespace, and declaration key. Transient state adds the nonzero operationId and exact runtime policyCodeHash; a separate init key distinguishes an initialized zero from unwritten state. EIP-1153 profiles without TSTORE and TLOAD support reject phase-aware bytecode during preflight.

Every successful policy install or replacement receives a fresh adapter-local generation namespace, even when the bytecode is identical. Revocation, clearPolicy, and uninstall remove the active binding but retain the generation counter. Reinstall cannot recover state from an earlier installation namespace.

Adapter lifecycle requirements

Safe runs non-phase policy code exactly once in checkTransaction, with a no-op post callback. Phase-aware Safe policies are capability-gated before snapshot TSTORE, then use the Safe transaction hash and bind namespace plus exact code hash across pre/post. Kernel requires KernelMatador to be paired with itself as validator and ERC-7579 hook. Validation performs no policy write; it mints a transient capability bound to the exact UserOperation, authenticating owner, retained installation generation, and policy binding. Every nonempty install payload advances that generation, including an owner-only empty-policy binding. Kernel invokes hook pre/post around executeUserOp in one account transaction and module storage context. During normal execution, pre requires the configured EntryPoint's matching nonzero getCurrentUserOpHash() result and a validator config nonce at or above Kernel's live validNonceFrom floor. The pinned v0.9 call-gas simulation may instead resolve an unambiguous zero-hash lookup created during validation; two different hashes for the same account, execution bytes, and policy binding make that lookup ambiguous and fail it closed. Phase post revalidates the same active validator/self-hook pairing, so protected execution cannot remove or nonce-revoke its own lifecycle configuration. Every nonempty install emits PolicyBindingActivated; uninstall emits the matching PolicyBindingCleared for policy-bearing and owner-only empty bindings. A transaction-scoped issued marker also prevents the exact same Kernel authorization from being minted a second time in one top-level transaction, even after its single-use capability has been consumed.

KernelMatador

KernelMatador is the paired Kernel validator and ERC-7579 hook. Policy bytes are stored in a Solady SSTORE2 pointer rather than a dynamic storage value. The pointer's runtime code contains one leading STOP, so the maximum accepted policy is 24,575 bytes; a larger install reverts with KernelPolicyTooLarge. accountPolicies(account) reads and returns the exact bytes from the pointer, while accountPolicyPointers(account) exposes the pointer address.

function accountPolicies(address account) external view returns (bytes memory);
function accountPolicyPointers(address account) external view returns (address);
function estimationStubSignature(address claimedAuthorizer)
    external
    pure
    returns (bytes memory);

ERC-7769 estimation signature

estimationStubSignature returns the only placeholder signature recognized by the module. Its exact 89-byte layout is:

Byte rangeLengthValue
0..340x52a99cb6, the first four bytes of keccak256("matador.kernel.estimation.stub.v1")
4..2320Nonzero claimed policy authorizer
24..5532All 0xff
56..8732All 0xff
8810xff

The claimed authorizer is deliberately not authenticated. The module validates the canonical UserOperation shape and hash and, unless owner bypass applies, runs authorize() against that claim. A malformed stub or policy-denied claim does not mint a capability. An admitted stub may mint the same exact-hash, one-shot transient capability used by normal validation, but validateUserOp always returns signature failure (1) for the stub. Canonical EntryPoint handleOps therefore rejects it before execution; the capability exists so an ERC-7769 simulation can measure the policy lifecycle.

At consumption, policy-gated capabilities run authorize() again against current durable state. A configured owner-bypass stub follows the real owner bypass path instead: it verifies that the claimed authorizer is still the configured owner and skips authorize(), main, pre, and post.

The capability page is stored directly in KernelMatador transient storage at an account-associated root: offsets 0..4 hold count, issued marker, authorizer, authenticated nonce, and owner-bypass status. A separate account/execution/policy-binding lookup root stores the simulation hash at offset 0 and an ambiguity marker at offset 1. The exact-hash capability does not store a validationFailed provenance flag; stub failure remains a return-value property of validateUserOp, not a capability field.

IPermissionAccount

BasePermissionAccount implements the account-level installation and typed-read wrappers. The active namespace is observable for integration/debugging, but only the account chooses it for interpreter execution.

contracts/src/interfaces/IPermissionAccount.sol
interface IPermissionAccount {
    function grantPermission(bytes32 permissionId, bytes calldata context) external;
    function revokePermission(bytes32 permissionId) external;
    function hasPermission(bytes32 permissionId) external view returns (bool);
    function getPermissionContext(bytes32 permissionId) external view returns (bytes memory);
    function getPermissionStateNamespace(bytes32 permissionId) external view returns (bytes32);
    function getPermissionGeneration(bytes32 permissionId) external view returns (uint256);

    function callPermissionBool(
        bytes32 permissionId,
        bytes4 selector,
        bytes calldata args,
        IPermissionInterpreter.ExecutionContext calldata exec
    ) external returns (bool);

    function callPermissionUint256(
        bytes32 permissionId,
        bytes4 selector,
        bytes calldata args,
        IPermissionInterpreter.ExecutionContext calldata exec
    ) external returns (uint256);

    function callPermissionBytes32(
        bytes32 permissionId,
        bytes4 selector,
        bytes calldata args,
        IPermissionInterpreter.ExecutionContext calldata exec
    ) external returns (bytes32);

    function callPermissionAddress(
        bytes32 permissionId,
        bytes4 selector,
        bytes calldata args,
        IPermissionInterpreter.ExecutionContext calldata exec
    ) external returns (address);
}

grantPermission rejects empty/malformed policy bytes, preflights the policy, increments the retained generation for that permissionId, and activates a fresh namespace. revokePermission clears the policy and active namespace but not the generation counter. Regranting the same ID and identical bytecode still creates a new state generation.

IModuleRegistry

The registry maps 1-byte module IDs to their deployed contract addresses.

contracts/src/interfaces/IModuleRegistry.sol
interface IModuleRegistry {
    function getModule(uint8 moduleId) external view returns (address module);
}

Errors

Matador uses custom errors for gas efficiency and clarity.

Permission Errors

Errors thrown when a specific policy condition fails.

ErrorParametersDescription
UnknownOpcodeuint8 opcodeThe bytecode contained an undefined opcode.
InvalidCondition-A logical condition (AND/OR) was malformed.
RateLimitExceededcount, limit, resetA rate limit has been breached.
BalanceCheckFailedtoken, account, req, actToken or native balance requirements were not met.
CallerCheckFailedrequired, actualThe caller is not allowed.
PermissionExpiredexpiry, currentThe policy includes an expiry timestamp that has passed.

System Errors

Errors related to the interpreter's internal execution or account management.

ErrorParametersDescription
Unauthorized-Caller is not authorized to grant/revoke permissions.
InvalidPermissionContext-Permission installation supplied empty policy bytes.
InvalidStateNamespace-An account adapter has no valid active namespace for the permission.
MissingStateNamespace-The interpreter received a zero state namespace.
SafeGuardStillEnabled-clearPolicy was called before disabling the guard on the Safe.
SafeOperationAlreadyActive-A second phase-aware lifecycle was attempted for the Safe in one top-level transaction.
KernelPolicyTooLargeuint256 lengthA Kernel policy exceeds the 24,575-byte SSTORE2 payload limit.
KernelUserOperationRequired-Kernel execution has neither an authoritative current UserOperation hash nor an unambiguous simulation lookup.
StackOverflow-Policy complexity exceeded the interpreter's stack limit (1024).
StackUnderflow-An opcode attempted to pop from an empty stack.
PermissionNotFoundbytes32 idThe requested permission ID does not exist on the account.

On this page