Matador Docs
API Reference

ABI Integration

Import and use external ABIs for type-safe contract interactions.

Matador's type system is extensible through standard Ethereum ABI (Application Binary Interface) files. Importing an ABI allows the compiler to validate function calls, decoding paths, and parameter types at compile time.

Importing ABIs

Use the import keyword to load a JSON ABI file and assign it an alias. This alias becomes a namespace for accessing the contract's functions and events.

import "abis/UniswapV3Router.json" as Uniswap;
import "abis/ERC20.json" as Token;

Path Resolution

The compiler supports flexible path resolution strategies to suit different project structures.

Paths starting with ./ or ../ are resolved relative to the location of the .matador policy file.

// Policy is in /policies/swap.matador
// ABI is in /policies/abis/Router.json
import "./abis/Router.json" as Router;

Capabilities

Importing an ABI unlocks three powerful capabilities in your policies.

Calldata Inspection

Validate incoming transaction data against specific function signatures. Imported ABI members are selector values, so policies must compare them against context.selector explicitly.

// Checks if the transaction is a call to 'transfer'
// AND if the recipient matches the parameter
if (context.selector == Token.transfer) {
  return Token.transfer.recipient == parameters.allowedRecipient;
}

External Read Calls

Callable policies can read external onchain state through ABI-typed parameters. The target is explicit: bind a policy parameter to an imported ABI type, then call a view or pure method on that parameter.

import "abis/ERC721.json" as NFT;

permission BalanceGate -> 1.0.0 {
  parameters: {
    collection: NFT
  }

  let minBalance = 10;

  fn main() -> bool {
    let ownerBalance = parameters.collection.balanceOf(context.caller);
    return ownerBalance > minBalance;
  }

  pub fn balanceOf(account: address) -> uint256 {
    return parameters.collection.balanceOf(account);
  }
}

View Functions Only

External ABI calls lower to bounded STATICCALL. Mutable ABI functions are rejected, and the compiler only supports one fixed-word return value in the initial implementation: bool, uint256, bytes32, or address.

Fixed-word arguments only

External read arguments currently support bool, uint256, bytes32, bytes4, and address. Dynamic arguments, arrays, strings, arbitrary bytes, multiple returns, dynamic returns, bytes4 returns, and overloaded unresolved methods fail closed before bytecode emission.

Reads do not authorize the transaction

parameters.collection.balanceOf(context.caller) reads onchain state from the bound collection address. It does not imply that context.target == parameters.collection, and it does not imply any context.selector check. Keep transaction target and selector guards explicit in main().

Type Definitions

Use the ABI alias as a type annotation for parameters. This ensures that the parameter passed at runtime is treated as that specific contract type.

parameters: {
    // 'router' must be an address that adheres to the Uniswap ABI
    router: Uniswap
}

Static ABI Field Access

Matador supports reading fixed-width fields from ABI calldata when the field can be resolved to one 32-byte ABI word and the source code is visibly guarded by the same function selector.

For example, given a function exactInputSingle that takes a ExactInputSingleParams struct:

struct ExactInputSingleParams {
    address tokenIn;
    address tokenOut;
    uint24 fee;
    address recipient;
    uint256 amountIn;
    uint256 amountOutMinimum;
    uint160 sqrtPriceLimitX96;
}

You can write a policy that enforces rules on specific fields of that struct:

fn main() -> bool {
    if (context.target != parameters.router) {
        return false;
    }

    if (context.selector == Uniswap.exactInputSingle) {
        // Enforce the recipient is the approved wallet
        if (Uniswap.exactInputSingle.params.recipient != parameters.recipient) {
            return false;
        }

        // Enforce an exact quoted minimum output amount
        return Uniswap.exactInputSingle.params.amountOutMinimum == parameters.minOut;
    }

    return false;
}

Selector checks are explicit

Uniswap.exactInputSingle is a bytes4 selector value. It is not a boolean condition by itself, and it does not imply a target contract check. Write context.selector == Uniswap.exactInputSingle and pair it with an explicit context.target rule.

Target checks are not selector checks

context.target == parameters.router proves only the destination address. It does not prove the called function. Write a visible selector comparison such as context.selector == Uniswap.exactInputSingle before reading that function's ABI fields.

Dynamic fields are deferred

Dynamic ABI fields, arrays, strings, arbitrary bytes, unguarded field reads, and unsupported static types fail closed before callable bytecode emission.

External reads are function-local

Top-level non-persist let declarations cannot perform external ABI reads. Put view/pure ABI calls inside fn main(), pub fn, or internal helper bodies so reviewers can see when the read executes.

ABI field predicates are equality-only initially

Static ABI field access currently supports direct == and != predicates under an explicit same-function selector guard. Relational checks such as amountOutMinimum >= parameters.minOut are deferred until range lowering is specified and tested.

Best Practices

Maintain a dedicated ABI directory

Keep all your JSON ABIs in a single abis/ or interfaces/ directory at the root of your project. This makes relative imports consistent across multiple policy files.

  1. Strip Unused Artifacts: Use "human-readable" ABIs or minimal JSON files containing only the functions you need. Full Foundry/Hardhat artifacts are supported but can be large.
  2. Version Control: Commit your ABIs to git alongside your policies to ensure reproducible builds.
  3. Use Aliases: Pick short, descriptive aliases (e.g., Aave instead of AavePoolV3Helper) to keep your policy logic readable.

On this page