Token-0x

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

The internal functions `ERC20Internals::totalSupply_` and `ERC20Internals::_balanceOf` use `return` in assembly, which can interrupt the execution of calling functions

Author Revealed upon completion

The internal functions ERC20Internals::totalSupply_ and ERC20Internals::_balanceOf use return in assembly, which can interrupt the execution of calling functions

Description

In an ERC20 implementation, internal functions like totalSupply_ and _balanceOf should act as normal helpers: returning a value to the calling function without altering the execution flow.

In this implementation, these functions use return in assembly, which executes an EVM RETURN and ends the entire call. If a child contract calls these functions within other logic, execution is immediately halted and any subsequent code is never run.

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 {
(... )
mstore(ptr, amount)
mstore(add(ptr, 0x20), 0)
@> return(ptr, 0x20)
}
}

Risk

Likelihood: Low

This only occurs when a child contract reuses these internal functions within logic that expects to continue execution after the call. It does not occur in the base ERC20.

Impact: Medium

The internal call prematurely ends all execution, causing unexpected behavior, skipping validations, and breaking inherited functions that depend on these helpers.

Proof of Concept

This test demonstrates that the internal functions _balanceOf and totalSupply_, by using return in assembly, immediately end the EVM call. When a child contract tries to use _balanceOf inside a function that should continue executing logic afterwards, that logic is never run. In this case, the expression return balance * 25 is completely ignored, returning only the raw value from _balanceOf.

contract Token is ERC20 {
(... )
function calcutale(address user) external view returns (uint256) {
uint256 balance = balanceOf(user);
return balance * 25;
}
}
function test_InternalReturnInAssembly_InterruptsExecution() public {
address alice = makeAddr("alice");
token.mint(alice, 2 ether);
// `_balanceOf` does an EVM RETURN and halts execution
// → the line `return balance * 25` is never run
uint256 result = token.calcutale(alice);
console.log(result); // 2 ether; would expect 2 ether * 25 if logic executed fully
}
Logs:
result: 2000000000000000000
Ran 1 test for test/TokenTest.t.sol:TokenTest
[PASS] test_InternalReturnInAssembly_InterruptsExecution() (gas: 60677)
// The line `return balance * 25` is NOT executed — the terminal marks this explicitly:
--> test/Token.sol:23:9:
|
23 | return balance * 25;
| ^^^^^^^^^^^^^^^^^^^

Recommended Mitigation

The mitigation consists of replacing the manual return in assembly with a simple assignment to the Solidity return variable (supply / amount), letting the compiler handle the return without interrupting the execution of the calling function.

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

Support

FAQs

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

Give us feedback!