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/UniswapRouter.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.to == 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.account);
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 return values must resolve to one fixed word:
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, dynamic returns, bytes4 returns, and overloaded unresolved
methods fail closed before bytecode emission.
Reads do not authorize the transaction
parameters.collection.balanceOf(context.account) 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 deadline;
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 the quoted minimum output amount is at least the policy floor
return Uniswap.exactInputSingle.params.amountOutMinimum >= parameters.minOut;
}
return false;
}Multi-output view functions are supported when you select one named
fixed-word output. The compiler uses the ABI to jump directly to the selected
return word instead of decoding the whole tuple.
import "abis/AavePool.json" as AavePool;
permission AaveHealthGate -> 1.0.0 {
parameters: {
authorizer: address,
pool: AavePool,
account: address,
minHealthFactor: uint256
}
pub fn authorize() -> bool {
return authorizedBy(parameters.authorizer);
}
fn main() -> bool {
let healthFactor = parameters.pool.getUserAccountData(parameters.account).healthFactor;
return healthFactor >= parameters.minHealthFactor;
}
}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.
Static field reads fail closed
Static ABI field reads require the matching visible selector guard. Wrong selectors, short calldata, unsupported field types, and dirty non-canonical words fail before the expression can continue.
Computed field arithmetic is checked
Static fields can participate in ordinary numeric expressions such as
Uniswap.exactInputSingle.params.amountOutMinimum + parameters.buffer >= parameters.requiredOut.
Arithmetic overflow, underflow, division by zero, and modulo by zero deny by
execution failure rather than returning false.
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.