Token-0x

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

Scratch space is clobbered in _approve despite the usage of 'memory-safe' annotation

Author Revealed upon completion

Description

  • When preparing event data in assembly, best practice is to write the event’s data payload into the free memory pointer region (mload(0x40)) and avoid touching the scratch space (0x00–0x3F). This keeps memory hygiene intact, prevents clobbering temporary workspace used by the compiler/runtime, and aligns with patterns already present in other functions (e.g., _allowance function computes with a ptr from 0x40).

  • In _approve, the code uses the memory-safe annotation but still writes to scratch space via mstore(0x00, value) right before emitting the Approval event with log3. This contradicts the intent of “memory-safe” and can introduce subtle fragility or maintenance hazards. The event payload should be written at ptr (the free memory pointer) instead.

// src/helpers/ERC20Internals.sol (excerpt)
function _approve(address owner, address spender, uint256 value) internal virtual returns (bool success) {
assembly ("memory-safe") {
// ... validations and slot calculations ...
sstore(allowanceSlot, value)
success := 1
// @> PROBLEM: Writes the event data to scratch space (0x00), despite "memory-safe"
mstore(0x00, value)
log3(
0x00,
0x20,
0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, // Approval topic
owner,
spender
)
}
}

Risk

Likelihood: High

  • _approve is executed frequently across wallets, DEXes, and protocols. The scratch-space write happens on every successful approval.

  • The contract uses inline assembly in multiple places; future refactors may introduce helpers that assume the scratch space isn’t clobbered right before/after event emissions.

Impact: Low

  • Memory hygiene violation / fragility: Clobbering 0x00 can interfere with temporary values expected by adjacent assembly or compiler-generated helpers, creating hard-to-debug behavior.

  • Inconsistency: Other functions in the codebase (e.g., _allowance) correctly use a ptr from mload(0x40); _approve should follow the same pattern to maintain consistency and safety.

Proof of Concept

// No runtime revert is necessary to demonstrate.
// The issue is a memory-hygiene/best-practice violation visible in source:
// mstore(0x00, value) // uses scratch space
// log3(0x00, 0x20, topic, owner, spender)

Recommended Mitigation

  • It is recommended to use the free memory pointer (ptr := mload(0x40)) for the event data buffer and emit the log from ptr.

function _approve(address owner, address spender, uint256 value) internal virtual returns (bool success) {
assembly ("memory-safe") {
// ... validations and slot calculations ...
sstore(allowanceSlot, value)
success := 1
- mstore(0x00, value)
- log3(0x00, 0x20,
- 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925,
- owner, spender)
+ // Use free memory pointer for event payload
+ let ptr := mload(0x40)
+ mstore(ptr, value)
+ log3(
+ ptr,
+ 0x20,
+ 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, // keccak256("Approval(address,address,uint256)")
+ owner,
+ spender
+ )
}
}

Support

FAQs

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

Give us feedback!