Token-0x

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

Missing Overflow Checks in Assembly-Based ERC20 Storage Operations

Author Revealed upon completion

The contract should safely perform arithmetic operations when minting, burning, and transferring tokens. When tokens are minted, the total supply should increase, and the recipient's balance should increase by the minted amount. When tokens are burned, the total supply should decrease, and the burner's balance should decrease by the burned amount. When tokens are transferred, the sender's balance should decrease and the recipient's balance should increase by the transferred amount

Specific Issue

The contract uses Yul assembly's add and sub instructions without overflow/underflow checks. While Solidity 0.8.x has built-in overflow protection at the Solidity level, these protections are bypassed when using inline assembly. The assembly blocks don't include manual overflow/underflow checks, allowing silent arithmetic overflows.

https://github.com/CodeHawks-Contests/2025-12-token-0x/blob/7f9f55d58a485a36fb56284d8d0e8a415544bf9b/src/helpers/ERC20Internals.sol#L147

Risk

Likelihood: High

  • The contract uses Yul assembly for all arithmetic operations, completely bypassing Solidity's built-in overflow protection

  • Minting additional tokens to an account with a high balance could overflow

Impact:

  • Token Supply Manipulation: An attacker could overflow the total supply, wrapping it to a small value

  • Balance Corruption: User balances could overflow/underflow, leading to incorrect token amounts

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ERC20InternalsPOC {
mapping(address account => uint256) internal _balances;
function setBalance(address account, uint256 amount) external {
_balances[account] = amount;
}
function vulnerableTransfer(address from, address to, uint256 value) external returns (bool) {
assembly {
let ptr := mload(0x40)
let baseSlot := _balances.slot
mstore(ptr, from)
mstore(add(ptr, 0x20), baseSlot)
let fromSlot := keccak256(ptr, 0x40)
let fromAmount := sload(fromSlot)
mstore(ptr, to)
mstore(add(ptr, 0x20), baseSlot)
let toSlot := keccak256(ptr, 0x40)
let toAmount := sload(toSlot)
if lt(fromAmount, value) {
revert(0, 0)
}
sstore(fromSlot, sub(fromAmount, value))
sstore(toSlot, add(toAmount, value)) // ← This can overflow!
}
return true;
}
function getBalance(address account) external view returns (uint256) {
return _balances[account];
}
function demonstrateOverflow() external {
Setup: Alice has max uint256 - 1000 tokens
address alice = address(0x1);
address bob = address(0x2);
Set Alice's balance to almost max uint256
_balances[alice] = type(uint256).max - 1000;
_balances[bob] = 500;
Transfer 2000 tokens from Alice to Bob
Bob's new balance should be: 500 + 2000 = 2500
But due to overflow: (max - 1000) + 2000 overflows!
uint256 aliceBefore = _balances[alice];
uint256 bobBefore = _balances[bob];
vulnerableTransfer(alice, bob, 2000);
uint256 aliceAfter = _balances[alice];
uint256 bobAfter = _balances[bob];
}
}

Recommended Mitigation

sstore(supplySlot, add(supply, value))
// For _mint
let newSupply := add(supply, value)
if lt(newSupply, supply) { revert(0, 0) } // overflow → revert entire tx
sstore(supplySlot, newSupply)
// For _burn
if lt(supply, value) { revert(0, 0) }
sstore(supplySlot, sub(supply, value))

Support

FAQs

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

Give us feedback!