Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: low
Likelihood: high

No Infinite Approval Handling

Author Revealed upon completion

Root + Impact

Description

  • The _spendAllowance() function always decrements the allowance, even when set to type(uint256).max (the standard "infinite approval" pattern).

  • Standard implementations skip the decrement for max approval to save gas and provide expected UX.

function _spendAllowance(address owner, address spender, uint256 value) internal {
assembly ("memory-safe") {
let currentAllowance := sload(allowanceSlot)
if lt(currentAllowance, value) {
// revert
}
// @> ALWAYS decrements - even if currentAllowance == type(uint256).max
sstore(allowanceSlot, sub(currentAllowance, value))
}
}

Risk

Likelihood:

  • Affects every user who sets infinite approval (common DEX pattern)

  • Standard practice for Uniswap, 1inch, and most DeFi protocols

Impact:

  • Unexpected allowance changes when users expect "unlimited"

  • Extra ~5000 gas per transferFrom due to unnecessary SSTORE

  • Breaks user expectations from standard ERC20 behavior

Proof of Concept

This test demonstrates that setting type(uint256).max as an allowance (the standard "infinite approval" pattern used by DEXes) does not behave as expected. After a single transfer, the allowance is decremented rather than remaining at max. This wastes gas and confuses users who expect infinite to mean infinite.

Place this test in test/InfiniteApprovalPOC.t.sol and run with forge test --match-test test_MaxApprovalIsDecremented -vv:

function test_MaxApprovalIsDecremented() public {
uint256 maxUint = type(uint256).max;
vm.prank(alice);
token.approve(spender, maxUint);
assertEq(token.allowance(alice, spender), maxUint);
// Transfer 100 tokens
vm.prank(spender);
token.transferFrom(alice, spender, 100 ether);
// Allowance decreased instead of staying at max
assertEq(token.allowance(alice, spender), maxUint - 100 ether);
}

Recommended Mitigation

Check if the current allowance equals type(uint256).max before performing the decrement. In Yul, not(0) equals type(uint256).max. If infinite approval is detected, skip both the sufficiency check (it will always pass) and the storage write (unnecessary gas cost).

function _spendAllowance(address owner, address spender, uint256 value) internal {
assembly ("memory-safe") {
let currentAllowance := sload(allowanceSlot)
+ // Skip update for infinite approval (type(uint256).max)
+ if iszero(eq(currentAllowance, not(0))) {
if lt(currentAllowance, value) {
// revert
}
sstore(allowanceSlot, sub(currentAllowance, value))
+ }
}
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!