When a decentralised application asks your wallet to sign a token approval, most users see a modal, click confirm, and move on. The modal is trying to summarise something that has four functionally distinct shapes, each with its own trust implications, each looking almost identical at the wallet layer. This post is a reference for what you are actually signing when you see those four shapes, rendered in plain language with the relevant specification notes for anyone who wants to dig deeper.
This is not a post arguing you should panic about approvals or never grant them. It is a post making sure that when you do grant one, you know what you have agreed to.
The short version
Four approval shapes, in rough order of how much trust you are extending:
approve(spender, amount)— the classic ERC-20 approval. You permitspenderto move up toamountof one specific token from your wallet. You send a transaction and pay gas.permit(spender, value, deadline, v, r, s)— EIP-2612. Same outcome asapprove, but you sign an off-chain message instead of paying gas. Anyone holding the signature can submit it to the chain before the deadline.setApprovalForAll(operator, true)— ERC-721/ERC-1155. You permitoperatorto move every token in a specific NFT collection you own — current and future — until you revoke it.- Permit2 (Uniswap) — a two-step pattern. You grant one standing approval to a Permit2 router contract, then sign cheap off-chain messages to authorise individual dApps that delegate through Permit2.
Each has a legitimate use case. Each has a different abuse path. The honest-reviewer heuristic is: the scope of what can happen if the spender turns out to be untrustworthy is different in each case, and your comfort with that scope should depend on how much you trust the contract and how much value the approval governs.
1. approve(spender, amount) — the original
This is the ERC-20 function from Ethereum Improvement Proposal 20, which every fungible token on Ethereum and its forks implements. The signature is simple: a four-byte selector (0x095ea7b3) followed by two thirty-two-byte arguments: the spender's address and the amount in the token's smallest unit.
What actually changes on-chain: the token's internal allowance[owner][spender] mapping gets updated. Once set, the spender can call transferFrom(owner, recipient, value) at any future point — in any transaction, from any caller — and the token contract will move up to amount of the owner's balance to recipient, as long as the allowance still has sufficient room and the owner's balance still has the tokens.
Three things to look at when your wallet shows you an approve modal:
- The spender address. This is who you are trusting. If it's the Uniswap Universal Router, that's a well-known contract; if it's an address you've never seen on a site you don't recognise, pause.
- The amount. Wallets typically default to the maximum 256-bit integer (
2^256 - 1, or in hex0xffff...ffff— sixty-four f characters). That number is so large it covers any practical token supply. The practical effect is unlimited. - The token contract. The approval is scoped to this token — not every token in your wallet. But if the token is a stablecoin like USDC and the spender is malicious, the full balance of that stablecoin is reachable.
Why do wallets default to unlimited?
Because the alternative is worse for the user experience of most dApps. A Uniswap swap that only approves the exact input amount forces the user to sign another approve transaction next time they swap, paying gas twice per swap instead of once. A perpetual-DEX interaction that only approves the opening margin cannot deposit more collateral later without another approval. The unlimited-approval default trades the long-tail safety of "I can never over-pay" for the everyday convenience of "this protocol works in one transaction per swap instead of two."
This is a defensible trade for a protocol you use often and trust highly. It is a bad trade for a one-off interaction with a contract you will not use again. Your wallet does not know which case applies; you do.
2. permit(...) — approval via signature
EIP-2612 (eips.ethereum.org/EIPS/eip-2612) added the permit function to ERC-20 tokens that opt in. It does the same thing approve does — setting allowance[owner][spender] — but the authorisation arrives as an EIP-712 typed-data signature rather than an on-chain transaction sent by the owner.
Mechanically: the owner signs a structured message containing {owner, spender, value, nonce, deadline}. Any party can then submit that signature to the token's permit function, along with the parameters. The token contract verifies the signature, increments the nonce (preventing replay), and updates the allowance. The spender then calls transferFrom as usual.
The practical UX benefit: the user doesn't pay gas for the approval — only for the subsequent action (the swap, the deposit) — and often those two can be bundled into a single user-submitted transaction that the dApp's smart contract composes. Less friction, less gas.
The practical risk: signatures in MetaMask and other wallets are shown as a typed-data screen that many users have learned to click through without reading. A permit signature looks deceptively similar to a harmless "sign in to this website" signature, but it is not harmless — if you sign a permit with yourself as owner, a malicious address as spender, and a future deadline, the malicious spender can submit it whenever it wants, without you paying gas, without you seeing a transaction confirmation.
The field to read in the wallet's typed-data screen: look for the word Permit as the message type. Look at the spender field. If the spender is not the contract you think you are interacting with, stop.
3. setApprovalForAll(operator, true) — NFTs
ERC-721 and ERC-1155 use a different approval model than ERC-20. You can approve a specific token ID (approve(to, tokenId)) to a specific operator, but the far more common call from NFT marketplaces is setApprovalForAll(operator, true).
When you sign that, you are authorising the operator to move any token you own in that collection — including tokens you don't own yet, if you acquire them later — until you call setApprovalForAll(operator, false) to revoke.
This is the scope that makes NFT drainers particularly effective. If you signed setApprovalForAll to a marketplace that later turned out to be compromised — as happened in several 2022–2023 incidents — the operator could sweep every NFT in that collection from your wallet in one call. The approval does not expire; it does not cap the number of NFTs; it is live until you manually revoke it.
The honest rule: for NFT collections you genuinely trade, setApprovalForAll to the marketplace you use is standard practice. When you stop using that marketplace — or when you've finished an intensive trading period — revoke. Don't leave dozens of collection-level "all-token" approvals standing across wallets and marketplaces you haven't touched in months.
4. Permit2 — the two-step pattern
Uniswap's Permit2 (github.com/Uniswap/permit2) was introduced in 2023 as a way to give many dApps a consistent, signature-based approval experience without each dApp needing to request its own standing token allowance. Permit2 has been adopted across the Uniswap ecosystem and is starting to appear in other DEX aggregators.
The mechanism is two layers:
- Layer 1: you make one standing
approve(permit2, max)on each ERC-20 you want to use with Permit2. This is an on-chain transaction; you pay gas. After this, the Permit2 contract has an unlimited allowance to move that token on your behalf. - Layer 2: every time a Uniswap-Permit2-aware dApp wants to spend some of that token, you sign an off-chain EIP-712 message naming that dApp as authorised for this amount for this deadline. The dApp presents the signature to Permit2, Permit2 presents
transferFromto the token, and the tokens move.
The UX is better than raw approve because you only ever do layer 1 once per token, and layer 2 is a gas-free signature. The trust shape is different: instead of trusting every individual dApp with a standing allowance, you are trusting Permit2 itself with a standing allowance and relying on per-use signatures to compartmentalise further trust.
Two things this means in practice:
- Permit2's own contract security matters a lot. If Permit2 were ever compromised, the standing allowances from every user who opted in would be at risk. The Permit2 contract has been heavily audited and the Uniswap team carries meaningful reputation on it, but this is the trust concentration you are accepting.
- The per-use signatures are still the thing to read carefully. A Permit2 signature names a specific spender (the dApp you are using right now). If the modal shows a different spender than the site you are on, treat that as an alarm.
What the wallet shows versus what the contract sees
A recurring gap in every approval shape is that the wallet's modal is a summary of something more complex than it presents. MetaMask's latest approval UI does a reasonable job of surfacing the token, spender, and amount. Rabby's presents a clearer risk snapshot including whether the spender is known-safe. Others vary.
But no wallet can surface the intent of the contract behind the spender address. The spender is just a twenty-byte Ethereum address. Whether that address is a Uniswap router, an Aave v3 pool, a Socket bridge, or a freshly deployed drainer from an hour ago — that's context you bring, not context the wallet shows. Copying the spender address into Etherscan and looking at its activity is the most reliable sanity check when you are not sure.
The unlimited number, in hex
When a wallet displays an approval as "Unlimited" or shows 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff — that is 2^256 − 1. Written out, it's roughly 1.15 × 10^77. For context: the total supply of USDC is about 3 × 10^16 raw units (six decimals). The supply of DAI is about 4 × 10^27 raw units (eighteen decimals). Even Bitcoin's total supply is 2.1 × 10^15 satoshis. The unlimited approval number exceeds every real token supply by such a margin that the practical effect is no cap.
This matters because some tooling (including ours, in the warning overlay) flags unlimited approvals specifically. The flag is not a claim that the approval is malicious. It is a claim that the approval has no natural ceiling — if the spender ever becomes untrustworthy, the cap won't stop them. For a highly-used DEX you interact with daily, that risk may be acceptable. For a random contract from a link in a Discord DM, it is not.
A practical checklist when you sign
Before pressing confirm on any of the four shapes:
- Read the spender address. Not just the wallet's name-resolved display — copy the address and recognise it. Save recurring ones (Uniswap router, Aave pool) to a notes file you trust.
- Ask whether the amount needs to be unlimited. If the dApp supports a bounded approval, the inconvenience of a second transaction in six months is cheaper than the risk of a compromised spender.
- If it's a
setApprovalForAll, think about how often you trade this NFT collection. Never? Decline. Occasionally? Accept and revoke when the session ends. Every day? Accept and monitor. - If it's a
permitor Permit2 signature, read the typed-data message. The spender and deadline fields are the most important. A deadline years in the future is a minor red flag; an unknown spender is a major one. - Revoke the ones you have forgotten. Every wallet has old approvals no one is using. The free scanner at allowanceguard.com shows them across 27 chains in about a minute.
The tools landscape
Wallet-side tools that help with this include: Rabby (shows spender reputation inline), MetaMask's transaction insights (partial), Revoke.cash (the longest-running dedicated allowance manager), and ourselves. They are complementary, not exclusive — different tools catch different attacker patterns, and running a periodic sweep through more than one is not wasted effort.
What no tool can do is make the decision for you on whether a specific spender is worth the approval. The decision is always yours. The tool's job is to surface the information clearly enough that the decision is possible.
A final note on the trend
Newer approval patterns are moving toward shorter-scope delegations: permit with tight deadlines, Permit2 with per-use signatures, EIP-7702 proposals that let an externally-owned account delegate narrowly-defined behaviour to a smart contract for a single transaction. The direction of travel is away from "grant a standing allowance and hope nothing breaks for the next eighteen months" toward "authorise this specific thing right now and don't leave state behind."
That is a better default. We're not there yet; most dApps still default to approve(max) for compatibility. In the meantime, the best you can do is read what you're signing, keep unlimited standing approvals to a short list of protocols you actively use, and clean up periodically. Everything else — including us — is a helper on top of that discipline.
