Token-0x

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

Unchecked arithmetic in low-level _mint (assembly) allows integer wraparound and breaks ERC-20 invariants

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: _mint should increase _totalSupply by value, increase account's balance by value, and emit the Transfer(address(0), account, value) event. All arithmetic must preserve invariants (no wraparound) and respect access control.

Specific issue: the function performs arithmetic and storage writes in inline assembly using the EVM add opcode. In Solidity ≥0.8 the high-level + operator is checked for overflow, but the raw add opcode used here is unchecked and will silently wrap on overflow. As written, an attacker or privileged minter can choose value such that supply + value or balance + value wraps modulo 2²⁵⁶, defeating the supply/balance invariants and enabling token inflation or logic bypasses that rely on monotonic total supply.

// Root cause in the codebase with @> marks to highlight the relevant section
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) //loading free memory pointer
let balanceSlot := _balances.slot //loading base slot of balances mapping
let supplySlot := _totalSupply.slot //loading total supply slot
let supply := sload(supplySlot) //loading current total supply
@> sstore(supplySlot, add(supply, value)) //updating total supply
mstore(ptr, account) //store account at ptr
mstore(add(ptr, 0x20), balanceSlot) //storing balance base slot in next 32 bytes
let accountBalanceSlot := keccak256(ptr, 0x40) //calculating account balance slot
let accountBalance := sload(accountBalanceSlot) //loading current account balance
@> sstore(accountBalanceSlot, add(accountBalance, value)) //updating account balance
}
}

Risk

Likelihood:

  • Requires a call path that executes this internal _mint. If such a call path exists (common in contracts with admin/owner minting), the vulnerability is exploitable.

Impact:

  • Creation of arbitrarily large effective token supply while observed totalSupply becomes inconsistent (wrap to small value).

Bypass of supply caps, distribution logic, or on-chain checks that rely on totalSupply monotonicity.

Proof of Concept

  • In the poc we can clearly see that the earlier balance of the account was uint256.max

  • When we try to mint even 3 tokens it resets back instead of reverting the call.

function test_mint_overflow() public {
//uint256 amount = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
address account = makeAddr("account");
token.mint(account,type(uint256).max); // should revert
token.mint(account,3);
// uint256 balance = token.balanceOf(account);
// assertEq(balance, amount);
console.log(token.totalSupply());
}
// Output
Ran 1 test for test/Token.t.sol:TokenTest
[PASS] test_mint_overflow() (gas: 61335)
Logs:
2
Traces:
[61335] TokenTest::test_mint_overflow()
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e]
├─ [0] VM::label(account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e], "account")
│ └─ ← [Return]
├─ [45004] Token::mint(account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ └─ ← [Stop]
├─ [1204] Token::mint(account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e], 3)
│ └─ ← [Stop]
├─ [317] Token::totalSupply() [staticcall]
│ └─ ← [Return] 2
├─ [0] console::log(2) [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.56ms (448.40µs CPU time)

Recommended Mitigation

  • Remove raw arithmetic in assembly unless strictly required for gas optimization and proven safe.

Prefer a high-level Solidity implementation that benefits from built-in checked arithmetic (Solidity ≥0.8)

function _mint(address account, uint256 value) internal {
require(account != address(0), "ERC20: mint to the zero address");
_totalSupply += value; // checked arithmetic in Solidity >=0.8
_balances[account] += value;
emit Transfer(address(0), account, value);
}

Support

FAQs

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

Give us feedback!