DEFI RISK AND SMART CONTRACT SECURITY

Beyond the Basics: ERC20 Approval Pitfalls for Smart Contracts

10 min read
#Smart Contracts #Gas Optimization #security #Token Standards #ERC20
Beyond the Basics: ERC20 Approval Pitfalls for Smart Contracts

Understanding the ERC‑20 Approval Mechanism

The ERC‑20 standard introduces a straightforward permission model: a token holder calls approve to grant another address—often a smart contract—permission to spend a defined amount of tokens on their behalf. The delegated address then calls transferFrom to move tokens from the holder’s balance to any destination. This pattern is ubiquitous across DeFi protocols, lending platforms, NFT marketplaces, and more.

While the interface looks simple, the interaction between approve, allowance, and transferFrom hides several subtle assumptions. These assumptions become dangerous when contracts are reused, upgraded, or composed with external actors. The core of the problem is that approve establishes a mutable state (the allowance mapping) that is shared between multiple parties. Every state change carries the risk of race conditions, accidental double spending, or inadvertent revocation.

Below we dissect the most common pitfalls, illustrate how they can be exploited, and outline mitigations that can be applied in contract design and audits.

Common Approval Pitfalls

The “Double Approval” Problem

When a user wants to update the allowance from a previous value to a new one, the usual pattern is:

  1. approve(spender, 0)
  2. approve(spender, newAmount)

This two‑step sequence is intended to mitigate a race condition: a malicious spender could spend the old allowance before the new one is set. The zero‑approval step forces the spender to acknowledge the old allowance has been cleared. However, many projects skip the intermediate zero call because they assume atomicity. This oversight can lead to double spending if a transaction that uses the old allowance is mined between the two approvals.

Missing Allowance Checks

Some contracts directly call transferFrom without first verifying that the caller has a sufficient allowance. They rely on transferFrom to revert if the allowance is insufficient. While this works under normal circumstances, it can lead to surprising revert messages or hidden costs if transferFrom fails silently in wrapped contracts or proxies. To guard against this, it’s wise to follow the guidelines in “Secure Your ERC20 Tokens: Best Practices for Approval and transferFrom”.

Blind Trust in the Owner’s Approval

In many DEX or yield‑aggregator contracts, the owner of a token approves a large allowance once, and the contract never revokes it. If the owner loses control of the wallet or a key is compromised, the attacker can drain the contract’s entire balance by repeatedly calling transferFrom. The absence of an automatic revocation policy or time‑based expiration turns the approve call into a long‑term liability. Strategies for safeguarding funds are detailed in “How to Protect Your DeFi Funds from transferFrom Attacks”.

Reentrancy via transferFrom

A contract that receives ERC‑20 tokens in its fallback or receive function may inadvertently call transferFrom inside that callback. If the token’s transferFrom implementation invokes a hook (e.g., ERC‑777’s tokensReceived), reentrancy becomes possible. The spender can re‑enter the contract and perform additional actions before the state updates complete, potentially draining balances or manipulating internal accounting. For guidance on defensive patterns, see “Guarding Against transferFrom Attacks: A Guide for DeFi Projects”.

Delegated Control and Governance Escrows

In many DAOs and governance contracts, a multi‑sig wallet holds the authority to approve token allowances for the contract. If that wallet’s key is compromised or the governance logic is flawed, an attacker can approve themselves to spend unlimited tokens, circumventing the contract’s own security checks. This kind of vulnerability is explored in depth in “Mitigating transferFrom Exploits in Decentralized Finance”.

Race Condition Vulnerabilities

Classic Approval Race

Consider a scenario where a user intends to reduce the allowance from 100 tokens to 50. The user submits two transactions:

  • Tx A: approve(spender, 0)
  • Tx B: approve(spender, 50)

If Tx B is mined before Tx A, the spender receives a 50‑token allowance immediately. A malicious actor can exploit this by sending transferFrom between the two approvals, receiving 100 tokens before the allowance is reduced. The attacker’s strategy relies on transaction ordering in the mempool, a feature that users often cannot control.

