Token-0x

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

Inconsistent return mechanisms in totalSupply_, _balanceOf vs. _allowance

Author Revealed upon completion

Description

  • Internal view functions should follow a consistent return mechanism—either using Solidity’s implicit return variables or using a unified assembly pattern that writes to the free memory pointer and returns from there. Consistency improves readability, maintainability, and reduces subtle ABI/memory handling mistakes.

  • In this codebase, different internal view functions use different return strategies:

    • totalSupply_() writes into scratch space (0x00) and returns from it.

    • _balanceOf(address) allocates ptr := mload(0x40), writes the value there, then returns (ptr, 0x20), but also performs an unnecessary extra mstore(add(ptr, 0x20), 0).

    • _allowance(address,address) relies on Solidity’s implicit return variable (remaining) rather than a manual return(ptr, 0x20) block.

// src/helpers/ERC20Internals.sol
// totalSupply_(): returns from scratch space (0x00)
function totalSupply_() internal view returns (uint256) {
assembly {
let slot := _totalSupply.slot
let supply := sload(slot)
// @> Writes to scratch space and returns from there
mstore(0x00, supply)
return(0x00, 0x20)
}
}
// _balanceOf(): returns from free memory ptr but also zeros the next word unnecessarily
function _balanceOf(address owner) internal view returns (uint256) {
assembly {
if iszero(owner) {
revert(0, 0) // non-standard; see separate bug report
}
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)
// @> Unnecessary: zeroes the next word though we return only 32 bytes
mstore(add(ptr, 0x20), 0)
return(ptr, 0x20)
}
}
// _allowance(): uses implicit return variable 'remaining'
function _allowance(address owner, address spender) internal view returns (uint256 remaining) {
assembly {
if or(iszero(owner), iszero(spender)) {
revert(0, 0) // non-standard; see separate bug report
}
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)
remaining := sload(allowanceSlot) // @> Implicit return variable
}
}

Risk

Likelihood: Low

  • This module is assembly-heavy and likely to be modified in the future. Mixed patterns make refactors and audits harder, and new contributors can inadvertently break memory hygiene or ABI assumptions.

  • Integrations frequently call these functions; any subtle change in return behavior can surface as silent incompatibilities or hard-to-debug issues.

Impact: Low

  • Maintainability & correctness risk: Inconsistent patterns can lead to mistakes when adding features (e.g., event additions, extra checks), potentially causing silent bugs (wrong offsets, clobbered scratch space).

  • Audit complexity / time: Auditors and maintainers must reason about three patterns instead of one, increasing the chance of missing edge cases.

Proof of Concept

// No runtime revert required to demonstrate.
// The inconsistency is visible in source:
// - totalSupply_ returns via scratch space return(0x00, 0x20)
// - _balanceOf returns via free memory pointer but zeroes an extra word
// - _allowance returns via implicit ABI (remaining)

Recommended Mitigation

  • Unify all internal view functions to one consistent return style - prefer using the free memory pointer and an explicit return(ptr, 0x20) (option 1).

  • Or use Solidity implicit returns everywhere (option 2).

  • Also avoid scratch-space writes and remove unnecessary memory operations.

Option 1:

function totalSupply_() internal view returns (uint256) {
assembly {
let slot := _totalSupply.slot
let supply := sload(slot)
- mstore(0x00, supply)
- return(0x00, 0x20)
+ let ptr := mload(0x40)
+ mstore(ptr, supply)
+ return(ptr, 0x20)
}
}
function _balanceOf(address owner) internal view returns (uint256) {
assembly {
if iszero(owner) {
- revert(0, 0)
+ // (Recommended elsewhere: return 0 instead of revert for zero address)
+ mstore(0x00, 0)
+ return(0x00, 0x20)
}
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) // remove unnecessary zeroing
return(ptr, 0x20)
}
}
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)
- 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)
- }
+ assembly {
+ if or(iszero(owner), iszero(spender)) {
+ // (Recommended elsewhere: return 0 instead of revert)
+ mstore(0x00, 0)
+ return(0x00, 0x20)
+ }
+ 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)
+ let rem := sload(allowanceSlot)
+ mstore(ptr, rem)
+ return(ptr, 0x20)
+ }
}

Option 2:

function totalSupply_() internal view returns (uint256) {
- assembly { /* sload + mstore + return */ }
+ return _totalSupply;
}
function _balanceOf(address owner) internal view returns (uint256) {
- assembly { /* mapping slot derivation + sload + return */ }
+ if (owner == address(0)) return 0; // standard behavior
+ return _balances[owner];
}
function _allowance(address owner, address spender) internal view returns (uint256) {
- assembly { /* nested mapping slot + sload + implicit return */ }
+ if (owner == address(0) || spender == address(0)) return 0; // standard behavior
+ return _allowances[owner][spender];
}

Support

FAQs

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

Give us feedback!