hunter-invariants

Hunter

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

What it catches

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.

Benchmark

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.

What it checks

Six universal value-conservation properties, plus one state-integrity check on by default (--no-state-integrity drops to six):

  1. no_free_swap_round_trip — a swap round-trip cannot return more than it spent.
  2. lp_no_free_round_trip — LP add then remove cannot profit.
  3. hook_cannot_drain_shared_manager — cannot drain another pool on the same PoolManager.
  4. callbacks_reject_non_poolmanager — callbacks revert unless the caller is PoolManager.
  5. lp_can_always_withdraw — LPs can eventually exit. The pool-hijack catch lives here.
  6. fee_within_sane_bound — a tiny round-trip cannot lose more than 50% to fees.
  7. phantom_liquidity_forbidden — a BEFORE_SWAP_RETURNS_DELTA hook cannot call modifyLiquidity inside its swap callbacks. Default-on.

Integration

- 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.

Verdicts

Scope and roadmap

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:

  1. Remapping auto-detection (soldeer dependencies/, node_modules, nested v4-core) to clear the ERROR cases.
  2. --auto on by default in scan to clear the common NEEDS_CONFIG.
  3. Discovery filtering so interfaces and v4-core internals are not listed.

MIT. No telemetry. Runs on your CI runner.