Token-0x

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

Unintentional Minting via Underflow in `_burn()`

Author Revealed upon completion

Unintentional Minting via Underflow in _burn()

Description

The intended behavior of ERC20Internals::_burn() is to permanently remove a specified value of tokens from the account and reduce the global _totalSupply. Under normal conditions, a burn operation must always decrease balances and never create new tokens.

However, the current implementation performs raw subtraction inside Yul without any underflow checks. Since inline assembly bypasses Solidity’s built-in overflow protection, subtracting a value larger than the actual balance or total supply results in a uint256 underflow, causing the storage slot to wrap around to a massive value (≈ 2^256 - x). This behavior mints tokens instead of burning them, breaking ERC20 invariants.

// Root cause (highlighted with @>)
function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
...
let supply := sload(supplySlot)
// @> Underflow here leads to totalSupply wrapping into a huge number
sstore(supplySlot, sub(supply, value))
...
let accountBalance := sload(accountBalanceSlot)
// @> Underflow here increases user balance to near-max uint256
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Risk

Likelihood

  • An underflow occurs whenever a burn amount is larger than the actual balance of the account.

  • Since arithmetic is handled in assembly, Solidity’s safe-math checks do not trigger, making this issue trivial to exploit.

Impact

  • Unbounded minting: Burning more than the balance inadvertently increases the user’s balance to a near-max uint256 value.

  • Economic collapse: Total supply is inflated, tokenomics break, and anyone—even with zero tokens—can mint a massive amount by calling burn.

Overall impact: High

Proof of Concept

function test_burnRevertOnUnderflow() public {
// @audit-high Burning more tokens than balance causes underflow
uint256 amount = 10e18;
address account = makeAddr("account");
token.mint(account, amount);
uint256 balance = token.balanceOf(account);
assertEq(balance, amount);
assertEq(token.totalSupply(), amount);
console.log("Balance before burn attempt:", balance);
// Trigger underflow by burning more than user has
uint256 extraAmountToBurn = 1e18;
token.burn(account, amount + extraAmountToBurn);
balance = token.balanceOf(account);
console.log("Balance after underflow burn attempt:", balance);
vm.expectRevert();
assertEq(balance, 0);
}
Console Output
[PASS] test_burnRevertOnUnderflow()
Logs:
Balance before burn attempt: 10000000000000000000
Balance after underflow burn attempt:
115792089237316195423570985008687907853269984665640564039456584007913129639936

The “after” balance is essentially 2^256 – (requestedBurn - balance), confirming underflow-based minting occurred.

Recommended Mitigation

Add explicit assembly-level checks to ensure both the account balance and total supply are sufficient before performing subtraction. If any condition fails, revert early.

Note: For security-sensitive logic like accounting, avoid using Yul unless absolutely necessary. High-level Solidity already provides safe arithmetic and is much less error-prone.

Patched Version (Assembly Checks Added)
function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
// Revert on zero address
if iszero(account) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}

// --- FIX: Ensure totalSupply >= value ---
++if lt(supply, value) {
//--- add corrsponding error ----
}
// --- FIX: Ensure balance >= value ---
++ if lt(accountBalance, value) {
//--- add corrsponding error ----
}
}

Support

FAQs

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

Give us feedback!