Docs · Tutorials

Build a token that passes 8 of 8

A complete ERC-20 with a fixed tax, annotated against each check, and the local test that tells you before you submit.

This is the fixture the worker's own tests use. It is deliberately plain. Every line that exists because of a check is marked.

The contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Clean {
    string public name = "Clean";
    string public symbol = "CLEAN";
    uint8 public constant decimals = 18;
    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    mapping(address => bool) public isExcludedFromFee;   // 04: read only to pick a fee, never in a require
    address public immutable treasury;                    // matching: immutables are blanked on both sides
    uint256 public constant taxBps = 300;                 // 03: constant, no setter, no cap needed
    bool public tradingOpen;                              // 05: one-way switch
    address public owner;

    constructor(address _treasury) {
        treasury = _treasury;
        owner = msg.sender;
        _mint(msg.sender, 1_000_000_000e18);              // 01: the only mint, in the constructor
        isExcludedFromFee[msg.sender] = true;
    }

    function openTrading() external { require(msg.sender == owner); tradingOpen = true; }   // 05: only ever set to true
    function excludeFromFee(address a, bool v) external { require(msg.sender == owner); isExcludedFromFee[a] = v; }

    function totalSupply() external view returns (uint256) { return _totalSupply; }
    function balanceOf(address a) external view returns (uint256) { return _balances[a]; }
    function allowance(address o, address s) external view returns (uint256) { return _allowances[o][s]; }
    function approve(address s, uint256 v) external returns (bool) { _allowances[msg.sender][s] = v; return true; }
    function transfer(address to, uint256 v) external returns (bool) { _transfer(msg.sender, to, v); return true; }   // 02: from the caller
    function transferFrom(address f, address to, uint256 v) external returns (bool) {
        uint256 a = _allowances[f][msg.sender];
        require(a >= v, "allowance");                     // 02: allowance spent before moving
        _allowances[f][msg.sender] = a - v;
        _transfer(f, to, v);
        return true;
    }

    function _transfer(address f, address to, uint256 v) internal {
        require(tradingOpen || f == owner, "not open");   // 05: guard reads the switch; blocks when false
        require(_balances[f] >= v, "balance");
        uint256 fee = isExcludedFromFee[f] ? 0 : (v * taxBps) / 10000;   // 03: denominator as a literal
        _balances[f] -= v;
        _balances[to] += v - fee;
        if (fee > 0) _balances[treasury] += fee;
    }

    function _mint(address to, uint256 v) internal { _totalSupply += v; _balances[to] += v; }
}

No proxy (06), no selfdestruct (08). Deployed and held, a fresh address can receive and send back (07).

What each check sees

Check Verdict Evidence
01 pass Minting happens in the constructor only.
02 pass Balances move only from the caller, or with an allowance.
03 pass taxBps cannot be changed after deployment.
04 pass No per-address switch in the transfer path.
05 pass tradingOpen can only be switched to let transfers through.
06 pass No DELEGATECALL in the runtime.
07 not run Until deployed and held.
08 pass No SELFDESTRUCT in the runtime.

Things people add that break it

  • A mint for "future rewards": fails 01. Mint the rewards now to a locked contract.
  • setTax(uint256) without require(x <= 1000): fails 03.
  • mapping(address => bool) bots in a require: fails 04. Use a time window and a max size instead.
  • pause()/unpause(): fails 05. If you must, accept the 7 of 8.
  • A rescueTokens(address from) on the token's own balances: fails 02. Rescue other tokens sent to the contract, not your own holders.

Test before you submit

cd zkcheck/worker && pnpm install          # the worker source, once you have it
cp -r /path/to/your/project test/fixtures/mine

Then in test/rules.test.ts, add a test that compiles "mine" and prints runStatic(...). pnpm test shows the eight answers in seconds, with the same evidence sentences the seal would show.