Fizz

SkillDev tools

Lets your agent automatically generate fuzz suite files that stress-test Ethereum smart contracts with Echidna and Medusa.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Fizz skill

About this skill

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".

What this skill tells your AI

The instructions your AI receives, as published by pashov/skills in fizz/SKILL.md and read by ahel’s review.

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 under fizz_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 this SKILL.md.
  • SUITE_DIR: test/fizz relative to PROJECT_ROOT. Pass --suite-dir to suite-generation steps.
  • META_DIR: fizz_data relative to PROJECT_ROOT. Pass --meta-dir to metadata steps.
  • Optional contract arguments narrow handler generation to specific contracts.
  • --no-invariants skips Step 9 only.
  • --max (or --opus, or "max quality") upgrades every subagent in this run from Sonnet to Opus. See "Subagent Model" below.
  • --guided / --automatic selects 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: guided or Mode: automatic — the resolved {MODE}.
  • Subagent model: sonnet (default) or Subagent model: opus (--max) (if --max / --opus / "max quality" / "use opus" / Opus selection was requested).

Step 1: Verify Tooling And Environment

Run sequentially:

  1. Read template-map.md.
  2. Run forge --version.
  3. If forge --version fails, tell the user that Foundry is missing and suggest installing it using the official documentation: Foundry install guide: https://www.getfoundry.sh/introduction/installation
  4. If forge --version fails, stop here. Foundry is required before proceeding with the rest of the workflow.
  5. Run bash {SKILL_PATH}/scripts/ensure_foundry.sh {PROJECT_ROOT}.
  6. If foundry.toml is missing, allow ensure_foundry.sh to create one. If it fails, stop and report the error.
  7. Run medusa --version.
  8. Run echidna --version.
  9. 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.html Echidna install guide: https://secure-contracts.com/program-analysis/echidna/introduction/installation.html
  10. If medusa --version fails, stop here. Medusa is required before proceeding with the rest of the workflow.
  11. If echidna --version fails 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:

  1. Read {PROJECT_ROOT}/foundry.toml.
  2. Run cd {PROJECT_ROOT} && forge build.
  3. 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-ray skill via the Skill tool with args="{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.md was 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-ray
    

    Then re-invoke Skill('x-ray', args="{PROJECT_ROOT}"). If {PROJECT_ROOT}/x-ray/x-ray.md is 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 record SKIPPED: 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 a general-purpose agent with model: "{AGENT_MODEL}". The agent reads the source files, then writes the analysis to {PROJECT_ROOT}/{META_DIR}/protocol-understanding.md so 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.sol integration 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.

  1. Read {PROJECT_ROOT}/foundry.toml and check whether via_ir = true is set under [profile.default] or at the top level.
  2. If via_ir is not enabled, skip this subsection — no fuzz profile is needed.
  3. If via_ir is enabled, run:
bash {SKILL_PATH}/scripts/setup_fuzz_profile.sh {PROJECT_ROOT}

This script:

  • Appends a [profile.fuzz] section to foundry.toml with via_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 = true and optimizer_runs = 0, exits 0, prints FUZZ_PROFILE=ir-no-opt
  • If both fail: exits 1 (use default profile, accept deflated coverage)
  1. Read the script output to determine the fuzz profile mode:

    • FUZZ_PROFILE=no-ir → accurate coverage, use standard targets
    • FUZZ_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%
  2. Record the profile mode in {PROJECT_ROOT}/{META_DIR}/coverage-targets.md at 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"
  3. For all subsequent forge build commands 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

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
1k
Forks
224
Last commit
Sep 2026

ahel review

  • K6low
    bundled executables the agent is told to run
  • K1info
    remote-installer-piped-to-shell (in scripts/ensure_foundry.sh)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
fizz
Source
github.com/pashov/skills