anchor: port every example to Anchor v2.0.0-rc.1 - #123
Merged
Conversation
Anchor v2 is a ground-up rewrite rather than a version bump: the crate is no_std and built on pinocchio, so handlers take `&mut Context<T>` instead of `Context<T>`, and the `#[program]` macro expands to `wincode` paths for instruction-data (de)serialization, which means `wincode` has to be a direct dependency of every program crate. The program body is otherwise unchanged, and `tests/test_hello.rs` needed no edits at all: `InstructionData`, `ToAccountMetas`, and `anchor_lang::solana_program::instruction::Instruction` all survive into v2. Verified with `cargo-build-sbf` (22 KB .so, down from ~180 KB on v1) and `cargo test`, both tests pass.
…nt, rent to Anchor v2
Four more of the stateless / system-account examples. The recurring edits are
the same ones hello-solana needed, plus a few v2 specifics:
- CPI account fields are handles now: `.to_account_info()` becomes
`.cpi_handle_mut()`, and `Program<System>` exposes `.address()` rather than
`.key()`.
- `Rent::minimum_balance` is deprecated in favour of `try_minimum_balance`.
- v2's `solana_program` compat shim has no `system_program` submodule, the
real module is at the crate root and exposes `ID`, not `id()`. That is the
only edit the tests needed.
- rent-example dropped borsh entirely: instruction data is wincode-encoded in
v2, so `AddressData` derives `wincode::Schema{Read,Write}` and the account
span comes from `SchemaWrite<BorshConfig>::size_of`. `BorshConfig` is
wincode's borsh-compatible wire format, so the 44-byte span is unchanged and
the test's size assertion still holds. Its fields are now `pub`, which lets
the test build the struct directly instead of round-tripping it through
hand-written borsh bytes.
- checking-accounts had to make its constraint fields `pub`: v2's derive no
longer reads them, so private unused fields trip dead_code.
All four build with cargo-build-sbf and pass their LiteSVM tests.
counter's `Counter { count: u64 }` is already a valid Pod layout, so it keeps
v2's default zero-copy `#[account]` backing and its test, which reads the
account as an 8-byte discriminator plus a little-endian u64, is unchanged.
transfer-sol's direct-lamport handler is the interesting one. v1 wrote through
`**account.try_borrow_mut_lamports()?`; v2 has no `RefCell` layer, so the
handler copies the `AccountView` (it is `Copy`, and a copy still points at the
same backing buffer) and calls `set_lamports`. The checked arithmetic stays in
the handler so the example keeps reporting its own `InsufficientFunds` /
`AmountOverflow` errors rather than the generic ones `Lamports::sub_lamports`
would raise.
Both build with cargo-build-sbf and pass their LiteSVM tests.
`User` holds a `String`, so it moves to `#[account(borsh)]` + `BorshAccount<User>`; `Pubkey` becomes `Address`, and the PDA seed reads `user.address().as_ref()`. This is also the first example to hit the wincode split: anchor-lang 2.0.0-rc.1 is built against wincode 0.5, but solana-address 2.7 moved to wincode 0.6, so `Address`'s `SchemaRead`/`SchemaWrite` impls belong to the version the `#[account(borsh)]` derive is not using and every bound fails. Pinning `solana-address = ">=2.6, <2.7"` unifies the graph on the 0.5 line. The pin needs the lower bound: a bare `<2.7` lets cargo satisfy the requirement by reusing the unrelated 1.1.0 already in the graph, and the 2.7 copy survives. Builds with cargo-build-sbf and passes its LiteSVM tests.
…gress) account-data, favorites, pda-rent-payer, program-derived-addresses, pyth, realloc, repository-layout and cross-program-invocation. All compile clean against anchor-lang 2.0.0-rc.1; SBF builds and LiteSVM runs are still going, so these are not yet confirmed green the way the earlier commits are. Notes on the less mechanical ones: - program-derived-addresses: `PageVisits` stays zero-copy, which means it needs explicit padding, bytemuck rejects implicit padding, so `u32 + u8` grows a named `[u8; 3]`. The test decodes the account with borsh and `try_from_slice` refuses trailing bytes, so its local struct spells the padding out too. - favorites: `String` + `Vec` state moves to `#[account(borsh)]` / `BorshAccount`. v2 has no `set_inner`, so the handler assigns through `DerefMut`, and no `init-if-needed` cargo feature, the constraint is always available. - realloc: the sub-constraints are `realloc_payer` / `realloc_zero` in v2, not `realloc::payer` / `realloc::zero`. - pyth: v2's `Owner` is `const OWNER: Address` rather than a method, which makes the vendored foreign-owned `PriceUpdateV2` much simpler, the manual `AccountSerialize`/`AccountDeserialize` impls are gone entirely, since `BorshAccount` drives wincode with the borsh-compatible config. The test takes `Clock` from `solana_clock` now; `anchor_lang`'s is pinocchio's on-chain type, which LiteSVM's `get_sysvar` does not accept. - cross-program-invocation: `idls/lever.json` is regenerated with the v2 CLI (built from the v2.0.0-rc.1 tag), since `hand` consumes it via `declare_program!`. `PowerStatus` uses `#[account(borsh)]` rather than the zero-copy default: zero-copy would force `is_on` to be `PodBool`, and the generated IDL renders `PodBool` as a plain `bool` alias, so `declare_program!` would regenerate a struct that is no longer `Pod`. Borsh keeps a real `bool` on both sides of the CPI.
program-derived-addresses: the test decodes the account with borsh, and `try_from_slice` rejects trailing bytes, so its local struct now mirrors the explicit `[u8; 3]` padding that zero-copy `PageVisits` grew. cross-program-invocation: `idls/lever.json` is regenerated with the v2 CLI. `PowerStatus` uses `#[account(borsh)]` rather than v2's zero-copy default, zero-copy would force `is_on` to be `PodBool` (bytemuck rejects `bool`, since only 0x00/0x01 are valid bit patterns), and the generated IDL renders `PodBool` as a plain `bool` alias, so `declare_program!` in `hand` regenerated a struct that was no longer `Pod`. Borsh keeps a real `bool` on both sides of the CPI and leaves the IDL shape identical to v1's.
…compile) Committing work-in-progress so it is not lost. `basics/` (the previous commit) is complete and green; these 38 projects are part-ported and 5 of them compile so far. Do not merge this commit on its own. Repo-wide v2 changes applied here, each found by compiling: - anchor-spl 2.0.0-rc.1 has no `idl-build` feature; IDL generation is driven entirely by anchor-lang's. This alone blocked 37 projects at resolution. - Extension CPI structs dropped `token_program_id`, the program comes from the `CpiContext` rather than a duplicated account field. - `Mint` moved from `anchor_spl::token` to `anchor_spl::mint`, and the namespaced `token::` / `mint::` constraints need those modules nameable in the file that uses them. - `Interface` is the one account wrapper that keeps a lifetime parameter, and it is always `Interface<'static, TokenInterface>`. - SPL account fields are behind accessors (`.amount()`, `.decimals()`) now that the structs are Pod. - Program state moves to `#[account(borsh)]` + `BorshAccount<T>`. v1 accounts were borsh-encoded, so this reproduces the on-chain layout byte for byte and keeps existing clients and tests valid, rather than making everything zero-copy and having to hand-pad every state struct to satisfy bytemuck. - anchor-lang's `compat` feature is enabled for the 13 programs that use the v1-shaped `error!` / `err!` / `pubkey!` macros; it exists for this migration. Known remaining work, which is not mechanical: - v2's typed CPI handles turn account aliasing into a compile error, so any handler passing one account into several CPI slots needs restructuring around a copied `AccountView` (the `CpiHandle::readonly` idiom used in create-token). - `associated_token` constraints now require sibling account field references. - compression/ vendors Bubblegum types with hand-written borsh `serialize` calls that have no wincode equivalent yet. CI still pins avm 1.1.2 deliberately: it should flip to 2.0.0-rc.1 in the commit that finishes these 38, since a v2 CLI cannot build v1 programs.
Continuing the compile-driven loop. Still in progress; not all of these compile. - `#[derive(AnchorSerialize, AnchorDeserialize)]` becomes the wincode derives plus `IdlType`, since v2 encodes instruction data with wincode rather than borsh. - v1's `#[instruction(discriminator = ...)]` is `#[discrim = ...]` in v2, and it takes a literal rather than a const expression, so the transfer-hook programs spell out the two SPL interface discriminators, each sha256(namespace)[..8]. - Arbitrary (non-one-byte) discriminators are only permitted on an interface program, so the seven transfer hooks are now `#[program(interface, program_id = ID)]`, which is what they always were. - The earlier AccountInfo rewrite over-reached into `#[derive(Accounts)]` structs; the v2 spelling there is `UncheckedAccount`, not the raw `AccountView`. Also recorded, because it bounds what is achievable on this RC: anchor-spl 2.0.0-rc.1 has no `extensions::*` init-constraint support at all. Its `MintInitParams` / `TokenAccountInitParams` cover only decimals, authority and mint, and none of anchor's own v2 test programs use extension init constraints. Six examples are built entirely on that constraint form and cannot be ported by translation alone, they need the mint creation hand-written (allocate with ExtensionType space, extension-init CPI, then InitializeMint2): tokens/token-extensions/group tokens/token-extensions/metadata tokens/token-extensions/mint-close-authority tokens/token-extensions/permanent-delegate tokens/token-extensions/transfer-hook/allow-block-list-token tokens/token-extensions/transfer-hook/hello-world
Correcting my earlier framing: these were never blocked upstream. anchor-spl 2.0.0-rc.1 is only missing the *constraint shorthand* (`extensions::close_authority::authority = payer` on an `#[account(init, ...)]` field). Every underlying piece is present, the extension-init CPIs live in `token_2022_extensions`, and `InitializeMint2` comes from pinocchio-token-2022. The repo already had the answer: `non-transferable` has always created its mint by hand, with a comment saying there is no anchor constraint for that extension. So these six now follow that same established pattern, allocate with `ExtensionType::try_calculate_account_len`, initialize the extension, then initialize the mint data: mint-close-authority, permanent-delegate, group, metadata, transfer-hook/hello-world `group`'s mint is a PDA, so it signs its own creation via `with_signer`. Also in this commit: - Transfer hooks: v2's `AccountView::try_borrow_mut` replaces v1's `try_borrow_mut_data`, and it needs a mutable view, `AccountView` is `Copy` and a copy still points at the same backing buffer, so the borrow writes through to the real account. - Where one account fills several CPI slots (payer as payer + mint authority + update authority), the read-only slots are built with `CpiHandle::readonly` over a copied view. v2's typed handles make aliasing a compile error rather than latent UB, so this is the shape the framework wants. - The `system_program::ID` / `anchor_lang::Address` test rewrite now covers tokens, finance and compression as well as basics. Token tier is at 12 of 27 compiling, up from 6.
The last two of the six extension examples. metadata: the mint is created by hand with the MetadataPointer extension (pointing at the mint itself, which is where Token-2022 stores the metadata). v2's `TokenMetadataInitialize` / `UpdateField` / `UpdateAuthority` structs drop the `program_id` slot, the program comes from the `CpiContext`, and `token_metadata_update_authority` now takes a plain `Option<&Address>` and does the `OptionalNonZeroPubkey` conversion itself. allow-block-list-token: three extensions on one mint (PermanentDelegate, TransferHook, MetadataPointer), so the allocation sizes for all three and initializes each before `InitializeMint2`. Its lamport top-up after the metadata write goes through the `system_program::transfer` CPI helper rather than a hand-rolled `invoke`. Also dropped the `extensions::*` *validation* constraints (as opposed to the init ones) from metadata's update_field/remove_key/update_authority, v2 has no such constraint, and Token-2022 checks the metadata pointer itself when the CPI runs.
v2's typed CPI handles make aliasing a compile error rather than latent UB, so
any handler passing one account into both a writable and a read-only CPI slot
stops building. That pattern recurs across the token and finance examples, so
this adds a fixer that works per function body: when a field is used both ways
inside the same function, it binds a copied `AccountView` and rebuilds the
read-only slots over it with `CpiHandle::readonly`. `AccountView` is `Copy` and
a copy still points at the same backing buffer, so behaviour is unchanged.
Other fixes in this pass:
- Files using the brace-form `use { .., anchor_spl::{..} }` never got the
`mint` / `token` module imports the namespaced constraints expand to; the
earlier pass only handled a standalone prelude import. Deduplicated where
that then collided with an existing `mint::{self, ..}`.
- `create_metadata_accounts_v3` takes four arguments in v2, not five,
`update_authority_is_signer` moved into the accounts struct, and the optional
`rent` account is gone.
- escrow's `shared.rs` helpers took `&AccountView` and called `cpi_handle_mut`
on it; building a writable handle needs a `&mut`, so they now take the view by
value (it is `Copy`) and use `CpiHandleMut::writable` / `CpiHandle::readonly`
explicitly, with the accounts themselves passed as `&mut`.
escrow is the important one, it exposed a real bug in my earlier approach. Passing one account into several CPI slots by copying its `AccountView` gets past the *compile-time* exclusivity check, but v2 still tracks borrows at runtime: a data account (`Account` / `BorshAccount` / `InterfaceAccount`) holds a live borrow on its buffer, so the CPI's own borrow fails with `AccountBorrowFailed`. escrow built cleanly and then failed two of its four tests that way. The correct mechanism is `release_borrow()` before the CPI and `reacquire_borrow_mut()` after, `reacquire_borrow_mut` re-runs the load-time owner and discriminator checks, because a CPI in the release window could have mutated either. escrow now does that around the vault transfer and close, and passes 4/4. The copied-view shortcut stays correct where the account holds no data borrow (`Signer`, `UncheckedAccount`, `Program`), which is most of its uses. Other fixes: - v2 takes a byte array directly in a seeds list and binds it itself; `id.to_le_bytes().as_ref()` produced a temporary that died before the derive used it (E0716). Applied repo-wide. - `associated_token::mint` resolves a sibling account field, not a field read off another account. - betting-market's `Event` was ambiguous, the crate root glob-re-exports the state struct while the prelude brings in anchor's `Event` trait. An explicit `use crate::state::Event;` resolves it. - The shared token helpers in escrow and betting-market took `&AccountView` and called `cpi_handle_mut` on it; a writable handle needs `&mut`, so they take the view by value (it is `Copy`) and build handles explicitly.
…et compile Continuing the port. 22 of 39 compile now, up from 17. The oracle programs needed a `last_restart.rs`: anchor-lang v2 is built on pinocchio, which ships only the Clock and Rent sysvars, so `LastRestartSlot` is declared locally and read through the raw `sol_get_sysvar` syscall. This mirrors what the Quasar variants of these same examples already do, the repo hit the identical gap in quasar-lang. Other shapes found this pass: - `Context` takes one accounts type and no user lifetime in v2; the account model is static-scoped, so `Context<'info, T<'info>>` becomes `Context<T>`. - `remaining_accounts()` is a fallible method (it walks the account cursor), and it takes `&mut self`, so anything holding a borrow of `context.accounts` across the call has to copy what it needs first. - `AccountView::owner` is a method, not a field; `Mint::supply` and `decimals` are accessors since the struct is Pod. - v2 only generates a `program` marker module from `declare_program!`, so a sibling program account is an `UncheckedAccount` validated by its constraint. - Accounts pulled out of `remaining_accounts` are plain `AccountView`s; the CPI slots want typed handles, so they are bound by value (`AccountView` is `Copy`) and wrapped with `CpiHandle::readonly` / `CpiHandleMut::writable` to match each slot's mutability. - `AssetConfig::load_checked` decodes with wincode under `BorshConfig` now, checking the discriminator explicitly first. betting-market and token-fundraiser build but still fail some tests, the runtime borrow rules need more work there, tracked in the next commit.
More applications of the release/reacquire rule, now that its shape is clear. token-fundraiser's `fundraiser` PDA signs the vault transfer, close and refund CPIs. It is a data account, so its borrow is released across each CPI and taken back afterwards. refund.rs also asked `maker`, a read-only account there, for a writable handle purely to read its address, which panics in v2; it reads `*maker.address()` instead. vault-strategy: `.address()` returns a reference that keeps the account borrowed, which blocked the `&mut` uses further down, and `remaining_accounts()` takes `&mut Context` so it has to be collected before the per-account views borrow `context.accounts`. Measured status of the finance tier: escrow 5/5, betting-market 9/9, token-fundraiser 17/17 passing; prop-amm, perpetual-futures and vault-strategy build; token-swap, lending and order-book do not yet compile.
Two fixes, one of them a regression of my own making. The codemod that dropped `<'info>` from handler signatures (v2's account model is static-scoped, so the lifetime is gone) also stripped `<'a>` from free functions that genuinely use it, `reserve_signer_seeds` borrows its arguments into the returned seed array. Restored. `refresh_obligation` loaded a reserve out of remaining_accounts with `Account::<Reserve>::try_from`. `Reserve` is borsh-backed in v2, so it now checks the discriminator explicitly and reads the payload with wincode under `BorshConfig`, the same shape vault-strategy's `AssetConfig::load_checked` uses.
A v2 CLI cannot build v1 programs and vice versa, so this pin can only move once every example has been ported, which is now the case for the source. The cache key moves with it so the old toolchain is not restored. 2.0.0-rc.1 is a pre-release, so avm needs it named explicitly rather than resolved as latest. The avm source also moves from coral-xyz/anchor to solana-foundation/anchor, matching the project's rename.
Two fixes the SBF sweep surfaced. The compression programs vendor Bubblegum types that derive `BorshSerialize`. borsh 1.x only exports its derive macros when the `derive` feature is named, so the three manifests ask for it explicitly. `error!` is only available under anchor-lang's `compat` feature, and my earlier edits had reintroduced it. The error enums convert into `Error` on their own, so the call sites use them directly, with an explicit `.into()` only where the `map_err` result is a tail expression, since `?` already performs the conversion. (The first pass added `.into()` everywhere, which made the target ambiguous at the `?` sites and broke allow-block-list-token; scoped back.)
Three renames v2 made to the zero-copy surface, applied repo-wide: - The `zero` account constraint is `zeroed`. - `#[account(zero_copy(unsafe))]` is just `#[account]`, v2's default account backing is already zero-copy, and the macro rejects any other argument. - v1's `AccountLoader<'info, T>` is gone. v2 has an `AccountLoader` but it is an internal account cursor, unrelated; the zero-copy account type is `Account<T>`, which derefs straight to `T`, so `load()` / `load_mut()` become plain borrows. Also generalised the borsh decode helper that lending needed: v2 has no `Account::try_from(&AccountView)`, so loading a borsh-backed account out of remaining_accounts checks the discriminator and reads the payload with wincode under `BorshConfig`. order-book is not finished, 30 errors remain, mostly around `load_init` and state types my blanket borsh conversion moved off Pod that this program wants zero-copy.
Everything learned porting the examples, in one place, so the next person (or the next session) does not rediscover it from compiler errors. The section that matters most is "Borrows across CPIs", it is the only rule in the document that the compiler will not catch. Copying an `AccountView` to get past v2's compile-time aliasing check is correct for `Signer` and `UncheckedAccount`, and silently wrong for data accounts, which hold a live borrow the runtime rejects with `AccountBorrowFailed`. That cost several programs a build-clean-then-fail-tests cycle each, along with its three corollaries: dereferencing after release panics (including the derive's own use after the handler returns), release and reacquire must sit on the same branch, and a read-only account cannot be reacquired at all. The rest is a rename table, the account-backing rules (default to `#[account(borsh)]`, it reproduces v1's on-chain layout byte for byte, and only keep zero-copy where a program deliberately wants it), the anchor-spl changes including the absent `extensions::*` constraints, and the handful of test-side imports that move.
From the v2 release notes, which the published changelog does not carry, its last entry is 1.0.0, and the tagged CHANGELOG.md lists a single CLI change under 2.0.0-rc.1. The consequential one: `has_one` is deprecated in favour of the more general `address` constraint, and the check moves off the owning account onto the sibling it names. It is only a deprecation, but `rust.yml` runs `cargo clippy -- -D warnings`, so every remaining use fails CI, escrow alone trips five. 269 uses across the repository. Also recorded: the v2-only primitives a mechanical port never produces but which are usually the right answer when a translation fights Pod, `Slab<H, Item>`, `PodVec<T, MAX>`, the alignment-1 Pod integer wrappers, `#[pod_wrapper]` and `Nested<T>`, plus the `guardrails` / `const-rent` / `compat` feature flags and the new debugger and profiling tooling.
The repo-wide `Pubkey` -> `Address` rename also rewrote the *path* segment, so `solana_program::pubkey::Pubkey` became `solana_program::pubkey::Address`, a module that does not exist outside the `compat` feature. Twelve test files were importing it and failing to build even though their programs compiled. `Address` lives at the anchor_lang crate root in v2, so the import moves there. Also removed the duplicate `Clock`: the earlier pass added `solana_clock::Clock` alongside the compat `solana_program::clock::Clock` that was already there.
Enable solana-address's borsh feature so the vendored Bubblegum types keep their BorshSerialize derives, bind remaining_accounts() once per handler now that it returns an owned vec, and erase CpiHandleMut into CpiHandle for the raw invoke/invoke_signed calls.
v2's invoke/invoke_signed match each instruction account meta to the next handle in order, so the program account is dropped from the list, an account that fills two meta slots supplies two handles, and read-only metas take read-only handles. Release the vault's data borrow before it signs, and read the v2 error code as discriminant + 6000 in the vault tests. All three compression programs now pass: cnft-burn 2/2, cnft-vault 5/5, cutils 2/2.
v2 deprecates has_one, and rust.yml runs `cargo clippy -- -D warnings`, so every remaining use is a hard CI failure. The check moves off the owning account and onto the sibling it names: `has_one = maker` on `offer` becomes `address = offer.maker` on `maker`, carrying any `@ Error` across. Forward references are fine, the derive resolves the constraint after all accounts are loaded, so the sibling may be declared before its owner. 152 constraints across 59 files, plus the prose that named the old spelling.
v2's `#[program(interface, ...)]` only generates a CPI client, no dispatch, so a program declared that way builds to a ~900-byte object with no entrypoint and fails to load. An executable `#[program]` does dispatch, but its custom discriminators are limited to one byte, which the transfer-hook interface's eight-byte `Execute` value cannot use. So the hook programs build with `no-entrypoint`, anchor then exports its dispatch as `__anchor_dispatch`, and supply an entrypoint that swaps `Execute` for the handler's own discriminator before delegating. The payload behind it is identical. Also ports allow-block-list-token the rest of the way: anchor-spl v2 has no token_2022 features, TokenMetadataUpdateField lost its program_id field, and the wallet account is decoded by hand now that Account::try_from is gone. Its tests pass.
Same treatment as allow-block-list-token, the interface discriminators are mapped onto handlers by a hand-written entrypoint instead of `#[discrim]`, which an executable `#[program]` limits to one byte. check_is_transferring also drops to a shared borrow: the account already holds one, so try_borrow_mut on a copy of the view was rejected at runtime with AccountBorrowFailed. Reading `transferring` never needed write access.
transfer-cost: bring the `token` module into scope so the token::mint / token::authority constraints resolve, read writability off the AccountView rather than asking a read-only account for a writable handle, and drop a stale `solana_program::pubkey::Address` import. whitelist, counter, hello-world, account-data-as-seed and transfer-cost all pass.
Deref the address before require_keys_eq, read the source token account through a shared borrow, take the extra-metas buffer off the AccountView, and pass bumps by reference so the handler does not move out of `context`. configure_admin's bootstrap passes one key as both `admin` and `new_admin`. v2 rejects an account that appears twice while any of its slots is in the mutable mask, and it flags both indices, so both slots carry `unsafe(dup)`. All seven transfer-hook programs now pass.
The `session-keys` crate cannot be used from v2: its `Session` derive requires `Option<Account<'info, SessionToken>>`, and `SessionToken` is not `Pod`, so v2's zero-copy `Account<T>` cannot hold it either. The token is four addresses of borsh data owned by the session-keys program, so the program reads that layout itself, checking owner, discriminator and PDA, and spells out the `#[session_auth_or]` fallback in the handler. The lesson and the security warning both survive. Also fixes two CPIs that named the token program where the system program belongs, and lines the raw invoke handle lists up with their metas: the metadata-pointer init takes only the mint, writable, and metadata initialize names the mint and the authority twice each.
`useless_conversion` on `ProgramError::X.into()`, in v2 the error type is
already a ProgramError, and the parentheses left behind where `set_inner(X {
.. })` became `*ctx.accounts.foo = X { .. }`.
Unused `#[instruction(...)]` bindings take an underscore, and two borrows that the compiler immediately dereferences are dropped.
The instruction args are used by a `space` constraint, but v2's derive also binds them on a path that does not, so the affected structs carry an `#[allow(unused_variables)]` rather than losing the binding. Plus two needless borrows and four more `ProgramError::X.into()` conversions that are no-ops now the error type is already a ProgramError.
Account fields the derive reads only from generated code are declared `pub`, matching every other example, and the three structs whose `#[instruction(...)]` binding is generated in more than one item carry a module-level allow.
An earlier rename had merged two use paths into a nonexistent `solana_pythexample`, and LiteSVM's get_sysvar needs the host-side Clock. 3/3 passing.
The field is on CreateMetadataAccountsV3, not CreateMasterEditionV3.
Minting or sending to yourself makes the authority and the recipient the same account, which v2 rejects while any of the duplicated slots is in the mutable mask; `unsafe(dup)` takes it out of that mask while keeping it writable. transfer-tokens' read-only mint slot also moves to the wrapper's own `cpi_handle()`, since a hand-built handle over a copy of the view keeps the runtime borrow check on and the account's borrow is live.
A `mut` data account is marked exclusively borrowed, so `try_borrow()` on it is rejected: transfer-fee's mint drops its `mut` (the withheld fee accrues on the destination account, not the mint), while interest-bearing and metadata genuinely write theirs and read through the exclusive borrow they hold. metadata's two hand-built invokes also line their handles up with the instruction's metas, the program account is not one of them, and `remove_key` names the mint writable.
…straints Required when the token program is an `Interface`; without it the init CPI is rejected with InvalidArgument.
perpetual-futures 26/26 and prop-amm 22/22. `Box` does not forward `cpi_handle_mut` to the inner type's borrow-releasing override, so every CPI in these two was rejected with AccountBorrowFailed.
Boxed accounts take `to_cpi_handle*`, the strategy PDA releases its data borrow while it signs, and the hand-built handles over copied AccountViews go back to the wrappers' own, a mutable data account is marked exclusively borrowed, so a copy trips the runtime check the wrapper's handle relaxes.
Four sites reached for `unsafe` where v2 has a safe route: - `token-extensions/interest-bearing` read the InterestBearingConfig extension through `borrow_unchecked`. anchor-spl exposes `TokenInterfaceAccountExtensions::get_extension::<T>()`, which parses the TLV through the borrow the wrapper already holds and checks Token-2022 ownership on the way. `check_mint_data` splits into `check_rate_authority` with two call routes: `initialize`'s mint is a `Signer`, which holds no data borrow, so it reads the buffer directly; `update_rate`'s is an `InterfaceAccount<Mint>` and uses the accessor. - `token-extensions/metadata` could not use that accessor, which is bounded on `Pod`, because `TokenMetadata` is variable-length. The mint is now an `UncheckedAccount` in the derive and is loaded as an `InterfaceAccount<Mint>` in the handler. `AnchorAccount::load` is safe, runs the same validation the derive would have run, and registers a shared borrow rather than an exclusive one, which leaves room for the TLV read. The `mut` stays so the IDL still marks the account writable. - `lending`, `prop-amm` and `perpetual-futures` read the LastRestartSlot sysvar through `solana-define-syscall` into a `MaybeUninit`. Replaced with `pinocchio::sysvars::get_sysvar`, a safe wrapper over the same syscall that is a no-op off-chain, so the `cfg` split goes away with it. - `transfer-tokens`' `transfer` carried `unsafe(dup)` on the sender for an aliasing case that never occurs: the test funds a fresh keypair as the recipient. Restored to `mut`, so the duplicate-account check stays live. What is left is the transfer-hook entrypoints, where `unsafe extern "C"` is the loader ABI, and order-book's `load_mut` on remaining_accounts, which has no safe equivalent. Both are now documented with why. docs/anchor-v2-migration.md gains the safe alternatives, the reasoning behind the duplicate-mutable-account rule, and an inventory of what still needs `unsafe`. Tests: metadata 2, interest-bearing 2, transfer-tokens 2, token-minter 3, lending 24, perpetual-futures 26, prop-amm 22, order-book 28.
mikemaccana
force-pushed
the
claude/anchor-v2-migration-d5hkh4
branch
from
August 19, 2026 19:40
4a6c5f9 to
e849dfc
Compare
The Solana skill bans em-dashes in code comments, and this repository's own CONTRIBUTING.md bans them in prose. The v2 port added 70 across 65 files. The 70 sites are 39 distinct comment texts, the most repeated seven times, so these are hand-written rewrites applied as fixed strings rather than a character substitution. An em-dash here almost always joined a consequence to its cause, so most become the comma the sentence already implied; a few become a colon where the second clause restates the first, parentheses where the dashes bracketed an aside, or a full stop where the clause stands alone. Comments only. The diff contains no non-comment lines. (cherry picked from commit 1d540d4)
The port changed code that ten documents still described in its pre-port form. Seven claimed an Anchor 1.x version, including the repository README, which said every example builds on Anchor 1.1. They say 2.0.0-rc.1 now, which is what `.github/workflows/anchor.yml` installs. Nine lines across five READMEs documented a `has_one` constraint. All 151 uses in the Anchor programs moved to `address` on the sibling field, so each line now names the constraint the code actually carries: `address = offer.maker`, `address = market.fee_vault`, `address = lending_market.owner`, `address = vault.authority`, `address = config.admin`. Every one of those greps in the source. The 118 remaining `has_one` uses are all in Quasar crates, where the constraint still exists, so their documentation is unchanged. `tokens/nft-operations` said the instructions sysvar ID was defined locally because it moved in Anchor 1.0. `verify_collection.rs` says why: pinocchio, which anchor-lang v2 is built on, does not re-export it. `basics/pyth` needed more than a version bump. It documented, in the README and again in `lib.rs`, that `pyth-solana-receiver-sdk` 1.2.0 builds against anchor-lang 0.32 and pulls a pythnet-sdk still on borsh 0.10. The SDK is at 2.0.0 now, on anchor-lang 1.0.2 and borsh 1.5.3, so that blocker is fixed upstream and the error text and issue link no longer describe anything. The vendored type is still needed, for the simpler reason that 1.0.2 and 2.0.0-rc.1 are different account models, and both places say that instead. `README.md`'s "since Anchor 1.0" is untouched: it dates when `anchor init` began scaffolding LiteSVM, which is still true. (cherry picked from commit b24acb7)
The entry claimed "all 152 uses across 59 files" of `has_one` moved to `address`. Measured against the base commit, the Anchor programs carried 161 across 66 files, and every one moved. "All" was also wrong without a scope: 43 Quasar crates still use `has_one`, correctly, because Quasar is not Anchor v2. The 55 examples and 304 tests in the opening line are right, re-derived here rather than taken on trust: `find -name Anchor.toml` and `find -type d -name anchor` both give 55, with no project counted by one and missed by the other, and the test total comes from summing the sweep per project. Also removes the five em-dashes on added lines, which CONTRIBUTING.md bans. (cherry picked from commit defd44a)
The guide is new in this branch and was written before the audit, so it broke four of the skill's rules at once. They overlap in the same paragraphs, so this is one pass rather than four. The four tables become nested bullet lists. Each was a two-column key and description table, which is a definition list. The 24-row signatures table already existed in bullet form in the plugin's ANCHOR-V2.md, and that rendering is better than the table was: it merges related rows and carries detail the cells had no room for, such as `remaining_accounts()?` returning an owned `Vec<AccountView>` and therefore needing to be called before anything borrows `ctx.accounts`. Ported, with the four rows it does not cover added back. 42 lines carried an em-dash. Each is rewritten rather than character-swapped: most become the comma the sentence already implied, some a colon where the second clause restates the first, a few parentheses or a full stop. Terminology: `on-chain` and `off-chain` are one word, and `Token-2022` is the Token Extensions Program. Nine headings were bare nouns (`Account backing`, `Tests`, `anchor-spl`) that say nothing read alone out of a table of contents, which is how a reader meets them. They name what the section gives you now. The two in-document links to `#borrows-across-cpis` follow the renamed heading, so they still resolve. (cherry picked from commit e849dfc)
mikemaccana
force-pushed
the
claude/anchor-v2-migration-d5hkh4
branch
from
August 19, 2026 23:11
e849dfc to
e67c4a1
Compare
This was referenced Aug 19, 2026
main's `lending: make slots-per-year configuration, not a constant` landed after this branch started. The program and test changes merge cleanly with the port; two import lines conflicted only because the port had reordered them. `rejects_zero_slots_per_year` needed porting. It asserts on the failure message containing "InvalidConfig", and v2's `#[error_code]` does not log variant names, so the test could never pass here. It now uses this suite's `assert_program_error!`, which checks the numeric custom code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports all 55 Anchor examples from v1 to 2.0.0-rc.1, and nothing else. Work that was previously mixed in here has moved out: #125 (Quasar), #126 (workspace lint coverage), #127 (compression tests use solana-kite).
Anchor v2 is a ground-up rewrite rather than a version bump. The crate is
no_stdand built on pinocchio, the account model is static-scoped, borsh is replaced by wincode, and accounts are zero-copy by default.What the port had to change
Types and signatures.
PubkeyisAddress.AccountInfo<'info>isAccountView. The'infolifetime is gone fromSigner,Program<T>and theAccountsstructs, withInterface<'static, TokenInterface>the one wrapper that keeps one. Handlers take&mut Context<T>..key()is.address(),.to_account_info()is.cpi_handle()or.cpi_handle_mut(), andctx.remaining_accountsis a fallible method returning an ownedVec.Account state. v2's
#[account]is zero-copy and needs aPodlayout, so state holdingString,Vecor enums moves to#[account(borsh)]withBorshAccount<T>. Fixed-layout state that stays zero-copy carries explicit padding. Every program crate needswincodeas a direct dependency, because the#[program]macro expands towincodepaths.has_oneis deprecated, and this repository'srust.ymlrunscargo clippy -- -D warnings, so every remaining use is a hard CI failure. All 161 uses across 66 files move to theaddressconstraint on the sibling field they named.Borrows across CPIs, which is the one rule the compiler does not catch. A data account holding a live borrow cannot be handed to a CPI, so it needs
release_borrow()andreacquire_borrow_mut()around the call. On aBoxed account,to_cpi_handle_mut()andcpi_handle_mut()both compile but only the first releases the borrow.Transfer hooks. v2's
#[program(interface, ...)]generates a CPI client and no dispatch, so the seven hook examples supply their own entrypoint that swaps the SPL interface discriminator for the handler's before delegating.unsafeis avoidable almost everywhere. anchor-spl'sget_extensionreads a Token Extensions mint through the borrow the wrapper already holds;pinocchio::sysvars::get_sysvaris a safe wrapper over the syscall the three finance programs need forLastRestartSlot. What is left is the transfer-hook entrypoints, whereunsafe extern "C"is the loader ABI, and order-book'sload_mutonremaining_accounts, which has no safe equivalent..github/workflows/anchor.ymlinstalls 2.0.0-rc.1, sinceanchor buildunder a v2 CLI will not build v1 programs.Documentation
docs/anchor-v2-migration.mdcollects every difference the port ran into, ordered by how often it bites, and leads with the borrow rule because it is the only one tests catch rather than the compiler.Ten documents that described the pre-port code are corrected: seven claimed an Anchor 1.x version, including the repository README, and nine lines across five READMEs documented
has_oneconstraints the code no longer carries.basics/pythneeded more than a version bump: it documented an upstreampyth-solana-receiver-sdkborsh incompatibility that has since been fixed, so both its README andlib.rsnow give the real reason the type is vendored.Verification
All 55 Anchor projects build and pass: 304 tests.
cargo fmt --checkandcargo clippy -- -D warnings -A clippy::diverging_sub_expressionare clean across the workspace.