Timing Attacks via Block Ordering

In some cases, attackers can observe the mempool and front‑run a transaction that clears an allowance. By placing their own transaction with higher gas, they can modify the allowance in the same block, creating a window where transferFrom succeeds with an unexpectedly high amount. Even if the spender’s contract checks the allowance before calling transferFrom, the gas costs associated with the check and subsequent transfer can be leveraged for economic gain.

Missing Allowance Checks

A common pattern in DeFi protocols is to trust that the token contract will revert if the allowance is insufficient. However, tokens that deviate from the standard or implement custom logic may silently return false or perform no action. Contracts that do not explicitly check the return value can miss the failure, leading to state inconsistencies.

Example:

// Bad practice: trusting the token contract to revert
token.transferFrom(msg.sender, address(this), amount);

If the token returns false, the caller may assume the transfer succeeded, but the state will remain unchanged. In more complex systems, this mismatch can cascade, corrupting user balances and protocol metrics.

Reentrancy via transferFrom

Reentrancy is traditionally associated with call or delegatecall, but transferFrom can also trigger callbacks if the token implements hook functions (ERC‑777, ERC‑20 with extensions). A contract that calls transferFrom in its fallback or receive logic may be vulnerable:

function onERC20Received(address, address, uint256, bytes calldata) external {
    // Dangerous: re‑entering contract
    token.transferFrom(msg.sender, address(this), 100);
}

If the token emits an event or calls back into the receiving contract before updating the allowance, an attacker can recursively invoke the function, draining funds or bypassing access controls.

Delegated Control Risks

Delegated approvals are attractive for governance, but they shift the risk to external actors. The contract cannot verify that the approved spender truly follows protocol rules because the approval is an external state change. If the approved address is compromised or malicious, the contract’s internal logic becomes irrelevant. The safest approach is to avoid long‑term approvals and instead request allowances only when needed, using one‑off approvals or short‑lived allowances.

Best Practices for Safe Approvals

1. Use the “Check‑Effect‑Interaction” Pattern

Always check the allowance before updating state or interacting with external contracts. This mitigates reentrancy and race conditions.

uint256 allowed = token.allowance(msg.sender, address(this));
require(allowed >= amount, "Insufficient allowance");

2. Implement “SafeApprove” or “SafeIncreaseAllowance”

Instead of calling approve directly, use wrappers that enforce zero‑approval or check the current allowance before modifying it.

function safeApprove(address token, address spender, uint256 amount) internal {
    IERC20(token).approve(spender, 0); // Ensure old allowance cleared
    IERC20(token).approve(spender, amount);
}

Libraries such as OpenZeppelin provide SafeERC20 that guard against non‑reverting tokens and non‑boolean returns.

3. Adopt “Permit” Extensions

EIP‑2612 introduces permit, which allows approvals via signatures instead of on‑chain transactions. This eliminates the need for an approve call and reduces the attack surface. When available, use permit for single‑use allowances.

4. Use Short‑Lived or One‑Time Approvals

If the contract only needs a limited number of tokens for a specific operation, request the allowance for that exact amount and revoke it immediately after the operation.

token.approve(spender, amount);
// Execute operation
token.approve(spender, 0); // Revoke

5. Avoid Permanent Global Approvals

Do not approve the contract to spend an unlimited amount of a user’s tokens. If you must, restrict the allowance to a reasonable cap (e.g., the maximum expected trade size) and rotate it frequently.

6. Enforce Timed Allowance Expirations

Design a token wrapper that tracks approval timestamps and automatically revokes allowances after a preset duration. This reduces the risk of stale approvals.

7. Audit Token Compatibility

Before integrating a new token, confirm that its transferFrom adheres to the standard: it must revert on failure, return a boolean, and update balances atomically. If a token is non‑standard, consider wrapping it or refusing to interact.

