Docs · Developers

Passing the checks

How to write a token that passes all eight, with the patterns that pass and the ones that fail, check by check.

This is the developer's side of The eight checks. For each check: the pattern that passes, the pattern that fails, and the fix.

01 · No hidden mint

Pass. Mint the whole supply in the constructor. Nothing else calls _mint or increases totalSupply.

Fail. Any external or public function whose call chain reaches _mint, or that increases a totalSupply-named variable. Owner-only does not help.

Fix. Remove the mint. If you need a reserve for later, mint it to a vesting or treasury contract at construction. Supply that exists and is locked is visible; supply that can be created is not.

02 · No owner drain

Pass. Only transfer and transferFrom move balances, or functions that pass msg.sender as the source, or that spend an allowance first.

Fail. rescue(from, amount), migrate(holder), direct writes to the balances mapping outside the standard functions.

Fix. If you need a recovery function for tokens sent to the contract itself, write it as IERC20(other).transfer(...) on the other token, not as a write to your own balances. If you need migration, have holders call it themselves.

03 · Tax under the cap

Pass. A constant tax. Or setters with require(x <= N) where N / denominator <= 0.10, and the denominator written as a plain literal (10000, 1000, 100) somewhere in the contract.

Fail. A setter without a literal bound. A bound above ten percent. A bound expressed through another variable.

Fix. require(bps <= 1000, "cap") in every setter, and / 10000 in the math. Name the variable with tax or fee in it so the check recognises it; name addresses with wallet, receiver or recipient so it does not.

04 · No blacklist

Pass. mapping(address => bool) variables that only pick a branch: fee exemptions, limit exemptions.

Fail. Any mapping(address => bool) read inside a require or a reverting if in the transfer path, with a setter.

Fix. Delete the blacklist. If you need anti-bot protection at launch, use a time-based rule (block.timestamp < openAt + 60) with a max transaction size, which is not per-address and not flagged.

05 · No pause on transfer

Pass. A bool switch whose every reachable write sets the non-blocking value: tradingOpen = true and nothing ever sets it false.

Fail. paused = v, pause() and unpause(), OpenZeppelin Pausable on transfers.

Fix. One-way switches only. If you truly need an emergency stop, understand that it fails this check by design, and that a seal with 7 of 8 and the evidence "paused can stop _transfer" is honest about it.

06 · Not upgradeable

Pass. A plain contract.

Fail. Any proxy pattern, clones, or external libraries with DELEGATECALL.

Fix. Deploy the implementation as the token. If you need upgradeability, the seal is not for you, and that is the point of the seal.

07 · Sell path clears

Pass. A fresh address that receives tokens can send them back.

Fail. A transfer restriction that a normal wallet hits.

Fix. Usually a consequence of 04 or 05. Also: a max-wallet rule that rejects the fresh address receiving one thousandth of the top holder's balance will fail this; keep limits proportional to supply, not tiny.

08 · No self-destruct

Pass. No selfdestruct anywhere.

Fix. Delete it.

Running the checks yourself

The worker's source is published with the registry release; until then it is available on request. With Foundry and Node installed:

cd worker && pnpm install
pnpm test          # the two fixture tokens: one clean, one rug

Point test/rules.test.ts at your own project to see the eight answers before you submit. A future release ships this as a CLI.

A token that passes 8 of 8

The fixture at worker/test/fixtures/clean/src/Clean.sol is a complete ERC-20 with a fixed 3% tax to a treasury, a fee exemption mapping, a one-way tradingOpen switch, and an immutable treasury. It passes every static check and is a reasonable starting point.