Verified Anchor
Formally verified account validation for Solana programs.
Verified Anchor is a drop-in replacement for the Anchor #[derive(Accounts)] macro that gates almost every Solana transaction in production. The macros emit the same Rust validation code stock Anchor would emit, plus a Lean 4 obligation discharged at build time against a contract that defines what "valid accounts" means in the Solana account model. The proven core covers signer / mut / owner / has_one / seeds + bump / discriminator, the typed-wrapper base checks (SystemAccount ownership, Program<P> executable + address), and the init / close lifecycle.
Why
Solana's correctness depends on a chain of trust: validator runtime (Agave, Firedancer), SBF execution, the Anchor macro that generates per-instruction account validation, and the program's business logic. Layers 1, 2, and 4 receive substantial attention. Layer 3 — the Anchor macro — has not. It is hundreds of lines of procedural-macro code that has never been formally verified, and a bug in it is a bug in every program that depends on it.
The cost of leaving that layer unverified is measurable. Four real Solana mainnet exploits — Cashio (March 2022, ~$48M), Crema Finance (July 2022, ~$8.8M), account-type confusion incidents, and PDA seeds misuse — share the same root cause: a check the program thought it was making was either missing, malformed, or trivially bypassable. Each of these is exactly what #[derive(Accounts)] is supposed to prevent.
Verified Anchor closes the gap. Every macro expansion ships with a Lean 4 theorem stating that the generated Rust validator is observably equivalent to a contract written in Lean. The theorem is proved once, parameterised over the user's struct. The user writes the same code they would write in stock Anchor. The four CVE classes above are reproduced in this repository as before/after litesvm tests; the verified versions reject the attacker on chain.
Status
v0.1.2, published on crates.io — one dependency:cargo add verified-anchor.v0.1.0is the tagged submission snapshot.- Lean theorems' axioms:
[propext, Quot.sound]only. Zerosorry/admit. - Out of scope:
realloc,zero, token / mint / associated-token constraints. Customconstraint = ...expressions. QEDGen composition demo.
Packages
| Crate | Description |
|---|---|
verified-anchor |
Runtime traits (Validate, Accounts<'info>), VAError, prelude, Context<T>. |
verified-anchor-macros |
Proc-macros: #[derive(VerifiedAccounts)], #[derive(AccountData)], #[account]. |
cargo-verified-anchor |
Cargo subcommand discharging Lean proof obligations via lake env lean. |
verified-anchor-example |
Worked user crate. |
verified-anchor-exploits |
Empirical exploit suite (Cashio, Crema, type confusion, PDA seeds). |
verified-anchor-program |
BPF program used by litesvm runtime tests. |
Repo structure
lean/ Lean 4 library (lake build)
VerifiedAnchor/Solana/ Solana account model + crypto (opaque sha256, isOnCurve)
VerifiedAnchor/Constraints/ Constraint AST (the Rust↔Lean seam) + Ctx
VerifiedAnchor/Contract/ `validates : AccountsStruct → Ctx → Prop`
VerifiedAnchor/Decision/ `validatesBool` + agreement theorem
VerifiedAnchor/Codegen/ `genValidate` + soundness proofs (Soundness, Lifecycle)
VerifiedAnchor/Examples/ Worked example (Withdraw.lean)
rust/ Cargo workspace
verified-anchor/ Runtime crate (traits, errors, prelude, integration tests)
verified-anchor-macros/ Proc-macro crate
cargo-verified-anchor/ Cargo subcommand
verified-anchor-program/ BPF program — init/close + a seeds PDA (litesvm fixture)
verified-anchor-example/ Worked user crate
verified-anchor-exploits/ Empirical exploit suite (four CVE classes)
docs/ Project documentation
verified-anchor-bridge.md Trust boundary + clause-by-clause Rust↔Lean correspondence
migrating-from-anchor.md Migration guide + supported constraint subset
exploit-case-studies.md The four Solana mainnet incidents, reproduced before/after
announcement-v0.1.0.md v0.1.0 release writeup
publish-checklist.md crates.io release steps
web/index.html Self-contained landing page (deployable on GitHub Pages)
verified_anchor_proposal.md Original proposal
LICENSE CC BY-NC-ND 4.0
Documentation
- Original proposal — problem statement, approach, milestones.
- v0.1.0 announcement post — the full technical writeup.
- Trust boundary — what is proven, what is not, the Rust↔Lean correspondence.
- Migrating from Anchor — supported constraint subset, syntax mapping.
- Exploit case studies — four Solana mainnet incidents reproduced on litesvm.
Quick start
Install from crates.io — no clone required:
Then write the same code you would in stock Anchor:
use *;
declare_id!;
// One line, anywhere in your crate's lib — lets `cargo verified-anchor check` discover the structs.
emit_specs!;
The whole prelude (Pubkey, AccountInfo, ProgramResult, declare_id!, the wrappers, the
derives) comes from the single verified-anchor dependency — no separate solana-program or
borsh. Then discharge the per-struct proof obligation (the first run fetches the pinned Lean
proof library automatically; you only need elan/lake installed):
Deep technical dive
The verification chain
The #[derive(VerifiedAccounts)] macro emits two artefacts from a single source struct:
- The Rust
impl Validate— avalidate(accounts, instr_data, program_id) -> Result<(), VAError>function that runs at transaction time. Per field it walks the declared constraints in order and short-circuits on the first failure. - A Lean
AccountsStructliteral — the same struct, rendered as a value of the Lean type that the proof side consumes.
A Lean function genValidate : AccountsStruct → Ctx → Bool recursively interprets the constraint list. By construction, the Rust validator and genValidate examine the same constraints in the same order and return the same answer on the same input. Equivalence is by construction; the proof side proves equivalence to the contract, not to the Rust.
The contract validates : AccountsStruct → Ctx → Prop is defined declaratively in Lean. It says exactly what each constraint kind means — signer means the slot is a signer, has_one = f means the 32 bytes at offset 8 of the account data equal the key of the named field, seeds = […], bump means the account key is the canonical PDA for those seeds under program_id, and so on.
The headline theorem ties the two together:
theorem genValidate_sound (s : AccountsStruct) (c : Ctx) (h : M4Subset s) :
genValidate s c = true ↔ validates s c
For every struct in the supported subset (called M4Subset in Lean — see the table below), genValidate returns true precisely when the declarative contract holds. The two sides cannot disagree. The lifecycle theorem lifecycle_sound discharges analogous Hoare obligations for init and close.
Per-program proof obligations are discharged by cargo verified-anchor check. For each user struct the cargo tool generates a one-line Lean obligation decide (M4Subset s), then invokes lake env lean. If the obligation fails, the build fails.
Proof scope
| Constraint | Proven by |
|---|---|
signer |
genValidate_sound |
mut |
genValidate_sound |
owner = <expr> |
genValidate_sound |
has_one = <field> |
genValidate_sound (relational) |
seeds = [...], bump |
genValidate_sound (canonical-only PDA) |
discriminator = "..." |
genValidate_sound |
SystemAccount base: owner |
genValidate_sound |
Program<P> base: executable + address |
genValidate_sound |
init/close |
lifecycle_sound (Hoare-style) |
What is proven, what is not
| In the proof | Outside the proof |
|---|---|
The constraint kinds above. The contract is in lean/VerifiedAnchor/Contract/; the proofs are in lean/VerifiedAnchor/Codegen/. |
Borsh deserialisation of typed account payloads. BorshFailed is an honest runtime error, not a silent gap. |
Concrete Solana primitives — real findProgramAddress, lamports, rent, owner / executable flags. Modelled under VerifiedAnchor.Solana. |
CPI effects beyond init / close. Token transfers, custom program calls. |
| The init/close lifecycle modelled as state transformers with Hoare pre/post-conditions. | Anchor constraints not modelled in v0.1.0: realloc, zero, token / mint / associated-token, custom constraint = expr. The macro emits a compile_error! with a migration-guide pointer. |
Empirical validation: four real Solana mainnet CVE classes are reproduced in rust/verified-anchor-exploits/ as litesvm before/after. The verified version rejects the attacker on chain in every case. |
The Solana runtime contract itself — we trust the runtime to enforce account ownership, signer flags, and writable flags as documented. |
The library's claim is not "your Solana program is now bug-free". The claim is that the macro-level account-validation bug class is eliminated at the framework level for the supported constraint subset. Full discussion in docs/verified-anchor-bridge.md.
Build and test
Lean (4.30.0, via elan; dependency: batteries):
&&
Rust workspace (1.93+):
&&
SBF programs (requires solana-cli platform-tools):
&&
&&
End-to-end proof discharge (Lean + cargo together):
Audit the proofs
The headline theorems' axiom dependencies are [propext, Quot.sound], the standard Lean propositional-extensionality and quotient-soundness axioms. No sorry, no Classical.choice, no native_decide.
Examples
rust/verified-anchor-example— worked user crate exercising validation + lifecycle.rust/verified-anchor-exploits— four real Solana mainnet CVE classes, naive vs verified.
Landing page
A self-contained static landing page lives under web/. It uses no build step, no
framework, and no external scripts beyond Google Fonts. Preview locally with any static
server:
&&
# then open http://localhost:8000
To host on GitHub Pages:
- In the repository on GitHub, open Settings → Pages.
- Under Build and deployment, set the source to Deploy from a branch.
- Choose Branch:
masterand Folder:/web. Save.
The site goes live at https://parthrathix0.github.io/Verified-Anchor/ within a minute. A
web/.nojekyll file is committed so GitHub Pages serves the file as-is without running it
through Jekyll.
Contributing
Issues and audit attempts are welcome. Substantive code patches require a prior contributor agreement granting the author the right to incorporate the change under the project license (see below). Open an issue first if you intend to send code.
License
CC BY-NC-ND 4.0. This is not a standard open-source license. Practical effects:
- NonCommercial. No use for commercial advantage without prior written permission.
- NoDerivatives. No distribution of modified versions.