Root + Impact
Unchecked Arithmetic in Mint & Burn Logic Causes Supply and Balance Wraparound
Description
A standard ERC20 token implementation must ensure that:
-
totalSupply never overflows.
-
balances[account] never underflows or overflows.
-
Minting increases total supply safely.
-
Burning decreases balances and supply safely.
All arithmetic should be safely checked, per Solidity best practices and ERC20 expectations.
The contract uses unchecked arithmetic (no unchecked block but implicitly unchecked because Solidity <0.8 or manual low-level ops), allowing:
-
Balance underflow during burn, causing user balance to wrap to 2^256 - 1.
-
Total supply overflow during mint, causing totalSupply to wrap back to zero.
Foundry test logs confirm both conditions can be triggered.
function _mint(address to, uint256 amount) internal {
}
function _burn(address from, uint256 amount) internal {
}
Risk
Likelihood:
-
Happens when mint amount exceeds 2^256 - 1 - totalSupply
-
Happens when burn amount exceeds balances[from]
-
No require checks exist
-
Foundry tests demonstrate actual occurrences
Impact:
Impact 2: totalSupply can overflow to 0, breaking accounting and enabling infinite mint exploits.
Proof of Concept
function test_mintOverflow_wrapsTotalSupply() public {
token.mintPublic(address(this), type(uint256).max);
uint256 s1 = token.totalSupply();
assertEq(s1, type(uint256).max);
token.mintPublic(address(this), 1);
uint256 s2 = token.totalSupply();
assertEq(s2, 0, "totalSupply wrapped to 0 (PoC of unchecked arithmetic)");
}
function test_burnUnderflow_wrapsBalance() public {
address alice = address(0x1);
token.mintPublic(alice, 100);
token.burnPublic(alice, 200);
uint256 bal = token.balanceOf(alice);
assertTrue(bal > 100, "balance wrapped due to underflow (PoC)");
Test Log
Ran 2 tests for test/Issue2_UncheckedArithmetic.t.sol:Issue2_UncheckedArithmeticTest
[PASS] test_burnUnderflow_wrapsBalance() (gas: 57959)
Traces:
[57959] Issue2_UncheckedArithmeticTest::test_burnUnderflow_wrapsBalance()
├─ [45071] TestToken::mintPublic(ECRecover: [0x0000000000000000000000000000000000000001], 100)
│ └─ ← [Stop]
├─ [1248] TestToken::burnPublic(ECRecover: [0x0000000000000000000000000000000000000001], 200)
│ └─ ← [Stop]
├─ [721] TestToken::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639836 [1.157e77]
├─ [0] VM::assertTrue(true, "balance wrapped due to underflow (PoC)") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
[PASS] test_mintOverflow_wrapsTotalSupply() (gas: 42988)
Traces:
[59001] Issue2_UncheckedArithmeticTest::test_mintOverflow_wrapsTotalSupply()
├─ [45071] TestToken::mintPublic(Issue2_UncheckedArithmeticTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ └─ ← [Stop]
├─ [317] TestToken::totalSupply() [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
├─ [0] VM::assertEq(115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]) [staticcall]
│ └─ ← [Return]
├─ [1271] TestToken::mintPublic(Issue2_UncheckedArithmeticTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1)
│ └─ ← [Stop]
├─ [317] TestToken::totalSupply() [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0, "totalSupply wrapped to 0 (PoC of unchecked arithmetic)") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Recommended Mitigation
Fix: Add overflow/underflow checks using require or SafeTransfer
function _mint(address to, uint256 amount) internal {
- totalSupply += amount;
- balances[to] += amount;
+ require(to != address(0), "mint to zero");
+ totalSupply = totalSupply + amount;
+ require(totalSupply >= amount, "supply overflow");
+ balances[to] = balances[to] + amount;
+ require(balances[to] >= amount, "balance overflow");
}
function _burn(address from, uint256 amount) internal {
- balances[from] -= amount;
- totalSupply -= amount;
+ require(from != address(0), "burn from zero");
+ require(balances[from] >= amount, "burn underflow");
+ balances[from] = balances[from] - amount;
+ totalSupply = totalSupply - amount;
}