diff --git a/skills/riptide-assess-skill/LICENSE b/skills/riptide-assess-skill/LICENSE new file mode 100644 index 0000000..1ec757a --- /dev/null +++ b/skills/riptide-assess-skill/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Riptide + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/riptide-assess-skill/README.md b/skills/riptide-assess-skill/README.md new file mode 100644 index 0000000..b395b35 --- /dev/null +++ b/skills/riptide-assess-skill/README.md @@ -0,0 +1,96 @@ +# Riptide Assess — Solana protocol risk-assessment skill + +A Claude Code skill that turns a Solana program repo into a reproducible +risk assessment, backed by deterministic guided LiteSVM simulations of the real +on-chain program. + +## Overview + +Founders shipping to mainnet, and the auditors reviewing them, need risk +evidence that someone else can reproduce — not a vibe, not a one-off script that +only ran on one laptop. Riptide answers that need by running **deterministic, +guided LiteSVM simulations against the real program `.so`**: it constructs the +external account bytes the program reads (oracle prices, attestations), drives +the protocol's actual lifecycle under an exogenous stress swept over a declared, +fixed-seed region, and records where the deciding metric moves. The output is an +`assessment.md` / `assessment.json` plus an evidence pack and the exact commands +to reproduce every figure. + +This is **simulation evidence over a declared region — not an audit signoff**, +not formal verification, and not a mainnet prediction. + +## What it does + +One agent-led flow, **Detect → Scope → Setup → Run → Surface → Assess**: + +- **Detect** the protocol family (lending, AMM, perps, LST, stablecoin) from + semantics, source, and IDL evidence. +- **Scope** what the guided sim must handle via the A–F trigger taxonomy — typed + enum/struct args, oracle-account byte construction, third-party (liquidator / + keeper) actors, multi-instruction sequences, dynamic `remaining_accounts`, CPI + bootstrapping — then ask at most three scoped questions. +- **Setup** the project-owned Rust sim crate: author the adapter, generate the + crate, and fill the `TODO(setup)` seams with deterministic facts. +- **Run** the fixed-seed sweep declared in `[sim.sweep]` — smoke first, then the + full sweep. +- **Surface** the cartography root (`campaign-summary.json`, + `risk-surface.json`, `retention-manifest.json`) the assessment reads. +- **Assess** — render `assessment.md` + `assessment.json` (plus an executive + brief), with a non-negotiable honesty discipline enforced as runtime gates + (positive control, real-program execution, determinism). + +## Install + +If `riptide --help` fails, install the CLI via the public installer — this works +from any directory (idempotent, safe to re-run): + +```bash +curl -fsSL https://riptide.run/install | sh +``` + +This skill also bundles an equivalent `install.sh` at its package root (it wraps +the same installer); run `bash install.sh` only from within the skill's own +directory. + +**Riptide is NOT on npm** — do not `npm i riptide`. The CLI scaffolds a +project-owned Rust crate that builds against a vendored runtime (no separate +engine binary), so the host needs: + +- `rustup` / `cargo` + `rustc` +- `node >= 20` and `npm` +- the Solana SBF toolchain (`cargo-build-sbf`, via the Anza/Solana install) + +Verify before doing anything else: + +```bash +riptide --version # expect v0.12.0 or newer +riptide doctor # static health check of the environment + adapter +``` + +## Structure + +``` +riptide-assess-skill/ +├── skill/ +│ ├── SKILL.md # Router: mission, prerequisites, CLI surface, flow +│ ├── detect-and-scope.md # Detect the family + the A–F trigger taxonomy +│ ├── setup.md # Adapter, sim crate, deterministic setup seams +│ ├── run-and-assess.md # Run → Surface → Assess +│ ├── family-library.md # Per-family personas, invariants, stress scenarios +│ ├── authoring-patterns.md # Oracle bytes, third-party dispatch, sweep scaffold +│ ├── honesty.md # The non-negotiable honesty discipline + gates +│ ├── worst-case-playbook.md # Per-archetype worst case to hunt +│ └── resources.md # References + file index +├── examples/ +│ └── assessment-input.json # Example AssessmentInputs shape +├── install.sh # Riptide CLI bootstrap (idempotent) +├── README.md # This file +└── LICENSE # MIT +``` + +The router (`skill/SKILL.md`) keeps the entry gate and the CLI surface inline, +and links each step to its focused file for progressive disclosure. + +## License + +MIT License — see [LICENSE](LICENSE) for details. diff --git a/skills/riptide-assess-skill/examples/assessment-input.json b/skills/riptide-assess-skill/examples/assessment-input.json new file mode 100644 index 0000000..90c2da0 --- /dev/null +++ b/skills/riptide-assess-skill/examples/assessment-input.json @@ -0,0 +1,56 @@ +{ + "_comment": "Example AssessmentInputs for `riptide assess --input examples/assessment-input.json`. The skill authors a file like this from what the detect/scope/run steps established. All fields are optional — omitted fields are derived deterministically from the guided-sim cartography root. Adapt the values to your protocol; do not ship these placeholders verbatim.", + "protocol": { + "name": "example-lending" + }, + "verdict": "needs_guided_sim", + "riskPlan": { + "protocol_class": "over-collateralized lending", + "target_claim": "Lenders do not accrue bad debt while the collateral price stays within the declared drop band and liquidation can keep pace.", + "p0_flows": [ + "deposit_collateral", + "borrow", + "liquidate" + ], + "p1_flows": [ + "repay", + "withdraw_collateral" + ], + "expected_failure_modes": [ + "collateral price drop outruns permissioned liquidation, leaving under-collateralized debt the protocol absorbs as lender bad debt" + ], + "guided_sim_boundaries": [ + "collateral_price_drop_bps swept across the declared region; the bad-debt onset is read from the risk surface, not asserted", + "liquidation is a third-party action dispatched by a keeper persona; the oracle is a hand-constructed Pyth PriceUpdateV2 crashed in place" + ], + "known_coverage_limits": [ + "interest accrual over long horizons is out of the swept region", + "cross-program liquidation routes are not modeled in this campaign" + ] + }, + "coverage": [ + { + "priority": "P0", + "flow": "Collateral-crash liquidation race", + "status": "covered by guided sim", + "evidence_tier": "guided sim", + "commands": [ + "riptide sim run .riptide/sim --flows 20 --out .riptide/sim/artifacts/crash", + "riptide sim surface .riptide/sim/artifacts/crash --sim .riptide/sim" + ], + "artifacts": [ + ".riptide/sim/artifacts/crash/guided-sim-run.json" + ], + "notes": "Bad-debt onset emerges from the swept collateral_price_drop_bps axis; positive control fires the invariant beyond the onset." + }, + { + "priority": "P1", + "flow": "Cross-program liquidation routes", + "status": "out of scope", + "evidence_tier": "none", + "commands": [], + "artifacts": [], + "notes": "No deterministic local fixtures for the external venues; named as a scope boundary, not silently skipped." + } + ] +} diff --git a/skills/riptide-assess-skill/install.sh b/skills/riptide-assess-skill/install.sh new file mode 100755 index 0000000..18f9b66 --- /dev/null +++ b/skills/riptide-assess-skill/install.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# riptide-assess — Riptide CLI bootstrap. +# +# Installs the Riptide CLI if `riptide` is not already on your PATH, then +# verifies it. Idempotent: safe to re-run. +# +# Riptide runs deterministic guided simulations of Solana programs. `riptide +# sim generate` scaffolds a project-owned Rust crate that builds against the +# vendored runtime, so there is no separate engine binary to manage — but you +# do need the build toolchain below. +# +# Prerequisites the installer expects (Linux; macOS may work, untested): +# - rustup / cargo + rustc +# - node >= 20 and npm +# - the Solana SBF toolchain (cargo-build-sbf, via the Anza/Solana install) +# Riptide is NOT published to npm — do not `npm i riptide`. + +set -euo pipefail + +if command -v riptide >/dev/null 2>&1; then + echo "riptide already installed: $(riptide --version 2>/dev/null || echo '(version check failed)')" + exit 0 +fi + +echo "riptide not found on PATH — installing via the public installer..." +echo "(needs cargo + node + the Solana SBF toolchain; Riptide is not on npm)" +curl -fsSL https://riptide.run/install | sh + +# The installer drops a launcher into \$HOME/.local/bin. +if ! command -v riptide >/dev/null 2>&1; then + case ":${PATH}:" in + *":${HOME}/.local/bin:"*) : ;; + *) + echo "" + echo "riptide installed, but \$HOME/.local/bin is not on your PATH. Add it:" + echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" + echo "then re-open your shell (or 'source' your shell rc) and re-run this script." + exit 1 + ;; + esac +fi + +echo "" +riptide --version +echo "riptide is ready. Next: riptide doctor" diff --git a/skills/riptide-assess-skill/skill/SKILL.md b/skills/riptide-assess-skill/skill/SKILL.md new file mode 100644 index 0000000..6e0620f --- /dev/null +++ b/skills/riptide-assess-skill/skill/SKILL.md @@ -0,0 +1,123 @@ +--- +name: riptide-assess +description: >- + Assess a Solana program with Riptide — run deterministic, guided LiteSVM + simulations against the real on-chain program to produce a risk assessment + with reproducible evidence. Use when the user says "assess my protocol", "is + my protocol safe", "run Riptide on this", "give me a risk assessment", or + "riptide-assess", or points you at an Anchor/Solana lending, AMM, perps, + liquid-staking, or stablecoin repo and wants one agent-led flow from program + source to an assessment report. Detects the protocol family, scopes what the + guided sim must handle (typed args, Pyth/Switchboard oracle-account bytes, + liquidator/keeper actors, multi-instruction sequences), authors and runs the + sim, then returns assessment evidence and exact rerun commands. +user-invocable: true +--- + +# riptide-assess + +Riptide runs **deterministic guided simulations** of Solana programs to produce +a **risk assessment** backed by reproducible evidence. This skill is the single +front door: point it at a Solana program repo, answer at most three scoped +questions, and it detects the protocol family, authors a project-owned Rust +simulation crate that drives the program's real `.so` under stress, runs it over +a declared fixed-seed sweep, and emits an assessment (`assessment.md` + +`assessment.json`) plus an executive brief — with the exact commands to +reproduce every figure. + +There is one execution path: a **guided simulation**. The agent authors the +adapter, sim crate, personas, flows, and invariants — the user does not. + +The outcome is an `assessment.md` / `assessment.json` plus an evidence pack and +the **exact rerun commands** over a declared, fixed-seed region. This is +**simulation evidence over a declared region — not an audit signoff**, not +formal verification, and not a mainnet prediction. Hold that boundary in every +claim you make. + +## First Contact / Prerequisites (auto-install) + +If `riptide --help` fails, the CLI is not installed. Install it with the command +below — **it works from any directory**. You are running inside the user's +program repo, not the skill folder, so do not rely on a bare `install.sh` path: + +```bash +curl -fsSL https://riptide.run/install | sh +``` + +The skill also bundles an `install.sh` at the root of its package that wraps this +same installer; use it only if you have `cd`'d into the skill's own directory. +Both are idempotent — safe to re-run. + +**Riptide is NOT on npm** — do not `npm i riptide`. The CLI scaffolds a +project-owned Rust crate that builds against a vendored runtime (there is no +separate engine binary), so the host needs: + +- `rustup` / `cargo` + `rustc` +- `node >= 20` and `npm` +- the Solana SBF toolchain (`cargo-build-sbf`, via the Anza/Solana install) + +Confirm the install before doing anything else: + +```bash +riptide --version # expect v0.12.0 or newer +riptide doctor # static health check of the environment + adapter +``` + +## How To Run (the CLI surface) + +Use **only** these commands. Never invent flags or subcommands. + +- `riptide init` — scaffold a thin `.riptide/` bootstrap in the target repo. +- `riptide readiness [--json]` — read-only repo classification check. +- `riptide doctor` — static adapter/environment health check. +- `riptide sim generate --adapter ` — scaffold the project-owned + guided-sim crate (`.riptide/sim`). +- `riptide sim refresh --adapter --dir .riptide/sim` — regenerate + builders after IDL changes without overwriting hand-authored flows. +- `riptide sim run [--iterations N] [--flows N] [--seed HEX] --out ` + — execute the sweep; reads `[sim.sweep]` from `Riptide.toml`. +- `riptide sim surface --sim ` — build the cartography + root (campaign-summary.json + risk-surface.json + retention-manifest.json). +- `riptide sim lint ` — validate the sim manifest. +- `riptide sim review ` — review a run's retained evidence. +- `riptide sim fork` / `riptide sim debug` — fork-cache and debug helpers. +- `riptide review ` — root reviewer over a surfaced root. +- `riptide assess [--input ] [--brief] [--html|--pdf]` + — ingest a surfaced root and emit the assessment. + +The parameter sweep lives in the `[sim.sweep]` block of +`.riptide/sim/Riptide.toml` — it is configuration, **not** a CLI flag. Do not +pass the sweep on the command line; `riptide sim run` reads it from the TOML. + +## The flow — routing + +Detect → Scope → Setup → Run → Surface → Assess. Work in one continuous session. +Each step's depth lives in a focused file: + +1. **Detect the protocol family and scope what the guided sim must handle** (the + full A–F trigger taxonomy + the three scoped questions) → + [detect-and-scope.md](./detect-and-scope.md) +2. **Author the adapter, generate the sim crate, fill the setup seams, author + flows + the sweep** → + [setup.md](./setup.md) +3. **Run, surface, and assess** (smoke → full sweep → cartography root → final + render) → + [run-and-assess.md](./run-and-assess.md) + +Supporting depth: + +- **Family library** — the recurring personas, invariants, and stress scenarios + per protocol family; consult the detected family's entry during Scope before + designing the campaign → + [family-library.md](./family-library.md) +- **Authoring patterns (guided sim)** — the library code to wire when triggers + fire (oracle-account construction, third-party dispatch, the sweep scaffold) → + [authoring-patterns.md](./authoring-patterns.md) +- **Honesty Discipline (non-negotiable — read this)** — the 7 honesty rules, the + three runtime-enforced gates, and fail-fast/file-an-issue guidance → + [honesty.md](./honesty.md) +- **Worst-case playbook** — per-archetype worst case to hunt, axis to sweep, + deciding invariant/metric, signal trap, honest framing → + [worst-case-playbook.md](./worst-case-playbook.md) +- **References + file index** → + [resources.md](./resources.md) diff --git a/skills/riptide-assess-skill/skill/authoring-patterns.md b/skills/riptide-assess-skill/skill/authoring-patterns.md new file mode 100644 index 0000000..fcb5f84 --- /dev/null +++ b/skills/riptide-assess-skill/skill/authoring-patterns.md @@ -0,0 +1,77 @@ +# Authoring patterns (guided sim) + +The library code to wire while filling the setup seams and authoring the sweep, +once the A–F triggers tell you which patterns the protocol needs. + +When triggers fire, the hard-won parts are already library code. Wire these +instead of re-deriving them. + +**Oracle-account construction (Trigger B).** Use +`riptide_sim::oracle::PythPriceUpdate` to build the Pyth `PriceUpdateV2` account +a program's `get_price_no_older_than(...)` reads. The builder owns the full +134-byte layout verified against `pyth-solana-receiver-sdk` — never hand-roll +those bytes: + +```rust +use riptide_sim::oracle::{crash_in_place, PythPriceUpdate}; + +let mut update = PythPriceUpdate::new(FEED_ID, INITIAL_PRICE, -8, base_ts); +update.install(&mut sim.world, price_update_key)?; +// Later, mid-lifecycle: the crash (the swept stress), re-stamped fresh so the +// program's freshness window is isolated from the price move. +crash_in_place(&mut sim.world, &price_update_key, crashed_price, now)?; +``` + +For non-Pyth attestors (custom NAV attestations, Switchboard), follow the same +shape — a deterministic byte builder in your sim's `services/` with a +`set`/`crash` mutator — rather than scattering offsets through flows. + +**Third-party-actor dispatch (Trigger C).** Use +`riptide_sim::dispatch::ThirdPartyDispatch` for any instruction where one actor +signs and operates on another actor's position/order. Push accounts in IDL +order; `build()` rejects the three recurring hand-roll bugs (target marked +signer, actor never signing, a stray third signer): + +```rust +use riptide_sim::dispatch::ThirdPartyDispatch; + +let mut dispatch = ThirdPartyDispatch::new(liquidator, position_owner); +dispatch + .shared(protocol_state, false) + .target_account(position_pda, true) // owner's position; never signs + .actor_account(liquidator_ata, true) // receives seized collateral + .actor_signer(true); // the sole signer +let (metas, signer) = dispatch.build_with_signer()?; +``` + +**The sweep + control + invariant scaffold.** A guided sim destined for a +risk-surface assessment declares these blocks in `.riptide/sim/Riptide.toml`: + +```toml +[sim.sweep] # the exogenous stress axis +name = "collateral_price_drop_bps" +values = [0, 1000, 2000, 3000, 4000, 5000, 6000] +seeds_per_value = 4 + +[sim.positive_control] # the known-correct baseline coordinate +value = 0 # parameter defaults to the sweep name + +[sim.lifecycle] # core flows that must execute on-chain +required_flows = ["create_lend_offer", "accept_lend_offer", "liquidate_loan"] + +[sim.cartography] # surface metadata +class = "lending.v1" +risk_objective = "" +``` + +In `flows.rs`, read the swept coordinate with `world.sweep_value("")`, +echo it with `world.record_parameter`, record the deciding signal with +`world.record_metric`, and fire the deciding invariant with +`world.record_invariant_fire` when the metric crosses the stated risk line — the +playbook entry names the metric, the trap, and the line. Author negative +controls (a healthy-state action that must reject) with the transaction +builder's `expect_error()`, so a rejection is asserted, not silently tolerated. + +For the deep per-archetype worst-case archetypes (worst case to hunt, axis to +sweep, deciding invariant, signal trap, honest framing), see +[worst-case-playbook.md](./worst-case-playbook.md). diff --git a/skills/riptide-assess-skill/skill/detect-and-scope.md b/skills/riptide-assess-skill/skill/detect-and-scope.md new file mode 100644 index 0000000..f0362cf --- /dev/null +++ b/skills/riptide-assess-skill/skill/detect-and-scope.md @@ -0,0 +1,145 @@ +# Detect & Scope + +The first two steps of the flow: establish the protocol family from real +evidence, then classify what authoring complexity the guided sim must handle so +the sim crate lands the flows right the first time. + +## 1. Detect + +1. Establish the repo root from `.riptide/`, `Anchor.toml`, `Cargo.toml`, + `target/idl`, or the current directory. +2. Read existing artifacts before asking the user: `.riptide/adapters/*.toml` + (especially `[semantics].class`), `target/idl/*.json`, `app/src/idl/*.json`, + source, tests, any existing `.riptide/sim/`, and `target/deploy/*.so`. +3. Optionally classify with the read-only check: `riptide readiness . --json`. +4. Classify the protocol family from semantics first, else source/IDL evidence: + - **lending** — `borrow`, `repay`, `deposit`, `withdraw`, `liquidate`, + collateral, debt, reserve, oracle. + - **amm** — `swap`, `add_liquidity`, `remove_liquidity`, pool, reserve, LP + mint, fee, tick/price. + - **perps** — `open_position`, `close_position`, margin, leverage, funding, + oracle, insurance fund. + - **lst** — stake, unstake, exchange rate, validator, reserve, withdrawal + queue, slash. + - **stablecoin** — mint, redeem, collateral, liability, peg, PSM, reserve, + hedge. +5. Record a one-screen detection note: family, semantic class, confidence + (`high`/`medium`/`low`), evidence paths, competing interpretations. If + confidence is low between two families, ask one classification question + (counts toward the three-question limit). +6. Once the family is fixed, read that family's entry in + [family-library.md](./family-library.md) — the recurring personas, + invariants, and stress scenarios for the archetype. Treat it as the starting + menu: apply/adapt the entries that fit the target program's real + instructions and accounts, then add protocol-specific ones the program's own + flows demand. Do this **before** designing the campaign, not instead of it. + +Read the P0/P1 state-changing instructions: for each, read the IDL `args` and +`accounts` entries plus the handler source. This feeds the next step. + +## 2. Scope — classify what the guided sim must handle (A–F) + +There is ONE execution path (the guided sim). This step is not "which path" — +it is "**what authoring complexity** does this protocol need", so the sim crate +lands the flows right the first time. For every P0/P1 instruction, check the six +triggers below. Each trigger that fires names a concrete authoring pattern. + +**Trigger A — non-primitive or enum instruction arguments.** The instruction +takes an enum, struct, `String`, or `Vec` argument, which raw scalar dispatch +cannot encode. Detect: IDL argument types other than integers, bools, and +pubkeys — `"defined"`, `"string"`, or `"vec"` entries in the IDL, or enum/struct +parameters in the handler signature. Worked example: a `swap` taking a +`SwapDirection` enum, or order placement taking side/kind enums — both need +typed argument builders in a generated sim crate. + +**Trigger B — external oracle accounts needing byte-construction.** The program +reads price or attestation bytes from an account owned by an external program +(Pyth receiver, Switchboard, a custom attestor), and the stress axis is that +account's contents, so the sim must construct and mutate those bytes +deterministically. Detect: external SDK account types in the handler (for +example `pyth_solana_receiver_sdk::price_update::PriceUpdateV2`), calls like +`get_price_no_older_than`, or freshness windows checked against the clock. +Worked example: a liquidation reads a Pyth `PriceUpdateV2`, so the sim builds the +account bytes and crashes the price; a withdrawal checks a NAV-attestation +account inside a freshness window. + +**Trigger C — third-party / target-vs-agent actions.** An actor signs an +instruction that operates on another actor's position or order — liquidator, +keeper, matcher, settler. A self-signed persona action only expresses an agent +acting on its own accounts. Detect: instruction account sets that contain both a +signer and a different user's position/order PDA — `liquidate`, `settle`, +`slash`, keeper cranks. Worked example: a liquidation that lets any third party +repay a borrower's debt and seize collateral; a keeper that settles a buyer and +a seller it does not own. + +**Trigger D — multi-instruction sequences.** A flow only completes across an +ordered multi-instruction transaction or a multi-transaction sequence (request, +then execute, then claim). Detect: request/execute instruction pairs, +pending-state accounts, or instruction-introspection requirements such as a +required ed25519 verification instruction. Worked example: a withdrawal that is +a multi-transaction sequence whose execute step must land inside the attestation +window; a flow requiring an ed25519 signature verification instruction ahead of +the consuming instruction in the same transaction. + +**Trigger E — dynamic `remaining_accounts`.** The instruction's account set +varies per call with protocol state, so no static account mapping exists. +Detect: `ctx.remaining_accounts` in handlers, or loops over member/position +lists. Worked example: a slash redistribution that iterates every remaining +member's account; an integration that passes a dependency account set changing +per call. + +**Trigger F — custom CPI bootstrapping.** Reaching a runnable tick-0 state needs +CPIs into external programs, or manual deployment and configuration of sibling +programs. Detect: init handlers that CPI into a dependency program, multi-program +genesis in `Anchor.toml` test config, or registration steps in the test suite. +Worked example: a program that must bootstrap its dependency programs and +register its signature oracle before any flow can run. + +**Verdict:** + +- **No trigger on any P0/P1 flow → `baseline-sim`.** Low-touch: primitive + arguments, self-signed instructions, no externally owned account bytes to + evolve mid-run. Confirm by running, not reading — a one-seed smoke. Borderline + calls (a keeper-reward liquidation that might still be self-service; a mock + oracle passed as a primitive argument a real deployment would replace with an + oracle account) flip on real evidence — record the fragility. +- **One or more triggers on a P0 flow → `guided-sim-authored`** for those flows: + the sim hand-authors the patterns the triggers named. Trigger-free flows stay + low-touch within the same crate. +- **FHE/MPC/ZK, external-venue execution, or off-chain matching the sim cannot + model → `unsupported`** for those surfaces. Name them as scope boundaries + instead of silently skipping them. + +Record the classification note and carry it into the final report: + +```text +program: +archetype: +triggers: +authoring patterns: +verdict: +``` + +Before asking any scoping question, pair two references for the archetype: +[family-library.md](./family-library.md) for the recurring personas, +invariants, and stress scenarios to start from, and +[worst-case-playbook.md](./worst-case-playbook.md) for the worst case to hunt, +the axis to sweep, and the deciding invariant/metric. The library seeds the +campaign; the playbook sharpens it to the worst case. + +Then ask **no more than three questions total**, one at a time, never for facts +already visible in source/IDL/tests/`.riptide`: + +1. **Primary risk objective** — two to four options derived from the family and + actual surfaces; recommend the archetype default unless evidence points + elsewhere. +2. **Flow emphasis** — stress-flow families or program-specific flows matching + real instructions/accounts; include one "balanced default". +3. **Missing assumption** — only when a material fact is not derivable (oracle + account layout, authority policy, dependency fixture source, intended fee + cap, accepted scope exclusion). + +If the user says "use defaults", proceed with archetype defaults narrowed to the +program, and still show the choices before running. diff --git a/skills/riptide-assess-skill/skill/family-library.md b/skills/riptide-assess-skill/skill/family-library.md new file mode 100644 index 0000000..3ac0252 --- /dev/null +++ b/skills/riptide-assess-skill/skill/family-library.md @@ -0,0 +1,140 @@ +# Family Library + +A starting knowledge base of the personas, invariants, and stress scenarios that +recur per protocol family. **Consult this file during Scope, before inventing a +simulation campaign from scratch.** For the detected family, read its entry, +apply or adapt what genuinely fits the target program's real instructions and +accounts, then add the protocol-specific personas/invariants/axes the program's +own flows demand. This is a menu of well-worn starting points, not a mandate — +skip anything the program doesn't actually expose, and never seed an actor or an +invariant the code cannot support. + +How the three columns map onto the guided-sim contract: + +- **Known personas** → adapter `[personas]`. Each is an actor archetype with an + action vocabulary; codegen renders them into `flows.rs`. Seed the ones whose + actions the program actually exposes; edit `action_weights` to match. +- **Known invariants** → `[[invariants]]`. Semantic expressions checked over + recorded observations. A flat aggregate (e.g. "no bad debt") becomes a metric + you `world.record_metric` and a `[[invariants]]` expr that fires when it + crosses the stated line. Wire the underlying observation first — an invariant + over a field you never record is dead. +- **Known stress scenarios** → the `[sim.sweep]` axis. Each names an exogenous + stress to sweep with a fixed-seed region and a healthy control at the origin. + The [worst-case-playbook.md](./worst-case-playbook.md) entry for the family + turns the chosen scenario into the worst case to hunt, the deciding metric, + and the signal trap. + +Every family also inherits a shared **generic persona pool** — +`arbitrageur`, `liquidator`, `whale`, `sandwich-attacker` (approximate), +`rug-puller` (opportunistic exit), `lp-provider`, `swapper`, +`leveraged-long`/`-short`, `delta-neutral-farmer`, `funding-arbitrageur` +(proxy). Pull any of these into any family when the program's surface warrants +it (a `liquidator` on anything with a liquidation path, a `whale` wherever +size concentration matters), then re-weight their actions to the real vocab. + +--- + +## lending + +- **Known personas:** `steady-lp` (long-tail supplier), `cautious-yield-farmer`, + `degen-borrower`, `aggressive-arb-bot`, `panic-whale` (mass withdraw), + `whale` (outsized borrow). Plus `liquidator` from the generic pool — anything + with a `liquidate` path wants a third-party liquidator actor (Trigger C). +- **Known invariants:** + - `no_bad_debt` — cumulative bad debt stays at zero; fires on any liquidation + insolvency (the deciding invariant for the crash-outruns-liquidation case). + - `debt_below_collateral` — `debt_value <= collateral_value` per position. + - `debt_below_max_borrow` — `debt_value <= max_borrow_value` per position. + - `health_factor_positive` — `health_factor > 1.0` per position. + - `utilization_bound` — pool utilization stays `<= 100%`. +- **Known stress scenarios:** + - **Oracle price shock** — crash the collateral oracle mid-run (a 40% drop is + the classic preset); surfaces under-collateralized positions and bad-debt + events. Swept as `collateral_price_drop_bps`. + - **Bank run** — a steeper, longer-tailed drop with mass withdrawals + (`panic-whale` + `degen-borrower` + `cautious-yield-farmer`); stresses the + withdrawal path and reserve under concentrated exit. + +## amm + +- **Known personas:** `swapper` (vanilla A→B), `arbitrageur` (one-sided + swapper), `lp-provider`, `sandwich-attacker` (approximate), `rug-puller` + (opportunistic LP exit). +- **Known invariants:** + - `k_invariant` — the constant product `k` does not decrease after fees + (template; wire a reserve-product observation first). Value leaking below + `k` net of fees is a real defect, distinct from LP impermanent loss. +- **Known stress scenarios:** + - **Baseline smoke** — normal price noise, no perturbation; the wiring smoke + test every family shares. + - **External price divergence** — move the paired asset's external-market + price and let an `arbitrageur` rebalance the pool toward it; the swept axis + is `price_divergence_bps`. AMMs frequently classify as `baseline-sim`; the + interesting signal is LP redeemable value versus hold, not the spot price. + +## perps + +- **Known personas:** `leveraged-long`, `leveraged-short`, + `delta-neutral-farmer`, `funding-arbitrageur` (proxy), `liquidator`. +- **Known invariants:** + - `no_socialized_loss` — a socialized-loss observation stays at zero once the + funding and liquidation paths are wired (template). + - `leverage_bound` — observed max leverage stays under the protocol cap + (template; default cap 10x — set to the program's real cap). +- **Known stress scenarios:** + - **Baseline smoke** — normal price noise, no perturbation. + - **Mark-price gap** — gap the mark oracle past maintenance margin so + liquidation recovers less than the position's loss; swept as + `mark_gap_bps`. The deciding signal is insurance-fund drawdown / + socialized loss, not "did liquidation fire". + +## lst (liquid staking) + +- **Known personas:** `steady-staker`, `yield-maxi`, `panic-exiter`, + `arb-redeemer`. +- **Known invariants:** + - Stake-pool solvency — the pool's redeemable backing covers outstanding + LST supply at the posted exchange rate (wire a backing/supply observation, + then check `backing_value >= lst_supply * exchange_rate`). +- **Known stress scenarios:** + - **Baseline smoke** — normal price noise, no perturbation. + - **Backing markdown / validator slash** — mark down staked backing (a slash) + while the exchange rate is stale-high and a `panic-exiter`/`arb-redeemer` + runs the withdrawal queue; swept as `backing_markdown_bps`. The signal is + realized redeemed value per share versus true backing (first-mover + dilution), not the posted exchange rate. + +## stablecoin + +- **Known personas:** `cautious-minter`, `leverage-looper`, `panic-redeemer`, + `arb-redeemer`. +- **Known invariants:** + - `backing_ratio` — collateral backing stays `>= 1` (100%) once supply and + backing observations are wired (template). +- **Known stress scenarios:** + - **Baseline smoke** — normal price noise, no perturbation. + - **Collateral crash under redemption race** — drop the collateral oracle + while `panic-redeemer`/`arb-redeemer` race the redemption/PSM path; swept + as `collateral_price_drop_bps`. The signal is realized redeemable collateral + per unit for the *marginal* (last) redeemer, not the peg or total supply. + +## orderbook + +- No prebuilt persona/invariant/scenario bucket ships for orderbooks — derive + from the target program's match/settle flows. Settlement is a keeper-driven + third-party flow (**Trigger C**): model the keeper/matcher/settler as a + distinct actor operating on positions/orders it does not own. The worst case + is typically a settle/match at a stale or crossed price; sweep the price + staleness / spread-cross magnitude and measure realized fill value versus a + fair reference. + +## custom / other + +- No family bucket. Fall back to the generic persona pool, derive invariants + from the program's own conservation laws (what quantity must be conserved, + bounded, or monotonic?), and pick the swept axis from the single most + economically consequential exogenous input the P0 flow reads (a price, a + rate, an attestation, a demand fraction). The A–F triggers in + [detect-and-scope.md](./detect-and-scope.md) tell you which authoring + patterns that input forces. diff --git a/skills/riptide-assess-skill/skill/honesty.md b/skills/riptide-assess-skill/skill/honesty.md new file mode 100644 index 0000000..525ecab --- /dev/null +++ b/skills/riptide-assess-skill/skill/honesty.md @@ -0,0 +1,65 @@ +# Honesty Discipline (non-negotiable) + +This is the credibility spine of every assessment — read it before delivering +any result. The runtime enforces the first three rules as execution-honesty +gates; the rest are framing rules only you can keep. + +Riptide has no automatic oracle for "is this finding real and honestly framed", +so the report's credibility rests on these rules. The runtime enforces the first +three as execution-honesty gates — evaluated at `riptide sim surface`, warned at +`riptide sim run`, and **blocking** at `riptide assess` — and the rest are +framing rules the gates cannot check, so following them is on you. + +1. **Positive control.** Every sweep declares a coordinate whose outcome is + known-correct (`[sim.positive_control]`, usually axis value `0`: no shock, + fresh-and-true attestation paying exact pro-rata) and that coordinate must + pass. Without it a flat surface is unfalsifiable. *Enforced: the + `positive_control` gate blocks emit when the declaration is missing, the + coordinate never ran, or it fired an invariant.* +2. **Real-program execution — never mock.** Flows execute the target program's + real `.so`. Constructed external-account bytes (oracle prices, attestations) + are the stress input, not a mock of the target; the target program itself is + never stubbed or reimplemented. A flat result is only robustness if the + intended lifecycle actually ran on-chain. *Enforced: the `lifecycle_executed` + gate blocks emit when declared `[sim.lifecycle] required_flows` never executed + successfully — "no-op, not robustness".* +3. **Determinism.** Fixed seeds, fixed amounts, no wall clock, no network. The + same command must reproduce the same bytes. *Enforced: the `determinism` gate + re-hashes `risk-surface.json` at emit against the hash recorded at surface + time, and `riptide assess` refuses to overwrite an existing + `assessment.json`/`assessment.md` that does not match the freshly rendered + bytes.* +4. **Scope and boundary framing.** The result is evidence over the declared + region — one configuration, the swept axis, the listed flows — not a safety + statement. Name what was held fixed, what was out of scope (e.g. an oracle's + own staleness guards when you drive the price directly, an external reserve + that is inert in stub mode), and say "evidence over the declared region", + never "safe". +5. **Robustness is a valid result — never manufacture a finding.** A flat + surface with the positive control passing and the lifecycle executed is a + real, publishable robustness result. State the structural reason the guard + holds rather than asserting safety, and do not tighten thresholds or distort + scenarios until something fires. +6. **The fire-threshold is a stated risk line; the gradient is the signal.** The + invariant's threshold (1% of reserve, 1% of debt value) is a chosen reporting + line, not a discovered boundary. Report where the metric starts moving and + where it crosses the line — both are surface facts. +7. **Exogenous-axis cover-framing.** The swept axis is an exogenous stress (a + market crash, a markdown), not a protocol knob. The auto-generated finding + title and any "keep `` in {…}" safe-region line must be reframed in your + delivery as "the onset sits at X on the axis" — a fact about where the risk + begins, never a tuning instruction to the protocol team. No gate can check + this; it is a delivery-step rule. + +## When it hits a wall — fail fast, file an issue + +If a protocol surface cannot be modeled — FHE/MPC/ZK, external-venue execution, +off-chain matching the sim cannot drive — do **not** paper over it. Name it as an +explicit **scope boundary** in the assessment (verdict `unsupported` for that +surface) and state what evidence the rest of the run still produced. + +If a blocker is in Riptide itself (a CLI validation gap, a missing builder, a +runtime limitation), report the blocked state with the exact failed command, the +error summary, what you repaired, and the smallest missing fact — then file or +link an issue at `https://github.com/riptidesim/riptide/issues`. Never substitute +a hand-written report for a blocked assess gate. diff --git a/skills/riptide-assess-skill/skill/resources.md b/skills/riptide-assess-skill/skill/resources.md new file mode 100644 index 0000000..f35c27c --- /dev/null +++ b/skills/riptide-assess-skill/skill/resources.md @@ -0,0 +1,35 @@ +# References & file index + +Where the deeper material lives, and the public source of record for the full +configuration contract. + +## References + +- Riptide (public repo + full configuration contract): + `https://github.com/riptidesim/riptide` +- Issues: `https://github.com/riptidesim/riptide/issues` +- Per-archetype worst-case authoring: [worst-case-playbook.md](./worst-case-playbook.md) +- Assessment-input shape: [`../examples/assessment-input.json`](../examples/assessment-input.json) + +## Skill file index + +- [SKILL.md](./SKILL.md) — the router: mission, prerequisites/auto-install, the + CLI surface, and the flow. +- [detect-and-scope.md](./detect-and-scope.md) — step 1 (Detect the protocol + family) + step 2 (Scope with the full A–F trigger taxonomy and the three + scoped questions). +- [family-library.md](./family-library.md) — per-family starting menu of + personas (→ `[personas]`), invariants (→ `[[invariants]]`), and stress + scenarios (→ `[sim.sweep]`); consulted during Scope before campaign design. +- [setup.md](./setup.md) — step 3: author the adapter, generate the sim crate, + fill the `TODO(setup)` seams with deterministic facts, declare external + programs/accounts/forks. +- [run-and-assess.md](./run-and-assess.md) — steps 4–6: run (smoke → full + sweep), surface the cartography root, and render the assessment with the brief. +- [authoring-patterns.md](./authoring-patterns.md) — the library code to wire + when triggers fire: oracle-account construction, third-party dispatch, the + sweep + control + invariant scaffold. +- [honesty.md](./honesty.md) — the 7 honesty rules, the three runtime-enforced + execution-honesty gates, and fail-fast/file-an-issue guidance. +- [worst-case-playbook.md](./worst-case-playbook.md) — per-archetype worst case + to hunt, axis to sweep, deciding invariant/metric, signal trap, honest framing. diff --git a/skills/riptide-assess-skill/skill/run-and-assess.md b/skills/riptide-assess-skill/skill/run-and-assess.md new file mode 100644 index 0000000..fecdedb --- /dev/null +++ b/skills/riptide-assess-skill/skill/run-and-assess.md @@ -0,0 +1,76 @@ +# Run, Surface & Assess + +The final three steps of the flow: validate and smoke the sim, run the full +sweep, build the cartography root the assessment reads, then render the +assessment with the brief. + +## 4. Run + +Validate, then smoke before the full sweep: + +```bash +riptide sim lint .riptide/sim +riptide sim run .riptide/sim --iterations 5 --flows 20 --seed 1337 --out .riptide/sim/artifacts/smoke +riptide sim review .riptide/sim/artifacts/smoke +``` + +`riptide sim run` reads `[sim.sweep]` and runs one iteration per +(value, seed replicate). Verified options: `--iterations `, `--flows `, +`--seed `, `--out `. Do not run the full sweep until the one-seed smoke +passes. + +Classify failures: **setup errors** are repair-loop inputs — return to the +responsible layer (adapter / setup seam / flow), fix it, and rerun from the +earliest affected gate. **Invariant failures are evidence, not setup failure** — +the artifacts are still reviewable; continue. If a flow needs Rust the crate does +not yet author, write it in `.riptide/sim/src/flows.rs` and keep coverage +bounded. + +Once the smoke passes, run the full sweep: + +```bash +riptide sim run .riptide/sim --flows 20 --out .riptide/sim/artifacts/ +``` + +## 5. Surface + +Build the cartography root the assessment reads: + +```bash +riptide sim surface .riptide/sim/artifacts/ --sim .riptide/sim +``` + +This writes the cartography root — `campaign-summary.json` + +`risk-surface.json` + `retention-manifest.json` — and records the +execution-honesty gate report. Note the root path it prints. + +## 6. Assess + +Author a repo-local `.riptide/assessment-input.json` (an `AssessmentInputs` +object) before the final render — it turns the generic templated defaults into +an assessment that names the protocol's actual flows, figures, and boundaries. +Skipping it ships the generic layer. Cover at minimum `verdict`, +`riskPlan.target_claim`, `riskPlan.guided_sim_boundaries`, and explicit +`coverage[]` rows (one per P0 flow: `priority`, `flow`, `status`, +`evidence_tier`, `commands`, `artifacts`, `notes`). Use an accepted `status` +(`covered`, `covered by guided sim`, `blocked`, `out of scope`, `not assessed`). +Every line must be backed by what actually ran — it adds protocol nouns and +figures, never new findings. See [`../examples/assessment-input.json`](../examples/assessment-input.json) +for the shape. + +Review the surfaced root, then generate the assessment with the brief — this is +the standard invocation, not an on-request variant: + +```bash +riptide review .riptide/sim/artifacts// +riptide assess --brief --input .riptide/assessment-input.json +``` + +`riptide assess` is ingest-only: it reads the surfaced root, re-verifies the +execution-honesty gates, and emits `assessment.json` + byte-deterministic +`assessment.md` (plus `brief.html` / `brief.pdf` with `--brief`). It **blocks** +on any failed gate. Use `--html` / `--pdf` only when the user asks for +presentation exports. A blocked assess that names a failed gate is a setup +repair (fix the positive control / make required lifecycle flows execute / +restore determinism, then re-run `sim run` + `sim surface` + `assess`) — never +a hand-written report. diff --git a/skills/riptide-assess-skill/skill/setup.md b/skills/riptide-assess-skill/skill/setup.md new file mode 100644 index 0000000..f9207dd --- /dev/null +++ b/skills/riptide-assess-skill/skill/setup.md @@ -0,0 +1,92 @@ +# Setup — the guided-sim authoring contract + +Step 3 of the flow: with the family detected and the triggers classified, author +the adapter, generate the project-owned sim crate, fill the setup seams with +deterministic facts, and author the flows + the sweep. + +## 3. Setup (folded inline — the guided-sim authoring contract) + +This skill is self-contained: the setup below is the essential authoring +altitude. For the full configuration contract (repair-loop taxonomy, profiles, +verdict semantics), see the public repo at +`https://github.com/riptidesim/riptide` — it is optional reading, never a +required co-located file. + +**a. Bootstrap the scaffold.** + +```bash +riptide init # only if .riptide/ is absent and the program name is unambiguous +``` + +`riptide init` creates only a thin `.riptide/` bootstrap (adapter placeholder + +`GETTING-STARTED.md`). You own the rest. + +**b. Author the adapter** (`.riptide/adapters/.toml`). It declares +account shape, instruction mappings, scheduled actions, observations, personas, +invariants, semantics, oracle channels, and `[lineage]`. Rules: + +- If `program_so` and `idl_path` are both set, the runtime is Generic SBF/IDL. +- Every required IDL account must be represented by `[accounts.]`, a + recognized signer alias (`authority`, `owner`, `user`, `payer`), a well-known + program/sysvar alias, or an IDL literal `address`. Do not omit setup-heavy + accounts (`price_update_v2`, reserve vaults, per-agent token accounts) just + because generated setup fills their bytes later. +- Top-level `[[invariants]]` may reference only keys declared in + `[observations]`. +- Always include `[lineage]` with the IDL source, assumptions, and unsupported + surfaces. + +Validate after every adapter edit: `riptide doctor`. Fix any named field error +before moving on. + +**c. Generate the sim crate.** + +```bash +riptide sim generate --adapter .riptide/adapters/.toml +``` + +This scaffolds `.riptide/sim` with `Riptide.toml`, `src/flows.rs`, +`src/invariants.rs`, generated `types.rs` / `accounts.rs`, and a `services/` +directory. Setup code carries `TODO(setup)` markers where pre-tick-0 state must +exist. Keep `types.rs` / `accounts.rs` regenerated-only; put hand-authored +actions, dynamic account resolution, and service models under `flows.rs`, +`invariants.rs`, `services/`. + +**d. Fill the `TODO(setup)` seams.** Fill every seam with **deterministic +facts** — account bytes, SPL mints/vaults, PDAs, sibling programs, oracle +accounts — derived from local source, IDL, tests, constants, and fixtures. +Fixed amounts, fixed decimals, fixed seeds, no network calls. **TODO-only setup +is not acceptable** when setup-heavy accounts are required and derivable: before +declaring a blocker, inspect source/tests/IDL/dependency types/constants/fixtures +for owners, discriminators, sizes, PDA seeds, feed IDs, and serialization. If a +fact genuinely cannot be determined locally, stop and report +`blocked = missing deterministic for guided-sim setup`, naming the +account/instruction — never hide it behind a vague comment. + +Declare external programs, accounts, and forked snapshots generically in +`Riptide.toml` (do not teach Riptide core protocol-specific layouts): + +```toml +[[sim.programs]] +address = "" +program = "../target/deploy/dependency.so" + +[[sim.accounts]] +address = "" +filename = "fixtures/accounts/dependency-account.json" + +[[sim.fork]] +address = "" +cluster = "mainnet" +filename = "fork-cache/mainnet/dependency-account.json" +overwrite = false +``` + +**e. Author flows, personas, and the sweep.** Generic personas stay inline in +the adapter; the sweep and flows in the crate drive them. Map triggers to seams: +A → typed builders; B → deterministic oracle-account bytes in setup seams or +project-owned services; C/D/E → hand-authored flows in `flows.rs`; F → +`Riptide.toml` program/account declarations plus bootstrap services. Then +declare the sweep + evidence-honesty blocks in `.riptide/sim/Riptide.toml` (see +[authoring-patterns.md](./authoring-patterns.md)). Map IDL changes forward with +`riptide sim refresh --adapter .riptide/adapters/.toml --dir .riptide/sim`. diff --git a/skills/riptide-assess-skill/skill/worst-case-playbook.md b/skills/riptide-assess-skill/skill/worst-case-playbook.md new file mode 100644 index 0000000..f08a399 --- /dev/null +++ b/skills/riptide-assess-skill/skill/worst-case-playbook.md @@ -0,0 +1,346 @@ +# Worst-Case Playbook + +Per-archetype authoring guidance for guided-sim assessments. The riptide-assess +skill loads this file when the execution-path classification returns +`guided-sim-authored`: the archetype from the classification note selects an +entry, and the entry tells the authoring pass what worst case to hunt before +any code is written. + +Every entry uses the same fields: + +- **Worst case to hunt** — the economically real failure this archetype is + exposed to, stated as a concrete scenario against the target program, not a + generic category. +- **Swept axis** — the exogenous stress parameter to sweep (declared as + `[sim.sweep]` in `.riptide/sim/Riptide.toml`) and the range that brackets + the interesting region. +- **Deciding invariant / metric** — the invariant or metric whose movement + decides the verdict, with the severity that makes the failure gradient + visible on the risk surface. +- **Signal trap** — where a naive measurement reads flat while the real + signal moves; name the field to measure instead. +- **Honest framing** — how to state the result: inherent risk versus bug, + the swept axis as exogenous stress rather than a protocol knob, and the + reminder that a held invariant across the full sweep is a robustness + result, not a failed assessment. + +If the target program's archetype has no entry here (a niche or hybrid family), +derive the worst case from the target program's actual P0 flows and its +conservation laws, and keep the five fields above as the working structure. The +[family-library.md](./family-library.md) entry for the family names the personas, +invariants, and stress scenarios these entries build on. + +## irs (interest-rate swap) + +- **Worst case to hunt:** The LP pool is the unhedged counterparty to a + one-sided book under a rate move that goes against it. A balanced book + self-hedges and shows zero LP loss, so seed the **adversarial** book: every + trader on the same side (e.g. all-PayFixed), then shock the rate index in the + direction that makes that side win. The pool pays the winners. Run the full + lifecycle on the real program: LPs deposit → traders `open_swap` (all one + side) → a rate-index shock via the program's oracle setter → `settle_period` + → third-party `liquidate_position` → LP `request_withdrawal`, with negative + controls (a stale-index `open_swap` must reject; a healthy-position + liquidation must reject). +- **Swept axis:** Rate-shock magnitude in bps (the exogenous market rate move), + bracketed from `0` (healthy control) up to just under the program's + per-period rate breaker — e.g. `rate_shock_bps ∈ {0, 100, 200, 300, 400, 490}` + under a 5%/period breaker. Declare it `[sim.sweep]` with a healthy `0` control. +- **Deciding invariant / metric:** Metric `lp_outflow` — the LP pool's payout + to the winning side, summed from each position's realized PnL. The deciding + invariant fires (error-severity, e.g. `lp_outflow_material`) when outflow + crosses a **stated** risk line (e.g. 1% of seeded reserve), which gives the + surface a visible gradient; the hard solvency bound + (`collateral_vault + lp_vault` covers all claims, no `unpaid_pnl`) is checked + separately and is expected to hold. +- **Signal trap:** Measure **`realized_pnl` summed across positions**, not + `lp_nav` or an LP-share token balance. The pool's bookkeeping NAV field can + read flat while value is actually flowing to the winning traders; the real + outflow only shows up in the positions' settled PnL. Reading the stub-inert + NAV field is how an agent concludes "no signal" on a pool that is in fact + bleeding. +- **Honest framing:** Directional LP P&L under a one-sided book is the + **inherent risk an LP underwrites**, not a protocol bug — a balanced book + hedges it away. The rate shock is an **exogenous** market move, not a protocol + knob, so the report's "keep `rate_shock_bps` in {…}" line must be reframed as + a statement about where the outflow threshold sits (a surface fact), not a + tuning instruction. Solvency holding across the whole sweep is a **robustness + result** worth stating plainly; the heatmap "failure rate" is the rate at + which outflow crossed the chosen line, not an insolvency rate. If an external + reserve is inert in stub-oracle mode, say the on-chain threshold is a + conservative (early) bound. *(Validated on a real interest-rate-swap protocol: solvency held to the 490 + bps breaker; LP outflow crossed 1% of the $1M reserve above ~300 bps.)* + +## lending + +- **Worst case to hunt:** A collateral price crash that **outruns** liquidation, + so recovery is capped below the outstanding debt and the lender (or protocol) + eats the shortfall as bad debt. The realistic worst case is the crash landing + **before** the liquidator reacts — and check *who* may liquidate: if + `liquidate_loan` is permissioned (lender-only, or keeper-only) there is no + third-party backstop on an active position, which widens the exposure window. + Run the lifecycle on the real program: borrower posts collateral at the + healthy ratio → lender funds → **crash the collateral oracle by the swept + bps** → liquidate → measure recovered collateral vs outstanding debt. Negative + control: a healthy (un-crashed) loan must reject liquidation. +- **Swept axis:** Collateral crash magnitude in bps (exogenous price move), + bracketed across and past the liquidation threshold — e.g. + `collateral_price_drop_bps ∈ {0, 1000, …, 6000}` (0–60%) so the surface spans + both the "liquidation becomes eligible" knee and the "collateral < debt" onset. + Crash via the **oracle account bytes** (see the Pyth `PriceUpdateV2` helper in + the authoring patterns), not a primitive argument. +- **Deciding invariant / metric:** Metric `bad_debt` — the USD shortfall + (`outstanding_debt − recovered_collateral_value`, clamped at zero) the lender + absorbs. The invariant fires (e.g. `lender_bad_debt`) when bad debt crosses a + stated line (e.g. 1% of debt value). Note the **two** thresholds the gradient + reveals: liquidation becomes *available* at one crash level (the + `liquidation_threshold` ratio) but the lender stays whole until a *later* + level where collateral value crosses below debt — report both. +- **Signal trap:** Measure the **realized shortfall** from actual recovered vs + owed value after the liquidation transaction runs — not a health-factor or + LTV field, and not "did liquidation succeed." Liquidation can transition the + loan to `Liquidated` correctly (status flips, accounting is right) while the + lender still eats bad debt; an agent that checks only the status byte or a + pre-crash health ratio reads "liquidation worked → safe" and misses the + shortfall entirely. The signal is the money, measured post-liquidation. +- **Honest framing:** Bad debt when a crash outruns liquidation is the + **inherent risk of collateralized lending**, not an accounting bug — the + liquidation math is doing exactly what it should (payout capped at collateral + held). The crash is **exogenous**. Surface the permissioning nuance as a + deliberate design question (no third-party backstop on an active underwater + loan), not a defect. If you drive the oracle directly, state the oracle's own + staleness/confidence/verification guards are out of scope — you are isolating + the protocol's response to a price *path*. *(Validated on a real lending protocol: liquidation + accounting correct at every level; lender bad-debt onset at ~33% crash — + liquidation eligible at ~20%, lender whole until ~33%; `liquidate_loan` + lender-only.)* + +## nav-vault + +- **Worst case to hunt:** A first-mover dilution run: a real asset markdown lands + on the vault while a **stale-high** NAV attestation is still inside its + freshness/TTL window, so an early withdrawer can redeem against the stale-high + NAV and drain value from the investor who stays. Run the lifecycle on the real + program: initialize → two investors `deposit` at par against a fresh + attestation → a real asset markdown of the swept magnitude lands **while the + prior high attestation is still in-window** (not yet refreshed) → the early + investor `initiate_withdrawal` → `finalize_withdrawal` under the stale-high NAV + → the staying investor withdraws against the refreshed, true NAV. Measure how + much the early mover extracted beyond fair share and how much the stayer lost. +- **Swept axis:** Markdown magnitude in bps of real vault assets lost while the + high attestation is in-window — e.g. `nav_markdown_bps ∈ {0, 500, …, 5000}` + (0–50%). `0` is the **positive control**: a fresh-and-true NAV must pay exact + pro-rata (this is the baseline the execution-honesty gates require). +- **Deciding invariant / metric:** Metrics `dilution_loss` (the stayer's + shortfall vs fair value) and `early_overpayment` (the runner's excess over + fair value), both in base units. The invariant fires (e.g. `investor_dilution`) + when `dilution_loss` crosses a stated line (e.g. 1% of the stayer's fair + value). A flat-zero `dilution_loss` across the full markdown sweep is the + robustness result — but it is only meaningful if the positive control passed + and the withdrawal lifecycle actually executed (both enforced by the + execution-honesty gates). +- **Signal trap:** Measure the **realized payout deltas** — `a_payout`/`b_payout` + vs each investor's `fair_value` (i.e. vault-drain), the value that actually + left the vault — not the attested NAV field. The NAV number is a + trusted-but-bounded *input* you constructed; reading it back tells you nothing + about whether value was extracted. A stale-high NAV that merely raises a + one-sided cap (payout = `min(real_pro_rata, nav_value × fraction)`) never binds + upward, so the drain is zero *because of the cap's direction* — which you can + only see by measuring payouts, not the NAV. +- **Honest framing:** A held (flat-zero) surface is **evidence over the tested + region, not unconditional safety** — say so explicitly. The markdown is an + exogenous asset-value move; the NAV attestation is trusted-but-bounded (you + test the mechanics' response to a NAV *path*, not the attestor's honesty — the + authority signer, TTL cap, and value cap are the controls on the attestation + itself and you rely on them). When the guard holds, explain the **structural + reason** (e.g. the NAV cap is one-sided and can only *lower* a payout priced + off the real vault balance) rather than asserting safety. *(Validated on + a real NAV-vault protocol: zero dilution across 0–50% markdown; the audit-fix NAV cap held + structurally; positive control at markdown 0 paid exact pro-rata.)* + +## amm + +AMMs frequently classify as `baseline-sim` — primitive swap args, self-signed +actions, no externally owned bytes to evolve. When they do, run the sweep below +inside the same crate; the worst case is still worth hunting. + +- **Worst case to hunt:** Value leaking *below* the constant product net of fees + — not the LP impermanent loss an arbitrageur realizes when an exogenous + external-market price move lets them rebalance the pool (that is inherent LP + risk), but a rounding/fee-accounting path where `k` actually decreases, so the + pool is drained beyond what IL explains. Run the lifecycle on the real + program: LP `add_liquidity` → an `arbitrageur` swaps the pool toward the + shocked external price over several ticks → LP `remove_liquidity`. Compare LP + redeemable value to a hold baseline, and watch `k` across every swap. +- **Swept axis:** External price divergence in bps — how far the paired asset's + market price moves away from the pool's implied price, `price_divergence_bps ∈ + {0, 500, …, 5000}` (0–50%). `0` is the positive control: no divergence must + leave `k` and LP value flat. +- **Deciding invariant / metric:** Metric `lp_value_delta` — LP redeemable value + at exit minus the hold-both-tokens baseline, which isolates protocol-induced + leak from ordinary IL. The invariant `k_non_decreasing` (`k_after >= k_before` + net of fees) fires error-severity on any true value leak — that, not the IL + magnitude, is the defect signal. +- **Signal trap:** Measure **LP redeemable value versus hold**, not the spot + price or the raw reserve balances. The pool marking to the new price is + correct behavior; reserves moving is correct behavior. An agent that reads + "price moved, reserves moved" concludes "working as designed" and misses a + fee-rounding drain that only shows up as `k` slipping below its pre-swap value. +- **Honest framing:** Impermanent loss under a price move is the **inherent risk + an LP underwrites**, not a bug — say so and do not report it as a failure. The + price divergence is **exogenous**. A `k` that holds (never decreases net of + fees) across the full sweep is a **robustness result** worth stating plainly; + only a real `k` decrease is a finding. + +## perps + +Perps almost always classify as `guided-sim-authored`: liquidation is a +third-party flow (**Trigger C**) and the mark price is an oracle-account read +(**Trigger B**). + +- **Worst case to hunt:** A mark-price **gap** that blows through maintenance + margin faster than liquidation can act, so the liquidation recovers less than + the position's loss and the shortfall socializes — onto the counterparty, the + insurance fund, or (via ADL) profitable traders. Run the lifecycle on the real + program: trader opens a leveraged position at healthy margin → funding accrues + → **gap the mark oracle past the maintenance-margin band in one step** → a + third-party `liquidator` liquidates → measure recovered margin vs the + position's realized loss. Negative control: a within-margin position must + reject liquidation. +- **Swept axis:** Mark-price gap in bps past the maintenance-margin threshold + (exogenous), bracketed from `0` through and past the point where the loss + exceeds posted margin — e.g. `mark_gap_bps ∈ {0, 500, …, 4000}`. Gap via the + **oracle account bytes** (the Pyth `PriceUpdateV2` helper), not a primitive + argument. `0` is the positive control. +- **Deciding invariant / metric:** Metric `socialized_loss` — the shortfall + (`position_loss − recovered_margin`, clamped at zero) that lands on someone + other than the position holder, plus `insurance_fund_drawdown`. The invariant + `no_socialized_loss` fires when the shortfall crosses a stated line. Note the + two knees: liquidation becomes *eligible* at one gap level, but the fund stays + whole until a *later* level where the loss exceeds posted margin — report both. +- **Signal trap:** Measure the **realized shortfall after liquidation runs** — + not "did liquidation fire", not the pre-gap leverage or margin ratio. + Liquidation can execute perfectly (position closes, status flips) while the + recovered margin still falls short of the loss; an agent that checks only the + close succeeded reads "liquidation worked → safe" and misses the socialized + loss. The signal is the money the fund/counterparty absorbs, measured post-fill. +- **Honest framing:** Gap risk beyond maintenance margin is the **inherent risk + of leveraged perps**, not a liquidation bug — the engine is capping recovery + at margin held, exactly as it should. The gap and the funding rate are + **exogenous** market inputs, not protocol knobs. An insurance fund that + absorbs every gap across the sweep is a robustness result; only a fund + *breach* (socialized loss reaching real traders) is a finding. + +## lst + +LSTs often classify as `baseline-sim` for the steady stake/unstake path, but a +withdrawal queue with a keeper crank is `guided-sim-authored` (**Trigger C/D**). + +- **Worst case to hunt:** A first-mover run on the withdrawal queue while backing + is **stale-high** — a validator slash or reward-loss marks down real staked + backing, but the posted exchange rate has not caught up, so an early + `panic-exiter`/`arb-redeemer` redeems LST at the stale-high rate and dilutes + the staker who stays. Run the lifecycle on the real program: two stakers + `stake` at par → a backing markdown of the swept magnitude lands **while the + exchange rate is still stale-high** → the early staker runs `unstake` / + withdrawal-queue redemption at the stale rate → the staying staker redeems + against the refreshed, true rate. Measure the early mover's excess and the + stayer's shortfall. +- **Swept axis:** Backing markdown in bps of staked value lost while the rate is + stale-high — `backing_markdown_bps ∈ {0, 500, …, 5000}` (0–50%, a slash). + `0` is the positive control: fresh-and-true backing must redeem exact + pro-rata. +- **Deciding invariant / metric:** Metrics `dilution_loss` (the stayer's + shortfall vs fair backing) and `early_overpayment` (the runner's excess), + in base units; plus a hard `pool_solvent` check (redeemable backing covers + outstanding LST at the true rate). The invariant `staker_dilution` fires when + `dilution_loss` crosses a stated line. Flat-zero across the sweep is the + robustness result — meaningful only if the positive control passed. +- **Signal trap:** Measure **realized redeemed value per share** for each staker + vs their fair backing, not the posted exchange rate. The rate is a + trusted-but-lagging *input*; reading it back tells you nothing about whether + value was extracted. The drain only shows in the payout deltas between the + runner and the stayer. +- **Honest framing:** A slash is an **exogenous** backing-value move, not a + protocol bug; first-mover advantage in a redemption queue is a **design + property** to surface (does the rate refresh before redemptions can race it?), + not a defect. A held (flat-zero) dilution surface is **evidence over the tested + region, not unconditional safety** — and when the guard holds, explain the + structural reason (e.g. the rate refreshes atomically before any redemption). + +## stablecoin + +Stablecoins usually classify as `baseline-sim` for mint/redeem, tipping to +`guided-sim-authored` when redemption reads an oracle account (**Trigger B**) or +runs a PSM keeper (**Trigger C**). + +- **Worst case to hunt:** A collateral crash that drives backing **below 100%** + while redemptions race, so the reserve is drained by early redeemers and the + **marginal (last) redeemer** cannot be made whole — the shortfall is + under-collateralization, not a mint bug. Run the lifecycle on the real + program: minters `mint` against healthy collateral → **crash the collateral + oracle by the swept bps** → `panic-redeemer`/`arb-redeemer` race the + redemption/PSM path → measure realized redeemable collateral per unit for the + first vs the last redeemer. Negative control: a fully-backed redemption must + pay par. +- **Swept axis:** Collateral price drop in bps (exogenous), bracketed across and + past the point where backing crosses 100% — e.g. `collateral_price_drop_bps ∈ + {0, 1000, …, 6000}` (0–60%). Crash via the **oracle account bytes**, not a + primitive argument. `0` is the positive control. +- **Deciding invariant / metric:** Metric `redemption_shortfall` — par value owed + minus realized collateral paid for the marginal redeemer — and `backing_ratio` + (total collateral value / outstanding supply). The invariant `fully_backed` + (`backing_ratio >= 1`) fires when backing crosses below par; the shortfall + metric shows *who* eats it. Report both the crash level where backing crosses + 100% and the level where the last redeemer starts taking a haircut. +- **Signal trap:** Measure the **realized redeemable collateral per unit for the + last redeemer**, not the peg price, the oracle, or total supply. A stablecoin + can hold its quoted peg and keep minting/redeeming correctly right up until the + reserve empties; an agent that watches the peg reads "still $1 → safe" and + misses that late redeemers are being paid in cents. The signal is the + marginal redemption, measured post-crash. +- **Honest framing:** Under-collateralization under a large enough collateral + crash is the **inherent risk of collateralized stablecoins**, not an accounting + bug — redemptions are paying out exactly the collateral held. The crash is + **exogenous**. Backing that holds `>= 1` across the whole sweep is a + **robustness result**; the "failure rate" on the surface is the rate at which + the marginal redeemer took a haircut, not a de-peg probability. + +## orderbook + +Orderbooks classify as `guided-sim-authored`: settlement and matching are +keeper-driven third-party flows (**Trigger C**), often multi-instruction +(**Trigger D**). No prebuilt bucket ships — derive from the program's real +match/settle flows. + +- **Worst case to hunt:** A settle or match that fills at a **stale or crossed** + reference price, letting one side extract from the counterparty, or a + settlement that leaves an **unmatched obligation** the program cannot honor. + Run the lifecycle on the real program: maker posts an order → taker crosses → + a third-party keeper `settle`/`match` runs → measure realized fill value vs a + fair mid, and check every matched obligation is fully settled. Model the + keeper/matcher as a distinct actor operating on orders it does not own (use + `ThirdPartyDispatch`). Negative control: a settle against a non-crossed book + must reject. +- **Swept axis:** Reference-price staleness or spread-cross magnitude in bps — + how far the settlement price may lag the true mid before a fill becomes + extractive, `settle_staleness_bps ∈ {0, 250, …, 2500}`. `0` (fresh reference) + is the positive control: a fair-priced settlement must transfer exact value. +- **Deciding invariant / metric:** Metric `settlement_value_delta` — realized + fill value minus fair-mid value for the disadvantaged side — and + `unmatched_obligation` (base units of any obligation left unsettled after the + crank). The invariant `conserves_value` (total in == total out across the + match) fires on any leak; `settlement_value_delta` shows the extraction + gradient. +- **Signal trap:** Measure **realized fill value vs a fair reference**, not "did + the match/settle instruction succeed". A settle can complete cleanly (orders + clear, status flips) while filling at a stale price that hands value to one + side; an agent that checks only the crank succeeded reads "settlement worked → + safe" and misses the extraction. The signal is the value transferred vs fair, + measured after the crank. +- **Honest framing:** Settlement is a **keeper-driven third-party flow** — model + the keeper explicitly, don't self-sign it. Price staleness is an **exogenous** + input (how fresh the keeper's reference is), not a protocol knob. A book that + conserves value and settles every obligation across the full staleness sweep + is a **robustness result**; only a value leak or an unmatched obligation is a + finding.