Token-0x

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

Integer Underflow and Missing Balance Validation in `_burn` Function

Author Revealed upon completion

Root + Impact

The _burn function does not validate that the account has sufficient balance before burning, and uses unchecked subtraction that can silently underflow, allowing burning more tokens than owned and corrupting accounting.

Description

  • The _burn function, does not check if accountBalance >= value before burning

  • Uses Yul's sub() which silently underflows (wraps to type(uint256).max - difference)

  • This allows burning more tokens than an account owns, causing both the balance and totalSupply to underflow.

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 balance check, no underflow check!
}
}

Risk

Likelihood:

  • Depends on how implementing contracts expose burn functionality

  • Could be triggered by buggy external contracts or malicious actors

  • No access control shown in base implementation

Impact:

  • Account balance can underflow to near type(uint256).max

  • totalSupply becomes incorrect

  • Token accounting completely broken

Proof of Concept

function test_burnUnderflow() public {
address account = makeAddr("account");
// Account has 100 tokens
token.mint(account, 100e18);
assertEq(token.balanceOf(account), 100e18);
assertEq(token.totalSupply(), 100e18);
// Burn 200 tokens (more than balance)
token.burn(account, 200e18);
// Balance has underflowed
assertEq(token.balanceOf(account), type(uint256).max - 100e18 + 1);
// totalSupply has also underflowed
assertEq(token.totalSupply(), type(uint256).max - 100e18 + 1);
}

Recommended Mitigation

Add balance validation and underflow checks before performing the subtraction.

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
+ // First check account balance
+ mstore(ptr, account)
+ mstore(add(ptr, 0x20), balanceSlot)
+ let accountBalanceSlot := keccak256(ptr, 0x40)
+ let accountBalance := sload(accountBalanceSlot)
+
+ // Revert if insufficient balance
+ if lt(accountBalance, value) {
+ mstore(0x00, shl(224, 0xe450d38c)) // ERC20InsufficientBalance
+ 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))
- mstore(ptr, account)
- mstore(add(ptr, 0x20), balanceSlot)
-
- let accountBalanceSlot := keccak256(ptr, 0x40)
- let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Support

FAQs

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

Give us feedback!