Token-0x

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

Missing Balance Underflow Check

Author Revealed upon completion

Description

  • The ERC20 standard requires that burning tokens should fail if the account doesn't have sufficient balance. OpenZeppelin's implementation reverts with ERC20InsufficientBalance error.

  • Token-0x's _burn function uses assembly sub(accountBalance, value) without checking if accountBalance >= value. In assembly, subtraction of a larger number from a smaller one wraps around (underflows) to a very large number (~2^256).

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))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
// @> NO CHECK: if accountBalance < value, this underflows!
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Risk

Likelihood:

  • Any contract inheriting from ERC20 that exposes _burn can trigger this

  • The test contract Token.sol exposes burn() as public, making this directly exploitable

Impact:

  • An attacker with 0 tokens can burn 1 token and receive ~2^256 tokens

  • Complete destruction of token economics

  • Unlimited token supply in attacker's hands

Proof of Concept

function test_burn_underflow_exploit() public {
// Alice has 50 tokens
token.mint(alice, 50e18);
// Burn 100 tokens (more than balance)
// This should revert but doesn't!
token.burn(alice, 100e18);
// Alice's balance has underflowed to ~2^256
uint256 balance = token.balanceOf(alice);
assertGt(balance, 50e18); // Balance is now astronomically large
// balance ≈ 115792089237316195423570985008687907853269984665640564039407584007913129639936
}

Recommended Mitigation

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)
+ // Check sufficient balance before burning
+ if lt(accountBalance, value) {
+ mstore(0x00, shl(224, 0xe450d38c)) // ERC20InsufficientBalance selector
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
let supply := sload(supplySlot)
sstore(supplySlot, sub(supply, value))
sstore(accountBalanceSlot, sub(accountBalance, value))
+
+ // Emit Transfer event to address(0)
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, account, 0)
}
}

Support

FAQs

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

Give us feedback!