| name | approvals |
| description | Token approval UX patterns for Ethereum dApps: exact-amount approvals, Permit2 gasless signatures, EIP-5792 batched approve+action, human-readable approval display, and in-app approval management/revocation. Use this skill whenever building or reviewing ANY ERC-20 token approval flow, permit signing, spending permission UI, allowance checking, or approval revocation feature — even if the user just mentions 'approve', 'allowance', 'permit', or 'spending permission'. |
Agent index: https://ethux.design/llms.txt
Token Approvals
Scope: ERC-20 token approval flows (approve, Permit2, EIP-5792 batching), approval display, and revocation.
Does NOT cover: Native token transfers (no approval needed), NFT approvals (ERC-721/1155 setApprovalForAll), or contract-level access control.
Cross-references: signing (EIP-712 for Permit2 signatures), gas (fee display for approval transactions), shared (loading labels, post-action confirmation, error formatting), EIP-5792 (capability detection for batched approve+action), irreversibility (framing for approval confirmations).
ALWAYS
- ALWAYS default to exact-amount approvals. The approval amount should equal the transaction amount, not
MAX_UINT256.
- ALWAYS check existing allowance before requesting a new approval. If the current allowance covers the amount, skip the approval step entirely.
- ALWAYS display the spender contract address, resolved name (if available), token symbol, and approval amount in plain language before the user confirms.
- ALWAYS provide a way for users to revoke approvals from within your app.
- ALWAYS handle the case where a token requires setting allowance to 0 before setting a new non-zero value (e.g., USDT).
NEVER
- NEVER request
MAX_UINT256 (unlimited) approval by default. If you offer it as an option, label it clearly as "Unlimited" with a risk disclaimer.
- NEVER hide the approval step. Even when batching, show the user what permissions they are granting.
- NEVER assume Permit2 or EIP-5792 is available. Always implement a standard
approve() fallback.
- NEVER silently fail if an approval transaction reverts. Surface the error with a specific message (see shared Error Message Formatting).
- NEVER show an approval confirmation dialog that looks identical to the action confirmation dialog. Users must be able to tell at a glance whether they are approving permission or executing the action. Use distinct visual treatment (different header, icon, or color weight).
- NEVER auto-revoke approvals without user initiation. Even if an approval is "old," the user may have a reason for it.
Patterns
1. Exact-Amount Approval
When: User needs to grant spending permission for a specific action (swap, deposit, stake).
How:
- While checking allowance, show button as disabled with "Checking permissions..." Determine the flow before showing UI.
- Read current allowance with
allowance(owner, spender). If sufficient, skip to the action transaction.
- If insufficient, call
approve(spender, exactAmount).
- For tokens that require zero-first reset (USDT pattern): check if current allowance is non-zero and the token is on a known list. If so, send
approve(spender, 0) first, wait for confirmation, then send approve(spender, exactAmount).
- After approval confirms, re-simulate the action transaction before submitting it. If conditions have changed (e.g., swap output dropped below minimum), show: "Prices have changed since you approved. New estimate: [amount]. Continue?" This prevents the frustrating experience of paying gas for an approval and then having the action revert.
- After the action transaction is submitted, show a post-action confirmation (see shared Post-Action Confirmation).
Fallback: This is the baseline. No fallback needed.
Error state:
- Approval rejected by user: "Approval cancelled. The [action] requires permission to spend your [TOKEN]."
- Approval reverted: "Approval failed. The token contract rejected the request. This can happen with tokens that require resetting approval to zero first."
2. Permit2 Gasless Approval
When: Your contract integrates with Permit2 (Canonical Permit2 at 0x000000000022D473030F116dDEE9F6B43aC78BA3). User has already granted a one-time unlimited approval to the Permit2 contract.
Permit2 has two distinct paradigms. Choose based on your use case:
- AllowanceTransfer: For recurring permissions with sub-limits (e.g., a DEX that the user trades on repeatedly).
- SignatureTransfer: For one-time transfers (e.g., a single swap or deposit).
How (common first step):
- Check if user has approved the Permit2 contract for this token:
allowance(owner, PERMIT2_ADDRESS).
- If not approved, request a one-time approval to Permit2. This is an acceptable trade-off because Permit2 enforces its own sub-limits per spender, so the unlimited approval to the Permit2 contract itself does not grant any individual dapp unlimited access.
Sub-pattern A: AllowanceTransfer (recurring permissions)
3. Build a PermitSingle struct with exact amount, expiration timestamp, and your contract as spender.
4. Request an EIP-712 signature using useSignTypedData with the Permit2 domain and PermitSingle types (see signing Pattern 1 for EIP-712 details).
5. Submit the signature to your contract. Your contract calls permit2.permit(owner, permitSingle, signature) to set the sub-approval, then calls permit2.transferFrom(from, to, amount, token) to pull the tokens.
AllowanceTransfer typed data structure:
domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS }
types: {
PermitSingle: [
{ name: "details", type: "PermitDetails" },
{ name: "spender", type: "address" },
{ name: "sigDeadline", type: "uint256" }
],
PermitDetails: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint160" },
{ name: "expiration", type: "uint48" },
{ name: "nonce", type: "uint48" }
]
}
Sub-pattern B: SignatureTransfer (one-time transfers)
3. Build a PermitTransferFrom struct with token, amount, nonce, and deadline. Build a SignatureTransferDetails struct with recipient and amount.
4. Request an EIP-712 signature using useSignTypedData.
5. Submit to your contract, which calls permit2.permitTransferFrom(permit, transferDetails, owner, signature).
SignatureTransfer typed data structure:
domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS }
types: {
PermitTransferFrom: [
{ name: "permitted", type: "TokenPermissions" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
],
TokenPermissions: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" }
]
}
Phishing risk note: Permit2 signatures (and EIP-2612 permit signatures) are a common phishing vector. Malicious sites can trick users into signing a permit that grants the attacker spending rights. ALWAYS display the spender address prominently in the signing UI, and ALWAYS resolve the spender to a known contract name when possible. If the spender is unknown, show a Caution-level warning (see safety Pattern 1).
Fallback: If the user has not approved Permit2 and does not want to, fall back to Pattern 1 (exact-amount direct approval).
Error state:
- Signature rejected: "Signature cancelled. You can still proceed with a standard approval transaction instead."
- Permit expired: "This permit has expired. Please sign a new one."
- Nonce already used: "This permit was already used. Please try again."
3. Batched Approve + Action (EIP-5792)
When: Wallet supports wallet_sendCalls (EIP-5792). You want to combine approval and action into a single user confirmation.
How:
- Check wallet capability: use
useCapabilities (wagmi) or call wallet_getCapabilities via the provider. Check atomic.status on the current chain.
- If supported, build a calls array:
- Call 1:
approve(spender, exactAmount) on the token contract.
- Call 2: Your action call (swap, deposit, etc.) on your contract.
- Submit via
useSendCalls (wagmi) or wallet_sendCalls on the provider. Pass both calls in the calls array.
- Track the batch status with
useCallsStatus or wallet_getCallsStatus using the returned batch ID.
Decision tree:
Check atomic.status on the current chain (via wallet_getCapabilities):
"supported" -> Use wallet_sendCalls with [approve, action]
"ready" -> Wallet can upgrade via EIP-7702 to gain batching. Prompt upgrade, then batch.
undefined -> Does token support EIP-2612 permit?
YES -> Use Permit2 (Pattern 2) or native permit + action
NO -> Sequential: approve tx, wait, action tx (Pattern 1)
Fallback: If wallet_getCapabilities is not available or atomic.status is not "supported" or "ready", fall back to Pattern 2 (Permit2) or Pattern 1 (sequential approval).
Error state:
- Batch rejected: "Transaction cancelled."
- Partial failure (if non-atomic): "The approval succeeded but the [action] failed. Your approval is still active. You can retry the [action] without re-approving."
- Capability check failed: Silently fall back to sequential flow. Do not show an error for this.
4. Human-Readable Approval Display
When: Every time an approval is requested, regardless of method.
How:
- Resolve the spender address: check if it matches a known contract in your app. Display the contract name (e.g., "Uniswap Router") alongside the address.
- Format the approval amount in token terms with the token symbol: "Allow spending of 1,500 USDC".
- For unlimited approvals (if offered): display "Unlimited USDC" with a warning: "This contract can spend all your USDC until you revoke."
- Show the token icon if available.
- For Permit2: show the expiration time in human terms: "This permission expires in 30 days."
Display template:
Approve [CONTRACT_NAME] to spend [AMOUNT] [TOKEN]
Address: [0x...abcd]
Network fee for this step: ~$X.XX
[If unlimited: Warning - This allows unlimited spending until revoked]
[If Permit2: Expires: [DATE]]
Below the approval display, include a one-sentence explainer in muted text: "This gives [CONTRACT_NAME] permission to move [TOKEN] from your wallet for this transaction." For first-time approvers, expand this: "Unlike a bank transfer, you need to give explicit permission before any app can move your tokens. You can remove this permission at any time."
Fallback: If contract name cannot be resolved, show the full address with a note: "Unverified contract."
Error state: N/A (this is a display pattern, not a transaction).
5. In-App Approval Management
When: User wants to view or revoke existing approvals.
How:
- Query approval events for the user's address. Use
getLogs or an indexer to find Approval events from ERC-20 tokens where owner matches the connected address.
- For each approval found, read current allowance:
allowance(owner, spender).
- Display active approvals (allowance > 0) in a list:
- Token name and symbol
- Spender address and resolved name
- Current allowance amount (or "Unlimited")
- Date of last approval (from event block timestamp)
- Provide a "Revoke" button for each entry. Before revoking, show a confirmation: "Remove permission for [CONTRACT_NAME] to spend your [TOKEN]? If you have active orders or positions using this permission, they may stop working. You'll need to re-approve if you want to use this app again." Buttons: "Remove permission" / "Cancel".
- On confirm, call
approve(spender, 0) via useWriteContract. Show post-action confirmation (see shared Post-Action Confirmation).
- For Permit2 approvals, call
permit2.lockdown() or permit2.approve(token, spender, 0, 0) to revoke.
Fallback: If event indexing is slow or unavailable, allow users to manually input a token address to check approvals against known spenders in your app.
Error state:
- Revocation failed: "Failed to revoke approval for [TOKEN]. The transaction was rejected."
- No approvals found: "No active approvals found for this wallet." (Note: this may be incomplete if event indexing is limited.)
Implementation Resources
This skill covers UX patterns -- what the user should see and experience. For implementation-level guidance: