Token-0x

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

`ERC20Internals::_mint` and `ERC20Internals::_burn` do not check for overflow/underflow on `totalSupply`, causing arithmetic overflow and state inconsistency

ERC20Internals::_mint and ERC20Internals::_burn do not check for overflow/underflow on totalSupply, causing arithmetic overflow and state inconsistency

Description

The _mint and _burn functions must update totalSupply using safe arithmetic: _mint should revert if totalSupply + value causes an overflow, and _burn should revert if totalSupply < value to prevent underflow. This behavior is natively provided by Solidity 0.8+ with checked arithmetic.

The current Yul implementation uses add and sub without any checks, causing silent overflow in _mint and silent underflow in _burn. This can leave totalSupply in an incorrect state and break the fundamental invariant of any ERC20.

function _mint(address account, uint256 value) internal {
(... )
let supply := sload(supplySlot)
@> // missing check
sstore(supplySlot, add(supply, value))
(... )
}
function _burn(address account, uint256 value) internal {
(... )
let supply := sload(supplySlot)
@> // missing check
sstore(supplySlot, sub(supply, value))
(... )
}

Risk

Likelihood: Low

Both overflow in _mint and underflow in _burn only manifest when working with extreme amounts or inconsistent states, which are unlikely in normal ERC20 usage.

Impact: Medium

Silent overflow or underflow can leave totalSupply with an incorrect value, breaking the fundamental ERC20 invariant. This can affect protocols that rely on totalSupply for internal calculations, leading to unexpected behavior or logic failures.

Proof of Concept

This test shows that _mint allows silent overflow: after minting type(uint256).max, a second mint operation with a small value causes totalSupply to wrap around and become 1 instead of reverting.
For _burn, the same problem occurs in reverse: if more than the available totalSupply is burned, a silent underflow leaves totalSupply at a huge value instead of reverting.

function test_Overflow() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// Mint the maximum possible value: no revert, but already at uint256 limit
token.mint(alice, type(uint256).max);
assertEq(token.totalSupply(), type(uint256).max);
// Minting 2 more causes silent overflow: (max + 2) mod 2^256 = 1
token.mint(bob, 2);
// totalSupply becomes 1 instead of reverting → overflow not detected
assertEq(token.totalSupply(), 1);
}
[PASS] test_Overflow() (gas: 83785)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.66ms (1.57ms CPU time)

Recommended Mitigation

Add an overflow check to _mint before adding to totalSupply, using a pattern equivalent to Solidity 0.8+. Overflow can be detected by checking if the sum (supply + value) is less than supply, indicating wrap-around. In this case, the function should revert with Panic(0x11).

Similarly, _burn should include an underflow check before subtracting from totalSupply, verifying that value is not greater than supply. If value > supply, it should also revert with Panic(0x11) to prevent silent underflow.

function _mint(address account, uint256 value) internal {
(... )
let supply := sload(supplySlot)
+ if lt(add(supply, value), value) {
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(add(0x00, 4), 0x11)
+ revert(0x00, 0x24)
+ }
sstore(supplySlot, add(supply, value))
(... )
function _burn(address account, uint256 value) internal {
(... )
let supply := sload(supplySlot)
+ if lt(supply, value) {
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(add(0x00, 4), 0x11)
+ revert(0x00, 0x24)
+ }
sstore(supplySlot, sub(supply, value))
(... )
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 19 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!