DEFI RISK AND SMART CONTRACT SECURITY

A Hands‑On Guide to Closing ERC20 Approve Loopholes

8 min read
#Ethereum #Smart Contracts #security #ERC20 #Approve
A Hands‑On Guide to Closing ERC20 Approve Loopholes

In many decentralized applications the ERC‑20 approve and transferFrom functions are the linchpin that allows users to delegate token transfers to other contracts, and as explained in Understanding the Risks of ERC20 Approval and transferFrom in DeFi, mis‑handling can lead to theft, draining user balances or hijacking funds from liquidity pools. When this delegation is mis‑handled it can become a vector for theft, draining user balances or hijacking funds from liquidity pools. The purpose of this guide is to walk through the common pitfalls, as detailed in Secure Your ERC20 Tokens: Best Practices for Approval and transferFrom, demonstrate how attackers exploit them, and provide a concrete set of best‑practice techniques that developers can adopt right away to seal these gaps.

Why ERC‑20 Approve Matters

The approve function lets an account grant an allowance to a spender:

function approve(address spender, uint256 amount) public returns (bool);

The spender then calls transferFrom to move tokens from the approver’s balance, a step that is explored in detail in Unpacking DeFi Risks: The Perilous transferFrom Feature:

function transferFrom(address from, address to, uint256 amount) public returns (bool);

This two‑step process is simple yet powerful. However, it relies on the assumption that the allowance is managed safely. If the allowance is accidentally left open or if a malicious contract can increase it, the spender can siphon tokens at will.

Common Loopholes

  1. Left‑Open Allowances – Users often approve a large amount once and forget to revoke it. If that allowance is compromised, the attacker can drain the entire balance.

  2. Allowance Race Conditionapprove can be called concurrently with transferFrom. If an attacker submits a transaction that increases the allowance just before a legitimate transfer, the contract may execute the transfer with the higher allowance, effectively stealing tokens.

  3. Missing approve Zero‑Reset – Some contracts do not require that the current allowance be zero before setting a new one, a pitfall discussed in Beyond the Basics: ERC20 Approval Pitfalls for Smart Contracts. If a contract allows overwriting an allowance without resetting, an attacker can increase the allowance arbitrarily.

  4. Unchecked Return Values – Many tokens ignore the return value of approve or transferFrom, a vulnerability highlighted in Smart Contract Vulnerabilities That Target ERC20 Approvals. A failed transaction could still update the allowance or balances if the caller does not check the success flag.

  5. Re‑entrancy in Approval Callbacks – If a token’s approve emits an event that triggers a fallback function in a malicious contract, the attacker can re‑enter the contract and manipulate state before the original call completes.

These weaknesses have been exploited in high‑profile incidents. For instance, the infamous OlympusDAO hack used a stale allowance to drain assets from a vault. Understanding these patterns is the first step toward prevention.

A Hands‑On Guide to Closing ERC20 Approve Loopholes - erc20 approval diagram

Attack Vectors in Detail

Stale Allowance Attack

When a user approves a large allowance to a DeFi protocol and the protocol never revokes it, a malicious contract can call transferFrom and drain the user’s balance. The attack hinges on the fact that the protocol may not check whether the spender is authorized for a particular transaction type.

Race Condition Exploit

Consider the following sequence:

  1. User approves 1000 tokens to Contract A.
  2. Attacker submits a transaction to increase the allowance to 5000 before the user’s intended transferFrom executes.
  3. The user’s transaction runs, but the allowance is already higher, allowing the attacker to extract more than intended.

Because transaction ordering is determined by the network and gas price, attackers can front‑run legitimate users.

Zero‑Reset Flaw

Suppose a contract contains:

function setAllowance(uint256 amount) external {
    allowances[msg.sender] = amount;
}

An attacker can call setAllowance(1000) and then immediately call setAllowance(5000) without ever setting the allowance to zero first. The intermediate state may trigger logic that expects an allowance to be reset.

Defensive Patterns

Reset Before Set

Always require that the current allowance be zero before setting a new value. The ERC‑2612 permit pattern automatically resets the allowance, but for traditional approve calls, you can enforce:

require(allowances[msg.sender] == 0, "Non-zero allowance");
allowances[msg.sender] = amount;

