Is ERC-20 approve dangerous? Allowances, permit and infinite approvals · Jumpstart Blockchain
Is ERC-20 approve dangerous? Allowances, permit and infinite approvals
Approve is safe; unlimited, forgotten allowances and unread permit signatures are not. Approve exact amounts, use permit with short deadlines, read every typed-data signature, and revoke old approvals.
approve itself isn't dangerous. What's dangerous is how much you approve, to whom, and for how long. An allowance is a standing permission for another address to move your tokens without asking you again, and it stays in force until you change it. Most token-drain incidents that don't involve a stolen key involve an allowance someone forgot about, or a signature someone didn't read.
How allowances work
ERC-20 tokens can't "push" themselves into a contract and notify it, so DeFi uses a two-step pull pattern:
You call token.approve(spender, amount). The token records allowance[you][spender] = amount.
The spender (a DEX router, a vault) calls token.transferFrom(you, recipient, x) whenever it wants, for any x up to the remaining allowance.
The token doesn't know or care why the spender is pulling funds. If the spender contract can be made to call transferFrom for an attacker, your allowance is the attacker's allowance.
Infinite approvals
Many dApps ask for type(uint256).max so users don't have to approve (and pay gas) before every deposit. OpenZeppelin's ERC-20 doesn't even decrease a max allowance on use. It's convenient, and the risk is simple: the allowance outlives your intent. If that spender is later exploited, upgraded to malicious code, or has a bug that allows arbitrary calls, every wallet with an open approval can be drained, including wallets that haven't used the app for years.
The most common bug pattern is a contract that executes user-supplied calls:
// DANGEROUS: anyone can make this contract call any target with any data
function execute(address target, bytes calldata data) external {
(bool ok, ) = target.call(data);
require(ok);
}
If users have approved this contract, an attacker sets target to the token and data to transferFrom(victim, attacker, balance). The token sees a legitimate call from an approved spender.
The approve race condition
Changing a non-zero allowance directly has a known issue: if you change an allowance from 100 to 50, a spender watching the mempool can spend the 100 first and then the new 50. Options:
MEV is profit from ordering transactions. A sandwich attack front-runs and back-runs a public swap, capped by its slippage. Use tight minimum outputs from fresh quotes and private RPCs.
set the allowance to 0 first, then to the new value;
use a protocol design where the spender only pulls exact amounts right after approval.
OpenZeppelin v5 removed increaseAllowance/decreaseAllowance from its ERC-20, so don't count on them existing.
Some tokens add their own quirks. Mainnet USDT, for example, doesn't return a bool from approve and rejects changing one non-zero allowance to another. In contracts, use OpenZeppelin's SafeERC20, whose forceApprove handles both cases:
using SafeERC20 for IERC20;
token.forceApprove(router, amount);
Permit (EIP-2612): approvals by signature
permit lets the owner sign an EIP-712 message off-chain, and anyone can then submit it to set the allowance. The user doesn't need a separate approve transaction, and the signature has a deadline and a nonce, so it can't be replayed.
Signing a permit in ethers v6, for a token that implements EIP-5267 (eip712Domain(), as OpenZeppelin v5 tokens do):
On the contract side, wrap permit in try/catch. Anyone who sees the signature in the mempool can submit it first, which would make your permit call revert even though the allowance is now set:
The risk with permit is phishing. A signature costs nothing, shows no transaction, and doesn't appear in your wallet's history. A malicious site can ask you to "sign in" with what is actually a permit for your entire balance, then submit it later. Wallets now display typed-data details, but you still have to read them: the spender, value and deadline fields tell you exactly what you're granting.
Permit2
Uniswap's Permit2 is a shared contract that brings signature-based approvals to any ERC-20, even ones without permit. You approve Permit2 once, then grant individual apps allowances by signature, each with an amount and an expiry. It makes per-app allowances short-lived by default, but the one approval to Permit2 is effectively a key to everything behind it, so Permit2 signatures deserve the same scrutiny as permits.
cast send $TOKEN"approve(address,uint256)"$SPENDER 0 --account me --rpc-url $RPC_URL
Block explorers' token-approval pages and tools like revoke.cash list all open approvals for an address. Revoking costs gas, so start with large or unlimited approvals to contracts you no longer use.
Guidance for developers
Request the exact amount needed by default; offer "unlimited" as a clear opt-in.
Prefer permit or Permit2 with short deadlines over long-lived approvals.
Never let users direct arbitrary calls from a contract that holds approvals.
Use SafeERC20 for every token interaction.
If your spender contract is upgradeable, remember that users approved today's code; guard upgrades with a multisig and timelock.
Checklist
An allowance is standing permission; treat it like a key.
Avoid unlimited approvals to contracts you don't use often.
Read every typed-data signature: spender, value, deadline. Never sign what you don't understand, and never paste your seed phrase anywhere.
Wrap permit in try/catch and use SafeERC20 in contracts.
Storage dominates gas costs: pack variables, cache storage reads, use constant, immutable, calldata and custom errors, emit events for off-chain data, and measure every change with forge snapshot.
It's usually a max fee below the base fee, a low tip, or an earlier stuck nonce. Speed up by resending with the same nonce and higher fees; cancel by sending 0 ETH to yourself with that nonce.