Token-0x

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

Mint and Burn Do Not Emit Required `Transfer` Events

Author Revealed upon completion

Description:
The ERC20 standard requires Transfer events to be emitted both when tokens are minted (from the zero address) and when they are burned (to the zero address). The Token-0x implementation correctly emits Transfer in _transfer, but does not emit any events in _mint and _burn.
This breaks the ERC20 event semantics: indexers, wallets, and DeFi protocols often rely on Transfer events to track token movements, including minting and burning.

Impact:

  • Block explorers and indexers will not see mint/burn operations, making supply tracking inaccurate.

  • Protocols that rely on Transfer events (e.g., to compute rewards, monitor emissions, or verify supply caps) will behave incorrectly or consider the token non‑compliant.

  • This significantly harms composability and integration with the existing ERC20 ecosystem.

It does not directly allow theft, but it breaks compatibility and can cause serious issues in production integrations.

Proof of Concept:

We use Foundry’s vm.recordLogs() to show that Token-0x fails to emit Transfer on mint, while OpenZeppelin does.

Token using Token-0x ERC20:

// Add this test to Token.t.sol
function test_mintDoesNotEmitTransferEvent() public {
address account = makeAddr("account");
// Start recording logs
vm.recordLogs();
token.mint(account, 100e18);
Vm.Log[] memory entries = vm.getRecordedLogs();
// keccak256("Transfer(address,address,uint256)")
bytes32 transferTopic = keccak256("Transfer(address,address,uint256)");
bool found;
for (uint256 i = 0; i < entries.length; i++) {
if (entries[i].topics.length > 0 && entries[i].topics[0] == transferTopic) {
found = true;
break;
}
}
assertFalse(found, "Token-0x unexpectedly emitted Transfer on mint");
}

Token2 using OpenZeppelin ERC20:

// Add this test to Token2.t.sol
function test_mintEmitsTransferEvent_OZ() public {
address account = makeAddr("account");
vm.recordLogs();
token.mint(account, 100e18);
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 transferTopic = keccak256("Transfer(address,address,uint256)");
bool found;
for (uint256 i = 0; i < entries.length; i++) {
if (entries[i].topics.length > 0 && entries[i].topics[0] == transferTopic) {
found = true;
break;
}
}
assertTrue(found, "OpenZeppelin ERC20 did not emit Transfer on mint");
}

Equivalent tests can be written for _burn (expecting Transfer(account, address(0), value)), with the same result: Token-0x emits no event, OZ does.

Mitigation:

  • Update _mint and _burn to emit Transfer events that conform to the ERC20 standard:

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);
+ let ptr := mload(0x40)
+ mstore(ptr, value)
+ log3(ptr, 0x20,
+ 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
+ 0x0,
+ 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);
+ let ptr := mload(0x40)
+ mstore(ptr, value)
+ log3(ptr, 0x20,
+ 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
+ account,
+ 0x0
+ )
}
}
  • Alternatively, implement mint/burn in Solidity and let the compiler emit the event, then optimize only where necessary.

Support

FAQs

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

Give us feedback!