Anchor v2 RC Is Here: 90% Smaller Programs, Fuzzing by Default

The Anchor team has shipped Anchor v2.0.0-rc.1, a complete rewrite of Solana's most widely used program framework on top of Pinocchio, with binaries over 90% smaller and compute costs 3 to 6 times lower than v1. It's the first release candidate for v2, and it arrives with a coverage-guided fuzzer wired into the CLI and two independent security audits behind it.

Anchor v2 was rebuilt from scratch. The work lives on the anchor-next branch of the otter-sec/anchor repository, with its own documentation site. The account model, the constraint system, the dispatcher, and testing have all been rebuilt.

Why a rewrite? v1 got Anchor a long way, but there's years of accumulated complexity. v2 is built on a trait-based architecture: less boilerplate, clearer constraints, and more extensible. Moving to Pinocchio lets us squeeze more out of the framework.

— Robert Chen, OtterSec

What does the move to Pinocchio actually buy you?

Rebuilding Anchor on Pinocchio cuts program binaries by more than 90% and drops compute unit consumption three to six times, according to the release announcement. The v2 docs claim more on their own benchmark set: up to 94% smaller binaries and 2.8x to 50.4x fewer CU depending on the program, with example programs landing between 6.2 KB and 37.4 KB.

Pinocchio is a zero-dependency Rust library for writing Solana programs that avoids the standard Solana SDK entirely. Anchor v2 is built as #![no_std] with explicit alloc, which means the framework stops dragging the Rust standard library and the full SDK onto the chain with every deploy.

Deploy cost on Solana scales with program size, so a 90% cut to your binary is a 90% cut to what it costs you to deploy. The CU savings show up as headroom. Every unit you don't spend on framework overhead is a unit available for your actual logic, or one fewer reason to split an instruction in two.

The account model is where most of this comes from.

Fixed-size state in v2 uses zero-copy Account<T> backed by Pod, so the runtime reads fields in place instead of deserializing the whole struct. Variable-length data, anything holding a Vec, a String, or an enum, moves to BorshAccount<T>.

In v1 that split was opt-in through AccountLoader, and most programs never bothered. In v2 zero-copy is the default and Borsh is the exception you ask for.

Trait vs Macros

Anchor v2 replaces v1's macro-heavy design with a small set of traits, so you can add account types and constraints from your own crate without forking the framework. v1 leaned hard on macros, and extending it usually meant patching Anchor itself.

Four of them carry the weight:

  • AnchorAccount makes a wrapper type usable as a field in #[derive(Accounts)]. Implement it and your type behaves like a first-class account.

  • AccountConstraint<A> defines custom namespaced constraints across four lifecycle phases: init, check, update, and exit.

  • Id returns a program's canonical address so Program<T: Id> can validate it at load time.

  • Discriminator lets you set custom 8-byte discriminators on accounts or instructions.

The practical result is that adding a constraint is a trait implementation in your own crate rather than a patch to Anchor's macro. anchor-spl now lives as a downstream crate on exactly that surface, and the docs ship a custom constraints example you can read in a sitting.

The entrypoint itself is replaceable. A custom entrypoint skips Anchor's dispatcher for specific instructions, either by swapping the 8-byte discriminator for a single byte with #[discrim = N] or by taking the entrypoint over completely and falling back to __anchor_dispatch for everything else. On the prop-amm benchmark, an assembly fast path gets an update instruction down to 26 CU.

The warning attached to that is real. Skipping the dispatcher skips the generated TryAccounts work, which means you lose automatic checks on account counts, duplicate mutables, owner verification, signer and writable flags, data length, and PDA derivation. You reimplement whatever your handler needs, by hand. The docs are direct about it: do this only when a benchmark proves the dispatcher is your problem.

Fuzzing, profiling, and debugging ship in the box

Four new CLI subcommands cover fuzzing, profiling, debugging, and coverage:

  • anchor fuzz runs Crucible, the coverage-guided invariant fuzzer for Solana programs built by Asymmetric Research. It mutates both inputs and instruction sequences, and it's on by default, so fuzzing is no longer a separate framework you bolt on later.

  • anchor test --profile records SBF register traces while your tests run, drops them in target/anchor-v2-profile/, and post-processes them into flamegraph SVGs.

  • anchor debugger opens a TUI over those traces with source-line mapping, an instruction timeline, CPI call stacks, and per-instruction CU costs. --gdb gives you breakpoint-style stepping, and --skip-run reuses traces you already have.

  • anchor coverage maps executed SBF program counters back to Rust source lines and writes an LCOV file to target/coverage/sbf.lcov, which drops straight into whatever coverage tooling your CI already runs.

Tests themselves default to LiteSVM through anchor_v2_testing::svm(), so anchor test loads your compiled .so, creates accounts, and sends transactions in-process without spinning up a validator.

What does migrating from Anchor v1 look like?

Expect real work. Four parts of Anchor changed enough that you'll need to relearn them, and the renames sitting on top of that are mechanical enough that the compiler will walk you through most of them.

How your state is laid out. This is the real migration. v1 deserialized a whole account struct with Borsh on every load, and zero-copy was something you opted into through AccountLoader once a struct got too big. v2 flips the default. Fixed-size accounts are zero-copy and have to implement Pod, which rules out Vec, String, and enums in the struct. Anything with variable-length fields moves to BorshAccount<T>, which you now ask for explicitly. Sorting your state definitions into those two buckets is most of the migration, and it takes real design judgment about which accounts are worth making fixed-size.

How you express validation. v1 constraints were attributes the macro recognized, so the vocabulary was whatever Anchor shipped. v2 turns constraints into trait implementations, which is what opens that vocabulary up to your own crate. The visible casualty is has_one, now deprecated in favor of stating the relationship directly on the account it governs through an address check. Slightly more typing, and the check is obvious at the point it happens.

How borrows are tracked. The <'info> lifetime threading is gone from account wrappers, handlers take a mutable context, and CPI runs through borrow-tracked handles. You stop propagating lifetimes through every signature and let the framework track access for you.

How you test. v1 pointed you at TypeScript and a local validator. v2 scaffolds Rust tests against LiteSVM in-process, so anchor test runs without a validator and the profiler, debugger, and coverage tools all read the traces it produces. If your suite is TypeScript today, budget for rewriting it in Rust.

Your client code is the thing that doesn't move yet. With no stable v2 TypeScript package published, your app stays on @anchor-lang/core ^1.0.0 while your program crosses over.

What's not ready yet

Anchor v2 is a release candidate, and the docs list the sharp edges. The crates aren't on crates.io yet, so you're pulling from git on anchor-next, and APIs can still shift between commits.

There's no stable v2 TypeScript package. Current scaffolds stay pinned to @anchor-lang/core ^1.0.0, so your client code is on v1 for now. anchor-spl covers core token, mint, interface account, and several Token-2022 extension paths, but less common SPL surfaces, metadata-oriented constraints in particular, may not be there yet.

The RC is a good weekend project. Read the v2 docs, pull the branch, and run anchor test --profile against a program you've already written. The flamegraph will show you where your CUs go.