Token-0x

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

Lack of `Transfer` Events in `mint` and `burn` Operations Causes Non-Standard Behavior for a Functional `ERC20` Token

Author Revealed upon completion

Lack of Transfer Events in mint and burn Operations Causes Non-Standard Behavior for a Functional ERC20 Token

Description

  • In a standard ERC20, after executing the logic of mint and burn, a Transfer event must be emitted from or to address(0) to correctly reflect the operation.

  • In this contract, although the logic for mint and burn is executed, no Transfer event is emitted, which prevents these operations from being detected by tools that rely on ERC20 events.

function _mint(address account, uint256 value) internal {
(... )
sstore(accountBalanceSlot, add(accountBalance, value))
@> // missing event
}
function _burn(address account, uint256 value) internal {
(... )
sstore(accountBalanceSlot, sub(accountBalance, value))
@> // missing event
}

Risk

Likelihood: Medium

  • This occurs every time the contract or a child contract performs mint or burn, since these operations do not emit the expected event.

  • Any off-chain tool that tries to read the supply or token history based on events will immediately fail.

Impact: Low

  • The operations are correct on-chain, but tools that depend on events will not detect mints or burns.

  • Protocols inheriting this implementation may experience unexpected behavior or incomplete data.

Proof of Concept

This test validates that the mint and burn functions correctly update balances and totalSupply, but do not emit any Transfer event.
All logs are recorded during execution and then checked to ensure none contain the Transfer topic, demonstrating that the contract does not follow the standard behavior expected by the ERC20 ecosystem.

function test_Mint_Burn_Dont_Emit_Transfer_Event() public {
address alice = makeAddr("alice");
uint256 amount = 100 ether;
// Transfer topic according to ERC20
bytes32 transferTopic = keccak256("Transfer(address,address,uint256)");
// Start recording all emitted events
vm.recordLogs();
// --- MINT ---
token.mint(alice, amount);
// Check that state is updated correctly
assertEq(token.balanceOf(alice), amount);
assertEq(token.totalSupply(), amount);
// --- BURN ---
uint256 burnAmount = 50 ether;
token.burn(alice, burnAmount);
// Check that burn also updates state correctly
assertEq(token.balanceOf(alice), amount - burnAmount);
assertEq(token.totalSupply(), amount - 50 ether);
// Retrieve all logs generated during mint and burn
Vm.Log[] memory entries = vm.getRecordedLogs();
// Check that none of the logs is a Transfer (neither main nor secondary topic)
for (uint256 i = 0; i < entries.length; i++) {
assertTrue(
entries[i].topics[0] != transferTopic,
"A Transfer was emitted when it should not have been"
);
assertTrue(
entries[i].topics[1] != transferTopic,
"A Transfer was emitted in a secondary topic"
);
}
}
[PASS] test_Mint_Burn_Dont_Emit_Transfer_Event() (gas: 64611)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.70ms (4.00ms CPU time)

Recommended Mitigation

Add emission of the Transfer event after completing the logic of mint and burn, using log3 with address(0) as sender or receiver as appropriate. This will align the contract's behavior with widely used ERC20 implementations and ensure compatibility with off-chain tools.

function _mint(address account, uint256 value) internal {
(... )
sstore(accountBalanceSlot, add(accountBalance, value))
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x0, account)
}
}
function _burn(address account, uint256 value) internal {
(... )
sstore(accountBalanceSlot, sub(accountBalance, value))
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, account,0x00)
}
}

Support

FAQs

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

Give us feedback!