AxonVault Reference

Complete reference for the AxonVault contract: structs, functions, events, access control, and EIP-712 details.

AxonVault Reference

AxonVault is the core Axon contract. One vault is deployed per owner via the AxonVaultFactory. It holds ERC-20 funds, manages bot authorization, and executes payments when called by an authorized relayer with a valid bot-signed intent.

Inheritance: Ownable2Step, Pausable, ReentrancyGuard, EIP712

Version: uint16 public constant VERSION = 4

Non-upgradeable. Once deployed, the contract code is immutable.

Structs

PaymentIntent

The EIP-712 typed structured data that bots sign to authorize a payment.

struct PaymentIntent {
    address bot;       // signer, must be active in vault
    address to;        // payment recipient
    address token;     // ERC-20 token address (USDC canonical, others accepted)
    uint256 amount;    // amount in token's smallest unit
    uint256 deadline;  // expiry timestamp (default window: ~5 minutes)
    bytes32 ref;       // reference linking on-chain tx to off-chain record
}

The ref field is the anchor between the on-chain transaction and the off-chain PostgreSQL record. It can be a raw short identifier (up to 32 bytes, e.g., "inv-001") or a keccak256 hash of a longer string.

BotConfig

Per-bot configuration stored on-chain. Policy values are readable by anyone for auditability; complex enforcement happens in the relayer.

struct BotConfig {
    bool isActive;                    // whether the bot can sign intents
    uint256 registeredAt;             // block.timestamp when bot was added
    uint256 maxPerTxAmount;           // hard cap for payments, enforced ON-CHAIN (0 = no limit)
    uint256 maxRebalanceAmount;       // hard cap for rebalancing input (USD), 0 = no cap
    SpendingLimit[] spendingLimits;   // flexible time-windowed limits (max 5)
    uint256 aiTriggerThreshold;       // amount above which AI scan is triggered
    bool requireAiVerification;       // always require AI scan for this bot
}

Enforcement split:

  • maxPerTxAmount -- enforced on-chain in executePayment with a require statement
  • maxRebalanceAmount -- enforced on-chain in executeSwap, checks INPUT amount (value at risk), not output
  • spendingLimits -- stored on-chain, enforced by the relayer via Redis rolling windows
  • aiTriggerThreshold / requireAiVerification -- stored on-chain, read and enforced by the relayer's policy engine

SpendingLimit

Flexible time-windowed spending limits. Each bot can have up to 5 entries.

struct SpendingLimit {
    uint256 amount;        // maximum spend within the window
    uint256 maxCount;      // max transactions in this window (0 = no count limit)
    uint256 windowSeconds; // rolling window duration
}

Examples: { amount: 1000e6, maxCount: 0, windowSeconds: 86400 } for a $1,000 daily limit with no transaction count cap, or { amount: 300e6, maxCount: 10, windowSeconds: 3600 } for a $300 hourly velocity cap with a maximum of 10 transactions per hour.

OperatorCeilings

Limits on what the operator (hot wallet) can configure. Set by the owner only. Prevents operator compromise from escalating beyond defined bounds.

struct OperatorCeilings {
    uint256 maxPerTxAmount;      // ceiling for per-bot maxPerTxAmount
    uint256 maxBotDailyLimit;    // ceiling for any single bot's daily limit
    uint256 maxOperatorBots;     // max bots the operator can register (0 = none)
    uint256 vaultDailyAggregate; // max total daily outflow across all operator-added bots
    uint256 minAiTriggerFloor;   // minimum AI trigger threshold (operator can't lower below this)
}

maxOperatorBots defaults to 0, meaning the operator cannot add any bots until the owner explicitly sets a non-zero value. This is restrictive by default.

Functions

Deposit

function deposit(address token, uint256 amount, bytes32 ref) external nonReentrant

Deposits ERC-20 tokens into the vault. Open to anyone -- no access restriction. This is required for bot-to-bot payments, programmatic funding, and exchange withdrawals.

The ref parameter links the deposit to an off-chain record (e.g., an invoice or payment request). Emits Deposited(msg.sender, token, amount, ref).

