diff --git a/CHANGELOG.md b/CHANGELOG.md index 236e8a170..d3add3c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ All notable changes to this repository are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2026-09-23] - Anchor v1 copies match their v2 counterparts where the version allows + +A scan of every `anchor/` and `anchor-v1/` pair, comparing function names, +error variants, account fields, constants and seeds, and listing every v2 +program commit since July that left its v1 copy untouched, found these +differences that the Anchor version does not force: + +- The transfer-hook `counter` and `account-data-as-seed` v1 copies still had + the bug v2 fixed in August: the hook computed the new transfer count and + dropped it, because `counter_account` was not `mut`, so the count read one + after every transfer. Both now write the count back and report an overflow + with the new `CounterOverflow` error instead of `AmountTooBig`, and each + test reads the counter back after the hooked transfer. +- Interest-bearing (v1): `check_mint_data` becomes `check_rate_authority` + with v2's signature, taking the parsed `InterestBearingConfig`. The + handlers read the extension with anchor-spl's `get_mint_extension_data`. +- Account fields that v2 declares `pub` are `pub` in the v1 copies of + account-data, checking-accounts, program-derived-addresses, rent, + transfer-sol and nft-operations. +- Order book (v1): `OrderTreeRoot` derives `Default` like the v2 copy, + replacing a hand-written impl. + +What the scan still reports is forced by the Anchor version: v2's +transfer-hook `entrypoint` fallback, the local session-token reader that +replaces the v1-only `session-keys` crate, the `last_restart_slot` syscall +wrapper (the v1 copies make the same restart check through the sysvar), the +wincode span in rent, zero-copy padding, the Pyth account traits, and the +betting market's borrow-release helper. + ## [2026-09-23] - Order book tests cover the deepest critbit path A critbit tree does not rebalance, so asks at doubling prices stretch the path diff --git a/basics/account-data/anchor-v1/programs/anchor-program-example/src/instructions/create.rs b/basics/account-data/anchor-v1/programs/anchor-program-example/src/instructions/create.rs index 535efc853..78f199055 100644 --- a/basics/account-data/anchor-v1/programs/anchor-program-example/src/instructions/create.rs +++ b/basics/account-data/anchor-v1/programs/anchor-program-example/src/instructions/create.rs @@ -4,15 +4,15 @@ use anchor_lang::prelude::*; #[derive(Accounts)] pub struct CreateAddressInfoAccountConstraints<'info> { #[account(mut)] - payer: Signer<'info>, + pub payer: Signer<'info>, #[account( init, payer = payer, space = AddressInfo::DISCRIMINATOR.len() + AddressInfo::INIT_SPACE, )] - address_info: Account<'info, AddressInfo>, - system_program: Program<'info, System>, + pub address_info: Account<'info, AddressInfo>, + pub system_program: Program<'info, System>, } pub fn handle_create_address_info( diff --git a/basics/checking-accounts/anchor-v1/programs/anchor-program-example/src/lib.rs b/basics/checking-accounts/anchor-v1/programs/anchor-program-example/src/lib.rs index 5cd90a851..70187616a 100644 --- a/basics/checking-accounts/anchor-v1/programs/anchor-program-example/src/lib.rs +++ b/basics/checking-accounts/anchor-v1/programs/anchor-program-example/src/lib.rs @@ -15,16 +15,16 @@ pub mod checking_account_program { // This is a simple example and does not include all possible constraints and types #[derive(Accounts)] pub struct CheckingAccountsAccountConstraints<'info> { - payer: Signer<'info>, // checks account is signer + pub payer: Signer<'info>, // checks account is signer /// CHECK: No checks performed, example of an unchecked account #[account(mut)] - account_to_create: UncheckedAccount<'info>, + pub account_to_create: UncheckedAccount<'info>, /// CHECK: Perform owner check using constraint #[account( mut, owner = id() )] - account_to_change: UncheckedAccount<'info>, - system_program: Program<'info, System>, // checks account is executable, and is the system program + pub account_to_change: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, // checks account is executable, and is the system program } diff --git a/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/create.rs b/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/create.rs index 2278e1d13..6e5cd56b6 100644 --- a/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/create.rs +++ b/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/create.rs @@ -4,7 +4,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] pub struct CreatePageVisitsAccountConstraints<'info> { #[account(mut)] - payer: Signer<'info>, + pub payer: Signer<'info>, #[account( init, @@ -16,8 +16,8 @@ pub struct CreatePageVisitsAccountConstraints<'info> { ], bump, )] - page_visits: Account<'info, PageVisits>, - system_program: Program<'info, System>, + pub page_visits: Account<'info, PageVisits>, + pub system_program: Program<'info, System>, } pub fn handle_create_page_visits( diff --git a/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/increment.rs b/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/increment.rs index 206ef7311..6e387efcb 100644 --- a/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/increment.rs +++ b/basics/program-derived-addresses/anchor-v1/programs/anchor-program-example/src/instructions/increment.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] pub struct IncrementPageVisitsAccountConstraints<'info> { - user: SystemAccount<'info>, + pub user: SystemAccount<'info>, #[account( mut, seeds = [ @@ -12,7 +12,7 @@ pub struct IncrementPageVisitsAccountConstraints<'info> { ], bump = page_visits.bump, )] - page_visits: Account<'info, PageVisits>, + pub page_visits: Account<'info, PageVisits>, } pub fn handle_increment_page_visits( diff --git a/basics/rent/anchor-v1/programs/rent-example/src/lib.rs b/basics/rent/anchor-v1/programs/rent-example/src/lib.rs index e2d8a840b..2a01093b4 100644 --- a/basics/rent/anchor-v1/programs/rent-example/src/lib.rs +++ b/basics/rent/anchor-v1/programs/rent-example/src/lib.rs @@ -55,6 +55,6 @@ pub struct CreateSystemAccountAccountConstraints<'info> { #[derive(AnchorSerialize, AnchorDeserialize, Debug)] pub struct AddressData { - name: String, - address: String, + pub name: String, + pub address: String, } diff --git a/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs b/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs index 4aa9ed0a9..01a346f6d 100644 --- a/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs +++ b/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_cpi.rs @@ -4,10 +4,10 @@ use anchor_lang::system_program; #[derive(Accounts)] pub struct TransferSolWithCpiAccountConstraints<'info> { #[account(mut)] - payer: Signer<'info>, + pub payer: Signer<'info>, #[account(mut)] - recipient: SystemAccount<'info>, - system_program: Program<'info, System>, + pub recipient: SystemAccount<'info>, + pub system_program: Program<'info, System>, } pub fn handler(context: Context, amount: u64) -> Result<()> { diff --git a/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs b/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs index 03d98b73b..2da88c9ee 100644 --- a/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs +++ b/basics/transfer-sol/anchor-v1/programs/transfer-sol/src/instructions/transfer_sol_with_program.rs @@ -15,10 +15,10 @@ pub struct TransferSolWithProgramAccountConstraints<'info> { mut, owner = crate::ID // value of declare_id!() )] - payer: UncheckedAccount<'info>, + pub payer: UncheckedAccount<'info>, #[account(mut)] - recipient: SystemAccount<'info>, + pub recipient: SystemAccount<'info>, } // Directly modifying lamports is only possible if the program is the owner of the account diff --git a/finance/order-book/anchor-v1/programs/order-book/src/state/order_book.rs b/finance/order-book/anchor-v1/programs/order-book/src/state/order_book.rs index 1786d5ac5..ec337f828 100644 --- a/finance/order-book/anchor-v1/programs/order-book/src/state/order_book.rs +++ b/finance/order-book/anchor-v1/programs/order-book/src/state/order_book.rs @@ -247,12 +247,3 @@ impl OrderBook { } } } - -impl Default for OrderTreeRoot { - fn default() -> Self { - Self { - maybe_node: 0, - leaf_count: 0, - } - } -} diff --git a/finance/order-book/anchor-v1/programs/order-book/src/state/slab/ordertree.rs b/finance/order-book/anchor-v1/programs/order-book/src/state/slab/ordertree.rs index ea6e983e1..5aeb71437 100644 --- a/finance/order-book/anchor-v1/programs/order-book/src/state/slab/ordertree.rs +++ b/finance/order-book/anchor-v1/programs/order-book/src/state/slab/ordertree.rs @@ -22,7 +22,7 @@ pub const MAX_TREE_NODES: usize = 1024; /// /// `maybe_node` is only meaningful when `leaf_count > 0` - a freshly-zeroed /// root represents an empty tree. -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] pub struct OrderTreeRoot { pub maybe_node: NodeHandle, diff --git a/tokens/nft-operations/anchor-v1/programs/mint-nft/src/instructions/create_collection.rs b/tokens/nft-operations/anchor-v1/programs/mint-nft/src/instructions/create_collection.rs index 6fd2722cc..7f338b9ac 100644 --- a/tokens/nft-operations/anchor-v1/programs/mint-nft/src/instructions/create_collection.rs +++ b/tokens/nft-operations/anchor-v1/programs/mint-nft/src/instructions/create_collection.rs @@ -19,7 +19,7 @@ use super::validate_metadata_strings; #[derive(Accounts)] pub struct CreateCollectionAccountConstraints<'info> { #[account(mut)] - user: Signer<'info>, + pub user: Signer<'info>, #[account( init, @@ -28,7 +28,7 @@ pub struct CreateCollectionAccountConstraints<'info> { mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] - mint: Account<'info, Mint>, + pub mint: Account<'info, Mint>, #[account( seeds = [b"authority"], @@ -39,11 +39,11 @@ pub struct CreateCollectionAccountConstraints<'info> { #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - metadata: UncheckedAccount<'info>, + pub metadata: UncheckedAccount<'info>, #[account(mut)] /// CHECK: This account will be initialized by the metaplex program - master_edition: UncheckedAccount<'info>, + pub master_edition: UncheckedAccount<'info>, #[account( init, @@ -51,12 +51,12 @@ pub struct CreateCollectionAccountConstraints<'info> { associated_token::mint = mint, associated_token::authority = user )] - destination: Account<'info, TokenAccount>, + pub destination: Account<'info, TokenAccount>, - 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<'info, System>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_metadata_program: Program<'info, Metadata>, } /// Creates a collection NFT with caller-supplied metadata. diff --git a/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/initialize.rs b/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/initialize.rs index cb34219a7..717638d8d 100644 --- a/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/initialize.rs +++ b/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/initialize.rs @@ -6,10 +6,14 @@ use anchor_spl::{ spl_token_2022::{extension::ExtensionType, pod::PodMint}, InitializeMint2, }, - token_interface::{interest_bearing_mint_initialize, InterestBearingMintInitialize, Token2022}, + token_2022::spl_token_2022::extension::interest_bearing_mint::InterestBearingConfig, + token_interface::{ + get_mint_extension_data, interest_bearing_mint_initialize, InterestBearingMintInitialize, + Token2022, + }, }; -use crate::check_mint_data; +use crate::check_rate_authority; #[derive(Accounts)] pub struct InitializeAccountConstraints<'info> { @@ -72,9 +76,9 @@ pub fn handler(context: Context, rate: i16) -> Res Some(&context.accounts.payer.key()), // freeze authority )?; - check_mint_data( + let config = get_mint_extension_data::( &context.accounts.mint_account.to_account_info(), - &context.accounts.payer.key(), )?; + check_rate_authority(&config, &context.accounts.payer.key())?; Ok(()) } diff --git a/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/update_rate.rs b/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/update_rate.rs index 87da63389..3f8da4f0f 100644 --- a/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/update_rate.rs +++ b/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/instructions/update_rate.rs @@ -1,9 +1,11 @@ use anchor_lang::prelude::*; +use anchor_spl::token_2022::spl_token_2022::extension::interest_bearing_mint::InterestBearingConfig; use anchor_spl::token_interface::{ - interest_bearing_mint_update_rate, InterestBearingMintUpdateRate, Mint, Token2022, + get_mint_extension_data, interest_bearing_mint_update_rate, InterestBearingMintUpdateRate, Mint, + Token2022, }; -use crate::check_mint_data; +use crate::check_rate_authority; #[derive(Accounts)] pub struct UpdateRateAccountConstraints<'info> { @@ -29,9 +31,9 @@ pub fn handler(context: Context, rate: i16) -> Res rate, )?; - check_mint_data( + let config = get_mint_extension_data::( &context.accounts.mint_account.to_account_info(), - &context.accounts.authority.key(), )?; + check_rate_authority(&config, &context.accounts.authority.key())?; Ok(()) } diff --git a/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/lib.rs b/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/lib.rs index b09782186..d93e1129d 100644 --- a/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/lib.rs +++ b/tokens/token-extensions/interest-bearing/anchor-v1/programs/interest-bearing/src/lib.rs @@ -1,11 +1,5 @@ use anchor_lang::prelude::*; -use anchor_spl::token_2022::spl_token_2022::{ - extension::{ - interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions, - StateWithExtensions, - }, - state::Mint as MintState, -}; +use anchor_spl::token_2022::spl_token_2022::extension::interest_bearing_mint::InterestBearingConfig; use anchor_spl::token_interface::spl_pod::optional_keys::OptionalNonZeroPubkey; mod instructions; @@ -27,16 +21,15 @@ pub mod interest_bearing { } } -pub fn check_mint_data(mint_account_info: &AccountInfo, authority_key: &Pubkey) -> Result<()> { - let mint_data = mint_account_info.data.borrow(); - let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; - let extension_data = mint_with_extension.get_extension::()?; - +/// Assert the extension names `authority_key` as the account allowed to change +/// the rate. Both callers read the extension with anchor-spl's +/// `get_mint_extension_data`, which parses the mint's TLV data. +pub fn check_rate_authority(config: &InterestBearingConfig, authority_key: &Pubkey) -> Result<()> { assert_eq!( - extension_data.rate_authority, + config.rate_authority, OptionalNonZeroPubkey::try_from(Some(*authority_key))? ); - msg!("{:?}", extension_data); + msg!("{:?}", config); Ok(()) } diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/src/instructions/transfer_hook.rs index 62aafc317..37125df9f 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -19,7 +19,7 @@ pub struct TransferHookAccountConstraints<'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)] + #[account(mut, seeds = [b"counter", owner.key().as_ref()], bump)] pub counter_account: Account<'info, CounterAccount>, } @@ -27,19 +27,24 @@ pub fn handler(context: Context, amount: u64) -> // 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-v1/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/src/lib.rs index c83525603..f064e424e 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/src/lib.rs @@ -25,6 +25,8 @@ pub enum TransferError { AmountTooBig, #[msg("The token is not currently transferring")] IsNotCurrentlyTransferring, + #[msg("The transfer counter would overflow")] + CounterOverflow, } #[program] diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/tests/test_transfer_hook.rs b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/tests/test_transfer_hook.rs index 4b083e01c..12d34a3ca 100644 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/tests/test_transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor-v1/programs/transfer-hook/tests/test_transfer_hook.rs @@ -7,6 +7,7 @@ use { }, InstructionData, ToAccountMetas, }, + borsh::BorshDeserialize, litesvm::LiteSVM, solana_kite::{ create_wallet, send_transaction_from_instructions, @@ -21,6 +22,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: &Pubkey) -> u64 { + let account = svm.get_account(counter_pda).unwrap(); + CounterAccountData::deserialize(&mut &account.data[..]) + .unwrap() + .counter +} + fn associated_token_program_id() -> Pubkey { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() @@ -132,6 +148,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-v1/programs/transfer-hook/src/instructions/transfer_hook.rs b/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/src/instructions/transfer_hook.rs index 31ee75fc4..4b2d13a6f 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/src/instructions/transfer_hook.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/src/instructions/transfer_hook.rs @@ -19,7 +19,7 @@ pub struct TransferHookAccountConstraints<'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"], bump)] + #[account(mut, seeds = [b"counter"], bump)] pub counter_account: Account<'info, CounterAccount>, } @@ -27,19 +27,24 @@ pub fn handler(context: Context, amount: u64) -> // 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-v1/programs/transfer-hook/src/lib.rs b/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/src/lib.rs index 7fabbb4b0..5d18c72fe 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/src/lib.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/src/lib.rs @@ -25,6 +25,8 @@ pub enum TransferError { AmountTooBig, #[msg("The token is not currently transferring")] IsNotCurrentlyTransferring, + #[msg("The transfer counter would overflow")] + CounterOverflow, } #[program] diff --git a/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/tests/test_transfer_hook_counter.rs b/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/tests/test_transfer_hook_counter.rs index ae927cd15..5dc54feba 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/tests/test_transfer_hook_counter.rs +++ b/tokens/token-extensions/transfer-hook/counter/anchor-v1/programs/transfer-hook/tests/test_transfer_hook_counter.rs @@ -7,6 +7,7 @@ use { }, InstructionData, ToAccountMetas, }, + borsh::BorshDeserialize, litesvm::LiteSVM, solana_kite::{ create_wallet, send_transaction_from_instructions, @@ -21,6 +22,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: &Pubkey) -> u64 { + let account = svm.get_account(counter_pda).unwrap(); + CounterAccountData::deserialize(&mut &account.data[..]) + .unwrap() + .counter +} + fn associated_token_program_id() -> Pubkey { "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" .parse() @@ -132,6 +148,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,