From 2af3903c30674ad12580a7833895e7685b5e3305 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 02:31:07 +0000 Subject: [PATCH 1/2] tokens/interest-bearing (Anchor v1): share v2's check_rate_authority helper v2 split check_mint_data into check_rate_authority, which takes the parsed InterestBearingConfig, because v2's exclusive borrows made the old read-the-account helper need unsafe. Only that parsing route is forced by the Anchor version. The v1 copy now has the same helper, name and signature, and its handlers parse the extension with anchor-spl's get_mint_extension_data. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01F8Wo5zNLqZ2NQqSjGD7WpK --- .../src/instructions/initialize.rs | 12 +++++++---- .../src/instructions/update_rate.rs | 10 +++++---- .../programs/interest-bearing/src/lib.rs | 21 +++++++------------ 3 files changed, 21 insertions(+), 22 deletions(-) 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(()) } From 5827007f66644c7d41ba5597f5043e1a3d57bd26 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 02:31:07 +0000 Subject: [PATCH 2/2] anchor-v1: bring the copies in line with v2 where the version allows A scan of every anchor/anchor-v1 pair (function names, error variants, account fields, constants, seeds) and of every v2 program commit since July that left its v1 copy untouched found: - transfer-hook counter and account-data-as-seed: port 14847e89. The hook dropped the incremented count because counter_account was not mut, so it read one after every transfer. Write it back, add CounterOverflow for the checked add, and assert the stored count in the tests. - account fields 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: derive Default on OrderTreeRoot like v2, dropping the hand-written impl. The remaining differences are forced by the Anchor version and are listed in the changelog. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01F8Wo5zNLqZ2NQqSjGD7WpK --- CHANGELOG.md | 29 +++++++++++++++++++ .../src/instructions/create.rs | 6 ++-- .../anchor-program-example/src/lib.rs | 8 ++--- .../src/instructions/create.rs | 6 ++-- .../src/instructions/increment.rs | 4 +-- .../programs/rent-example/src/lib.rs | 4 +-- .../src/instructions/transfer_sol_with_cpi.rs | 6 ++-- .../instructions/transfer_sol_with_program.rs | 4 +-- .../order-book/src/state/order_book.rs | 9 ------ .../order-book/src/state/slab/ordertree.rs | 2 +- .../src/instructions/create_collection.rs | 18 ++++++------ .../src/instructions/transfer_hook.rs | 15 ++++++---- .../programs/transfer-hook/src/lib.rs | 2 ++ .../transfer-hook/tests/test_transfer_hook.rs | 20 +++++++++++++ .../src/instructions/transfer_hook.rs | 15 ++++++---- .../programs/transfer-hook/src/lib.rs | 2 ++ .../tests/test_transfer_hook_counter.rs | 20 +++++++++++++ 17 files changed, 122 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e9e880d..d86fbee72 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] - Finance examples point their production oracle path at Pyth The oracle network that the lending, perpetual futures and prop AMM examples 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/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,