Token-0x

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

Memory Pointer Not Updated in _approve() Function

Author Revealed upon completion

Memory Pointer Not Updated After Usage in Approve Function Causing Potential Memory Corruption

Description

  • The _approve function uses inline assembly marked as memory-safe but fails to update the free memory pointer at 0x40 after allocating and using memory. This violates Solidity's memory safety model.

function _approve(address owner, address spender, uint256 value) internal virtual returns (bool success) {
assembly ("memory-safe") {
if iszero(owner) {
mstore(0x00, shl(224, 0xe602df05))
mstore(add(0x00, 4), owner)
revert(0x00, 0x24)
}
if iszero(spender) {
mstore(0x00, shl(224, 0x94280d62))
mstore(add(0x00, 4), spender)
revert(0x00, 0x24)
}
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)
sstore(allowanceSlot, value)
@> // Never updated mstore(0x40, add(ptr, 0x40))
@> // Free memory pointer still points to 0x80!
success := 1
mstore(0x00, value)
log3(0x00, 0x20, 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, owner, spender)
}
}

Risk

The function reads the free memory pointer (stored at 0x40) which tells it where safe memory starts, typically 0x80

  • It uses 64 bytes of memory (from 0x80 to 0xC0) for temporary calculations

  • Bug: It never updates the free memory pointer to 0xC0 after using the space

The next function that needs memory also reads the pointer at 0x40

  • It gets the same answer: 0x80 (not updated to 0xC0)

  • It writes to 0x80, overwriting data from the previous operation

  • This memory collision can corrupt calculations and cause unexpected transaction failures

Issue Arises when:

  • Multiple approve calls in one transaction

  • Approve followed by transfer in a multicall


IMPACT

  • Any contract that inherits this code and uses memory-intensive operations after `_approve` calls is at risk of memory corruption.

  • Memory Collision in Complex Transactions

  • Incorrect Storage Calculations (Edge Case)

Proof of Concept

  • This vulnerability is difficult to reproduce in Foundry , I have added the memory layout of before nad after approve.

**Before `_approve`:**
```
Memory Layout:
0x400x80 (free memory pointer)
0x600x00 (zero slot)
0x80 → [FREE]
```
**After `_approve` (vulnerable version):**
```
Memory Layout:
0x400x80 Still points to 0x80 (should be 0xC0)
0x80 → spender address (last value written)
0xA0 → initialHash (last value written)

Recommended Mitigation

Add mstore(0x40, add(ptr, 0x40)) before success := 1

This ensures the free memory pointer correctly points to 0xC0 (64 bytes past the initial pointer), marking memory 0x80-0xC0 as used.

function _approve(address owner, address spender, uint256 value) internal virtual returns (bool success) {
assembly ("memory-safe") {
if iszero(owner) {
mstore(0x00, shl(224, 0xe602df05))
mstore(add(0x00, 4), owner)
revert(0x00, 0x24)
}
if iszero(spender) {
mstore(0x00, shl(224, 0x94280d62))
mstore(add(0x00, 4), spender)
revert(0x00, 0x24)
}
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)
sstore(allowanceSlot, value)
// update free memory pointer
+ mstore(0x40, add(ptr, 0x40))
success := 1
mstore(0x00, value)
log3(0x00, 0x20, 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, owner, spender)
}
}

Support

FAQs

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

Give us feedback!