Anchor v2 alpha is here! Up to 95% smaller binaries, 3.0 to 50.4× fewer CU
Anchor Docs
A lone boy hauls an anchor on the shore, evoking the weight of maritime destiny and Homer's mastery of atmosphere and technique.
Boy with Anchor, Winslow Homer, 1873
Overview

Get started

Install the Anchor v2 toolchain, scaffold your first program, and migrate existing v1 programs.

Installation

Install the Anchor CLI and depend on the alpha crates from the anchor-next branch.

Anchor v2 installs from source while the alpha is branch-only. The normal Solana program toolchain is a prerequisite, so make sure Rust, Cargo, and the Solana CLI are already on your PATH:

Terminal window
rustc --version
cargo --version
solana --version

If you are setting up a machine from scratch, install Rust with rustup and install the Solana CLI from the Agave install docs. On Windows, run the Solana and Anchor commands inside WSL rather than PowerShell.

The v2 alpha crates have not been published to crates.io yet, so install the CLI directly from the anchor-next branch:

Terminal window
cargo install --git https://github.com/otter-sec/anchor.git --branch anchor-next anchor-cli --locked --force
Warning (Overwrites the anchor binary)

cargo install overwrites the anchor binary on your PATH. To keep another Anchor install available, run this binary from a source checkout instead of putting it first on PATH.

AVM tracks published Anchor releases. Until v2 has published releases, use cargo install --git ... --branch anchor-next for the CLI you use with these docs.

On macOS, the final link can fail with an error like ld: could not parse bitcode object file ... Unknown attribute kind. If that happens, disable LTO for this install:

Terminal window
CARGO_PROFILE_RELEASE_LTO=off cargo install --git https://github.com/otter-sec/anchor.git --branch anchor-next anchor-cli --locked --force

Local checkout#

A local checkout is useful for inspecting internals, patching the CLI, or running the debugger against the checked-in bench programs:

Clone the repository
Terminal window
git clone https://github.com/otter-sec/anchor.git
cd anchor
git checkout anchor-next
Install the CLI from source
Terminal window
cargo install --path cli --force

Prepend CARGO_PROFILE_RELEASE_LTO=off on macOS if you hit the LTO error above.

Program dependencies#

New workspaces created by anchor init already include the git dependency for anchor-lang-v2. Add anchor-spl-v2 when the program uses SPL token helpers or Token-2022 interfaces:

Cargo.toml
[dependencies]
anchor-lang-v2 = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }
anchor-spl-v2 = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }

Add these to the program crate at programs/<program-name>/Cargo.toml, not the workspace root Cargo.toml.

The default anchor-lang-v2 feature set is alloc, guardrails, and account-resize. Optional features include const-rent, compat, and testing. See Feature flags for the tradeoffs.

Test dependencies#

The default scaffold uses Rust tests with anchor-v2-testing. Add the dev dependency to the same programs/<program-name>/Cargo.toml:

Cargo.toml
[dev-dependencies]
anchor-v2-testing = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }
[features]
profile = ["anchor-v2-testing/profile"]

That profile feature is what anchor test --profile, anchor debugger, and anchor coverage use to record SBF register traces.

First program

Scaffold, build, and test an Anchor program with the LiteSVM template.

The quickest path is the default anchor init template. It creates a workspace, a multi-file Rust program, and a LiteSVM integration test under programs/<name>/tests/.

This walkthrough uses a tiny counter program so the generated files are easy to inspect. What matters is the shape of the project. Anchor writes the entrypoint, account validation, tests, IDL, and build artifacts into predictable places.

Scaffold the workspace
Terminal window
anchor init --no-install counter
cd counter

--no-install skips the JavaScript package install. The default test template is Rust + LiteSVM, so the program can build and test without Node.

The new workspace has this shape:

  • app/Frontend code, empty by default
  • migrations/Optional deploy scripts
    • deploy.ts
  • programs/On-chain programs
    • counter/
      • src/
        • instructions/Instruction handlers
          • initialize.rs
        • constants.rs Program constants
        • error.rs Custom errors
        • instructions.rs Instruction module root
        • lib.rs Program entrypoint
        • state.rs Account data types
      • tests/LiteSVM integration tests
        • test_initialize.rs
      • Cargo.toml Program crate manifest
  • Anchor.toml Workspace config
  • Cargo.toml Rust workspace manifest
  • package.json TypeScript package metadata
  • rust-toolchain.toml Rust toolchain pin
  • tsconfig.json TypeScript config
