solaudit 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,8 @@ solaudit [path] [options]
44
44
  | `--json` | Print raw scan results as JSON |
45
45
  | `--sarif` | Print results as SARIF 2.1.0 (for GitHub Code Scanning) |
46
46
  | `--min-severity <level>` | Only report findings at or above `CRITICAL`\|`HIGH`\|`MEDIUM`\|`LOW`\|`INFO` |
47
- | `--experimental` | Also run opt-in experimental rules (currently `SOL-012` Duplicate Mutable Accounts); higher signal, more false positives |
47
+ | `--experimental` | Also run opt-in experimental rules (`SOL-012` Duplicate Mutable Accounts, `SOL-013` Token Account Not Bound to Mint); higher signal, more false positives |
48
+ | `--include-tests` | Also scan test/fuzz/fixture/example code (skipped by default; findings there are rarely actionable) |
48
49
  | `--help` | Show usage |
49
50
 
50
51
  ### Example
@@ -54,7 +55,7 @@ $ solaudit ./examples
54
55
  ```
55
56
 
56
57
  ```
57
- SolAudit v0.3.0 — static analysis for Anchor/Solana programs
58
+ SolAudit v0.3.2 — static analysis for Anchor/Solana programs
58
59
  Scanned 1 file(s) at 2026-08-27T06:00:00.000Z
59
60
  ──────────────────────────────────────────────────────────────────────
60
61
 
@@ -95,15 +96,15 @@ bun run dev
95
96
 
96
97
  | ID | Severity | Name | What it catches |
97
98
  |---|---|---|---|
98
- | `SOL-001` | CRITICAL | Missing Signer Check | `transfer`/`mint_to` calls made without a nearby `Signer<'info>` type or `.is_signer` check |
99
- | `SOL-002` | HIGH | Unchecked Account Deserialization | `Account::try_from` / `AccountLoader::try_from` used without a validating `#[account(...)]` constraint |
100
- | `SOL-003` | HIGH | Arbitrary CPI | `invoke`/`invoke_signed` targeting a program ID sourced from a caller-supplied account instead of a verified constant |
101
- | `SOL-004` | HIGH | Missing Bump Seed Canonicalization | `create_program_address` used directly, or `find_program_address`'s bump discarded instead of stored/validated |
102
- | `SOL-005` | MEDIUM | Integer Overflow Risk | Raw `+`/`-`/`*` arithmetic on balance-like fields instead of `checked_*`/`saturating_*` |
99
+ | `SOL-001` | CRITICAL | Missing Signer Check | `transfer`/`mint_to` calls with no signer evidence anywhere: no nearby `is_signer` check, no `Signer<'info>` field on the governing Accounts struct, and no PDA signing (`invoke_signed` / `new_with_signer`). Also flags PDA-signed transfers whose *destination* is a caller-supplied unchecked account with no `Signer` governing the instruction (the classic vault drain) |
100
+ | `SOL-002` | HIGH | Unchecked Account Deserialization | `Account::try_from` / `AccountLoader::try_from` on an account with no validating `#[account(...)]` constraint (constraint-aware: fields carrying `owner`/`constraint`/`has_one` bindings are recognized as validated) |
101
+ | `SOL-003` | HIGH | Arbitrary CPI | `invoke`/`invoke_signed` whose program ID comes from a raw (unchecked) program account (`program_id: ctx.accounts.*` or a raw `*_program` field); typed `Program<'info, T>` targets are recognized as safe |
102
+ | `SOL-004` | HIGH | Missing Bump Seed Canonicalization / Unverified PDA | `create_program_address` called directly with a bump that didn't come from a nearby `find_program_address` (which is canonical by construction); also raw, mutable vault/treasury/pool-style accounts used in value movement with no `seeds`/`address` binding |
103
+ | `SOL-005` | MEDIUM | Integer Overflow Risk | Raw `+`/`-`/`*` arithmetic *written into account state* (field/compound assignment) on balance-like values instead of `checked_*`/`saturating_*`; also variable-exponent `pow()` in value math (e.g. `2u64.pow(level)` in a fee curve) |
103
104
  | `SOL-006` | HIGH | Type Cosplay / Missing Discriminator Check | Manual `try_from_slice` deserialization with no discriminator validation or `Account<'info, T>` wrapper |
