Skip to content

hunterinvariants/hunter-invariants

Repository files navigation

Hunter

Hunter generates a Foundry invariant harness for a Uniswap v4 hook, runs it under fuzzing in CI, and fails the PR if value conservation breaks. Pure-Python generator (regex + string templates). No LLM. Runs offline by default; network only if you opt into --fork-url.

Registry: https://hunterinvariants.github.io/hunter-invariants/

What it catches

Run against a hook that bricks LP withdrawals (a planted reproduction of the pool-hijack-via-reinitialization pattern OpenZeppelin documented in their 2025 Rewind), Hunter failed invariant_lp_can_always_withdraw and Foundry shrank the counterexample to a single call:

[FAIL: an LP could not remove their liquidity: the hook can brick withdrawals (funds locked)]
  [Sequence] (shrunk: 1)  h_check_can_withdraw(445175632382375062)
  -> beforeRemoveLiquidity reverts WrongPool() -> withdrawal bricked

No hand-written harness. Hunter parsed the hook, mined a flag-correct address, deployed it against a real PoolManager, fuzzed the swap and liquidity handlers, and reduced the failure to its minimum reproducing call. The hook is a known-bad reproduction, not a live-protocol finding; the catch is a real forge run.

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 (the catch above)
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

Totals: 3 PASS (every one exercised, thousands of value-bearing ops, not vacuous), 1 FAIL (shrunk to one call), 27 NEEDS_CONFIG, 4 ERROR. Zero fabricated PASSes across all 35 contracts.

That distribution is the point, not a caveat. Hunter auto-runs hooks with the standard BaseHook(IPoolManager) shape and defaultable constructors. Everything else it refuses to fake:

  • NEEDS_CONFIG names the exact blocker. A hook taking ICurveStableSwap or IWstETH in its constructor gets reported as needing that specific dependency, not silently passed or failed. Supply it with a one-line --ctor-args/--setup.
  • ERROR names the exact unresolved import or solc conflict. A build problem is never reported as an invariant violation.

A safety gate is only worth running if its green check is trustworthy. The 31 contracts Hunter declined to auto-run are the evidence that its 3 PASSes mean something.

What it checks

Seven value-conservation properties under invariant fuzzing: six universal, plus one state-integrity check that is on by default. --no-state-integrity drops to the six.

  1. no_free_swap_round_trip — a 0->1->0 swap cannot return more than it spent.
  2. lp_no_free_round_trip — an LP add then remove, with no swap between, cannot profit.
  3. hook_cannot_drain_shared_manager — fuzzing the hook's pool cannot drain another pool's reserves on the same PoolManager.
  4. callbacks_reject_non_poolmanager — every callback reverts unless the caller is the PoolManager.
  5. lp_can_always_withdraw — LPs can eventually exit. The pool-hijack catch above lives here.
  6. fee_within_sane_bound — a tiny round-trip cannot lose more than 50% to fees.
  7. phantom_liquidity_forbidden — a hook holding BEFORE_SWAP_RETURNS_DELTA cannot call modifyLiquidity synchronously inside its swap callbacks. Default-on; exempted for hook classes where it does not apply.

A run where every fuzz call reverted reports INCONCLUSIVE, not PASS. A build or config failure reports ERROR, not FAIL.

Install

# .github/workflows/hunter-invariants.yml
name: hunter-invariants
on: [pull_request]
permissions:
  contents: read
  pull-requests: write
jobs:
  invariants:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { submodules: recursive }
      - uses: hunterinvariants/hunter-invariants/actions/v4-invariants@v3
        with:
          hook: src/MyHook.sol

Run it locally

python3 -m hunter.ci scan path/to/foundry-project

Walks the project, finds the v4 hooks, picks up deps and solc, gates each one it can build. Flags worth knowing:

  • --auto — resolve an abstract hook to its concrete *Mock/subclass automatically.
  • --json / --json-out PATH — structured report: per-invariant status, coverage, counterexamples.
  • --seed 0x.. — pin the fuzz seed for a reproducible run.
  • --fork-url URL — fuzz against live chain state for hooks with external calls.
  • --no-state-integrity — drop to the six universal invariants.

After pip install, the same engine is on PATH as a console script:

forge-hunter check src/MyHook.sol
forge-hunter scan .

Verdicts

The verdicts are load-bearing. They are the reason a green check from Hunter means something.

  • PASS — the suite ran, every invariant held, and the anti-vacuous coverage gate was satisfied.
  • FAIL — a value-conservation invariant was violated. A real bug, with a shrunk reproducing sequence.
  • NEEDS_CONFIG — Hunter could not auto-deploy the hook (abstract base, or a constructor argument it will not fabricate). It names the blocker. Skipped, not failed.
  • ERROR — the harness did not build or run (solc, remapping, missing dependency). A toolchain problem, not an invariant violation.
  • INCONCLUSIVE — the suite ran but coverage was too thin to trust a PASS.

Scope and limits

  • Fuzzing, not proof. Not an audit. Catches structural value-conservation breaks, not protocol-specific logic.
  • Auto-runs hooks with the standard BaseHook(IPoolManager) shape. Production hooks with protocol-specific constructor arguments need a one-line --ctor-args/--setup; Hunter tells you which argument.
  • Discovery currently over-matches some interfaces and v4-core internals (it will list contracts like PoolManager or IHooks). The benchmark table shows the raw scan output; refining discovery is on the roadmap.
  • EVM / Solidity / Foundry only. No telemetry. Runs on your CI runner.
  • MIT. Deleting the workflow removes the gate.

Roadmap

The benchmark above is the to-do list:

  1. Remapping auto-detection for soldeer (dependencies/), node_modules, and nested v4-core layouts. Clears most of the ERROR cases with no config change.
  2. --auto on by default in scan, so abstract bases resolve to their concrete subclasses without a flag. Clears the most common NEEDS_CONFIG.
  3. Discovery filtering so interfaces and v4-core internals are not listed as hooks.

Badge

After Hunter passes on your hook in CI:

Hunter

[![Hunter](https://img.shields.io/endpoint?url=https%3A%2F%2Fhunterinvariants.github.io%2Fhunter-invariants%2Fbadge.json)](https://hunterinvariants.github.io/hunter-invariants/)

License

MIT.

About

Automated value-conservation invariant safety gate for Uniswap v4 hooks and ERC-4626 vaults in GitHub Actions.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages