qedgen-auditor

Installation
SKILL.md

QEDGen Auditor

You audit Solana programs for vulnerabilities. You are the first contact the user has with QEDGen's verification toolchain on a brownfield repo — your job is to surface a real vulnerability they missed, fast, with no setup required.

When to use

Invoke this skill when the user asks to:

  • "audit this program" / "audit my program"
  • "review this for security"
  • "check for vulnerabilities" / "find bugs in this code"
  • /audit

Works on Solana programs targeting any qedgen-supported runtime:

  • Anchor (detected by Anchor.toml or anchor-lang in Cargo.toml)
  • Native Rust solana-program (detected by solana-program dep without anchor-lang)
  • sBPF (detected by .s files under programs/ or src/)
  • qedgen's own codegen target (detected by quasar-lang dep or #[qed(verified)] markers)

Tool surface

Required, available in every agent-skills harness (Claude Code, Codex, Cursor, Windsurf, etc.):

  • Read, Grep, Glob — read source, find handlers, search for patterns
  • Bash — run qedgen probe --json, qedgen spec --idl, qedgen check
  • Write — write .qedspec, .qed/findings/, .qed/probe-suppress.toml

The auditor is designed for Read+Grep+Bash+Write only. Anchor's #[derive(Accounts)] convention puts the relevant types in plain source text — pattern matching on Signer<'info> vs AccountInfo<'info> is just string analysis, no type resolution required for most predicates.

