Token-0x

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

Missing Transfer Event in _mint() Function

Author Revealed upon completion

Root + Impact

Description

  • The ERC20 standard requires that all balance changes emit Transfer events, including mint operations which should emit Transfer(address(0), account, value). The _mint() function in Token-0x's base implementation updates balances and total supply but completely omits the required Transfer event emission .


    The _transfer() function correctly emits Transfer events using log3, proving this is an omission rather than a design choice

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)) // Updates totalSupply
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, add(accountBalance, value)) // Updates balance
// @> MISSING: No Transfer event emitted here
}
}

Risk

Likelihood:

  • Any derived contract that calls _mint() will trigger this issue

  • All token deployments using Token-0x as base will exhibit this behavior

  • Every mint operation in production will lack event emission

Impact:

  • Off-chain indexers cannot track mint operations, breaking blockchain explorers

  • DEX integrations fail as they rely on events to detect supply changes

  • Wallets show incorrect balances after minting

  • Audit trail loss - no forensic record of token creation

Proof of Concept

// Added to test/Token.t.sol
// Added Vm alongside Test at import
// Add this at the bottom of the contract
contract VulnerableToken is ERC20 {
constructor() ERC20("Vuln", "V") {}
function mint(address to, uint256 amount) external { _mint(to, amount); }
}
function test_BaseMintHasNoTransferEvent() public {
// Create contract that directly exposes base _mint
VulnerableToken baseToken = new VulnerableToken();
vm.recordLogs();
baseToken.mint(makeAddr("recipient"), 100e18);
Vm.Log[] memory logs = vm.getRecordedLogs();
assertEq(logs.length, 0, "Base _mint emits no Transfer event");
}

Recommended Mitigation

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))
+ // FIX: Add Transfer event emission
+ 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!