ERC-20 tokens are among the most attacked contracts in DeFi. Understanding the common vulnerability patterns will help you protect your tokens and evaluate the safety of protocols you interact with. The four major attack vectors are: the approval front-running attack, unlimited allowance risk, reentrancy via token callbacks, and phishing approvals.
1// VULNERABILITY 1: Approval Front-Running Attack2// Scenario: You approved Spender for 100 tokens.3// Now you want to change it to 50.45// If you just call approve(spender, 50), the spender can:6// 1. See your pending tx in the mempool7// 2. Quickly call transferFrom() to spend the original 1008// 3. After your approve(50) confirms, spend 50 more9// Total stolen: 150 tokens instead of your intended 501011// SAFE PATTERN: Set to 0 first, then set new amount12await token.approve(spenderAddress, 0); // Step 1: revoke13await token.approve(spenderAddress, 50); // Step 2: set new amount1415// BETTER: Use increaseAllowance/decreaseAllowance (OpenZeppelin)16function increaseAllowance(address spender, uint256 addedValue)17public returns (bool) {18_approve(msg.sender, spender,19_allowances[msg.sender][spender] + addedValue);20return true;21}
The unlimited approval pattern is pervasive in DeFi. Many dApps ask you to approve type(uint256).max so you only pay the approval gas once. While convenient, this means if that contract is ever exploited or has a hidden backdoor, the attacker can drain every single token you hold of that type. Safer alternatives include approving only the exact amount needed, or using ERC-2612 permit() which combines approval and transfer into one step.
Real-world impact: Over $200 million has been stolen through approval exploits. Tools like Revoke.cash and Etherscan's Token Approval Checker let you view and revoke all your outstanding approvals. You should audit your approvals regularly.
1// Checking and revoking approvals with viem2const allowance = await publicClient.readContract({3address: tokenAddress,4abi: erc20Abi,5functionName: "allowance",6args: [myAddress, suspiciousContract],7});89console.log("Can spend:", formatEther(allowance), "of your tokens");1011// Revoke by setting allowance to 012const hash = await walletClient.writeContract({13address: tokenAddress,14abi: erc20Abi,15functionName: "approve",16args: [suspiciousContract, 0n],17});1819// Phishing attack to watch for:20// A site asks you to "claim a free airdrop" but the transaction21// is actually approve(attackerAddress, type(uint256).max)22// ALWAYS read what you are signing in your wallet!
1// SAFE: Checks-Effects-Interactions pattern2function withdraw(uint256 amount) external {3// CHECK4require(balances[msg.sender] >= amount, "Insufficient");56// EFFECT (update state BEFORE external call)7balances[msg.sender] -= amount;89// INTERACTION (external call last)10token.transfer(msg.sender, amount);11}1213// UNSAFE: State updated after external call14function withdrawUnsafe(uint256 amount) external {15require(balances[msg.sender] >= amount);16token.transfer(msg.sender, amount); // External call FIRST17balances[msg.sender] -= amount; // State update AFTER18// ^ vulnerable to reentrancy!19}
Reentrancy is another concern, particularly with tokens that have callback hooks (like ERC-777 tokens backward-compatible with ERC-20). The solution is the checks-effects-interactions pattern: check conditions, update state, then make external calls. For standard ERC-20 tokens without hooks, reentrancy via transfer() is not possible, but always be cautious when integrating unknown token contracts.
Connect your wallet to mark this lesson as complete
Earn 15 EFFORT tokens