104
105
  | `SOL-007` | MEDIUM | Insecure Account Initialization | `#[account(init, ...)]` constraints missing `payer` and/or `space` |
105
- | `SOL-008` | LOW | Missing Owner Check | Raw `AccountInfo` fields used without a nearby `.owner` comparison |
106
- | `SOL-009` | HIGH | Insecure Manual Account Closure | An account's lamports manually drained to zero without discriminator zeroing or owner reassignment (unlike Anchor's `close = destination`), enabling closed-account revival |
106
+ | `SOL-008` | LOW | Missing Owner Check | Raw `AccountInfo`/`UncheckedAccount` fields with no constraint binding whose data is trusted (deserialized/read, including through `let x = &ctx.accounts.f` aliases) or whose key is assigned into persistent state, with no genuine `.owner ==` comparison; usages are scoped to the owning struct's handlers |
107
+ | `SOL-009` | HIGH | Insecure Manual Account Closure | An account's lamports manually drained to zero (`lamports.borrow_mut() = 0` or `try_borrow_mut_lamports() = 0`) without discriminator zeroing or owner reassignment (unlike Anchor's `close = destination`), enabling closed-account revival |
107
108
  | `SOL-010` | MEDIUM | Reinitialization Risk via init_if_needed | `#[account(init_if_needed, ...)]` without canonical PDA seeds / `has_one` constraints, letting a pre-existing attacker-crafted account pass as freshly initialized |
108
109
  | `SOL-011` | MEDIUM | Missing Rent-Exemption Check | A partial lamport withdrawal with no nearby `Rent::get()?.minimum_balance(...)` check, risking runtime garbage-collection of the account |
109
110
 
