← Back to Blog

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

2026-07-21 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.

Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →