Anchor v0.32 → v1 Migration
Full upgrade checklist for an Anchor workspace from v0.32.x to v1. Triage which items apply, then work through them in order.
Items marked [COMPILE] will prevent the program from building if not addressed. Items marked [TS] affect TypeScript clients. Items marked [CLI] affect developer workflow. Items marked [DEPLOY] must happen in the right order relative to deployment. Items marked [CLIENT] affect the Rust anchor-client crate.
Applying the Migration (order matters)
IDL housekeeping and the program code upgrade are independent tracks that can be done in parallel, but have one hard constraint: legacy IDL accounts must be closed with the v0.32 CLI before deploying v1.
Before deploying v1 (old program still live)
A1. Re-publish IDL to the new v1 location *(v1 CLI)* — anchor idl init / anchor idl upgrade, or use program-metadata CLI directly (see §5). A2. Update and publish clients — update any clients that fetch the on-chain IDL to read from the new v1 location, then deploy them. A3. Close legacy IDL accounts *(v0.32 CLI)* on every cluster (see §5). Deploying the v1 binary or upgrading the CLI first makes this impossible.
Client notice: for minimal downtime, follow this order — new IDL first, then clients, then close legacy accounts. Clients depending on the old location will continue to work until A3.
Program code upgrade (requires v1 CLI)
- Update toolchain — bring Anchor CLI, AVM, and Solana CLI to the required versions first (see §0).
- Audit — run
cargo checkwith bumped deps and collect all errors before fixing anything. - Fix compile errors in order — deps → CPI context → duplicate accounts →
declare_program!renames → multiple#[error_code]blocks →Contextlifetime annotations. anchor build— confirms Rust is clean.- Update TS — rename package imports, rerun
yarn install/npm install. - Run tests —
anchor test(surfpool) oranchor test -- --features some-feature. - Deploy —
anchor deploy.
0. Check and update toolchain [CLI]
Verify your current versions before touching any code:
anchor --version # target: 1.0.0
avm --version
solana --version # recommended: 3.1.10
rustc --version # must support edition 2021; 1.75+ recommendedUpdate AVM and Anchor CLI:
If your current avm supports self-update:
avm self-update
avm install 1.0.0
avm use 1.0.0Otherwise bootstrap via cargo:
cargo install avm --git https://github.com/solana-foundation/anchor --tag v1.0.0 --locked
avm install 1.0.0
avm use 1.0.0Without AVM — install anchor-cli directly:
cargo install --git https://github.com/solana-foundation/anchor --tag v1.0.0 anchor-cli --lockedUpdate Solana CLI (if below 3.x):
sh -c "$(curl -sSfL https://release.anza.xyz/v3.1.10/install)"
solana --version # confirm 3.1.101. Update dependencies [COMPILE]
Cargo.toml (workspace root and program crate):
# Before
anchor-lang = "0.32.1"
anchor-spl = "0.32.1"
solana-program = "2"
# After
anchor-lang = "1.0.0"
anchor-spl = "1.0.0"
solana-program = "^3" # and any other solana-* crate that appears directly- All
solana-*crates that appear in[dependencies]must be^3or higher.
Add resolver = "2" to the workspace root Cargo.toml:
[workspace]
members = ["programs/*"]
resolver = "2" # required for edition 2021 membersWithout resolver = "2", Cargo uses the v1 feature resolver, which unifies features across all targets. This causes spurious dependency conflicts and unexpected feature activation when mixing Solana/Anchor crates — manifesting as duplicate type errors or missing trait implementations that disappear once the resolver is set correctly. Any workspace containing at least one edition = "2021" crate should have this set.
- The
cargo updateworkarounds for 0.32 (base64ct --precise 1.6.0,constant_time_eq --precise 0.4.1,blake3 --precise 1.5.5) are no longer needed — remove them. - If you transitively depended on
solana-sdkfor signing, usesolana-signerdirectly.
See compatibility-matrix.md for the full Anchor v1 ↔ Solana CLI version table.
Dev dependencies — litesvm / anchor-litesvm:
When you bump solana-* crates to ^3, also bump litesvm in [dev-dependencies]. The correct version depends on which minor series of the granular solana-* crates your workspace resolves to:
| litesvm | Solana granular crates era | Key markers |
|---|---|---|
0.8.2 | ~3.0 | solana-hash ~3.0, solana-vote-interface 4.0, solana-system-interface 2.0 |
0.9.1 | ~3.1–~3.3 | solana-hash 4.0, solana-vote-interface 5.0, solana-system-interface 3.0 |
>0.10.0 | 3.3+ | follow latest releases when on cutting-edge solana-* deps |
# [dev-dependencies] — pick the row that matches your solana-* versions
litesvm = "0.8.2" # solana-* ~3.0
# litesvm = "0.9.1" # solana-* ~3.1–3.3 (solana-hash 4.0, solana-vote-interface 5.0)
# If using the Anchor wrapper:
anchor-litesvm = "0.3" # requires anchor-lang ^1.0.0 and litesvm ^0.8.2Tip: run
cargo tree -dafter bumping — a duplicatesolana-*in the dependency tree at two incompatible minor versions is the most common sign you've picked the wronglitesvmversion.
package.json [TS] — full package rename table:
Before (@coral-xyz/…) | After (@anchor-lang/…) |
|---|---|
@coral-xyz/anchor | @anchor-lang/core |
@coral-xyz/spl-token | @anchor-lang/spl-token |
@coral-xyz/anchor-errors | @anchor-lang/errors |
@coral-xyz/borsh | @anchor-lang/borsh |
@coral-xyz/anchor-cli | @anchor-lang/cli |
// Before
{
"@coral-xyz/anchor": "^0.32.1",
"@coral-xyz/spl-token": "^0.32.1"
}
// After
{
"@anchor-lang/core": "^1.0.0",
"@anchor-lang/spl-token": "^1.0.0"
}// Before
import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorProvider, BN } from "@coral-xyz/anchor";
import { Idl } from "@coral-xyz/anchor/dist/cjs/idl"; // deep import
// After
import * as anchor from "@anchor-lang/core";
import { Program, AnchorProvider, BN } from "@anchor-lang/core";
import { Idl } from "@anchor-lang/core"; // IDL types live at root nowFind all occurrences:
grep -r "@coral-xyz" --include="*.ts" --include="*.js" --include="package.json" .
grep -r "dist/cjs/idl" --include="*.ts" --include="*.js" .2. Fix CPI context construction [COMPILE]
CpiContext::new and CpiContext::new_with_signer no longer accept a program AccountInfo as the first argument. Pass the program's Pubkey (program ID) directly instead. Remove the program account from the accounts struct.
// Before (v0.32)
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>, // <-- needed to pass AccountInfo
}
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
token::transfer(cpi_ctx, amount)
}
// After (v1) — program ID as first argument; program field removed from struct
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
// token_program no longer needed for CPI
}
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(Token::id(), cpi_accounts);
token::transfer(cpi_ctx, amount)
}
// PDA-signed CPI
// Before
let cpi_ctx = CpiContext::new_with_signer(ctx.accounts.token_program.to_account_info(), cpi_accounts, signer_seeds);
// After
let cpi_ctx = CpiContext::new_with_signer(Token::id(), cpi_accounts, signer_seeds);Well-known IDs: Token::id(), System::id(), system_program::ID. For external programs declared with declare_program!, use my_program::ID.
3. Resolve duplicate mutable account errors [COMPILE]
Anchor now rejects instructions where the same account appears more than once as mutable.
error: duplicate mutable account `vault` — use `dup` constraint if intentionalOption A — prevent aliasing with a constraint (accidental duplication):
#[account(
mut,
constraint = token_b.key() != token_a.key() @ MyError::SameAccount
)]
pub token_b: Account<'info, TokenAccount>,Option B — allow intentional duplication:
#[account(mut, dup)]
pub destination: Account<'info, TokenAccount>,Checked types: Account, LazyAccount, InterfaceAccount, Migration. Read-only types and UncheckedAccount are not checked. Accounts under init_if_needed are now included in the check.
4. Update declare_program! usages [COMPILE]
Rename utils module to parsers:
// Before
use my_external_program::utils::*;
use my_external_program::utils::parse_instruction;
// After
use my_external_program::parsers::*;
use my_external_program::parsers::parse_instruction;grep -r "::utils::" --include="*.rs" .Remove interface-instructions feature and #[interface] attribute:
The feature and attribute are gone. Use #[instruction(discriminator = <const>)] instead.
# Before (Cargo.toml)
anchor-lang = { version = "0.32.1", features = ["interface-instructions"] }
# After — feature removed entirely
anchor-lang = "1.0.0"// Before
#[interface(spl_transfer_hook_interface::execute)]
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> { Ok(()) }
// After — use the interface crate's discriminator constant directly
#[instruction(discriminator = spl_transfer_hook_interface::instruction::ExecuteInstruction::SPL_DISCRIMINATOR)]
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> { Ok(()) }5. Close legacy IDL accounts and re-publish [DEPLOY]
⚠️ Do this just before deploying the v1 binary. Once a v1 binary is live, the legacy IDL instructions are gone — rent in those accounts becomes permanently inaccessible.
Step 1 — close the legacy IDL account on every cluster:
This must be run with the Anchor CLI v0.32 while the v0.32 binary is still deployed. The v1 CLI's
idlcommands target the Program Metadata program and cannot interact with legacy IDL accounts. Upgrading the CLI before closing means you lose the ability to recover that rent.
# with anchor-cli 0.32.x still installed
anchor idl close --provider.cluster devnet <PROGRAM_ID>
anchor idl close --provider.cluster mainnet-beta <PROGRAM_ID>Step 2 — deploy the v1 binary: anchor deploy.
Step 3 — re-publish the IDL via Program Metadata.
Two options — pick one:
Option A: Anchor CLI (resolved from workspace, no program ID needed):
anchor idl init --filepath target/idl/my_program.json # first publish
anchor idl upgrade --filepath target/idl/my_program.json # subsequent updatesOption B: program-metadata CLI (usable immediately after closing, independent of the Anchor CLI and deploy cycle):
npm install -g @solana-program/program-metadata
program-metadata upload idl target/idl/my_program.json --program-id <PROGRAM_ID>Option B is useful when you want to push an updated IDL without going through a full anchor deploy, or when working outside an Anchor workspace. See the program-metadata README for the full command reference and options.
What changes in v1: programs have no idl_create_buffer, idl_write, idl_set_buffer entrypoints. IDL lives in a Program Metadata account managed by a separate on-chain program. Already-deployed v0.32 programs that were not closed retain their legacy IDL account; v1 tooling can read them but cannot manage them.
6. Update AccountInfo usage [WARNING]
Using raw AccountInfo<'info> in #[derive(Accounts)] now emits a compile-time warning. These are warnings, not errors — migration can be incremental.
| Old | New |
|---|---|
AccountInfo<'info> (unknown data) | UncheckedAccount<'info> + /// CHECK: comment |
AccountInfo<'info> (token account) | InterfaceAccount<'info, TokenAccount> |
AccountInfo<'info> (executable program) | Program<'info, MyProgram> or Interface<'info, T> |
UncheckedAccount::clone() vs .to_account_info()
In anchor v1, .clone() on UncheckedAccount<'info> returns UncheckedAccount<'info>, not AccountInfo<'info>. Any function or CPI account struct that expects AccountInfo<'info> will now fail:
// Error: mismatched types — expected AccountInfo<'_>, found UncheckedAccount<'_>
let ctx_accounts = MyCpiAccounts {
some_account: ctx.accounts.some_unchecked.clone(),
..
};
// Fix: use .to_account_info() explicitly
let ctx_accounts = MyCpiAccounts {
some_account: ctx.accounts.some_unchecked.to_account_info(),
..
};This commonly surfaces when constructing CPI account structs or calling helper functions that accept AccountInfo<'info> directly. Find occurrences:
grep -rn "\.clone()" --include="*.rs" . | grep "UncheckedAccount\|merkle_tree\|tree_authority"7. Suppress unexpected_cfgs warnings from macros [WARNING]
Anchor and Solana derive macros emit code gated on cfg flags (anchor-debug, custom-heap, custom-panic) that Rust's unexpected_cfgs lint doesn't know about. This produces a wall of warnings after a clean build.
Option A — declare them as features in each program's [features] (more targeted, recommended):
# programs/my_program/Cargo.toml
[features]
anchor-debug = []
custom-heap = []
custom-panic = []
# keep your existing features — add these alongside themDeclaring them as features tells Cargo they are valid cfg values, so the compiler stops warning about them without blanket-suppressing the entire unexpected_cfgs lint.
Option B — suppress workspace-wide via lints (blunter, but simpler for large workspaces):
# Cargo.toml (workspace root)
[workspace.lints.rust]
unexpected_cfgs = { level = "allow" }Then opt every program crate into the workspace lints:
# programs/my_program/Cargo.toml
[lints]
workspace = trueNote on Option B: Do not use the
check-cfglist form (check-cfg = ['cfg(anchor-debug)', ...]) — cfg names containing hyphens are rejected by the compiler withinvalid '--check-cfg' argument. Uselevel = "allow"without the list.
# find all program Cargo.toml files that still need updating
grep -rL "workspace = true" programs/*/Cargo.toml8. Handle IDL external account exclusion [IDL]
External account types (e.g. SPL Token Mint, TokenAccount) are no longer inlined in the generated IDL. Clients that relied on your IDL to deserialize third-party accounts must now use those programs' own clients.
// Before — type came from your program's IDL automatically
const mintAccount = await program.account.mint.fetch(mintAddress);
// After — use the token program's own client
import { getMint } from "@solana/spl-token";
const mintAccount = await getMint(connection, mintAddress);9. Switch the test runner [CLI]
anchor test and anchor localnet now use surfpool by default.
# Anchor.toml — opt out to standard validator
[tooling]
validator = "solana"
# Or configure surfpool
[surfpool]
startup_wait = 5000
log_level = "info" # default is "none"
block_production_mode = "clock" # or "transaction"
datasource_rpc_url = "https://api.mainnet-beta.solana.com" # optional forkAdd to .gitignore:
.surfpool/CI — surfpool must be installed explicitly:
- name: Install surfpool
run: curl -sL https://run.surfpool.run/ | bash10. Remove external solana CLI dependency [CLI]
Anchor no longer shells out to solana. Update CI pipelines and scripts.
| Before | After |
|---|---|
solana address | anchor address |
solana balance | anchor balance |
solana airdrop | anchor airdrop |
solana program deploy | anchor deploy |
solana logs | anchor logs |
Keep the solana CLI install step only if you use it directly (keypair generation, cluster switching, etc.).
11. Clean up Anchor.toml and removed CLI commands [CLI]
Remove [registry] from Anchor.toml:
# Before — remove this entire section
[registry]
url = "https://anchor.projectserum.com"Remove arch build options from Anchor.toml (if present — arch = "sbf" etc. are no longer recognised):
grep -n "arch" Anchor.tomlanchor login is removed. Remove it from CI scripts and Makefile targets; the [registry] section it served is gone.
12. Disallow multiple #[error_code] blocks [COMPILE]
Having more than one #[error_code] block in a single program is now a compile-time error. Merge all error enums into one.
// Before — two separate blocks compiled fine
#[error_code]
pub enum InitError {
AlreadyInitialized,
}
#[error_code]
pub enum UpdateError {
InvalidAmount,
}
// After — single merged enum
#[error_code]
pub enum MyProgramError {
AlreadyInitialized,
InvalidAmount,
}If you used offset = N to avoid code collisions between separate enums, that attribute continues to work on the merged single enum.
grep -r "#\[error_code\]" --include="*.rs" .13. Update Context lifetime annotations [COMPILE]
Context was simplified from four lifetime parameters ('a, 'b, 'c, 'info) to one ('info). Most programs use Context<MyAccounts> without explicit lifetimes and are unaffected. If you annotated the lifetimes explicitly, remove the extra three.
// Before (v0.32)
pub fn my_handler<'a, 'b, 'c, 'info>(
ctx: Context<'a, 'b, 'c, 'info, MyAccounts<'info>>,
) -> Result<()> { ... }
// After (v1)
pub fn my_handler<'info>(ctx: Context<'info, MyAccounts<'info>>) -> Result<()> { ... }
// or simply (when the lifetime is inferred)
pub fn my_handler(ctx: Context<MyAccounts<'_>>) -> Result<()> { ... }grep -rn "Context<'" --include="*.rs" .14. Update Borsh 1.x serialization usage [COMPILE]
Anchor v1 depends on borsh 1.x, which removed several APIs present in borsh 0.10.
try_to_vec() removed
BorshSerialize::try_to_vec() no longer exists. Replace every call with borsh::to_vec(&value)?.
// Before
let data = my_struct.try_to_vec()?;
let hash = hashv(&[metadata.try_to_vec()?.as_slice()]);
// After
let data = borsh::to_vec(&my_struct)?;
let hash = hashv(&[borsh::to_vec(metadata)?.as_slice()]);grep -rn "try_to_vec" --include="*.rs" .Enum explicit discriminants conflict with anchor derive macros
In borsh 1.x, enums with explicit integer discriminants require #[borsh(use_discriminant=true)]. However, this attribute conflicts with #[derive(AnchorSerialize, AnchorDeserialize)], producing:
error: multiple `borsh` attributes not allowed on a single item
error: cannot find attribute `borsh` in this scopeFix: If the explicit discriminants match the default ordinal values (0, 1, 2, …), simply remove them. The serialized layout is identical.
// Before — conflicts with anchor derive macros
#[derive(AnchorSerialize, AnchorDeserialize)]
pub enum MyType {
Variant1 = 0,
Variant2 = 1,
}
// After — remove explicit discriminants; borsh ordinal encoding is the same
#[derive(AnchorSerialize, AnchorDeserialize)]
pub enum MyType {
Variant1,
Variant2,
}If discriminant values *don't* match ordinal order (e.g., Foo = 5, Bar = 10) you must implement borsh serialization manually instead of relying on the derive macro.
grep -rn " = [0-9]" --include="*.rs" programs/ # find enums with explicit discriminants15. Update Solana SDK 3.x API changes [COMPILE]
Anchor v1 uses Solana SDK 3.x, which has breaking API changes beyond just the version bump.
anchor_lang::solana_program re-export gaps
In anchor v0.31, anchor_lang::solana_program re-exported the full solana-program crate. In v1 several sub-modules are no longer re-exported:
| Module | Old import | New import |
|---|---|---|
keccak | anchor_lang::solana_program::keccak | solana_program::keccak |
hash | anchor_lang::solana_program::hash | solana_program::hash |
ed25519_program | anchor_lang::solana_program::ed25519_program | solana_program::ed25519_program |
sysvar::instructions | anchor_lang::solana_program::sysvar::instructions | solana_program::sysvar::instructions |
instruction::Instruction | anchor_lang::solana_program::instruction::Instruction | solana_program::instruction::Instruction |
program::invoke_signed | anchor_lang::solana_program::program::invoke_signed | solana_program::program::invoke_signed |
system_instruction quirk: system_instruction is *not* accessible as solana_program::system_instruction in SDK 3.x when you depend on solana-program directly — that sub-module was removed from the crate root. However, it is still re-exported by Anchor: anchor_lang::solana_program::system_instruction continues to work. Use that path rather than importing from solana_program directly.
// Fails in SDK 3.x with direct solana-program dep
use solana_program::system_instruction;
// Works — Anchor still re-exports it
use anchor_lang::solana_program::system_instruction;Fix: Add solana-program = { workspace = true } to the program's Cargo.toml and import directly from solana_program.
# program/Cargo.toml
[dependencies]
anchor-lang = { workspace = true }
solana-program = { workspace = true } # add this// Before
use anchor_lang::{prelude::*, solana_program::keccak};
use anchor_lang::{prelude::*, solana_program::sysvar::instructions::ID as IX_ID};
// After
use anchor_lang::prelude::*;
use solana_program::keccak;
use solana_program::sysvar::instructions::ID as IX_ID;grep -rn "anchor_lang::solana_program" --include="*.rs" programs/AccountInfo::realloc renamed to resize
The realloc(new_len, zero_init) method was renamed to resize(new_len) in Solana SDK 3.x. The zero_init parameter is gone (new space is always zeroed).
// Before
account_info.realloc(new_len, false)?;
account_info.realloc(0, false).map_err(Into::into)
// After
account_info.resize(new_len)?;
account_info.resize(0).map_err(Into::into)grep -rn "\.realloc(" --include="*.rs" .MAX_PERMITTED_DATA_INCREASE path change
// Before
use solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE;
// or
use anchor_lang::solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE;
// After
use solana_program::account_info::MAX_PERMITTED_DATA_INCREASE;CpiContext::new takes Pubkey, not AccountInfo
In anchor v1, the first argument to CpiContext::new / CpiContext::new_with_signer changed from AccountInfo to Pubkey.
// Before
CpiContext::new(ctx.accounts.system_program.to_account_info(), cpi_accounts)
CpiContext::new_with_signer(ctx.accounts.system_program.to_account_info(), cpi_accounts, signer_seeds)
// After — pass the program's Pubkey directly
CpiContext::new(System::id(), cpi_accounts)
CpiContext::new_with_signer(System::id(), cpi_accounts, signer_seeds)
// or use the constant
CpiContext::new(system_program::ID, cpi_accounts)
CpiContext::new_with_signer(solana_program::system_program::ID, cpi_accounts, signer_seeds)16. Audit external program CPI crates [COMPILE]
Any external CPI crate compiled against anchor 0.31 will produce dozens of trait-bound errors in your workspace because the AccountDeserialize, AccountSerialize, Owner, and Id traits changed in anchor v1. Common culprits: bubblegum-cpi, account-compression-cpi, tuktuk-program.
Symptoms:
error[E0277]: the trait bound `Noop: anchor_lang::Id` is not satisfied
error[E0277]: the trait bound `SplAccountCompression: anchor_lang::Id` is not satisfied
error[E0277]: `Program<'info, T>: anchor_lang::Id` not satisfiedStep 1 — identify affected crates
cargo tree 2>&1 | grep -E "anchor-lang|anchor-spl" | sort -uLook for any dependency still pulling in anchor-lang 0.x. Each such crate needs to be updated before your workspace will compile.
Step 2 — check for an updated release
For each affected crate, check whether the upstream maintainer has already published an anchor v1-compatible version:
cargo search <crate-name>Or check the crate's repository for a release or branch targeting anchor v1. If a compatible version exists, bump the version specifier in Cargo.toml and you're done.
Step 3 — update the crate yourself (if you own the repo)
If you control the affected crate in a separate repository, apply the same migration steps from this guide to that crate first (deps, CPI context, borsh, SDK API changes), publish or reference it via a git dep, then return here.
# Temporary git dep while waiting for a crates.io release
my-cpi-crate = { git = "https://github.com/my-org/my-cpi-crate", branch = "anchor-v1" }Step 4 (last resort) — vendor the crate locally
If no update is available and you don't control the upstream repo, vendor a minimal local copy. Create a vendor/ crate that uses declare_program! against the program's IDL JSON, add it as a workspace member, and point your workspace dependency to the path.
Do not use
[patch]for workspace members — cargo will see two versions of the same crate and reportmultiple 'crate-name' packages in this workspace. Usepath = ...in[workspace.dependencies]instead.
Apply the standard anchor v1 fixes to any vendored source (realloc → resize, trytovec → borsh::to_vec, CpiContext::new, etc.) as you go.
17. Migrate spl-token / spl-token-2022 / spl-associated-token-account direct dependencies [COMPILE]
spl-token 7.x, spl-token-2022 7.x, and spl-associated-token-account 6.x depend on solana-program 2.x. If your program has a direct [dependencies] entry for any of these crates, you'll see type mismatches because your workspace uses solana-program 3.x:
error[E0308]: mismatched types
expected `Pubkey` (solana-program 3.x)
found `__Pubkey` (solana-program 2.x, re-exported via spl-token)This also affects any import path through these crates:
use spl_token::solana_program::instruction::Instruction; // wrong Instruction type
use spl_token::ID; // wrong Pubkey typePreferred fix — migrate to the interface crates
The interface crates (spl-token-interface, spl-token-2022-interface, spl-associated-token-account-interface) are slim, solana-program-3.x-compatible crates that expose the IDs, instruction builders, and account types you need without dragging in the old SDK version.
If you depend on spl-token → migrate to spl-token-interface 2.0:
# Cargo.toml
spl-token-interface = "2.0" # replaces spl-token = "7.x"// Before
use spl_token::ID as TOKEN_PROGRAM_ID;
use spl_token::instruction::transfer;
// After
use spl_token_interface::ID as TOKEN_PROGRAM_ID;
use spl_token_interface::instruction::transfer;If you depend on spl-token-2022 → migrate to spl-token-2022-interface 2.1:
# Cargo.toml
spl-token-2022-interface = "2.1" # replaces spl-token-2022 = "7.x"// Before
use spl_token_2022::ID as TOKEN_2022_PROGRAM_ID;
// After
use spl_token_2022_interface::ID as TOKEN_2022_PROGRAM_ID;If you depend on spl-associated-token-account → migrate to spl-associated-token-account-interface 2.0:
# Cargo.toml
spl-associated-token-account-interface = "2.0" # replaces spl-associated-token-account = "6.x"// Before
use spl_associated_token_account::ID as ATA_PROGRAM_ID;
use spl_associated_token_account::get_associated_token_address;
// After
use spl_associated_token_account_interface::ID as ATA_PROGRAM_ID;
use spl_associated_token_account_interface::get_associated_token_address;Fallback — use anchor_spl re-exports
If you only need program IDs, ATAs, or account structs and don't call instruction builders directly, anchor_spl re-exports compatible versions and requires no extra dependency entry:
use anchor_spl::token::ID as TOKEN_PROGRAM_ID;
use anchor_spl::token_2022::ID as TOKEN_2022_PROGRAM_ID;
use anchor_spl::associated_token::ID as ATA_PROGRAM_ID;
use anchor_spl::associated_token::get_associated_token_address;
use solana_program::instruction::Instruction; // not via spl_tokengrep -rn "spl_token::\|spl_token_2022::\|spl_associated_token_account::" --include="*.rs" programs/
grep -rn "spl-token\|spl-token-2022\|spl-associated-token-account" --include="Cargo.toml" programs/What's New in v1
Worth adopting during migration:
Migration<'info, From, To>— safe account schema migrations between layouts.LazyAccount— heap-allocated read-only access, auto-optimized for unit-variant enums and empty arrays.- Relaxed seeds syntax — PDA seeds accept richer Rust expressions beyond literals and
.as_ref(). FnMutevent closures — event listeners now acceptFnMut, allowing mutable captures.- Generic
Program<'info>— usable without a type parameter for executable-only validation when the concrete program type is not statically known:pub program: Program<'info>. declare_program!withoutanchor_lang—anchor_clientalone is now sufficient for client-sidedeclare_program!usage; no need to pull inanchor_langas a dependency.- Owner re-checked on
.reload()—account.reload()now re-validates the account owner the same as initial load. Programs that previously reloaded accounts owned by a different program will now error. common::closeaccepts references — no need to call.to_account_info()at everycommon::close(...)call site.Ownersin prelude —anchor_lang::prelude::Ownersis re-exported; remove any manualusestatement for it.- Borsh 1.5.7 — both Rust and TypeScript Borsh implementations upgraded. Ensure your
borshentry inCargo.tomlis compatible. - Lifecycle hooks — add a
[hooks]section toAnchor.tomlto run shell commands atpre_build,post_build,pre_test,post_test,pre_deploy,post_deploy.