Inspect the generated program

Start with programs/counter/src/lib.rs. It declares the modules, the program id, and the instruction entrypoints. The default multi-file template keeps the handler body in instructions/initialize.rs, then the #[program] module forwards to it.

programs/counter/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang_v2::prelude::*;
pub use instructions::*;
declare_id!("3ynNB373Q3VAzKp7m4x238po36hjAGFXFJB4ybN2iTyg");
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {
initialize::handler(ctx)
}
}

Your generated declare_id!() value will be different. Anchor creates it from the program keypair under target/deploy/.

The first real instruction is in programs/counter/src/instructions/initialize.rs. It uses anchor_lang_v2::prelude::*, &mut Context<T>, and account wrappers without <'info> lifetimes:

programs/counter/src/instructions/initialize.rs
use anchor_lang_v2::prelude::*;
use crate::state::Counter;
#[derive(Accounts)]
pub struct Initialize {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer)]
pub counter: Account<Counter>,
pub system_program: Program<System>,
}
pub fn handler(ctx: &mut Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = *ctx.accounts.payer.address();
msg!("Counter initialized");
Ok(())
}

The account data is fixed-size, so it uses the default zero-copy account form:

programs/counter/src/state.rs
use anchor_lang_v2::prelude::*;
#[account]
pub struct Counter {
pub count: u64,
pub authority: Address,
}

#[account] makes Counter Pod-backed and gives Account<Counter> the layout [8-byte discriminator][Counter bytes]. Use BorshAccount<T> instead when the account contains Vec, String, or payload enums.

The #[derive(Accounts)] struct is the checklist Anchor runs before the handler. Here, payer must be writable, counter is created and paid for by payer, and system_program is the well-known program field the scaffold expects.

Build and test
Terminal window
anchor build
anchor test

anchor build compiles the program and writes the SBF binary to target/deploy/counter.so. It also writes client-facing artifacts such as target/idl/counter.json and target/types/counter.ts.

anchor test runs the script in Anchor.toml. For the default LiteSVM template, that script is cargo test, and the workspace is configured to skip starting a local validator.

The LiteSVM test adds the deployed SBF program to an in-process VM, builds an instruction using generated account structs, and sends a signed transaction:

programs/counter/tests/test_initialize.rs
use {
anchor_lang_v2::{
accounts::Account, bytemuck, programs::System,
solana_program::instruction::Instruction, Id, InstructionData, Space, ToAccountMetas,
},
anchor_v2_testing::{Keypair, LiteSVM, Message, Signer, VersionedMessage, VersionedTransaction},
};
#[test]
fn test_initialize() {
let program_id = counter::id();
let payer = Keypair::new();
let counter = Keypair::new();
let mut svm = anchor_v2_testing::svm();
svm.add_program(program_id, include_bytes!("../../../target/deploy/counter.so")).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&counter::instruction::Initialize {}.data(),
counter::accounts::Initialize {
payer: payer.pubkey(),
counter: counter.pubkey(),
system_program: System::id(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(msg),
&[&payer, &counter],
)
.unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok(), "send_transaction failed: {:?}", res);
let account = svm.get_account(&counter.pubkey()).expect("counter account");
assert_eq!(account.data.len(), <Account<counter::state::Counter> as Space>::INIT_SPACE);
let counter_state: &counter::state::Counter = bytemuck::from_bytes(&account.data[8..]);
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
}
Profile and debug

The generated profile = ["anchor-v2-testing/profile"] feature lets the same tests feed the profiler and debugger. Run either tool directly. Each tool records traces on demand if they are not already present.

Terminal window
anchor test --profile # renders flamegraph SVGs
anchor debugger # opens the instruction-stepper TUI

The trace-backed tooling is covered in Profiling and debugger.

Migrating from v1

Port a v1 program to v2 with the main rename and account-model changes.

