Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: high
Valid

Unchecked overflow in _mint() corrupts totalSupply and balances

Root + Impact

Unchecked overflow in _mint() can corrupt totalSupply and user balances, potentially breaking token accounting and downstream integrations.


Description

  • Normal behavior: A correct ERC20 _mint() implementation should increase totalSupply and the recipient’s balance, and revert if the operation would overflow uint256. In practice, totalSupply + value and balance + value must always remain within type(uint256).max.

  • Issue: The current _mint() implementation performs raw Yul add() operations on both totalSupply and accountBalance without any overflow checks. Since Yul arithmetic is modulo 2²⁵⁶, minting enough tokens causes both totalSupply and the recipient’s balance to silently wrap around to a much smaller value instead of reverting, breaking ERC20 invariants.

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
// ... zero-address check and setup omitted ...
let supply := sload(supplySlot)
@> sstore(supplySlot, add(supply, value)) // ❌ unchecked addition (totalSupply overflow)
// ... load balance slot ...
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, add(accountBalance, value)) // ❌ unchecked addition (balance overflow)
}
}

Risk

Likelihood:

  • Any derived token that exposes _mint() via a public mint() or uses it in token distribution logic can accidentally mint near the uint256 limit, especially for uncapped or governance-controlled tokens.

  • Developers may assume Solidity 0.8+ style overflow protection applies everywhere and forget that this low-level assembly bypasses it, making misuse likely in future extensions.

Impact:

  • totalSupply can overflow, wrapping from a large value back to a very small value (even 0), breaking supply-based logic, caps, and downstream protocols.

  • User balances can overflow, resetting balances or making accounting inconsistent across DEXes, lending protocols, and indexers that rely on ERC20 invariants.


Proof of Concept

The following Foundry test demonstrates how the _mint() function performs unchecked addition on both totalSupply and user balances. By setting these values close to uint256.max and calling mint(), both variables silently overflow and wrap to a small number instead of reverting.

function testMintOverflow() public {
uint256 nearMax = type(uint256).max - 5;
// Force totalSupply near uint256 max
vm.store(address(token), bytes32(uint256(2)), bytes32(nearMax));
// Force attacker balance near uint256 max
bytes32 balanceSlot = keccak256(abi.encode(attacker, uint256(0)));
vm.store(address(token), balanceSlot, bytes32(nearMax));
console.log("=== BEFORE MINT OVERFLOW ===");
console.log("totalSupply:", token.totalSupply());
console.log("attacker balance:", token.balanceOf(attacker));
vm.prank(attacker);
token.mint(attacker, 10); // triggers overflow
console.log("=== AFTER MINT OVERFLOW ===");
console.log("totalSupply:", token.totalSupply());
console.log("attacker balance:", token.balanceOf(attacker));
uint256 expectedOverflow;
unchecked { expectedOverflow = nearMax + 10; }
console.log("expected overflowed value:", expectedOverflow);
}

Observed Results

=== BEFORE MINT OVERFLOW ===
totalSupply:
115792089237316195423570985008687907853269984665640564039457584007913129639930
attacker balance:
115792089237316195423570985008687907853269984665640564039457584007913129639930
=== AFTER MINT OVERFLOW ===
totalSupply: 4
attacker balance: 4
expected overflowed value: 4

The logs confirm that when both totalSupply and the attacker’s balance are set near uint256.max, calling _mint(attacker, 10) causes the following:

  • totalSupply + value overflows and wraps to 4

  • balanceOf(attacker) + value also wraps to 4

  • No revert occurs because _mint() uses raw Yul add() which does not check for overflow

This demonstrates a critical accounting flaw: minting tokens near the uint256 limit silently corrupts both balances and total supply, violating ERC20 invariants and risking downstream protocol failures.


Recommended Mitigation

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
- let supply := sload(supplySlot)
- sstore(supplySlot, add(supply, value))
+ let supply := sload(supplySlot)
+ // Prevent totalSupply overflow
+ if gt(value, sub(not(0), supply)) {
+ revert(0, 0)
+ }
+ sstore(supplySlot, add(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
- sstore(accountBalanceSlot, add(accountBalance, value))
+ // Prevent balance overflow
+ if gt(value, sub(not(0), accountBalance)) {
+ revert(0, 0)
+ }
+ sstore(accountBalanceSlot, add(accountBalance, value))
+ // Emit Transfer(address(0), account, value)
+ mstore(ptr, value)
+ log3(
+ ptr,
+ 0x20,
+ 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
+ 0x0000000000000000000000000000000000000000000000000000000000000000,
+ account
+ )
}
}

The fix adds explicit overflow checks to both totalSupply and user balances before performing the Yul add() operation, which does not revert on overflow. By verifying that value does not exceed uint256.max - currentValue, the mint operation can only proceed when arithmetic stays within bounds. This prevents silent wrap-around that previously caused totalSupply and balances to reset to small numbers when near the limit. Additionally, the fix corrects zero-address error handling and adds the required Transfer(address(0), account, value) event for ERC20 compliance. As a result, minting can no longer corrupt supply or balances, ERC20 invariants remain intact, and the overflow exploit demonstrated in the PoC is fully mitigated.

  • Prevents overflow of totalSupply

  • Prevents overflow of user balances

  • Ensures ERC20-compliant event emission

  • Eliminates silent wrap-around demonstrated in the exploit

Updates

Lead Judging Commences

gaurangbrdv Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

overflow & underflow

missing checks for overflow and underflow.

Appeal created

sidd Submitter
18 days ago
gaurangbrdv Lead Judge
17 days ago
gaurangbrdv Lead Judge 13 days ago
Submission Judgement Published
Validated
Assigned finding tags:

overflow & underflow

missing checks for overflow and underflow.

Support

FAQs

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

Give us feedback!