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 permissionContext Properties
The context object provides access to the runtime execution environment. These properties map directly to Solidity global variables or transaction fields.
| Property | Type | Description |
|---|---|---|
context.caller | address | The address initiating the execution (e.g., msg.sender equivalent). |
context.target | address | The contract being called. |
context.account | address | The smart account being controlled. |
context.value | uint256 | The ETH value (in wei) sent with the call. |
context.data | bytes | The raw calldata. |
context.blockNumber | uint256 | Current block number (block.number). |
context.timestamp | uint256 | Current block timestamp (block.timestamp). |
context.chainId | uint256 | Current 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.
| Operator | Description | Logic |
|---|---|---|
== | Equal | Strict equality check. |
!= | Not Equal | Inequality check. |
> | Greater Than | a > b |
< | Less Than | a < b |
>= | Greater Than or Equal | a >= b |
<= | Less Than or Equal | a <= 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 afterwhen: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 intoExecutionContext.datapointing at the length word of an ABI-encodedbytesvalue.
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(bitiindicates 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.