|
| 1 | +# <picture><img src="assets/logo.png" height="48" align="left" alt="Counterflow logo"/></picture>Counterflow |
| 2 | + |
| 3 | +> **Prove the contract, or reveal the exploit.** |
| 4 | +
|
| 5 | +AI-translated, machine-proved smart contract invariant checking — three layers: |
| 6 | + |
| 7 | +1. **Z3 abstract model** — inductive proofs over a fixed vocabulary (allowances, transfers, vault shares, ghost-sum accounting). 6 benchmark exploit classes, all caught. |
| 8 | +2. **Halmos bytecode** — symbolic execution against compiled EVM bytecode, closing the spec-vs-implementation gap. |
| 9 | +3. **LLM translation** — Solidity + English → structured binding (the only untrusted layer; never decides the verdict). |
| 10 | + |
| 11 | +**The LLM never decides the verdict.** A ~350-line, human-auditable Z3 core either *proves* |
| 12 | +the invariant holds for **all** inputs, or produces a **concrete |
| 13 | +counterexample** (an exploit trace). Hallucination is structurally contained: |
| 14 | +a bad translation is caught by deterministic vocabulary validation or produces a wrong |
| 15 | +model — never a false proof of the trusted core's semantics. |
| 16 | + |
| 17 | +``` |
| 18 | +Solidity + English invariants |
| 19 | + │ |
| 20 | + ▼ |
| 21 | + [Slither extraction] structural AST pass — function selectors, state vars |
| 22 | + │ |
| 23 | + ▼ |
| 24 | + [LLM translate] untrusted — DeepSeek/OpenAI, temperature 0 |
| 25 | + │ |
| 26 | + ▼ |
| 27 | + binding.json human-reviewable artifact (the real spec) |
| 28 | + │ |
| 29 | + ▼ |
| 30 | + [validate] deterministic vocabulary/schema gate |
| 31 | + │ |
| 32 | + ├─────────────────────────────────────────┬──────────────────────────┐ |
| 33 | + ▼ ▼ ▼ |
| 34 | + [Z3 inductive check] [Halmos bytecode] [Echidna validation] |
| 35 | + TRUSTED — abstract model TRUSTED — EVM symbolic exec fuzzing harness gen |
| 36 | + │ │ │ |
| 37 | + ▼ ▼ ▼ |
| 38 | + PROVED | VIOLATED (+ cex) PASS | FAIL (+ cex calldata) echidna test file |
| 39 | + │ │ │ |
| 40 | + └───────────────┬─────────────────────────┴──────────────────────────┘ |
| 41 | + ▼ |
| 42 | + audit.jsonl SHA-256 hash-chained, tamper-evident run log |
| 43 | + │ |
| 44 | + ▼ |
| 45 | + [Foundry export] renderSolidity → handler + invariants .sol files |
| 46 | +``` |
| 47 | + |
| 48 | +## Quickstart |
| 49 | + |
| 50 | +```bash |
| 51 | +# —— deterministic (no LLM needed) —— |
| 52 | +node src/cli.js check examples/TokenPool.binding.json # PROVED |
| 53 | +node src/cli.js check examples/TokenPoolBuggy.binding.json # VIOLATED + exploit |
| 54 | + |
| 55 | +# —— full AI pipeline (needs DEEPSEEK_API_KEY or OPENAI_API_KEY) —— |
| 56 | +node src/cli.js verify examples/TokenPoolBuggy.sol examples/invariants.txt |
| 57 | + |
| 58 | +# —— two-step, human-in-the-loop (recommended) —— |
| 59 | +node src/cli.js extract examples/TokenPool.sol examples/invariants.txt -o binding.json |
| 60 | +# review binding.json ... |
| 61 | +node src/cli.js check binding.json |
| 62 | + |
| 63 | +# —— bytecode-level (needs foundry + halmos) —— |
| 64 | +pip install z3-solver halmos # or use project .venv |
| 65 | +node src/cli.js bytecode HalmosTest |
| 66 | + |
| 67 | +# —— everything —— |
| 68 | +npm run all |
| 69 | + |
| 70 | +# —— audit chain —— |
| 71 | +node src/cli.js audit |
| 72 | +``` |
| 73 | + |
| 74 | +Requires Node >= 18, Python 3 with `z3-solver` and `halmos`, and [Foundry](https://getfoundry.sh/) (`brew install foundry`). Set `COUNTERFLOW_PYTHON` to a venv python if needed. |
| 75 | + |
| 76 | +## Audit Binding |
| 77 | + |
| 78 | +```bash |
| 79 | +# —— audit-binding: review & compare binding snapshots —— |
| 80 | +node src/cli.js audit-binding binding.json # show binding structure & validation |
| 81 | +node src/cli.js audit-binding a.json b.json # diff two bindings side-by-side |
| 82 | +``` |
| 83 | + |
| 84 | +Audit-binding loads one or two binding files and produces a human-readable summary: |
| 85 | +- Validates the binding vocabulary and schema |
| 86 | +- Lists all functions, guards, effects, and invariants |
| 87 | +- When given two bindings, computes a structural diff showing added/removed/changed |
| 88 | + functions, guards, effects, and invariants |
| 89 | +- Useful in CI to detect unintended binding drift across revisions |
| 90 | + |
| 91 | +## Export |
| 92 | + |
| 93 | +```bash |
| 94 | +# —— generate Echidna fuzzing harness —— |
| 95 | +node src/cli.js gen-echidna binding.json > EchidnaTest.sol |
| 96 | +# produces: contract EchidnaBindingTest with echidna_ prefixed properties |
| 97 | + |
| 98 | +# —— generate Foundry invariant test files —— |
| 99 | +node src/cli.js gen-foundry binding.json -o foundry-tests/ |
| 100 | +# produces: handler .sol + invariants .sol with function invariant_ stubs |
| 101 | +``` |
| 102 | + |
| 103 | +Export commands generate derivative artifacts from a binding: |
| 104 | +- **Echidna**: produces a standalone Solidity file with `echidna_`-prefixed property |
| 105 | + functions ready for `echidna-test`. Each invariant becomes an Echidna property. |
| 106 | +- **Foundry**: produces `{Model}Handler.sol` (actor management) and |
| 107 | + `{Model}Invariants.sol` (per-invariant `function invariant_*` stubs) for use |
| 108 | + with `forge test` invariant testing. |
| 109 | + |
| 110 | +## DeFiHackLabs Benchmark |
| 111 | + |
| 112 | +The defihack runner executes Counterflow against the |
| 113 | +[DeFiHackLabs](https://github.com/SunWeb3Sec/DeFiHackLabs) exploit corpus: |
| 114 | + |
| 115 | +```bash |
| 116 | +node bench/defihack.js # run all cases |
| 117 | +node bench/defihack.js --case cream-finance # single case |
| 118 | +``` |
| 119 | + |
| 120 | +Each case: |
| 121 | +1. Loads the Solidity source and English invariants from the corpus |
| 122 | +2. Runs LLM translation → binding validation → Z3 proof |
| 123 | +3. Compares the verdict against the known exploit label |
| 124 | +4. Produces a per-case report (contract, invariants, verdict, counterexample) |
| 125 | + |
| 126 | +This validates that the pipeline catches real-world DeFi exploits, not just |
| 127 | +synthetic examples. Current coverage includes CREAM Finance, Hundred Finance, |
| 128 | +and other high-profile incidents. |
| 129 | + |
| 130 | +## Benchmark |
| 131 | + |
| 132 | +``` |
| 133 | +npm run bench |
| 134 | +# or: npm run all (includes e2e tests + bytecode check) |
| 135 | +
|
| 136 | +6/6 cases correct: |
| 137 | +
|
| 138 | + TokenPool (safe) PROVED [reference] |
| 139 | + TokenPoolBuggy (missing balance check) VIOLATED [underflow-drain] |
| 140 | + SafeVault (safe) PROVED [reference] |
| 141 | + ApprovalDrain (transferFrom no check) VIOLATED [approval-drain] |
| 142 | + UnbackedMintVault (owner infinite mint) VIOLATED [unbacked-mint] |
| 143 | + BurnDesyncVault (totalShares desync) VIOLATED [accounting-desync] |
| 144 | +
|
| 145 | +Bytecode: Halmos symbolic test |
| 146 | + ✓ TokenPool withdraw no underflow PASS |
| 147 | + ✗ TokenPoolBuggy withdraw no underflow FAIL (amt > pre counterexample) |
| 148 | +``` |
| 149 | + |
| 150 | +## State model (v2) |
| 151 | + |
| 152 | +| Vocabulary | Covers | |
| 153 | +|---|---| |
| 154 | +| `balances[a]`, `shares[a]`, `allowances[a]` | ERC20 + ERC4626 + approvals | |
| 155 | +| `totalAssets`, `totalShares` | pool totals | |
| 156 | +| ghost sums: `sumBalances`, `sumShares` | exact accounting integrity | |
| 157 | +| 9 invariants | nonneg_balance/shares/allowance/total/total_shares, solvency, shares_integrity, backing, supply_cap | |
| 158 | +| 9 guards, 13 effects | deposit/withdraw/mint/burn/transfer/transferFrom/approve | |
| 159 | + |
| 160 | +### Reentrancy |
| 161 | + |
| 162 | +The reentrancy vocabulary models the standard check-effects-interaction pattern |
| 163 | +with a reentrancy lock: |
| 164 | + |
| 165 | +| Vocabulary | Covers | |
| 166 | +|---|---| |
| 167 | +| guard `not_locked` | `require(!locked)` — the function's reentrancy lock is free | |
| 168 | +| effect `reentrancy_lock_acquire` | `locked = true` | |
| 169 | +| effect `reentrancy_lock_release` | `locked = false` | |
| 170 | +| effect `external_call` | marker for an external call — snapshots state for reentrancy checks | |
| 171 | +| invariant `reentrancy_safe` | if in external call, storage (total, sumBalances) has not changed since the snapshot | |
| 172 | + |
| 173 | +A properly modeled withdraw with reentrancy: |
| 174 | + |
| 175 | +```json |
| 176 | +{ |
| 177 | + "name": "withdraw", |
| 178 | + "guards": ["amt_gt_0", "bal_ge_amt", "not_locked"], |
| 179 | + "effects": ["reentrancy_lock_acquire", "bal_sub_amt", "total_sub_amt", |
| 180 | + "external_call", "reentrancy_lock_release"] |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +The Z3 core tracks per-function locks, an `in_call` flag, and storage snapshots |
| 185 | +taken at the `external_call` point. The `reentrancy_safe` invariant is violated if |
| 186 | +any state variable changes between the snapshot and the end of the call — catching |
| 187 | +cross-function reentrancy even when locks appear to be used. |
| 188 | + |
| 189 | +## What a verdict means (read this) |
| 190 | + |
| 191 | +- **PROVED** — the modeled transition preserves the invariant for *all* |
| 192 | + possible inputs and states (an inductive proof over the reviewed binding). |
| 193 | + It is **not** a claim that the contract is "safe": the binding is an |
| 194 | + abstraction and must be reviewed; bytecode verification closes the gap. |
| 195 | +- **VIOLATED** — Z3 or Halmos found a concrete counterexample. |
| 196 | +- **UNKNOWN** — the solver could not decide within limits. Nothing is claimed. |
| 197 | + |
| 198 | +## Open core |
| 199 | + |
| 200 | +MIT-licensed: CLI, translation prompts, validation, the trusted Z3 core, |
| 201 | +Halmos symbolic tests, and example bindings. Commercial layer (separate): |
| 202 | +hosted pipeline, CI integration, dashboards, proof storage, multi-contract |
| 203 | +projects. |
| 204 | + |
| 205 | +## Roadmap |
| 206 | + |
| 207 | +- CVL export for Certora interop |
| 208 | +- Richer state model: reentrancy flags, compound interest/accrual |
| 209 | +- L2/bridge message verification targets |
| 210 | +- Public leaderboard of verified contracts |
0 commit comments