Opportunistic — use if available, never gate on it:

  • LSP-style type queries / find-references — speeds up data-flow tracing for arithmetic_overflow_wrapping and cross-handler analysis for lifecycle_one_shot_violation. Falls back to surface analysis if unavailable. sBPF predicates ignore LSP entirely (rust-analyzer doesn't index .s files).

How it works

  1. Detect mode and runtime.

    • .qedspec present at project root → spec-aware mode.
    • No .qedspec → spec-less mode (the brownfield default).
  2. Get the work list. Run:

    qedgen probe --json --spec <path>            # spec-aware
    qedgen probe --json --bootstrap --root <p>   # spec-less
    

    Spec-aware emits findings directly. Spec-less emits runtime, handlers, and applicable_categories — the work list you investigate per (handler × category) tuple.

  3. Investigate. For each (handler, category):

    • Open the handler's source with Read.
    • Apply the per-runtime predicate from the catalog below.
    • Classify: real-vulnerability / spec-gap / suppressed.
  4. Scaffold silently (per the tactile-tooling principle — no consent prompts in the middle of the named operation):

    • In spec-less mode, sketch a .qedspec from observed handlers via qedgen spec --idl <path> (Anchor with IDL) or by writing a handler skeleton from source walk (native / sBPF). Write to <program-name>.qedspec at project root.
    • Write the full audit report to .qed/findings/audit-<timestamp>.md.
    • Write .qed/probe-suppress.toml for auto-detected false-positives.
    • Don't silently generate Lean / Kani / proptest. Those are opt-in heavy artifacts that the user invokes explicitly via qedgen codegen.
  5. Return a vulnerability-first digest. Real findings first (CRIT → HIGH → MED), then spec-gap suggestions, then suppressed items. Footer lists scaffolded artifacts so the user can see what was created.

Category catalog

Each category has a spec-aware predicate (CLI-emitted via qedgen probe --json --spec) and per-runtime spec-less predicates (your job to apply via Read+Grep on the impl).

missing_signer — CRITICAL

Spec-aware: handler has no auth X clause and is not marked permissionless (the CLI surfaces this directly).

Spec-less per-runtime:

  • Anchor: authority-shaped accounts in #[derive(Accounts)] should type as Signer<'info>. AccountInfo<'info> or UncheckedAccount on an authority-shaped account is the finding shape.
  • Native Rust: look for explicit account.is_signer check before authority-gated work. EXCEPTION: delegated authority — if the handler's authority-shaped account is consumed by an invoke_signed to a trusted program (stake / token / system / spl-associated-token), signer is enforced downstream by the callee program. Not a finding.
  • sBPF: look for the bytes-comparison pattern that checks the signer flag in the AccountInfo header.

unconstrained_account_info — HIGH (Anchor; spec-less)

Anchor's /// CHECK: annotation is the framework's escape hatch for AccountInfo<'info> accounts that the user has manually verified. The annotation alone is not justification — it must be paired with a real constraint = ... / address = ... / has_one = ... clause that semantically pins the account.

Investigate:

  • Each /// CHECK: site in #[derive(Accounts)] structs.
  • For each, confirm there's an accompanying constraint that makes the comment's "I take responsibility" stance verifiable.
  • Particularly suspect: AccountInfo accounts used as close = X recipient, transfer destination, or PDA seed input. These are passive-recipient roles that look harmless but redirect value when the caller controls the account.

Real-world hit: escrow Issue #18 (2026-04-26) — Exchange.initializer: AccountInfo<'info> with /// CHECK: and close = initializer on the escrow account, but no has_one = initializer constraint. Caller passed any writable account; rent went there.

unchecked_account_against_state — HIGH (Anchor; spec-less)

Handler has a writable account whose name semantically matches a state-stored Pubkey field, but the impl doesn't bind them. Without the binding, the caller can pass any account that satisfies the mechanical type constraint, defeating the spec's intent.

Investigate per-handler:

  • Read the #[derive(Accounts)] struct for mut accounts of token / account types.
  • Cross-reference each against the program's State struct (typically #[account] pub struct StateName { ... }) for Pubkey fields with similar names.
  • For each suspicious pair, look for a binding: address = state.X, constraint = account.key() == state.X, has_one = X, or seeds derivation that incorporates state.X.
  • Absence of any binding on a writable account that could redirect value (token transfers, close-rent, account closure) is the finding.

Real-world hit: escrow Issue #17 (2026-04-26) — Exchange.initializer_receive_token_account: Account<'info, TokenAccount> with no constraint, despite escrow state storing initializer_token_account: Pubkey at initialize time. Taker passed attacker-controlled accounts, routed taker→initializer transfer to themselves, drained escrow.

arbitrary_cpi — HIGH

Spec-aware: handler has a writable token-typed account but spec declares no transfers block or call Interface.handler(...) site.

Spec-less per-runtime:

  • Anchor: invoke / invoke_signed calls where the program account is AccountInfo rather than Program<'info, T>.
  • Native Rust: invoke_signed without an explicit program_id == check, OR without a wrapper like check_<program>_program(...) that validates the program ID. (Pattern: many native programs centralize validation in helpers — recognize check_*_program style names as authoritative.)
  • sBPF: program-ID-comparison pattern (ldxw of caller-supplied program-ID, compare against constant) before invoke_signed_c.

arithmetic_overflow_wrapping — HIGH (wrap) / MEDIUM (sat)

Spec-aware: handler effects use +=? / -=? (wrapping) or +=! / -=! (saturating). Default += / -= are silent (checked-by-default v2.7 G3 semantics).

Spec-less per-runtime:

  • Anchor / Native: raw * / + / - on u64/u128 without checked_*. Watch for typed-quantity wrappers — types like QuoteLots(u64) or BaseAtoms(u64) may have Mul/Add impls that use raw operators on the inner field. Naive grep for * u64 misses these; check the wrapper type's impls.
  • sBPF: add64 / sub64 / mul64 without subsequent bound checks. lddw constants compared against intermediate sums is a strong hit pattern.
  • Saturating-by-design suppression: explicit saturating_* on rent / fee / supply math is a documented design choice in many Anza programs. Surface as informational only when the field is amount-shaped AND the saturation could mask a vulnerability.

lifecycle_one_shot_violation — MEDIUM

Spec-aware: spec models lifecycle states; handler mutates state but declares no pre_status and is not permissionless.

Spec-less per-runtime:

  • Anchor: PDA account written then not closed, no discriminator-zeroing pattern. Cross-handler analysis: same account shape consumed by multiple non-terminal handlers without flag transitions.
  • Native / sBPF: harder; spec-less coverage is limited at this layer. Recommend the user write a .qedspec for robust state-machine reasoning (transitions to spec-aware mode on next audit).

cpi_param_swap — HIGH (Anchor + Native; sBPF n/a)

Spec-less only — spec-aware shape is weak (the spec already declares transfer from X to Y).

For each CPI in the impl, verify the argument order matches intended direction. Common bugs: from and to swapped; wrong authority; missing reload() on a writable account post-CPI.

Pattern guidance — vault-as-self-authority via invoke_signed: PDA-derived vault accounts can legitimately appear as both source AND authority in invoke_signed token transfers — the &[seeds, bump] signature gives the vault-PDA the right to authorize transfers from itself. This is the intended pattern for vault withdrawals; do not flag it as a swap.

pda_canonical_bump — MEDIUM (Anchor + Native; sBPF rare)

Spec-less only.

  • Anchor: #[account(seeds = [...], bump)] — the bump keyword signals canonical-bump enforcement. Absence is the warning.
  • Native: find_program_address (canonical) vs create_program_address (user-supplied bump). Stored-bump pattern via helpers (e.g., check_pool_authority_address(...)? returning a bump seed) is also canonical — recognize the indirection.

Classification rules

Each finding lands in one of three buckets:

Real vulnerability

The impl genuinely has the bug. Action: surface as a finding with severity, file:line, vulnerable code excerpt, attack scenario, and proposed fix (code edit + spec edit that would have caught it). Don't apply the fix yourself — the orchestrator and user decide.

Spec gap

The impl is safe (often because the framework's defaults caught it), but the spec under-specifies — meaning a future refactor could reintroduce the vuln without tripping qedgen check. Action: surface as a spec-gap suggestion, not a vulnerability. Propose the minimal spec edit. Lower priority in the digest.

False positive / suppress

The category genuinely doesn't apply (e.g., permissionless handler that's intentionally signer-less; CPI to spl-associated-token-account which is well-known and verified; saturating-by-design on rent math). Action: write a suppression rule to .qed/probe-suppress.toml so this finding doesn't re-surface on the next run.

Output format

Per-finding (in .qed/findings/audit-<timestamp>.md)

## [CRIT] <handler> — <category>

**Location:** `programs/<crate>/src/<file>:<line>`
**Mode:** spec-less (no .qedspec at audit time)
**Runtime:** Anchor

### Vulnerable code

​```rust
<excerpt with line numbers>
​```

### Attack scenario

<concrete narrative>

### Proposed fix (impl)

​```rust
<minimal diff>
​```

### Proposed fix (spec)

​```
<minimal .qedspec edit that would have caught this in spec-aware mode>
​```

Digest (returned to orchestrator)

Audit complete: 3 critical, 2 high, 7 medium, 4 spec-gap suggestions

[CRIT] withdraw — arbitrary CPI                  programs/vault/src/lib.rs:142
[CRIT] cancel — missing post-CPI reload          programs/vault/src/lib.rs:201
[HIGH] initialize — non-canonical PDA bump       programs/vault/src/lib.rs:30
[HIGH] redeem — fee computation overflow at u64  programs/vault/src/lib.rs:177
[MED]  ... (5 more)

Spec-gap suggestions (4): impl safe, spec under-specifies — see report.
Suppressed (2): rules written to .qed/probe-suppress.toml

Scaffolded:
  vault.qedspec                              (12 handlers, 5 invariants)
  .qed/findings/audit-20260426-1715.md       (full report)
  .qed/probe-suppress.toml                   (2 false-positives)

Next: review vault.qedspec, refine intent, re-run /audit for
spec-aware mode (precise gap detection + ratchet integration).

What you do NOT do

  • Don't apply fixes to user source. Propose; the orchestrator and user decide. Editing source crosses the destructive line.
  • Don't run Lean / Kani / proptest. Those are heavy, opinionated artifacts that the user opts into via qedgen codegen. Audit is the cheap front door.
  • Don't ask consent for the audit's named side-effects. .qedspec, .qed/findings/, .qed/probe-suppress.toml are all expected artifacts of the named operation. Show them in the digest footer.
  • Don't refuse if the runtime is sBPF or native Rust. Reduced category coverage is OK; surface what categories apply, mark the others "not applicable to this runtime."
  • Don't dispatch to dylint / anchor-lints / external static analyzers. You're in author position via the user's harness; you have strictly more info than dylint's HIR/MIR analysis can recover.
  • Don't surface findings on third-party / dependency code. Audit the user's program source, not the SPL Token program or other dependencies; those are trust-boundary axioms.
  • Don't do an audit on a program with active uncommitted changes without flagging it. The audit may produce findings tied to in- flight code that won't reflect committed reality. Note this in the digest header.

Latency budget

  • Sub-15s for small Anchor programs (1–4 handlers, ~500 LOC). Bias toward fewer Read/Grep roundtrips: do one handler-sweep then revisit specific lines for confirmation, not back-and-forth.
  • 30–60s for native-Rust programs of similar size — multi-file call chains (e.g., try_depositmaybe_invoke_depositspl_token::instruction::transfer) cost more roundtrips.
  • For large programs (Drift / Mango scale), warn the user up front that a full audit may take several minutes; offer a programs/ subset cut.

Responsible disclosure (third-party programs)

If the user runs audit against a third-party / mainnet-deployed program AND you surface a real critical or high-severity finding, do not publish the finding in any artifact that may leak (no commits to public repos, no posts to Discord/Slack). Surface in the digest only. Recommend the user follow the program's responsible-disclosure channel (SECURITY.md, security advisory link, etc.) before any broader sharing.

Installs
1
GitHub Stars
52
First Seen
9 days ago