Token-0x

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

_spendAllowance ignores max allowance optimization

_spendAllowance ignores max allowance optimization

Description

  • Normal behavior: _spendAllowance decrements a spender’s allowance during transferFrom.

Problem: The current implementation always subtracts value from the allowance, even if the allowance is type(uint256).max (infinite allowance). This wastes gas unnecessarily.

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 updates storage, even if allowance is max
}
}

Risk

Likelihood:

  • This occurs whenever a spender uses transferFrom after being granted an infinite allowance (type(uint256).max).

This occurs for most real-world ERC20s because many clients (e.g., DEXes, routers, aggregators) always request infinite approvals.

Impact:

  • Each transferFrom results in an unnecessary sstore, wasting ~5k gas per swap.

  • High-frequency users (DEX traders, routers) incur large cumulative gas waste.

Proof of Concept

contract TestMaxAllowance is ERC20 {
function test() external {
_approve(msg.sender, address(this), type(uint256).max);
_spendAllowance(msg.sender, address(this), 1); // unnecessary gas-heavy sstore
}
}

Recommended Mitigation

Only update allowance if current allowance is not 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)
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))
+ if iszero(eq(currentAllowance, not(0))) {
+ sstore(allowanceSlot, sub(currentAllowance, value))
+ }
}
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 18 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!