Token-0x

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

Missing Transfer Event in _mint() Function Violates ERC20 Standard and Breaks Protocol Integrations

Author Revealed upon completion

Root + Impact

Description

  • According to EIP-20, the _mint() function must emit a Transfer event with the from address set to address(0) to signal token creation. This allows external systems to track supply changes and update user balances in real-time.

  • The assembly implementation updates storage slots for balance and total supply but does not include the log3 instruction to emit the Transfer event. This creates a silent minting operation where tokens appear in balances without any event trail, breaking compatibility with all ERC20-dependent infrastructure.

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: Transfer event emission
}
}

Risk

Likelihood:

  • Every minting operation triggers this vulnerability immediately upon contract deployment, affecting all token creation events from the first transaction.

  • DeFi protocols perform automated compatibility checks during token listing, and missing Transfer events cause instant rejection from major platforms like Uniswap, Aave, and Compound.

Impact:

  • Complete incompatibility with decentralized exchanges and lending protocols prevents the token from being traded or used as collateral, eliminating core DeFi functionality.

  • Wallet interfaces and block explorers cannot track token supply or holder balances accurately, creating user confusion and preventing adoption by displaying incorrect or zero balances despite successful minting.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {Token} from "./Token.sol";
contract MintEventTest is Test {
Token public token;
address public account;
event Transfer(address indexed from, address indexed to, uint256 value);
function setUp() public {
token = new Token();
account = makeAddr("account");
}
function test_MintMissingTransferEvent() public {
// According to ERC20, mint should emit Transfer(address(0), account, amount)
// This test demonstrates the event is NOT emitted
uint256 amount = 100e18;
// Record all events
vm.recordLogs();
// Mint tokens
token.mint(account, amount);
// Get recorded logs
Vm.Log[] memory logs = vm.getRecordedLogs();
console.log("Number of events emitted during mint:", logs.length);
// Verify no Transfer event was emitted
assertEq(logs.length, 0, "Expected zero events but got some");
// Verify balance was updated (so mint worked)
assertEq(token.balanceOf(account), amount);
console.log("Vulnerability confirmed: Mint succeeded but no Transfer event emitted");
console.log("This breaks ERC20 compliance and DeFi integrations");
}
}

Result:

forge test --match-path test/MintEvent.t.sol -vvv
[⠆] Compiling...
No files changed, compilation skipped
Ran 1 test for test/MintEvent.t.sol:MintEventTest
[PASS] test_MintMissingTransferEvent() (gas: 63531)
Logs:
Number of events emitted during mint: 0
Vulnerability confirmed: Mint succeeded but no Transfer event emitted
This breaks ERC20 compliance and DeFi integrations
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 688.50µs (116.68µs CPU time)
Ran 1 test suite in 7.63ms (688.50µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Add the Transfer event emission at the end of the mint function to comply with ERC20 standard requirements. The event signals token creation by using address(0) as the sender.

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, 0x00, account)
}
}

Support

FAQs

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

Give us feedback!