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:
rustc --versioncargo --versionsolana --versionIf 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:
cargo install --git https://github.com/otter-sec/anchor.git --branch anchor-next anchor-cli --locked --forceWarning (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:
CARGO_PROFILE_RELEASE_LTO=off cargo install --git https://github.com/otter-sec/anchor.git --branch anchor-next anchor-cli --locked --forceLocal checkout#
A local checkout is useful for inspecting internals, patching the CLI, or running the debugger against the checked-in bench programs:
git clone https://github.com/otter-sec/anchor.gitcd anchorgit checkout anchor-nextcargo install --path cli --forcePrepend 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:
[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:
[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.
anchor init --no-install countercd 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
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.
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:
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:
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.
anchor buildanchor testanchor 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:
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());}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.
anchor test --profile # renders flamegraph SVGsanchor debugger # opens the instruction-stepper TUIThe 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.
Update the crate name, handler signatures, wrapper lifetimes, and key type names using the table below.
Move fixed-size state to Account<T> and move variable-length state to BorshAccount<T>.
Replace deprecated has_one checks with explicit address checks where possible, then verify PDA seeds and bumps compile.
Use LiteSVM tests through anchor_v2_testing::svm() so the same tests can feed profiling, debugger, and coverage tooling.
Rename table#
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:
#[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)]:
#[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:
#[derive(Accounts)]pub struct Withdraw { #[account(has_one = creator)] pub config: Account<Config>, pub creator: UncheckedAccount,}#[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:
[dependencies]anchor-lang-v2 = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next", features = ["compat"] }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.