Token-0x

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

Missing Transfer Event Emission on _mint() and _burn()

Author Revealed upon completion

_mint() and _burn() do not include event emission.

Description

Expected Behavior
According to the ERC-20 standard, every change in balance for a user — including minting and burning — must emit a Transfer event, with

Mint → Transfer(address(0), to, amount)

Burn → Transfer(from, address(0), amount)

This ensures:

  1. Indexers

  2. Wallets show correct balances

  3. Block explorers can reconstruct token history

  4. DeFi protocols can detect supply changes

Actual Behavior / Issue

The current implementation of _mint() and _burn() performs balance and supply updates without emitting Transfer events.This breaks ERC-20 compliance and leads to:
Invisible minting and burning activity
Incorrect token charts on explorers

Broken integration with DEXes, bridges, staking contracts, etc.


Risk :

Likelihood:

  • Occurs every single time someone mints or burns.

  • Guaranteed to break DeFi integrations relying on logs.

  • Deterministic and reproducible (confirmed by Foundry tests).

Impact:

  1. Wallets cannot show correct balances

  2. Exchanges cannot track deposits/withdrawals

  3. Indexers cannot reconstruct supply

  4. DeFi protocols relying on events will malfunction

This is a breaking ERC-20 compliance issue.

Proof of Concept

Same root cause for _burn.

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0xec442f05))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
// @> Missing: log Transfer(address(0), account, value)
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
let supply := sload(supplySlot)
sstore(supplySlot, add(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, add(accountBalance, value))
}
}

##Add this test confirming the bug:

function test_mintDoesNotEmitTransfer() public {
vm.recordLogs();
token.mintPublic(address(0xBEEF), 100);
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries.length, 0, "Mint did NOT emit Transfer");
}
function test_burnDoesNotEmitTransfer() public {
token.mintPublic(address(this), 200);
vm.recordLogs();
token.burnPublic(address(this), 50);
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries.length, 0, "Burn did NOT emit Transfer");
}

##Test Log

Ran 2 tests for test/Issue1_MissingTransferEvents.t.sol:Issue1_MissingTransferEventsTest
[PASS] test_burnDoesNotEmitTransfer() (gas: 57758)
Traces:
[57758] Issue1_MissingTransferEventsTest::test_burnDoesNotEmitTransfer()
├─ [45071] TestToken::mintPublic(Issue1_MissingTransferEventsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 200)
│ └─ ← [Stop]
├─ [0] VM::recordLogs()
│ └─ ← [Return]
├─ [1248] TestToken::burnPublic(Issue1_MissingTransferEventsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 50)
│ └─ ← [Stop]
├─ [0] VM::getRecordedLogs()
│ └─ ← [Return] []
├─ [0] VM::assertFalse(false, "burn SHOULD NOT have emitted Transfer in current implementation (PoC)") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
[PASS] test_mintDoesNotEmitTransfer() (gas: 55553)
Traces:
[55553] Issue1_MissingTransferEventsTest::test_mintDoesNotEmitTransfer()
├─ [0] VM::recordLogs()
│ └─ ← [Return]
├─ [45071] TestToken::mintPublic(0x000000000000000000000000000000000000bEEF, 100)
│ └─ ← [Stop]
├─ [0] VM::getRecordedLogs()
│ └─ ← [Return] []
├─ [0] VM::assertFalse(false, "mint SHOULD NOT have emitted Transfer in current implementation (PoC)") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.83ms (7.80ms CPU time)
Ran 1 test suite in 43.68ms (12.83ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

Recommended Mitigation

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
...
}
+ // Emit Transfer(address(0), account, value)
+ let ptr := mload(0x40)
+ mstore(ptr, value)
+ log3(ptr, 0x20,
+ 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
+ 0x00,
+ account
+ )
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
//and for burn
function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
...
}
+ // Emit Transfer(account, address(0), value)
+ let ptr := mload(0x40)
+ mstore(ptr, value)
+ log3(ptr, 0x20,
+ 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
+ account,
+ 0x00
+ )
let ptr2 := mload(0x40)
...

Support

FAQs

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

Give us feedback!