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 about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

accounting

accounting related issue in token-0x

Appeal created

s3mvl4d Submitter
about 2 months ago
gaurangbrdv Lead Judge
about 2 months ago
s3mvl4d Submitter
about 2 months ago
gaurangbrdv Lead Judge
about 2 months ago
gaurangbrdv Lead Judge about 2 months 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!