Skip to content

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.

The owner grants the policy, the agent signs nine fields, and the relayer supplies the two unsigned inputs the proxy bounds at execution. Owner grantAgent(…) policy · generation gen token basket · per-epoch caps on-chain, owner only Agent signs an AgentOrder agent · generation · router tokenIn · amountIn · tokenOut minOut · nonce · deadline Relayer — anyone spender · routerData not in the signed order chosen at execution time UserProxyV6.executeAsAgent policy active · generation matches · ACTION_EXECUTE (0x01) what bounds the relayer's unsigned inputs spender ∈ { the signed router, Permit2 } — else SpenderNotAllowed amountIn is charged against the per-epoch cap for tokenIn the measured output must reach the agent's signed minOut routersigned by the agent output measured on the proxy the owner receives tokenOut
The dashed line is the unsigned half of the call. A relayer picks the pull target and the route calldata, so the agent's minOut, the spender check and the epoch cap are the whole of what limits it.
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).

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.

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,
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(
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. Require the signed generation to match the current policy generation.
  9. Consume the agent nonce or revert with NonceAlreadyUsed.
  10. Charge the per-epoch input cap.
  11. 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.

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:

  1. order.owner and order.recipient must both equal the proxy owner (RecipientNotOwner if recipient differs).
  2. Both tokenIn and tokenOut must belong to the active basket, with tokenIn having a non-zero cap.
  3. The signed generation must match the agent’s current generation.
  4. The agent nonce must be unused; it is verified at announce and consumed at pull.
  5. The call timestamp must not exceed deadline. A fill attempted after deadline reverts with AuthorizationExpired(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.