Token-0x

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

[H-02] Unchecked Arithmetic Underflow in _burn Function (Missing Underflow Protection in Yul Assembly)

Author Revealed upon completion

Description

The _burn function performs two subtraction operations in Yul assembly without underflow checks: decreasing _totalSupply and the sender's balance. Since Solidity 0.8.x's automatic underflow protection does not apply to inline assembly, an attacker can burn more tokens than available, causing integer underflow and corrupting the token's total supply and account balances.

Root Cause

The function uses Yul assembly for gas optimization but omits mandatory underflow checks for subtraction operations. The vulnerability occurs because the sub opcode in Yul silently wraps on underflow, unlike Solidity's checked arithmetic.

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

Risk

Likelihood:

  • Any address with burning privileges (or any user if _burn is exposed) can intentionally cause underflow by burning more tokens than the current balance or total supply.

  • The attack requires only a single transaction with a burn amount larger than the current balance, which is trivial to execute.


Impact:

  • Inflation Attack: Account balances and total supply can underflow to near-maximum values, creating an enormous amount of tokens out of thin air.

  • Total Supply Corruption: _totalSupply becomes mathematically incorrect, breaking all supply-dependent calculations.

  • Protocol-Wide Disruption: The token's economic model is destroyed, as the circulating supply can be artificially inflated to extreme values.

Proof of Concept

function test_burn_totalSupply_underflow() public {
uint256 mintAmount = 500;
token.mint(alice, mintAmount);
uint256 burnAmount = 1000;
token.burn(alice, burnAmount);
uint256 newSupply = token.totalSupply();
uint256 newBalance = token.balanceOf(alice);
console.log("totalSupply after underflow:", newSupply);
console.log("Balance Alice after underflow:", newBalance);
uint256 max = type(uint256).max;
uint256 expected = max - 499;
assertEq(newSupply, expected, "Underflow totalSupply Valid");
assertEq(newBalance, expected, "Underflow balance Valid");
}

Result :

Ran 1 test for test/Token.t.sol:TokenTest
[PASS] test_burn_totalSupply_underflow() (gas: 62515)

Logs:
totalSupply afetr underflow: 115792089237316195423570985008687907853269984665640564039457584007913129639436
Balance Alice after underflow: 115792089237316195423570985008687907853269984665640564039457584007913129639436

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.56ms (757.08µs CPU time)



Recommended Mitigation

Add explicit underflow checks before both subtraction operations in the _burn function

Alternative Solution: Create reusable safe arithmetic functions (e.g., safeAdd and safeSub) to avoid code duplication and ensure consistent underflow/overflow protection across the entire contract.

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)
+ if gt(value, supply) {
+ //Panic: arithmetic underflow (error code 0x11)
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(0x04, 0x11)
+ revert(0x00, 0x24)
+ }
sstore(supplySlot, sub(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
+ if gt(value, accountBalance) {
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(0x04, 0x11)
+ revert(0x00, 0x24)
+ }
sstore(accountBalanceSlot, sub(accountBalance, value))
}

Support

FAQs

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

Give us feedback!