Token-0x

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

Missing Transfer Events in _mint() and _burn() Breaks ERC20 Compliance

Author Revealed upon completion

Root + Impact

Description

According to the ERC-20 standard, a Transfer event MUST be emitted when tokens are created (minted) or destroyed (burned)

// src/helpers/ERC20Internals.sol
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))
@> // Missing: emit Transfer(address(0), account, value)
}
}
function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
// ... validation and balance updates ...
sstore(accountBalanceSlot, sub(accountBalance, value))
@> // Missing: emit Transfer(account, address(0), value)
}
}

Risk

Likelihood:

  • Every call to _mint() or _burn() triggers this issue - it occurs 100% of the time these functions are used

  • Any protocol or token that inherits this ERC20 implementation and uses minting/burning functionality will be affected

Impact:

  • Off-chain services (block explorers, wallets, analytics platforms) will not track minted/burned tokens, leading to incorrect balance displays and supply calculations

  • DeFi protocols relying on Transfer event indexing will fail to detect token supply changes, potentially breaking integrations

  • The token becomes non-compliant with ERC-20 standard, which explicitly requires Transfer events for minting and burning operations

Proof of Concept

just call mint or burn function to see the issue , no event generated

Recommended Mitigation

Emit the Transfer event at the end of both _mint() and _burn() functions using the log3 opcode, following the same pattern already implemented in the _transfer() function.

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

Support

FAQs

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

Give us feedback!