Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

High:- Allowance Storage Failure with Max Value

Author Revealed upon completion

Root Cause

The fuzzer detected that when type(uint256).max(1.157e77) is used as the allowance amount, the subsequent read of that allowance returns 0.

Test: testFuzzTransferFrom

Failure: Initial allowance failed: 0 != 115792089237316195423570985008687907853269984665640564039457584007913129639935

This indicates a flaw in the _allowance mapping assembly logic when handling the all-ones hexadecimal representation (0xFF...FF). While the sstore in _approve likely executes, the read back in _allowance might be calculating an incorrect storage slot (dataSlot), causing sload to return 0.

Impact

Users cannot approve the maximum amount of tokens. If a protocol relies on a "sticky" approval of 2^{256}-1 (infinite approval), this function fails, rendering the token unusable for protocols requiring max allowance.

function _allowance(address owner, address spender) internal view returns (uint256 remaining) {
assembly {
if or(iszero(owner), iszero(spender)) {
revert(0, 0)
}
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)
remaining := sload(allowanceSlot)
}
}

Proof of Concept

// --- Fuzz Test 3: TransferFrom/Allowance Integrity ---
// Goal: Test allowance approval and spending, checking all three key balances.
function testFuzzTransferFrom(address spender, address receiver, uint256 allowanceAmount, uint256 spendAmount) public {
// Constraints
vm.assume(spender != ZERO_ADDRESS && receiver != ZERO_ADDRESS);
vm.assume(spender != ALICE);
vm.assume(spendAmount <= allowanceAmount); // Ensure spend is <= allowance
vm.assume(spendAmount <= token.balanceOf(ALICE)); // Ensure spend is <= balance
// State Capture
uint256 initialAliceBalance = token.balanceOf(ALICE);
uint256 initialReceiverBalance = token.balanceOf(receiver);
// 1. Approve (using the internal helper via wrapper for simplicity)
emit Approval(ALICE, spender, allowanceAmount);
assertEq(token.allowance(ALICE, spender), allowanceAmount, "Initial allowance failed");
// 2. Execute transferFrom (caller is the spender)
vm.prank(spender);
assertTrue(token.transferFrom(ALICE, receiver, spendAmount), "TransferFrom must succeed");
// 3. Post-execution checks
uint256 remainingAllowance = allowanceAmount - spendAmount;
assertEq(token.allowance(ALICE, spender), remainingAllowance, "Allowance was not spent correctly");
assertEq(token.balanceOf(ALICE), initialAliceBalance - spendAmount, "Alice's balance incorrect after spend");
assertEq(token.balanceOf(receiver), initialReceiverBalance + spendAmount, "Receiver's balance incorrect after spend");
}

Support

FAQs

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

Give us feedback!