pashov/skills
GitHub用于从Foundry或Hardhat项目生成兼容Echidna和Medusa的Solidity模糊测试套件。支持状态模糊测试、不变性验证及属性测试,可自动处理编译、部署与测试用例生成,提供引导式与全自动两种运行模式。
Install All Skills
npx skills add pashov/skills --all -g -y
More Options
List skills in collection
npx skills add pashov/skills --list
Skills in Collection (5)
fizz/SKILL.md
npx skills add pashov/skills --skill fizz -g -y
SKILL.md
Frontmatter
{
"name": "fizz",
"description": "Generate Echidna\/Medusa-compatible Solidity fuzz suites from Foundry or Hardhat projects. Trigger on \"fizz\", \"generate fuzz suite\", \"build fuzz harness\", \"stateful fuzzing\", \"fuzzing harness\", \"property testing\", and \"invariant suite\"."
}
Fizz
Generate a stateful Solidity fuzz suite under {SUITE_DIR} (default: test/fizz/), with metadata and fuzzer runtime files under {META_DIR} (default: fizz_data/).
Use Echidna and Medusa for invariant campaigns. Use Foundry for compilation, smoke testing, and quick debugging.
Workflow Rules
- Follow the steps in order. Do not skip forward if a required artifact for the current step does not exist yet.
- If a step fails, stop there and report the blocker.
- If tooling is missing, say exactly what was attempted and what is missing.
- Keep the generated Solidity suite isolated under
test/fizz/and the metadata/runtime files underfizz_data/unless the user explicitly asks for different paths. - Reuse existing project setup and test logic whenever possible; do not invent a deployment flow if the repo already has one.
Parameters
PROJECT_ROOT: user-provided path, otherwise the current working directory.SKILL_PATH: the directory containing thisSKILL.md.SUITE_DIR:test/fizzrelative toPROJECT_ROOT. Pass--suite-dirto suite-generation steps.META_DIR:fizz_datarelative toPROJECT_ROOT. Pass--meta-dirto metadata steps.- Optional contract arguments narrow handler generation to specific contracts.
--no-invariantsskips Step 9 only.--max(or--opus, or "max quality") upgrades every subagent in this run from Sonnet to Opus. See "Subagent Model" below.--guided/--automaticselects the run mode. See "Run Mode" below.
Run Mode
The skill runs in one of two modes, resolved once at the start of the run and reused for every checkpoint below:
{MODE} = "guided"— the parent agent pauses for user input at key checkpoints: Step 3 (additional docs), Step 4 (interactive function picker UI), Step 4.5 (cost confirmation), Step 6 (setup review), Step 8 (per-cycle coverage decision), Step 9c (property review), Step 10 (fuzzer choice).{MODE} = "automatic"— the parent agent never pauses. Step 4 runs with--auto, Step 8 loops up to 3 coverage cycles then proceeds, Step 10 defaults to Medusa, and the cost estimate from Step 4.5 is printed but not gated on user confirmation.
Resolving {MODE}
- If the user invoked with
--guided/ "guided mode" / "walk me through" / "let me review" →{MODE} = "guided". - If the user invoked with
--automatic/--auto/ "unguided" / "run the whole thing" / "no prompts" →{MODE} = "automatic". - Otherwise, leave
{MODE}unresolved; Step 0 asks for it via the selection prompt after printing the banner.
Every subsequent instruction referencing {MODE} must substitute the resolved value. Do NOT switch modes mid-run.
Subagent Model
All subagents spawned by this skill (Step 3 fallback Protocol Analyzer, the 5 Step 9b discovery agents, the Step 9c Synthesizer, the 2 Step 9d Implementers, and the Step 11 Report Writer) default to Sonnet for cost and latency.
The parent agent orchestrating the pipeline is whatever model the user's Claude Code session is running (this skill does not control it). Only the delegated subagents are covered by {AGENT_MODEL}.
Resolving {AGENT_MODEL}
Resolve once at the start of the run and reuse it for every spawn below:
- If the user invoked with
--max/--opus/ "max quality" / "run on opus" / similar →{AGENT_MODEL} = "opus". - If the user invoked with
--sonnet/ "use sonnet" / "default model" →{AGENT_MODEL} = "sonnet". - Otherwise, leave
{AGENT_MODEL}unresolved; Step 0 asks for it via the selection prompt after printing the banner.
Every subsequent spawn instruction below references {AGENT_MODEL} — substitute the resolved value when making the actual tool call. Do NOT mix tiers within a single run.
Step 0: Print Banner
At the start of every skill run, first print this ASCII banner once before any other output — including any selection prompt:
██████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
██╔══██╗██╔══██╗██╔════╝██║ ██║██╔═══██╗██║ ██║ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
██████╔╝███████║███████╗███████║██║ ██║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║ ██║╚██╗ ██╔╝ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
██║ ██║ ██║███████║██║ ██║╚██████╔╝ ╚████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝
After the banner, resolve {MODE} per the "Run Mode" section and {AGENT_MODEL} per the "Subagent Model" section. For any value still unresolved from invocation flags, ask the user via a single AskUserQuestion tool call containing only the unresolved questions (skip the call entirely if both were resolved from flags).
Output discipline (mandatory): Between the banner block and the AskUserQuestion invocation, emit no user-facing text whatsoever — no "I'll ask about…", no "loading the tool…", no acknowledgement that flags were missing. If AskUserQuestion's schema needs to be fetched via ToolSearch first, do that silently as well. The user should see banner → selection UI → resolved-values lines, with nothing in between. This overrides the default behavior of narrating intent before tool calls.
- Question for
{MODE}—header: "Run mode",question: "How should I run?", options:label: "Automatic (Recommended)",description: "Run end-to-end with no prompts."label: "Guided",description: "Pause at 7 checkpoints: extra docs, entry-point picker (browser UI), cost confirm, setup review, per-cycle coverage decision, property review, fuzzer choice."
- Question for
{AGENT_MODEL}—header: "Subagent model",question: "Which model should drive the subagents?", options:label: "Sonnet (Recommended)",description: "Default. Faster and cheaper for the 5 discovery agents, synthesizer, and 2 implementers."label: "Opus",description: "Higher quality but ~10× the cost. Equivalent to passing --max / --opus."
Map the user's selections back to {MODE} (automatic / guided) and {AGENT_MODEL} (sonnet / opus), then print these two lines so the resolved values are visible in transcript:
Mode: guidedorMode: automatic— the resolved{MODE}.Subagent model: sonnet(default) orSubagent model: opus (--max)(if--max/--opus/ "max quality" / "use opus" / Opus selection was requested).
Step 1: Verify Tooling And Environment
Run sequentially:
- Read template-map.md.
- Run
forge --version. - If
forge --versionfails, tell the user that Foundry is missing and suggest installing it using the official documentation: Foundry install guide:https://www.getfoundry.sh/introduction/installation - If
forge --versionfails, stop here. Foundry is required before proceeding with the rest of the workflow. - Run
bash {SKILL_PATH}/scripts/ensure_foundry.sh {PROJECT_ROOT}. - If
foundry.tomlis missing, allowensure_foundry.shto create one. If it fails, stop and report the error. - Run
medusa --version. - Run
echidna --version. - If either command fails, tell the user which tool is missing and suggest installing it using the official documentation:
Medusa install guide:
https://secure-contracts.com/program-analysis/medusa/docs/src/getting_started/installation.htmlEchidna install guide:https://secure-contracts.com/program-analysis/echidna/introduction/installation.html - If
medusa --versionfails, stop here. Medusa is required before proceeding with the rest of the workflow. - If
echidna --versionfails but Foundry and Medusa are installed, you may continue, but keep the installation recommendation in the user-facing summary because Echidna is still expected for the full workflow.
Step 2: Compile And Extract
Run sequentially:
- Read
{PROJECT_ROOT}/foundry.toml. - Run
cd {PROJECT_ROOT} && forge build. - Run
node {SKILL_PATH}/scripts/extract_abis.js {PROJECT_ROOT} --meta-dir {META_DIR}.
Step 3: Understand The Protocol
This step exists to drive setup, handler selection, and invariant generation quality.
If {MODE} = "guided", before touching any analysis source first ask the user: "Any additional docs, links, whitepapers, spec files, or prior-audit notes I should consider? (paste paths or URLs, or reply 'none')". If the user provides anything, write the raw list to {PROJECT_ROOT}/{META_DIR}/additional-context.md (one entry per line, include URLs verbatim). Later sub-steps of this step — and Step 9a — must read that file if it exists and fold it into the protocol-understanding context.
Start by checking whether {PROJECT_ROOT}/x-ray/ exists and contains x-ray.md. x-ray.md is REQUIRED — without it, x-ray output is considered unavailable regardless of which other files are present.
If {PROJECT_ROOT}/x-ray/x-ray.md exists, read it first as the primary project-understanding source. Then also read any of these supplementary files present in {PROJECT_ROOT}/x-ray/:
If {PROJECT_ROOT}/x-ray/x-ray.md does NOT exist, you MUST run the x-ray Acquisition Protocol below. The Protocol Analyzer fallback (Attempt 4) is FORBIDDEN until Attempts 1–3 have each been executed and their outcomes recorded in /tmp/x-ray-attempts.md. "I think x-ray isn't available" is NOT a valid skip — only the recorded output of an actual tool/command counts.
x-ray Acquisition Protocol
Before Attempt 1, delete /tmp/x-ray-attempts.md if it exists (rm -f /tmp/x-ray-attempts.md) — stale entries from a previous run would falsely satisfy the Attempt 4 gate. Then create a fresh /tmp/x-ray-attempts.md and append one entry per attempt: timestamp, attempt name, command/tool invoked, exact output (or "no output"), outcome (SUCCESS / FAILED: {reason} / SKIPPED: {reason}). Attempt 4 requires the file to contain exactly 3 entries (one per Attempt 1, 2, 3) — SKIPPED entries count toward this total.
-
Attempt 1 — invoke the skill. Call the
x-rayskill via theSkilltool withargs="{PROJECT_ROOT}". Do NOT pre-judge availability — invoke it. Only a runtime error of the form "skill not found" / "unknown skill" counts as unavailable. If it runs, wait for completion, then verify{PROJECT_ROOT}/x-ray/x-ray.mdwas written. If yes → SUCCESS, exit Protocol. -
Attempt 2 — install from the official source and re-invoke. Run:
git clone --depth 1 https://github.com/pashov/skills.git /tmp/pashov-skills-xray-install \ && mkdir -p ~/.claude/skills \ && cp -r /tmp/pashov-skills-xray-install/x-ray ~/.claude/skills/x-rayThen re-invoke
Skill('x-ray', args="{PROJECT_ROOT}"). If{PROJECT_ROOT}/x-ray/x-ray.mdis produced → SUCCESS, exit Protocol. If the re-invocation still returns "skill not found" / "unknown skill" (auto-discovery did not pick up the freshly installed skill mid-session), do NOT mark this attempt failed yet — instead read~/.claude/skills/x-ray/SKILL.md(or/tmp/pashov-skills-xray-install/x-ray/SKILL.md) and execute its instructions inline against{PROJECT_ROOT}. If that produces{PROJECT_ROOT}/x-ray/x-ray.md→ SUCCESS, exit Protocol. Only if ALL of (Skill re-invocation, inline execution) fail does this attempt count as FAILED. -
Attempt 3 — guided-mode user gate (guided only). If
{MODE} = "guided"AND Attempts 1–2 both failed, ASK the user: "Could not obtain x-ray automatically (logs in/tmp/x-ray-attempts.md). Options: (a) paste an x-ray.md path, (b) authorize Protocol Analyzer fallback, (c) abort. Choose a/b/c." Record their answer. If (a) and the file exists → copy to{PROJECT_ROOT}/x-ray/x-ray.md, SUCCESS. If (c) → halt the skill. Only (b) — explicit user authorization — permits Attempt 4. In{MODE} = "automatic", skip this attempt and recordSKIPPED: automatic mode. -
Attempt 4 — Protocol Analyzer fallback. Permitted ONLY after Attempts 1–3 are recorded in
/tmp/x-ray-attempts.md(with status FAILED, SKIPPED, or — for Attempt 3 only —(b) authorized). Before spawning, confirm the file exists and contains 3 entries; if not, GO BACK to the missing attempt — do not proceed.Fallback: Read
{SKILL_PATH}/agents/protocol-analyzer.md, replace{SKILL_PATH}with the actual{SKILL_PATH},{PROJECT_ROOT}with the actual{PROJECT_ROOT}, and{META_DIR}with the actual{META_DIR}, then spawn as ageneral-purposeagent withmodel: "{AGENT_MODEL}". The agent reads the source files, then writes the analysis to{PROJECT_ROOT}/{META_DIR}/protocol-understanding.mdso that later steps can read it back instead of relying on conversation context.
From the x-ray documentation or protocol-understanding.md infer and summarize:
- deployment order
- constructor parameter meaning
- required post-deploy initialization
- actor roles and permissioned actions
- approvals, liquidity, or other state needed before handlers will be useful
- which external functions are real fuzzing entry points versus protocol-internal plumbing
- candidate invariants to carry forward into Step 9
If something is still ambiguous after those reads, keep going with a conservative assumption and leave a targeted TODO later instead of guessing broadly.
Do not plan full ghost-variable layouts, snapshot structs, or final implementation details here. Do record the likely invariants clearly so Step 9 can reuse them from {PROJECT_ROOT}/x-ray/ or {PROJECT_ROOT}/{META_DIR}/protocol-understanding.md as its starting point.
Step 4: Select Entry Points
Read selection-policy.md.
Create {PROJECT_ROOT}/{META_DIR}/entry-point-selection.json as a filtered copy of {PROJECT_ROOT}/{META_DIR}/contracts.json that keeps the functions most likely to produce useful state transitions.
Build the preselection from the protocol understanding gathered in Step 3 — primarily the x-ray entry-point map (if available) and source-level access control observations. If Step 3 produced an entry-point map with caller or access annotations, use that as the primary filter: exclude functions marked as internal-caller-only or contract-to-contract plumbing. Use {PROJECT_ROOT}/{META_DIR}/contracts.json only as the structural template for the output JSON format, not to decide which functions to include.
Then run:
- If
{MODE} = "automatic":node {SKILL_PATH}/scripts/select_functions.js {PROJECT_ROOT} --contracts {PROJECT_ROOT}/{META_DIR}/contracts.json --selection {PROJECT_ROOT}/{META_DIR}/entry-point-selection.json --meta-dir {META_DIR} --auto - If
{MODE} = "guided":node {SKILL_PATH}/scripts/select_functions.js {PROJECT_ROOT} --contracts {PROJECT_ROOT}/{META_DIR}/contracts.json --selection {PROJECT_ROOT}/{META_DIR}/entry-point-selection.json --meta-dir {META_DIR}
The --auto flag accepts the inferred selection and exits immediately. Without it, the script opens a browser UI with the inferred selection pre-checked so the user can adjust and confirm. Both paths write entry-point-selection.json.
After the script completes, read {PROJECT_ROOT}/{META_DIR}/entry-point-selection.json.
If the script exits before writing entry-point-selection.json, stop and report that failure.
Print a short summary:
- selected contracts
- selected functions by contract
- notable excluded functions
Dispatcher for Low-Frequency Functions
After reading the selection, classify the selected functions into two tiers:
- Primary: core user flows that should be called frequently by the fuzzer (deposit, withdraw, mint, redeem, borrow, repay, swap, stake, unstake, claim, liquidate, etc.)
- Secondary: less common functions that are still useful but should be called less often (admin setters, configuration changes, pause/unpause, role grants, parameter tuning, etc.)
Write this classification to {PROJECT_ROOT}/{META_DIR}/entry-point-selection.json by adding a "tier": "primary" or "tier": "secondary" field to each function entry.
In Step 7, secondary-tier functions will be wrapped in a dispatcher handler that groups them behind a single entry point with an enum selector. This reduces call frequency naturally without excluding them entirely — the fuzzer picks a random selector value, so secondary functions get exercised occasionally but don't dominate the call sequence.
If the user already excluded a function during selection, it stays excluded. The dispatcher is only for functions the user chose to keep but that should be deprioritized.
{PROJECT_ROOT}/{META_DIR}/entry-point-selection.json limits handler generation only. It does not limit the setup dependency graph.
Step 4.5: Cost Estimate
Run:
node {SKILL_PATH}/scripts/estimate_cost.js {PROJECT_ROOT} --meta-dir {META_DIR} --model {AGENT_MODEL} --mode {MODE}
The script reads entry-point-selection.json, applies a size bucket based on selected-function count, and writes {PROJECT_ROOT}/{META_DIR}/cost-estimate.md with a per-stage breakdown plus a total and an expected range. The numbers are Anthropic list-price ballparks; actual cost varies with coverage cycles, re-runs, and prompt-cache hit rate.
Print the cost estimate table to the user. Then:
- If
{MODE} = "automatic": continue to Step 5 without pausing. - If
{MODE} = "guided": ask the user "Proceed with this estimate, or abort?" and wait for confirmation before continuing. If the user aborts, stop the run and report where the artifacts so far were written.
Step 5: Generate Scaffold
Run:
node {SKILL_PATH}/scripts/generate_suite.js {PROJECT_ROOT} --suite-dir {SUITE_DIR} --meta-dir {META_DIR}
This copies the full template scaffold into {PROJECT_ROOT}/{SUITE_DIR}/, including core harness files and utility files such as utils/MockERC20.sol. It also writes the fuzzer config files (echidna.yaml, medusa.json) into {PROJECT_ROOT}/.
It also reads {PROJECT_ROOT}/{META_DIR}/entry-point-selection.json and generates one stub handler file per selected contract under {PROJECT_ROOT}/{SUITE_DIR}/handlers/. Those stubs include the clamped and unclamped section headers but no sample handler functions. Handlers.sol is scaffolded to import and inherit from all generated handler stubs.
Treat the copied files as the starting point only. The next steps must modify them to fit the target protocol.
Step 6: Modify Core Files And Wire Setup
Read setup-playbook.md and template-map.md.
Modify the scaffolded core files under {PROJECT_ROOT}/{SUITE_DIR}/. Use template-map.md as the source of truth for the inheritance chain, file roles, and which scaffolded files are expected to be refined in this step versus later steps.
Use setup-playbook.md as the source of truth for:
- proxy and upgradeability detection
- setup requirements and good defaults
- mock-versus-real dependency choices
- signature-dependent setup guidance
Base.solintegration points and TODO/FIXME policy
When the target protocol depends on simple external ERC20s that are not part of the in-scope deployment graph, prefer the scaffolded utils/MockERC20.sol helper unless the project already includes a more faithful token mock.
The key output of this step is a compiling scaffold with a realistic Base.sol::setup() function and the rest of the core scaffold adjusted to match it.
If {MODE} = "guided", after the edits to Base.sol are complete and before running forge build, print a Setup Review block summarising what was wired:
- Contracts deployed in
setup()(name + address variable + constructor args source) - Proxies detected and which implementation each wraps
- Mocks vs real dependencies used, with the reason for each mock
- Actors configured (addresses + role), and which ones the fuzzer will impersonate via handler caller selection
- Seeded balances (token, recipient, amount)
- Roles / access-control grants (role, grantee)
- Approvals (token, owner, spender, amount)
Then ask the user: "Setup looks right? Reply 'proceed' to build, or tell me what to adjust." If the user requests adjustments, apply them and re-print the review block; loop until they approve. In {MODE} = "automatic", skip the review block and continue directly.
Run cd {PROJECT_ROOT} && forge build before moving on.
Step 7: Generate Handlers
Read handler-patterns.md.
First, run the handler generation script to produce pre-populated stubs with correct function signatures and type mappings:
node {SKILL_PATH}/scripts/generate_handlers.js {PROJECT_ROOT} --suite-dir {SUITE_DIR} --meta-dir {META_DIR}
Then read these in parallel:
{PROJECT_ROOT}/{SUITE_DIR}/handlers/Handlers.sol- each generated
{PROJECT_ROOT}/{SUITE_DIR}/handlers/<Contract>Handler.sol - each selected contract source file
Then refine the generated {PROJECT_ROOT}/{SUITE_DIR}/handlers/<Contract>Handler.sol for the selected contracts. The stubs already contain correct signatures and clamping hints — focus on wiring the actual protocol calls, adding semantic clamping, and implementing boundary-value stress variants.
Use handler-patterns.md as the source of truth for:
- clamped versus unclamped handler structure
- handler shaping and semantic action selection
- caller context
- clamping strategy
- edge-case and signature-dependent handler guidance
Update Handlers.sol to import and inherit from all generated handlers.
Run cd {PROJECT_ROOT} && forge build and fix compile issues before moving on.
Step 8: Reach Coverage With Medusa
Before generating invariants, ensure the generated harness can drive enough protocol coverage under Medusa.
Via-IR Coverage Deflation Handling
When via_ir = true is set in foundry.toml, the Yul IR optimizer aggressively merges and eliminates branches. This deflates Medusa's coverage numbers — you may be at 85% source coverage but Medusa reports 65%. This must be handled before the first Medusa run.
Step 8.0: Detect and configure fuzz profile.
- Read
{PROJECT_ROOT}/foundry.tomland check whethervia_ir = trueis set under[profile.default]or at the top level. - If
via_iris not enabled, skip this subsection — no fuzz profile is needed. - If
via_iris enabled, run:
bash {SKILL_PATH}/scripts/setup_fuzz_profile.sh {PROJECT_ROOT}
This script:
- Appends a
[profile.fuzz]section tofoundry.tomlwithvia_ir = false - Runs
FOUNDRY_PROFILE=fuzz forge build - If compilation succeeds: exits 0, prints
FUZZ_PROFILE=no-ir - If "stack too deep" error: retries with
via_ir = trueandoptimizer_runs = 0, exits 0, printsFUZZ_PROFILE=ir-no-opt - If both fail: exits 1 (use default profile, accept deflated coverage)
-
Read the script output to determine the fuzz profile mode:
FUZZ_PROFILE=no-ir→ accurate coverage, use standard targetsFUZZ_PROFILE=ir-no-opt→ reduced deflation but still some; lower coverage targets by ~10%- Script failed → fall back to default profile; lower coverage targets by ~15-20%
-
Record the profile mode in
{PROJECT_ROOT}/{META_DIR}/coverage-targets.mdat the top:no-ir: "Fuzz profile: via_ir disabled — coverage numbers are accurate"ir-no-opt: "Fuzz profile: via_ir required (stack too deep), optimizer_runs=0 — coverage deflated ~10%, targets adjusted"- default fallback: "Fuzz profile: via_ir required with optimizer — coverage deflated ~15-20%, targets adjusted"
-
For all subsequent
forge buildcommands in Steps 8–11, use:cd {PROJECT_ROOT} && FOUNDRY_PROFILE=fuzz forge build(if fuzz profile was created)cd {PROJECT_ROOT} && forge build(if no fuzz profile needed)
Store the build command in a variable {FUZZ_BUILD_CMD} for reuse in later steps.
Medusa Runs
Every Medusa run in this step must use run_medusa.js, including reruns after harness changes. Do not switch to raw medusa fuzz for later cycles in this step.
Before launching Medusa, rebuild with the fuzz profile if one was configured:
{FUZZ_BUILD_CMD}
Run the Medusa script asynchronously using the agent's command-execution tool. This is critical — do NOT use shell backgrounding (&), do NOT use sleep + tail to poll, and do NOT wait synchronously in a single blocking command. Start the process in a way the agent runtime can track and notify on completion.
node {SKILL_PATH}/scripts/run_medusa.js {PROJECT_ROOT} --meta-dir {META_DIR} --coverage-mode
The wrapper starts a temporary local browser log viewer and prints its URL, but does not open the browser by default. Read the viewer URL from the wrapper output and provide it to the user. Add --logs to the command only if the user asks for the viewer to open in their browser automatically.
When the background command completes and you are notified, inspect the resulting coverage with focus on the core protocol contracts, not peripheral mocks or helper libraries.
Dynamic Coverage Targets
Not all contracts require the same coverage. Assign per-contract targets based on the contract's role:
| Contract Role | Target (no-ir) | Target (ir-no-opt) | Target (ir fallback) |
|---|---|---|---|
| Core protocol logic (vault, pool, lending core, staking engine) | 80%+ | 70%+ | 65%+ |
| Access control / role management | 60%+ | 50%+ | 45%+ |
| Peripheral helpers (routers, views, adapters) | 50%+ | 40%+ | 35%+ |
| Libraries and math utilities | Coverage inherited from callers | Same | Same |
Use the column matching the fuzz profile mode determined in Step 8.0.
After the first Medusa run, review the per-contract coverage report and classify each contract. If a contract has legitimately unreachable paths in the harness context (e.g., fork-only branches, multi-block MEV paths, oracle-failure paths), note them and adjust that contract's target downward rather than wasting cycles on unreachable code.
Write the per-contract targets and any skip justifications to {PROJECT_ROOT}/{META_DIR}/coverage-targets.md so the user can review them.
Coverage Iteration Loop
- with
--coverage-mode, the wrapper runsmedusa fuzz --timeout 300, allows at least 60 seconds of fuzzing, then stops earlier if 5 consecutive progress lines show no increase inbranches hit, and prints the coverage report path when finished (add--logsto also open it in the browser) - if coverage is below target, improve handler shapes, clamping, setup, approvals, seed balances, caller roles, and any missing lifecycle actions
- when debugging why a specific handler or call sequence is not reaching the expected code paths, or when validating a hypothesis, use
FoundryTester.solto quickly PoC it - rerun
{FUZZ_BUILD_CMD}before each new Medusa run - treat one cycle as: run
node {SKILL_PATH}/scripts/run_medusa.js {PROJECT_ROOT} --meta-dir {META_DIR} --coverage-mode, inspect coverage, adjust the harness, then rebuild
After each cycle, build and print a per-contract coverage summary table from the Medusa coverage report:
| Contract | Role | Target | Hit | Status |
|---|
Status is ✅ if Hit ≥ Target, else ❌. Append the same table to {PROJECT_ROOT}/{META_DIR}/coverage-targets.md under a timestamped ## Cycle N heading so the history is preserved.
Then branch on {MODE}:
{MODE} = "automatic": if all contracts meet target, give a brief summary and proceed. Otherwise loop; cap at 3 cycles total, then log remaining gaps incoverage-targets.mdand proceed to the next step with the current harness.{MODE} = "guided": after every cycle (including cycle 1), ask the user "iterate / adjust targets / proceed".iterate= run another cycle with the current harness adjustments.adjust targets= let the user edit per-contract targets incoverage-targets.mdbefore the next cycle.proceed= exit the loop regardless of gaps. Honour the user's choice; there is no hard cap in guided mode.
After exiting the loop, provide the coverage report path for optional review: {PROJECT_ROOT}/{META_DIR}/corpus_medusa/coverage/coverage_report.html.
Acceptable Skip Reasons
It is acceptable to skip coverage for specific functions or paths when:
- the function requires external state that cannot be simulated (e.g., specific oracle prices from a live feed)
- the path is a revert-only guard that is intentionally unreachable in the harness (e.g.,
require(msg.sender == bridgeContract)) - the function is behind a time-lock or multi-sig that the harness doesn't simulate
- the function interacts with an external protocol that is not mocked
Document each skip in {PROJECT_ROOT}/{META_DIR}/coverage-targets.md with the reason.
Do not move to invariant generation while Medusa coverage is still clearly too low for the selected flows, unless the user explicitly tells you to proceed anyway.
Step 9: Generate Invariants (5 Parallel Discovery Agents + Synthesizer + 2 Implementers)
Skip this step only when the user passed --no-invariants.
This is the make-or-break step. It uses 5 specialized discovery agents in parallel, each applying a different invariant discovery approach drawn from 50+ real DeFi bugs caught by fuzzers.
Step 9a: Build Invariant Context
Read property-generation.md, then build INVARIANT_CONTEXT by reading:
- the invariant notes captured in Step 3 from
{PROJECT_ROOT}/x-ray/x-ray.mdwhen available, otherwise{PROJECT_ROOT}/{META_DIR}/protocol-understanding.md {PROJECT_ROOT}/{META_DIR}/additional-context.mdif it exists (guided-mode supplementary docs/links from Step 3)- all in-scope source files
Base.sol,Snapshots.sol,Properties.sol- all generated handler files
Extract from the codebase:
- AGGREGATE_VARIABLES: grep for variables named
total*,sum*,accumulated*, or any variable that multiple functions write to - PAIRED_OPERATIONS: match function pairs (deposit/withdraw, mint/burn, add/remove, lock/unlock, open/close, stake/unstake, borrow/repay, create/destroy, join/exit)
- CONVERSION_FUNCTIONS: grep for
convertTo*,preview*,toAssets,toShares, or any function mapping between two unit systems - ACCESS_CONTROL: grep for
onlyOwner,onlyAdmin,onlyRole,require(msg.sender, custom role modifiers
Step 9b: Spawn 5 Discovery Agents in Parallel
CRITICAL: All 5 agents MUST be spawned in a SINGLE message (one tool call per agent, all in the same response).
| Agent | File | Discovery Approach |
|---|---|---|
| 1. Conservation Auditor | {SKILL_PATH}/agents/invariant-discovery/conservation-auditor.md |
Sum-of-parts = tracked-whole for every aggregate variable |
| 2. Round-Trip & Rounding Analyst | {SKILL_PATH}/agents/invariant-discovery/roundtrip-rounding-analyst.md |
Forward+inverse operations, directional rounding |
| 3. State Transition Mapper | {SKILL_PATH}/agents/invariant-discovery/state-transition-mapper.md |
Postconditions, monotonicity, entity counts, state machine |
| 4. Adversarial Profit Maximizer | {SKILL_PATH}/agents/invariant-discovery/adversarial-profit-maximizer.md |
Attacker thinking — DoS, value extraction, edge states |
| 5. Protocol-Type Specialist | {SKILL_PATH}/agents/invariant-discovery/protocol-type-specialist.md |
Auto-detect type, apply domain templates (vault/lending/AMM/etc.) |
Read each agent file, replace {INVARIANT_CONTEXT} and {FILE_PATHS} with actual values, spawn as general-purpose agent with model: "{AGENT_MODEL}".
Step 9c: Synthesize Property Plan
After all 5 agents return, read the Synthesizer agent file:
| Agent | File |
|---|---|
| 6. Synthesizer | {SKILL_PATH}/agents/invariant-discovery/synthesizer.md |
Replace {AGENT_OUTPUTS} with the outputs from agents 1-5, {META_DIR} with the actual {META_DIR} path, {PROJECT_ROOT} with the actual {PROJECT_ROOT} path, and {SUITE_DIR} with the actual {SUITE_DIR} path, then spawn as general-purpose agent with model: "{AGENT_MODEL}".
The Synthesizer merges, deduplicates, prioritizes, and writes BOTH:
{PROJECT_ROOT}/{META_DIR}/property-plan.md— implementation tables with stable Spec IDs (GL-NN,SP-NN){PROJECT_ROOT}/PROPERTIES.md— English-language spec with[ ]checkboxes, one entry per property, identified by the same Spec IDs. This is the artifact that the/fizz-convertcommand and the implementers in Step 9d operate on.
Each property carries a Guarantee tag set at generation time — SHOULD-HOLD (explicitly guaranteed by docs/spec/standard or an exact identity, with evidence cited) or EXPLORATORY (inferred). This tag is what lets Step 10 triage a violation without post-campaign severity guessing: a violated SHOULD-HOLD property is a confirmed bug, a violated EXPLORATORY property is a lead for human review.
Print a summary: "Generated X properties (N HIGH, N MEDIUM, N LOW; P SHOULD-HOLD, Q EXPLORATORY)" with a brief list of the top properties by priority.
If {MODE} = "guided", pause here: tell the user the file path ({PROJECT_ROOT}/PROPERTIES.md), summarise what the Synthesizer produced, and ask "Review PROPERTIES.md and edit freely — rename, add, remove, or reword properties. Keep the Spec IDs (GL-NN / SP-NN) on entries you want implemented, and leave [ ] checkboxes unchanged. Reply 'proceed' when done, or 'regenerate' to re-run the Synthesizer with additional guidance." If they reply regenerate, ask what to change, then re-spawn the Synthesizer (Step 9c) with the extra guidance appended to its input. If they reply proceed, continue to Step 9d. In {MODE} = "automatic", skip the pause and proceed directly.
Step 9d: Implement Properties (2 Parallel Agents)
Read each agent file:
| Agent | File | Scope |
|---|---|---|
| 7A. Global Property Implementer | {SKILL_PATH}/agents/implementers/global-property-implementer.md |
Ghosts in Base.sol, State in Snapshots.sol, global properties in Properties.sol, harness contracts if needed |
| 7B. Specific Property Implementer | {SKILL_PATH}/agents/implementers/specific-property-implementer.md |
Specific properties in Properties.sol, handler wiring (ghost updates + snapshot calls + property assertions) |
Replace {META_DIR} with the actual {META_DIR} path, {SKILL_PATH} with the actual {SKILL_PATH} path, {PROJECT_ROOT} with the actual {PROJECT_ROOT} path, and {SUITE_DIR} with the actual {SUITE_DIR} path, then spawn both as general-purpose agents with model: "{AGENT_MODEL}" in parallel.
Both implementers MUST flip [ ] → [x] in {PROJECT_ROOT}/PROPERTIES.md for each property they actually implement (matching by Spec ID GL-NN / SP-NN). Properties left as TODO stubs stay [ ] so /fizz-convert can pick them up later.
Step 9e: Validate
Run {FUZZ_BUILD_CMD} after the edits. Fix any compilation errors.
If a property is low-confidence after implementation, prefer a commented TODO over a brittle assertion.
Step 10: Run Campaigns
After invariant generation and validation, run a full fuzzing campaign to find property violations.
Fuzzer Selection
{MODE} = "automatic": default to Medusa — faster with multi-worker parallelism and better suited for initial runs.{MODE} = "guided": ask the user "Which fuzzer for this campaign — Medusa (default, parallel workers) or Echidna?" and use their answer. If they express no preference, default to Medusa.
Do not run both fuzzers simultaneously — this consumes excessive resources and can cause system instability or crashes. Campaign Iteration (below) is where a complementary run on the other fuzzer is offered.
Running the Campaign
Run the wrapper for the chosen fuzzer, either Medusa or Echidna (ONLY ONE), asynchronously using the agent's command-execution tool, then wait for that tracked background job to finish — the wrapper exits when the fuzzer's own stop condition triggers or the --timeout is reached. Both wrappers start a temporary local browser log viewer and print its URL, but do not open the browser by default; read the viewer URL from the wrapper output and provide it to the user. Add --logs to the command only if the user asks for the viewer to open in their browser automatically.
For Medusa, run this command:
node {SKILL_PATH}/scripts/run_medusa.js {PROJECT_ROOT} --meta-dir {META_DIR} --timeout 600
For Echidna, run this command:
node {SKILL_PATH}/scripts/run_echidna.js {PROJECT_ROOT} --meta-dir {META_DIR} --timeout 600
If Echidna fails with unlinked libraries detected in bytecode, link the libraries in echidna.yaml:
deployContracts: [["0xf1", "Lib1"], ["0xf2", "Lib2"]]
cryticArgs: ["--compile-libraries=(Lib1,0xf1), (Lib2,0xf2)"]
Replace Lib1, Lib2 with the actual library names and 0xf1, 0xf2 with the desired deployment addresses.
Interpreting Results
After the campaign completes:
- check for property violations (failed assertions)
- for each violation, extract the call sequence that triggered it
- verify the violation is a real bug, not a harness issue — if the property or handler has a bug, fix it and rerun
- triage by the violated property's Guarantee tag (from
PROPERTIES.md): a violated SHOULD-HOLD property is a confirmed bug — the protocol broke a documented/mathematical guarantee, so report it as such; a violated EXPLORATORY property is flagged for human review — it may be a real bug or an over-strong inferred assumption, so present the call sequence and let a human judge rather than asserting a bug. Always rule out a harness/property bug first regardless of tag. - if no violations were found, report that the campaign completed cleanly with the coverage achieved
- if violations were found, document each one with the failing property name, its Guarantee tag, the call sequence, and the contract state at failure
Campaign Iteration
If the user wants to explore further after the first campaign:
- offer to run the other fuzzer (Echidna if Medusa was used, or vice versa) for a complementary pass
- offer to increase the timeout or test limit
- offer to adjust handler shapes based on coverage gaps observed during the campaign
Step 11: Validate And Report
Run:
{FUZZ_BUILD_CMD}- If
FoundryTester.solexists,cd {PROJECT_ROOT} && FOUNDRY_PROFILE=fuzz forge test --match-contract FoundryTester(or withoutFOUNDRY_PROFILEif no fuzz profile was created)
If validation fails:
- fix the specific broken files
- do not regenerate the whole suite unless the user asks
- limit repair retries to 3 cycles, then report the remaining issues cleanly
Generate Violation Repros
If the campaign in Step 10 found property violations, generate a Foundry reproduction test for each distinct violation in FoundryTester.sol. This turns fuzzer output into deterministic, one-command proof that the violation is real.
When to run: only when {PROJECT_ROOT}/{META_DIR}/corpus_medusa/test_results/ contains violation JSON files (Medusa) or the Echidna log contains failed! lines with call sequences. If the campaign found no violations, skip this sub-step entirely.
How to generate repros:
- Read each violation file (Medusa: one JSON per violation in
test_results/; Echidna: parse call sequences from the log after eachfailed!line). - For each distinct violated property (group multiple reproductions of the same property — use only the shortest call sequence):
- Create a
test_repro_<propertyName>()function inFoundryTester.sol. - Replay the shrunk call sequence by calling the handler functions from the JSON
methodSignatureandinputValuesfields directly. - Between calls, use
vm.roll()andvm.warp()to advance block number and timestamp by theblockNumberDelayandblockTimestampDelayfrom each call entry. - The fuzzer's
fromaddresses map to actors by index:0x10000→actors[0],0x20000→actors[1],0x30000→actors[2]. The clamped handlers accept an actor seed parameter that runs throughtoActor(), so pass the raw fuzzer input values — the seed modulus selects the right actor. - Two violation patterns require different test structures:
- Global property violations (
property_*functions that returnbool): replay the full call sequence, then assert the property returnsfalse:assertFalse(property_xxx(), "property should be violated");. The test passes when the property is violated. - Inline assertion violations (
_prop_*/_check*assertions inside handlers that revert viaassert()/t()): the violating handler call will revert withpanic(0x01)before any post-call assertion can run. Wrap the violating call intry this._repro_helperN() { revert("assertion should have fired"); } catch {}, where_repro_helperN()is anexternalhelper function that calls the handler. Thetry/catchproves the assertion fired. The test passes when the catch triggers.
- Global property violations (
- Create a
- Run
{FUZZ_BUILD_CMD}and thencd {PROJECT_ROOT} && FOUNDRY_PROFILE=fuzz forge test --match-contract FoundryTester -vvvto confirm all repro tests pass. - If a repro test fails to compile or does not reproduce the violation, fix it (max 2 attempts per test). If it still fails, comment out the test body with a
// TODO: manual repro needed — fuzzer sequence did not reproduce under Foundrynote and move on.
Repro naming convention: test_repro_<propertyName> (e.g., test_repro_property_solvency, test_repro_prop_depositIncreasesShares). If the same property was violated by multiple distinct root causes (different call sequences hitting different code paths), suffix with _1, _2.
Final Report
After validation succeeds, spawn the Report Writer subagent to synthesize the final report. Read {SKILL_PATH}/agents/report-writer.md, replace {SKILL_PATH} with the actual {SKILL_PATH}, {PROJECT_ROOT} with the actual {PROJECT_ROOT}, {META_DIR} with the actual {META_DIR}, and {SUITE_DIR} with the actual {SUITE_DIR}, then spawn as a general-purpose agent with model: "{AGENT_MODEL}".
The agent reads the campaign outputs (coverage, corpus, medusa-run.log, PROPERTIES.md, Properties.sol, handler files, open TODOs) and writes {PROJECT_ROOT}/{META_DIR}/report.md.
The report-writer agent will also print the report content to the conversation (so the user sees it inline) and remind the user of the commands to run campaigns manually:
medusa fuzz(from project root)echidna . --contract FuzzTester --config echidna.yaml
Snapshot For Future Re-Use
After the final report is written, capture a sync snapshot so the /fizz-sync skill can detect drift on subsequent source changes without re-running the full pipeline:
node {SKILL_PATH}/scripts/fizz_sync.js {PROJECT_ROOT} --init --meta-dir {META_DIR} --suite-dir {SUITE_DIR}
If the snapshot already exists (because a prior run already initialised it), re-run with --refresh-snapshot instead so the new baseline reflects the current state:
node {SKILL_PATH}/scripts/fizz_sync.js {PROJECT_ROOT} --refresh-snapshot --meta-dir {META_DIR} --suite-dir {SUITE_DIR}
This writes {PROJECT_ROOT}/{META_DIR}/last-run.json — a hash+signature snapshot of the in-scope contracts, handlers, and PROPERTIES.md entries. Later, when the user modifies sources and invokes /fizz-sync, that skill diffs against this file to detect added/removed/changed functions, quarantine stale properties, and regenerate only the drifted handler stubs.
fizz/skills/fizz-convert/SKILL.md
npx skills add pashov/skills --skill fizz-convert -g -y
SKILL.md
Frontmatter
{
"name": "fizz-convert",
"description": "Convert English-language properties in PROPERTIES.md (produced by the Fizz skill) into Solidity assertions inside the existing fuzz harness, then flip their checkboxes. Trigger on \"fizz-convert\", \"convert properties\", \"implement properties from PROPERTIES.md\", \"convert PROPERTIES.md to Solidity\"."
}
Property Conversion Skill — fizz-convert
You are an expert Solidity fuzzing engineer working inside a project that was set up by the Fizz skill. Your job is to convert English-language properties in PROPERTIES.md (project root) into Solidity code inside the existing harness, then flip their checkboxes from [ ] to [x].
Arguments: optional space-separated property IDs (e.g., GL-01 SP-03) passed when the user invokes the skill. If no IDs are given, convert ALL properties currently marked [ ].
Parameters
SUITE_DIR: Solidity suite directory relative to project root (default:test/fizz). Use the same value that was passed to thefizzskill when the harness was generated.META_DIR: Metadata directory relative to project root (default:fizz_data). Use the same value that was passed to thefizzskill.
Checkbox states and how they map to actions
PROPERTIES.md uses three states. The action you take depends on BOTH the current checkbox AND whether tagged Solidity for that Spec ID already exists in Properties.sol / handlers.
Tagged Solidity = a function whose natspec starts with /// @notice <ID>: (e.g. /// @notice GL-05:). The implementer agents are required to emit this doctag for every property they write, so it is the canonical way to find the existing code for any Spec ID. Search both {SUITE_DIR}/Properties.sol and every file under {SUITE_DIR}/handlers/.
Action matrix:
| Current checkbox | Tagged Solidity exists? | Action |
|---|---|---|
[ ] |
NO | Implement fresh. Normal pending case. On success → [x]. On infeasible/skip → [-]. |
[ ] |
YES | Regenerate. The user manually downgraded [x]→[ ] because they want it re-implemented (description changed, prior impl was wrong, etc.). Delete the existing tagged function AND any handler call sites (for SP-*), then implement fresh as if it were the row above. On success → [x]. |
[x] |
YES | Skip. Already implemented. Never re-implement, never overwrite. If the user explicitly listed this ID in arguments, warn and skip. |
[x] |
NO | DRIFT — stop and warn. The spec claims it's done but no tagged Solidity exists. This means either the user deleted the function manually without updating PROPERTIES.md, or the doctag was lost. Do NOT silently rewrite the spec. Print a warning naming the ID and ask the user to either restore the function, flip the checkbox to [ ], or flip it to [-]. Skip this ID for the rest of this run. |
[-] |
NO | Leave alone. Manual / do-not-touch. Never read as a target, never flip, never write Solidity. If the user explicitly listed a [-] ID, refuse and tell them to flip it to [ ] first. |
[-] |
YES | Drop. The user manually downgraded [x]→[-] because they want the property removed entirely. Delete the existing tagged function AND any handler call sites (for SP-*). Leave the checkbox as [-]. Report under "Dropped" in the summary. |
Hard rule: never modify a [x]+YES line and never flip [x]→anything automatically except as part of a build-failure rollback for a property you generated in the same run.
STEP 1: READ CURRENT STATE
Read in parallel:
PROPERTIES.md(project root) — the source of truth for what needs to be converted{SUITE_DIR}/Properties.sol— where property functions live{SUITE_DIR}/Base.sol— for theGhostsstruct,actorsarray,NUMBER_OF_ACTORS, available contract instances{SUITE_DIR}/Snapshots.sol— for theStatestruct,stateBefore/stateAfter, and_takeSnapshot{SUITE_DIR}/handlers/— every handler file, to see existing function names,snapshotBefore()/snapshotAfter()placement, and ghost update conventions{META_DIR}/property-plan.mdif it exists — for the ghost/snapshot/wiring tables that back each Spec ID- The relevant in-scope source contracts (only the ones referenced by the properties you are about to convert)
If PROPERTIES.md does not exist at the project root, stop and tell the user to run the Fizz skill (Step 9) first.
STEP 1.5: RECONCILE SPEC WITH CODE
For every Spec ID in PROPERTIES.md (regardless of checkbox state), check whether tagged Solidity exists. The doctag pattern is exact:
/// @notice GL-NN:
/// @notice SP-NN:
Grep {SUITE_DIR}/Properties.sol and {SUITE_DIR}/handlers/**/*.sol for each pattern. Build a single in-memory map: spec_id → {checkbox, has_tagged_solidity, function_name_if_found, file_if_found, handler_call_sites_if_SP}.
Then classify each ID against the action matrix above. The four action types are:
- IMPLEMENT (
[ ]+ NO Solidity) — process in STEP 4 normally. - REGENERATE (
[ ]+ Solidity) — first delete, then process in STEP 4. - DROP (
[-]+ Solidity) — delete only, no STEP 4 work. - DRIFT_WARN (
[x]+ NO Solidity) — print a warning and skip this ID. Do NOT touch the checkbox. - SKIP (
[x]+ Solidity, or[-]+ NO Solidity) — no work.
Deletion procedure (used by REGENERATE and DROP)
For a Spec ID whose tagged Solidity must be removed:
- Locate the function block in
Properties.sol. Start at the line/// @notice <ID>:and read upward to capture any preceding///natspec lines that belong to the same block. Read downward through the function signature and body until the matching closing}at the function's brace depth. Delete the entire span (natspec + signature + body), plus any single blank line immediately following. - For
SP-*only, scan every file under{SUITE_DIR}/handlers/for call sites of the deleted function name. The grep target is the bare function name followed by((e.g.property_depositIncreasesTotalSupply(). Delete each such call line. If the call has surrounding comments specific to that property (e.g.// SP-01 postcondition), delete those too. - Do not touch any other functions, ghost variables, or snapshot fields. If the deleted property was the only consumer of a ghost field or snapshot field, leave the field in place — it is safer to have an unused field than to risk breaking another property's wiring.
- After all deletions for this run are queued, perform them in a single edit pass per file (so line numbers don't shift mid-stream).
Argument-list handling
If the user passed explicit IDs:
- Apply the matrix above to each requested ID.
- Requested ID is
DRIFT_WARN→ warn, skip, continue with the others. - Requested ID is
SKIPbecause already[x]+ Solidity → warn ("already implemented"), skip. - Requested ID is
[-]+ NO Solidity → refuse and tell the user to flip to[ ]first (this is the same rule as before). - Requested ID does not exist in
PROPERTIES.md→ stop and report.
STEP 2: IDENTIFY PROPERTIES TO CONVERT
Parse PROPERTIES.md. The format is:
- [ ] **GL-01** — <english description>. (Category: ...; Guarantee: SHOULD-HOLD|EXPLORATORY; Priority: ...; Sources: ...)
- [ ] **SP-01** — <english description>. (Category: ...; Guarantee: SHOULD-HOLD|EXPLORATORY; After: <handler>; Priority: ...; Sources: ...)
The parenthetical fields are metadata; only After: affects wiring. Preserve the entire parenthetical verbatim when you flip a checkbox — never strip the Guarantee: tag, since downstream triage (a violated SHOULD-HOLD = confirmed bug, EXPLORATORY = human review) depends on it. If you add a brand-new property of your own, tag it Guarantee: EXPLORATORY unless you can cite a doc/spec/standard or exact identity that makes it SHOULD-HOLD.
Use the action map you built in STEP 1.5. The set of IDs that get Solidity work in STEP 4 is the union of:
- All IDs classified as
IMPLEMENT - All IDs classified as
REGENERATE(these will have their existing Solidity deleted first)
DROP IDs get deletion-only in STEP 4 (no new Solidity). SKIP and DRIFT_WARN IDs do nothing.
If the user passed explicit IDs, intersect the working set with their list (per the rules in STEP 1.5).
For each selected property, classify:
| Prefix | Type | Where it lives | Visibility | When it runs |
|---|---|---|---|---|
GL- |
Global | Properties.sol |
public function property_*() |
After every handler call (fuzzer auto-discovers) |
SP- |
Specific | Properties.sol |
internal function property_*() |
Called explicitly at the end of a specific handler, after snapshotAfter() |
STEP 3: PLAN EACH IMPLEMENTATION
For each selected property, work out:
- Function name —
property_<camelCaseDescription>. Ifproperty-plan.mdalready lists a name for this Spec ID, use that exact name. - Reads from
stateBefore/stateAfter? — if yes, list the snapshot fields. If a needed field is missing fromSnapshots.sol'sStatestruct, you must add it (and update_takeSnapshotto populate it). - Reads from
ghosts? — if yes, list the ghost fields. If a needed field is missing fromBase.sol'sGhostsstruct, you must add it AND wire its update into the relevant handler(s). - Assertion helper — pick the right one:
eq,gt,gte,lt,lte,t(boolean). Always include a descriptive failure message string. - For SP- only* — which handler to wire it into. Match against the
After:field in PROPERTIES.md and the actual function name inhandlers/<Contract>Handler.sol. The call site is aftersnapshotAfter()and after any ghost updates. - Loop safety — no unbounded loops in global properties. Loops over
actorsare fine (NUMBER_OF_ACTORS is small).
If any property cannot be implemented as a real assertion (missing source data, requires unbounded iteration, requires state the harness cannot reach), do NOT write a fake assertion. Skip it, flip its checkbox to [-] (so future runs leave it alone), and report it under "Skipped" in the final summary.
STEP 4: APPLY EDITS
Process in this order:
4a. Deletions first (REGENERATE + DROP)
For every ID classified as REGENERATE or DROP in STEP 1.5, perform the deletion procedure (locate tagged function block, delete it, delete handler call sites for SP-*). Do all deletions BEFORE any new insertions, in a single edit pass per file. This keeps line numbers stable and avoids the situation where a regenerated property's new function lands on top of its own old line range.
4b. Insertions (IMPLEMENT + REGENERATE)
For each property in the implementation set, in turn:
- If new ghost fields are needed: edit
Base.sol'sGhostsstruct. - If new snapshot fields are needed: edit
Snapshots.sol'sStatestruct AND_takeSnapshotso the field is populated for both before and after. - Add the property function to
Properties.solin the appropriate section (Global properties section forGL-*, Specific properties section forSP-*). Match the existing comment-banner style. MANDATORY: the natspec MUST start with/// @notice <ID>: <one-line description>so future runs can find and reconcile this function. - For
SP-*: edit the relevant handler file in{SUITE_DIR}/handlers/and call the property function aftersnapshotAfter(). If ghost updates are needed, add them before the property call.
4c. Checkbox flips
After STEP 5 (build) confirms success:
- For each
IMPLEMENTorREGENERATEID where the new Solidity compiled and (forSP-*) the call site is wired: change its checkbox to[x]. - For each
IMPLEMENTorREGENERATEID you decided to skip mid-implementation (infeasible, requires harness restructuring, etc.): change its checkbox to[-]. ForREGENERATEIDs that you abandoned mid-flight, the deletion in 4a still stands — the user explicitly asked for it to be re-done by downgrading from[x], so leaving the old code in place would contradict their intent. - For each
DROPID: leave the checkbox as[-](deletion was the whole job; the checkbox is already correct). - For each
DRIFT_WARNID: do not modify the checkbox at all.
Match by exact Spec ID. Do NOT renumber, reorder, or rewrite other lines. Never modify a line that was already [x] and had matching Solidity at the start of the run (those are SKIP).
Respect the existing inheritance chain in the Fizz skill's references/template-map.md if present — do not invent new files.
STEP 5: BUILD
Run from the project root:
forge build
If foundry.toml defines a [profile.fuzz] section, prefer:
FOUNDRY_PROFILE=fuzz forge build
Fix any compile errors caused by your edits. Do NOT touch unrelated compile errors that already existed before your changes — report them instead.
If a property you wrote fails to compile and you cannot fix it within 2 attempts:
- Revert that single property's edits (Properties.sol + any handler wiring + any ghost/snapshot fields you added solely for it)
- Flip its checkbox to
[-]so future runs leave it alone - Report it under "Failed to compile" in the summary
For a REGENERATE ID whose new Solidity fails to compile: do NOT restore the deleted old version. The user downgraded [x]→[ ] deliberately; restoring the old code would silently undo their intent. Treat this exactly like an IMPLEMENT failure — flip to [-] and report. The user can manually reinstate the old function from git if they want it back.
STEP 6: REPORT
Print a concise summary:
fizz-convert results
Implemented (NEW, checkbox flipped to [x]):
GL-01 property_solvency
SP-03 property_depositIncreasesShares (wired into vault_deposit)
Regenerated (deleted + re-implemented, checkbox stays [x]):
GL-05 property_oTokenB_perTokenSolvency
SP-02 property_roundTripNoFreeValue (wired into vault_roundTrip)
Dropped (deleted, checkbox stays [-]):
GL-12 removed property_oldHelper
Skipped (checkbox flipped to [-]):
GL-07 reason: requires unbounded loop over historical state
Drift warnings (NO change made):
GL-09 PROPERTIES.md says [x] but no /// @notice GL-09: function found.
Please restore the function or flip the checkbox to [ ] / [-].
Failed to compile (reverted, flipped to [-]):
SP-09 reason: <error>
Build: PASS / FAIL
Do NOT mark a property [x] unless its Solidity exists, the build passes, and (for SP-*) it is actually called from a handler.
fizz/skills/fizz-sync/SKILL.md
npx skills add pashov/skills --skill fizz-sync -g -y
SKILL.md
Frontmatter
{
"name": "fizz-sync",
"description": "Reconcile an existing Fizz harness with a changed source tree. Detects added\/removed\/changed contract functions, quarantines stale properties, regenerates drifted handler stubs, and refreshes the snapshot. Trigger on \"fizz-sync\", \"resync fuzzing\", \"sync fuzz harness\", \"refresh fuzzing properties\", \"fuzzing drift check\"."
}
Fizz Sync Skill — fizz-sync
Reconcile a project previously processed by the Fizz skill with a changed source tree. This is the re-use entry point: after the user modifies Solidity sources, fizz-sync detects drift, quarantines stale properties, regenerates drifted handler stubs, and refreshes the snapshot — WITHOUT re-running the full 11-step pipeline.
When to use
- The source contracts under
src/changed sincefizzlast ran. - A property in
PROPERTIES.mdfails to compile or references a function that no longer exists. - The user added new external functions they want fuzzed.
- The user wants a "is anything stale?" health check before re-running a campaign.
Parameters
SUITE_DIR: Solidity suite directory relative to project root (default:test/fizz). Use the same value that was passed to thefizzskill when the harness was generated.META_DIR: Metadata directory relative to project root (default:fizz_data). Use the same value that was passed to thefizzskill.
Arguments
--init— one-time bootstrap: create{META_DIR}/last-run.jsonfrom the current state. Use this the first time fizz-sync runs on a project (or when adopting fizz-sync on a project that was fuzzed before the snapshot format existed).--only <Contract>— scope the sync to a single contract name.--apply— actually perform the automatic fixes. Without--apply, fizz-sync runs in dry-run mode and only reports drift.--no-property-quarantine— skip the property quarantine phase (useful if the user wants to handle stale properties manually).
The default (no flags) runs a dry-run drift report.
Preconditions
- The project must have been processed by the
fizzskill at least once. Look for:{PROJECT_ROOT}/{META_DIR}/contracts.json{PROJECT_ROOT}/{META_DIR}/entry-point-selection.json{PROJECT_ROOT}/{SUITE_DIR}/(suite directory)
- If any of the above are missing, stop and tell the user to run
fizzfirst.
STEP 0: REBUILD CURRENT ABIS
The snapshot diff compares against contracts.json and entry-point-selection.json. Those files reflect the state from the last fizz run, NOT the current source tree. You must refresh them first.
Run sequentially:
-
cd {PROJECT_ROOT} && forge build --skip 'test/**/*.sol'The
--skipflag is critical: if the user's source change broke downstream call sites intest/, a plainforge buildwill fail on the harness code — which is exactly the drift fizz-sync is supposed to fix. Skippingtest/means we only validate that the SOURCE compiles cleanly, which is all we need to refresh the ABI.If the build still fails with
--skip 'test/**/*.sol', the source itself has a compile error. Stop and report it. Fuzz-sync cannot proceed without valid source artifacts. -
node {SKILL_PATH}/../../scripts/extract_abis.js {PROJECT_ROOT} --meta-dir {META_DIR}This refreshes
contracts.jsonfrom the latest artifacts. It overwrites the old file in place. -
Do NOT re-run
select_functions.jsautomatically. The existingentry-point-selection.jsonencodes the user's prior tier assignments and selection choices. fizz-sync treats it as the source of truth for what's "in scope" and diffs only within that scope.If new contracts have been added that the user probably wants to fuzz, the drift report will surface them and the user can manually add them to
entry-point-selection.jsonand re-run fizz-sync.
STEP 1: HANDLE --init
If the user passed --init:
-
Run:
node {SKILL_PATH}/../../scripts/fizz_sync.js {PROJECT_ROOT} --init -
If the script reports the snapshot already exists, ask the user whether they want to overwrite with
--force(they usually do NOT — it would erase drift history). -
Report the number of contracts and properties captured, then stop. No further steps.
STEP 2: RUN THE DRIFT REPORT
Run:
node {SKILL_PATH}/../../scripts/fizz_sync.js {PROJECT_ROOT}
The script:
- Reads
{META_DIR}/last-run.json(created by--initor by Step 11 of the mainfizzskill). - Builds a fresh snapshot from the current
entry-point-selection.json, source file hashes, andPROPERTIES.md. - Diffs them and writes
{META_DIR}/sync-report.json. - Prints a human-readable summary.
- Exits 0 if no drift, 1 if drift was detected.
If exit 0 and no drift, stop and tell the user the harness is already in sync.
Read the JSON report:
{PROJECT_ROOT}/{META_DIR}/sync-report.json
The JSON schema is:
{
"generatedAt": "...",
"hasDrift": true,
"contracts": {
"added": [{"name":"...", "sourcePath":"...", "functions":[{"signature":"...", "tier":"primary"}]}],
"removed": [{"name":"...", "handlerFile":"...Handler.sol"}],
"changed": [{"name":"...", "functionsAdded":[...], "functionsRemoved":[...], "functionsChanged":[{"oldSignature":"...","newSignature":"...","handlerMethodName":"..."}], "tierChanged":[...]}],
"sourcesChanged": [{"name":"...", "sourcePath":"..."}]
},
"handlers": {
"orphan": [{"file":"...", "contract":"..."}],
"modified": [{"file":"..."}],
"missing": [{"file":"..."}]
},
"properties": {
"referencingRemoved": [{"file":"...", "reference":"lender_oldMethod", "reason":"..."}],
"fromSnapshot": [{"id":"GL-01", "checkbox":"x", "functionName":"property_..."}]
}
}
Use this object as the canonical work list for the rest of the skill.
STEP 3: DRY-RUN SUMMARY (ALWAYS)
Regardless of whether --apply was passed, print a concise summary to the user that mirrors the report:
fizz-sync drift report
──────────────────────
Added contracts: N
Removed contracts: N
Drifted contracts: N (M added / M removed / M changed functions)
Orphan handlers: N
Stale properties: N
Modified handlers: N (user-edited since last snapshot)
If fizz-sync was invoked WITHOUT --apply, stop here and tell the user:
Dry-run complete. Re-run with
--applyto regenerate handlers and quarantine stale properties, or handle items manually using the report at{META_DIR}/sync-report.json.
STEP 4: APPLY — HANDLER REGENERATION
Only run this step if --apply was passed.
4a. Back up user-modified handlers
For every entry in report.handlers.modified, the user has hand-edited that handler since the last snapshot. Regenerating it would destroy their clamping logic. The script already backs up <file>.pre-sync.bak, but the user should be warned.
Print a warning listing every modified handler and ask the user whether to proceed. If the user says no, skip to Step 5.
4b. Regenerate drifted handlers
Run:
node {SKILL_PATH}/../../scripts/fizz_sync.js {PROJECT_ROOT} --apply-handlers
The script:
- Regenerates handler files for contracts listed in
report.contracts.addedandreport.contracts.changed. - Creates
<name>Handler.sol.pre-sync.bakbackups next to each regenerated file. - Uses the existing
generate_handlers.jsunder the hood with a scoped selection file, so tier assignments are respected.
After the script runs, for each regenerated handler:
- Read the
.pre-sync.bakversion and the new version. - Port any still-valid clamping bodies and ghost wiring from the backup into the new handler. Functions whose signature did not change can usually be copied verbatim. Functions whose signature changed must be manually rewritten against the new signature.
- Keep
/// @notice SP-NN:doctags intact — they are howfizz-convertand futurefizz-syncruns identify which Solidity corresponds to which Spec ID. - For functions that were REMOVED from the source contract, delete the corresponding handler method from the regenerated file AND delete any call sites elsewhere.
4c. Handle orphan handlers
For every entry in report.handlers.orphan, the contract no longer exists in the selection. Offer the user two choices:
- Delete — remove the handler file entirely and remove its inheritance line from
handlers/Handlers.sol. Also remove anycontract_handlerfield wiring. - Quarantine — leave the file in place but rename it
<name>Handler.sol.orphansogenerate_suite.jsdoes not re-pick it up, and comment out its inheritance line inHandlers.solwith a// FUZZ-SYNC: orphan — contract removedmarker.
Default to quarantine. Only delete if the user explicitly confirms.
4d. Handle missing handlers
For every entry in report.handlers.missing, the file disappeared since the last snapshot. If the contract still exists in contracts.changed / unchanged, treat this as a regeneration case — re-run the generate step without --only scoping (or manually invoke generate_handlers.js) to recreate it.
STEP 5: APPLY — PROPERTY QUARANTINE
Skip if --no-property-quarantine was passed.
This is the most delicate phase. A property can be stale for several reasons:
- Its tagged handler function was renamed or removed (detected in
report.properties.referencingRemoved). - A ghost variable it depends on was deleted.
- The source function it asserts against changed semantics without changing signature (
report.contracts.sourcesChanged). - It no longer compiles at all.
5a. Compile-first triage
Run the scoped build that skips non-harness test files:
cd {PROJECT_ROOT} && FOUNDRY_PROFILE=fuzz forge build $(find test -maxdepth 1 -name '*.sol' -exec echo --skip {} \;)
(fall back to plain forge build ... without FOUNDRY_PROFILE=fuzz if no fuzz profile is configured).
The find ... --skip wrapper is essential: without it, compile errors in the user's own top-level test files (e.g. test/MyContract.t.sol, which is outside the fuzzing harness and NOT managed by this skill) would block the build and prevent fizz-sync from running its quarantine logic. Those top-level test files are the user's responsibility to fix; this skill only owns {SUITE_DIR}/.
If the build FAILS, parse every error. For each error whose location is inside {SUITE_DIR}/:
- Identify the enclosing function (walk up from the error line until you hit a
function <name>(header). - Check whether that function has a
/// @notice (GL-NN|SP-NN):doctag — if yes, the Spec ID is the source of truth for reconciliation. - Quarantine the function:
- Replace the entire function body with a single
revert("FUZZ-SYNC: quarantined");statement. - Move the old body into a multi-line comment (
/* ... */) directly above the function so the user can see the original logic and restore it manually when ready. - Add a single-line annotation above the comment:
// FUZZ-SYNC: quarantined — <short reason from build error>. Restore by deleting the revert body and uncommenting the block above. if (false) { ... }is NOT a valid quarantine strategy — Solidity type-checks dead branches and the original compile error will persist.
- Replace the entire function body with a single
- If the function had a Spec ID, flip its checkbox in
PROPERTIES.mdfrom[x]to[~]and append(stale — source changed, re-opened)to the description. - Rebuild. Repeat up to 3 cycles. If errors persist after 3 cycles, stop and report the remaining errors to the user — they need manual help.
Important: do NOT quarantine handler methods this way. If a handler method fails to compile, the fix belongs in Step 4b (handler regeneration) or 4c (orphan handling), not here.
5b. Semantic-drift sweep
After the compile triage passes, scan report.properties.referencingRemoved. For each entry:
- Find the enclosing tagged function in the named file.
- Read the whole function body.
- If the dead reference is the ONLY purpose of the property (e.g., the property exists solely to check a post-condition of a now-removed handler call), quarantine it per the same procedure as 5a.
- If the dead reference is incidental (e.g., the property also checks other state), leave it alone but add a
// FUZZ-SYNC: references removed method <name>marker at the reference site. Rely on the compile triage to catch it on the next cycle if it actually breaks.
5c. Source-hash-only drift
For every entry in report.contracts.sourcesChanged that did NOT appear in contracts.changed (i.e., the source file changed but the ABI did not):
- This is a semantic refactor. Properties still compile but might assert the wrong thing.
- Do NOT auto-quarantine. Instead, list the affected contracts under a "Review recommended" section in the final report so the user can eyeball the properties tied to those contracts.
STEP 6: REBUILD AND VALIDATE
Run:
cd {PROJECT_ROOT} && FOUNDRY_PROFILE=fuzz forge build $(find test -maxdepth 1 -name '*.sol' -exec echo --skip {} \;)
(fall back to forge build ... without the profile variable if no fuzz profile is configured).
If it fails, go back to Step 5a for one more cycle. If it has already been through 3 cycles, stop and report.
If it succeeds, proceed to snapshot refresh.
STEP 7: REFRESH SNAPSHOT
Only after Step 6 succeeds and the user is happy with the result:
node {SKILL_PATH}/../../scripts/fizz_sync.js {PROJECT_ROOT} --refresh-snapshot
This overwrites {META_DIR}/last-run.json with the post-sync state. Future fizz-sync runs will diff against this new baseline.
STEP 8: REPORT
Print a concise summary:
fizz-sync results
─────────────────
Handlers regenerated:
VaultHandler.sol (3 added, 1 removed, 1 signature changed)
LendingPoolHandler.sol (2 added)
Handlers ported from backup:
VaultHandler.sol (5/6 clamping bodies preserved)
Orphan handlers:
OldStakingHandler.sol quarantined (renamed .orphan)
Properties quarantined:
SP-04 referenced removed function lender_oldDeposit
SP-07 build error on line 128
Properties untouched (review recommended):
GL-02 source hash changed but ABI stable — verify semantic intent
GL-09 idem
Build: PASS
Snapshot refreshed: {META_DIR}/last-run.json
Next actions
Tell the user:
- For newly added functions without handler bodies: re-run
fizzStep 7 for just those contracts, or use thefizz-convertskill if the user also wants to regenerate properties for them. - For quarantined properties: either rewrite them manually (flip
[~]back to[ ]inPROPERTIES.mdand run/fizz-convert), or leave them quarantined. - For source-changed contracts without ABI drift: read the diff manually to confirm existing properties still encode the intended semantics.
RULES
- Never run a full Fizz pipeline as part of a sync. Sync is surgical by design.
- Never regenerate ALL handlers — only the drifted subset.
- Never auto-delete orphan handlers without user confirmation.
- Never flip a
[ ]or[-]checkbox to[x]in this skill. Only[x] → [~]for quarantining is allowed. - Always back up files before destructive edits. The
.pre-sync.bakfiles are the user's safety net. - Always refresh the snapshot at the end of a successful
--applyrun, and NEVER refresh it mid-run before Step 6 passes — that would bake in the broken state.
solidity-auditor/SKILL.md
npx skills add pashov/skills --skill solidity-auditor -g -y
SKILL.md
Frontmatter
{
"name": "solidity-auditor",
"description": "Security audit of Solidity code while you develop. Trigger on \"audit\", \"check this contract\", \"review for security\". Modes - default (full repo) or a specific filename."
}
Smart Contract Security Audit
You are the orchestrator of a parallelized smart contract security audit.
Mode Selection
Exclude pattern: skip directories interfaces/, lib/, mocks/, test/ and files matching *.t.sol, *Test*.sol or *Mock*.sol.
- Default (no arguments): scan all
.solfiles using the exclude pattern. Use Bashfind(not Glob). $filename ...: scan the specified file(s) only.
Flags:
--file-output(off by default): also write the report to a markdown file (path per{resolved_path}/report-formatting.md). Never write a report file unless explicitly passed.
Orchestration
Turn 1 — Discover. Print the banner, then make these parallel tool calls in one message:
a. Bash find for in-scope .sol files per mode selection
b. Glob for **/references/hacking-agents/shared-rules.md — extract the references/ directory (two levels up) as {resolved_path}
c. ToolSearch select:Agent
d. Read the local VERSION file from the same directory as this skill
e. Bash curl -sf https://raw.githubusercontent.com/pashov/skills/main/solidity-auditor/VERSION
f. Bash mktemp -d ./.audit-XXXXXX → store as {bundle_dir}
If the remote VERSION fetch succeeds and differs from local, print ⚠️ You are not using the latest version. Please upgrade for best security coverage. See https://github.com/pashov/skills. If it fails, skip silently.
Turn 1b — Model selection (Claude Code only). This turn applies ONLY when both AskUserQuestion and the Agent tool (with a model parameter) are available in your runtime — i.e., Claude Code. On Codex, Gemini, Cursor's native agent, or any runtime without these, SKIP this turn entirely, leave {agent_model} unset, and proceed to Turn 2. Do NOT emit the question as prose. Do NOT substitute any other mechanism.
On Claude Code:
-
Read your system prompt to detect your own model family (Opus, Sonnet, or Haiku). Ignore the version digits — the Agent tool's
modelparameter takes the family name ("opus"/"sonnet"/"haiku"), and the runtime resolves to the latest version in that family. -
Call
AskUserQuestionwith:- Question:
"Which Claude model should the 12 audit agents use?" - Three single-select options. Mark the orchestrator's own family as
(Recommended)and place it first. - On each option, set the
descriptionfield tolatest. - On each option, set the
previewfield verbatim (preserve all whitespace exactly — the box widths must stay equal across all three):
Opus preview:
┌──────────────────────────────────────────────────────────┐ │ opus · highest reasoning · most expensive │ └──────────────────────────────────────────────────────────┘Sonnet preview:
┌──────────────────────────────────────────────────────────┐ │ sonnet · balanced reasoning · mid cost │ └──────────────────────────────────────────────────────────┘Haiku preview:
┌──────────────────────────────────────────────────────────┐ │ haiku · lowest reasoning · cheapest │ └──────────────────────────────────────────────────────────┘ - Question:
-
Store the runner's choice as
{agent_model}. If no answer, default to the orchestrator's own model.
Turn 2 — Prepare. In one message, make parallel tool calls: (a) Read {resolved_path}/report-formatting.md, (b) Read {resolved_path}/judging.md.
Then build all bundles in a single Bash command using cat (not shell variables or heredocs):
{bundle_dir}/source.md— ALL in-scope.solfiles, each with a### pathheader and fenced code block.- Agent bundles =
source.md+ agent-specific files:
| Bundle | Appended files (relative to {resolved_path}) |
|---|---|
agent-1-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/math-precision-agent.md + hacking-agents/shared-rules.md |
agent-2-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/access-control-agent.md + hacking-agents/shared-rules.md |
agent-3-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/economic-security-agent.md + hacking-agents/shared-rules.md |
agent-4-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/execution-trace-agent.md + hacking-agents/shared-rules.md |
agent-5-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/invariant-agent.md + hacking-agents/shared-rules.md |
agent-6-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/periphery-agent.md + hacking-agents/shared-rules.md |
agent-7-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/first-principles-agent.md + hacking-agents/shared-rules.md |
agent-8-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/asymmetry-agent.md + hacking-agents/shared-rules.md |
agent-9-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/boundary-agent.md + hacking-agents/shared-rules.md |
agent-10-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/numerical-gap-agent.md + hacking-agents/shared-rules.md |
agent-11-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/trust-gap-agent.md + hacking-agents/shared-rules.md |
agent-12-bundle.md |
source.md + senior-auditor-sop.md + hacking-agents/flow-gap-agent.md + hacking-agents/shared-rules.md |
Each bundle = source.md + SOP + specialty + shared-rules. Agents read the bundle; no Read/Grep needed for the initial scan. Targeted Read/Grep allowed for cross-file investigation.
Print line counts for every bundle and source.md. Do NOT inline source code into the Agent call prompt itself.
Turn 3a — Spawn all 12 agents. In one message, spawn all 12 agents as parallel BACKGROUND Agent calls (run_in_background=true). If Turn 1b set {agent_model}, pass model={agent_model} on every Agent call. If {agent_model} is unset (Turn 1b skipped — Codex, Gemini, others), omit the model parameter entirely — do NOT substitute any default. The orchestrator will receive a notification when each agent completes — do NOT poll or sleep. Single phase, no later spawns. Proceed to Turn 3b only after all 12 have notified completion.
Agents 1–9 use the single-specialty prompt (Turn 3a-i). Agents 10–12 use the gap-hunter prompt (Turn 3a-ii).
Turn 3a-i — Single-specialty prompt template (agents 1–9, substitute real values):
You are an attacker. Your specialty, mindset, source, and output rules
are in your bundle. Read it fully before producing findings.
Read first:
- {bundle_dir}/agent-N-bundle.md (XXXX lines) — source + SOP + specialty + shared rules.
The bundle contains all in-scope source. Do NOT re-read in-scope files
for the initial scan. Use Read/Grep only for cross-file searches or
out-of-scope context (interfaces/, lib/, mocks/, test/).
What a finding looks like:
- file, function
- root cause — the one-sentence code-level defect
- minimal fix — the smallest change that eliminates the defect
- proof — concrete numbers, a trace, or quoted code
Without concrete proof, it's a LEAD, not a finding. Leads are honest
about what you couldn't verify — they're not failures, they're
calibration. Emit them.
Don't skim. Don't trust your first read. Trust your discomfort.
Output format: see shared-rules.md inside your bundle.
Turn 3a-ii — Gap-hunter prompt template (agents 10–12, substitute real values):
You are an attacker. Your gap-hunter specialty, mindset, source, and
output rules are in your bundle. Read it fully before producing findings.
Read first:
- {bundle_dir}/agent-N-bundle.md (XXXX lines) — source + SOP + gap-hunter specialty + shared rules.
The bundle contains all in-scope source. Do NOT re-read in-scope files
for the initial scan. Use Read/Grep only for cross-file searches or
out-of-scope context (interfaces/, lib/, mocks/, test/).
What a finding looks like:
- file, function
- seam — which two or three lenses combine
- root cause — the one-sentence code-level defect that lives at the seam
- minimal fix — the smallest change that eliminates the defect
- proof — concrete numbers, a trace, or quoted code showing the seam
Without concrete proof of the seam, it's a LEAD, not a finding.
Leads are honest about what you couldn't verify — they're not failures,
they're calibration. Emit them.
Don't skim. Don't trust your first read. Trust your discomfort.
Output format: see shared-rules.md inside your bundle (gap-hunter-specific
output fields are in your specialty file).
Turn 3b — Wait for all 12 agents to complete. Once every one of the 12 spawned agents has notified completion, proceed to Turn 4. Do NOT proceed to dedup until every agent has finished — let them run to natural completion. Do NOT poll or sleep; act only on completion notifications.
Turn 4 — Deduplicate, validate & output. Single-pass: deduplicate all agent results, gate-evaluate, and produce the final report in one turn. Do NOT print an intermediate dedup list — go straight to the report.
-
Dedup. Parse every FINDING and LEAD from the 12 agents. Group by
group_key(Contract | function | bug-class). Exact-match first; merge synonymous bug_class within same (Contract, function). Keep best per group, number sequentially, annotate[agents: N].MANDATORY — Wide-description (group_key). Merged group with distinct mechanisms (different
fix:, code-level cause, or attack path) MUST list every mechanism. No dropping. Same function can have multiple coexisting bugs at the same group_key — all MUST appear.MANDATORY — Function-level second pass (after group_key dedup). Run at (Contract, function), ignoring bug_class. Agents often label coexisting bugs with different bug_class tags but reference multiple mechanisms in the body. For every (Contract, function) with multiple final findings: scan body (description, path, proof, fix) of every constituent for distinct mechanisms across bug_class boundaries. Every mechanism in any constituent body MUST appear in ≥1 final finding.
MANDATORY — Function isolation (HARD). NEVER merge across different
function:fields. Dedup only within (Contract, function). Different function = different bug. Second pass above stays WITHIN (Contract, function), never across.MANDATORY — Fix preservation (HARD GATE). Before writing merged
fix:on a multi-finding (Contract, function):- Collect every raw
fix:from agents flagging the tuple. - Group by ADD-lines (
+lines, or equivalent require/assignment). - Distinct if ADD-lines differ in: called function/expression (e.g.,
require(msg.value == amount)vsrequire(zrc20 != _ETH_ADDRESS_)), check direction (validate/restrict/ban), or checked parameter. - ≥2 distinct → present as Option A, B, … — one block per distinct fix, verbatim from agent text (no paraphrase).
- Label intuitively: validate / restrict / allow-and-handle / ban-path.
Output format when 2+ distinct fixes exist:
**Fix (Option A — <label>)**: ```diff <verbatim diff from raw agent N1's fix>Fix (Option B — :
<verbatim diff from raw agent N2's fix>**Inline check before printing**: count distinct fixes from raw for this (Contract, function). ≥2 distinct but merged shows 1 → violation, add alternatives. **MANDATORY — Completeness (HARD GATE).** Before print: list every unique (Contract, function, bug-class) in any raw FINDING/LEAD across the 12 agents. Every unique (Contract, function) MUST have ≥1 item in final. Zero = silent drop, fix it. Multiple bug-class within same (Contract, function) MAY collapse to one item (wide-description), but the (Contract, function) MUST survive. Print inline before report: `Completeness: N unique (Contract, function) in raw, N covered in final.` Composite chains: if A's output feeds B's precondition AND combined impact > either alone, add `Chain: [A] + [B]` at conf = min(A, B). Most audits: 0–2. - Collect every raw
-
Gate. Run each deduped finding through the four gates in
judging.md(no skip, no reorder, no revisit after verdict).Single-pass: every relevant code path ONCE in fixed order (constructor → setters → swap → mint → burn → liquidate). One-line verdict:
BLOCKS/ALLOWS/IRRELEVANT/UNCERTAIN.UNCERTAIN = ALLOWS. Commit, no re-examination. -
Lead promotion / rejection.
- LEAD → FINDING (conf 75) if: full exploit chain in source, OR
[agents: 2+]demoted (not rejected) same issue. [agents: 2+]does NOT override a code path that interrupts attack before harm — demote to LEAD if execution uncertain.- No deployer-intent reasoning — what code allows, not how deployer might use it.
- LEAD → FINDING (conf 75) if: full exploit chain in source, OR
-
Format/print per
report-formatting.md. Exclude rejected. If--file-output: also write to file. Do NOT re-read source to "verify the most critical claim" — agents did that, dedup filtered. Re-verification costs ~5min, rarely changes verdicts. Skip. -
Auto-clean. After print (and
--file-outputwrite):rm -rf {bundle_dir}. Bundle dir = transient build state, not an artifact. Don't skip. For debugging: copy bundle elsewhere before re-running.
Banner
Before doing anything else, print this exactly:
██████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
██╔══██╗██╔══██╗██╔════╝██║ ██║██╔═══██╗██║ ██║ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
██████╔╝███████║███████╗███████║██║ ██║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║ ██║╚██╗ ██╔╝ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
██║ ██║ ██║███████║██║ ██║╚██████╔╝ ╚████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝
x-ray/SKILL.md
npx skills add pashov/skills --skill x-ray -g -y
SKILL.md
Frontmatter
{
"name": "x-ray",
"description": "Generates an x-ray.md pre-audit report covering overview, enhanced threat model (protocol-type profiling, git-weighted attack surfaces, temporal risk analysis, composability dependency mapping), invariants, integrations, docs quality, test analysis, and developer\/git history. Triggers on 'x-ray', 'audit readiness', 'readiness report', 'pre-audit report', 'prep this protocol', 'protocol prep', 'summarize this protocol'."
}
X-Ray
Generate an x-ray/ folder at the project root containing all output files. Pipeline: 3 steps, always sequential.
$SKILL_DIR = the directory containing this SKILL.md file. Resolve it from the path you loaded this skill from (e.g. if this file is at /path/to/x-ray/SKILL.md, then $SKILL_DIR = /path/to/x-ray).
Progress tracking (MANDATORY)
Before doing anything else, call TodoWrite with these 3 todos (all pending):
Phase 1: Enumerate & measure codebasePhase 2: Read sources, classify entry points, synthesize invariantsPhase 3: Write x-ray report files
Transitions (update via TodoWrite — never batch):
- Mark Phase 1
in_progressimmediately, before runningenumerate.sh. - When Step 1's parallel batch returns, in ONE TodoWrite call mark Phase 1
completedand Phase 2in_progress. - When Step 2 (including 2b–2g) finishes, in ONE TodoWrite call mark Phase 2
completedand Phase 3in_progress. - After all Step 3 output files are written, mark Phase 3
completed.
Rule: exactly one todo is in_progress at any time. Status updates happen the moment a phase starts or ends.
Step 1: Enumerate & Measure
If the user specifies a path, use it as project root. Otherwise use cwd. If no .sol files or foundry.toml/hardhat.config.* at root, check one level deep.
Source directory detection: Auto-detect from foundry.toml (src = "...") or hardhat.config.*. If no config, try both src/ and contracts/. Read foundry.toml first if present.
Run enumeration (single Bash call — includes output directory creation):
mkdir -p [project-root]/x-ray && bash $SKILL_DIR/scripts/enumerate.sh [project-root] [src-dir]
Immediately after, launch ALL of the following in a single message (parallel):
0. Version check (foreground):
- Read the local
VERSIONfile from$SKILL_DIR/VERSION - Bash
curl -sf https://raw.githubusercontent.com/pashov/skills/main/x-ray/VERSION - If the remote VERSION fetch succeeds and differs from local, print
⚠️ You are not using the latest version. Please upgrade for best security coverage. See https://github.com/pashov/skills. If it fails, skip silently.
1. Coverage (run_in_background: true):
For Foundry:
cd [project-root] && forge coverage 2>&1 || (echo "RETRYING_WITH_IR_MINIMUM" && forge coverage --ir-minimum 2>&1)
For Hardhat:
cd [project-root] && npx hardhat coverage 2>&1
If toolchain is not installed (e.g., forge: command not found, npx: command not found, missing node_modules/), the coverage command will fail. This is expected — test existence is already captured by enumeration in the step above. Coverage failure does NOT mean tests are absent.
2. Git security analysis + JSON read (foreground, single Bash call):
cd [project-root] && python3 $SKILL_DIR/scripts/analyze_git_security.py --repo . --src-dir [src-dir] --json x-ray/git-security-analysis.json 2>&1 && cat x-ray/git-security-analysis.json
The JSON has 7 sections: repo_shape, fix_candidates, dangerous_area_changes, late_changes, forked_deps, tech_debt, dev_patterns.
3. Preload reference files (2 parallel Read calls — these must be in context before Step 2d/3a):
$SKILL_DIR/references/threats.md— threat profiles, temporal threats, composability threats$SKILL_DIR/references/templates.md— output template, entry points template, architecture guide
4. Spec/whitepaper detection (1 Glob: **/{whitepaper,spec,design,protocol,architecture,overview,README}*.{pdf,md} excluding node_modules/, lib/, x-ray/, test/). Skip user-facing docs (tutorials, API refs, changelogs, contribution guides). Then apply size-aware handling:
- Path A (≤5 docs, each ≤300 lines): Include them as Read calls in Step 2's parallel message. Direct reads — no subagent needed.
- Path B (>5 docs OR any doc >300 lines): Launch a single subagent (
model: "sonnet") that reads ALL doc files and returns a structured extraction (max 200 lines). Subagent prompt:
Include this subagent in Step 1's parallel message. Its output feeds Step 3 report writing.Read each doc file listed below. Extract ONLY security-relevant information into this format: Files: [list of doc file paths] Return this exact structure: ### Doc-Stated Global Invariants [Bullet list of every invariant, constraint, or guarantee the docs claim must hold globally across calls. Treated like NatSpec-stated invariants by Step 2g — routed directly to §2 / §3 / §4 of invariants.md by shape, NOT into §1 (Enforced Guards, which is per-call preconditions only).] ### Actor Definitions [Each actor/role with stated permissions and trust level] ### Trust Assumptions [What the protocol assumes about external systems, oracles, admins, users] ### Cross-System Flows [How value/data moves between contracts or external systems] ### Economic Properties [Fee structures, reward mechanisms, tokenomics, bounded parameters] ### Key Design Decisions [Explicit "we chose X over Y because Z" statements] Rules: Quote the source doc for each claim. Omit sections with no relevant content. Max 200 lines total.
For both paths, extract only: doc-stated global invariants, actor definitions, cross-system flows, trust assumptions, economic properties, key design decisions. Tag all spec-derived claims in the report with (per spec) so auditors know what is code-verified vs spec-stated. Doc-stated global invariants feed Step 2g's NatSpec routing step — they route to §2 / §3 / §4 of invariants.md by shape, NOT to §1 (Enforced Guards).
ALL calls (coverage, git analysis, reference reads, spec glob) MUST appear in the same message. Proceed to Step 2 without waiting for forge coverage.
Step 2: Read Source Files + Entry Point Scan (SINGLE message, ALL tool calls parallel)
CRITICAL: Every tool call — Bash, Agent, Read, Grep — MUST be issued in ONE message so they run concurrently. This includes source file reads, the entry point grep scan, and any spec doc detected in Step 1 (they are all independent).
Scope Filtering
- Skip interfaces:
interfaces/dirs or filenamesI+ uppercase letter - Skip vendored libs: Uniswap FullMath/TickMath, OZ copies
- When uncertain, include it but exclude from scope table
Path A: ≤20 source files (direct reads)
One Read call per file. Do NOT read README, docs, or foundry.toml (already read in Step 1).
Extract per file: contract type & inheritance, roles & access control, value-holding state vars, external calls, fund flows, invariant comments, assert/require, backwards-compatibility indicators (see below), delta writes (per function: storage variables and the symbolic delta applied — e.g. Δ(totalSupply) = +shares, Δ(balanceOf[msg.sender]) = +shares — same-basic-block only, no cross-function inference; inherited helpers like OZ _mint/_burn may be resolved only when their effect on balanceOf/_totalSupply is semantically unambiguous), guard predicates (every require/assert/if-revert that references a storage variable, quoted verbatim with line number; skip guards that reference only function parameters), enum/one-shot transitions (every require(state == X); ...; state = Y pair, recorded as X@Lx → Y@Ly — include one-shot latches like require(addr == address(0)); addr = concrete).
Path B: >20 source files (parallel subagents)
Tier 1 — Small files (≤120 lines): Batch into single Bash cat call.
Tier 2 — Large files (>120 lines): Group by subsystem. Launch one subagent per subsystem (model: "sonnet", up to 5, max ~10 files each). Subagent prompt:
Read each file listed below and return a structured summary. Do NOT analyze — just extract facts.
Files: [list of file paths]
For EACH file, return this exact format:
### [filename]
- **Type**: contract | library | abstract
- **Inherits**: [parent contracts]
- **Imports**: [imported libraries/contracts]
- **Roles/Access**: [onlyOwner, role constants, modifiers]
- **State vars (value-holding)**: [mappings/vars that hold balances, collateral, etc.]
- **External calls**: [calls to other contracts, ERC20 transfers, etc.]
- **Fund flows**: [deposit/withdraw/mint/burn/transfer paths]
- **Invariants**: [require/assert statements, NatSpec invariant comments]
- **Delta writes**: For EACH non-view non-pure function, list storage variables that change and the symbolic delta applied. Use format `Δ(var) = +expr` or `Δ(var) = -expr`. Only report pairs where BOTH writes appear in the same function body with no intervening call to an unknown external contract. Do NOT chase writes through inherited/imported functions unless the semantic effect is unambiguous (OZ `_mint` → `balanceOf` + `_totalSupply` is fine; custom internal helpers are NOT — list those deltas only in the internal helper's own entry). Example:
- `deposit()`: `Δ(totalSupply) = +shares`, `Δ(balanceOf[msg.sender]) = +shares`
- `borrow()`: `Δ(totalBorrows) = +amount`, `Δ(underlyingBalance) = -amount`
- **Guard predicates**: Every `require`/`assert`/`if-revert` in the file that references a storage variable. Quote verbatim with line number. Skip guards that reference only function parameters.
- `Vault.sol:206`: `require(_fee <= 10, "fee is capped at 0.1%")`
- **Enum/one-shot transitions**: Every pattern of `require(var == X); ...; var = Y` where `var` is a storage enum, uint, or address. Record as `X@Lx → Y@Ly`. Include one-shot latches like `require(addr == address(0)); addr = concrete`.
- **Key logic**: [1-2 sentences on what the contract does]
- **Function-level access map** (REQUIRED for contracts, skip for libraries):
List every public/external non-view non-pure function with its access control:
- `functionName()` — [modifier name, e.g. `onlyRole(OCT_KEEPER)`] or [NONE — permissionless]
For functions with NO modifier, also list which external calls they make:
- `functionName()` — NONE — calls `ContractName.method()`
Entry Point Grep Scan (INCLUDED in the same parallel message as source reads)
Launch these two Bash calls in the SAME message as the source file reads above — they are independent and can run concurrently. Commands use only POSIX ERE + POSIX character classes (no -P / PCRE, no GNU-only escapes like \s \w \b), so they work identically on GNU grep (Linux/WSL), BSD grep (macOS default /usr/bin/grep, FreeBSD), and ripgrep:
# 1. Single-line signatures: function name and visibility on same line
grep -rnE 'function[[:space:]]+[[:alnum:]_]+[[:space:]]*\([^)]*\)[[:space:]]+(external|public)' [src-dir]/ --include='*.sol' \
| grep -v '/interfaces/' | grep -v '/mock/' \
| grep -Ev '(^|[^[:alnum:]_])(view|pure)([^[:alnum:]_]|$)'
# 2. Multiline signatures: visibility keyword on the closing-paren line (covers 90%+ of multiline cases)
grep -rnE '^[[:space:]]*\)[[:space:]]+(external|public)' [src-dir]/ --include='*.sol' -B5 \
| grep -v '/interfaces/' | grep -v '/mock/' \
| grep -Ev '(^|[^[:alnum:]_])(view|pure)([^[:alnum:]_]|$)'
Combine results from both. The multiline grep is critical — Solidity functions often split parameters across lines, putting external/public on the ) line while function name( is lines above. The trailing grep -Ev '(^|[^[:alnum:]_])(view|pure)([^[:alnum:]_]|$)' is the POSIX-portable substitute for \b(view|pure)\b: it drops any line where view or pure appears as a standalone identifier (surrounded by non-identifier chars or line boundaries), while preserving lines that merely contain view_param / pure_x identifier substrings.
Portability guarantees:
-E,-v,-r,-n,-B→ POSIX (2001+) / supported by macOS, FreeBSD, Linux GNU grep, busybox grep, ripgrep[[:space:]],[[:alnum:]_]→ POSIX character classes, supported by all above--include='*.sol'→ GNU + macOS BSD grep + ripgrep. Not supported by busybox grep (niche; Alpine minimal); if the skill ever needs to run there, replace--include='*.sol' [src]/with$(find [src]/ -name '*.sol')passed as arguments.
ALL tool calls (source reads/Bash/subagents, BOTH grep scans) MUST be in ONE message.
Do NOT read test files or documentation files.
Step 2b: Entry Point Classification
Using the grep results already returned from Step 2's parallel message, classify ALL entry points. Do NOT rely solely on subagent summaries — subagents extract facts at the contract level and can misattribute which function makes which external call or which function has which modifier.
Exclude from entry points: view/pure functions, interface-only declarations, library internal functions (they're downstream calls, not entry points), mock contracts.
For each result, classify into one of three categories:
- Permissionless — no access-control modifier AND no internal caller restriction in the function body. You MUST verify the function body before classifying as permissionless. For Path A (≤20 files), the bodies are already in context from Step 2 reads — classify directly without additional Read calls. For Path B, batch all candidate body reads into a SINGLE parallel message. Look for ANY of these patterns that restrict the caller:
require(msg.sender == X)orif (msg.sender != X) revert ...if (msg.sender != X || ...) revert ...(compound conditions)- Calls to internal functions that check
msg.senderA function without a modifier but WITH an internalmsg.sendercheck is role-gated, not permissionless. Common examples:acceptOwnership(),acceptMsig(),confirmX()— these often have no modifier but restrict the caller to a specific pending address viaif (msg.sender != pendingX) revert.
- Role-gated — has a role modifier (
onlyRole(X),onlyOwner,onlyRouter, etc.) OR an internalmsg.senderrestriction (viarequire,if/revert, or delegated check). Record which role or address is required. - Admin-only — gated by
DEFAULT_ADMIN_ROLE,onlyOwnerpointing to the protocol admin, or similar top-level authority.
Note: nonReentrant alone is NOT access control. initializer/reinitializer are one-time deployment functions — track separately.
For each entry point, record:
- Contract name and function name
- Access level (permissionless / role name / admin)
- Caller (User, Keeper, Admin, LP, etc.)
- Parameters with trust level:
(user-controlled),(user-signed),(keeper-provided),(protocol-derived) - Call chain: trace downstream calls using subagent summaries + function-level access maps. Format:
→ Contract.fn() → Contract.fn() - State modified: which storage vars/mappings change
- Value flow:
in(tokens deposited),out(tokens withdrawn),none - Reentrancy guard: yes/no
This data feeds TWO outputs:
- The permissionless entry points list in
x-ray.md(Section 2) — use the permissionless subset only - The full entry-points.md file (Step 3c) — uses all categories
Step 2b-flow: Protocol Flow Path Construction
Using the entry point data already collected in Step 2b, construct flow paths for entry-points.md. This is NOT a separate analysis pass — it reorganizes data you already have.
For each major user-facing entry point (permissionless and role-gated functions that move value):
- Identify its
requirestatements and state variable checks - For each check, find which function WRITES that state variable (already known from the "State modified" field of other entry points)
- Chain these backwards: destination ← writer of its precondition ← writer of THAT precondition ← ... ← deployment
- Note non-function preconditions (time passage, market conditions, external state) with
◄──annotations
Output: Simple arrow chains grouped by actor flow. Reference earlier flows instead of repeating. 15-30 lines total. See the Protocol Flow Paths section in the entry-points.md template for exact format.
The grep scan is a hard gate: the permissionless entry points section in the report must match this grep-verified list, not the subagent summaries. If there is a conflict, the grep + code reading result wins.
Step 2c: Backwards-Compatibility Code Detection
While reading source files, watch for code that appears to be remnants of a removed mechanism kept so the remaining codebase does not break. Common signals: empty or trivial function bodies, state variables declared but never meaningfully read or written, comments containing "deprecated" / "legacy" / "backwards compat" / "no longer used", functions that implement an interface but always return a default, and storage variables preserved solely for proxy storage layout compatibility.
After reading ALL source files, cross-reference candidates against these mandatory verification checks before classifying anything as backwards-compatibility. Batch ALL caller-check Grep calls for all candidates into a SINGLE parallel message — do not verify them one-by-one:
- Caller check (REQUIRED): Use Grep to confirm the function/variable has NO active callers in the current codebase. If it IS called from active code paths, it is NOT backwards-compatibility — it is the current design, regardless of whether it returns defaults or zeros.
- NatSpec/comment check (REQUIRED): If the code has NatSpec or inline comments explaining WHY it behaves a certain way (e.g., "simplified for X mode", "by design", "intentionally zero"), this is documented intentional design, NOT backwards-compatibility code. Do not override explicit developer documentation with heuristic pattern matching.
- Interface obligation check: A function that returns default values but exists because an interface requires it AND is actively called is part of the current architecture, not a remnant.
Only classify code as backwards-compatibility when ALL of: (a) no active callers exist, (b) no NatSpec/comments document the behavior as intentional, and (c) git history shows the mechanism it belonged to was removed.
Do not describe backwards-compatibility code as active features in the report. Instead, note them explicitly in Section 1 (see output template) so auditors know which parts of the codebase are retained for compatibility rather than being live functionality. If no backwards-compatibility code survives the verification checks above, omit the subsection entirely.
Step 2d: Centralization & Pause Coverage Analysis
After reading source files and classifying entry points, perform two analyses that feed into the Actors table, Trust Boundaries, and Key Attack Surfaces (Section 2). These are NOT standalone sections — the results integrate into existing report sections.
Centralization analysis — For each privileged role (admin, owner, operator, keeper, service, etc.):
- List every operational action the role can take (from the function-level access map)
- For each action, note whether a timelock, multi-sig, or delay exists. Distinguish between role transfer delays (e.g.,
AccessControlDefaultAdminRules1-day delay) and operational action delays — they are not the same. A role transfer delay does NOT protect against a compromised holder using instant operational functions. - Identify which actions can extract or redirect user funds (e.g.,
emergencyWithdraw,setTreasury,transferFee)
Integrate into: Actors table (Capabilities column should be specific about what's instant vs timelocked), Trust Boundaries (describe what each boundary actually protects vs what bypasses it), Key Attack Surfaces (frame as "Admin operational powers" or "[Role] compromise" — the attack surface is the role compromise, not individual functions).
Pause coverage analysis — For each critical state-changing function:
- Check whether
whenNotPaused(or equivalent) is applied - Note which functions are pausable vs not
- If a function that should logically be pausable is not (e.g., a function callable by a bounded role that operates on user funds), integrate this finding into the relevant attack surface for that role. The missing pause is not itself an attack surface — it's a detail that worsens the relevant role's compromise scenario.
Anti-pattern: Do NOT create a standalone "Centralization Risks" subsection. Centralization details belong distributed across Actors, Trust Boundaries, Key Attack Surfaces, and Protocol-Type Concerns. A dedicated section duplicates information already present in those sections. The same applies to pause coverage — integrate into the relevant role's attack surface description.
Step 2e: Protocol Classification
After reading source, classify the protocol following the procedures in references/threats.md (type detection + hybrid classification, phase detection, and external call classification — all in one file).
Step 2f: nSLOC
Use the exact nSLOC TOTAL from the Step 1 enumerate output (no ~ prefix) in the report header and scope table.
Step 2g: Invariant Synthesis
Using the delta writes, guard predicates, enum/one-shot transitions, and invariant comments extracted in Step 2 (from direct reads in Path A, or subagent output in Path B), systematically walk the following taxonomy to produce invariant candidates. This is a reasoning pass — no new tool calls needed (except the Grep batch in step 2 Pass B — see below).
Terminology: A guard is a per-call precondition enforced at a single callsite (e.g., require(amount >= MIN)). It is NOT a falsifiable invariant — the code itself guarantees it at that callsite. An invariant is a property that must hold globally across any sequence of calls (e.g., "every active position ≥ MIN"). Guards feed §1 of invariants.md (Enforced Guards reference) only. Invariants that are lifted from guards (see step 2 below) or stated in NatSpec feed §2 / §3 / §4.
NatSpec routing (run before the structural walk): For each NatSpec @invariant tag or inline comment asserting a global property (e.g., "totalSupply always equals Σ balances", "fee never exceeds MAX_BP", "only one active epoch at a time"), route DIRECTLY to §2 (or §3/§4 if the property spans contracts or derives from multiple primitives) by category shape (Conservation / Bound / Ratio / StateMachine / Temporal). Source tag: NatSpec: Contract.sol:LN. Do NOT place developer-stated global invariants in §1 — §1 is per-call guard predicates only. After routing, still run the structural scans below — they may confirm (On-chain=Yes) or contradict (On-chain=No) the NatSpec claim.
Walk order (each step uses the raw extraction data, not prior-step conclusions):
-
Conservation scan: For each function, find delta-write pairs where
Δ(A) = +exprandΔ(B) = -expr(orΔ(B) = +exprfor a mapping counterpart) in the same function body. Each matched pair is a conservation candidate:A + B = constorA == Σ B[key].- For mapping writes (
mapping[key] += epaired withscalar += e), inferscalar == Σ mapping[key]. Verify the pattern holds across ALL functions that write to either variable — if ANY function writes to one without the other, note the gap as "partial conservation" and split into Yes/No rows. - For transfer patterns (
mapping[from] -= e,mapping[to] += ewith no scalar change), confirm the mapping sum is self-conserving. - Negative conservation (important): If a function that ought to track a flow (e.g., flashloan pull/push, receive/forward) has zero storage Δ, record this as a Conservation-negative finding. Absence of Δ is itself an invariant observation.
- For mapping writes (
-
Guard extraction and lift (two passes over each
require/assert/if-revert):Pass A — Extract verbatim (Enforced Guards reference): Every
require/assert/if-revertbecomes aG-Nrow in §1 ofinvariants.md. Quote the predicate verbatim with source location. This is a mechanical dump of per-call preconditions — not falsifiable, not fuzzed. Skip guards that only reference function parameters with no storage tie-back AND have no global implication (pure local input validation with no audit value beyond Pass B).Pass B — Lift to global property, then check all write sites: For each guard, ask: "does this imply a property that must hold across any sequence of calls, not just at this callsite?"
- If NO (the guard only constrains a transient parameter that is consumed by the function and does not tie to persistent storage) → leave in §1 only. Do not promote.
- If YES (the guard implies a persistent property — e.g.,
require(amount >= MIN)at deposit implies "every active position ≥ MIN";require(_fee <= 10)at setter implies "fee ∈ [0, 10]") → rewrite the guard as a global property, then locate ALL write sites of the constrained storage variable using Grep on the variable name across scope files. Batch ALL write-site Greps for all lifted guards into a SINGLE parallel message — do not verify one at a time:- If ALL write sites enforce an equivalent guard → promote to §2 as a Bound invariant with On-chain=Yes. Derivation: cite the guard + confirm all write sites.
- If ANY write site writes the variable without an equivalent guard → promote to §2 as a Bound invariant with On-chain=No, and cite the unguarded write site(s) as the gap. This is the high-signal output — the gap is simultaneously an invariant and a potential bug.
Include setter-level bounds where a setter writes to a storage variable constrained by its own parameter check. Run the same all-write-site check — if multiple setters write the same variable but only some enforce the bound, the property is On-chain=No.
-
Ratio scan: For each storage write of form
A = B * C / Dwhere B, C, D are storage variables or function-scoped snapshots of storage, record the ratio. Note whether the snapshot is taken before or after other state changes in the same function (ordering matters — e.g.,totalSupplysnapshotted before_burnvs after). -
State machine / one-shot scan: For each enum/uint/address variable in
require(var == X); ... var = Ypatterns, record the transition. Distinguish:- One-shot latch:
require(var == default); var = concretewith no path back (e.g.,setStrategy,setLeverager). - Togglable flag:
require(var == false); var = truebut another function flips it back (e.g.,freeze/unFreeze,toggleVaultLeverage). NOT a state machine invariant — skip. - Cyclic state:
false → true → falsedriven by timing/condition (e.g.,ongoingVestingPosition). Record as a cycle invariant.
- One-shot latch:
-
Temporal scan: For each
block.timestamporblock.numbercomparison involving a storage variable (deadline, lastUpdate, lockPeriod, interval), extract the temporal constraint. Note whether the constraint is checked-then-updated (safe) or updated-then-checked (potential stale read). -
Cross-contract scan: For each external call where the return value is used in arithmetic or a storage write, record what the caller assumes. Then find the callee's write sites for that state. If the callee can change it independently (via another function), the assumption is unvalidated — record as a cross-contract invariant with On-chain=No. ONLY include rows where BOTH sides (caller assumption + callee write sites) are inside the scope files. Do not speculate about out-of-scope contracts.
- Also include: setter-vs-invariant mismatches — where an admin setter writes a storage value without checking that existing invariants still hold (e.g.,
setReserveCapacitywithout checking against current liquidity). These are cross-contract in the sense that the setter is one contract/function and the invariant is enforced elsewhere.
- Also include: setter-vs-invariant mismatches — where an admin setter writes a storage value without checking that existing invariants still hold (e.g.,
-
Economic derivation: After steps 1-6, check if any combination of single-contract + cross-contract invariants implies a higher-order property. Each economic invariant must cite the specific I-N / X-N IDs it derives from. If the derivation chain has a gap (one of the source invariants is On-chain=No), the economic invariant is also On-chain=No.
Verification gate (MANDATORY before including any inferred invariant):
- Conservation: confirm the Δ-pair exists at the cited lines (same function body).
- Guard (Pass A, §1 row): confirm the require/assert/if-revert is verbatim from code.
- Guard lift (Pass B, §2 row): confirm the lifted global property references persistent storage (not just a transient parameter restatement). Confirm all write sites of the constrained variable have been enumerated via Grep, and the On-chain=Yes/No verdict matches the enumeration — if any write site lacks the guard and the row says On-chain=Yes, the row is invalid.
- NatSpec: confirm the
@invarianttag or comment exists verbatim at the cited location AND asserts a global property (not a per-call note). If it's a per-call note, drop — do not route to §2. - Ratio: confirm the formula is exact and the snapshot ordering (before/after other writes in the same function) is noted.
- StateMachine: confirm both sides of the edge exist AND confirm no reverse path. If there IS a reverse path, it's a togglable flag — drop.
- Temporal: confirm the comparison involves a storage variable, not just block.timestamp vs parameters.
- Cross-contract: confirm both caller usage AND callee write site exist in scope.
- Economic: confirm all referenced I-N / X-N IDs are themselves verified.
- If you cannot verify → drop the row. "Could not verify" is not a valid row.
Output: Invariant candidates feed directly into invariants.md (Step 3a). x-ray.md Section 3 gets Enforced Guards (Reference) + top 3-5 inferred (prioritize On-chain=No from Conservation, Cross-Contract, and lifted-guard gaps; include one high-signal Yes row for structural coverage like a ratio or state-machine latch).
Step 3: Write Output
Test existence vs. coverage execution (CRITICAL)
Test presence is determined by Step 1 enumeration (test_files, test_functions, stateless_fuzz, foundry_invariant, echidna, medusa, hardhat_fuzz, fork, certora, halmos, hevm counts). These are file-scan results and are ALWAYS reliable regardless of whether the toolchain can compile or run. Multi-signal categories (echidna, medusa, certora, halmos) output as functions:configs — e.g., 5:1 means 5 test functions + 1 config file detected.
Coverage metrics (line/branch %) come from forge coverage or hardhat coverage which require installed dependencies, successful compilation, and passing tests. Coverage can fail for many reasons unrelated to test quality:
- Dependencies not installed (
npm install/forge installnot run) - Compiler errors (stack-too-deep, version mismatch)
- Test execution failures (missing RPC, fork config)
Rules:
- Use
test_files/test_functionsfrom Step 1 enumeration for ALL test existence claims. Never infer "no tests" from coverage tool failure. - If coverage fails but enumeration shows tests exist, report:
"[N] test files with [M] test functions detected; coverage metrics unavailable — [failure reason]". - In "Gaps" subsection, only flag missing test categories (stateless_fuzz=0, foundry_invariant=0, echidna=0, medusa=0, certora=0, halmos=0, hevm=0, fork=0), never flag "no tests" when enumeration shows they exist. Prioritize gaps by audit impact: missing stateful fuzz and formal verification for math-heavy/financial logic is higher priority than missing fork tests.
- In git history "Security Observations", never claim "commits without tests" based on coverage failure. The
test_co_change_ratefrom git analysis measures file co-modification in commits, not coverage — qualify it as such. - If coverage fails, do NOT let the failure cascade into threat model or risk assessments. Test presence (from enumeration) and coverage metrics (from tooling) are independent signals.
Check forge coverage status: include results if done, failure reason if failed, "pending" if still running. Do NOT wait.
3a. Write ALL output files (4 parallel Write calls in ONE message)
All output files go into the x-ray/ directory. Write ALL FOUR files in a SINGLE message so they are created concurrently:
1. x-ray/architecture.json — Follow format and rules in the architecture guide section of references/templates.md (already loaded in Step 1).
2. x-ray/x-ray.md — Follow template in the output template section of references/templates.md (already loaded in Step 1). Under 500 lines. No fabrication. Section 3 (Invariants) is a POINTER ONLY to invariants.md — do NOT include a Guards table, do NOT list top inferred invariants. One blockquote callout with counts (guards / single-contract / cross-contract / economic) and a strong link to the invariants.md file is the entire §3. The invariant catalog lives exclusively in invariants.md; duplicating it in x-ray.md was old V2 behavior and is no longer correct.
Key Attack Surfaces cross-link requirement: When writing Section 2 Key Attack Surfaces, cross-reference each surface against the invariants.md blocks you just produced. If the surface's cited file:line falls within the Location / Derivation / Caller side / Callee side window of any G-N / I-N / X-N / E-N block, append the matching IDs as bracketed markdown links immediately after the surface title using LOWERCASE slug fragments: - **Surface name** [[X-4](invariants.md#x-4), [I-17](invariants.md#i-17)] — .... Separate each surface bullet with a blank line. Surfaces that are purely access-control or upgrade-ability concerns may be left unlinked — that is a healthy signal, not a gap. Typical hit rate on non-trivial protocols: ≥70% of surfaces link to at least one invariant.
3. x-ray/entry-points.md — Using the full entry point data collected in Step 2b and the flow paths from Step 2b-flow, follow the entry points template section of references/templates.md (already loaded in Step 1). Start with the Protocol Flow Paths section (arrow chains showing prerequisite sequences for each major entry point), then the access-level detail sections. Factual only — no threat analysis (that stays in x-ray.md). If the protocol has >30 entry points, use compact tables for role-gated and admin sections instead of per-function detail blocks. Only permissionless entry points get the full detail block treatment regardless of count.
4. x-ray/invariants.md — Follow the invariant map template section of references/templates.md (already loaded in Step 1). Four sections: Enforced Guards (Reference), Inferred (Single-Contract), Inferred (Cross-Contract), Economic. Use #### G-N / #### I-N / #### X-N / #### E-N heading blocks — NOT tables. Heading anchors (slug #g-1, #i-17, …) are the target of cross-file markdown links from x-ray.md attack surfaces; inline <a id> anchors inside table cells do NOT work cross-file in VS Code. Each G-N block must include a Purpose line explaining what the guard protects (not just what it checks). Every inferred block MUST cite a concrete Δ-pair, guard-lift + write-sites, edge, temporal predicate, or NatSpec claim — drop blocks that cannot. Every cross-contract block must cite BOTH caller-side assumption AND callee-side write sites (both must be inside the scope files). Every economic block must derive from specific I-N / X-N IDs. No cap on block count. Factual only — no threat analysis.
Writing Section 2 (Threat & Trust Model) — Follow the structure in the output template. Use references/threats.md for threat profiles, temporal threats, and composability threats content (all in one file, already loaded in Step 1). For hybrids, merge: primary adversary list first, then unique secondary threats (de-duplicate overlapping ones).
Verification rules (apply during Section 2 writing):
- Permissionless entry points: Use only the grep-verified list from Step 2b. The Step 2b procedure is the source of truth — do not rely on subagent summaries.
- Security claims: Before writing any claim that a security check is missing, incomplete, or bypassable, you MUST trace the actual data flow by reading the relevant code. Specifically: (1) identify all write sites for the variable under question (use Grep), (2) confirm your claim holds against those write sites. Subagent summaries are not sufficient. If you cannot verify, qualify the claim with "could not confirm" rather than stating it as fact.
Section 7 (Git History): Integrate x-ray/git-security-analysis.json into: Contributors, Review Signals, Hotspots, Security-Relevant Commits (score >= 5), Dangerous Area Evolution, Forked Dependencies, Tech Debt, Cross-Reference Synthesis (2-4 bullets connecting git signals to Sections 2-3).
Branch scoping (CRITICAL)
The git analysis is scoped to the current branch only (HEAD). The git_branch field in the JSON meta tells you which branch was analyzed. All git signals (fix candidates, hotspots, dangerous areas, late changes) reflect ONLY commits reachable from HEAD — not other branches.
Rules:
- State the analyzed branch in the report header or git history section: "Analyzed branch:
[branch]at[commit]". - When describing fix commits or code changes from git history, always describe them as what the current branch code does — not what a fix "changed" if you cannot see the before/after on this branch.
- Never describe code state from other branches. The source files you read in Step 2 are the current branch's files — git history describes how those files evolved on this branch only.
- If the repo shape is
squashed_import(1 commit), there is no meaningful evolution to describe — state this and skip fix/hotspot analysis.
3b. Generate & Validate Architecture SVG
python3 $SKILL_DIR/scripts/generate_svg.py x-ray/architecture.json x-ray/architecture.svg
Then follow the rendering, audit checklist, and fix loop in the architecture guide section of references/templates.md. Max 3 iterations. Cleanup temp files after (including x-ray/git-security-analysis.json).
3c. Terminal Verdict
After all files are written and cleanup is done, read the ## X-Ray Verdict section from the generated x-ray/x-ray.md and print it verbatim to the terminal. Do NOT paraphrase, summarize, or rewrite — copy the exact tier, justification, and key observations as they appear in the file.
Constraints
- Under 500 lines. Protect threat model, invariants, test gaps, git analysis, verdict — compress other sections if needed.
- No fabrication. Say "could not determine" when uncertain.
- Steps 1-3 fully autonomous. No user interaction required.
- Always group contracts by subsystem in scope table.
- Single pass. No partial outputs.
- Never reference audit platforms, contest rules, or bounty program framing — keep the report vendor-neutral.
- If git security analysis script fails, fall back to bash-only git stats. Never block on a missing script.
Before doing anything else, print this exactly:
██████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
██╔══██╗██╔══██╗██╔════╝██║ ██║██╔═══██╗██║ ██║ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
██████╔╝███████║███████╗███████║██║ ██║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║ ██║╚██╗ ██╔╝ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
██║ ██║ ██║███████║██║ ██║╚██████╔╝ ╚████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝


