Back to Lessons
Lesson 08

ERC-20 Security: Common Vulnerabilities

+15EFFORT
7 sections
security()
01
text

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.

02
code
solidity
1// VULNERABILITY 1: Approval Front-Running Attack
2// Scenario: You approved Spender for 100 tokens.
3// Now you want to change it to 50.
4
5// If you just call approve(spender, 50), the spender can:
6// 1. See your pending tx in the mempool
7// 2. Quickly call transferFrom() to spend the original 100
8// 3. After your approve(50) confirms, spend 50 more
9// Total stolen: 150 tokens instead of your intended 50
10
11// SAFE PATTERN: Set to 0 first, then set new amount
12await token.approve(spenderAddress, 0); // Step 1: revoke
13await token.approve(spenderAddress, 50); // Step 2: set new amount
14
15// BETTER: Use increaseAllowance/decreaseAllowance (OpenZeppelin)
16function increaseAllowance(address spender, uint256 addedValue)
17 public returns (bool) {
18 _approve(msg.sender, spender,
19 _allowances[msg.sender][spender] + addedValue);
20 return true;
21}
03
text

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.

04
note
Key Insight

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.

05
code
javascript
1// Checking and revoking approvals with viem
2const allowance = await publicClient.readContract({
3 address: tokenAddress,
4 abi: erc20Abi,
5 functionName: "allowance",
6 args: [myAddress, suspiciousContract],
7});
8
9console.log("Can spend:", formatEther(allowance), "of your tokens");
10
11// Revoke by setting allowance to 0
12const hash = await walletClient.writeContract({
13 address: tokenAddress,
14 abi: erc20Abi,
15 functionName: "approve",
16 args: [suspiciousContract, 0n],
17});
18
19// Phishing attack to watch for:
20// A site asks you to "claim a free airdrop" but the transaction
21// is actually approve(attackerAddress, type(uint256).max)
22// ALWAYS read what you are signing in your wallet!
06
code
solidity
1// SAFE: Checks-Effects-Interactions pattern
2function withdraw(uint256 amount) external {
3 // CHECK
4 require(balances[msg.sender] >= amount, "Insufficient");
5
6 // EFFECT (update state BEFORE external call)
7 balances[msg.sender] -= amount;
8
9 // INTERACTION (external call last)
10 token.transfer(msg.sender, amount);
11}
12
13// UNSAFE: State updated after external call
14function withdrawUnsafe(uint256 amount) external {
15 require(balances[msg.sender] >= amount);
16 token.transfer(msg.sender, amount); // External call FIRST
17 balances[msg.sender] -= amount; // State update AFTER
18 // ^ vulnerable to reentrancy!
19}
07
text

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.

Complete

Connect your wallet to mark this lesson as complete

Earn 15 EFFORT tokens