Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

_mint() does not check for overflow

Author Revealed upon completion

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

  • ERC20Internals.sol _mint()should check and revert on overflow.

  • _mint() does not check for overflow. Minting more than max uint256 tokens will result in overflow reducing the ERC20.sol totalSupply().

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:

  • type(uint256).max tokens have already been minted. Then 1 or more tokens are minted.

Impact:

  • The _totalSupply will overflow and wrap around starting from 0 again.

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)
  • Overflow occurs after minting the final one token reducing the totalSupp() to 0.

Recommended Mitigation

  • Add in a check for whether the supply + value will be less than supply to check for overflow.

  • Revert if it does.

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))
}
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!