Token-0x

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

`_spendAllowance` Does Not Handle Infinite Allowance Correctly

Author Revealed upon completion

Root + Impact

The _spendAllowance function always decrements allowance, even when set to type(uint256).max. This breaks the expected infinite allowance behavior used by many DeFi protocols and causes unnecessary gas consumption.

Description

  • By convention (established by OpenZeppelin and widely adopted), when a user approves type(uint256).max tokens, this represents "infinite" or "unlimited" approval.

  • The allowance should NOT be decremented on transfers. This implementation always decrements the allowance.

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)
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)) // Always decrements, even for max allowance
}
}

Risk

Likelihood:

  • Many DeFi protocols use infinite approval pattern

  • Users commonly approve type(uint256).max to avoid repeated approve transactions

  • This affects every transfer using such approvals

Impact:

  • Unexpected behavior for users expecting infinite approval

  • Additional gas costs from unnecessary storage writes

  • Breaking DeFi integrations that depend on persistent max allowance

  • Users need to re-approve after their "infinite" allowance is depleted

Proof of Concept

function test_infiniteAllowance() public {
address owner = makeAddr("owner");
address spender = makeAddr("spender");
address recipient = makeAddr("recipient");
token.mint(owner, 1000e18);
// Owner sets "infinite" allowance
vm.prank(owner);
token.approve(spender, type(uint256).max);
// Spender transfers
vm.prank(spender);
token.transferFrom(owner, recipient, 100e18);
// Allowance should still be type(uint256).max, but it's decremented
// Expected: type(uint256).max
// Actual: type(uint256).max - 100e18
assertEq(token.allowance(owner, spender), type(uint256).max - 100e18);
}

Recommended Mitigation

Skip the allowance deduction when the current allowance equals type(uint256).max.

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)
+ // Skip deduction for infinite allowance
+ if eq(currentAllowance, sub(0, 1)) { // sub(0,1) = type(uint256).max
+ // Don't modify allowance, just return
+ 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!