This pattern eliminates the race condition because an attacker cannot increase the allowance until the previous one is cleared.

Safe Approve Wrapper

Wrap approve calls in a helper that first sets the allowance to zero before setting a new value:

function safeApprove(address token, address spender, uint256 amount) internal {
    IERC20(token).approve(spender, 0);
    IERC20(token).approve(spender, amount);
}

Although this introduces an extra transaction, it prevents the stale allowance problem and is widely adopted in audited contracts.

Permit and Off‑Chain Approvals

ERC‑2612 introduces the permit function, allowing approvals via signed messages rather than on‑chain transactions. Because the approval is a signed message, the token contract does not need to maintain state that could be manipulated. Encourage users to use permit where available.

Re‑entrancy Guards on Approval Callbacks

If your contract listens to Approval events or calls external contracts during approve, wrap those interactions with nonReentrant modifiers or use the checks‑effects‑interactions pattern to avoid re‑entrancy.

Audit and Static Analysis

Use tools such as Slither, MythX, and OpenZeppelin’s Defender to scan for allowance mismanagement. Regular audits by third parties can surface hidden loopholes that may not be apparent during unit testing.

Closing the Loophole: A Checklist

  • [ ] Verify that all approve calls set the allowance to zero first.
  • [ ] Use permit whenever the token supports it.
  • [ ] Ensure that transferFrom checks the spender’s purpose (e.g., only allow certain actions).
  • [ ] Validate return values of ERC‑20 functions.
  • [ ] Add re‑entrancy guards around external calls triggered by approvals.
  • [ ] Keep allowances minimal and short‑lived; prefer transfer over approve/transferFrom when possible.
  • [ ] Log all approval changes and monitor for unusual patterns.

Real‑World Example: The OlympusDAO Incident

In 2022, OlympusDAO suffered a loss of 10,000 USD through a stale allowance exploit, a case that illustrates the hidden dangers described in The Hidden Threats of ERC20 Approve and transferFrom Functions. The attackers had previously approved a large amount of OETH to a malicious contract and then called transferFrom to drain the funds. The incident highlighted the importance of revoking allowances after use. OlympusDAO’s response included updating their smart contracts to require a zero allowance before setting a new one and educating users to revoke unnecessary approvals.

Practical Implementation: A Sample Contract

Below is a minimal contract that demonstrates safe approval and transfer patterns:

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureVault is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit(address token, uint256 amount) external nonReentrant {
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        balances[msg.sender] += amount;
    }

    function safeWithdraw(address token, uint256 amount) external nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        IERC20(token).transfer(msg.sender, amount);
    }

    function approveSpender(address token, address spender, uint256 amount) external {
        // Reset allowance
        IERC20(token).approve(spender, 0);
        // Set new allowance
        IERC20(token).approve(spender, amount);
    }
}

Key takeaways from this snippet:

  • nonReentrant protects against re‑entrancy attacks that might target approval callbacks.
  • Allowance reset is performed before setting a new value.
  • Minimal state changes occur before external calls, following the checks‑effects‑interactions pattern.

Continuous Monitoring

Even with best practices in place, attackers adapt. Deploy a monitoring system that watches for sudden spikes in approvals or large transferFrom calls. Many on‑chain monitoring services provide alerts for unusual activity. Combine on‑chain event streams with off‑chain analytics to detect anomalous patterns early.

Conclusion

The simplicity of ERC‑20 approve and transferFrom is both a boon and a curse. While they enable powerful composability across DeFi protocols, they also open doors for subtle vulnerabilities. By adopting strict allowance management, leveraging ERC‑2612 permits, guarding against re‑entrancy, and routinely auditing code, developers can close the most common loopholes that have plagued the ecosystem.

The DeFi landscape is evolving rapidly, and staying ahead requires a disciplined approach to security. Treat every approval as a potential attack vector, and ensure that your contracts enforce the strongest safety checks available. With the right safeguards in place, you can build DeFi applications that are both user‑friendly and resistant to the most common approval‑based exploits.

Sofia Renz
Written by

Sofia Renz

Sofia is a blockchain strategist and educator passionate about Web3 transparency. She explores risk frameworks, incentive design, and sustainable yield systems within DeFi. Her writing simplifies deep crypto concepts for readers at every level.

Contents