Root + Impact
Description
-
Normal behavior: Minting increases totalSupply and the recipient balance and emits Transfer(address(0), recipient, value) so indexers, wallets and explorers see the tokens
-
But the current implementation, _mint updates on‑chain state but does not emit the required Transfer event; off‑chain tooling that relies on events will not detect mint operations.
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:
-
Reason 1: Mint calls happen during token issuance; any accessible mint function will execute the _mint path immediately.
-
Reason 2:Indexers/wallets attempt to detect mints via Transfer(0x0..., to, amount) because the event is missing, monitoring will be difficult as soon as mints occur.
Impact:
-
Impact 1: Wallets, block explorers, and accounting tools will not register the minted tokens , balances and histories will be inconsistent off‑chain
-
Impact 2: Compliance and bookkeeping processes that rely on events (Airdrops, token supply monitoring, analytics) will be broken or misleading.
Proof of Concept
pragma solidity ^0.8.24;
import {Test, Vm} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
contract MintEventHarness is ERC20 {
constructor() ERC20("EVENT", "EVNT") {}
function exposedMint(address account, uint256 value) external { _mint(account, value); }
}
contract MintMissingEventTest is Test {
MintEventHarness internal token;
bytes32 internal constant TRANSFER_TOPIC = keccak256("Transfer(address,address,uint256)");
function setUp() public {
token = new MintEventHarness();
}
function test_mintMissingTransferEvent() public {
address recipient = address(0x2032);
vm.recordLogs();
token.exposedMint(recipient, 1 ether);
Vm.Log[] memory entries = vm.getRecordedLogs();
bool foundTransfer;
for (uint256 i = 0; i < entries.length; i++) {
if (entries[i].topics.length == 3 && entries[i].topics[0] == TRANSFER_TOPIC) {
foundTransfer = true;
break;
}
}
assertFalse(foundTransfer, "expected Transfer event on mint but none was emitted");
}
}
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))
+ // Emit standard ERC-20 Transfer(address(0), account, value)
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,0, account)
}
}