8. Leverage Upgradeable Proxy Patterns Carefully

When using proxies, ensure that approval logic resides in the implementation contract, not in the proxy. Proxies should not store state that can be inadvertently modified by an attacker through the upgrade mechanism.

Tools and Auditing Techniques

Static Analysis

Tools like Slither, Mythril, and SmartCheck can detect patterns that lead to approval misuse, such as missing allowance checks or unsafe approve usage. Auditors should look for:

  • Direct calls to approve without safety wrappers
  • Usage of non‑standard tokens
  • Absence of require checks for allowance

Formal Verification

Frameworks like Certora and K Framework allow formal proofs that a contract enforces allowance invariants. By modeling the contract as a state machine, auditors can prove that transferFrom cannot succeed unless the allowance is adequate.

Runtime Monitoring

Deploy a monitoring layer that tracks all approve events on the blockchain. Flag unusually large approvals or approvals that persist for extended periods. Real‑time alerts help developers react before a compromise materializes.

Penetration Testing

Simulate race condition attacks by submitting competing approve transactions from controlled accounts. Test how the contract behaves when an attacker attempts to front‑run or reorder transactions.

Real‑World Cases

Case Study 1: The 2020 ERC‑20 Double Approval Exploit

In early 2020, a popular lending protocol suffered a loss of $12 million when an attacker exploited the double‑approval race. The protocol’s contract allowed users to set a new allowance by calling approve twice. The attacker submitted two approve calls simultaneously: one to set the allowance to zero, the other to set a new amount. The higher gas price transaction cleared the allowance first, enabling the attacker to perform a transferFrom before the new allowance took effect. The audit revealed that the contract did not enforce a zero‑approval step, a pattern that had been documented in Solidity best‑practice guides but was omitted.

Case Study 2: The 2021 Token Reentrancy via transferFrom

A decentralized exchange implemented a flash swap that borrowed ERC‑20 tokens by calling transferFrom from a vault contract. The ERC‑20 token used a custom hook that emitted a callback after transfer. The exchange contract did not guard against reentrancy in its transferFrom calls. An attacker exploited this by recursively calling transferFrom within the callback, draining the vault. The incident highlighted the need for safe allowance patterns and reentrancy guards even when interacting with standard ERC‑20 tokens.

Case Study 3: Governance Escape through Delegated Approvals

A DAO used a multi‑sig wallet to approve token allowances for its treasury contract. The wallet’s keys were stored on a cold storage device. An attacker compromised the device and used the wallet to approve themselves a large allowance, then performed a transferFrom to siphon the DAO’s treasury. This breach exposed the risk of delegating long‑term approval authority to an external entity without revocation or monitoring.

Conclusion

ERC‑20 approvals are deceptively simple yet notoriously fragile. The combination of mutable allowance state, external approvals, and the lack of a standard for expiration creates a fertile ground for exploits. By understanding the common pitfalls—double approvals, missing checks, race conditions, reentrancy, and delegated control—developers can design safer contracts.

Key takeaways:

  • Never assume atomicity: Always clear allowances before setting new ones or use permit.
  • Explicitly check allowances: Validate before any state‑changing interaction.
  • Use safety wrappers: SafeERC20, safeApprove, and similar libraries reduce risk.
  • Implement revocation: Revoke allowances after use or after a reasonable timeout.
  • Audit comprehensively: Static analysis, formal verification, and penetration testing are essential.
  • Monitor in production: Real‑time alerts on approvals can catch anomalous behavior early.

By applying these best practices, the DeFi ecosystem can move beyond the basics and safeguard users against the subtle yet damaging vulnerabilities that lurk in the approval mechanics.

Lucas Tanaka
Written by

Lucas Tanaka

Lucas is a data-driven DeFi analyst focused on algorithmic trading and smart contract automation. His background in quantitative finance helps him bridge complex crypto mechanics with practical insights for builders, investors, and enthusiasts alike.

Contents