Token-0x

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

M01. Unchecked arithmetic in _mint

Author Revealed upon completion

Root + Impact

Unchecked arithmetic in _mint allows totalSupply and balance overflow, leading to silent token supply corruption

Description

In a standard ERC20 implementation, minting tokens increases both the recipient’s balance and the global totalSupply. These increments must revert on overflow to preserve the core invariant that the sum of all balances equals totalSupply.

In this contract, the _mint function is implemented in inline assembly and performs raw add operations when updating _totalSupply and the recipient’s balance. Because these additions are not guarded by explicit overflow checks, minting values near type(uint256).max causes arithmetic wraparound. This results in totalSupply and balances silently resetting to zero, permanently corrupting accounting invariants.

// Root cause in the codebase with @> marks to highlight the relevant section
function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
...
let supply := sload(supplySlot)
@> sstore(supplySlot, add(supply, value)) // unchecked overflow
...
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, add(accountBalance, value)) // unchecked overflow
}
}

Risk

Likelihood:

  • This occurs whenever minting is performed with values close to type(uint256).max, which is feasible in tests, upgrades, governance actions, or misconfigured integrations.

  • The vulnerability is reachable in any environment where _mint is callable, including privileged roles or internal minting logic.

Impact:

  • totalSupply can silently wrap to zero or an arbitrary low value, breaking ERC20 supply invariants.

  • User balances can be reset or corrupted, enabling loss of funds, incorrect accounting, and downstream protocol failures.

Proof of Concept

The following test demonstrates that minting type(uint256).max followed by an additional mint does not revert, but instead overflows both the user balance and totalSupply.

Explanation:
The first mint sets both the user’s balance and totalSupply to the maximum uint256 value. The second mint performs add(max, 1) inside assembly, which wraps to zero instead of reverting. This confirms that overflow checks are missing and state corruption occurs silently.

function test_mint_balanceAndTotalSupplyOverflow() public {
address user = makeAddr("user");
// Set balance and totalSupply to uint256 max
token.mint(user, type(uint256).max);
// Overflow occurs here; no revert
token.mint(user, 1);
// Both values wrapped to zero due to unchecked add
assertEq(token.totalSupply(), 0);
assertEq(token.balanceOf(user), 0);
}

Recommended Mitigation

Add explicit overflow checks in _mint before performing arithmetic, or delegate arithmetic to Solidity where overflow protection is enforced automatically.

_mint function:

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
...
let supply := sload(supplySlot)
+ if lt(add(supply, value), supply) {
+ revert(0, 0)
+ }
- sstore(supplySlot, add(supply, value))
+ sstore(supplySlot, add(supply, value))
...
let accountBalance := sload(accountBalanceSlot)
+ if lt(add(accountBalance, value), accountBalance) {
+ revert(0, 0)
+ }
- sstore(accountBalanceSlot, add(accountBalance, value))
+ sstore(accountBalanceSlot, add(accountBalance, value))
}
}

Alternatively, remove the assembly arithmetic entirely and perform the balance and supply updates in Solidity to inherit built-in overflow checks.

Support

FAQs

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

Give us feedback!