Token-0x

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

Missing `Transfer` Event in `_burn` Violates ERC20 Standard and Hides Supply Reduction

Author Revealed upon completion

Root + Impact

Description

  • The ERC-20 specification (EIP-20) requires that every burn operation emit a Transfer event with the sender as the from address and the zero address (address(0)) as the to address. This serves as the canonical on-chain signal that tokens were destroyed and the token supply was reduced.entences

  • The _burn function reduces the account balance and total supply via inline assembly but does not emit the corresponding event. As a result, external indexers, block explorers, and monitoring systems cannot detect supply reductions or token destruction events.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
// ...
sstore(accountBalanceSlot, sub(accountBalance, value))
@> // Missing log3 Transfer(account, address(0), value)
}
}

Risk

Likelihood:

  • Every _burn execution changes state but emits zero events.

  • Any function calling _burn (deflationary burns, fee burns, admin burns) exhibits this 100% of the time.

Impact:

  • Invisible supply reduction: Explorers and analytics show outdated totalSupply values that don't match actual contract state.

  • Broken protocol logic: Deflationary mechanisms, fee trackers, or burn-triggered events fail to execute.

  • ERC‑20 non-compliance: Prevents integration with strict EIP‑20 protocols and exchange listings.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {ERC20} from "../src/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor() ERC20("Test", "TST") {}
function mint(address to, uint256 amount) public { _mint(to, amount); }
function burn(address from, uint256 amount) public { _burn(from, amount); }
}
contract BurnEventTest is Test {
ERC20Mock public token;
address public alice = makeAddr("alice");
function setUp() public {
token = new ERC20Mock();
token.mint(alice, 1000); // підготовка балансу
}
function test_burn_missing_event() public {
vm.recordLogs();
token.burn(alice, 500);
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(token.balanceOf(alice), 500);
// головне — відсутність Transfer event
assertEq(entries.length, 0, "Critical: Transfer event missing on burn");
}
}

Run:

forge test --match-test test_burn_missing_event -vvvv

Output:

[PASS] test_burn_missing_event() (gas: 24401)
├─ [10870] ERC20Mock::burn(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 500)
│ └─ ← [Stop]
├─ [0] VM::getRecordedLogs() [staticcall]
│ └─ ← [Return] []


Recommended Mitigation

// src/helpers/ERC20Internals.sol
// In _burn function
sstore(accountBalanceSlot, sub(accountBalance, value))
+ // Emit Transfer(account, address(0), value)
+ mstore(0x00, value) // Store 'value' in memory for the non-indexed data
+ log3(
+ 0x00,
+ 0x20,
+ 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, // keccak256("Transfer(address,address,uint256)")
+ account,
+ 0
+ )
}
}

Support

FAQs

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

Give us feedback!