diff --git a/.github/workflows/anchor.yml b/.github/workflows/anchor.yml index 24c1ab449..1789e0749 100644 --- a/.github/workflows/anchor.yml +++ b/.github/workflows/anchor.yml @@ -236,7 +236,7 @@ jobs: return 1 fi - # Sync program IDs (Anchor 1.0+ requires keypair and declare_id! to match) + # Sync program IDs (Anchor requires the keypair and declare_id! to match) anchor keys sync # Update IDL address fields to match the synced keys. diff --git a/.github/workflows/kani.yml b/.github/workflows/kani.yml index 3b7181e89..32f30ead7 100644 --- a/.github/workflows/kani.yml +++ b/.github/workflows/kani.yml @@ -64,6 +64,17 @@ jobs: steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + # Each proof crate declares its own `[workspace]`, so the repository-wide + # `cargo fmt` and `cargo clippy` jobs in rust.yml never see it. Lint here + # instead, or these crates drift. + - name: Enforce formatting + working-directory: finance/${{ matrix.program }}/kani-proofs + run: cargo fmt --check + - name: Linting + working-directory: finance/${{ matrix.program }}/kani-proofs + run: cargo clippy --all-targets -- -D warnings - name: Run unit tests working-directory: finance/${{ matrix.program }}/kani-proofs run: cargo test diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 52c1d4f2a..89496edd2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,5 +44,5 @@ jobs: with: components: clippy - name: Linting - # Allow diverging_sub_expression: false positive from Anchor 1.0's #[program] macro expansion + # Allow diverging_sub_expression: false positive from Anchor v2's #[program] macro expansion run: cargo clippy -- -D warnings -A clippy::diverging_sub_expression diff --git a/.github/workflows/solana-asm.yml b/.github/workflows/solana-asm.yml index fbe781d0a..9cb0e8b7e 100644 --- a/.github/workflows/solana-asm.yml +++ b/.github/workflows/solana-asm.yml @@ -203,7 +203,10 @@ jobs: # ELF, which requires litesvm >= 0.13 (Agave 4.0) to load; the # workspace pins litesvm 0.13.1. If a future sbpf change breaks # loading, pin a revision here with --rev. - cargo install --git https://github.com/blueshift-gg/sbpf.git + # + # The package name is required: the repository now also ships an + # `xtask` binary, and cargo refuses to guess between two binaries. + cargo install --git https://github.com/blueshift-gg/sbpf.git sbpf - name: Setup Solana Stable uses: heyAyushh/setup-solana@v5.9 with: diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index bab054b61..81369c340 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -28,3 +28,24 @@ jobs: # --ignore-workspace: only install root deps, not all 94 subprojects - run: pnpm install --frozen-lockfile --ignore-workspace - run: pnpm run check + + # The only TypeScript application in the repository. Biome above formats and + # lints it, but nothing was compiling it, so a client that no longer matched + # the program's IDL would have gone unnoticed. `verify` runs the client's + # instruction encoding, account decoding and PDA derivation against the + # committed IDL, offline, with no validator. + vault-strategy-app: + name: Vault strategy app + runs-on: ubuntu-latest + defaults: + run: + working-directory: finance/vault-strategy/anchor/app + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: pnpm install --frozen-lockfile + - run: pnpm run typecheck + - run: pnpm run verify diff --git a/basics/close-account/anchor/README.md b/basics/close-account/anchor/README.md index b3f932537..fba83a7c8 100644 --- a/basics/close-account/anchor/README.md +++ b/basics/close-account/anchor/README.md @@ -9,10 +9,13 @@ Two [instruction handlers](https://solana.com/docs/terminology#instruction-handl init, payer = user, space = User::DISCRIMINATOR.len() + User::INIT_SPACE, - seeds = [b"USER", user.key().as_ref()], - bump, + seeds = [ + b"USER", + user.address().as_ref(), + ], + bump )] - pub user_account: Account<'info, User>, + pub user_account: BorshAccount, ``` See [`programs/close-account/src/instructions/create_user.rs`](programs/close-account/src/instructions/create_user.rs). @@ -22,11 +25,14 @@ Two [instruction handlers](https://solana.com/docs/terminology#instruction-handl ```rust #[account( mut, - seeds = [b"USER", user.key().as_ref()], + seeds = [ + b"USER", + user.address().as_ref(), + ], bump = user_account.bump, close = user, // close account and return lamports to user )] - pub user_account: Account<'info, User>, + pub user_account: BorshAccount, ``` See [`programs/close-account/src/instructions/close_user.rs`](programs/close-account/src/instructions/close_user.rs). diff --git a/basics/counter/anchor/README.md b/basics/counter/anchor/README.md index b4afefe53..1ba5dff87 100644 --- a/basics/counter/anchor/README.md +++ b/basics/counter/anchor/README.md @@ -1,6 +1,6 @@ # Counter (Anchor) -Increment a global counter stored in a [PDA](https://solana.com/docs/terminology#program-derived-address-pda). [Anchor](https://solana.com/docs/terminology#anchor) adds an explicit `initialize` handler that the native variant handles differently. +Increment a global counter stored in a [PDA](https://solana.com/docs/terminology#program-derived-address-pda). [Anchor](https://solana.com/docs/terminology#anchor) adds an explicit `initialize_counter` handler that the native variant handles differently. See also: the [repository catalog](../../README.md). diff --git a/basics/cross-program-invocation/anchor/idls/lever.json b/basics/cross-program-invocation/anchor/idls/lever.json index a8ace89ce..3ec862d79 100644 --- a/basics/cross-program-invocation/anchor/idls/lever.json +++ b/basics/cross-program-invocation/anchor/idls/lever.json @@ -9,16 +9,7 @@ "instructions": [ { "name": "initialize", - "discriminator": [ - 175, - 175, - 109, - 31, - 13, - 152, - 155, - 237 - ], + "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], "accounts": [ { "name": "power", @@ -39,16 +30,7 @@ }, { "name": "switch_power", - "discriminator": [ - 226, - 238, - 56, - 172, - 191, - 45, - 122, - 87 - ], + "discriminator": [226, 238, 56, 172, 191, 45, 122, 87], "accounts": [ { "name": "power", @@ -66,16 +48,7 @@ "accounts": [ { "name": "PowerStatus", - "discriminator": [ - 145, - 147, - 198, - 35, - 253, - 101, - 231, - 26 - ] + "discriminator": [145, 147, 198, 35, 253, 101, 231, 26] } ], "types": [ diff --git a/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml b/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml index ff89b4f26..c61b932ca 100644 --- a/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml +++ b/compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml @@ -32,7 +32,7 @@ wincode = { version = "0.5", features = ["derive"] } # vendored Bubblegum types need; pinocchio re-exports this same type. solana-address = { version = ">=2.6, <2.7", features = ["borsh"] } # mpl-bubblegum and spl-account-compression removed: they depend on solana-program 2.x -# which is incompatible with Anchor 1.0's solana 3.x types. CPI calls are built manually +# which is incompatible with Anchor v2's solana 3.x types. CPI calls are built manually # using raw invoke() with hardcoded program IDs and discriminators. borsh = { version = "1", features = ["derive"] } diff --git a/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml b/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml index 56ccfe2a9..3f311392e 100644 --- a/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml +++ b/compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml @@ -32,7 +32,7 @@ wincode = { version = "0.5", features = ["derive"] } # vendored Bubblegum types need; pinocchio re-exports this same type. solana-address = { version = ">=2.6, <2.7", features = ["borsh"] } # mpl-bubblegum and spl-account-compression removed: they depend on solana-program 2.x -# which is incompatible with Anchor 1.0's solana 3.x types. CPI calls are built manually +# which is incompatible with Anchor v2's solana 3.x types. CPI calls are built manually # using raw invoke_signed() with hardcoded program IDs and discriminators. borsh = { version = "1", features = ["derive"] } diff --git a/compression/cutils/anchor/programs/cutils/Cargo.toml b/compression/cutils/anchor/programs/cutils/Cargo.toml index ce870ee74..2b77af9cd 100644 --- a/compression/cutils/anchor/programs/cutils/Cargo.toml +++ b/compression/cutils/anchor/programs/cutils/Cargo.toml @@ -32,7 +32,7 @@ wincode = { version = "0.5", features = ["derive"] } # vendored Bubblegum types need; pinocchio re-exports this same type. solana-address = { version = ">=2.6, <2.7", features = ["borsh"] } # mpl-bubblegum and spl-account-compression removed: they depend on solana-program 2.x -# which is incompatible with Anchor 1.0's solana 3.x types. CPI calls are built manually +# which is incompatible with Anchor v2's solana 3.x types. CPI calls are built manually # using raw invoke() with hardcoded program IDs and discriminators. Bubblegum types # (MetadataArgs, LeafSchema, etc.) are re-implemented in bubblegum_types.rs. borsh = { version = "1", features = ["derive"] } diff --git a/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs b/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs index 176ec4c65..6a37755f4 100644 --- a/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs +++ b/compression/cutils/anchor/programs/cutils/src/bubblegum_types.rs @@ -1,7 +1,7 @@ -/// Re-implementation of mpl-bubblegum types using borsh 1.x and Anchor 1.0's Address. +/// Re-implementation of mpl-bubblegum types using borsh 1.x and Anchor v2's Address. /// /// mpl-bubblegum 2.1.1 depends on solana-program 2.x which is incompatible with -/// Anchor 1.0's solana 3.x types. These types are borsh-compatible reproductions +/// Anchor v2's solana 3.x types. These types are borsh-compatible reproductions /// that produce identical binary serialization. use anchor_lang::prelude::*; use borsh::BorshSerialize; diff --git a/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs b/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs index 323d8f529..0dce161b5 100644 --- a/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs +++ b/compression/cutils/anchor/programs/cutils/src/instructions/verify.rs @@ -51,7 +51,7 @@ pub fn handle_verify( ); // Build verify_leaf instruction manually because spl-account-compression 1.0.0 - // depends on solana-program 2.x which is incompatible with Anchor 1.0's solana 3.x + // depends on solana-program 2.x which is incompatible with Anchor v2's solana 3.x // types. Once a compatible version is available, replace this with the CPI wrapper. let mut accounts = vec![AccountMeta::new_readonly( *context.accounts.merkle_tree.address(), diff --git a/finance/betting-market/kani-proofs/src/lib.rs b/finance/betting-market/kani-proofs/src/lib.rs index fbd52929d..ea9ba1522 100644 --- a/finance/betting-market/kani-proofs/src/lib.rs +++ b/finance/betting-market/kani-proofs/src/lib.rs @@ -147,8 +147,7 @@ fn proof_parimutuel_solvency() { // ...so total payouts (stakes back + winnings) never exceed the vault // balance after the fee (winning_pool + distributable). - let total_payout = - winning_pool as u128 + total_winnings; + let total_payout = winning_pool as u128 + total_winnings; assert!(total_payout <= winning_pool as u128 + distributable as u128); } diff --git a/finance/escrow/kani-proofs/src/lib.rs b/finance/escrow/kani-proofs/src/lib.rs index ef4a53278..d9c621a2d 100644 --- a/finance/escrow/kani-proofs/src/lib.rs +++ b/finance/escrow/kani-proofs/src/lib.rs @@ -46,7 +46,9 @@ pub enum TokenError { /// only if it holds enough, credits `to` only if the sum fits in `u64`, and the /// two operations together conserve the total. This models exactly that. pub fn token_transfer(from: &mut u64, to: &mut u64, amount: u64) -> Result<(), TokenError> { - let new_from = from.checked_sub(amount).ok_or(TokenError::InsufficientFunds)?; + let new_from = from + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)?; let new_to = to.checked_add(amount).ok_or(TokenError::Overflow)?; *from = new_from; *to = new_to; @@ -236,7 +238,12 @@ fn proof_take_offer_conserves_value() { let vault_a: u64 = kani::any(); let wanted_b: u64 = kani::any(); - let mut b = TakeBalances { taker_a, taker_b, maker_b, vault_a }; + let mut b = TakeBalances { + taker_a, + taker_b, + maker_b, + vault_a, + }; let total_a_before = taker_a as u128 + vault_a as u128; let total_b_before = taker_b as u128 + maker_b as u128; @@ -272,8 +279,16 @@ fn proof_take_offer_guard_never_overflows() { let vault_a: u64 = kani::any(); let wanted_b: u64 = kani::any(); - let mut b = TakeBalances { taker_a, taker_b, maker_b, vault_a }; - assert_ne!(take_offer(&mut b, wanted_b), Err(TakeError::ConservationOverflow)); + let mut b = TakeBalances { + taker_a, + taker_b, + maker_b, + vault_a, + }; + assert_ne!( + take_offer(&mut b, wanted_b), + Err(TakeError::ConservationOverflow) + ); } /// Companion to the finding above: once we assume the SPL invariant that a @@ -295,8 +310,16 @@ fn proof_take_offer_guard_dead_under_spl_invariant() { kani::assume((taker_a as u128 + vault_a as u128) <= u64::MAX as u128); kani::assume((maker_b as u128 + wanted_b as u128) <= u64::MAX as u128); - let mut b = TakeBalances { taker_a, taker_b, maker_b, vault_a }; - assert_ne!(take_offer(&mut b, wanted_b), Err(TakeError::ConservationOverflow)); + let mut b = TakeBalances { + taker_a, + taker_b, + maker_b, + vault_a, + }; + assert_ne!( + take_offer(&mut b, wanted_b), + Err(TakeError::ConservationOverflow) + ); } // --------------------------------------------------------------------------- @@ -438,7 +461,12 @@ mod tests { #[test] fn take_offer_swaps() { - let mut b = TakeBalances { taker_a: 0, taker_b: 50, maker_b: 0, vault_a: 10 }; + let mut b = TakeBalances { + taker_a: 0, + taker_b: 50, + maker_b: 0, + vault_a: 10, + }; take_offer(&mut b, 7).unwrap(); assert_eq!(b.vault_a, 0); assert_eq!(b.taker_a, 10); diff --git a/finance/lending/anchor/Anchor.toml b/finance/lending/anchor/Anchor.toml index bd3e3a070..955a7e536 100644 --- a/finance/lending/anchor/Anchor.toml +++ b/finance/lending/anchor/Anchor.toml @@ -17,5 +17,5 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -# Anchor 1.0+ runs Rust + LiteSVM tests via cargo test. +# Anchor runs Rust + LiteSVM tests via cargo test. test = "cargo test" diff --git a/finance/lending/kani-proofs/src/lib.rs b/finance/lending/kani-proofs/src/lib.rs index bff811a18..067308dc0 100644 --- a/finance/lending/kani-proofs/src/lib.rs +++ b/finance/lending/kani-proofs/src/lib.rs @@ -342,9 +342,18 @@ mod tests { fn rate_curve_endpoints() { // min 100, optimal 300, max 2000, kink at 8000 bps. // util 0 -> min; util 8000 -> optimal; util 10000 -> max. - assert_eq!(borrow_rate_bps(0, 100, 300, 2000, 8000, 10000).unwrap(), 100); - assert_eq!(borrow_rate_bps(8000, 100, 300, 2000, 8000, 10000).unwrap(), 300); - assert_eq!(borrow_rate_bps(10000, 100, 300, 2000, 8000, 10000).unwrap(), 2000); + assert_eq!( + borrow_rate_bps(0, 100, 300, 2000, 8000, 10000).unwrap(), + 100 + ); + assert_eq!( + borrow_rate_bps(8000, 100, 300, 2000, 8000, 10000).unwrap(), + 300 + ); + assert_eq!( + borrow_rate_bps(10000, 100, 300, 2000, 8000, 10000).unwrap(), + 2000 + ); } #[test] diff --git a/finance/order-book/anchor/Anchor.toml b/finance/order-book/anchor/Anchor.toml index 0505fe40d..6765707a1 100644 --- a/finance/order-book/anchor/Anchor.toml +++ b/finance/order-book/anchor/Anchor.toml @@ -1,7 +1,7 @@ [toolchain] # Match the repo package manager (pnpm-lock.yaml at root); avoids Anchor's yarn default. package_manager = "pnpm" -# Pin Solana to the version used across the repo's Anchor 1.0 examples so the +# Pin Solana to the version used across the repo's Anchor examples so the # bundled test validator and BPF toolchain stay in lock-step. solana_version = "3.1.8" diff --git a/finance/order-book/anchor/README.md b/finance/order-book/anchor/README.md index 67854b149..0fa7f2f5b 100644 --- a/finance/order-book/anchor/README.md +++ b/finance/order-book/anchor/README.md @@ -170,7 +170,9 @@ Market PDA. Every taker fee - `ceil(gross * fee_bps / 10_000)` per fill - moves here in one batched CPI at the end of `place_order`. **Remaining accounts.** Solana lets the caller pass a tail of extra -`AccountInfo`s beyond the ones named in `#[derive(Accounts)]`. The +accounts beyond the ones named in `#[derive(Accounts)]`. The handler +reaches them through `context.remaining_accounts()`, which hands back +an owned `Vec`. The `place_order` handler uses them for the resting orders the taker wants to cross: for each one, the caller supplies `(maker_order_pda, maker_user_account_pda)` in the book's price-time @@ -200,7 +202,7 @@ A price of **960** means "960 USDC per NVDAx". The same program logic - identica ### Step 1 - Maria creates the market -**Instruction: `initialize_market(fee_basis_points=25, tick_size=1, min_order_size=1)`** +**Instruction: `initialize_market(fee_basis_points=25, tick_size=1, base_lot_size=1, quote_lot_size=1, min_order_size=1)`** **Key accounts: `base_mint = NVDAx`, `quote_mint = USDC`** Maria's wallet signs. Five accounts are created: @@ -390,9 +392,12 @@ Each side of the book is a critbit tree whose leaves are 88-byte `LeafNode`s: ```rust +#[repr(C, packed(8))] pub struct LeafNode { + pub tag: u8, // NodeTag::LeafNode + pub padding: [u8; 7], pub key: u128, // high 64 bits = price; low 64 = seq_num (time priority) - pub owner: Pubkey, + pub owner: Address, pub quantity: u64, // remaining quantity on this resting order pub order_id: u64, // links to the full Order PDA pub timestamp: i64, @@ -410,8 +415,8 @@ From [`state/order.rs`](programs/order-book/src/state/order.rs): ```rust pub struct Order { - pub market: Pubkey, - pub owner: Pubkey, + pub market: Address, + pub owner: Address, pub order_id: u64, pub side: OrderSide, // Bid | Ask pub price: u64, @@ -430,8 +435,8 @@ by `cancel_order` to decide how much to credit back to the user. ```rust pub struct MarketUser { - pub market: Pubkey, - pub owner: Pubkey, + pub market: Address, + pub owner: Address, pub unsettled_base: u64, pub unsettled_quote: u64, pub open_orders: Vec, // capped at 20 via Anchor max_len @@ -520,9 +525,11 @@ Token flow shorthand: ```rust pub fn initialize_market( - context: Context, + context: &mut Context, fee_basis_points: u16, tick_size: u64, + base_lot_size: u64, + quote_lot_size: u64, min_order_size: u64, ) -> Result<()> ``` @@ -543,6 +550,8 @@ pub fn initialize_market( **Checks:** - `tick_size > 0` → `InvalidTickSize` +- `base_lot_size > 0` → `InvalidBaseLotSize` +- `quote_lot_size > 0` → `InvalidQuoteLotSize` - `min_order_size > 0` → `BelowMinOrderSize` - `fee_basis_points <= 10_000` → `InvalidFeeBasisPoints` @@ -586,8 +595,8 @@ open orders. **Parameters:** ```rust -pub fn place_order<'info>( - context: Context<'info, PlaceOrder<'info>>, +pub fn place_order( + context: &mut Context, side: OrderSide, // Bid | Ask price: u64, quantity: u64, @@ -607,8 +616,8 @@ pub fn place_order<'info>( - `owner` (signer, mut) - `token_program`, `system_program` -**Accounts in (remaining):** a list of `AccountInfo`s passed via the -transaction's remaining accounts, grouped in pairs. For each resting +**Accounts in (remaining):** a list of `AccountView`s read from +`context.remaining_accounts()`, grouped in pairs. For each resting order the caller wants the taker to cross, in the book's price-time order: diff --git a/finance/order-book/kani-proofs/src/lib.rs b/finance/order-book/kani-proofs/src/lib.rs index e761337f4..4fa4807cd 100644 --- a/finance/order-book/kani-proofs/src/lib.rs +++ b/finance/order-book/kani-proofs/src/lib.rs @@ -177,7 +177,7 @@ fn proof_fee_is_ceiling_and_bounded() { let fee = ceil_fee(gross, fee_bps).expect("no overflow for bounded gross"); let exact = gross as u128 * fee_bps as u128; // the un-rounded numerator - // Ceiling: fee*DENOM is the least multiple of DENOM >= exact. + // Ceiling: fee*DENOM is the least multiple of DENOM >= exact. assert!((fee as u128) * BASIS_POINTS_DENOMINATOR >= exact); assert!(fee == 0 || (fee as u128 - 1) * BASIS_POINTS_DENOMINATOR < exact); diff --git a/finance/prop-amm/kani-proofs/src/lib.rs b/finance/prop-amm/kani-proofs/src/lib.rs index b648c9fef..3973c7d2e 100644 --- a/finance/prop-amm/kani-proofs/src/lib.rs +++ b/finance/prop-amm/kani-proofs/src/lib.rs @@ -114,8 +114,9 @@ pub fn quote_out_for_base_in( let numerator = (base_in as u128) .checked_mul(bid)? .checked_mul(10u128.checked_pow(quote_decimals as u32)?)?; - let denominator = - 10u128.checked_pow(oracle_scale)?.checked_mul(10u128.checked_pow(base_decimals as u32)?)?; + let denominator = 10u128 + .checked_pow(oracle_scale)? + .checked_mul(10u128.checked_pow(base_decimals as u32)?)?; if denominator == 0 { return None; } @@ -193,7 +194,10 @@ fn proof_sell_never_exceeds_oracle_value() { let base_value = (base_in as u128) * (price as u128) * 10u128.pow(quote_decimals as u32); let quote_value = (quote_out as u128) * 10u128.pow(oracle_scale) * 10u128.pow(base_decimals as u32); - assert!(quote_value <= base_value, "sell paid out above oracle value"); + assert!( + quote_value <= base_value, + "sell paid out above oracle value" + ); } // =========================================================================== @@ -232,7 +236,10 @@ fn proof_round_trip_never_profits_the_trader() { quote_out_for_base_in(base_out, bid, oracle_scale, base_decimals, quote_decimals) .expect("sell computes"); - assert!(quote_back <= quote_in, "round trip must not profit the trader"); + assert!( + quote_back <= quote_in, + "round trip must not profit the trader" + ); } // =========================================================================== diff --git a/finance/token-fundraiser/anchor/README.md b/finance/token-fundraiser/anchor/README.md index a51071b74..32a247525 100644 --- a/finance/token-fundraiser/anchor/README.md +++ b/finance/token-fundraiser/anchor/README.md @@ -7,11 +7,11 @@ Onchain crowdfunding on Solana: a program that collects tokens toward a target a The fundraiser state account: ```rust -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Fundraiser { - pub maker: Pubkey, - pub mint_to_raise: Pubkey, + pub maker: Address, + pub mint_to_raise: Address, pub amount_to_raise: u64, pub current_amount: u64, pub time_started: i64, @@ -35,10 +35,11 @@ The `InitSpace` derive macro implements the `Space` trait, which calculates the A per-contributor record: ```rust -#[account] +#[account(borsh)] #[derive(InitSpace)] pub struct Contributor { pub amount: u64, + /// Canonical bump for this PDA. pub bump: u8, } ``` @@ -75,7 +76,7 @@ All balance arithmetic uses `checked_*` operations and returns `FundraiserError: ## Lifecycle -### `initialize` +### `initialize_fundraiser` [`programs/fundraiser/src/instructions/initialize.rs`](programs/fundraiser/src/instructions/initialize.rs), account constraints `InitializeFundraiserAccountConstraints`. @@ -148,7 +149,7 @@ The suite uses a nonzero duration and warps the LiteSVM `Clock` sysvar to exerci ### How do I build crowdfunding on Solana? -A maker opens a fundraiser with `initialize`, naming the token, target amount, and duration. Contributors deposit with `contribute` while the window is open, and the funds sit in a program-controlled vault that neither side can raid. When the target is reached, the maker claims the raise with `check_contributions`, which pays out the vault and closes the fundraiser. +A maker opens a fundraiser with `initialize_fundraiser`, naming the token, target amount, and duration. Contributors deposit with `contribute` while the window is open, and the funds sit in a program-controlled vault that neither side can raid. When the target is reached, the maker claims the raise with `check_contributions`, which pays out the vault and closes the fundraiser. ### What happens if the fundraiser misses its target? diff --git a/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs b/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs index 2b1191729..b529eedd3 100644 --- a/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs +++ b/finance/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs @@ -19,7 +19,7 @@ fn integer_sqrt(n: u128) -> u128 { return n; } let mut x = n; - let mut y = (x + 1) / 2; + let mut y = x.div_ceil(2); while y < x { x = y; y = (x + n / x) / 2; diff --git a/finance/token-swap/kani-proofs/src/lib.rs b/finance/token-swap/kani-proofs/src/lib.rs index 97f889086..48c7a7fd2 100644 --- a/finance/token-swap/kani-proofs/src/lib.rs +++ b/finance/token-swap/kani-proofs/src/lib.rs @@ -122,8 +122,8 @@ fn proof_swap_preserves_constant_product() { // A trade needs a non-empty denominator. kani::assume(reserve_in as u128 + taxed_input as u128 > 0); - let output = swap_output(taxed_input, reserve_in, reserve_out) - .expect("swap output must compute"); + let output = + swap_output(taxed_input, reserve_in, reserve_out).expect("swap output must compute"); // Reserve transition (effective reserves): let new_in = reserve_in as u128 + taxed_input as u128 + lp_fee as u128; @@ -203,12 +203,16 @@ fn proof_swap_at_zero_reserve_drains_whole_pool() { // =========================================================================== /// Verbatim copy of `deposit_liquidity::integer_sqrt` (Newton's method, floor). +/// +/// Only the `#[cfg(kani)]` proof and the unit tests call it, so a plain +/// `cargo build` of the library sees no caller. +#[allow(dead_code)] fn integer_sqrt(n: u128) -> u128 { if n < 2 { return n; } let mut x = n; - let mut y = (x + 1) / 2; + let mut y = x.div_ceil(2); while y < x { x = y; y = (x + n / x) / 2; diff --git a/finance/vault-strategy/kani-proofs/src/lib.rs b/finance/vault-strategy/kani-proofs/src/lib.rs index ea3b62c85..1f51888e0 100644 --- a/finance/vault-strategy/kani-proofs/src/lib.rs +++ b/finance/vault-strategy/kani-proofs/src/lib.rs @@ -132,8 +132,8 @@ fn proof_fee_shares_bounded_by_supply() { // fee_bps <= 10_000 and elapsed <= SECONDS_PER_YEAR together give: kani::assume(numerator_factor <= denominator); - let fee_shares = mul_div_floor(total_shares as u128, numerator_factor, denominator) - .expect("computes"); + let fee_shares = + mul_div_floor(total_shares as u128, numerator_factor, denominator).expect("computes"); assert!(fee_shares <= total_shares as u128); // <= 100%/year dilution } @@ -161,7 +161,8 @@ mod tests { #[test] fn round_trip_not_profitable() { let minted = deposit_shares(100, 200, 150).unwrap(); - let back = mul_div_floor((150 + 100) as u128, minted as u128, (200 + minted) as u128).unwrap(); + let back = + mul_div_floor((150 + 100) as u128, minted as u128, (200 + minted) as u128).unwrap(); assert!(back <= 100); } } diff --git a/tokens/nft-operations/anchor/README.md b/tokens/nft-operations/anchor/README.md index 4dbe277d9..fb5db065f 100644 --- a/tokens/nft-operations/anchor/README.md +++ b/tokens/nft-operations/anchor/README.md @@ -12,9 +12,9 @@ The accounts needed to create an NFT collection are: ```rust #[derive(Accounts)] -pub struct CreateCollectionAccountConstraints<'info> { +pub struct CreateCollectionAccountConstraints { #[account(mut)] - user: Signer<'info>, + pub user: Signer, #[account( init, @@ -23,22 +23,22 @@ pub struct CreateCollectionAccountConstraints<'info> { mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] - mint: Account<'info, Mint>, + pub mint: Account, #[account( seeds = [b"authority"], bump, )] /// CHECK: This account is not initialized and is being used for signing purposes only - pub mint_authority: UncheckedAccount<'info>, + pub mint_authority: UncheckedAccount, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - metadata: UncheckedAccount<'info>, + pub metadata: UncheckedAccount, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - master_edition: UncheckedAccount<'info>, + pub master_edition: UncheckedAccount, #[account( init, @@ -46,12 +46,12 @@ pub struct CreateCollectionAccountConstraints<'info> { associated_token::mint = mint, associated_token::authority = user )] - destination: Account<'info, TokenAccount>, + pub destination: Account, - system_program: Program<'info, System>, - token_program: Program<'info, Token>, - associated_token_program: Program<'info, AssociatedToken>, - token_metadata_program: Program<'info, Metadata>, + pub system_program: Program, + pub token_program: Program, + pub associated_token_program: Program, + pub token_metadata_program: Program, } ``` @@ -71,9 +71,9 @@ The `metadata` and `master_edition` accounts are `UncheckedAccount` because the ```rust #[account(mut)] -metadata: Account<'info, MetadataAccount>, +pub metadata: MetadataAccount, #[account(mut)] -master_edition: Account<'info, MasterEditionAccount>, +pub master_edition: MasterEditionAccount, ``` the instruction would fail because [Anchor](https://solana.com/docs/terminology#anchor) would expect the accounts to already be initialized. @@ -94,90 +94,88 @@ pub fn handle_create_collection( ) -> Result<()> { validate_metadata_strings(&name, &symbol, &uri)?; - let metadata = &accounts.metadata.to_account_info(); - let master_edition = &accounts.master_edition.to_account_info(); - let mint = &accounts.mint.to_account_info(); - let authority = &accounts.mint_authority.to_account_info(); - let payer = &accounts.user.to_account_info(); - let system_program = &accounts.system_program.to_account_info(); - let spl_token_program = &accounts.token_program.to_account_info(); - let spl_metadata_program = &accounts.token_metadata_program.to_account_info(); - let seeds = &[&b"authority"[..], &[bumps.mint_authority]]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = MintTo { - mint: accounts.mint.to_account_info(), - to: accounts.destination.to_account_info(), - authority: accounts.mint_authority.to_account_info(), + mint: accounts.mint.cpi_handle_mut(), + to: accounts.destination.cpi_handle_mut(), + authority: accounts.mint_authority.cpi_handle(), }; let cpi_ctx = - CpiContext::new_with_signer(accounts.token_program.key(), cpi_accounts, signer_seeds); + CpiContext::new_with_signer(accounts.token_program.address(), cpi_accounts, signer_seeds); mint_to(cpi_ctx, 1)?; msg!("Collection NFT minted!"); let creator = vec![Creator { - address: accounts.mint_authority.key(), + address: *accounts.mint_authority.address(), verified: true, share: 100, }]; - let metadata_account = CreateMetadataAccountV3Cpi::new( - spl_metadata_program, - CreateMetadataAccountV3CpiAccounts { - metadata, - mint, - mint_authority: authority, - payer, - update_authority: (authority, true), - system_program, - rent: None, - }, - CreateMetadataAccountV3InstructionArgs { - data: DataV2 { - name, - symbol, - uri, - seller_fee_basis_points: 0, - creators: Some(creator), - collection: None, - uses: None, + create_metadata_accounts_v3( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + CreateMetadataAccountsV3 { + metadata: accounts.metadata.cpi_handle_mut(), + mint: accounts.mint.cpi_handle(), + mint_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.user.cpi_handle_mut(), + update_authority: accounts.mint_authority.cpi_handle(), + system_program: accounts.system_program.cpi_handle(), + update_authority_is_signer: true, }, - is_mutable: true, - collection_details: Some(CollectionDetails::V1 { size: 0 }), + signer_seeds, + ), + DataV2 { + name, + symbol, + uri, + seller_fee_basis_points: 0, + creators: Some(creator), + collection: None, + uses: None, }, - ); - metadata_account.invoke_signed(signer_seeds)?; + true, + Some(CollectionDetails::V1 { size: 0 }), + )?; msg!("Metadata Account created!"); - let master_edition_account = CreateMasterEditionV3Cpi::new( - spl_metadata_program, - CreateMasterEditionV3CpiAccounts { - edition: master_edition, - update_authority: authority, - mint_authority: authority, - mint, - payer, - metadata, - token_program: spl_token_program, - system_program, - rent: None, - }, - CreateMasterEditionV3InstructionArgs { - max_supply: Some(0), - }, - ); - master_edition_account.invoke_signed(signer_seeds)?; + create_master_edition_v3( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + CreateMasterEditionV3 { + edition: accounts.master_edition.cpi_handle_mut(), + mint: accounts.mint.cpi_handle_mut(), + update_authority: accounts.mint_authority.cpi_handle(), + mint_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.user.cpi_handle_mut(), + metadata: accounts.metadata.cpi_handle_mut(), + token_program: accounts.token_program.cpi_handle(), + system_program: accounts.system_program.cpi_handle(), + }, + signer_seeds, + ), + Some(0), + )?; msg!("Master Edition Account created"); Ok(()) } ``` +The CPIs go through anchor-spl's `anchor_spl::metadata` wrappers rather than the +`*Cpi` builders in `mpl-token-metadata`. The builders want `&AccountInfo`, and an +account reaches a v2 CPI as a `CpiHandle`, which the wrapper structs take. Each +account slot picks the handle that matches how the CPI uses it: `cpi_handle_mut()` +for the accounts the CPI writes, and `cpi_handle()` for the read-only slots. +`cpi_handle()` takes `&self`, so a single account can fill several read-only slots +in one call, which is what `mint_authority` does here. + Three steps: 1. Mint one token to the destination token account via a CPI to the [Classic Token Program](https://solana.com/docs/terminology#token-program). -2. Create a metadata account for the mint via a CPI to the Token Metadata program. The mint authority signs the CPI, so we use `invoke_signed` with the authority PDA's seeds. +2. Create a metadata account for the mint via a CPI to the Token Metadata program. The mint authority signs the CPI, so the `CpiContext` is built with `new_with_signer` and the authority PDA's seeds. 3. Create a master edition account for the mint via a CPI to the Token Metadata program. This enforces the NFT-specific constraints and transfers both the mint authority and freeze authority to the Master Edition PDA. Again, the mint authority signs. More on Token Metadata: @@ -188,9 +186,9 @@ The accounts needed to mint an NFT: ```rust #[derive(Accounts)] -pub struct MintNftAccountConstraints<'info> { +pub struct MintNftAccountConstraints { #[account(mut)] - pub owner: Signer<'info>, + pub owner: Signer, #[account( init, @@ -199,7 +197,7 @@ pub struct MintNftAccountConstraints<'info> { mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] - pub mint: Account<'info, Mint>, + pub mint: Account, #[account( init, @@ -207,30 +205,30 @@ pub struct MintNftAccountConstraints<'info> { associated_token::mint = mint, associated_token::authority = owner )] - pub destination: Account<'info, TokenAccount>, + pub destination: Account, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - pub metadata: UncheckedAccount<'info>, + pub metadata: UncheckedAccount, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - pub master_edition: UncheckedAccount<'info>, + pub master_edition: UncheckedAccount, #[account( seeds = [b"authority"], bump, )] /// CHECK: This is account is not initialized and is being used for signing purposes only - pub mint_authority: UncheckedAccount<'info>, + pub mint_authority: UncheckedAccount, #[account(mut)] - pub collection_mint: Account<'info, Mint>, + pub collection_mint: Account, - pub system_program: Program<'info, System>, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub token_metadata_program: Program<'info, Metadata>, + pub system_program: Program, + pub token_program: Program, + pub associated_token_program: Program, + pub token_metadata_program: Program, } ``` @@ -256,8 +254,9 @@ That's where the `collection_mint` account comes from - it provides the address For the collection NFT: ```rust -CreateMetadataAccountV3InstructionArgs { - data: DataV2 { +create_metadata_accounts_v3( + cpi_context, + DataV2 { name, symbol, uri, @@ -266,9 +265,9 @@ CreateMetadataAccountV3InstructionArgs { collection: None, uses: None, }, - is_mutable: true, - collection_details: Some(CollectionDetails::V1 { size: 0 }), -} + true, + Some(CollectionDetails::V1 { size: 0 }), +)?; ``` We set `collection_details`. @@ -276,8 +275,9 @@ We set `collection_details`. For a regular NFT: ```rust -CreateMetadataAccountV3InstructionArgs { - data: DataV2 { +create_metadata_accounts_v3( + cpi_context, + DataV2 { name, symbol, uri, @@ -285,13 +285,13 @@ CreateMetadataAccountV3InstructionArgs { creators: Some(creator), collection: Some(Collection { verified: false, - key: accounts.collection_mint.key(), + key: *accounts.collection_mint.address(), }), uses: None, }, - is_mutable: true, - collection_details: None, -} + true, + None, +)?; ``` We set the `collection` field with the key of the collection. `verified` starts false until the NFT is verified. @@ -302,32 +302,33 @@ The accounts needed to verify an NFT as part of a collection: ```rust #[derive(Accounts)] -pub struct VerifyCollectionMintAccountConstraints<'info> { - pub authority: Signer<'info>, +pub struct VerifyCollectionMintAccountConstraints { + #[account(mut)] + pub authority: Signer, #[account(mut)] - pub metadata: Account<'info, MetadataAccount>, - pub mint: Account<'info, Mint>, + pub metadata: MetadataAccount, + pub mint: Account, #[account( seeds = [b"authority"], bump, )] /// CHECK: This account is not initialized and is being used for signing purposes only - pub mint_authority: UncheckedAccount<'info>, - pub collection_mint: Account<'info, Mint>, + pub mint_authority: UncheckedAccount, + pub collection_mint: Account, #[account(mut)] - pub collection_metadata: Account<'info, MetadataAccount>, - pub collection_master_edition: Account<'info, MasterEditionAccount>, - pub system_program: Program<'info, System>, + pub collection_metadata: MetadataAccount, + pub collection_master_edition: MasterEditionAccount, + pub system_program: Program, #[account(address = INSTRUCTIONS_SYSVAR_ID)] /// CHECK: Sysvar instruction account that is being checked with an address constraint - pub sysvar_instruction: UncheckedAccount<'info>, - pub token_metadata_program: Program<'info, Metadata>, + pub sysvar_instruction: UncheckedAccount, + pub token_metadata_program: Program, } ``` ### Account breakdown -- `authority`: signer of the transaction. You can add constraints to restrict who can verify a collection. +- `authority`: signer of the transaction, and the payer for the verification. You can add constraints to restrict who can verify a collection. - `metadata`: the metadata account of the NFT being verified. - `mint`: the NFT mint being verified. - `mint_authority`: the mint authority of the collection NFT. @@ -338,7 +339,7 @@ pub struct VerifyCollectionMintAccountConstraints<'info> { - `sysvar_instruction`: provides access to the serialized instruction data for the running transaction. - `token_metadata_program`: MPL Token Metadata, used to perform the verification CPI. -Only the NFT and collection NFT metadata accounts need to be mutable - both are updated. The NFT metadata gets its `verified` boolean flipped to true, and the collection NFT metadata has its collection size incremented. +The two metadata accounts are mutable because both are updated: the NFT metadata gets its `verified` boolean flipped to true, and the collection NFT metadata has its collection size incremented. `authority` is mutable because it pays for the rent the size increment needs. ### Implementation for `verify_collection` @@ -347,38 +348,35 @@ pub fn handle_verify_collection( accounts: &mut VerifyCollectionMintAccountConstraints, bumps: &VerifyCollectionMintAccountConstraintsBumps, ) -> Result<()> { - let metadata = &accounts.metadata.to_account_info(); - let authority = &accounts.mint_authority.to_account_info(); - let collection_mint = &accounts.collection_mint.to_account_info(); - let collection_metadata = &accounts.collection_metadata.to_account_info(); - let collection_master_edition = &accounts.collection_master_edition.to_account_info(); - let system_program = &accounts.system_program.to_account_info(); - let sysvar_instructions = &accounts.sysvar_instruction.to_account_info(); - let spl_metadata_program = &accounts.token_metadata_program.to_account_info(); - let seeds = &[&b"authority"[..], &[bumps.mint_authority]]; let signer_seeds = &[&seeds[..]]; - let verify_collection = VerifyCollectionV1Cpi::new( - spl_metadata_program, - VerifyCollectionV1CpiAccounts { - authority, - delegate_record: None, - metadata, - collection_mint, - collection_metadata: Some(collection_metadata), - collection_master_edition: Some(collection_master_edition), - system_program, - sysvar_instructions, - }, - ); - verify_collection.invoke_signed(signer_seeds)?; + verify_sized_collection_item( + CpiContext::new_with_signer( + accounts.token_metadata_program.address(), + VerifySizedCollectionItem { + metadata: accounts.metadata.cpi_handle_mut(), + collection_authority: accounts.mint_authority.cpi_handle(), + payer: accounts.authority.cpi_handle_mut(), + collection_mint: accounts.collection_mint.cpi_handle(), + collection_metadata: accounts.collection_metadata.cpi_handle_mut(), + collection_master_edition: accounts.collection_master_edition.cpi_handle(), + }, + signer_seeds, + ), + None, + )?; msg!("Collection Verified!"); + Ok(()) } ``` +The collection was created sized, with `CollectionDetails::V1`, so +`verify_sized_collection_item` is the matching instruction: it is the variant that +also increments the collection's size counter. + > `INSTRUCTIONS_SYSVAR_ID` is the well-known sysvar address `Sysvar1nstructions1111111111111111111111111`, defined directly in [`verify_collection.rs`](programs/mint-nft/src/instructions/verify_collection.rs) because pinocchio, which anchor-lang v2 is built on, does not re-export it. `verify_collection` performs a CPI to the Token Metadata program with the right accounts. The collection NFT's mint authority signs the CPI, and the NFT is verified as part of the collection. diff --git a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs index 9da7392a0..5c3f78e9d 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/create_collection.rs @@ -20,7 +20,7 @@ use super::validate_metadata_strings; #[derive(Accounts)] pub struct CreateCollectionAccountConstraints { #[account(mut)] - user: Signer, + pub user: Signer, #[account( init, @@ -29,7 +29,7 @@ pub struct CreateCollectionAccountConstraints { mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] - mint: Account, + pub mint: Account, #[account( seeds = [b"authority"], @@ -40,11 +40,11 @@ pub struct CreateCollectionAccountConstraints { #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - metadata: UncheckedAccount, + pub metadata: UncheckedAccount, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - master_edition: UncheckedAccount, + pub master_edition: UncheckedAccount, #[account( init, @@ -52,12 +52,12 @@ pub struct CreateCollectionAccountConstraints { associated_token::mint = mint, associated_token::authority = user )] - destination: Account, + pub destination: Account, - system_program: Program, - token_program: Program, - associated_token_program: Program, - token_metadata_program: Program, + pub system_program: Program, + pub token_program: Program, + pub associated_token_program: Program, + pub token_metadata_program: Program, } /// Creates a collection NFT with caller-supplied metadata. diff --git a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs index 1fbd1ebb7..ab72a24f8 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/src/instructions/verify_collection.rs @@ -5,8 +5,7 @@ use anchor_lang::prelude::*; // usable here. The collection is created sized (`CollectionDetails::V1`), so // the sized-item variant is the matching instruction. use anchor_spl::metadata::{ - verify_sized_collection_item, MasterEditionAccount, MetadataAccount, - VerifySizedCollectionItem, + verify_sized_collection_item, MasterEditionAccount, MetadataAccount, VerifySizedCollectionItem, }; use anchor_spl::{metadata::Metadata, token::Mint}; // pinocchio does not re-export the instructions sysvar id; decode it here. diff --git a/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs b/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs index a44799564..c4d0a48b6 100644 --- a/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs +++ b/tokens/nft-operations/anchor/programs/mint-nft/src/lib.rs @@ -14,7 +14,7 @@ pub mod mint_nft { /// Create a collection NFT with the given metadata. pub fn create_collection( - mut context: &mut Context, + context: &mut Context, name: String, symbol: String, uri: String, @@ -30,7 +30,7 @@ pub mod mint_nft { /// Mint an NFT into the collection with the given metadata. pub fn mint_nft( - mut context: &mut Context, + context: &mut Context, name: String, symbol: String, uri: String, @@ -46,7 +46,7 @@ pub mod mint_nft { /// Verify an NFT as a member of the collection. pub fn verify_collection( - mut context: &mut Context, + context: &mut Context, ) -> Result<()> { instructions::verify_collection::handle_verify_collection( &mut context.accounts, diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml index f0168ff5d..b7d115789 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml @@ -34,8 +34,8 @@ anchor-spl = { version = "2.0.0-rc.1" } # session-keys is Anchor v1 only: its `Session` derive requires # `Option>`, which v2 has no equivalent for. The # account layout is read directly in src/session.rs instead. -# (so it builds against Anchor 1.0). Earlier 2.x releases pin Anchor <=0.30 -# and fail to compile against the Anchor 1.0 / Solana 3.x API. Provides the +# (so it builds against Anchor v2). Earlier 2.x releases pin Anchor <=0.30 +# and fail to compile against the Anchor v2 / Solana 3.x API. Provides the # gasless session-token lesson via `#[session_auth_or]` / `SessionToken`. # Token-2022 + token-metadata access goes through anchor-spl's bundled # re-exports (`anchor_spl::token_interface::spl_token_2022`, which is diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/README.md b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/README.md index 4a19404be..7b3093828 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/README.md +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/README.md @@ -4,25 +4,26 @@ Sometimes you want to use [account](https://solana.com/docs/terminology#account) When creating an `ExtraAccountMeta`, the data of any account can be used as an extra seed. In this example we derive a counter account from the token account owner and the literal `"counter"`. The counter records how many times that owner has transferred tokens. -This is the setup in `extra_account_metas()`: +This is the setup in `handle_extra_account_metas()`: ```rust // Define extra account metas to store on the extra_account_meta_list account -impl<'info> InitializeExtraAccountMetaList<'info> { - pub fn extra_account_metas() -> Result> { - Ok(vec![ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { bytes: b"counter".to_vec() }, - Seed::AccountData { - account_index: 0, - data_index: 32, - length: 32, - }, - ], - false, // is_signer - true, // is_writable - )?]) - } +pub fn handle_extra_account_metas() -> Result> { + Ok(vec![ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"counter".to_vec(), + }, + Seed::AccountData { + account_index: 0, + data_index: 32, + length: 32, + }, + ], + false, // is_signer + true, // is_writable + ) + .map_err(|_| ProgramError::InvalidArgument)?]) } ``` @@ -50,53 +51,54 @@ Because we derive the counter account from the *sender's* token account owner, w ```rust #[derive(Accounts)] -pub struct InitializeExtraAccountMetaList<'info> { +pub struct InitializeExtraAccountMetaListAccountConstraints { #[account(mut)] - payer: Signer<'info>, + payer: Signer, - /// CHECK: ExtraAccountMetaList account, must use these seeds. + /// CHECK: ExtraAccountMetaList Account, must use these seeds #[account( init, - seeds = [b"extra-account-metas", mint.key().as_ref()], + seeds = [b"extra-account-metas", mint.address().as_ref()], bump, + // size_of returns Result with spl's ProgramError - unwrap is safe for known-good input space = ExtraAccountMetaList::size_of( - InitializeExtraAccountMetaList::extra_account_metas()?.len() - )?, - payer = payer, + handle_extra_account_metas_count() + ).unwrap(), + payer = payer )] - pub extra_account_meta_list: AccountInfo<'info>, - pub mint: InterfaceAccount<'info, Mint>, + pub extra_account_meta_list: UncheckedAccount, + pub mint: InterfaceAccount, #[account( init, - seeds = [b"counter", payer.key().as_ref()], + seeds = [b"counter", payer.address().as_ref()], bump, payer = payer, - space = COUNTER_ACCOUNT_SIZE, + space = CounterAccount::DISCRIMINATOR.len() + CounterAccount::INIT_SPACE, )] - pub counter_account: Account<'info, CounterAccount>, - pub token_program: Program<'info, Token2022>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, + pub counter_account: BorshAccount, + pub token_program: Program, + pub associated_token_program: Program, + pub system_program: Program, } ``` -The counter account also has to appear on the `TransferHook` struct - the [program](https://solana.com/docs/terminology#program) needs to know about every account passed in by the runtime: +The counter account also has to appear on the `TransferHookAccountConstraints` struct - the [program](https://solana.com/docs/terminology#program) needs to know about every account passed in by the runtime. It is `mut` because the hook writes the incremented count back: ```rust #[derive(Accounts)] -pub struct TransferHook<'info> { +pub struct TransferHookAccountConstraints { #[account(token::mint = mint, token::authority = owner)] - pub source_token: InterfaceAccount<'info, TokenAccount>, - pub mint: InterfaceAccount<'info, Mint>, + pub source_token: InterfaceAccount, + pub mint: InterfaceAccount, #[account(token::mint = mint)] - pub destination_token: InterfaceAccount<'info, TokenAccount>, - /// CHECK: source token account owner; may be a SystemAccount or a PDA owned by another program. - pub owner: UncheckedAccount<'info>, - /// CHECK: ExtraAccountMetaList account. - #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] - pub extra_account_meta_list: UncheckedAccount<'info>, - #[account(seeds = [b"counter", owner.key().as_ref()], bump)] - pub counter_account: Account<'info, CounterAccount>, + pub destination_token: InterfaceAccount, + /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program + pub owner: UncheckedAccount, + /// CHECK: ExtraAccountMetaList Account, + #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] + pub extra_account_meta_list: UncheckedAccount, + #[account(seeds = [b"counter", owner.address().as_ref()], bump)] + pub counter_account: BorshAccount, } ``` diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index a19d9ffa5..d273781f4 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -37,7 +37,7 @@ pub fn handler( // initialize ExtraAccountMetaList account with extra accounts // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types // `AccountView` is Copy, and a copy still points at the same backing // buffer, so the borrow writes through to the real account. let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs index 33128c47e..731c23446 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -20,7 +20,7 @@ pub struct TransferHookAccountConstraints { /// CHECK: ExtraAccountMetaList Account, #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] pub extra_account_meta_list: UncheckedAccount, - #[account(seeds = [b"counter", owner.address().as_ref()], bump)] + #[account(mut, seeds = [b"counter", owner.address().as_ref()], bump)] pub counter_account: BorshAccount, } @@ -28,19 +28,24 @@ pub fn handler(context: &mut Context, amount: u6 // Fail this instruction if it is not called from within a transfer hook check_is_transferring(&context)?; - // Check if the amount is too big + // A hook can reject a transfer by returning an error. This one only logs, + // so the example stays runnable: `amount` arrives in minor units, so any + // transfer of a token with decimals clears 50 immediately. Return + // `err!(TransferError::AmountTooBig)` here to make the limit binding, and + // pick a threshold in the mint's own minor units. if amount > 50 { msg!("The amount is too big: {}", amount); - //return err!(TransferError::AmountTooBig); } - // Increment the transfer count safely + // Increment the transfer count safely and write it back, so the count + // survives the transfer that produced it. let count = context .accounts .counter_account .counter .checked_add(1) - .ok_or(TransferError::AmountTooBig)?; + .ok_or(TransferError::CounterOverflow)?; + context.accounts.counter_account.counter = count; msg!("This token has been transferred {} times", count); diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs index 1a89d0cd2..7a1bf4f89 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs @@ -24,6 +24,8 @@ pub enum TransferError { AmountTooBig, #[msg("The token is not currently transferring")] IsNotCurrentlyTransferring, + #[msg("The transfer counter would overflow")] + CounterOverflow, } pub mod entrypoint; @@ -75,7 +77,7 @@ pub fn check_is_transferring(context: &Context) // Define extra account metas to store on extra_account_meta_list account pub fn handle_extra_account_metas() -> Result> { // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types Ok(vec![ExtraAccountMeta::new_with_seeds( &[ Seed::Literal { diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs index c60b466ef..e5b7f4f2b 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/tests/test_transfer_hook.rs @@ -3,6 +3,7 @@ use { solana_program::instruction::Instruction, system_program, Address, InstructionData, ToAccountMetas, }, + borsh::BorshDeserialize, litesvm::LiteSVM, solana_keypair::Keypair, solana_kite::{ @@ -17,6 +18,21 @@ use { solana_signer::Signer, }; +/// Deserialize the CounterAccount (8-byte discriminator + fields). +#[derive(BorshDeserialize)] +struct CounterAccountData { + _discriminator: [u8; 8], + counter: u64, + _bump: u8, +} + +fn read_counter(svm: &LiteSVM, counter_pda: &Address) -> u64 { + let account = svm.get_account(counter_pda).unwrap(); + CounterAccountData::deserialize(&mut &account.data[..]) + .unwrap() + .counter +} + fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() @@ -115,6 +131,10 @@ fn test_transfer_hook_account_data_as_seed() { .unwrap(); svm.expire_blockhash(); + // The hook writes the incremented count back, so it survives the transfer. + let counter_after = read_counter(&svm, &counter_pda); + assert_eq!(counter_after, 1, "hook should have recorded one transfer"); + // Step 5: Try calling transfer_hook directly (should fail - not transferring) let direct_hook_ix = Instruction::new_with_bytes( program_id, diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml index 1a341a025..352ae672e 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/programs/abl-token/Cargo.toml @@ -25,7 +25,7 @@ custom-panic = [] [dependencies] -# interface-instructions feature removed in Anchor 1.0 +# interface-instructions feature was removed in Anchor 1.0 and does not exist in v2 anchor-lang = "2.0.0-rc.1" # The `#[program]` macro expands to `wincode` paths for instruction-data # (de)serialization, so the crate has to be a direct dependency. diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index 236ee3ccd..4ad269008 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -37,7 +37,7 @@ pub fn handler( // initialize ExtraAccountMetaList account with extra accounts // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types // `AccountView` is Copy, and a copy still points at the same backing // buffer, so the borrow writes through to the real account. let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs index 8f173c59e..00bf43b6a 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -20,7 +20,7 @@ pub struct TransferHookAccountConstraints { /// CHECK: ExtraAccountMetaList Account, #[account(seeds = [b"extra-account-metas", mint.address().as_ref()], bump)] pub extra_account_meta_list: UncheckedAccount, - #[account(seeds = [b"counter"], bump)] + #[account(mut, seeds = [b"counter"], bump)] pub counter_account: BorshAccount, } @@ -28,19 +28,24 @@ pub fn handler(context: &mut Context, amount: u6 // Fail this instruction if it is not called from within a transfer hook check_is_transferring(&context)?; - // Check if the amount is too big + // A hook can reject a transfer by returning an error. This one only logs, + // so the example stays runnable: `amount` arrives in minor units, so any + // transfer of a token with decimals clears 50 immediately. Return + // `err!(TransferError::AmountTooBig)` here to make the limit binding, and + // pick a threshold in the mint's own minor units. if amount > 50 { msg!("The amount is too big: {}", amount); - //return err!(TransferError::AmountTooBig); } - // Increment the transfer count safely + // Increment the transfer count safely and write it back, so the count + // survives the transfer that produced it. let count = context .accounts .counter_account .counter .checked_add(1) - .ok_or(TransferError::AmountTooBig)?; + .ok_or(TransferError::CounterOverflow)?; + context.accounts.counter_account.counter = count; msg!("This token has been transferred {} times", count); diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs index 4aabf0894..de839dae5 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs @@ -24,6 +24,8 @@ pub enum TransferError { AmountTooBig, #[msg("The token is not currently transferring")] IsNotCurrentlyTransferring, + #[msg("The transfer counter would overflow")] + CounterOverflow, } pub mod entrypoint; @@ -75,7 +77,7 @@ pub fn check_is_transferring(context: &Context) // Define extra account metas to store on extra_account_meta_list account pub fn handle_extra_account_metas() -> Result> { // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types Ok(vec![ExtraAccountMeta::new_with_seeds( &[Seed::Literal { bytes: b"counter".to_vec(), diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs index a7fa1c4d7..5fc389b08 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor/programs/transfer-hook/tests/test_transfer_hook_counter.rs @@ -3,6 +3,7 @@ use { solana_program::instruction::Instruction, system_program, Address, InstructionData, ToAccountMetas, }, + borsh::BorshDeserialize, litesvm::LiteSVM, solana_keypair::Keypair, solana_kite::{ @@ -17,6 +18,21 @@ use { solana_signer::Signer, }; +/// Deserialize the CounterAccount (8-byte discriminator + fields). +#[derive(BorshDeserialize)] +struct CounterAccountData { + _discriminator: [u8; 8], + counter: u64, + _bump: u8, +} + +fn read_counter(svm: &LiteSVM, counter_pda: &Address) -> u64 { + let account = svm.get_account(counter_pda).unwrap(); + CounterAccountData::deserialize(&mut &account.data[..]) + .unwrap() + .counter +} + fn associated_token_program_id() -> Address { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() @@ -115,6 +131,10 @@ fn test_transfer_hook_counter() { .unwrap(); svm.expire_blockhash(); + // The hook writes the incremented count back, so it survives the transfer. + let counter_after = read_counter(&svm, &counter_pda); + assert_eq!(counter_after, 1, "hook should have recorded one transfer"); + // Step 5: Try calling transfer_hook directly (should fail - not transferring) let direct_hook_ix = Instruction::new_with_bytes( program_id, diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index b22372d0e..7c20db260 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -38,7 +38,7 @@ pub fn handler( // initialize ExtraAccountMetaList account with extra accounts // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types // `AccountView` is Copy, and a copy still points at the same backing // buffer, so the borrow writes through to the real account. let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml index 9f340987c..1cd187d69 100644 --- a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/programs/transfer-hook/Cargo.toml @@ -34,7 +34,7 @@ wincode = { version = "0.5", features = ["derive"] } # `SchemaRead`/`SchemaWrite` bound fails. 2.6.1 is the last 0.5-line release. solana-address = ">=2.6, <2.7" anchor-spl = "2.0.0-rc.1" -# SPL crates v3.x-compatible - uses solana-program-error 3.x matching anchor-lang 1.0 +# SPL crates v3.x-compatible - uses solana-program-error 3.x matching anchor-lang v2 spl-discriminator = "0.5.2" spl-tlv-account-resolution = "0.11.1" spl-transfer-hook-interface = "2.1.0" diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs index c014196d6..636dc8ce7 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rs @@ -34,7 +34,7 @@ pub fn handle_initialize_extra_account_metas_list( bumps: &InitializeExtraAccountMetasAccountConstraintsBumps, ) -> Result<()> { // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types let account_metas = vec![ // 5 - wallet (sender) config account ExtraAccountMeta::new_with_seeds( diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs index aa692605b..76cb90534 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/instructions/initialize_extra_account_meta_list.rs @@ -39,7 +39,7 @@ pub fn handler( // initialize ExtraAccountMetaList account with extra accounts // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types // `AccountView` is Copy, and a copy still points at the same backing // buffer, so the borrow writes through to the real account. let mut meta_list_view = *context.accounts.extra_account_meta_list.account(); diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs index 315d898da..ed411879e 100644 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs @@ -77,7 +77,7 @@ pub fn check_is_transferring(context: &Context) // Define extra account metas to store on extra_account_meta_list account pub fn handle_extra_account_metas() -> Result> { // .map_err() needed because spl-tlv-account-resolution uses solana-program-error 2.x - // while anchor-lang 1.0 uses 3.x - structurally identical but different semver types + // while anchor-lang v2 uses 3.x - structurally identical but different semver types Ok(vec![ExtraAccountMeta::new_with_seeds( &[Seed::Literal { bytes: "white_list".as_bytes().to_vec(),