Token-0x

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

Unchecked `burn` underflow lets attackers mint enormous balances and inflate `totalSupply`

Root + Impact

ERC20Internals::_burn updates both totalSupply and balances[account] using unchecked sub in inline assembly without any prior balance or supply checks, so burning more tokens than the account balance or total supply causes both values to underflow to huge numbers, effectively minting tokens instead of reducing them.

Description

  • Normal behavior: In a correct ERC-20 implementation, burn should reduce both totalSupply and the caller’s balance by value, and revert when value is greater than either the holder’s balance or the current totalSupply. This preserves the fundamental accounting invariant and prevents accidental or malicious over-burning.

  • Actual behavior: In ERC20Internals::_burn, the contract directly performs sub(supply, value) and sub(balance, value) in assembly and stores the result without any prior checks. When value exceeds totalSupply or the account’s balance, the subtraction wraps around at 2^256, causing both totalSupply and balances[account] to jump to extremely large values. This effectively mints a huge amount of tokens instead of burning, fully breaking ERC-20 accounting and enabling an attacker to escalate their balance to near-uint256 max with a single over-burn.

Issue in 171 and 178 lines

let supplySlot := _totalSupply.slot
let supply := sload(supplySlot)
@> sstore(supplySlot, sub(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, sub(accountBalance, value))
}

Risk

Likelihood:

  • The bug is triggered whenever a contract exposes _burn through a public or external function (e.g. burn(address,uint256) or burn(uint256)) and allows a caller to pass a value greater than their current balance or the token’s total supply. This is exactly how the provided Token test contract is wired, and it is a very common pattern for burnable ERC-20 tokens.

  • Any future integration or in-scope token that reuses this _burn implementation without adding explicit balance >= value and totalSupply >= value checks will immediately inherit the vulnerability, making the issue highly likely to manifest in practice as the codebase evolves.

Impact:

  • A malicious user can call the burn entrypoint with an oversized value to cause both totalSupply and their own balance to underflow to values close to type(uint256).max, effectively minting an enormous amount of tokens in a single transaction instead of burning.

  • With this artificially inflated balance and inflated totalSupply, the attacker can drain any pools, lending markets, or protocols that rely on this token’s balances and supply for pricing, collateral, rewards, or governance, leading to catastrophic and unrecoverable loss of funds and total loss of trust in the asset.

Proof of Concept

The following Foundry test demonstrates how calling burn with an amount larger than the holder’s balance causes both totalSupply and the attacker’s balance to increase due to unchecked underflow in _burn.

Just paste in test/Token.t.sol

function test_underflowViaBurn() public {
address attacker = makeAddr("hacker");
uint256 mintAmount = 10e18;
// Mint some tokens to the attacker
vm.prank(attacker);
token.mint(attacker, mintAmount);
uint256 totalSupplyBefore = token.totalSupply();
uint256 attackerBalanceBefore = token.balanceOf(attacker);
// Burn more than the attacker balance
vm.prank(attacker);
token.burn(attacker, 100e18);
uint256 totalSupplyAfter = token.totalSupply();
uint256 attackerBalanceAfter = token.balanceOf(attacker);
// Both totalSupply and attacker balance increased due to underflow
assertGt(
totalSupplyAfter,
totalSupplyBefore,
"Burning more than total supply did not increase totalSupply as expected (underflow bug)"
);
assertGt(
attackerBalanceAfter,
attackerBalanceBefore,
"Burning more than balance did not increase attacker balance as expected (underflow bug)"
);
}

Recommended Mitigation

Add explicit supply and balance checks in _burn before performing the sub operations in assembly, mirroring the safety pattern already used in _transfer and _spendAllowance. The function should revert when value exceeds either totalSupply or the account’s balance, and only then update storage.

- sstore(supplySlot, sub(supply, value))
+ // Prevent totalSupply underflow: revert when value > supply
+ if lt(supply, value) {
+ // minimal revert; can be replaced with a dedicated error selector
+ revert(0x00, 0x00)
+ }
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
- sstore(accountBalanceSlot, sub(accountBalance, value))
+ // Prevent balance underflow: revert when value > accountBalance
+ if lt(accountBalance, value) {
+ // reuse the existing ERC20InsufficientBalance_style error if desired
+ mstore(0x00, shl(224, 0xfc14934b)) // selector placeholder
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
+
+ // Safe updates (no underflow after the checks)
+ sstore(supplySlot, sub(supply, value))
+ sstore(accountBalanceSlot, sub(accountBalance, 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!