Matador Docs
API Reference

DSL Syntax

The complete grammar and syntax reference for the Matador DSL.

The Matador DSL (Domain-Specific Language) is a human-readable language designed for defining secure, gas-optimized permission policies. It compiles down to compact bytecode executable by the on-chain interpreter.

File Structure

A standard .matador policy file consists of four main sections.

Imports

Import external ABI definitions to enable type-safe checking of contract calls and calldata.

import "abis/UniswapV3.json" as Uniswap;

Declaration

Define the policy name and semantic version. This helps with off-chain indexing and management.

permission SwapPolicy -> 1.0.0 {

Metadata & Parameters

Define off-chain metadata (author, description) and runtime parameters that the policy expects.

metadata: {
    author: "Steer Finance",
    description: "Limits swap amount"
}

parameters: {
    token: address,
    maxAmount: uint256
}

Condition Block

The core logic. The when block contains the rules that must evaluate to true for the transaction to succeed.

when: {
    Uniswap.swap(amount: context.amount),
    context.amount <= parameters.maxAmount
}
} // End permission

Context Properties

The context object provides access to the runtime execution environment. These properties map directly to Solidity global variables or transaction fields.

PropertyTypeDescription
context.calleraddressThe address initiating the execution (e.g., msg.sender equivalent).
context.targetaddressThe contract being called.
context.accountaddressThe smart account being controlled.
context.valueuint256The ETH value (in wei) sent with the call.
context.databytesThe raw calldata.
context.blockNumberuint256Current block number (block.number).
context.timestampuint256Current block timestamp (block.timestamp).
context.chainIduint256Current chain ID (block.chainid).

Advanced Context

Extended properties like gasPrice, baseFee, blobBaseFee, and prevRandao are also available for advanced use cases.

Operators

Matador supports standard comparison operators.

OperatorDescriptionLogic
==EqualStrict equality check.
!=Not EqualInequality check.
>Greater Thana > b
<Less Thana < b
>=Greater Than or Equala >= b
<=Less Than or Equala <= b

Arithmetic expressions

Arithmetic operators (+ - * / %) are supported for numeric expressions, but arithmetic is currently limited to literals, local let variables, and state.<var> reads. Arithmetic directly over context.* and parameters.* is not yet supported.

Type casts

You can write casts like uint8(10) or uint64(parameters.value). Casts in arithmetic expressions behave as a no-op at runtime but can be used to narrow parameter/context references for opcode arguments.

Control Flow

Logical grouping allows you to combine multiple conditions.

all (AND)

Requires all nested conditions to be true. Equivalent to logical &&.

all {
    context.value > 0,
    context.target == parameters.allowedTarget
}

any (OR)

Requires at least one nested condition to be true. Equivalent to logical ||. Short-circuits on the first success.

any {
    context.caller == parameters.owner,
    context.caller == parameters.guardian
}

Negation (`not`)

not { ... } blocks invert the truth value of the enclosed block. The block behaves like an implicit all { ... } before negation.

Contract Calls

You can check calldata against specific function signatures defined in your imported ABIs.

// Checks if the transaction is a call to 'transfer' on the 'Token' contract
// AND if the 'recipient' argument equals the 'allowedRecipient' parameter
Token.transfer(recipient: parameters.allowedRecipient)

Deep Nested Access

You can access nested struct fields in calldata (e.g. params.exactInput.recipient), provided the ABI definition supports it.

Persistent State

Matador supports per-account persistent state. The compiler produces a two-phase program:

  • The when: section is read-only and must succeed before any state changes occur.
  • The commit: section executes only after when: succeeds and only in mutable mode.

Use @persist let to declare a persisted variable and state.<name> to read/write it.

permission Subscription -> v1 {
  @persist let chargeCount: uint64 = 0;

  when: {
    state.chargeCount < 10
  }

  commit: {
    state.chargeCount = state.chargeCount + 1;
  }
}

Merkle Allowlist (Recipient)

Merkle roots allow you to enforce a large allowlist without storing an address[] onchain. Store a bytes32 Merkle root under a persistent rootKey, then prove membership per transaction.

Canonical Leaf Encoding

For a recipient allowlist, use this canonical leaf:

leaf = keccak256(abi.encode(
  keccak256("matador.merkle.allowlist.recipient.v1"),
  policyNamespace,
  rootKey,
  recipient
));

Proofs from Execution Context

Use persist_merkle_membership_ctx to read a proof from ExecutionContext.data.

  • rootKey: the persistent key that stores the Merkle root.
  • leaf: the leaf hash you computed offchain.
  • proofOffset: a byte offset into ExecutionContext.data pointing at the length word of an ABI-encoded bytes value.
permission RecipientAllowlist -> v1 {
  when: {
    persist_merkle_membership_ctx(
      rootKey: "0x…",
      leaf: "0x…",
      proofOffset: 32
    )
  }
}

The proof payload is parsed as:

  • depth: uint256 (must be <= 32)
  • directionBits: uint256 (bit i indicates whether the sibling is left/right)
  • siblings: bytes32[depth]

Who supplies the proof?

The proof bytes are part of the transaction payload. In ERC-4337 flows, the user’s signature covers callData, so a bundler/solver can fetch a proof but cannot change it after the user signs.

On this page