Token-0x

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

`ERC20Internals::_burn` subtracts the user's balance without checking if it is sufficient, causing arithmetic underflow

Author Revealed upon completion

ERC20Internals::_burn subtracts the user's balance without checking if it is sufficient, causing arithmetic underflow

Description

  • In an ERC20, _burn must check that the account has enough balance before subtracting, reverting with a clear error if balance < amount.

  • In this implementation, _burn subtracts the balance directly without any prior check, causing automatic underflow.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
(... )
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> // missing user balance check
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Risk

Likelihood: Medium

  • The error occurs whenever _burn receives an amount greater than the account's actual balance.

  • Any internal or external call using _burn without prior balance validation will immediately cause an underflow.

Impact: High

  • Underflow in Yul does not revert and causes wrap-around, leaving the account with a massively inflated balance.

  • It also incorrectly alters totalSupply, completely breaking the basic ERC20 invariants and affecting system integrity.

Proof of Concept

This test demonstrates that _burn does not check if the user has enough balance: when more is burned than owned, Yul underflow inflates the user's balance to a huge value instead of reverting.

function test_Burn_Doesnt_Check_User_Balance() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// Give 10 tokens to each account
token.mint(alice, 10 ether);
token.mint(bob, 10 ether);
// Try to burn 15 tokens from alice (she only has 10)
// → this should revert, but in this implementation it DOES NOT
token.burn(alice, 15 ether);
// Underflow causes alice's balance to wrap around and become huge
assertGt(token.balanceOf(alice), 10 ether);
console.log("alice token balance:", token.balanceOf(alice));
}
Logs:
alice token balance: 115792089237316195423570985008687907853269984665640564039452584007913129639936

Recommended Mitigation

Add a prior check to validate that the account has enough balance before performing the subtraction. If value exceeds the available balance, the function should revert with the appropriate error to prevent underflow and maintain ERC20 invariants.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
(... )
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
+ 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)
+ }
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Support

FAQs

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

Give us feedback!