_spendAllowance ignores max allowance optimization
Description
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 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);
}
}
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))
+ }
}
}