Direct ERC-20 transfers to the vault address also work (funds arrive, but no Deposited event is emitted). No receive() function exists -- ETH is rejected by default.

Withdraw

function withdraw(address token, uint256 amount, address to) external onlyOwner nonReentrant

Withdraws ERC-20 tokens from the vault. Owner only. The relayer, bots, and Axon cannot call this function. This is the non-custodial guarantee: only the owner can remove funds.

Execute Payment

function executePayment(
    PaymentIntent calldata intent,
    bytes calldata signature,
    address fromToken,
    uint256 maxFromAmount,
    address swapRouter,
    bytes calldata swapCalldata
) external onlyRelayer whenNotPaused nonReentrant

The core payment execution function. Called by the authorized relayer with a bot-signed EIP-712 intent. Handles both direct payments and swap-and-pay (see below).

On-chain checks (in order):

  1. Caller is an authorized relayer (via AxonRegistry)
  2. Vault is not paused
  3. intent.bot is active in the vault's bot whitelist
  4. EIP-712 signature is valid and was signed by intent.bot
  5. intent.deadline has not passed
  6. intent.amount does not exceed the bot's maxPerTxAmount (if set)
  7. intent.to is in the destination whitelist (if whitelist is non-empty)
  8. Intent hash has not been used before (on-chain deduplication)

Emits PaymentExecuted(intent.bot, intent.to, intent.token, intent.amount, intent.ref).

Execute Payment (with swap routing)

The full signature of executePayment supports optional swap routing parameters:

function executePayment(
    PaymentIntent calldata intent,
    bytes calldata signature,
    address fromToken,
    uint256 maxFromAmount,
    address swapRouter,
    bytes calldata swapCalldata
) external onlyRelayer whenNotPaused nonReentrant

This is a unified function that handles both direct payments and swap-and-pay in a single entry point. The bot always signs a standard PaymentIntent -- the relayer decides whether a swap is needed and supplies the routing parameters.

Key behavior:

  • If fromToken is address(0), it is a direct payment -- no swap is performed
  • If fromToken is non-zero, the relayer routes through a swap first, converting fromToken into intent.token before paying the recipient
  • The bot does not need to know what token the vault holds
  • The relayer determines whether a swap is needed and constructs the route
  • The contract verifies that the recipient received at least intent.amount of intent.token after the swap
  • swapRouter must be approved in the AxonRegistry

Bot Management

function addBot(address bot, BotConfigParams calldata params) external onlyOwnerOrOperator
function removeBot(address bot) external onlyOwnerOrOperator
function updateBotConfig(address bot, BotConfigParams calldata params) external onlyOwnerOrOperator

Register, deregister, or update bots. BotConfigParams is the input struct (excludes isActive and registeredAt, which are managed automatically by the contract):

struct BotConfigParams {
    uint256 maxPerTxAmount;
    uint256 maxRebalanceAmount;
    SpendingLimit[] spendingLimits;
    uint256 aiTriggerThreshold;
    bool requireAiVerification;
}

When called by the operator, all values are capped by OperatorCeilings. The operator can lower limits but cannot raise them above ceilings.

Operator Management

function setOperator(address newOperator) external onlyOwner

Sets or changes the operator address. The operator is enforced to be different from the owner: require(newOperator != owner()). Setting to address(0) removes the operator.

Destination Whitelist

function addGlobalDestination(address dest) external onlyOwner
function removeGlobalDestination(address dest) external onlyOwnerOrOperator
function addBotDestination(address bot, address dest) external onlyOwner
function removeBotDestination(address bot, address dest) external onlyOwnerOrOperator

Two levels of destination whitelisting:

  • Global: applies to all bots in the vault
  • Per-bot: applies to a specific bot only

If both whitelists are empty for a given bot, any destination is allowed (opt-in restriction). Only the owner can add destinations (loosening). The operator can remove destinations (tightening).

Swap Router Management

function addSwapRouter(address router) external onlyOwner
function removeSwapRouter(address router) external onlyOwner

Owner-managed list of approved DEX routers for swap routing. Targets Uniswap V3.

