Mutanchor is a testing tool for Solana programs built with Anchor. It makes small, deliberate changes to a program's source code, one at a time, then runs the program's test suite against each changed version. If the tests catch the change, it was a good test. If they don't, that change is a bug your tests would miss in production — and the report shows you exactly where it is and what an attacker could do with it.
It is a CLI, not an AI tool and not a skill: every change comes from a fixed rule set, so results are deterministic and reproducible. AI is used to build the tool, never to judge the results.
cargo-mutants has zero Solana support. No Solana mutation tool ships. Mutanchor is that gap.
Docs ↗ · Source ↗ · Architecture ↓ · Run it locally ↓
Mutanchor is a local CLI. The site at mutanchor.vercel.app is documentation only — nothing is uploaded, nothing is executed on the site. Every report is generated on your machine by running
mutanchor runlocally.
Built for the Solana ecosystem. MIT licensed.
- See it in one command
- The problem Mutanchor solves
- How Mutanchor works
- Architecture
- Operators
- How it uses Solana
- Engineering decisions & the hard problems
- What's real vs pending — the honesty table
- Honest limitations
- Tests
- Performance
- Run it locally
- Deploy
- Project layout
- Tech stack
- Roadmap
- License
$ cargo run -- --help
Mutation testing for Solana Anchor programs
Usage: mutanchor <COMMAND>
Commands:
init Parse the Anchor IDL and locate instruction source files
run Mutate, build each mutant, and run the suite on LiteSVM
report Emit the mutation report
ci CI mode: fail when surviving mutants exceed a threshold
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print versionThat output is real — build the crate and run it. init finds what to change,
run does the changing and testing, report produces the scorecard, ci
fails the build when too many bugs slip through.
- Line coverage lies. Coverage counts which lines ran, not whether the checks are real. A suite can touch every line and still miss every bug that matters.
- Anchor bugs are repetitive. Solana audits keep finding the same classes: missing signer checks, wrong PDA bumps, dropped authority checks. If your tests do not catch one, an attacker can use it.
- Nothing measures the gap. Existing tools generate tests for you, but nothing checks whether those tests would actually fail when the program is broken.
Mutanchor measures the gap directly: kill a mutant, your tests proved something. Let it survive, and the report shows the exact line an attacker could stand on.
- Locate. The tool reads the Anchor program and maps every instruction to its source file.
- Mutate. It makes one small, deliberate change to the source code — the kind of mistake Solana audits keep finding. One change per version, never two at once.
- Build. Each changed version is compiled with Solana's official build toolchain. One that fails to compile is recorded as build-failed, which is itself useful information.
- Execute. The program's own test suite runs against each changed version on LiteSVM, a fast in-memory Solana runtime — no network, no waiting.
- Classify. Killed (a test failed), survived (all tests passed), or timed out.
- Report. A mutation score per instruction and overall, plus a list of every surviving change with its file and line and what an attacker could do with it.
┌─────────────┐ ┌──────────────────────────────────────────┐ ┌──────────────┐
│ Anchor │──▶│ mutanchor (Rust CLI) │──▶│ LiteSVM │
│ program │ │ │ │ runtime │
│ (source + │ │ ┌──────────────┐ ┌──────────────┐ │ │ │
│ tests) │ │ │ source │──▶│ build │──change─▶│ runs the │
│ │ │ │ rewriter │ │ (Solana │ .so │ │ test suite │
│ │ │ │ (one change │ │ toolchain) │ │ │ per version │
│ │ │ │ per mutant) │ │ │ │ └──────┬───────┘
│ │ │ └──────────────┘ └──────────────┘ │ │
│ │ │ ┌──────────────┐ ┌──────────────┐ │ ▼
│ │ │ │ operator │ │ report │◀─────│ mutation score
│ │ │ │ table │ │ scorecard │ │
│ │ └──┴──────────────┴───┴──────────────┘ └──────────────
└─────────────┘
| Step | How | What comes out |
|---|---|---|
| Read the program | mutanchor init |
instruction → source file map |
| Make one change | source rewriter | one changed version per fault |
| Build the change | Solana build toolchain | a runnable program per version |
| Run the tests | LiteSVM | killed / survived / build-failed / timeout |
| Score | report | mutation score per instruction + overall |
Each operator models a bug class from the Solana audit-findings corpus:
| Operator | Bug class it models |
|---|---|
| signer check removal | missing signer validation |
| PDA seed swap | wrong seeds, wrong account resolution |
| bump mismatch | using the wrong PDA bump |
| authority check drop | missing owner/authority validation |
| discriminator check removal | accepting wrong instruction discriminators |
| CPI target swap | calling the wrong program |
| close/rent check drop | accounts closed or rent-exempt not validated |
| comparison flip | boundary errors (off-by-one, wrong operator) |
| unchecked math | silent arithmetic overflow (checked_add/sub/mul dropped) |
| realloc check drop | unchecked account resize / re-init |
| close receiver swap | close-to-wrong-receiver (lamports leak) |
| reinit zero guard drop | re-initialization with stale bytes |
Builds. Every changed version is compiled with Solana's official build toolchain, the same one real Anchor programs use.
Executes. Versions run on LiteSVM, the in-memory Solana runtime used by the Anchor test harness — instruction-level tests, no validator, no cluster, no wait.
Reads. The Anchor program's own interface file drives the tool: every instruction is mapped to its source file, so changes land exactly where the audit corpus says bugs live.
-
A CLI, not an AI tool. Mutanchor is a standalone command-line tool, not a skill and not an agent. The changes come from a fixed set of rewriting rules — same input, same mutants, every time. AI tools speed up building the tool itself; they never decide whether a change was caught.
-
One fault per mutant. Two changes at once make results impossible to read. Every mutant carries exactly one injected bug, so every survivor is attributable to one rule at one file:line.
-
Meaningless mutants are dropped. A change that cannot affect behavior would drag the score down without meaning, so it is detected and removed.
-
Build-failed is a signal, not a crash. A mutant that does not compile is recorded and the run continues. A mutant that hangs hits a timeout — the run never waits forever.
-
Fast execution is the whole point. LiteSVM runs everything in one process, which is what makes this kind of testing practical for Solana at all.
-
No secrets, no hardcoded paths. The repo carries no keys and no machine paths. Configuration arrives with the runner: environment variables with defaults only.
| Capability | Status |
|---|---|
| CLI scaffold — init / run / report / ci | Real — builds, runs, output above |
Fresh-clone build — cargo check on clean clone |
Real — verified |
| Docs site — install / quickstart / operators / CLI / report | Real — pure documentation at mutanchor.vercel.app. No hosted execution — Mutanchor runs locally. |
| Source rewriting — one change per mutant | Real — engine implemented, unit-tested |
| Operators — 12 implemented | Real — all 12 implemented, unit-tested on fixture programs (signer, PDA seed, bump, authority, discriminator, CPI, close/rent, comparison flip, unchecked math, realloc check, close-receiver swap, reinit-zero-guard drop) |
| LiteSVM runner — build pipeline + classification | Real — implemented; run against the demo program (see below) |
| Report — scorecard per instruction | Real — HTML + JSON + dashboard.json generated by the CLI |
| Demo program + reproducible report | Real — demo/demo-vault: 72.2% mutation score (9 killed, 5 survived, 4 build-failed, 0 timeout, 18 mutants). Reproduce locally with mutanchor run demo/demo-vault; the same run is re-executed by CI in run 33082543614. The 5 survivors are 3× authority_check_drop (L32/L45/L73) + 2× unchecked_math (L34/L47) — the unchecked_math operator immediately surfaced 2 real test gaps in the demo. |
| Second in-repo program | Real — demo/demo-registry (name-registry, 4 instructions, 10-test LiteSVM suite): 80.0% mutation score (8 killed, 3 survived, 4 build-failed, 0 timeout, 15 mutants). Reproduce with mutanchor run demo/demo-registry. Different bug surface from demo-vault: transfer of ownership, checked_add pointer bound, unchecked-math survivor found. |
| Third-party program evidence | Real — the engine also runs on the unmodified favorites program from solana-developers/program-examples (Anchor 1.0, mirrored at demo/favorites/ with ORIGIN.md). Building it surfaced and fixed two real engine bugs; see demo/favorites/ORIGIN.md. |
| CI — build, test, clippy, fmt, cargo audit | Real — GitHub Actions workflow in this repo |
Mutanchor is a developer tool, not an audit. Read this before treating a score as a safety claim.
- A high mutation score is not a security audit. It measures whether your test suite would catch a fixed catalog of small, syntactic bugs. Real audits cover economic design, protocol composition, and multi-transaction attacks that no single-line mutation can express.
- The operator set is finite and rule-based. Eight operators cover the most common Anchor bug classes (signer / PDA / bump / authority / discriminator / CPI / close-rent / comparison flip). Bug classes outside this catalog — reentrancy across CPIs, sysvar drift, oracle staleness, arithmetic overflow patterns — are not measured today.
- Equivalent mutants can inflate "survivors". Some source changes are behaviorally identical to the original and can never be killed by any test. Mutanchor drops obvious duplicates but does not claim to eliminate all equivalents; treat surviving mutants as leads, not verdicts.
- Reproducibility depends on the Solana toolchain, not just Rust. The
authoritative run of the demo uses a machine with a modern Rust; the Solana
release channel bundles Cargo 1.79, which cannot compile current
anchor-langdependencies (edition2024). The CI demo-job runs best-effort — see.github/workflows/ci.yml. - Demo coverage. The published 72.2% score is on
demo/demo-vaultalone; the in-repo second programdemo/demo-registry(4 instructions, 10-test LiteSVM suite) scores 80.0% on the same pipeline. Numbers on other programs will differ; treat these scores as a working proof of the pipeline, not as a benchmark of arbitrary Anchor code. - AI is used to build the tool, never to judge results. Every mutation
comes from a deterministic rule, and every verdict comes from a real
cargo build-sbf+ LiteSVM run. There is no model in the analysis path.
The mutation engine ships with a unit test per operator, each running against a
small fixture program (demo/fixture), plus property-style checks that every
mutant is exactly one fault and that generic/lifetime syntax is never mutated:
cargo testCurrently 37 tests are green (12 engine + 9 CLI + 6 report + 10 demo-vault LiteSVM). Coverage:
- Per-operator unit tests on
demo/fixture. - CLI:
--helpon every subcommand, missing-program-dir failure paths on all four subcommands,--version, dry-run listing. - Report contract: publishable JSON shape (matches the frontend types),
self-contained HTML, empty-state, all-timeout-scores-zero, verdict
wire-format pin, and an aggregate-invariant sweep that asserts
killed + survived + build_failed + timed_out == mutants_totalandscore() ∈ [0, 1]across synthetic reports. - Demo program: 10 LiteSVM tests (deposit/withdraw/pay + revert paths + boundary + close/lamport transfer).
Nothing in this README is sample data — every number quoted is the output
of a real mutanchor run on the in-repo demo programs, reproducible by
cloning and running the tool locally.
The following numbers are the honest wall-clock output of a real
mutanchor run demo/demo-vault on a 2-core / 8 GB laptop, captured
2026-08-27. They are the reference budget; a faster host will beat them
linearly.
| Metric | Value |
|---|---|
| Program | demo/demo-vault (5 instructions, ~140 LoC) |
| Mutants generated | 18 (0 equivalent/duplicate dropped) |
| Total wall time | ~14 min (measured 812 s at 16 mutants; per-mutant cost is stable) |
| Warm-up (pristine build + test, one-off) | ~11.5 min |
| Per-mutant, steady-state (incremental cache hot) | ~11 s (build ~4.5 s + test ~6.5 s; build-fails exit in ~2 s) |
| Peak memory | ~1.6 GB (single scratch tree) |
The tool builds one scratch tree and reuses it across every mutant, so
cargo build-sbf only recompiles the mutated crate on each iteration.
That is the whole trick behind these numbers — a naive
"rebuild-everything-per-mutant" implementation would be 40–60× slower
here and unusable on this class of hardware.
What to expect on a larger program. Because per-mutant cost is dominated by rebuilding the mutated crate (not the full workspace), runtime scales roughly linearly with mutant count once the cache is warm. On the same 2-core laptop:
| Program size (rough) | Mutants (est.) | Warm-up | Per-mutant | Total (est.) |
|---|---|---|---|---|
Small (5 instructions, demo-vault) |
16 | 11.5 min | 11 s | 13.5 min (measured) |
| Medium (15 instructions) | ~60 | 12–15 min | 12 s | ~25 min |
| Large (30 instructions) | ~150 | 15–20 min | 12 s | ~50 min |
On a modern 8-core desktop, warm-up and per-mutant both drop 3–4× — a
150-mutant run finishes in ~15 min. Parallel scratch trees are
shipped: pass --jobs N (or -j N) to fan mutants out across N worker
threads, each with its own warm build tree copied from the primed one.
Memory scales linearly with --jobs (≈ 1.5–2 GB per worker), so 2
workers on 8 GB is the sweet spot; a 16 GB / 8-core host runs at
--jobs 4.
Honest caveat. These extrapolations are model-based, not benchmark-measured on ≥3 real programs yet — that work is scoped for the next milestone.
Any Anchor repo can run Mutanchor as a one-line CI gate:
- name: Run Mutanchor
uses: subheeksh5599/mutanchor@v0.1.0
with:
program-dir: programs/my_program
max-survivors: 0The action installs the modern Agave toolchain, installs the pinned
mutanchor CLI, runs it against your program, uploads the report as an
artifact, and fails the build if surviving mutants exceed
max-survivors. Full example: examples/github-action-usage.yml.
Inputs: program-dir, max-survivors, survivors-only,
skip-operators, timeout, solana-version, mutanchor-ref,
upload-report, artifact-name.
Outputs: mutation-score, killed, survived, build-failed,
timeout-count, report-path.
Prerequisites: Rust stable (2021 edition), Node.js 18+ (docs site only).
git clone https://github.com/subheeksh5599/mutanchor.git
cd mutanchor
# Build the CLI
cargo build --release
# Run it
cargo run -- --help
# (Optional) run the docs site locally
cd frontend && npm install && npm run dev # :5173See .env.example for the full list. Mutanchor's CLI needs no environment
variables — run sets the one it uses itself:
| Variable | Required | Used by | Purpose |
|---|---|---|---|
MUTANCHOR_PROGRAM_SO |
only for cargo test on a program |
mutanchor run (set automatically) + the demo program's LiteSVM test suite |
Path to the compiled program.so the suite loads into LiteSVM. mutanchor run points each mutant's test run at that mutant's freshly built .so. You normally never set this by hand. |
There are no secrets in this project. Nothing is read from the environment that isn't documented here, and the repo carries no keys.
| Docs site | mutanchor.vercel.app — Vercel, single-page docs (install / quickstart / operators / CLI / report). No hosted execution. |
| Source | github.com/subheeksh5599/mutanchor |
Mutanchor runs entirely on your machine. The site is documentation only:
no uploaded reports, no server-side state. To view a real run, execute
mutanchor run locally and open target/mutanchor/report.html in a
browser — it is a fully self-contained scorecard.
# rebuild and redeploy the docs site (Vercel static build of frontend/)
cd frontend && npm ci && npm run build && vercel --prodRollback. Every deploy is a stateless static build of frontend/.
Revert to any prior good commit and rebuild:
git checkout <last-good-commit> -- frontend/
cd frontend && npm ci && npm run build && vercel --prod
# or, via the Vercel dashboard: Deployments → pick a previous build → "Promote to Production"mutanchor/
├── src/
│ ├── main.rs # CLI entry — init / run / report / ci
│ ├── engine.rs # orchestration: scan → mutate → run → report
│ ├── ops.rs # the 8 mutation operators (deterministic rules)
│ ├── init.rs # instruction → source-file mapping
│ ├── runner.rs # build each mutant + execute on LiteSVM
│ ├── report.rs # HTML + JSON report rendering
│ └── model.rs # shared data model (Mutant, Verdict, Report)
├── demo/
│ ├── fixture/ # tiny fixture program for operator unit tests
│ └── demo-vault/ # demo Anchor program + LiteSVM test suite
├── frontend/ # docs site (Vite + React + Tailwind v4, single page)
├── .github/workflows/ # CI: build, test, clippy, fmt, audit, demo-job
├── Cargo.toml
├── LICENSE
└── README.md
- CLI: Rust
- Mutation engine: source rewriting with a fixed rule set — deterministic, no AI in the analysis path
- Execution: LiteSVM (anchor-litesvm) + Solana's official build toolchain
- Report: generated by the CLI — self-contained HTML + JSON, viewable straight from disk
- Docs site: Vite, React, Tailwind v4 — self-hosted fonts, light mode, single page
- Mutation engine: all 12 operators, unit-tested on fixture programs
- LiteSVM runner: build each mutant + run the suite on LiteSVM, k/s/bf/t classification
- Report: per-instruction scores, surviving-mutant annotations with exploit sketches, JSON + HTML + dashboard artifact
- Demo:
demo/demo-vault(real Anchor program, 10-test LiteSVM suite) with a reproducible mutation report, 72.2% score (18 mutants, 9 killed / 5 survived / 4 build-failed / 0 timeout — reproduced locally and by CI). - Second in-repo Anchor program:
demo/demo-registry(name-registry with claim/transfer/set_pointer/reclaim, 10-test LiteSVM suite). 80.0% mutation score (15 mutants, 8k/3s/4bf). - Hosted GitHub Action:
uses: subheeksh5599/mutanchor@v0.2.0— one-line CI gate for any Anchor repo (composite action inaction.yml). - 12 operators:
unchecked_math,realloc_check_drop,close_receiver_swap,reinit_zero_guard_dropadded — the first immediately surfaced 2 real test gaps on demo-vault. - CI/CD: build, test, clippy, fmt, cargo audit; demo-job publishes the report artifact (workflow in
.github/workflows/ci.yml) - Third-party program with full kill-path:
demo/favorites(mirrored fromsolana-developers/program-examples, Anchor 1.0, hand-written Rust LiteSVM test suite viaanchor-litesvm 0.4,no-entrypointfeature to avoid SPL-deps duplicate-symbol linker collision — engine passes--test-features no-entrypoint). - v0.1.0 tagged; release workflow packages +
cargo publish --dry-runverified in CI (workflow:.github/workflows/release.yml) - Parallel scratch trees:
--jobs Nfans mutants out across N worker threads (each with its own warm build tree). Serial semantics preserved when N=1. - crates.io publish: crates.io/crates/mutanchor — install with
cargo install mutanchor.
MIT — see LICENSE.