How the smart contracts are tested.
Every contract behind the Pigfox demos passes the same gates before it reaches Base Sepolia: two linters, unit and invariant tests, static analysis, two independent property fuzzers, and a 100% coverage gate. Not a house style — a shared pipeline, in its own public repository, that all 17 demo repositories consume.
PIGFOX SOLIDITY PIPELINE v1
The pipeline lives in one place: github.com/pigfox/solidity-pipeline. Each contract repository vendors it as a submodule and calls its reusable workflow, so there is exactly one definition of “green” and a fix to a gate lands once instead of once per repository. It also means the gates that run in CI are the same bytes a developer runs locally, rather than an approximation of them.
One property set per repository. Three engines each.
Each repository has exactly one test/Properties.sol holding its invariants, and Foundry, Echidna and Medusa all execute that same file — so a property cannot hold in one engine and silently not exist in another. What each set proves is documented in that repository, beside the properties themselves.
Where the figures are.
This page used to print a table of test counts, property counts and fuzzing-call totals. It no longer does, and the reason is the point of the page.
Those numbers were measured honestly. Then more demos shipped and the table did not move: it went on naming a subset of the repositories, and totalling only their properties, long after both were wrong — on a page whose entire subject is not trusting a figure you have not checked. A number nothing verifies is a number that rots, and rots quietly.
So the figures live where they are produced and where they are enforced: in each repository, on every push. The pipeline asserts them there — the coverage gate fails below 100%, the property count must agree four ways before a build passes — and no copy of them here could be more current than the run itself. Each repository below links to its live CI, which updates itself and cannot be out of date by construction.
A page giving each demo its own pipeline evidence — the figures, pegged to the commit that produced them, so a displayed number is true by construction rather than true until the repository changes — is the work this table is a placeholder for.
Slither runs on every repository at fail-on: low, against one shared configuration. Every excluded detector is written down with a reason in the pipeline repository, because a detector switched off in a config file with no reason attached is indistinguishable from one switched off to make a build pass. Findings below that severity are printed rather than suppressed.
The stages.
Every stage runs on every push, with no fast path and no opt-out: the gates self-test, then the two linters, a doctrine gate, the property-count check, formatting on a pinned toolchain, build and test and coverage, static analysis, then each fuzzer in turn, and a single required check that the rest report into. Grouped by what they catch:
- forge lint and Solhint. The pipeline’s first stage, and first because it is the cheapest: no fuzzing, no coverage run and no chain, so a style or natspec regression fails in seconds rather than after the fuzzers. Two linters rather than one, because they cover different ground —
forge lintis Foundry’s own and needs no npm, and Solhint adds the natspec surface it does not check. Every rule the estate turns off is argued in writing in the pipeline repository, the same discipline as the Slither exclusions, and per-line suppression comments are not used at all. Becauseforge linthas no config-file flag, its configuration is copied into each repository rather than consumed — so a further gate checks each copy still matches the canonical one, and fails the build when it does not. - Foundry. Unit, fuzz and invariant tests on a pinned toolchain, all passing and none skipped — a skipped test is a red build here, not a yellow one. The formatting gate runs on a deliberately pinned compiler version so an unrelated upstream release cannot turn an unchanged tree red.
- Slither. Static analysis on every push, failing the build on low-severity findings and up. The exclusions are written down in the pipeline repository rather than silently configured away, and per-file suppression comments are not used at all.
- Echidna and Medusa. Two independent fuzzers hammer the same property file, in an in-memory EVM built fresh from source. Both assert the number of properties they registered — because a stale build artifact once silently shrank a property set and reported a smaller green run. The count is declared as a literal in Solidity, and four independent things must agree with it before a build passes.
- Live chain. Every demo is deployed to Base Sepolia and verified on Basescan. For zk-escrow that also covers a UUPS upgrade and AI-arbitrated dispute settlement: the arbiter's ruling and its full reasoning are written on-chain, and a UUPS upgrade added arbiter rotation to recover disputes stranded behind an arbiter address for which no private key existed. The claim is that the mechanism has been exercised on a real chain, not that it has been exercised often — and the current state of it is readable on the demo pages, which read the chain rather than restating it here. Upgrades are rehearsed first on the same live chain, against throwaway contracts the drill deploys itself so it structurally cannot touch the real proxy, then broadcast, then verified on-chain.
Coverage without asterisks.
The gate requires 100% line, statement, branch, and function coverage on every file under src/. The only exclusions the estate grants are generated verifier contracts in the zero-knowledge repositories — produced verbatim by snarkjs from that repository's own proving key, so their identifiers and their residual uncovered lines are not ours to change. Those lines are the assembly paths taken when the BN254 precompiles themselves report failure: reaching them would need a broken EVM, not a broken proof, and the verifiers are still exercised end-to-end by real proofs. Every exclusion in force, and the argument for it, is written down in the pipeline repository rather than inferred from a percentage.
Generated code is excluded by name, printed on every run, never folded into a percentage. The check fails on a report it cannot parse, so a format change cannot pass by default; and it fails on an exclusion that no longer matches a real file, because a stale exclusion is a hole that reads as a decision.
What broke decides what gets fixed.
A failing pipeline is not a single event, and treating it as one is how a contract ends up edited to satisfy a broken tool. The rule is written down:
- A coverage shortfall means the code is right and a path is untested. Write the test. No source change, and no redeploy — redeploying identical bytecode fixes nothing.
- A tooling or configuration failure is fixed in the tooling or the configuration. Never in a contract.
- A failing test or violated property means the deployed contract is not demo-worthy. The source is fixed; a plain contract is redeployed and re-verified, and a contract behind an upgradeable proxy has its implementation upgraded rather than replaced — the proxy holds the history, and preserving it is what a proxy is for. Then the entire pipeline runs again.