Token-0x

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

Unchecked `_mint` Can Overflow `totalSupply` and Account Balances

Author Revealed upon completion

Description:
The internal _mint implementation in ERC20Internals performs unchecked arithmetic operations on both totalSupply and the account balance, without proper overflow validation:

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
// ...
let supply := sload(supplySlot)
sstore(supplySlot, add(supply, value)) // No overflow check
// ...
sstore(accountBalanceSlot, add(accountBalance, value)) // No overflow check
}
}

There are no checks to ensure that totalSupply + value and balanceOf(account) + value do not overflow. It can lead to:

  • Wrap totalSupply back to zero or a small number.

  • Wrap a holder’s balance back to zero or a smaller number, essentially “burning” their tokens without any revert.

All of this happens silently, which is very dangerous for a library that aims to be “secure and cheap” and is intended as a base for DeFi protocols.

Impact:

  • If the contract mints very large amounts or is used in a context where totalSupply can approach type(uint256).max, it will overflow and wrap to a small number without reverting.

  • This breaks core invariants expected by other contracts (e.g., sum(balances) == totalSupply).

  • In extreme scenarios, user balances can wrap to zero after minting (a permanent loss of effective tokens from the holder’s perspective).

This is a high‑risk correctness bug. While it may seem unlikely to hit 2^256 in practice, protocols often reason symbolically about totalSupply, so the lack of any safety guard is not acceptable in a reusable base contract.

Proof of Concept:

Token using Token-0x ERC20:

// Add this test to Token.t.sol
function test_mintOverflowWrapsSupplyAndBalance() public {
address alice = makeAddr("alice");
// First mint: set balance and totalSupply to max uint256
token.mint(alice, type(uint256).max);
assertEq(token.totalSupply(), type(uint256).max);
assertEq(token.balanceOf(alice), type(uint256).max);
// Second mint of 1 token will overflow both totalSupply and balance
token.mint(alice, 1);
// Now both should have wrapped around (max + 1 == 0 mod 2^256)
assertEq(token.totalSupply(), 0, "totalSupply has wrapped to 0");
assertEq(token.balanceOf(alice), 0, "balance has wrapped to 0");
}

Token2 using OpenZeppelin ERC20:

// Add this test to Token2.t.sol
function test_mintOverflowReverts_OZ() public {
address alice = makeAddr("alice");
// First mint to max uint256
token.mint(alice, type(uint256).max);
assertEq(token.totalSupply(), type(uint256).max);
// Second mint of 1 in OZ should revert due to checked arithmetic
vm.expectRevert();
token.mint(alice, 1);
}

OpenZeppelin’s implementation reverts when totalSupply + amount overflows, while Token-0x silently wraps, proving the issue.

Mitigation:

  • Add overflow checks in _mint for both totalSupply and the recipient’s balance:

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0xec442f05))
mstore(add(0x00, 4), account)
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 lt(newSupply, supply) {
+ // Revert with overflow error
+ revert(0, 0)
+ }
+ sstore(supplySlot, newSupply)
- sstore(supplySlot, add(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
+ let newBalance := add(accountBalance, value)
+ // Check for overflow
+ if lt(newBalance, accountBalance) {
+ revert(0, 0)
+ }
+ sstore(accountBalanceSlot, newBalance)
- sstore(accountBalanceSlot, add(accountBalance, value))
}
}
  • Implement mint logic in Solidity with default overflow checks and only optimize storage access with assembly.

Support

FAQs

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

Give us feedback!