@@ -111,16 +112,37 @@ Each rule is implemented as real regex/line-scanning logic in [`packages/core/sr
111
112
 
112
113
  ## Accuracy
113
114
 
114
- On the committed labeled suite in [`benchmark/`](./benchmark), the deterministic engine scores **94.1% precision, 100% recall, 97.0% F1** on rule-covered categories.
115
+ On the committed labeled suite in [`benchmark/`](./benchmark), the deterministic engine scores **100% precision, 100% recall, 100% F1** on rule-covered categories.
115
116
 
116
- **Duplicate mutable accounts** is covered by the experimental `SOL-012` rule, available via `solaudit --experimental`. A “distinctness” heuristic false-positives on common legitimate patterns (for example a deposit instruction’s user/vault token accounts of the same type with no explicit `!=` constraint), so it stays opt-in to protect the default ruleset's precision. These numbers describe this suite, not general security effectiveness.
117
+ ### Real-world validation (0.3.1)
118
+
119
+ 0.3.0 was scanned against 9 major open-source protocols (marginfi, Drift, Orca Whirlpools, Marinade, Squads, Phoenix, Clockwork, Saber, and Cashio — the real $50M exploit, as a control). The result drove a rewrite of how the engine reads Anchor code: it now parses `#[derive(Accounts)]` structs and understands the constraint layer (`Signer`, `owner`/`has_one`/`constraint`/`seeds`, handler `Context<T>` mapping, `#[cfg(test)]` exclusion), instead of pattern-matching handler bodies alone.
120
+
121
+ - **Default findings across the 9 repos dropped from 1,232 to 158 (−87%)** — marginfi went from 521 findings to 20; Drift from 454 to 77.
122
+ - The remaining findings are *more* actionable, not fewer ideas: suppressions only happen on positive evidence of validation.
123
+ - Under `--experimental`, the new `SOL-013` rule fires on Cashio's actual vulnerable pattern — a token account whose mint is never bound despite a mint account being in scope (the class behind the infinite-mint exploit).
124
+
125
+ Benchmark label integrity note: three samples' labels were revised in 0.3.1 to match SOL-008's sharpened definition (raw accounts whose *data* is trusted or whose key is *assigned into state*); each revision is documented inline in [`benchmark/labels.json`](./benchmark/labels.json).
126
+
127
+ ### Ground-truth validation (0.3.2): SolScape
128
+
129
+ 0.3.1 was measured against **purpose-built ground truth**: [SolScape](https://github.com/blueeyez425-art/solaudit/tree/main/examples/solscape), a complete (intentionally vulnerable) Anchor game program — a persistent on-chain territory map with a marketplace, treasury, and player profiles — seeded with 9 real exploit classes (missing-signer treasury drain, unchecked PDA substitution, overflow price curve, arbitrary CPI, unbound token mint, close-then-revive, raw-byte trust, reinitialization) plus 4 correctly-secured control handlers. The answer key was written **before** the scan.
130
+
131
+ - **0.3.1 caught 2 of 9 seeded bugs** and false-flagged 2 of 4 controls. We published that scorecard, then fixed the engine.
132
+ - **0.3.2 catches 8 of 9 by default, 9 of 9 with `--experimental`** (the unbound-mint class is SOL-013), with **all 4 controls clean**.
133
+ - Fixes: SOL-001 now detects PDA-signed transfers to caller-chosen unchecked recipients when no `Signer` governs the instruction (the classic vault drain); SOL-003 only flags CPIs whose *program* is a raw account (typed `Program<'info, T>` no longer flags); SOL-004 also flags unseeds-verified vault/treasury/pool accounts used in value movement; SOL-005 catches variable-exponent `pow()` in value math; SOL-008 follows `let x = &ctx.accounts.f` aliases, scopes usages to the owning struct's handlers, and no longer mistakes `field.owner = ...` assignments for checks; SOL-009 recognizes `try_borrow_mut_lamports() = 0` manual closes.
134
+ - marginfi re-scan sanity check: 24 findings (was 20) — the main program stays clean; the new signal lands in mock/integration programs.
135
+
136
+ Fixture revisions disclosed: SolScape's `withdraw_treasury` was rewritten to the genuinely exploitable `invoke_signed` shape (v1's plain `invoke` could not actually drain), `close_tile` now leaves data intact (v1 zeroed it, which is the *safe* pattern), and `buy_tile_spl` gained the realistic typed-token + mint shape SOL-013 is designed to scope to. v1 is archived in the fixture directory.
137
+
138
+ **Duplicate mutable accounts** and **token accounts unbound to their mint** (the Cashio class) are covered by the experimental `SOL-012` and `SOL-013` rules, available via `solaudit --experimental`. A “distinctness” heuristic false-positives on common legitimate patterns (for example a deposit instruction’s user/vault token accounts of the same type with no explicit `!=` constraint), so it stays opt-in to protect the default ruleset's precision. These numbers describe this suite, not general security effectiveness.
117
139
 
118
140
  ## Output formats (CLI)
119
141
 
120
142
  - **Human** (default) — colorized terminal report grouped by severity.
121
143
  - **JSON** (`--json`) — the full `ScanResult` object (`scannedFiles`, `findings[]`, `summary`).
122
144
  - **SARIF** (`--sarif`) — [SARIF 2.1.0](https://sarifweb.azurewebsites.net/) output compatible with GitHub Code Scanning.
123
- - **Experimental rules** (`--experimental`) — opt into noisier, higher-signal heuristics (currently `SOL-012` Duplicate Mutable Accounts). Recommended for deep manual reviews rather than CI gates.
145
+ - **Experimental rules** (`--experimental`) — opt into noisier, higher-signal heuristics (`SOL-012` Duplicate Mutable Accounts, `SOL-013` Token Account Not Bound to Mint). Recommended for deep manual reviews rather than CI gates.
124
146
 
125
147
  ## GitHub Actions
126
148