← Research

Token approval phishing and the risk of approve(max)

When a wallet is drained, the intuition is that someone stole the private key. Usually no one did. The owner signed something, and that signature was enough. The most reliable way to empty a wallet today is to obtain an approval, a piece of authorization the user grants willingly and then forgets they granted.

Approvals are not an attack by themselves. They are how every exchange, lending market, and NFT marketplace moves tokens on a user's behalf. The problem is how much they ask for and how long it lasts.

What an approval actually grants

An ERC-20 approve(spender, amount) lets spender move up to amount of the owner's tokens at any future time, through transferFrom. To save the user a transaction later, most apps request the maximum:

token.approve(router, type(uint256).max);   // "approve once, never think about it again"

That allowance does not expire. It survives until it is explicitly set back to zero. If the spender contract is ever compromised, upgraded to something hostile, or was malicious from the start, it can drain the full token balance the moment it chooses, with no further interaction from the owner. The signature the user made months ago is still live.

Where it turns into theft

Several variants share the same root.

The phished approval. A fake site, a poisoned airdrop claim, a lookalike dApp prompts the user to approve a token to an attacker-controlled address. The transaction looks ordinary in the wallet, often with no value attached, so it clears the user's mental filter. The drain happens later, on the attacker's schedule.

The gasless signature. EIP-2612 permit and Permit2 let a user authorize spending with an off-chain signature, no transaction and no gas. Convenient, and far easier to phish, because the wallet prompt is a hard-to-read typed-data blob rather than a recognizable transfer. One signature on a spoofed page yields a transferable approval.

The blanket NFT operator. setApprovalForAll(operator, true) hands an operator every NFT in a collection the user holds, present and future. Marketplaces need it to function, which is exactly why a fake marketplace asks for it.

The rule

An approval is a standing key to someone else's funds. Grant the least that the operation needs, scope it to a spender you can name, and give it an end.

For builders, that means designing approvals down rather than up:

  • Request the exact amount a transaction needs, not max, unless the user has knowingly opted into a persistent allowance.
  • Prefer Permit2's expiring, scoped allowances over unbounded legacy approvals where the integration allows it.
  • Make the spender address and the amount legible in your UI, so a user can tell a real prompt from a forged one.
  • Give users a visible path to review and revoke, and do not rely on them finding a third-party tool.

For the protocol receiving approvals, the obligation is the contract that holds the allowance. A spender with a max allowance over thousands of users is a single point of catastrophic failure, so its upgrade path, its access control, and any call that forwards transferFrom deserve the same scrutiny as a contract that custodies funds directly. In effect, it does.

How we review it

On a protocol that requests or holds approvals, we trace every path that can reach transferFrom or safeTransferFrom on user funds and ask what authorizes it. The questions that surface most issues:

  • Does any externally reachable function move tokens using a stored allowance without re-checking the caller's intent?
  • If the spender contract is upgradeable, who controls the upgrade, and what stops a new implementation from sweeping existing allowances?
  • Are Permit signatures bound to a deadline, a nonce, and this contract, so a captured signature cannot be replayed?

Why it hides

Approval risk hides because the dangerous step is consensual and the damage is deferred. The user clicked approve, so nothing looks broken, and the loss arrives weeks later when the allowance is finally used. The contract that asked for max did nothing wrong on the day it asked. The risk is the gap between granting authority and exercising it, and that gap is where you have to look.

:: dr-note-006 ::

If your protocol asks users for token approvals or holds allowances on their behalf, that authorization surface is one we map carefully during review. Related: we gate disclosure on a working proof.