Token-0x

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

Unchecked Arithmetic Underflow in _burn() Function

Author Revealed upon completion

Root + Impact

Description

  • The ERC20 standard requires that burn operations include underflow protection to prevent balances from wrapping around when burning more than the available balance. The _burn() function in Token-0x's base implementation performs unchecked subtractions for both totalSupply and user balances, allowing balances to wrap to type(uint256).max when underflow occurs.


  • The _transfer() function correctly checks for insufficient balances before subtraction, demonstrating that underflow protection was intentionally omitted in _burn().

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0x96c6fd1e))
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, sub(supply, value)) // Line 171: Unchecked underflow
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, sub(accountBalance, value)) // Line 178: Unchecked underflow
}
}

Risk

Likelihood:

  • Any derived contract that allows burning can trigger this when burning more than available balance

  • Malicious actors can deliberately burn to cause underflow conditions

  • Normal operations can accidentally trigger underflow with incorrect burn amounts

Impact:

  • Balance underflow creates maximum uint256 balance, allowing unlimited token transfers

  • Total supply underflow breaks economic model and token valuation

  • Complete loss of token value and protocol functionality

Proof of Concept

The test demonstrates that burning 1 token from a zero balance causes the balance to wrap to type(uint256).max, bypassing any underflow protection that should exist in a secure ERC20 implementation.

// Added to test/Token.t.sol
contract VulnerableToken is ERC20 {
constructor() ERC20("Vuln", "V") {}
function burn(address from, uint256 amount) external { _burn(from, amount); }
}
function test_BurnFromZeroCreatesMaxBalance() public {
VulnerableToken baseToken = new VulnerableToken();
address victim = makeAddr("victim");
// Victim has 0 balance
assertEq(baseToken.balanceOf(victim), 0);
// Burn from zero balance - should revert but doesn't
baseToken.burn(victim, 1);
// Victim now has max balance due to underflow
assertEq(baseToken.balanceOf(victim), type(uint256).max);
}

Recommended Mitigation

Add underflow checks before performing subtractions in the _burn() function. The fix should validate that the balance is sufficient before burning, preventing underflow conditions.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
+ // FIX: Check balance before burning
+ if lt(accountBalance, value) {
+ mstore(0x00, shl(224, 0xe450d38c)) // InsufficientBalance error
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
let supply := sload(supplySlot)
+ // FIX: Check supply underflow (defensive, should never happen if balance check passed)
+ if lt(supply, value) {
+ mstore(0x00, shl(224, 0x35278d12)) // Overflow/underflow error
+ revert(0x00, 0x04)
+ }
+
sstore(supplySlot, sub(supply, value))
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Support

FAQs

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

Give us feedback!