← Back to Blog

Token Approval Security: The Infinite Allowance Problem and How to Fix It

2026-04-17 solidity security erc-20 approve allowance permit defi smart-contract 2026

Token approvals are the most common user action in DeFi — and one of the most dangerous. Approving a contract to spend your tokens seems routine. But the near-universal practice of requesting approve(spender, type(uint256).max) (infinite allowance) has been a factor in hundreds of millions of dollars of losses.

This post covers the problem from both directions: what users face, and what smart contract developers can do to make their protocols safer by design.


How Token Approvals Work

ERC-20's approve() function grants another address permission to spend up to a specified amount of your tokens:

// User calls this to let a DEX spend their USDC
IERC20(usdc).approve(dex, amount);

// DEX then calls this to actually move the tokens
IERC20(usdc).transferFrom(user, dex, amount);

The allowance mapping tracks what each spender is permitted to take:

mapping(address => mapping(address => uint256)) public allowance;
// allowance[owner][spender] = amount

The problem: once approved, the spender can call transferFrom at any time in the future, up to the approved amount.


Why Infinite Approvals Became Standard Practice

The two-transaction UX of "approve exact amount → spend" frustrated users. Every time you wanted to swap 100 USDC, you had to:
1. Submit approve(dex, 100e6) — wait for confirmation, pay gas
2. Submit swap(100e6) — wait for confirmation, pay gas

To avoid this, DeFi UX teams switched to requesting approve(dex, type(uint256).max) on first use. After that, the DEX can spend any amount forever — no re-approval needed.

This trades user experience for a permanent open door.


The Attack Surface: Three Ways Infinite Approvals Get Exploited

1. Protocol Contract Gets Exploited

The most common scenario: you approve a legitimate DeFi protocol, then that protocol gets hacked.

User approves ProtocolV1 for MAX_UINT256 USDC
↓
6 months later: ProtocolV1 exploit — attacker drains contract
↓  
Attacker calls: ProtocolV1.exploit() → IERC20(usdc).transferFrom(user, attacker, MAX_UINT256)
↓
User's entire USDC balance is drained (even assets not in the protocol)

The key point: the user's tokens weren't in the protocol — they were in the user's wallet. But the standing infinite approval let the attacker drain them through the compromised protocol.

Real examples:
- Badger DAO exploit (2021, $120M): front-end compromise injected malicious approvals that allowed draining approved tokens
- Multiple DEX aggregator exploits: logic bugs in routing contracts that allowed calling transferFrom with attacker-controlled parameters

2. Malicious Contract Disguised as Legitimate

Phishing sites deploy contracts that look like real DeFi protocols. Users are tricked into calling approve(malicious_contract, MAX_UINT256). The attacker waits and drains later.

3. Deprecated/Upgraded Contract Still Has Approvals

Protocols upgrade their contracts but users' approvals for the old address remain. If the old contract has any vulnerability (even a minor one that wasn't worth patching before deprecation), it's now permanently dangerous.


Contract-Level Patterns for Safer Approvals

While users bear responsibility for managing their approvals, contract developers can significantly reduce the attack surface:

Pattern 1: Request Exact Amounts, Not MAX_UINT256

// BAD: requests unlimited approval from user
function depositForUser(address token, uint256 amount) external {
    // Implies the UI will request approve(address(this), MAX_UINT256)
    IERC20(token).transferFrom(msg.sender, address(this), amount);
}

// BETTER: use permit() for exact-amount, single-use approval
function depositWithPermit(
    address token,
    uint256 amount,
    uint256 deadline,
    uint8 v, bytes32 r, bytes32 s
) external {
    IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s);
    IERC20(token).transferFrom(msg.sender, address(this), amount);
}

Using permit() (EIP-2612) means the approval is for exactly the amount needed and expires at deadline. No standing approval remains after the transaction.

Pattern 2: Decrease Allowance After Use

For protocols that must use traditional approvals, consume the entire approved amount or explicitly reduce it:

// After using tokens, reduce approval to zero
function processAndClear(address token, address user, uint256 amount) internal {
    IERC20(token).transferFrom(user, address(this), amount);
    // If there's remaining allowance and the protocol is done, clear it
    // (Note: this requires the user to have approved the contract calling this)
}

Pattern 3: Validate transferFrom Parameters

A class of exploits uses malicious inputs to re-route transferFrom against user approvals. Always validate that from is the intended source:

// VULNERABLE: attacker can set `from` to any approved user
contract VulnerableRouter {
    function swap(
        address token,
        address from,   // ← attacker controls this
        uint256 amount
    ) external {
        IERC20(token).transferFrom(from, address(this), amount);
        // ... swap logic
    }
}

// SAFE: always use msg.sender as the source
contract SafeRouter {
    function swap(address token, uint256 amount) external {
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        // ... swap logic
    }
}

Pattern 4: Use Callback-Based Approval (ERC-777 / Flash Callbacks)

ERC-777 tokens use a callback model where the token notifies the recipient, removing the need for pre-approval entirely. Flash loan callbacks follow a similar pattern: the protocol calls you, you act, and the result is checked atomically.

Pattern 5: Approve-and-Call in One Transaction

For protocols that own the token contract (e.g., governance tokens), implement approveAndCall:

// Approve + immediate consumption in one transaction
function approveAndCall(
    address spender,
    uint256 amount,
    bytes calldata data
) external returns (bool) {
    approve(spender, amount);
    ISpenderCallback(spender).tokensReceived(msg.sender, amount, data);
    // After callback, spender should have consumed the full amount
    require(allowance[msg.sender][spender] == 0, "Allowance not fully consumed");
    return true;
}

What Auditors Check for Approval Patterns

When auditing a protocol, look for:

High risk:
- transferFrom(from, ...) where from is a function parameter (caller-controlled) — allows draining any user with existing approval
- Logic that calls transferFrom conditionally (approval exists but only used in some branches) — creates dormant attack surface
- Contracts that accept and process MAX_UINT256 as a valid amount without special handling

Medium risk:
- Protocol holds user approvals across multiple transactions without a mechanism to revoke them
- Upgrade pattern that deploys a new contract but doesn't invalidate old approvals

Best practices present:
- Uses permit() instead of persistent approvals where possible
- Validates from == msg.sender in all transferFrom calls
- Documents approval requirements clearly so users know what they're approving


Detection: What Automated Scanners Find

Issue Slither Semgrep AI
Unchecked transferFrom return value
transferFrom(from, ...) with controllable from ⚠️ ⚠️
Persistent approval with no revocation mechanism
Missing permit() usage in approval-heavy flows
Contracts that call approve(MAX_UINT256) internally ⚠️ ⚠️

The pattern of "controllable from in transferFrom" is the most dangerous and also the hardest for static analysis to flag — it requires understanding the data flow from external caller to the transferFrom call. AI-based analysis can reason about this flow and flag it even across multiple function calls.


The Broader Picture: Approval Management

From a user perspective, the best mitigations are:
- Use revoke.cash or similar to audit and revoke unnecessary approvals
- Prefer protocols that use permit() for one-time approvals
- Never approve contracts you don't recognize

From a developer perspective: design your protocol so users never need to grant approvals larger than what your specific operation requires. Permit-based flows, flash callbacks, and approveAndCall patterns all achieve this.

Scan your contracts for approval-related vulnerabilities with ContractScan — the AI engine specifically flags controllable-from patterns and approves without revocation paths that static analysis typically misses.


Related: EIP-2612 permit() and signature replay attacks — the gasless approval alternative introduces its own security considerations.

Scan your contract now
Slither + AI analysis — Unlimited quick scans. No signup required.
Try Free Scan →