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;Absolute paths are resolved from the root of the filesystem. Use with caution as this reduces portability.
import "/usr/local/project/abis/Router.json" as Router;If no prefix is provided, paths are resolved relative to the compiler's current working directory (usually the project root).
// Run compiler from project root
import "src/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.
- 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.
- Version Control: Commit your ABIs to git alongside your policies to ensure reproducible builds.
- Use Aliases: Pick short, descriptive aliases (e.g.,
Aaveinstead ofAavePoolV3Helper) to keep your policy logic readable.