Hunter is a CI gate for Uniswap v4 hooks. It generates a Foundry invariant harness from a hook source file, fuzzes it against a real PoolManager, and fails the PR if value conservation breaks. Pure-Python generator. No LLM. Runs offline by default.
Source: https://github.com/hunterinvariants/hunter-invariants
Against a hook that bricks LP withdrawals (a planted reproduction of the pool-hijack-via-reinitialization pattern in OpenZeppelin’s 2025 Rewind), Hunter failed invariant_lp_can_always_withdraw and Foundry shrank the counterexample to one call:
[FAIL: an LP could not remove their liquidity: the hook can brick withdrawals]
[Sequence] (shrunk: 1) h_check_can_withdraw(445175632382375062)
-> beforeRemoveLiquidity reverts WrongPool() -> withdrawal bricked
The hook is a known-bad reproduction, not a live-protocol finding. The catch is a real forge run, reduced to its minimum reproducing call with no hand-written harness.
scan (zero-config) against seven real third-party v4 hook repositories, 35 contracts discovered:
| Repo | Contracts | Verdict |
|---|---|---|
| Uniswap v4-template (Counter) | 1 | 1 PASS |
| AqilJaafree/v4-dynamic | 3 | 2 PASS, 1 FAIL |
| revert-finance/stableswap-hooks | 7 | 7 NEEDS_CONFIG |
| Uniswap v4-hooks-public | 15 | 15 NEEDS_CONFIG |
| euler-xyz/eulerswap | 2 | 1 NEEDS_CONFIG, 1 ERROR |
| kvhook | 1 | 1 ERROR |
| async-swap | 6 | 4 NEEDS_CONFIG, 2 ERROR |
3 PASS (all exercised, not vacuous), 1 FAIL (shrunk to one call), 27 NEEDS_CONFIG, 4 ERROR. Zero fabricated PASSes across all 35.
Hunter auto-runs hooks with the standard BaseHook(IPoolManager) shape. The rest it does not fake: NEEDS_CONFIG names the exact missing constructor dependency, ERROR names the exact unresolved import. The contracts it declined to run are the reason its PASSes are trustworthy.
Six universal value-conservation properties, plus one state-integrity check on by default (--no-state-integrity drops to six):
no_free_swap_round_trip — a swap round-trip cannot return more than it spent.lp_no_free_round_trip — LP add then remove cannot profit.hook_cannot_drain_shared_manager — cannot drain another pool on the same PoolManager.callbacks_reject_non_poolmanager — callbacks revert unless the caller is PoolManager.lp_can_always_withdraw — LPs can eventually exit. The pool-hijack catch lives here.fee_within_sane_bound — a tiny round-trip cannot lose more than 50% to fees.phantom_liquidity_forbidden — a BEFORE_SWAP_RETURNS_DELTA hook cannot call modifyLiquidity inside its swap callbacks. Default-on.- uses: hunterinvariants/hunter-invariants/actions/v4-invariants@v3
with:
hook: src/MyHook.sol
Or locally: python3 -m hunter.ci scan path/to/project, or forge-hunter scan . after pip install.
PASS — suite ran, all invariants held, anti-vacuous coverage gate satisfied.FAIL — a value-conservation invariant was violated. A real bug, with a shrunk sequence.NEEDS_CONFIG — could not auto-deploy (abstract base or a non-defaultable constructor arg). Names the blocker. Skipped, not failed.ERROR — did not build or run (solc / remapping / dependency). Not an invariant violation.INCONCLUSIVE — ran, but coverage too thin to trust a PASS.Fuzzing, not proof. Not an audit. Auto-runs standard-shape hooks; production hooks with protocol-specific constructors need a one-line --ctor-args/--setup. Discovery currently over-matches some interfaces and v4-core internals; the table is raw scan output.
Next, in priority order set by the benchmark:
dependencies/, node_modules, nested v4-core) to clear the ERROR cases.--auto on by default in scan to clear the common NEEDS_CONFIG.MIT. No telemetry. Runs on your CI runner.