Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: low
Valid

Silent Mint: Missing Transfer Event Emission in `ERC20Internals::_mint`

The `ERC20Internals::_mint` function updates both the total supply and the recipient’s balance when new tokens are created, but it does not emit the required ERC20 Transfer(address(0), account, value) event. This omission breaks ERC20 standard behavior, leaving token creation unobservable to off‑chain systems such as wallets, explorers, and monitoring tools that depend on event logs to track supply changes. As a result, tokens can be minted silently, undermining transparency and interoperability across the ecosystem.

Description

The `ERC20Internals::_mint` function correctly updates the total supply and the recipient’s balance when new tokens are created, but it fails to emit the mandatory ERC20 Transfer(address(0), account, value) event. This omission breaks ERC20 compliance and causes minted tokens to be invisible to off‑chain systems such as wallets, block explorers, and analytics tools that rely on event logs to track token creation. As a result, tokens can be minted silently without triggering monitoring alerts, creating opportunities for stealth inflation and undermining trust in the token’s supply integrity.
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))
}
}

Risk

Likelihood:

High — the issue occurs every time `ERC20Internals::_mint` is called, since the event emission is completely absent.Impact:

  • Impact 1

  • High — minted tokens remain invisible to off‑chain systems, breaking ERC20 compliance and enabling silent inflation that undermines transparency and trust.Impact 2

Proof of Concept

The suite demonstrates ERC20 compliance failure (missing event) and safety failure (unchecked arithmetic).

  • Case 1 (Event): Confirms balances and supply update correctly, but fails because no Transfer(address(0), account, value) event is emitted.

  • Case 2 (Overflow): Shows that minting with type(uint256).max silently wraps supply and balances instead of reverting.
    Add below code into a test file

contract TestToken is ERC20Internals {
function mint(address account, uint256 value) external {
_mint(account, value);
}
function balanceOf(address account) external view returns (uint256) {
return _balanceOf(account);
}
function totalSupply() external view returns (uint256) {
return totalSupply_();
}
}
contract MintEventTest is Test {
event Transfer(address indexed from, address indexed to, uint256 value);
TestToken token;
function setUp() public {
token = new TestToken();
}
function testMintUpdatesBalanceAndSupply() public {
address alice = address(1);
uint256 amount = 100;
token.mint(alice, amount); /
assertEq(token.balanceOf(alice), amount);
assertEq(token.totalSupply(), amount);
}
function testMintDoesNotEmitTransferEvent() public {
address alice = address(1);
uint256 amount = 100;
vm.expectEmit(true, true, false, true);
emit Transfer(address(0), alice, amount);
token.mint(alice, amount);
}
}

Recommended Mitigation

Emit the required ERC20 event inside _mint() after updating supply and balances.

function _mint(address account, uint256 value) internal {
assembly ("memory-safe") {
// Revert if minting to the zero address
if iszero(account) {
mstore(0x00, shl(224, 0xec442f05)) // Error selector
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
// Update total supply
let supply := sload(supplySlot)
sstore(supplySlot, add(supply, value))
// Update account balance
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, add(accountBalance, value))
+ let topic := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
+ mstore(0x00, value)
+ log3(0x00, 0x20, topic, 0, account)
}
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

missing event

missing event emission

Support

FAQs

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

Give us feedback!