Skip to content

Agent delegation lets an owner authorize a separate agent to sign bounded market-swap orders. The owner submits the policy on-chain. The agent signs an AgentOrder, and any caller can relay that order through executeAsAgent.

The agent is not the proxy owner. Direct owner authority and the agent policy are separate. See the proxy overview for the actor model and security for the delegation boundary.

grantAgent(
address agent,
uint64 expiry,
uint32 epochLen,
uint8 actionMask,
address[] tokensIn,
uint256[] caps
)

Only the owner can call grantAgent. It rejects a zero address or the owner itself with InvalidAgent, an expiry that is not in the future with InvalidExpiry, a zero epoch length with InvalidEpochLen, unequal arrays with LengthMismatch, and a zero address or native sentinel in tokensIn with InvalidToken.

The stored policy is:

Field Meaning
expiry Policy is inactive when zero or when the current time is at or after the expiry.
epochLen Length of the per-token spending epoch.
actionMask Enabled delegated actions; ACTION_EXECUTE is 0x01.
gen Current policy generation used for caps and token permissions.

The cap for each token is denominated in that input token. A token is usable only when its stored cap is greater than zero.

grantAgent increments the agent’s gen and stores the new policy. Token permissions and caps are keyed by (agent, gen, token). revokeAgent(agent) increments gen, sets expiry and actionMask to zero, and emits AgentRevoked.

Incrementing the generation orphans all permissions and caps from the previous generation in one state update. No loop over previously granted tokens is required. Execution reads permissions and caps from the current generation; gen is not a field in AgentOrder.

setAgentCap(agent, tokenIn, cap) changes one cap on the current generation. It rejects the native sentinel and zero address with InvalidToken, and rejects a policy whose expiry is zero with PolicyInactive. A cap of zero also clears the token’s usable flag. To replace the full policy, call grantAgent and create a new generation.

The epoch is a sliding window anchored at first use for each (agent, generation, tokenIn) pair. It is not a calendar period. On the first spend, the proxy records epochStart. When block.timestamp >= epochStart + epochLen, it resets the used amount to zero and anchors a new window.

Before the swap is executed, the proxy charges amountIn against the current token cap. If the charge would exceed the cap, it reverts with:

CapExceeded(uint256 used, uint256 amount, uint256 cap)

Read agentTokenInfo(agent, tokenIn) for the current generation’s allowed, cap, used, and epochStart values.

The agent signs this EIP-712 structure:

AgentOrder(
address agent,
address router,
address tokenIn,
uint256 amountIn,
address tokenOut,
uint256 minOut,
uint256 nonce,
uint256 deadline
)

The public AGENT_ORDER_TYPEHASH() getter has selector 0x5b56c616. hashAgentOrder(order) returns the typed-data digest used for verification. The proxy’s EIP-712 domain is EIP712("AgentSwap UserProxy", "2"): the version is "2", even though the contract is called V4. Clients must not derive the version from the contract name.

The nonce space is a bitmap per agent. isAgentNonceUsed(agent, nonce) reads a bit, and a consumed bit causes NonceAlreadyUsed on reuse.

executeAsAgent(
AgentOrder order,
bytes agentSig,
address spender,
bytes routerData
) external returns (uint256 amountOut)

Anyone may call this entry point. Its validation order is:

  1. Reject an order past deadline with AgentOrderExpired.
  2. Require an active, unexpired policy or revert with PolicyInactive.
  3. Require actionMask & ACTION_EXECUTE or revert with ActionNotAllowed.
  4. Reject native input or output with NativeNotSupportedInOrder.
  5. Reject equal input and output tokens with SameToken.
  6. Require the input token to be allowed in the current generation or revert with TokenNotAllowed.
  7. Verify the agent signature or revert with InvalidSignature.
  8. Consume the agent nonce or revert with NonceAlreadyUsed.
  9. Charge the per-epoch input cap.
  10. Pull the owner’s input, approve the selected target, call the signed router with routerData, and use the shared settlement path.

The output is settled to the owner, with the same non-zero minOut and owner-delivery checks as execute. The relayer is emitted as msg.sender in ExecutedAsAgent.

router is in the signed order. spender and routerData are not. The relayer therefore chooses the pull target and route calldata at execution time, down to the agent’s signed minOut.

On this path, spender == address(0) selects the signed router; a non-zero pull target must be the signed router or canonical Permit2, or the call reverts with SpenderNotAllowed. The per-epoch cap still limits the input amount. This bounds the relayer’s discretion, but does not remove it: the agent should sign a tight minOut for the intended route.

Native ETH is not available on either leg of an agent order.