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))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, sub(accountBalance, value))
}
}
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");
token.mint(account, 100e18);
assertEq(token.balanceOf(account), 100e18);
assertEq(token.totalSupply(), 100e18);
token.burn(account, 200e18);
assertEq(token.balanceOf(account), type(uint256).max - 100e18 + 1);
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))
}
}