Token-0x

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

Infinite Allowance Not Handled

Author Revealed upon completion

Description

  • OpenZeppelin's ERC20 treats type(uint256).max allowance as "infinite" - it never decreases when spending. This is a widely adopted convention for gas-efficient unlimited approvals.

  • Token-0x always decrements the allowance, even when set to type(uint256).max. This causes behavioral differences that may break integrations expecting OpenZeppelin-style behavior.

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

Risk

Likelihood:

  • Many DeFi protocols use type(uint256).max approvals

  • Users commonly set unlimited approvals to avoid repeated approve transactions

Impact:

  • Protocols expecting infinite allowances will need to re-approve

  • Gas inefficiency for users who wanted unlimited approvals

  • Behavioral inconsistency with standard ERC20 implementations

Proof of Concept

function test_infinite_allowance() public {
token.mint(alice, 1000e18);
vm.prank(alice);
token.approve(bob, type(uint256).max);
vm.prank(bob);
token.transferFrom(alice, charlie, 100e18);
// Token-0x: allowance decreased
uint256 allowance = token.allowance(alice, bob);
assertLt(allowance, type(uint256).max);
// OpenZeppelin: allowance stays at max
// token2.allowance(alice, bob) == type(uint256).max
}

Recommended Mitigation

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 if infinite allowance (type(uint256).max)
+ if eq(currentAllowance, not(0)) {
+ 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!