01 · No hidden mint
No path a caller can reach creates supply after the constructor.
The question
Can anyone, after deployment, make more tokens exist?
A token whose supply can grow at the owner's will is a token whose price is the owner's will. It is the oldest rug there is, and it hides well: behind a function named airdrop, reward, rebase, or inside a helper three calls deep.
How it decides
The check looks for sinks: functions that create supply. Two shapes count.
- A function that calls a function named
_mint. - A function that assigns to a state variable whose name contains
totalSupply(any case) and whose type is an unsigned integer, with+=,++, or=where the right side is not a subtraction.
Three functions are exempt from being sinks by name: _update, _mint and _burn. In OpenZeppelin's ERC20, _update is the one place supply changes, and it is reached by every transfer; treating it as a sink would fail every token. _mint is the sink's caller that matters, and _burn reduces supply.
Then it asks whether any sink is reachable from an entry point, following the call graph from every external or public function of the sealed contract and its bases, constructor excluded.
- Reachable: fail, with the path.
Supply can grow after deployment: mint → _mint. - Sinks exist but only the constructor reaches them: pass.
Minting happens in the constructor only. - No sink at all: pass.
Nothing in the code creates supply.
What passes
constructor() { _mint(msg.sender, 1_000_000_000e18); }
Fixed supply, minted once. Also passes: a burn function, a _burn in the transfer path, a rebasing token that only lowers supply.
What fails
function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); }
The modifier does not matter. The check does not ask who can call it; it asks whether the code can do it. An owner-only mint is still a mint, and the owner is a key that can be lost, sold or used.
Also fails: a reward() that writes totalSupply += x; a rebase() that can go up; any public function whose call chain, however long, ends in _mint.
Known edges
Tokens that keep supply in a variable not named like totalSupply, and increase it without _mint, are not seen by this check. The name rule is deliberate: it makes the check decidable and the same for everyone. Rule set two may add a check on the Transfer(address(0), …) event path instead, which would catch that shape.