Token-0x

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

`_burn` Function Lacks Balance Validation, Causing Integer Underflow and Token Creation

Author Revealed upon completion

Description

In a standard ERC20 implementation, the _burn function should verify that the account has sufficient balance before burning tokens. If the burn amount exceeds the account's balance, the function should revert with an appropriate error.

The _burn function in ERC20Internals.sol does not validate that the account has sufficient balance before performing the subtraction. Due to Solidity 0.8.x's unchecked behavior inside inline assembly blocks, the subtraction underflows silently, resulting in the account receiving an astronomically large balance (close to type(uint256).max).

// @> Root cause in ERC20Internals.sol::_burn (lines 158-180)
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)) // @> No overflow check - can underflow!
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, sub(accountBalance, value)) // @> No balance check - underflows!
}
}

Risk

Likelihood: Medium

  • Requires a derived contract to expose _burn functionality (as it's internal)

  • When exposed, any authorized caller can burn more than an account's balance

  • Admin/governance functions that call _burn may inadvertently trigger this

Impact: Critical

  • Account balance underflows to type(uint256).max - value + balance

  • Total supply also underflows, completely breaking token accounting

  • Attacker can gain near-infinite token balance

  • Token economics and all integrations are destroyed

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Token} from "./Token.sol";
contract BurnUnderflowExploit is Test {
Token public token;
function setUp() public {
token = new Token();
}
function test_Burn_Underflow() public {
address alice = makeAddr("alice");
// Mint 100 tokens
token.mint(alice, 100e18);
assertEq(token.balanceOf(alice), 100e18);
// Burn 150 tokens (more than balance) - should revert but doesn't!
token.burn(alice, 150e18);
// Balance underflows to max uint256 - 50e18
uint256 balance = token.balanceOf(alice);
console.log("Balance after burning 150e18 from 100e18:", balance);
// Balance is now approximately 2^256 - 50e18
assertGt(balance, type(uint256).max - 100e18);
// Total supply also underflowed
console.log("Total supply:", token.totalSupply());
}
}

Test Output:

[PASS] test_Burn_Underflow() (gas: 63260)
Logs:
Balance after burning 150e18 from 100e18: 115792089237316195423570985008687907853269984665640564039407584007913129639936

Recommended Mitigation

Add a balance check before performing the subtraction:

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 burn
+ if lt(accountBalance, value) {
+ // ERC20InsufficientBalance(account, accountBalance, value)
+ mstore(0x00, shl(224, 0xe450d38c))
+ 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))
- mstore(ptr, account)
- mstore(add(ptr, 0x20), balanceSlot)
-
- let accountBalanceSlot := keccak256(ptr, 0x40)
- let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, sub(accountBalance, value))
+
+ // Emit Transfer event to address(0) per ERC20 standard
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, account, 0x00)
}
}

Support

FAQs

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

Give us feedback!