Root + Impact
Description
-
According to EIP-20, the _mint() function must emit a Transfer event with the from address set to address(0) to signal token creation. This allows external systems to track supply changes and update user balances in real-time.
-
The assembly implementation updates storage slots for balance and total supply but does not include the log3 instruction to emit the Transfer event. This creates a silent minting operation where tokens appear in balances without any event trail, breaking compatibility with all ERC20-dependent infrastructure.
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:
-
Every minting operation triggers this vulnerability immediately upon contract deployment, affecting all token creation events from the first transaction.
-
DeFi protocols perform automated compatibility checks during token listing, and missing Transfer events cause instant rejection from major platforms like Uniswap, Aave, and Compound.
Impact:
-
Complete incompatibility with decentralized exchanges and lending protocols prevents the token from being traded or used as collateral, eliminating core DeFi functionality.
-
Wallet interfaces and block explorers cannot track token supply or holder balances accurately, creating user confusion and preventing adoption by displaying incorrect or zero balances despite successful minting.
Proof of Concept
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {Token} from "./Token.sol";
contract MintEventTest is Test {
Token public token;
address public account;
event Transfer(address indexed from, address indexed to, uint256 value);
function setUp() public {
token = new Token();
account = makeAddr("account");
}
function test_MintMissingTransferEvent() public {
uint256 amount = 100e18;
vm.recordLogs();
token.mint(account, amount);
Vm.Log[] memory logs = vm.getRecordedLogs();
console.log("Number of events emitted during mint:", logs.length);
assertEq(logs.length, 0, "Expected zero events but got some");
assertEq(token.balanceOf(account), amount);
console.log("Vulnerability confirmed: Mint succeeded but no Transfer event emitted");
console.log("This breaks ERC20 compliance and DeFi integrations");
}
}
Result:
forge test --match-path test/MintEvent.t.sol -vvv
[⠆] Compiling...
No files changed, compilation skipped
Ran 1 test for test/MintEvent.t.sol:MintEventTest
[PASS] test_MintMissingTransferEvent() (gas: 63531)
Logs:
Number of events emitted during mint: 0
Vulnerability confirmed: Mint succeeded but no Transfer event emitted
This breaks ERC20 compliance and DeFi integrations
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 688.50µs (116.68µs CPU time)
Ran 1 test suite in 7.63ms (688.50µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Recommended Mitigation
Add the Transfer event emission at the end of the mint function to comply with ERC20 standard requirements. The event signals token creation by using address(0) as the sender.
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)
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x00, account)
}
}