Token-0x

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

Infinite Approval Bypass in `_spendAllowance`

Author Revealed upon completion

Description:
The _spendAllowance() function does not implement the standard ERC20 behavior for infinite approvals (type(uint256).max). In OpenZeppelin's implementation and per common ERC20 conventions, when an allowance is set to the maximum uint256 value, it should not be decremented to avoid gas costs and maintain infinite approval. The current implementation always decrements the allowance, breaking this expected behavior.

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
assembly ("memory-safe") {
// ... gets currentAllowance ...
if lt(currentAllowance, value) {
// Reverts if insufficient
// ...
}
// ALWAYS decrements allowance, even if it's max uint256
sstore(allowanceSlot, sub(currentAllowance, value))
}
}

Impact:

  • Gas Inefficiency: Users who set infinite approvals must update them after each transfer, costing additional gas.

  • DApp Integration Issues: Many DApps rely on infinite approval mechanics for better UX. This implementation breaks that pattern.

  • Non-Standard Behavior: Deviates from widely accepted ERC20 conventions, potentially causing confusion and integration problems.

Proof of Concept:

Add this test to Token.t.sol

function test_InfiniteApprovalShouldNotDecrement() public {
address owner = makeAddr("owner");
address spender = makeAddr("spender");
address recipient = makeAddr("recipient");
// Mint tokens to owner
token.mint(owner, 1000e18);
// Owner approves spender with infinite allowance
vm.prank(owner);
token.approve(spender, type(uint256).max);
uint256 allowanceBefore = token.allowance(owner, spender);
assertEq(allowanceBefore, type(uint256).max);
// Spender transfers tokens
vm.prank(spender);
token.transferFrom(owner, recipient, 100e18);
// Allowance should still be max (infinite)
uint256 allowanceAfter = token.allowance(owner, spender);
// This will FAIL - allowance was decremented
assertEq(allowanceAfter, type(uint256).max);
// Actually: allowanceAfter == type(uint256).max - 100e18
}

Mitigation:
Modify _spendAllowance() to skip decrementing when allowance is infinite:

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
assembly ("memory-safe") {
let ptr := mload(0x40)
let baseSlot := _allowances.slot
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let initialHash := keccak256(ptr, 0x40)
mstore(ptr, spender)
mstore(add(ptr, 0x20), initialHash)
let allowanceSlot := keccak256(ptr, 0x40)
let currentAllowance := sload(allowanceSlot)
+ // Check if allowance is infinite (max uint256)
+ let maxValue := sub(0, 1) // type(uint256).max
+ // Skip spending if allowance is infinite
+ if eq(currentAllowance, maxValue) {
+ leave
+ }
if lt(currentAllowance, value) {
mstore(0x00, shl(224, 0xfb8f41b2))
mstore(add(0x00, 4), spender)
mstore(add(0x00, 0x24), currentAllowance)
mstore(add(0x00, 0x44), value)
revert(0, 0x64)
}
sstore(allowanceSlot, sub(currentAllowance, value))
}
}

Support

FAQs

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

Give us feedback!