qedgen-auditor
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.tomloranchor-langin Cargo.toml) - Native Rust solana-program (detected by
solana-programdep withoutanchor-lang) - sBPF (detected by
.sfiles underprograms/orsrc/) - qedgen's own codegen target (detected by
quasar-langdep 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_wrappingand cross-handler analysis forlifecycle_one_shot_violation. Falls back to surface analysis if unavailable. sBPF predicates ignore LSP entirely (rust-analyzer doesn't index.sfiles).
How it works
-
Detect mode and runtime.
.qedspecpresent at project root → spec-aware mode.- No
.qedspec→ spec-less mode (the brownfield default).
-
Get the work list. Run:
qedgen probe --json --spec <path> # spec-aware qedgen probe --json --bootstrap --root <p> # spec-lessSpec-aware emits
findingsdirectly. Spec-less emitsruntime,handlers, andapplicable_categories— the work list you investigate per (handler × category) tuple. -
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.
-
Scaffold silently (per the tactile-tooling principle — no consent prompts in the middle of the named operation):
- In spec-less mode, sketch a
.qedspecfrom observed handlers viaqedgen spec --idl <path>(Anchor with IDL) or by writing a handler skeleton from source walk (native / sBPF). Write to<program-name>.qedspecat project root. - Write the full audit report to
.qed/findings/audit-<timestamp>.md. - Write
.qed/probe-suppress.tomlfor 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.
- In spec-less mode, sketch a
-
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 asSigner<'info>.AccountInfo<'info>orUncheckedAccounton an authority-shaped account is the finding shape. - Native Rust: look for explicit
account.is_signercheck before authority-gated work. EXCEPTION: delegated authority — if the handler's authority-shaped account is consumed by aninvoke_signedto 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:
AccountInfoaccounts used asclose = Xrecipient, 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 formutaccounts 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, orseedsderivation that incorporatesstate.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_signedcalls where the program account isAccountInforather thanProgram<'info, T>. - Native Rust:
invoke_signedwithout an explicitprogram_id ==check, OR without a wrapper likecheck_<program>_program(...)that validates the program ID. (Pattern: many native programs centralize validation in helpers — recognizecheck_*_programstyle names as authoritative.) - sBPF: program-ID-comparison pattern (
ldxwof caller-supplied program-ID, compare against constant) beforeinvoke_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
*/+/-onu64/u128withoutchecked_*. Watch for typed-quantity wrappers — types likeQuoteLots(u64)orBaseAtoms(u64)may haveMul/Addimpls that use raw operators on the inner field. Naive grep for* u64misses these; check the wrapper type's impls. - sBPF:
add64/sub64/mul64without subsequent bound checks.lddwconstants 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
.qedspecfor 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)]— thebumpkeyword signals canonical-bump enforcement. Absence is the warning. - Native:
find_program_address(canonical) vscreate_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.tomlare 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_deposit→maybe_invoke_deposit→spl_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.