Root + Impact
The _spendAllowance function always decrements allowance, even when set to type(uint256).max. This breaks the expected infinite allowance behavior used by many DeFi protocols and causes unnecessary gas consumption.
Description
By convention (established by OpenZeppelin and widely adopted), when a user approves type(uint256).max tokens, this represents "infinite" or "unlimited" approval.
The allowance should NOT be decremented on transfers. This implementation always decrements the allowance.
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))
}
}
Risk
Likelihood:
Many DeFi protocols use infinite approval pattern
Users commonly approve type(uint256).max to avoid repeated approve transactions
This affects every transfer using such approvals
Impact:
Unexpected behavior for users expecting infinite approval
Additional gas costs from unnecessary storage writes
Breaking DeFi integrations that depend on persistent max allowance
Users need to re-approve after their "infinite" allowance is depleted
Proof of Concept
function test_infiniteAllowance() public {
address owner = makeAddr("owner");
address spender = makeAddr("spender");
address recipient = makeAddr("recipient");
token.mint(owner, 1000e18);
vm.prank(owner);
token.approve(spender, type(uint256).max);
vm.prank(spender);
token.transferFrom(owner, recipient, 100e18);
assertEq(token.allowance(owner, spender), type(uint256).max - 100e18);
}
Recommended Mitigation
Skip the allowance deduction when the current allowance equals type(uint256).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)
+ // Skip deduction for infinite allowance
+ if eq(currentAllowance, sub(0, 1)) { // sub(0,1) = type(uint256).max
+ // Don't modify allowance, just return
+ 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))
}
}```