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))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, add(accountBalance, value))
}
}
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.
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");
uint256 nearMax = type(uint256).max - 1000;
baseToken.mint(recipient, nearMax);
baseToken.mint(recipient, 1001);
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)
}
}