Token-0x

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

Integer Overflow in `_mint` Corrupts `totalSupply` and Account Balances

Author Revealed upon completion

Root Cause :
The _mintfunction uses inline assembly to update both _totalSupply and the user's _balances using the add opcode without validating that the result does not overflow type(uint256).max.

Impact :
Overflow causes the totalSupply and the user's balance to wrap around to a small number. This completely destroys the token's accounting invariants, allowing the creation of "phantom" tokens or erasing the total supply record while tokens still exist in circulation.

Description

  • In _mint, the state updates are performed using raw assembly:

    sstore(supplySlot, add(supply, value))

    sstore(accountBalanceSlot, add(accountBalance, value))

  • In Solidity 0.8+ high-level arithmetic, supply + value would revert on overflow, but here the inline assembly add silently wraps modulo 22562256.
    If supply or accountBalance are close to type(uint256).max, minting more tokens causes:

    • totalSupply to wrap to a small value (e.g., 99)

    • the account’s balance to wrap to the same small value

    This breaks ERC‑20 accounting invariants and makes total supply and balances untrustworthy.

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
// ...
let supply := sload(supplySlot)
// Missing: check that supply + value does not overflow
@> sstore(supplySlot, add(supply, value))
// ...
let accountBalance := sload(accountBalanceSlot)
// Missing: check that accountBalance + value does not overflow
@> sstore(accountBalanceSlot, add(accountBalance, value))
// ...
}
}

Risk

Likelihood:

  • High in any system where _mint can be called repeatedly (staking rewards, farming, airdrops, admin mint, etc.).

  • No special conditions besides being able to accumulate large balances and supply over time.


Impact:

  • Corrupted totalSupply: Global supply shows a tiny number while many tokens exist in circulation.

  • Corrupted balances: A large holder’s balance can wrap down to a small value, breaking all accounting based on i

  • DeFi integrations at risk: Protocols that use totalSupply in pricing (e.g., pool reserves / supply), lending ratios, or risk models can be manipulated or broken once supply is no longer reliable.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol"; // Adjust path to your ERC20 file
// Mock contract to expose internal _mint function
contract ERC20Mock is ERC20 {
constructor() ERC20("Test", "TST") {}
function mint(address to, uint256 amount) public { _mint(to, amount); }
}
contract MintOverflowTest is Test {
ERC20Mock public token;
address public alice = makeAddr("alice");
function setUp() public {
token = new ERC20Mock();
}
function test_mint_overflow() public {
// 1. Initialize with a massive supply (near max uint256)
uint256 initialSupply = type(uint256).max - 100;
token.mint(alice, initialSupply);
console.log("Supply Before:", token.totalSupply());
// 2. Mint 200 more tokens.
// Expected: Revert or Safe Failure
// Actual: Overflow (wraps to ~99)
token.mint(alice, 200);
uint256 supplyAfter = token.totalSupply();
uint256 balanceAfter = token.balanceOf(alice);
console.log("Supply After :", supplyAfter);
console.log("Balance After:", balanceAfter);
// 3. Assertions proving the overflow occurred
assertLt(supplyAfter, initialSupply);
assertEq(supplyAfter, 99); // (MAX - 100) + 200 = MAX + 100 -> wraps to 99
}
}

Run:

forge test --match-test test_mint_overflow -vvvv

Output:

[PASS] test_mint_overflow() (gas: 64297)
Logs:
Supply Before: 115792089237316195423570985008687907853269984665640564039457584007913129639835
Supply After : 99
Balance After: 99

Recommended Mitigation

let supply := sload(supplySlot)
- sstore(supplySlot, add(supply, value))
+ let newSupply := add(supply, value)
+ if lt(newSupply, supply) {
+ mstore(0x00, shl(224, 0x4e487b71)) // Panic(uint256)
+ mstore(add(0x00, 4), 0x11) // Code 0x11 (Overflow)
+ revert(0x00, 0x24)
+ }
+ sstore(supplySlot, newSupply)
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
- sstore(accountBalanceSlot, add(accountBalance, value))
+ let newBalance := add(accountBalance, value)
+ if lt(newBalance, accountBalance) {
+ mstore(0x00, shl(224, 0x4e487b71)) // Panic(uint256)
+ mstore(add(0x00, 4), 0x11) // Code 0x11 (Overflow)
+ revert(0x00, 0x24)
+ }
+ sstore(accountBalanceSlot, newBalance)

Support

FAQs

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

Give us feedback!