Token-0x

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

Missing Balance Check in `ERC20Internals::_burn` Function, causing underflow inflating balance and total supply.

Author Revealed upon completion

Root + Impact

Description

Description: The _burn function does not verify that the account has sufficient balance before burning tokens. This allows burning more tokens than the account holds, which can lead to:

  • Underflow in balance calculation - While Solidity 0.8+ will revert on underflow, the explicit check should be present for clarity and to emit proper errors

  • Invariant violation - Breaks the critical ERC20 invariant: sum(userBalances) == totalSupply

  • State corruption - If balance underflows, the totalSupply will be reduced but the balance becomes corrupted

// Root cause in the codebase with @> marks to highlight the relevant section
function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0xec442f05))
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, add(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
@> let accountBalanceSlot := keccak256(ptr, 0x40)
@> let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, add(accountBalance, value))
}
}

Risk

Likelihood:

  • Direct call to the function with an incorrect amount.

  • Any access restrictions would not prevent this situation, since the internal function does not mitigate such an error.

Impact:

  • Users can burn more tokens than they own, corrupting the token's accounting

  • Protocols relying on the invariant sum(balances) == totalSupply will break

  • Total supply can become less than the sum of all balances

  • In Yul assembly, sub() will wrap around on underflow (not revert), potentially causing silent corruption

Proof of Concept

This test demonstrates the overflow in the `_burn` function.

function test_burnMoreThanBalance() public {
uint256 amount = 100e18;
address account = makeAddr("account");
token.mint(account, amount);
uint256 balance = token.balanceOf(account);
assertEq(balance, amount);
assertEq(token.totalSupply(), amount);
token.burn(account, amount + 1);
balance = token.balanceOf(account);
assertEq(balance, type(uint256).max);
}

Recommended Mitigation

Add a balance check in the _burn function before burning tokens to prevent 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))
- mstore(ptr, account)
- mstore(add(ptr, 0x20), balanceSlot)
- let accountBalanceSlot := keccak256(ptr, 0x40)
- let accountBalance := sload(accountBalanceSlot)
+
+ // Load account balance first
+ mstore(ptr, account)
+ mstore(add(ptr, 0x20), balanceSlot)
+ let accountBalanceSlot := keccak256(ptr, 0x40)
+ let accountBalance := sload(accountBalanceSlot)
+
+ // Check balance is sufficient
+ 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!