Token-0x

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

Gas consuming return mechanisms in totalSupply_, _balanceOf

Author Revealed upon completion

Description

  • View functions like totalSupply() and balanceOf(address) should read from storage and return the value to the caller with minimal overhead. In this codebase, _allowance demonstrates an efficient pattern: it loads a value into a local variable (remaining := sload(…)) and lets Solidity handle the return implicitly, avoiding extra memory writes.

  • totalSupply_ and _balanceOf manually build return data using mstore and return inside inline assembly, which adds extra memory operations and a raw return, increasing gas costs compared to the implicit return pattern used in _allowance. The discrepancy leads to avoidable gas consumption, especially when these functions are called frequently by off-chain indexers and on-chain contracts.

// Root cause in the codebase with @> marks to highlight the relevant section
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 {
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)
}
}

Risk

Likelihood: High

  • High usage of view functions: Calls to totalSupply() and balanceOf() occur constantly (wallets, explorers, price oracles, strategy contracts). Each call pays the overhead introduced by manual mstore + return.

  • Pattern repeated across code: Both functions employ the same manual return pattern, so the additional cost compounds across the most frequently used endpoints.

Impact: Medium

  • Increased gas for on-chain callers: On-chain systems that read balances/supply (e.g., vaults performing checks during state transitions) pay more gas than necessary.

  • Higher operational cost in aggregate: Even if off-chain eth_call gas is not charged to users, many rollups and metered environments account for execution resources; the inefficiency can affect system throughput and costs over time.

Proof of Concept

  • A minimal illustration using the two patterns side by side:

// Inefficient pattern (manual mstore + return)
function totalSupply_Manual() internal view returns (uint256 supply) {
assembly {
let slot := _totalSupply.slot
supply := sload(slot)
mstore(0x00, supply)
return(0x00, 0x20)
}
}
// Efficient pattern (implicit return like _allowance)
function totalSupply_Implicit() internal view returns (uint256 supply) {
assembly {
supply := sload(_totalSupply.slot)
// No mstore / return; Solidity returns 'supply' implicitly
}
}

Recommended Mitigation

  • Adopt the _allowance pattern: load into a local return variable and rely on Solidity’s implicit return, removing manual mstore/return.

- function totalSupply_() internal view returns (uint256) {
- assembly {
- let slot := _totalSupply.slot
- let supply := sload(slot)
- mstore(0x00, supply)
- return(0x00, 0x20)
- }
- }
+ function totalSupply_() internal view returns (uint256 supply) {
+ assembly {
+ // Load directly into the named return variable; Solidity will return implicitly.
+ supply := sload(_totalSupply.slot)
+ }
+ }
- 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)
- }
- }
+ function _balanceOf(address owner) internal view returns (uint256 amount) {
+ assembly {
+ // Keep original guard. (Optional: set 'amount := 0' instead to mirror OZ.)
+ if iszero(owner) {
+ // Optional compatibility path:
+ // amount := 0
+ revert(0, 0)
+ }
+ let baseSlot := _balances.slot
+ let ptr := mload(0x40)
+ mstore(ptr, owner)
+ mstore(add(ptr, 0x20), baseSlot)
+ let dataSlot := keccak256(ptr, 0x40)
+ // Load directly into the named return variable; no manual mstore/return.
+ amount := sload(dataSlot)
+ }
+ }

Support

FAQs

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

Give us feedback!