Token-0x

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

_burn Function Lacks Overflow Check, Leading to Manipulation of Total Supply and Balance

Author Revealed upon completion

_burn Function Lacks Overflow Check, Leading to Manipulation of Total Supply and Balance

Description

In the _burn function of an ERC20 token, it is crucial to ensure that the amount being burned does not exceed the current total supply and the account balance to prevent underflow.
However, the function directly uses Yul's sub instruction to perform subtraction without verifying supply >= value and accountBalance >= value.
Since sub(a, b) in Yul silently wraps around to a very large value (i.e., a negative number under modulo 2²⁵⁶) when a < b, this will cause _totalSupply or the user's balance to be incorrectly set to an invalid value close to type(uint256).max, severely disrupting the token's state consistency.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
// ...
let supply := sload(supplySlot)
@> sstore(supplySlot, sub(supply, value)) // No underflow check
// ...
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, sub(accountBalance, value)) // No underflow check
}
}

Risk

Likelihood:

  • When value passed to _burn is greater than the account balance or total supply (e.g., due to logical errors, malicious calls, or external contract integration defects), underflow will be triggered.

  • Since the function is internal, if the upper-level business logic (such as custom burn interfaces) lacks verification, this risk is easily exposed.

Impact:

  • _totalSupply or user balance becomes an extremely large value due to underflow, destroying the token's total supply conservation and ledger correctness.

  • May cause catastrophic errors in other logic dependent on balance or supply (such as dividends, staking, governance), or even lead to fund theft or freezing.

Proof of Concept

  • Add the function test_1_burn_overflow in Token.t.sol as follows:

function test_1_burn_overflow() public {
address account = makeAddr("account");
// Burn 1 token
token.burn(account, 1);
assertEq(token.balanceOf(account), type(uint256).max); // After overflow, wraps around to the maximum value
assertEq(token.totalSupply(), type(uint256).max); // After overflow, wraps around to the maximum value
}
  • Input command: forge test --mt test_1_burn_overflow -vv

Ran 1 test for test/Token.t.sol:TokenTest
[PASS] test_1_burn_overflow() (gas: 46828)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.00ms (385.20µs CPU time)

Recommended Mitigation

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)
+
+ // Check if total supply is sufficient for burning
+ if gt(value, supply) {
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(add(0x00, 4), 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)
+
+ // Check if account balance is sufficient for burning
+ if gt(value, accountBalance) {
+ mstore(0x00, shl(224, 0xe450d38c))
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
+ sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Support

FAQs

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

Give us feedback!