Agent Delegation
Agent delegation lets an owner authorize a separate agent to sign bounded market-swap orders and
resting intents.
The owner submits the policy on-chain. The agent signs an AgentOrder, and any caller can relay
that order through executeAsAgent on UserProxyV6.
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.
minOut, the spender check and the epoch cap are the whole of what limits it.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, ACTION_INTENT is 0x04. |
gen |
Current policy generation used for caps and token permissions. |
tokensIn is the basket, governing both input and output tokens for the agent. The cap for each
token is denominated in that token. Only the input token needs a cap greater than zero; a token with
cap 0 can be received but not spent. An order whose tokenOut is outside the basket reverts with
TokenNotAllowed(tokenOut).
Policy revision and revocation
Section titled “Policy revision 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 prior policy state in one
state update. No loop over granted tokens is required. Execution reads permissions and caps from the
current generation, which is also bound into the AgentOrder as generation.
setAgentCap(agent, token, cap) changes one cap on the current generation. It rejects the native
sentinel and zero address with InvalidToken, a token outside the basket with
TokenNotAllowed(token), and a policy whose expiry is zero with PolicyInactive.
setAgentCap(agent, token, 0) leaves membership set and only makes the token unspendable as
tokenIn. To replace the full policy or widen the basket, 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, uint64 generation, 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", "5"). generation binds the order to the policy generation
that was active when the agent signed it, so a revoked generation cannot be revived by a later grant.
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. - Require the signed
generationto match the current policy generation. - 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.
Agent-authorised intents
Section titled “Agent-authorised intents”When an agent’s policy permits ACTION_INTENT (0x04), the agent can authorize resting
settler intents on behalf of the owner. Instead of signing an AgentOrder for direct
execution, the agent signs an IntentAuthorization:
struct IntentAuthorization { bytes32 orderHash; address agent; uint64 generation; uint256 nonce; uint64 deadline;}The authorization is signed over the proxy’s EIP-712 domain (EIP712("AgentSwap UserProxy", "5")),
binding orderHash (keccak256(abi.encode(order))), the active policy generation, an
agent nonce, and an authorization deadline.
The authentication envelope passed to announce and fill on the settler is:
abi.encode(AuthKind.Agent, abi.encode(authorization, signature))The proxy checks the authorization at announce and again at pull:
order.ownerandorder.recipientmust both equal the proxy owner (RecipientNotOwnerifrecipientdiffers).- Both
tokenInandtokenOutmust belong to the active basket, withtokenInhaving a non-zero cap. - The signed
generationmust match the agent’s current generation. - The agent nonce must be unused; it is verified at
announceand consumed at pull. - The call timestamp must not exceed
deadline. A fill attempted afterdeadlinereverts withAuthorizationExpired(deadline).
Because the proxy rejects fills after deadline, the agent’s authorization deadline must cover the
order’s endTime for the entire order window to be fillable.