Token-0x

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

Unchecked _mint function overflow

Author Revealed upon completion

Root + Impact

Description

  • Under normal ERC-20 behavior, each mint increases both totalSupply and the recipient’s balance(accountBalance) by the minted amount, and the arithmetic reverts if it would overflow.

  • In ERC20Internals._mint, both additions happen in assembly without overflow checks, so minting type(uint256).max followed by another X amounts wraps supply and the recipient’s balance back to 0, letting anyone reset accounting.

// the user balance should be checked befor minting to overcome overflow
function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0xec442f05))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
let supply := sload(supplySlot)
@> sstore(supplySlot, add(supply, value)) //unchecked accountBalance
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, add(accountBalance, value)) //unchecked accountBalance
}
}

Risk

Likelihood:

  • Reason 1: Whenever _mint is callable (directly or via a derived contract), an attacker can immediately trigger it with the crafted two-mint sequence.The attack uses only public API calls and values any chain can represent, so it succeeds every time.

Impact:

  • Impact 1:totalSupply and affected balances wrap to zero, destroying token accounting and enabling arbitrary re-minting, which will result downstream users and protocols lose funds because the token supply becomes meaningless.

  • Impact 2

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
// Harness exposes the vulnerable internal _mint so we can hit ERC20Internals directly.
contract MintHarness is ERC20 {
constructor() ERC20("Token", "TKN") {}
function exposedMint(address account, uint256 value) external {
_mint(account, value);
}
}
contract MintOverflow is Test {
MintHarness internal token;
address internal attacker = address(0x2029 //attacker's address
function setUp() public {
token = new MintHarness();
}
function test_mintOverflow() public {
// First mint pushes balance and supply to uint256.max without reverting.
token.exposedMint(attacker, type(uint256).max);
// Second mint of 1 causes supply and balance to wrap back to zero.
token.exposedMint(attacker, 1);
// Confirm the values wrapped, proving unchecked addition in _mint.
assertEq(token.totalSupply(), 0, "totalSupply wrapped to 0");
assertEq(token.balanceOf(attacker), 0, "attacker balance wrapped to 0");
}
}

Recommended Mitigation

//Fixed the overflow by checking the user balance before proceeding minting
function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0xec442f05))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
let supply := sload(supplySlot)
+ let supplyHeadroom := sub(not(0), supply)
+ if gt(value, supplyHeadroom) {
+ mstore(0x00, shl(224, 0xe450d38c))
+ mstore(add(0x00, 4), address())
+ mstore(add(0x00, 0x24), supply)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
+ let balanceHeadroom := sub(not(0), accountBalance)
+ if gt(value, balanceHeadroom) {
+ mstore(0x00, shl(224, 0xe450d38c))
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
sstore(supplySlot, add(supply, value))
sstore(accountBalanceSlot, add(accountBalance, value))
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0, account)
}
}

Support

FAQs

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

Give us feedback!