ERC20Internals.sol _mint() does not check for overflow + after minting max uint256 tokens, minting more will result in overflow reducing the ERC20.sol totalSupply()
Description
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:
Impact:
Proof of Concept
Place at the top of test/Token.t.sol:
import {console} from "forge-std/Test.sol";
Place in contract test/Token.t.sol Tokentest:
function test_mintOverflow() public {
console.log("type(uint256).max");
console.log(type(uint256).max);
uint256 amount = type(uint256).max;
address account = makeAddr("account");
token.mint(account, amount);
uint256 balance = token.balanceOf(account);
assertEq(balance, amount);
uint256 totalSupply = token.totalSupply();
assertEq(totalSupply, amount);
console.log("balance");
console.log(balance);
console.log("token.totalSupply()");
console.log(totalSupply);
amount = 1;
token.mint(account, amount);
balance = token.balanceOf(account);
totalSupply = token.totalSupply();
console.log("balance after");
console.log(balance);
console.log("token.totalSupply() after");
console.log(totalSupply);
}
Lines 2 to 18: Mint the maximum number of tokens that will fit in a uint256. This will be the type(uint256).max value.
Line 20 to 21: Mint one more token.
Lines 23 to 30: check that overflow indeed occurs.
Run with:
forge test -vvv --match-test test_mintOverflow
Sample output:
[PASS] test_mintOverflow() (gas: 54160)
Logs:
type(uint256).max
115792089237316195423570985008687907853269984665640564039457584007913129639935
balance
115792089237316195423570985008687907853269984665640564039457584007913129639935
token.totalSupply()
115792089237316195423570985008687907853269984665640564039457584007913129639935
balance after
0
token.totalSupply() after
0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.60ms (4.46ms CPU time)
Ran 1 test suite in 44.87ms (7.60ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Recommended Mitigation
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)
+
+ // test for overflow
+ let predictedSupply := add(supply, value)
+ if lt(predictedSupply, supply) {
+ revert(0, 0)
+ }
+
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))
}
}