Token-0x

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

M02. Unchecked arithmetic in _burn

Author Revealed upon completion

Root + Impact

Unchecked arithmetic in _burn allows balance and totalSupply underflow, causing wraparound to maximum values

Description

In a correct ERC20 implementation, burning tokens must decrease both the account balance and the global totalSupply. If the burn amount exceeds either the account balance or the total supply, the operation must revert to prevent underflow and preserve accounting invariants.

In this contract, _burn is implemented in inline assembly and uses raw sub operations to decrease totalSupply and the account balance. These subtractions are performed without any underflow checks. When value is greater than the current balance or supply, the subtraction wraps around to type(uint256).max, silently corrupting state.

// Root cause in the codebase with @> marks to highlight the relevant section
function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
...
let supply := sload(supplySlot)
@> sstore(supplySlot, sub(supply, value)) // unchecked underflow
...
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, sub(accountBalance, value)) // unchecked underflow
}
}

Risk

Likelihood:

  • This occurs whenever _burn is called with a value larger than the account balance or current totalSupply, which is possible through misuse, faulty integrations, or logic errors in higher-level burn functions.

  • Any internal or privileged path that invokes _burn without strict pre-validation exposes this vulnerability.

Impact:

  • totalSupply can underflow to type(uint256).max, permanently breaking ERC20 supply invariants.

  • User balances can wrap to type(uint256).max, effectively granting unlimited tokens and enabling severe accounting and economic exploits.

Proof of Concept

The following test demonstrates that burning more tokens than the user owns does not revert, but instead causes both the user balance and totalSupply to underflow and wrap to type(uint256).max.

Explanation:
The user is minted 10 tokens. Burning 11 tokens triggers sub(10, 11) inside assembly for both balance and totalSupply. Since no underflow check exists, the result wraps to 2^256 - 1, leaving the contract in a severely corrupted state.

function test_burn_underflowBalanceAndTotalSupply() public {
address user = makeAddr("user");
// Give user a small balance
token.mint(user, 10);
// Burn more than balance → raw assembly underflow
token.burn(user, 11);
// Values wrap to uint256 max
assertEq(token.totalSupply(), type(uint256).max);
assertEq(token.balanceOf(user), type(uint256).max);
}

Recommended Mitigation

Add explicit underflow checks in _burn to ensure that both the account balance and total supply are sufficient before performing subtraction, or rely on Solidity’s checked arithmetic.

_burn function:

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
...
let supply := sload(supplySlot)
+ if lt(supply, value) {
+ revert(0, 0)
+ }
- sstore(supplySlot, sub(supply, value))
+ sstore(supplySlot, sub(supply, value))
...
let accountBalance := sload(accountBalanceSlot)
+ if lt(accountBalance, value) {
+ revert(0, 0)
+ }
- sstore(accountBalanceSlot, sub(accountBalance, value))
+ sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Alternatively, move balance and supply updates out of assembly and into Solidity to automatically enforce underflow protection.

Support

FAQs

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

Give us feedback!