_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:
Indexers
Wallets show correct balances
Block explorers can reconstruct token history
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:
Wallets cannot show correct balances
Exchanges cannot track deposits/withdrawals
Indexers cannot reconstruct supply
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)
}
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)
...