Token-0x

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

Fragile Use of Inline Assembly `return` in Internal View Functions

Description:

The internal view helpers totalSupply_ and _balanceOf use Yul’s return opcode:

function totalSupply_() internal view returns (uint256) {
assembly {
let slot := _totalSupply.slot
let supply := sload(slot)
mstore(0x00, supply)
return(0x00, 0x20)
}
}
function _balanceOf(address owner) internal view returns (uint256) {
assembly {
// ...
return(ptr, 0x20)
}
}

These functions are declared internal but use return(...) at the EVM level, which aborts execution of the entire call, not just the internal function. In the current codebase they are only used as direct return expressions in the public view wrappers:

function totalSupply() public view virtual returns (uint256) {
return totalSupply_();
}
function balanceOf(address owner) public view virtual returns (uint256) {
return _balanceOf(owner);
}

Here, the pattern works because totalSupply_ and _balanceOf are effectively acting as fully inlined implementations of the public functions. However, any future attempt to reuse these internal helpers in more complex logic (e.g., uint256 s = totalSupply_(); uint256 x = s + 1;) would silently break: the return in the assembly would exit the entire call before executing subsequent code.

Impact:

  • Currently low, because the functions are only used as direct return targets.

  • Future maintainers might treat totalSupply_ / _balanceOf as “normal” internal helpers and call them in more complex functions, leading to:

    • Unexpected early returns.

    • ABI decoding errors (if the outer function expects multiple return values but return sends only one).

    • Very subtle bugs that are hard to diagnose.

Proof of Concept:

You can create a derived token that tries to use totalSupply_ in additional logic:

contract BadExtension is ERC20 {
constructor() ERC20("Bad", "BAD") {}
function buggy() external view returns (uint256, uint256) {
uint256 s = totalSupply_(); // assembly `return` fires here
uint256 x = s + 1; // never executed
return (s, x); // never reached, and ABI expects 64 bytes
}
}

Any call to buggy() will actually return only the 32 bytes written by totalSupply_ and then stop execution. The ABI decoder on the caller side will expect two uint256 values (64 bytes) and likely revert or misinterpret the data.

Mitigation:

  • Avoid using low‑level return in internal helpers. Instead, write them in normal Solidity or restrict the pattern to external/public functions where you fully implement the ABI in assembly.

  • For example, implement totalSupply and balanceOf directly as assembly in the public functions, and make the internal helpers pure-Solidity:

function totalSupply() public view virtual returns (uint256 supply) {
assembly {
let slot := _totalSupply.slot
supply := sload(slot)
}
}
function _balanceOf(address owner) internal view returns (uint256) {
return _balances[owner];
}
  • If you keep the current pattern, clearly document (in comments) that totalSupply_ and _balanceOf must only be used as direct return targets and are not safe as general internal helpers.

Updates

Lead Judging Commences

gaurangbrdv Lead Judge 19 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

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

opcode disaster

the vulnerabilities related to incorrect opcode used

Support

FAQs

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

Give us feedback!