Token-0x

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

Missing Transfer Event in _burn() Function Violates ERC20 Standard and Breaks Protocol Integrations

Author Revealed upon completion

Root + Impact

Description

  • According to EIP-20, the _burn() function must emit a Transfer event with the to address set to address(0) to signal token destruction. This allows external systems to track supply decreases and update circulating supply metrics in real-time.

  • The assembly implementation updates storage slots for balance and total supply but does not include the log3 instruction to emit the Transfer event. This creates a silent burning operation where tokens disappear from balances without any event trail, breaking compatibility with all ERC20-dependent infrastructure that monitors deflationary mechanisms.

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))
@> // Missing: Transfer event emission
}
}

Risk

Likelihood:

  • Every burning operation triggers this vulnerability whenever tokens are destroyed, affecting all deflationary mechanisms and buyback-and-burn programs from deployment onwards.

  • Analytics platforms and token trackers rely on Transfer events to calculate circulating supply, and missing burn events cause permanent inaccuracies in supply metrics displayed across all aggregators.

Impact:

  • DeFi protocols using the token for governance or collateral cannot accurately track voting power or liquidation thresholds, as circulating supply calculations diverge from actual on-chain state.

  • Market cap calculations become permanently incorrect on aggregators like CoinGecko and CoinMarketCap, displaying inflated supply numbers that misrepresent token value and mislead investors.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {Token} from "./Token.sol";
contract BurnEventTest is Test {
Token public token;
address public account;
event Transfer(address indexed from, address indexed to, uint256 value);
function setUp() public {
token = new Token();
account = makeAddr("account");
}
function test_BurnMissingTransferEvent() public {
// Mint tokens first
uint256 amount = 100e18;
token.mint(account, amount);
// Record events during burn
vm.recordLogs();
// Burn tokens
token.burn(account, amount);
// Get recorded logs
Vm.Log[] memory logs = vm.getRecordedLogs();
console.log("Number of events emitted during burn:", logs.length);
// Verify no Transfer event was emitted
assertEq(logs.length, 0, "Expected zero events but got some");
// Verify balance was updated (so burn worked)
assertEq(token.balanceOf(account), 0);
console.log("Vulnerability confirmed: Burn succeeded but no Transfer event emitted");
console.log("ERC20 requires Transfer(account, address(0), amount) on burn");
}
}

Result:

forge test --match-path test/BurnEvent.t.sol -vvv
[⠆] Compiling...
No files changed, compilation skipped
Ran 1 test for test/BurnEvent.t.sol:BurnEventTest
[PASS] test_BurnMissingTransferEvent() (gas: 48408)
Logs:
Number of events emitted during burn: 0
Vulnerability confirmed: Burn succeeded but no Transfer event emitted
ERC20 requires Transfer(account, address(0), amount) on burn
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 319.55µs (82.60µs CPU time)
Ran 1 test suite in 8.17ms (319.55µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

dd the Transfer event emission at the end of the burn function to comply with ERC20 standard requirements. The event signals token destruction by using address(0) as the recipient.

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))
+
+ // Emit Transfer(account, address(0), value)
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, account, 0x00)
}
}

Support

FAQs

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

Give us feedback!