From c0583a4fac63fecf2077361b2e58bec39541e9aa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 01:55:56 +0000 Subject: [PATCH 1/6] ci: fix the two red workflows on main The TypeScript workflow runs `biome check ./`, which formats JSON as well as TypeScript. `basics/cross-program-invocation/anchor/idls/lever.json` was regenerated by `anchor build` with one array element per line, which Biome reformats onto a single line, so the check has failed on every commit since the IDL was last regenerated. Reformatted; the file's contents are unchanged. The ASM workflow installs the sbpf assembler with `cargo install --git`. The upstream repository now ships a second binary crate, `xtask`, and cargo refuses to pick between two binaries, so the install step dies before any project is built. Naming the package fixes it. --- .github/workflows/solana-asm.yml | 5 ++- .../anchor/idls/lever.json | 33 ++----------------- 2 files changed, 7 insertions(+), 31 deletions(-) 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/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": [ From 14847e89764c22c21bbc5497f9a7b4ae5148f185 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 01:56:07 +0000 Subject: [PATCH 2/6] transfer-hook: keep the transfer count the examples say they keep Both counter examples exist to show a hook writing to a PDA, and both READMEs say the hook increments a counter on every transfer. The handler added one to the stored count, logged the result, and dropped it: `counter_account` was not `mut`, so nothing was ever written back and the count read one after every transfer no matter how many had happened. The extra account meta already declares the PDA writable, so the fix is the `mut` constraint and the assignment. A new assertion in each test reads the counter back after the hooked transfer. The dead `amount > 50` check kept a commented-out `return err!(...)` beside it. `amount` arrives in minor units, so any transfer of a token with decimals clears 50, and returning the error there would fail the example's own test. Replaced the commented-out line with a comment saying what to change to make the limit binding. `AmountTooBig` was also standing in as the checked-add overflow error, which is a different failure. Added `CounterOverflow` for that. --- .../src/instructions/transfer_hook.rs | 15 +++++++++----- .../anchor/programs/transfer-hook/src/lib.rs | 4 +++- .../transfer-hook/tests/test_transfer_hook.rs | 20 +++++++++++++++++++ .../src/instructions/transfer_hook.rs | 15 +++++++++----- .../anchor/programs/transfer-hook/src/lib.rs | 4 +++- .../tests/test_transfer_hook_counter.rs | 20 +++++++++++++++++++ 6 files changed, 66 insertions(+), 12 deletions(-) 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/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, From 4c918df8416196df510b6a63875b76c409846e9a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 01:56:22 +0000 Subject: [PATCH 3/6] docs: describe the programs as they are now implemented The v2 port changed code that five READMEs still show in its pre-port form, so the repository documented account types, handler signatures and CPI builders that no longer exist. Each code block now matches the source it is quoting, and each identifier named in prose resolves to a real definition. - `tokens/nft-operations`: the CPIs go through anchor-spl's `create_metadata_accounts_v3` / `create_master_edition_v3` / `verify_sized_collection_item` wrappers and a `CpiContext`, not the `mpl-token-metadata` `*Cpi` builders with `invoke_signed`. Added a note on why, and on how the read-only and mutable `CpiHandle`s differ. - `tokens/token-extensions/transfer-hook/account-data-as-seed`: the extra account metas come from a free function, the constraint structs carry the `AccountConstraints` suffix, and `extra_account_meta_list` is an `UncheckedAccount`. - `finance/order-book`: `initialize_market` takes `base_lot_size` and `quote_lot_size` (with the errors that reject zero), `place_order` takes `&mut Context`, the state accounts store `Address`, and the maker pairs arrive as `AccountView`s from `context.remaining_accounts()`. - `basics/close-account` and `finance/token-fundraiser`: `BorshAccount`, `#[account(borsh)]`, `Address`, and `user.address()` in seeds. - `basics/counter` and `finance/token-fundraiser` named an `initialize` handler; the handlers are `initialize_counter` and `initialize_fundraiser`. `tokens/nft-operations`'s program is not a workspace member, so nothing lints it: its account fields were private (which the README's example could not be), one handler took a `mut` binding on a reference, and one `use` was not rustfmt-clean. Fixed alongside the README that quotes them. --- basics/close-account/anchor/README.md | 16 +- basics/counter/anchor/README.md | 2 +- finance/order-book/anchor/README.md | 33 ++- finance/token-fundraiser/anchor/README.md | 13 +- tokens/nft-operations/anchor/README.md | 256 +++++++++--------- .../src/instructions/create_collection.rs | 18 +- .../src/instructions/verify_collection.rs | 3 +- .../anchor/programs/mint-nft/src/lib.rs | 6 +- .../account-data-as-seed/anchor/README.md | 88 +++--- 9 files changed, 225 insertions(+), 210 deletions(-) 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/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/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/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/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, } ``` From 0498619b04f39c5e04a9b5ff083308c779bfc923 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 01:56:30 +0000 Subject: [PATCH 4/6] comments: stop describing the framework as Anchor 1.0 Twenty comments across workflows, manifests and program sources explained a version skew or a workaround in terms of "Anchor 1.0" or "anchor-lang 1.0". The programs are on 2.0.0-rc.1, so a reader checking one of these against the manifest finds a version that is not there and cannot tell whether the workaround still applies. Each now names v2. Two mentions stay: `README.md` on when `anchor init` began scaffolding LiteSVM, and the abl-token manifest on when `interface-instructions` was removed. Both are statements about the past and both are true. --- .github/workflows/anchor.yml | 2 +- .github/workflows/rust.yml | 2 +- compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml | 2 +- compression/cnft-vault/anchor/programs/cnft-vault/Cargo.toml | 2 +- compression/cutils/anchor/programs/cutils/Cargo.toml | 2 +- .../cutils/anchor/programs/cutils/src/bubblegum_types.rs | 4 ++-- .../cutils/anchor/programs/cutils/src/instructions/verify.rs | 2 +- finance/lending/anchor/Anchor.toml | 2 +- finance/order-book/anchor/Anchor.toml | 2 +- .../anchor-example/anchor/programs/extension_nft/Cargo.toml | 4 ++-- .../src/instructions/initialize_extra_account_meta_list.rs | 2 +- .../anchor/programs/abl-token/Cargo.toml | 2 +- .../src/instructions/initialize_extra_account_meta_list.rs | 2 +- .../src/instructions/initialize_extra_account_meta_list.rs | 2 +- .../transfer-cost/anchor/programs/transfer-hook/Cargo.toml | 2 +- .../src/instructions/initialise_extra_account_metas_list.rs | 2 +- .../src/instructions/initialize_extra_account_meta_list.rs | 2 +- .../whitelist/anchor/programs/transfer-hook/src/lib.rs | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) 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/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/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/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/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/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/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/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/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(), From 7ce9081f5a02dc432c5649485b9269dd9e1280fb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 01:59:20 +0000 Subject: [PATCH 5/6] kani: lint the proof crates, which nothing was linting Each proof crate declares its own `[workspace]` (deliberately: the Kani model must not drag in the Solana and SPL dependency tree), so the repository-wide `cargo fmt` and `cargo clippy` jobs in rust.yml never see it. Nothing else did either, and seven of the eight crates had drifted out of rustfmt. `kani.yml` already runs a per-crate matrix for the unit tests, so the fmt and clippy steps go there. Reformatted all eight. `token-swap`'s copy of `integer_sqrt` also needed two fixes to pass clippy. It is documented as a verbatim copy of the program's function, so the `(x + 1) / 2` that clippy wants written as `div_ceil` is changed in the program and in the proof together, keeping the two identical. Only the `#[cfg(kani)]` proof and the unit tests call it, so the plain library build sees no caller; that now says so with an `allow`. --- .github/workflows/kani.yml | 11 +++++ finance/betting-market/kani-proofs/src/lib.rs | 3 +- finance/escrow/kani-proofs/src/lib.rs | 42 +++++++++++++++---- finance/lending/kani-proofs/src/lib.rs | 15 +++++-- finance/order-book/kani-proofs/src/lib.rs | 2 +- finance/prop-amm/kani-proofs/src/lib.rs | 15 +++++-- .../src/instructions/deposit_liquidity.rs | 2 +- finance/token-swap/kani-proofs/src/lib.rs | 10 +++-- finance/vault-strategy/kani-proofs/src/lib.rs | 7 ++-- 9 files changed, 83 insertions(+), 24 deletions(-) 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/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/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/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-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); } } From 061d45ac7f912f0d7fd979fa896651be05a288b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 02:01:44 +0000 Subject: [PATCH 6/6] ci: typecheck and verify the vault strategy app `finance/vault-strategy/anchor/app` is the repository's only TypeScript application: 110 files, an Anchor client, and a committed IDL. The TypeScript workflow ran Biome over it, which formats and lints but never compiles, so a client that had drifted from the program's IDL would have looked fine. The app already ships the two scripts that catch it. `typecheck` compiles it, and `verify` exercises instruction encoding, account decoding and PDA derivation against the committed IDL offline, with no validator. Both pass today; CI now runs them. --- .github/workflows/typescript.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) 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