Token-0x

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

Wasting gas to erase memories

Author Revealed upon completion

Root + Impact

Description

  • The _balanceOf function is designed to be a gas-efficient implementation for retrieving token balances using inline assembly to minimize overhead

  • The function contains two gas inefficiencies: (1) an unnecessary memory write of zero to add(ptr, 0x20) immediately before return, and (2) an unnecessary intermediate variable amount that adds extra stack operations when the result could be written directly to memory

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) // Unnecessary intermediate variable
@> mstore(ptr, amount) // Could write sload result directly
@> mstore(add(ptr, 0x20), 0) // Unnecessary gas waste
return(ptr, 0x20)
}
}

Risk

Likelihood:

  • These gas inefficiencies occur whenever _balanceOf is called within state-changing transactions such as transfer, transferFrom, mint, or burn operations

  • The wasteful operations execute unconditionally in 100% of internal balance queries during paid transactions

  • High-frequency DeFi operations that check balances multiple times per transaction will multiply this overhead

Impact:

  • Additional ~8 gas wasted per internal balance query:

    • ~6 gas from unnecessary memory write (3 gas ADD + 3 gas MSTORE)

    • ~1-2 gas from unnecessary stack operations with intermediate variable

  • Contradicts the contract's stated goal of being "maximally gas efficient"

  • Cumulative gas waste across thousands of transactions increases costs for all users

  • Reduces competitiveness compared to other gas-optimized token implementations in the ecosystem

Proof of Concept

The following test demonstrates the gas waste by comparing execution costs with and without the unnecessary operations:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {ERC20} from "../src/Token.sol";
contract GasWasteTest is Test {
Token public token;
TokenOptimized public tokenOptimized;
address public user = address(0x1);
function setUp() public {
token = new Token();
tokenOptimized = new TokenOptimized();
token.mint(user, 1000e18);
tokenOptimized.mint(user, 1000e18);
}
function testBalanceOfGasWaste() public {
// Measure gas for current implementation
uint256 gasBefore = gasleft();
token.balanceOf(user);
uint256 gasUsedCurrent = gasBefore - gasleft();
// Measure gas for optimized implementation
gasBefore = gasleft();
tokenOptimized.balanceOf(user);
uint256 gasUsedOptimized = gasBefore - gasleft();
console.log("Gas used (current):", gasUsedCurrent);
console.log("Gas used (optimized):", gasUsedOptimized);
console.log("Gas saved:", gasUsedCurrent - gasUsedOptimized);
// The unnecessary operations add:
// - mstore(add(ptr, 0x20), 0): ADD (3 gas) + MSTORE (3 gas) = 6 gas
// - let amount intermediate variable: ~1-2 gas in stack operations
// Total waste: ~7-8 gas per call
}
function testTransferGasImpact() public {
// When _balanceOf is called within transfer operations,
// the gas waste occurs on paid transactions
uint256 gasBefore = gasleft();
vm.prank(user);
token.transfer(address(0x2), 100e18);
uint256 gasUsedCurrent = gasBefore - gasleft();
gasBefore = gasleft();
vm.prank(user);
tokenOptimized.transfer(address(0x2), 100e18);
uint256 gasUsedOptimized = gasBefore - gasleft();
console.log("Transfer gas (current):", gasUsedCurrent);
console.log("Transfer gas (optimized):", gasUsedOptimized);
console.log("Gas saved per transfer:", gasUsedCurrent - gasUsedOptimized);
}
}

Explanation:

When _balanceOf is invoked during state-changing operations like transfers, the unnecessary operations add 7-8 gas overhead. The intermediate amount variable requires additional stack manipulation (POP to store, then load back for mstore), and the zero memory write serves no purpose. While this may seem minor for a single call, token contracts process millions of transactions, making even small optimizations significant for a "maximally gas efficient" implementation.

Recommended Mitigation

Remove both gas inefficiencies by eliminating the unnecessary memory write and the intermediate variable:

Code change:

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)
+ mstore(ptr, sload(dataSlot))
return(ptr, 0x20)
}
}

Rationale:

Issue 1 - Unnecessary memory write:
In the EVM, memory exists only for the duration of the current call context and is automatically discarded after execution completes. Writing zero to add(ptr, 0x20) serves no purpose because:

  1. The function immediately returns after this operation, terminating the call context

  2. No subsequent code reads from this memory location

  3. Memory cleanup provides no gas refund (unlike storage SSTORE operations)

  4. We only return the first 32 bytes (0x20) starting from ptr, so the second slot is never used

Issue 2 - Unnecessary intermediate variable:
The amount variable creates unnecessary stack operations:

  • let amount := sload(dataSlot) - POPs the SLOAD result from stack into a variable

  • mstore(ptr, amount) - Loads the variable back to stack, then executes MSTORE

By writing mstore(ptr, sload(dataSlot)) directly:

  • SLOAD result stays on the stack

  • MSTORE consumes it directly from stack

  • Eliminates redundant stack manipulation operations

Combined benefit:
These optimizations save approximately 7-8 gas per internal balance query:

  • ~6 gas from removing unnecessary ADD and MSTORE opcodes

  • ~1-2 gas from eliminating redundant stack operations

  • Zero functional changes - identical behavior with better efficiency

This change better aligns with the stated goal of maximum gas efficiency while maintaining code clarity and correctness.

Support

FAQs

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

Give us feedback!