Token-0x

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

Memory Pointer Non-Update in Assembly Functions

Root + Impact

Description

  • Normal memory management in assembly requires loading the free memory pointer, allocating memory, then updating the free memory pointer to prevent overwrites. The Token-0x implementation loads the free memory pointer in every assembly function but never updates it after allocation, creating a pattern where subsequent operations can overwrite previously allocated memory regions.


  • All internal functions follow the same vulnerable pattern of loading ptr := mload(0x40 without subsequent mstore(0x40, newPtr) updates

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
if iszero(owner) {
revert(0, 0)
}
let baseSlot := _balances.slot
@> let ptr := mload(0x40) // Loads free memory pointer
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let dataSlot := keccak256(ptr, 0x40)
let amount := sload(dataSlot)
mstore(ptr, amount)
mstore(add(ptr, 0x20), 0)
return(ptr, 0x20)
// @> MISSING: mstore(0x40, newPtr) - never updates free memory pointer
}
}

Risk

Likelihood:

  • Every internal assembly function exhibits this pattern across the entire codebase

  • Derived contracts performing complex operations with multiple memory allocations will trigger this

  • Any function chaining internal calls creates the overwrite condition

Impact:

  • Memory corruption in derived contracts with complex operations

  • Silent data overwrites leading to incorrect calculations or state changes

  • Potential for arbitrary state manipulation through memory collision attacks

Proof of Concept

The test demonstrates that while read-only operations appear safe, the memory management pattern is fundamentally broken. In complex derived contracts, this could lead to memory overwrites where subsequent internal calls corrupt data from previous operations.

function test_MemoryPointerNotUpdated() public {
VulnerableToken baseToken = new VulnerableToken();
address user = makeAddr("user");
// First call loads ptr at current free memory pointer
uint256 balance1 = baseToken.balanceOf(user);
// Allocate memory in Solidity context
bytes memory data = new bytes(100);
// Second internal call should use updated ptr but uses stale one
uint256 balance2 = baseToken.balanceOf(user);
// Both calls return same value, proving memory corruption doesn't affect read-only
// But the pattern is vulnerable in write operations
assertEq(balance1, balance2);
}

Recommended Mitigation

Update the free memory pointer after each memory allocation in all assembly functions. This ensures subsequent operations use fresh memory regions and prevents overwrites.

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
if iszero(owner) {
revert(0, 0)
}
let baseSlot := _balances.slot
let ptr := mload(0x40)
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let dataSlot := keccak256(ptr, 0x40)
let amount := sload(dataSlot)
mstore(ptr, amount)
mstore(add(ptr, 0x20), 0)
+ // FIX: Update free memory pointer after allocation
+ let newPtr := add(ptr, 0x40)
+ mstore(0x40, newPtr)
+
return(ptr, 0x20)
}
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge
25 days ago
gaurangbrdv Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

memory curruption

corrupted memory while integrating with complex protocol

Support

FAQs

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

Give us feedback!