Token-0x

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

Memory Boundary Violation in Nested Allowance Calculation

Author Revealed upon completion

Root + Impact

Description

  • Normal assembly memory management should include boundary validation to ensure memory operations don't interfere with each other. The _allowance() function in Token-0x's base implementation uses memory regions without proper boundary checks, making it vulnerable to memory corruption from external operations.


  • The vulnerability occurs because assembly functions assume memory regions are clean but don't validate them before use, allowing derived contracts or external operations to corrupt memory used for critical calculations.

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) // Loads free memory pointer without validation
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) // Uses potentially corrupted memory
remaining := sload(allowanceSlot)
}
}

Risk

Likelihood:

  • Every internal assembly function that uses memory is susceptible to boundary violations

  • Derived contracts performing complex memory operations can corrupt shared memory regions

  • Any external assembly operation can interfere with Token-0x's memory usage

Impact:

  • Storage slot calculations can return incorrect values, leading to wrong allowance amounts

  • Critical operations like transferFrom() may fail or behave unexpectedly

  • Silent data corruption can occur without immediate detection, affecting contract state integrity

Proof of Concept

The test demonstrates memory boundary vulnerability by corrupting memory regions around the free memory pointer without corrupting the pointer itself. This shows that while the current implementation may not be immediately exploitable, the pattern creates risks for derived contracts.

function test_MemoryBoundaryViolation() public {
VulnerableToken baseToken = new VulnerableToken();
address owner = makeAddr("owner");
address spender = makeAddr("spender");
baseToken.mint(owner, 100e18);
// Approve spender first
vm.prank(owner);
baseToken.approve(spender, 50e18);
// Verify allowance works normally
uint256 allowance1 = baseToken.allowance(owner, spender);
assertEq(allowance1, 50e18);
// Corrupt memory AROUND the free memory pointer region
assembly {
mstore(0x80, 0xdeadbeefdeadbeef)
mstore(0xa0, 0xcafebabecafebabe)
}
// Query allowance again - should still work
uint256 allowance2 = baseToken.allowance(owner, spender);
assertEq(allowance2, 50e18, "Allowance should remain correct");
}

Recommended Mitigation

Implement proper memory boundary validation and use dedicated memory regions for critical operations to prevent corruption from external sources.

function _allowance(address owner, address spender) internal view returns (uint256 remaining) {
assembly {
if or(iszero(owner), iszero(spender)) {
revert(0, 0)
}
+ // FIX: Use dedicated memory region with boundary validation
+ let ptr := 0x80 // Fixed memory location for calculations
+ // Clear memory region to ensure clean state
+ mstore(ptr, 0)
+ mstore(add(ptr, 0x20), 0)
+ mstore(add(ptr, 0x40), 0)
+ mstore(add(ptr, 0x60), 0)
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)
}
}

Support

FAQs

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

Give us feedback!