Token-0x

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

Unchecked Arithmetic Overflow in _mint() Function

Author Revealed upon completion

Root + Impact

Description

  • The ERC20 standard requires that mint operations include overflow protection to prevent totalSupply from wrapping around when reaching type(uint256).max. The _mint() function in Token-0x's base implementation performs unchecked additions for both totalSupply and user balances, allowing the supply to wrap to zero when overflow occurs.


  • The _transfer() function correctly checks for insufficient balances before subtraction, demonstrating that overflow protection was intentionally omitted in _mint().

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)) // Line 147: Unchecked overflow
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, add(accountBalance, value)) // Line 154: Unchecked overflow
}
}

Risk

Likelihood:

  • Any derived contract that allows minting can trigger this when totalSupply approaches maximum

  • Malicious actors can deliberately mint to cause overflow conditions

  • Normal operations can accidentally trigger overflow in long-running protocols

Impact:

  • Total supply wraps to zero, breaking economic model and token valuation

  • Attackers can mint unlimited tokens after overflow occurs

  • Complete loss of token value and protocol functionality

Proof of Concept

The test demonstrates that minting type(uint256).max - 1000 tokens followed by 1001 more tokens causes the totalSupply to wrap to exactly zero, bypassing any overflow protection that should exist in a secure ERC20 implementation.

// Added to test/Token.t.sol
contract VulnerableToken is ERC20 {
constructor() ERC20("Vuln", "V") {}
function mint(address to, uint256 amount) external { _mint(to, amount); }
}
function test_MintOverflowWrapsSupply() public {
VulnerableToken baseToken = new VulnerableToken();
address recipient = makeAddr("recipient");
// Mint near maximum supply
uint256 nearMax = type(uint256).max - 1000;
baseToken.mint(recipient, nearMax);
// This should revert but doesn't - overflow vulnerability
baseToken.mint(recipient, 1001);
// Supply wrapped to 0 due to overflow
assertEq(baseToken.totalSupply(), 0);
assertEq(baseToken.balanceOf(recipient), 0);
}

Recommended Mitigation

Add overflow checks before performing additions in the _mint() function. The fix should validate that the new value is greater than the previous value, which indicates no overflow occurred.

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)
+ // FIX: Check for overflow
+ if lt(newSupply, supply) {
+ mstore(0x00, shl(224, 0x35278d12)) // Overflow error selector
+ revert(0x00, 0x04)
+ }
+
sstore(supplySlot, newSupply)
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
let newBalance := add(accountBalance, value)
+ // FIX: Check balance overflow too
+ if lt(newBalance, accountBalance) {
+ mstore(0x00, shl(224, 0x35278d12))
+ revert(0x00, 0x04)
+ }
+
sstore(accountBalanceSlot, newBalance)
}
}

Support

FAQs

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

Give us feedback!