Rebalance Token Whitelist

function addRebalanceToken(address token) external onlyOwner
function removeRebalanceToken(address token) external onlyOwnerOrOperator

Controls which tokens executeSwap() can swap into. This restricts in-vault rebalancing only — swap-within-payment (executePayment with swap routing) is not restricted by this whitelist.

Key behavior:

  • Empty whitelist (default): Any token is allowed as output. The relayer applies its own default allowlist (USDC, WETH, USDT) as a safety net.
  • Non-empty whitelist: Only listed tokens are allowed. The relayer defaults are overridden entirely.
  • Only the owner can add tokens (loosening). Owner or operator can remove (tightening).
  • Adding address(0) reverts with ZeroAddress(). Adding a token twice is a no-op.

Why this matters: A compromised bot could call executeSwap() to swap vault USDC into a worthless token — destroying value even though funds technically stay in the vault. The rebalance whitelist prevents this by restricting swap outputs to known, owner-approved tokens.

function rebalanceTokenWhitelist(address token) external view returns (bool)
function rebalanceTokenCount() external view returns (uint256)

Read-only views for checking the whitelist state.

Pause / Unpause

function pause() external  // onlyOwner or onlyOperator
function unpause() external onlyOwner

Global emergency brake. Both the owner and operator can pause the vault. Only the owner can unpause it. When paused, executePayment, executeProtocol, and executeSwap revert.

Access Control Matrix

FunctionOwnerOperatorRelayerAnyone
depositYes
withdrawYes
executePaymentYes
executeProtocolYes
executeSwapYes
addBotYesYes (capped)
removeBotYesYes
updateBotConfigYesYes (capped)
setOperatorYes
setOperatorCeilingsYes
addGlobalDestinationYes
removeGlobalDestinationYesYes
addBotDestinationYes
removeBotDestinationYesYes
addSwapRouterYes
removeSwapRouterYes
addRebalanceTokenYes
removeRebalanceTokenYesYes
pauseYesYes
unpauseYes
transferOwnershipYes
acceptOwnershipPending owner

Events

event Deposited(address indexed sender, address indexed token, uint256 amount, bytes32 ref);
event PaymentExecuted(address indexed bot, address indexed to, address token, uint256 amount, bytes32 ref);
event SwapAndPayExecuted(address indexed bot, address indexed to, address fromToken, address toToken, uint256 fromAmount, uint256 toAmount, bytes32 ref);
event BotAdded(address indexed bot);
event BotRemoved(address indexed bot);
event BotConfigUpdated(address indexed bot);
event OperatorSet(address indexed operator);
event OperatorCeilingsUpdated();
event GlobalDestinationAdded(address indexed dest);
event GlobalDestinationRemoved(address indexed dest);
event BotDestinationAdded(address indexed bot, address indexed dest);
event BotDestinationRemoved(address indexed bot, address indexed dest);
event SwapRouterAdded(address indexed router);
event SwapRouterRemoved(address indexed router);
event RebalanceTokenAdded(address indexed token);
event RebalanceTokenRemoved(address indexed token);
// Inherited: Paused(address account), Unpaused(address account)
// Inherited: OwnershipTransferStarted, OwnershipTransferred

EIP-712 Domain

The vault uses EIP-712 for structured data signing. The domain separator is constructed at deploy time:

EIP712("AxonVault", "1")
  • name: "AxonVault"
  • version: "1"
  • chainId: block.chainid (set automatically, prevents cross-chain replay)
  • verifyingContract: the vault's own address

The PAYMENT_INTENT_TYPEHASH is:

keccak256("PaymentIntent(address bot,address to,address token,uint256 amount,uint256 deadline,bytes32 ref)")

Immutable Configuration

These values are set at deployment and cannot be changed:

ParameterDescription
axonRegistryAddress of the AxonRegistry contract

The contract always tracks executed intent hashes and rejects exact duplicates, preventing replay attacks.

Changing the relayer infrastructure requires deploying a new vault version. This is intentional -- the owner chose to trust a specific Axon registry at deployment time.