Token-0x

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

Insecure Internal Burn Design Allows Arbitrary Token Destruction and Supply Corruption

Author Revealed upon completion

[H-02] Insecure Internal Burn Design Allows Arbitrary Token Destruction and Supply Corruption

Description

The ERC20 core implementation contains a critical design weakness in the internal burning logic.
The _burn() function inside ERC20Internals.sol allows unrestricted destruction of user balances and total supply without any built-in authorization or safety checks.

Although _burn() is marked internal, this contract is built to be inherited. Any child contract can expose this functionality through a public or external wrapper, unintentionally or maliciously.

In addition, the function performs raw arithmetic inside inline assembly without underflow protection. This enables balance and total supply corruption if the burn amount exceeds the user’s balance or the current total supply.

// src/helpers/ERC20Internals.sol
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 supplySlot := _totalSupply.slot
let balanceSlot := _balances.slot
let supply := sload(supplySlot)
sstore(supplySlot, sub(supply, value))
let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Risk

Likelihood: High

  • The contract is designed for inheritance.

  • There are no internal access restrictions.

  • Arithmetic is performed without safety checks.

Impact: High

  • Arbitrary destruction of user balances.

  • Total supply corruption via underflow.

  • Permanent loss of funds

Proof of Concept

  • The following PoC shows how a malicious inheriting contract can expose the burn functionality publicly and destroy tokens from any address.

contract MaliciousToken is ERC20 {
constructor() ERC20("Evil", "EVL") {}
function burnAnyone(address victim, uint256 amount) public {
_burn(victim, amount);
}
}
function test_PoC_UnrestrictedBurn() public {
MaliciousToken evil = new MaliciousToken();
address victim = makeAddr("victim");
evil.mintAnyone(victim, 100e18);
// Attacker destroys victim's tokens
evil.burnAnyone(victim, 100e18);
assertEq(evil.balanceOf(victim), 0);
}
forge test --match-test test_PoC_UnrestrictedBurn -vvv
  • The test passes, confirming arbitrary burning is possible when inherited.

Recommended Mitigation

  • Enforce strict authorization and balance checks inside _burn().

function _burn(address account, uint256 value) internal {
+ require(msg.sender == account, "Not authorized");
+ require(balanceOf(account) >= value, "Insufficient balance");
let supply := sload(supplySlot)
sstore(supplySlot, sub(supply, value))
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!