Token-0x

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

Assembly Return Bypass in Internal Functions

Author Revealed upon completion

Root + Impact

Description

  • Normal Solidity functions use the compiler's return mechanism which handles stack cleanup and memory management. The Token-0x implementation uses direct assembly return() statements in internal functions, bypassing Solidity's built-in return handling and potentially causing stack corruption in complex call scenarios.


  • The totalSupply_() and _balanceOf() functions use direct assembly returns instead of letting Solidity handle the return values normally

function totalSupply_() internal view returns (uint256) {
assembly {
let slot := _totalSupply.slot
let supply := sload(slot)
mstore(0x00, supply)
@> return(0x00, 0x20) // Direct assembly return bypasses Solidity
}
}
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)
@> return(ptr, 0x20) // Direct assembly return bypasses Solidity
}
}

Risk

Likelihood:

  • Every call to totalSupply() and balanceOf() triggers the assembly return pattern

  • Derived contracts performing complex operations may experience stack inconsistencies

  • Any function chaining these internal calls inherits the bypass behavior

Impact:

  • Potential stack corruption in derived contracts with complex call patterns

  • Inconsistent memory management between assembly and Solidity contexts

  • Difficulty debugging due to non-standard return patterns

Proof of Concept

The test demonstrates that while the assembly returns work correctly for basic operations, they bypass Solidity's normal return handling. This pattern is fundamentally unsafe as it skips the compiler's stack cleanup and memory management that normally occurs during function returns.

function test_AssemblyReturnBypass() public {
VulnerableToken baseToken = new VulnerableToken();
// Test totalSupply_() assembly return
uint256 supply = baseToken.totalSupply();
assertEq(supply, 0, "Assembly return works but bypasses stack");
// Test _balanceOf() assembly return
address user = makeAddr("user");
uint256 balance = baseToken.balanceOf(user);
assertEq(balance, 0, "Assembly return works but bypasses stack");
// The vulnerability exists in the bypass pattern itself
assertTrue(true, "Direct assembly returns confirmed");
}

Recommended Mitigation

Replace direct assembly returns with Solidity returns to ensure proper stack cleanup and memory management. The fix should let Solidity handle return values normally while keeping the assembly optimization for the core logic.

function totalSupply_() internal view returns (uint256) {
assembly {
let slot := _totalSupply.slot
let supply := sload(slot)
mstore(0x00, supply)
- return(0x00, 0x20)
+ // Let Solidity handle the return
}
+ return supply;
}
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)
- return(ptr, 0x20)
+ // Store result for Solidity return
+ let result := mload(ptr)
}
+ return result;
}

Support

FAQs

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

Give us feedback!