Token-0x

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

_mint and _burn functions do not emit Transfer events, violating ERC20 standard

Author Revealed upon completion

Root + Impact

Description


    • Normal Behavior: The ERC20 standard (EIP-20) mandates that a Transfer event MUST be triggered when tokens are transferred, including when tokens are created (mint) or destroyed (burn). For minting, the _from address is 0x0. For burning, the _to address is 0x0.

    • Specific Issue: The _mint and _burn functions in ERC20Internals.sol update the state (balances and total supply) but fail to emit the required Transfer event.// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Medium: The contract is a base implementation meant to be inherited. Any project using this base and exposing mint/burn functionality will inherit this non-compliance.

Impact:

  • Medium:

    • Broken Integrations: Off-chain indexers (like Etherscan, The Graph) and wallets rely on Transfer events to track token balances and activity. Minted or burned tokens will not be reflected in these services, leading to incorrect balance displays and history.

    • Standard Violation: The token will not be fully ERC20 compliant.Impact 1


Proof of Concept

We verify the issue by calling `mint` and expecting a `Transfer` event using Foundry's `vm.expectEmit`. The test fails because the `_mint` function updates the state (balances) but does not execute the `log3` opcode to emit the event. The same applies to `_burn`.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/ERC20.sol";
contract Token is ERC20 {
constructor() ERC20("Token", "TKN") {}
function mint(address to, uint256 amount) public { _mint(to, amount); }
function burn(address from, uint256 amount) public { _burn(from, amount); }
}
contract MissingEventsTest is Test {
Token token;
address user = address(0x1);
event Transfer(address indexed from, address indexed to, uint256 value);
function setUp() public { token = new Token(); }
function testMintEmitsTransfer() public {
vm.expectEmit(true, true, false, true);
emit Transfer(address(0), user, 100);
token.mint(user, 100); // Fails to emit event
}
}

Recommended Mitigation

Add the missing event logs in the assembly blocks.

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
// ... existing code ...
sstore(accountBalanceSlot, add(accountBalance, value))
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0, account)
}
}
function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
// ... existing code ...
sstore(accountBalanceSlot, sub(accountBalance, value))
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f16function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
// ... existing code ...
sstore(accountBalanceSlot, add(accountBalance, value))
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0, account)
}
}
function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
// ... existing code ...
sstore(accountBalanceSlot, sub(accountBalance, 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!