Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: low
Valid

Missing infinite allowance optimization in _spendAllowance

Description

  • In widely used ERC‑20 implementations (e.g., OpenZeppelin), the _spendAllowance helper does not decrease allowance when it is set to type(uint256).max. This enables the “infinite approval” pattern: users approve MAX_UINT once and keep spending without needing to re‑approve, which many protocols (e.g., DEXes like Uniswap) rely on.

  • In this codebase, _spendAllowance always decreases the allowance, even when it equals type(uint256).max. This breaks the infinite approval pattern: the allowance is reduced after each transferFrom, eventually hitting zero and requiring users to re‑approve, thereby degrading UX and potentially breaking integrations expecting infinite approvals to remain infinite.

// src/helpers/ERC20Internals.sol (excerpt)
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
assembly ("memory-safe") {
// ... compute allowanceSlot ...
let currentAllowance := sload(allowanceSlot)
if lt(currentAllowance, value) {
// revert with ERC20InsufficientAllowance
}
// @> BUG: Always decreases allowance, even if currentAllowance == type(uint256).max
sstore(allowanceSlot, sub(currentAllowance, value))
}
}

Risk

Likelihood: Medium

  • Users commonly set infinite approvals on DEXes and DeFi protocols to avoid repeated approvals.

  • Any protocol that assumes “MAX_UINT means do not decrease” will hit unexpected reverts or forced re‑approvals during normal operation.

Impact: Medium

  • Integration breakage / UX regression: Workflows expecting a permanent approval will stop working after several transferFrom calls as allowance drains.

  • Re-approval friction and costs: Users must re‑approve, incurring additional on-chain transactions, gas costs, and potential confusion, which can reduce protocol adoption or cause support load.

Proof of Concept

  • Create poc1.t.sol under test directory and copy the code below.

  • Run forge test --mp poc1 -vvvv.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Token} from "../test/Token.sol";
import {IERC20Errors} from "../src/helpers/IERC20Errors.sol";
contract InfiniteAllowanceTest is Test {
Token internal token;
address owner = address(0xA11CE);
address spender = address(0xB0B);
address to = address(0xCAFE);
function setUp() public {
token = new Token();
token.mint(owner, 1_000 ether);
}
/// @notice Demonstrates that allowance set to MAX_UINT is incorrectly decreased by _spendAllowance.
function test_infiniteApproval_isIncorrectlyDecreased() public {
// Owner approves infinite allowance to spender
vm.prank(owner);
bool ok = token.approve(spender, type(uint256).max);
assertTrue(ok);
// Check initial infinite approval
assertEq(token.allowance(owner, spender), type(uint256).max, "expect infinite approval");
// Spender uses transferFrom for a small value
vm.prank(spender);
token.transferFrom(owner, to, 5 ether);
// EXPECTED (OpenZeppelin pattern): allowance should remain MAX_UINT.
// ACTUAL (this implementation): allowance decreased to MAX_UINT - 5 ether
uint256 afterAllowance = token.allowance(owner, spender);
assertEq(
afterAllowance,
type(uint256).max - 5 ether,
"BUG: infinite approval was decreased; should remain MAX_UINT"
);
// A second spend further reduces the allowance
vm.prank(spender);
token.transferFrom(owner, to, 10 ether);
assertEq(
token.allowance(owner, spender),
type(uint256).max - 15 ether,
"BUG: infinite approval keeps decreasing; expected to remain unchanged"
);
}
}

Output:

[⠊] Compiling...
[⠘] Compiling 1 files with Solc 0.8.30
[⠃] Solc 0.8.30 finished in 679.68ms
Compiler run successful!
Ran 1 test for test/poc1.t.sol:InfiniteAllowanceTest
[PASS] test_infiniteApproval_isIncorrectlyDecreased() (gas: 90980)
Traces:
[90980] InfiniteAllowanceTest::test_infiniteApproval_isIncorrectlyDecreased()
├─ [0] VM::prank(0x00000000000000000000000000000000000A11cE)
│ └─ ← [Return]
├─ [24985] Token::approve(0x0000000000000000000000000000000000000B0b, 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ ├─ emit Approval(owner: 0x00000000000000000000000000000000000A11cE, spender: 0x0000000000000000000000000000000000000B0b, value: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ └─ ← [Return] true
├─ [0] VM::assertTrue(true) [staticcall]
│ └─ ← [Return]
├─ [1312] Token::allowance(0x00000000000000000000000000000000000A11cE, 0x0000000000000000000000000000000000000B0b) [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
├─ [0] VM::assertEq(115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], "expect infinite approval") [staticcall]
│ └─ ← [Return]
├─ [0] VM::prank(0x0000000000000000000000000000000000000B0b)
│ └─ ← [Return]
├─ [30742] Token::transferFrom(0x00000000000000000000000000000000000A11cE, 0x000000000000000000000000000000000000cafE, 5000000000000000000 [5e18])
│ ├─ emit Transfer(from: 0x00000000000000000000000000000000000A11cE, to: 0x000000000000000000000000000000000000cafE, value: 5000000000000000000 [5e18])
│ └─ ← [Return] true
├─ [1312] Token::allowance(0x00000000000000000000000000000000000A11cE, 0x0000000000000000000000000000000000000B0b) [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039452584007913129639935 [1.157e77]
├─ [0] VM::assertEq(115792089237316195423570985008687907853269984665640564039452584007913129639935 [1.157e77], 115792089237316195423570985008687907853269984665640564039452584007913129639935 [1.157e77], "BUG: infinite approval was decreased; should remain MAX_UINT") [staticcall]
│ └─ ← [Return]
├─ [0] VM::prank(0x0000000000000000000000000000000000000B0b)
│ └─ ← [Return]
├─ [4042] Token::transferFrom(0x00000000000000000000000000000000000A11cE, 0x000000000000000000000000000000000000cafE, 10000000000000000000 [1e19])
│ ├─ emit Transfer(from: 0x00000000000000000000000000000000000A11cE, to: 0x000000000000000000000000000000000000cafE, value: 10000000000000000000 [1e19])
│ └─ ← [Return] true
├─ [1312] Token::allowance(0x00000000000000000000000000000000000A11cE, 0x0000000000000000000000000000000000000B0b) [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039442584007913129639935 [1.157e77]
├─ [0] VM::assertEq(115792089237316195423570985008687907853269984665640564039442584007913129639935 [1.157e77], 115792089237316195423570985008687907853269984665640564039442584007913129639935 [1.157e77], "BUG: infinite approval keeps decreasing; expected to remain unchanged") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.21ms (321.90µs CPU time)
Ran 1 test suite in 15.32ms (1.21ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Skip decreasing the allowance when it is type(uint256).max.

  • In Yul, type(uint256).max can be represented as not(0) (i.e., all bits set to 1). Only subtract when not infinite:

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
assembly ("memory-safe") {
// ... compute allowanceSlot ...
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))
+ // Infinite allowance optimization: if currentAllowance == type(uint256).max, do not decrease.
+ // In Yul, type(uint256).max is represented by not(0).
+ if iszero(eq(currentAllowance, not(0))) {
+ sstore(allowanceSlot, sub(currentAllowance, value))
+ }
}
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

accounting

accounting related issue in token-0x

Appeal created

s3mvl4d Submitter
18 days ago
gaurangbrdv Lead Judge
17 days ago
s3mvl4d Submitter
17 days ago
gaurangbrdv Lead Judge
17 days ago
gaurangbrdv Lead Judge 14 days ago
Submission Judgement Published
Validated
Assigned finding tags:

accounting

accounting related issue in token-0x

Support

FAQs

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

Give us feedback!