Most v1 programs port to v2 with mechanical renames plus one account-model decision. Fixed-size state goes through zero-copy Account<T>. Variable-length state goes through BorshAccount<T>.

This page is for existing programs. If you are learning Anchor for the first time, start with First program and come back here when you need to compare old code to new code.

Apply the mechanical renames

Update the crate name, handler signatures, wrapper lifetimes, and key type names using the table below.

Choose the account model

Move fixed-size state to Account<T> and move variable-length state to BorshAccount<T>.

Refresh constraints

Replace deprecated has_one checks with explicit address checks where possible, then verify PDA seeds and bumps compile.

Rebuild the test harness

Use LiteSVM tests through anchor_v2_testing::svm() so the same tests can feed profiling, debugger, and coverage tooling.

Rename table#

v1v2
use anchor_lang::prelude::*use anchor_lang_v2::prelude::*
solana_programpinocchio or v2 re-exports
std required#![no_std] compatible, with alloc on by default
<'info> lifetimes on wrappersremoved
Context<T> handlers&mut Context<T> handlers
PubkeyAddress
Account<'info, T> for fixed-size dataAccount<T> with T: Pod
Account<'info, T> for Vec, String, payload enumsBorshAccount<T>
AccountLoader<'info, T> zero-copyAccount<T>
ctx.bumps.get("name")ctx.bumps.name
has_one = fieldaddress = parent.field on the sibling account
Note (AccountLoader means something else in v2)

v2 still has a type named AccountLoader, but it is the sequential account cursor used internally by generated validation code. It is not the v1 user-facing zero-copy wrapper.

Account data choice#

Account<T> is now zero-copy by default. The account bytes are cast directly into T, so T must be fixed-size and Pod-compatible:

v2 fixed-size state
#[account]
pub struct Counter {
pub count: u64,
pub authority: Address,
}
#[derive(Accounts)]
pub struct Increment {
#[account(mut)]
pub counter: Account<Counter>,
}

For variable-length data, use #[account(borsh)]:

v2 variable-length state
#[account(borsh)]
pub struct Profile {
pub owner: Address,
pub name: String,
pub friends: Vec<Address>,
}
#[derive(Accounts)]
pub struct EditProfile {
#[account(mut)]
pub profile: BorshAccount<Profile>,
}

Constraint changes#

The v1 has_one constraint still parses, but v2 emits a deprecation warning. Prefer moving the check onto the sibling account with address:

v1 style
#[derive(Accounts)]
pub struct Withdraw {
#[account(has_one = creator)]
pub config: Account<Config>,
pub creator: UncheckedAccount,
}
v2 style
#[derive(Accounts)]
pub struct Withdraw {
pub config: Account<Config>,
#[account(address = config.creator)]
pub creator: UncheckedAccount,
}

address is more general. The right-hand side can be any expression, not only a field with the same name as a sibling account.

Compat feature#

The compat feature re-exposes a small set of v1-shaped helpers so existing call sites compile without rewrites:

Cargo.toml
[dependencies]
anchor-lang-v2 = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next", features = ["compat"] }
v1 helperNotes
debug!Logging macro that accepts any Rust format string by routing through alloc::format!.
error!, err!Convert an error code into a ProgramError (and an Err(...) wrapper).
pubkey!Compile-time base58 decode that yields an Address.
PubkeyType alias for Address under anchor_lang_v2::solana_program::pubkey.
AccountViewCompatExtension trait that exposes v1-style accessors on pinocchio’s AccountView.

error! is implemented as a thin shim in v2, not a proc-macro, so it does not allocate a rich AnchorError with source and message metadata the way v1 did. v2 keeps runtime errors as ProgramError and routes error text through the IDL.

Keep compat off in production unless one of these shims is on a hot path. debug! in particular allocates and is more expensive than v2’s native msg!.

The Lamports trait, which gives accounts v1-style get_lamports, add_lamports, and sub_lamports methods, is in the prelude unconditionally and does not require the compat feature.

Still alpha#

Some surfaces are intentionally incomplete while v2 is alpha. Less-common anchor-spl constraints, parts of the TypeScript publishing pipeline, and some client-codegen shapes are still settling. See Alpha limitations before porting a large production program.

Esc

Start typing to search the docs.