Agent Delegation
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.
Grant a policy
Section titled “Grant a policy”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.
Generations and revocation
Section titled “Generations and revocation”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.
Epoch caps
Section titled “Epoch caps”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 signed order
Section titled “The signed order”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
Section titled “executeAsAgent”executeAsAgent( AgentOrder order, bytes agentSig, address spender, bytes routerData) external returns (uint256 amountOut)Anyone may call this entry point. Its validation order is:
- Reject an order past
deadlinewithAgentOrderExpired. - Require an active, unexpired policy or revert with
PolicyInactive. - Require
actionMask & ACTION_EXECUTEor revert withActionNotAllowed. - Reject native input or output with
NativeNotSupportedInOrder. - Reject equal input and output tokens with
SameToken. - Require the input token to be allowed in the current generation or revert with
TokenNotAllowed. - Verify the agent signature or revert with
InvalidSignature. - Consume the agent nonce or revert with
NonceAlreadyUsed. - Charge the per-epoch input cap.
- 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.
Relayer discretion
Section titled “Relayer discretion”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.