Token-0x

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

High Severity : Unchecked Underflow in _burn Function

Author Revealed upon completion

Unchecked Underflow in _burn Function

Description

  • The _burn function is an internal utility intended to reduce an account's token balance and the total supply by a specified value. Ensuring the token's state remains consistent with ERC20 standarts.

  • The function is missing a check to ensure value does not exceed the account's current balance which allows unchecked subtraction to underflow: if value > balance, the balance wraps around a massive value (2^256 - difference).

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 Check; supply decreases even if value > balance
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, sub(accountBalance, value)) //<@ No check; Underflowsif value > accountBalance
}
}

Risk

Likelihood:

  • During inheritance in extensible ERC20 contracts where _burn is exposed via public wrappers or proxies.

  • In batch operations or user-facing burn functions that pass not validated inputs from external sources.

Impact:

  • Arbitary balance inflation allows attackers to drain liquidity pools or manipulate shares, potentially leading to major losses.

  • Brakes the core ERC20 invariants, causing failures in integrations.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract BurnPoCTest is ERC20Internals {
function setup() external {
_mint(address(this), 10 ether);
}
function exploit() external {
_burn(address(this), 11 ether); // Triggers underflow: balance inflates, supply desyncs
}
function check() external view returns (uint256 balance, uint256 supply) {
balance = _balanceOf(address(this)); // Returns ~2^256 -1
supply = totalSupply_(); // Returns ~9 ether
}
}

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
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))
// Check balance first
+ if it(accountBalance, value) {
+ mstore(0x00, 0x12345678)
+ mstore(add(0x00, 0x04), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
}
// Safe subtractions
+ let newBalance := sub(accountBalance, value)
+ let newSUpply := sub(supply,value)
+ sstore(accountBalanceSlot, newBalance)
+ sstore(supplySlot, newSupply)
}
}

Support

FAQs

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

Give us feedback!