Predict Smart-Contract Audit
⛔ STOP — explicit run confirmation is MANDATORY
This skill launches expensive multi-agent audits — a whole-fleet run (all three harnesses at 'all' scope) is dozens of agents and a few million tokens. Each harness requires an explicit scope and bounds its own agent count by construction, so it cannot go maximal by accident — but it must NEVER run automatically or as a side effect of being invoked, read, or mentioned.
Before launching ANY harness run (orchestrator / ownership-walk / rule-sweep), you MUST:
- Show the user the run plan — which harness(es) and scope (
lenses/units/rules; the explicit string'all'is the whole-fleet opt-in) — plus cost. Quote cost from the LAST run's measured actuals (its usage block records agents and tokens), never from intuition: guessed estimates have been 2x+ wrong. - Ask explicitly — e.g. "Run this audit? (≈N agents / ≈X tokens)" — and WAIT for an explicit "yes."
- Only after the user confirms, call
Workflow(...). If anything is ambiguous, ask; never assume.
Invoking, reading, explaining, or editing this skill never needs the gate — only executing a harness run does. When in doubt, do not run.
A self-driving harness for a deep, adversarial, smart-contract-only audit of DeepBook "Predict" — a binary prediction market on Sui — and its three split-out sibling packages. The goal is to find real bugs: solvency/fund-loss, liveness/abort (a bricked flow), griefing/DoS, mispricing, broken invariants, and authorization holes — plus, via a dedicated lens, architecture/cohesion/readability issues (overgrown "god" modules, coupling, decomposition). Findings are reasoned from the code AND, where it matters, empirically reproduced on localnet or in Python.
This is a code-audit skill (security/correctness + maintainability). It does NOT do testnet-readiness/deploy/ops checks (Move.toml hygiene, post-deploy admin steps, indexer parity, runbooks). If asked for those, say so and scope them separately. Scope is FIXED to the three Predict-cluster packages (predict + propbook + account); do NOT broaden it to deepbook core or other repo packages. Upgrade / object-layout migration correctness is also out of scope (matches move.md pre-deploy) — revisit only when nearing deploy.
When to use
- "Audit Predict" / "deep audit of the predict contracts" / "find bugs in Predict before testnet."
- "Review these Predict changes for correctness/exploits/invariant violations."
- Any request to assess Predict economic safety at the Move-source level.
Packages in scope (all Move source, read-only)
The oracle and custody layers were extracted out of predict, so the audit spans three packages; Block Scholes data enters through the external bs_oracle signature verifier (Block Scholes-owned, git-pinned — a trust boundary to reason about, not audited surface). Dependency order (leaves first):
bs_oracle (external verifier) account (account, account_registry, account_events)
fixed_math (math) ─────────────┐ │
▼ │
propbook (registry, oracle_lane, feeds/{pyth,block_scholes_sid,block_scholes_store}, constants)
▼ ▼
predict (31 modules — the center of gravity)
predict is the top consumer and gets most of the audit budget; the siblings are audited for their own correctness AND for the trust predict places in them (lens 08).
Method (5 phases)
- Ground truth — MAIN LOOP ONLY.
sui build+sui testall three packages (--warnings-are-errors), and a localnet sim smoke. "Tests don't compile / are red" is itself a finding. Results feed the audit. - Find. The orchestrator's lenses loop-until-dry (bug findings are SAMPLED, so each lens re-runs — bounded by
maxRounds, retiring per-lane once dry — until it stops surfacing new ones). The rule sweep and ownership walk are SINGLE-PASS: mechanical and per-module conformance ENUMERATE rather than sample, and the next run is round 2. - Adversarial verify — High/Critical only, cross-model. Only findings that can lose funds or brick a flow earn verify subagents: the orchestrator panels High/Critical (codex refute + codex repro + Claude settled — an escalated finding clears a different model than the one that found it; needs the codex CLI), the walk sends its 'high' violations to one codex verifier. Everything Medium and below is reported RAW — the operator is the verifier for that tail (that is where dispositions actually come from). A null codex verdict retries once on Claude, then surfaces as
unverified-panelrather than silently degrading. - Empirical deep pass. The economic-simulation lens (orchestrator 09) runs the localnet + Python sims and writes new adversarial scenarios to actually break invariants.
- Consolidate (no-slip) + curate. Read each harness's FULL output file (the notification preview is truncated), run
consolidate.pyto deterministically merge all three into ONEconsolidated-report.mdcovering open + settled + refuted + coverage with aDROPPED 0accounting guarantee, then reason over the committedpackages/predict/predeploy/open-items.mdand curate findings into it. The consolidator's coverage section catches buried items; never hand-pick from a truncated return.
Harnesses
Three complementary review shapes, all sharing this primer.md, the report format, and the read-only / compiler-in-main-loop discipline. Read primer.md first either way. Scope is REQUIRED for each — there is no whole-fleet default.
- Lens fan-out —
orchestrator.workflow.js: the 10 perspective lenses (broad multi-angle bug hunt). Or hand a singlelenses/NN-*.mdto one session. Scope withlenses: [<keys>],'security'(all lenses minus the cleanup-tier pair surface-area + architecture — the default choice for a bug hunt), or'all'. Loops until dry with per-lane retirement (maxRounds3 /dryRounds2 defaults; explicit cap args are the only tuning knobs) and panels High/Critical findings cross-model. - Ownership walk —
ownership-walk.workflow.js: recursive per-module conformance against the contextual ownership/boundary/policy rules inreferences/ownership-rules.md(R1–R7). Map (barrier) → single per-module check pass → one codex verifier per 'high' violation ('correctness'/'cleanup' reported raw). Scope withunits: [<keys>]or'all'. Use this for "who owns what / where are boundaries violated." - Rule sweep (refreshed from the old root
rule-auditor.md) —rule-sweep.workflow.js: per-rule sweep for the 17 mechanical/local rule families (config-reads, config API shape, public-API exposure, object-identity keys,create_and_sharenaming, ProtocolConfig gate matrix, arithmetic-guard noise, test-coverage, timestamp semantics, events hygiene, dead-field liveness — the exhaustive write-only / read-only-mirror sweep that catches the write-only-reserve bug class — plus the hygiene trio signature-shape / module-layout / comment-hygiene and the polish trio test-independence / docs-drift / abort-code-multiplexing). ONE agent per family, ALL findings raw — the operator is the verifier for this mechanical tier (it enumerates, it doesn't sample). Scope withrules: [<keys>]or'all'. The contextual ownership families (ex-rule-auditor 6/8/9) live in the ownership walk instead.
Axis split: lenses = perspective × whole codebase; ownership walk = per-module (deep context); rule sweep = per-rule × whole codebase (mechanical).
Launching a run (copyable)
Step 1 — ground truth, MAIN LOOP only (feeds every subagent via args.groundTruth; a wrong command starts the run from a false "all green"):
python3 .claude/skills/predict-audit/preflight.py # drift lint FIRST: fatal on primer module-map drift, warns on dangling D-ids
python3 packages/predict/predeploy/check.py # dev-system linter: register pinning tests, ID cross-refs, MEASURED links, dead paths
sui move build --path packages/<pkg> --warnings-are-errors # pkg ∈ predict propbook account
sui move test --path packages/<pkg> --gas-limit 100000000000 # each of the three (predict is the big suite)
(cd packages/predict && npm run build && npm test)
(cd packages/predict && python3 -m unittest discover -s simulations/tests -p 'test_*.py' -v)
(cd packages/predict && python3 -m harness parity --source /path/to/scenario_dataset.csv --max-rows 20) # localnet parity, MAIN LOOP only
preflight.py guards the primer (the single point of failure — its module map + D-id citations reach every subagent): a missing module path is fatal, and a cited D-id without an explicit canonical decision or response-policy entry warns. check.py guards the predeploy system itself (a register decision whose pinning test vanished is un-enforced; a dangling tracker cross-ref means a broken workflow). The workflows also self-warn if groundTruth looks empty/short, but run the lint yourself so drift is caught before the launch questionnaire.
Register obligations (every run). packages/predict/predeploy/response-policies.md is part of the audited surface: (a) re-verify each entry at HEAD — the code still implements the recorded response, the pinning tests still pin it, and docs/risks.md still describes it truthfully; drift between an entry and HEAD is itself a finding. (b) Do NOT re-flag a registered decision whose reasoning verifies at HEAD — cite the entry instead. (c) Anti-ossification: pick 1–2 entries per run and adversarially re-derive them from scratch, ignoring the recorded reasoning (a well-reasoned decision can still be wrong by omission — the P-1 circuit-breaker removal is the precedent: its fairness argument was sound and it still silently dropped the only u64-headroom bound on LP fill math).
Step 2 — launch a harness. Scope is REQUIRED (lenses/units/rules; the explicit 'all' is the whole-fleet opt-in — a no-arg launch errors with the valid keys). The orchestrator loops-until-dry but is BOUNDED BY CONSTRUCTION (maxRounds 3 / dryRounds 2 / verifyCap 60 defaults; total agents ≤ maxRounds×lenses + verifyCap×panel); the siblings are single-pass (1 agent per family / check unit, plus the walk's 'high'-tier verifiers). Do NOT rely on a +NNNm budget directive to bound a run — the budget often does NOT propagate into a background workflow (budget.total arrives null). Deepen the orchestrator by raising maxRounds/verifyCap/maxFindings via args; deepen a sibling by running it again (the next run is round 2).
Workflow({ scriptPath: '.claude/skills/predict-audit/orchestrator.workflow.js',
args: { groundTruth: '<build/test summary>', scope: 'delta since <sha>', lenses: 'security', files: ['<changed file>', '...'] } })
Workflow({ scriptPath: '.claude/skills/predict-audit/ownership-walk.workflow.js',
args: { groundTruth: '<build/test summary>', units: ['predict-plp'] } })
Workflow({ scriptPath: '.claude/skills/predict-audit/rule-sweep.workflow.js',
args: { groundTruth: '<build/test summary>', rules: 'all' } })
Step 3 — consolidate in the MAIN LOOP (no-slip guarantee). Each harness's FULL result is persisted to its task output file — the notification preview is TRUNCATED; never synthesize from it. Re-run any failed units first (Workflow({ scriptPath, resumeFromRunId })), then run the deterministic consolidator over the full files:
python3 .claude/skills/predict-audit/consolidate.py .claude/predict-review/reports/<run>/ <orchestrator.output> <ownership-walk.output> <rule-sweep.output>
It emits ONE consolidated-report.md (a per-run snapshot under reports/<run>/) covering open + settled + refuted + coverage, plus findings.json, with an ACCOUNTING — … DROPPED 0 line proving every input finding is accounted for (it exits non-zero if anything would be lost). Write the executive summary on top of that report — do NOT hand-pick from the truncated returns.
Step 4 — reasoned update to the committed worklist. Manually curate audit findings into packages/predict/predeploy/open-items.md, which is the only open-items document and the team-facing consolidated tracker for deploy gates, audit findings, stress-test follow-ups, and manual items. This must be a reasoning pass, not a script or mechanical overwrite:
- Read
packages/predict/predeploy/open-items.mdcompletely before editing it. - Compare the new
findings.jsonand the per-runconsolidated-report.mdagainst the committed worklist. - Preserve manual/non-audit items and existing grouping unless the content is stale.
- Merge by substance, not by exact wording: if a finding is already represented, refresh severity/action/evidence/source as needed instead of duplicating it.
- Add genuinely new findings under the most appropriate existing section, or create a concise new section only when none fits.
- Keep the committed item actionable: state the risk, current evidence, and required action; include the audit finding id only as supporting provenance when useful.
- Remove or edit resolved/stale items only after verifying the current tree or run output supports that change.
- Use normal file edits (
apply_patchin Codex), not a generated sync, becauseopen-items.mdis a general curated document and may contain manual items audit output cannot understand. - Then run the no-slip check so a curation drop is loud, not silent (the guarantee
track.pyused to give, without the mechanical merge):
It fails unless everypython3 .claude/skills/predict-audit/check_curation.py reports/<run>/findings.json packages/predict/predeploy/open-items.md [dispositions.json]openfinding (and every panel-deadunverified-panelfinding) is EITHER referenced by its 6-char id inopen-items.mdOR listed in an optionaldispositions.json({ "<id>": "reason" }) as a conscious merge/refute/duplicate/accept. Write a disposition for anything you curated by substance without pasting the id.
Incremental & delta runs (the everyday mode)
The whole-codebase last-line-of-defense run is the exception, not the default. For a per-PR or since-last-time audit, scope the run — marginal whole-codebase rounds are the lowest-yield tokens; diff-focused rounds are the highest.
args.files(orchestrator + rule-sweep) — the changed files. Lenses/families concentrate on them plus their direct callers/callees (grep both directions) and report anywhere the blast radius reaches. Scout the change set in the main loop first (git diff --name-only <base>..HEAD), then pass it.Ownership-walk uses
units, notfiles— map a changed file to its subsystem cluster and passunits:changed path unit predict/sources/plp/**predict-plppredict/sources/strike_exposure/**predict-strikepredict/sources/pricing/**predict-pricingpredict/sources/registry/**,market_managerpredict-registrypredict/sources/config/**predict-configpredict/sources/capabilities/**predict-capabilitiespredict/sources/{expiry_market,expiry_cash,order,ewma,builder_code,predict_account,constants}.movepredict-corepredict/sources/events/**predict-eventspropbook/**/account/**propbook/accountCross-run memory lives in the committed registers, not in run artifacts. There is no adjudication carry between runs: route a disposition worth remembering through the Predict development-system authority order to its single owning register, and make finders/verifiers prior-aware through that pointer. A disposition not worth committing is not worth carrying.
Watermark: record the audited SHA (in
reports/<run>/or anopen-items.mdnote) so the next incremental run scopesfiles/unitstogit diffsince it and refreshes the watermark.
Which harness, in what order
- "Audit Predict" / find bugs → the lens fan-out with
lenses:'security'(delta-scope viafilesfor a change set). For a pre-merge pass, add the ownership walk + rule sweep on the touched units/families. - Subsystem deep-dive → ownership walk on that
unitscluster. Mechanical hygiene / cleanup triage → rule sweep (raw findings; you triage) — add the cleanup lenses (lenses: ['surface-area','assertions','architecture']) when hunting surface/assertion/architecture cleanups specifically. - Last-line-of-defense (pre-deploy): ground truth → all three harnesses at
'all'scope in separate turns (so a slow one doesn't block the others, and to read each full output before the next) →consolidate.py→ reasoned update topackages/predict/predeploy/open-items.md.
Hard rules (non-negotiable)
- Read-only on source. Never modify
packages/*/sources/**. Audit-run writes are reports (to.claude/predict-review/reports/<date>/, gitignored), temp sims/scripts (to the session scratchpad), and the reasoned curation update topackages/predict/predeploy/open-items.md. - Compiler/localnet run in the MAIN LOOP, never in a subagent.
sui build,sui test, andpython3 -m harness paritytrip the 600s subagent watchdog and the run is lost (theCLAUDE.md"Common verification commands" guardrail). The orchestrator runs these itself and passes results in asargs; subagents reason from source, grep, git, and Python sims (fast, watchdog-safe). - After editing
consolidate.py, runpython3 .claude/skills/predict-audit/evals/test_consolidate.py(main loop). It locks the no-slip and id/dedup invariants found across skill reviews. It exits non-zero on regression; treat a red as a blocker. - Check the REAL exit code. Never pipe
sui build/testthroughtail(it masks failures). Grep the captured log forerror/Test result, or read${PIPESTATUS[0]}. - Be prior-aware. Do not re-litigate settled decisions. Before raising anything, apply the Predict development-system authority order. Do not use local ignored design scratch as authority for audit triage. A finding that matches an accepted/rejected decision is tagged with its D-id or committed-policy reference and downranked to Info, not raised as new.
- Verify each claim against the function body, not its name. Trace call sites with grep before judging. Prefer a few verified findings over many speculative ones; rank uncertain items low-confidence rather than overstating.
The 10 lenses (lenses/)
| # | Lens | Hunts |
|---|---|---|
| 01 | invariants & solvency | conservation, NAV/backing, rounding (R1–R3), value leaks — empirically tested |
| 02 | adversarial audit | actor-by-actor exploit chains + PoCs (theft, mint-from-nothing, brick, extract) |
| 03 | oracle, pricing & numerical | propbook stores + the external bs_oracle boundary + predict::pricing; fixed-point/sentinel/overflow |
| 04 | access control & capabilities | Move ownership/visibility, cap system, version-gating, cross-package auth |
| 05 | surface-area reduction | dead/over-broad authority paths, eliminable state, behavior-preserving cuts |
| 06 | assertions & error model | guard ownership/placement/dedup, error-name correctness |
| 07 | state machine & lifecycle | phase/lock atomicity, intra-PTB interleaving, stranded/non-landable states |
| 08 | cross-package trust boundaries | the split seams; forgeable/stub inputs; Account custody boundary |
| 09 | economic simulation & invariant fuzzing | localnet + Python adversarial sims that break solvency/NAV/rounding/liquidation |
| 10 | architecture, cohesion & maintainability | overgrown "god" modules, coupling, decomposition/DRY, readability vs the repo's move.md conventions |
Lenses 05 and 10 are the cleanup tier — excluded from lenses:'security'; name them explicitly (or use 'all') when cleanup is the goal.
References & self-validation
references/— distilled external audit knowledge (Move/Sui checklist, DeFi invariant classes, economic-attack catalog) that lenses cite instead of re-deriving.evals/— the harness's own regression guards, so it doesn't silently rot as the code changes:test_consolidate.py(consolidator no-slip + panel-health render),verify_corpus.json+verify-bench.workflow.js(verify-panel PRECISION bench — a cheap panel-only run),seeds.md(seeded-bug RECALL harness — apply a planted bug, confirm a run catches it), and theevals.mdchecklist (must-rediscover / must-settle / ground-truth-gate).preflight.py(skill root) lints primer/D-id drift before a run;check_curation.pyguards the findings.json → open-items.md hop.
Operational notes (read before relying on a single run)
- StructuredOutput flakiness. A small fraction (~5–15%) of find/check/sweep agents can exhaust the StructuredOutput retry cap on the most verbose units and return nothing (the run reports them under
failures/failed_families/failed_check_units, and the config log shows what ran). Mitigated by concise-candidate output + lower effort, but not eliminated. Recovery:Workflow({scriptPath, resumeFromRunId})re-runs only the failed units — successful agents return cached — so a partial run is cheaply completed. - Run-to-run variance. The orchestrator handles it in-harness (loop-until-dry with per-lane retirement — a converged lens retires individually). The sweep and walk are deliberately single-pass: their content enumerates rather than samples (in the last multi-round run no family or lane ever retired dry, and the extra rounds only lengthened the raw tail) — re-run them to sample again; the union across runs replaces rounds within one. A converged run is a strong floor; still treat it as a floor, not a proof of absence.
- Cost is bounded by construction. Orchestrator agents ≤
maxRounds×lenses +verifyCap×panel; siblings ≤ 1 agent per family / check unit (+ the walk's 'high' verifiers, capped byverifyCap). Verify is severity-gated — only High/Critical (orchestrator) and 'high' (walk) findings get subagents; the rest is raw for operator triage. Machine-verifying the tail was the historical cost blow-up; the operator dispositions it faster and cheaper. - Cross-model verify needs the codex CLI. The orchestrator's High/Critical panel runs codex refute + codex repro + Claude settled; the walk's 'high' verifier runs on codex — a different model than the Claude finder, for bias reduction. A
nullverdict (codex unavailable or a StructuredOutput flake) is retried once on the default Claude agent; if it still dies, the finding is surfaced asunverified-panel/ averifier-deadunverifiedentry — never silently dropped. High/Critical that lose the panel to attrition (<2 live verdicts) are flaggedpanel_degraded. Codex prompts are kept tightly scoped (it can balloon/hang on open-ended reads). NOTE: codex reasoning effort is governed by codex config, not the workfloweffortknob — confirm it runs at the intended tier when wiring. keptis three populations, not one (orchestrator).keptis sortedconfirmed→uncertain→unverified-panel, with per-status counts insummary(kept_confirmed/kept_uncertain/kept_panel_dead) and apanel_severityfield recording the panel's own (upgrades-only-excluded) severity. Triageconfirmedfirst; treatuncertain/panel-deadas needing a human look. The consolidator renders each with its own tag.