From aa75ae4eecccd797e78c45b27758d5900c1e275a Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:08:57 +0900 Subject: [PATCH 01/36] Support Token-2022 in `CreateBuffer` and `ReclaimBuffer` `CreateBuffer` and `ReclaimBuffer` each took a `token_program` account and rejected anything that wasn't the legacy SPL Token program. They now accept Token-2022 as well and issue all of their CPIs against whichever of the two they were handed, so a buffer can be allocated, initialized and closed under either. Token-2022 encodes the instructions this program issues exactly as the legacy program does, so only the CPI target changes. Two things do differ: - Account data. A Token-2022 account carrying extensions is longer than the base layout, so the legacy reader (exact length, legacy owner) rejects it. `token::read_token_account` dispatches on the validated program and reads by value, which also drops the borrow before `ReclaimBuffer` closes the same account. - Buffer sizing. A buffer now gets the length its mint actually needs: a mint with no extension data keeps the base layout, and anything longer is priced by asking the token program via `GetAccountDataSize`, the way the associated-token-account program does. That keeps the answer authoritative at run time rather than freezing a mint-extension-to-account-extension table into the program. The program account is shared by the whole instruction, so the mints one instruction touches must all live under the same token program; splitting a mixed batch across two instructions is the caller's job. Adds `SettlementError::BufferSizeUnavailable` (35), reachable only defensively: a token program that fails the size query aborts the instruction on its own. `BeginSettle` and `FinalizeSettle` keep rejecting everything but the legacy program; Token-2022 for the settlement pair follows separately. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 136 ++++++++- Cargo.toml | 4 +- bench-report.json | 44 +-- interface/Cargo.toml | 1 + interface/src/instruction/create_buffer.rs | 7 +- interface/src/lib.rs | 6 + interface/src/pda/buffer.rs | 4 +- interface/src/token_program.rs | 57 ++++ programs/settlement/Cargo.toml | 1 + programs/settlement/src/create_buffer.rs | 37 ++- programs/settlement/src/lib.rs | 1 + programs/settlement/src/reclaim_buffer.rs | 26 +- programs/settlement/src/token.rs | 338 +++++++++++++++++++++ 13 files changed, 600 insertions(+), 62 deletions(-) create mode 100644 interface/src/token_program.rs create mode 100644 programs/settlement/src/token.rs diff --git a/Cargo.lock b/Cargo.lock index e72c5921..5f41caff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -910,6 +910,7 @@ dependencies = [ "solana-sdk-ids", "solana-sha256-hasher", "solana-system-interface 3.2.0", + "spl-token-2022-interface 3.1.1", "spl-token-interface 3.0.0", ] @@ -2005,6 +2006,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsecp256k1" version = "0.7.2" @@ -2338,6 +2345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2489,14 +2497,29 @@ dependencies = [ [[package]] name = "pinocchio-token" -version = "0.6.0" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217e3259f93a1520e4692b18653dad4d29af54ffc8b3a09819808be85dada074" +dependencies = [ + "solana-account-view", + "solana-address 2.6.1", + "solana-instruction-view", + "solana-program-error", +] + +[[package]] +name = "pinocchio-token-2022" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825f59c8348e5c2d3fd56432ef927f5819542b3b05fae4f5b6869801113e775e" +checksum = "3a3c164973916f044bebff3c8cb4467088bafb13cbdb818522b9ca8782940d06" dependencies = [ + "pinocchio-token", "solana-account-view", "solana-address 2.6.1", "solana-instruction-view", + "solana-nullable", "solana-program-error", + "solana-zero-copy", ] [[package]] @@ -3145,6 +3168,7 @@ dependencies = [ "pinocchio", "pinocchio-system", "pinocchio-token", + "pinocchio-token-2022", "proptest", "solana-address-lookup-table-interface", "solana-compute-budget", @@ -3321,10 +3345,10 @@ dependencies = [ "solana-sysvar 4.1.0", "solana-vote-interface", "spl-generic-token", - "spl-token-2022-interface", + "spl-token-2022-interface 2.1.0", "spl-token-group-interface", "spl-token-interface 2.0.0", - "spl-token-metadata-interface", + "spl-token-metadata-interface 0.8.0", "thiserror 2.0.19", "zstd", ] @@ -4156,6 +4180,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "889194d8c5faec648f2f6fadddb60566249921ebb074e2707e7095458d5864e2" dependencies = [ + "borsh", "bytemuck", ] @@ -5199,6 +5224,22 @@ dependencies = [ "bytemuck_derive", ] +[[package]] +name = "solana-zk-elgamal-proof-interface" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8da7f01db2148a1dc16261ff1dc6f3930a1e255a33cece4f1b56658694f27f7" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-address 2.6.1", + "solana-instruction", + "solana-sdk-ids", + "solana-zk-sdk-pod", +] + [[package]] name = "solana-zk-elgamal-proof-program" version = "4.1.2" @@ -5284,6 +5325,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "solana-zk-sdk-pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a800583b7a4cea3e851686af162cc6e4712eef97fa91dfa9aca2459b95c84777" +dependencies = [ + "base64", + "bytemuck", + "bytemuck_derive", + "solana-nullable", + "thiserror 2.0.19", +] + [[package]] name = "solana-zk-token-proof-program" version = "4.1.2" @@ -5427,10 +5481,40 @@ dependencies = [ "solana-sdk-ids", "solana-zk-sdk 4.0.0", "spl-pod", - "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-extraction 0.5.1", "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface", - "spl-token-metadata-interface", + "spl-token-metadata-interface 0.8.0", + "spl-type-length-value", + "thiserror 2.0.19", +] + +[[package]] +name = "spl-token-2022-interface" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "821d96d034ea31c4965d182c742153c491ae0abee531331b55771086c5030d86" +dependencies = [ + "arrayref", + "bytemuck", + "getrandom 0.2.17", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-address 2.6.1", + "solana-instruction", + "solana-nullable", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-sdk-ids", + "solana-zero-copy", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", + "spl-token-confidential-transfer-proof-extraction 0.6.1", + "spl-token-group-interface", + "spl-token-metadata-interface 1.0.1", "spl-type-length-value", "thiserror 2.0.19", ] @@ -5455,6 +5539,26 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a845ec724e807643a04f1d43439ee3571dd913848112ac76aa90b850eaf7c" +dependencies = [ + "bytemuck", + "solana-account-info", + "solana-address 2.6.1", + "solana-curve25519 4.0.1", + "solana-instruction", + "solana-instructions-sysvar 3.0.1", + "solana-msg", + "solana-program-error", + "solana-sdk-ids", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", + "thiserror 2.0.19", +] + [[package]] name = "spl-token-confidential-transfer-proof-generation" version = "0.5.1" @@ -5544,6 +5648,26 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "spl-token-metadata-interface" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3d96f175e7022ff200464dfa75a3708a4e9b70c83c4ecd04fe52ee479f4fef" +dependencies = [ + "borsh", + "num-derive", + "num-traits", + "num_enum", + "solana-address 2.6.1", + "solana-borsh", + "solana-instruction", + "solana-nullable", + "solana-program-error", + "spl-discriminator", + "spl-type-length-value", + "thiserror 2.0.19", +] + [[package]] name = "spl-type-length-value" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index 6e242088..5aeaf74b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,8 @@ litesvm-token = "0.15" num_enum = "0.7" pinocchio = "0.11" pinocchio-system = "0.6" -pinocchio-token = "0.6" +pinocchio-token = "0.7" +pinocchio-token-2022 = "0.4" proptest = "1" cow-settlement-client = { path = "client", version = "0.2.0" } cow-settlement-interface = { path = "interface", version = "0.2.0" } @@ -56,6 +57,7 @@ solana-sha256-hasher = { version = "3", features = ["sha2"] } solana-system-interface = "3" spl-associated-token-account-interface = "2" spl-token = "9" +spl-token-2022-interface = "3" spl-token-interface = "3" [workspace.metadata.cli] diff --git a/bench-report.json b/bench-report.json index 2b9053c5..3fe35f5b 100644 --- a/bench-report.json +++ b/bench-report.json @@ -24,28 +24,28 @@ "transfer_authority/reclaim_authority_can_transfer_itself": 4 }, "compute_units": { - "create_buffers/happy_path_creates_initialized_buffer_token_account": 10340, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21731, - "create_buffers/max_buffers_in_one_instruction": 176947, - "create_order/happy_path_creates_order_pda_with_expected_body": 7921, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4526, - "reclaim_buffer/funded_buffer_is_skipped": 6299, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7447, - "reclaim_buffer/max_buffers_in_one_instruction": 136501, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18043, - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2133, - "settle/finalizes_with_no_pushes": 7043, - "settle/pulls_from_multiple_orders": 19750, - "settle/pulls_funds_to_destination": 13417, - "settle/pulls_to_multiple_destinations": 14564, - "settle/pushes_a_single_order": 12267, - "settle/pushes_several_orders_from_different_buffers": 17451, - "settle/pushes_several_orders_from_one_buffer": 17452, - "settle/settles_a_single_order": 12285, - "settle/settles_multiple_orders": 22679, - "transfer_authority/manager_can_transfer_manager": 3170, - "transfer_authority/manager_can_transfer_reclaim_authority": 3172, - "transfer_authority/reclaim_authority_can_transfer_itself": 3175 + "create_buffers/happy_path_creates_initialized_buffer_token_account": 10361, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21790, + "create_buffers/max_buffers_in_one_instruction": 177519, + "create_order/happy_path_creates_order_pda_with_expected_body": 7914, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4519, + "reclaim_buffer/funded_buffer_is_skipped": 6321, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7470, + "reclaim_buffer/max_buffers_in_one_instruction": 137046, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18083, + "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2127, + "settle/finalizes_with_no_pushes": 7025, + "settle/pulls_from_multiple_orders": 19713, + "settle/pulls_funds_to_destination": 13390, + "settle/pulls_to_multiple_destinations": 14536, + "settle/pushes_a_single_order": 12241, + "settle/pushes_several_orders_from_different_buffers": 17416, + "settle/pushes_several_orders_from_one_buffer": 17417, + "settle/settles_a_single_order": 12259, + "settle/settles_multiple_orders": 22635, + "transfer_authority/manager_can_transfer_manager": 3163, + "transfer_authority/manager_can_transfer_reclaim_authority": 3165, + "transfer_authority/reclaim_authority_can_transfer_itself": 3168 }, "transaction_bytes": { "create_buffers/happy_path_creates_initialized_buffer_token_account": 303, diff --git a/interface/Cargo.toml b/interface/Cargo.toml index 348ef7b2..a922b381 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -30,6 +30,7 @@ solana-pubkey = { workspace = true, features = ["curve25519"] } solana-sdk-ids.workspace = true solana-sha256-hasher.workspace = true solana-system-interface.workspace = true +spl-token-2022-interface.workspace = true spl-token-interface.workspace = true [dev-dependencies] diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index 0d3b25ef..77d69cec 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -14,9 +14,10 @@ pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; use super::InstructionInputParsing; use crate::SettlementInstruction; -/// The SPL Token program. Buffers are created as token accounts owned by this -/// program. -pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; +/// The legacy SPL Token program, the default this builder places in the +/// `token_program` account. Buffers are created as token accounts owned by +/// whichever of [`crate::token_program::SUPPORTED_TOKEN_PROGRAMS`] is passed. +pub use crate::token_program::SPL_TOKEN_PROGRAM_ID; /// Builder for a `CreateBuffer` instruction that creates one buffer per /// `(buffer_pda, mint)` pair in `buffers`. diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 0e019aa0..5348ac5c 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -9,6 +9,7 @@ solana_pubkey::declare_id!("J516Mv7YvvvJyMvNEca8tWNTJyDHbFpzwDZD96BNfR3w"); pub mod data; pub mod instruction; pub mod pda; +pub mod token_program; #[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] #[repr(u8)] @@ -219,6 +220,11 @@ pub enum SettlementError { /// `TransferAuthority`'s signer is neither the manager nor the current /// holder of the role being transferred, so it may not transfer it. UnauthorizedAuthorityTransfer = 34, + /// `CreateBuffer` asked the token program how long a token account for a + /// mint has to be and couldn't read the answer, so it can't size the + /// buffer. Defensive: a token program that fails this query aborts the + /// instruction on its own. + BufferSizeUnavailable = 35, } impl From for u32 { diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 8345f7db..143b2cb4 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -16,8 +16,8 @@ //! contents are stranded. //! //! Unlike the order PDA, which stores its own bump (see -//! [`crate::data::order::OrderAccount`]), a buffer is a fixed-size SPL token -//! account with no room for one. +//! [`crate::data::order::OrderAccount`]), a buffer's layout belongs entirely to +//! the token program, leaving no room for one. use solana_account_view::AccountView; use solana_address::Address; diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs new file mode 100644 index 00000000..5a72adad --- /dev/null +++ b/interface/src/token_program.rs @@ -0,0 +1,57 @@ +//! The token programs a buffer may be created under. +//! +//! `CreateBuffer` and `ReclaimBuffer` take a single `token_program` account and +//! issue every one of their CPIs against it. That account has to be one of +//! [`SUPPORTED_TOKEN_PROGRAMS`], which is what [`is_supported`] checks. +//! +//! Because the program account is shared by the whole instruction, the mints an +//! instruction touches must all live under the same token program: a legacy SPL +//! mint and a Token-2022 mint can't have their buffers created by one +//! `CreateBuffer`. Splitting them across two is the caller's job. +//! +//! `BeginSettle` and `FinalizeSettle` still accept only the legacy program. + +use crate::Pubkey; + +/// The legacy SPL Token program. +pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; + +/// The SPL Token-2022 program. Its instruction encoding is a superset of the +/// legacy program's, so the instructions this program issues are byte-identical +/// either way and only the CPI target changes. +pub use spl_token_2022_interface::ID as TOKEN_2022_PROGRAM_ID; + +/// Every token program a token-moving instruction accepts, in no particular +/// order. +pub const SUPPORTED_TOKEN_PROGRAMS: [Pubkey; 2] = [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]; + +/// Whether `address` is a token program buffers may be created under, that is, +/// whether it is one of [`SUPPORTED_TOKEN_PROGRAMS`]. +pub fn is_supported(address: &Pubkey) -> bool { + SUPPORTED_TOKEN_PROGRAMS.contains(address) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixtures::pubkey_from_seed; + + #[test] + fn supported_programs_are_supported() { + for program in SUPPORTED_TOKEN_PROGRAMS { + assert!(is_supported(&program), "{program} should be supported"); + } + } + + #[test] + fn unrelated_program_is_not_supported() { + assert!(!is_supported(&pubkey_from_seed("not a token program"))); + } + + /// The list is two distinct programs, so it can't have been built from one + /// program repeated. + #[test] + fn supported_programs_are_distinct() { + assert_ne!(SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID); + } +} diff --git a/programs/settlement/Cargo.toml b/programs/settlement/Cargo.toml index 7638cf58..05a0f16f 100644 --- a/programs/settlement/Cargo.toml +++ b/programs/settlement/Cargo.toml @@ -21,6 +21,7 @@ crate-type = ["cdylib", "lib"] pinocchio = { workspace = true, features = ["cpi"] } pinocchio-system.workspace = true pinocchio-token.workspace = true +pinocchio-token-2022.workspace = true cow-settlement-interface.workspace = true solana-instruction = { workspace = true, features = ["syscalls"] } diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/create_buffer.rs index c1a59ae4..653fb40a 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/create_buffer.rs @@ -2,15 +2,18 @@ use cow_settlement_interface::{ instruction::{ - create_buffer::{BufferAccounts, CreateBufferInput, SPL_TOKEN_PROGRAM_ID}, + create_buffer::{BufferAccounts, CreateBufferInput}, InstructionInputParsing, }, pda::{buffer::buffer_pda_seeds, state::state_pda_seeds}, }; -use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; -use pinocchio_token::{instructions::InitializeAccount3, state::Account as TokenAccount}; +use pinocchio::{AccountView, Address, ProgramResult}; +use pinocchio_token::instructions::InitializeAccount3; -use crate::processor::CanonicalPda; +use crate::{ + processor::CanonicalPda, + token::{token_account_len, validate_token_program}, +}; pub fn process_create_buffer( program_id: &Address, @@ -19,12 +22,10 @@ pub fn process_create_buffer( ) -> ProgramResult { let input = CreateBufferInput::parse(instruction_data, accounts)?; - // Only the legacy SPL Token program is supported. The InitializeAccount3 - // CPI targets that program unconditionally; reject a mismatching account - // up front so the caller gets a clear error. - if input.token_program.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } + // Every buffer this instruction creates belongs to the one token program + // it was handed, so reject an unsupported one up front rather than at the + // first CPI. + let token_program = validate_token_program(input.token_program)?; // The buffers' token authority is the settlement state PDA, the single // authority over every buffer. Derive it once for all buffers. @@ -34,8 +35,12 @@ pub fn process_create_buffer( // One buffer per token. `CanonicalPda::create_idempotent` derives the // canonical bump and, by signing the allocation with the buffer seeds, // rejects any `buffer_pda` that isn't the canonical address. The buffer - // is a token account, so it's assigned to the SPL Token program rather - // than to the settlement program. + // is a token account, so it's assigned to the token program rather than + // to the settlement program. + // + // The buffer is sized for the mint it will hold: a Token-2022 mint + // whose extensions require matching ones on its token accounts needs + // room for them, and `token_account_len` is what works out how much. // // We don't validate `mint` here. `InitializeAccount3` requires a real, // token-program-owned mint (and special-cases the native mint), so a @@ -45,8 +50,8 @@ pub fn process_create_buffer( program_id, payer: input.payer, pda: buffer_pda, - size: TokenAccount::LEN as u64, - owner: &SPL_TOKEN_PROGRAM_ID, + size: token_account_len(token_program, mint)?, + owner: token_program, seeds: buffer_pda_seeds(mint_key), } .create_idempotent()?; @@ -54,7 +59,8 @@ pub fn process_create_buffer( // An existing buffer is already an initialized token account, so only // initialize a freshly created one. if created { - InitializeAccount3::new(buffer_pda, mint, &state_pda).invoke()?; + InitializeAccount3::new(buffer_pda, mint, &state_pda) + .invoke_with_unverified_program(token_program)?; } } @@ -68,6 +74,7 @@ mod tests { create_buffer_data, NUM_SHARED_ACCOUNTS, }; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; + use pinocchio::error::ProgramError; /// Arbitrary placeholder program id. The failure path exercised below /// returns before the program id is used for any syscall. diff --git a/programs/settlement/src/lib.rs b/programs/settlement/src/lib.rs index af2dfa52..65090613 100644 --- a/programs/settlement/src/lib.rs +++ b/programs/settlement/src/lib.rs @@ -7,6 +7,7 @@ mod processor; mod reclaim_buffer; mod reclaim_order; mod settle; +mod token; mod transfer_authority; use cow_settlement_interface::{recover_discriminator, SettlementInstruction}; diff --git a/programs/settlement/src/reclaim_buffer.rs b/programs/settlement/src/reclaim_buffer.rs index 30e57d81..d06216f9 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/reclaim_buffer.rs @@ -7,17 +7,17 @@ use cow_settlement_interface::{ data::state::EncodedStateAccount, - instruction::{ - create_buffer::SPL_TOKEN_PROGRAM_ID, reclaim_buffer::ReclaimBufferInput, - InstructionInputParsing, - }, + instruction::{reclaim_buffer::ReclaimBufferInput, InstructionInputParsing}, pda::buffer::find_buffer_pda, Pubkey, Role, SettlementError, }; use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; -use pinocchio_token::{instructions::CloseAccount, state::Account as TokenAccount}; +use pinocchio_token::instructions::CloseAccount; -use crate::processor::with_state_pda_signer; +use crate::{ + processor::with_state_pda_signer, + token::{read_token_account, validate_token_program}, +}; pub fn process_reclaim_buffer( program_id: &Address, @@ -32,9 +32,7 @@ pub fn process_reclaim_buffer( buffers, } = ReclaimBufferInput::parse(instruction_data, accounts)?; - if token_program.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } + let token_program = validate_token_program(token_program)?; with_state_pda_signer(program_id, state_pda, |state_signer| { let reclaim_authority_pubkey: Pubkey = { @@ -58,9 +56,7 @@ pub fn process_reclaim_buffer( return Err(SettlementError::ReclaimBufferNotCanonical.into()); } - let amount = TokenAccount::from_account_view(buffer_pda) - .map_err(|_| ProgramError::InvalidAccountData)? - .amount(); + let amount = read_token_account(token_program, buffer_pda)?.amount; // A token account can't be closed while it still holds a balance, and this // instruction has no mandate to move those tokens elsewhere or destroy them. @@ -70,7 +66,10 @@ pub fn process_reclaim_buffer( } CloseAccount::new(buffer_pda, reclaim_recipient, state_pda) - .invoke_signed(core::slice::from_ref(state_signer))?; + .invoke_signed_with_unverified_program( + core::slice::from_ref(state_signer), + token_program, + )?; } Ok(()) @@ -88,6 +87,7 @@ mod tests { reclaim_buffer_data, NUM_SHARED_ACCOUNTS, }; use cow_settlement_interface::pda::state::state_pda_seeds; + use cow_settlement_interface::token_program::SPL_TOKEN_PROGRAM_ID; use litesvm_token::spl_token::state::{Account as SplTokenAccount, AccountState}; use solana_program_pack::Pack; diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs new file mode 100644 index 00000000..8ed52bf9 --- /dev/null +++ b/programs/settlement/src/token.rs @@ -0,0 +1,338 @@ +//! Token-program validation and token-account reads shared by `CreateBuffer` +//! and `ReclaimBuffer`. +//! +//! Each takes one `token_program` account, validates it with +//! [`validate_token_program`], and issues all of its CPIs against the address +//! that returns. Token-2022 encodes the instructions this program issues +//! exactly as the legacy program does, so only the CPI target changes; nothing +//! else about them depends on which program it is. +//! +//! What does differ is the account data. A Token-2022 account carrying +//! extensions is longer than the base layout, so the legacy reader (which +//! insists on an exact length and the legacy owner) rejects it. Read token +//! accounts through [`read_token_account`], which dispatches on the validated +//! program. + +use cow_settlement_interface::{ + token_program::{is_supported, SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID}, + SettlementError, +}; +use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView, Address}; +use pinocchio_token::{instructions::GetAccountDataSize, state::Mint}; + +/// The length of a token account holding nothing but the base layout, which is +/// every legacy token account and a Token-2022 one carrying no extensions. +const BASE_TOKEN_ACCOUNT_LEN: u64 = pinocchio_token::state::Account::LEN as u64; + +/// Validate that `token_program_account` is a token program this program may +/// issue CPIs against, returning its address for the instruction to target. +/// +/// This is the single gate in front of every token CPI: the callers pass the +/// address it returns to `invoke_*_with_unverified_program`, which skips the +/// program check that this already made. +#[must_use = "the returned address is the program the transfers must target"] +pub fn validate_token_program( + token_program_account: &AccountView, +) -> Result<&Address, ProgramError> { + let address = token_program_account.address(); + if !is_supported(address) { + return Err(ProgramError::IncorrectProgramId); + } + Ok(address) +} + +/// The data length a token account holding `mint` has to be allocated at, +/// under `token_program`. +/// +/// A mint carrying no extension data needs no extension space on the accounts +/// that hold it, which is the base layout. Every legacy mint is exactly that +/// long, and so is a Token-2022 mint without extensions; a Token-2022 mint that +/// carries any is padded out past a token account's base layout to make room +/// for its account-type marker, so nothing that short can be one. Anything +/// shorter still isn't a mint at all — including the native mint, which the +/// token program recognizes by address without reading an account — and +/// `InitializeAccount3` is what rejects the ones that matter, as it always has. +/// +/// Otherwise the token program is asked, the way the associated-token-account +/// program asks it. That keeps the answer authoritative at run time instead of +/// freezing a mint-extension-to-account-extension table into this program, +/// which would need a redeploy every time Token-2022 grows one. +/// +/// Nothing trusts this length for safety, only for liveness: too short and +/// `InitializeAccount3` rejects the account, reverting the whole instruction; +/// too long and the only cost is rent, paid by this instruction's own payer and +/// recovered when the buffer is reclaimed. +// `get_return_data` returns its 1 KiB buffer by value, so keep it in a leaf +// frame of its own rather than the caller's: SBF stack frames are 4 KiB and +// don't grow. +#[inline(never)] +pub fn token_account_len(token_program: &Address, mint: &AccountView) -> Result { + if mint.data_len() <= Mint::LEN { + return Ok(BASE_TOKEN_ACCOUNT_LEN); + } + + // The CPI below targets whatever address it's handed, so re-establish that + // it is a token program at all before handing it the mint. Callers have + // validated it already; this keeps the guarantee local, as + // `read_token_account` does. + if !is_supported(token_program) { + return Err(ProgramError::IncorrectProgramId); + } + + GetAccountDataSize::new(mint).invoke_with_unverified_program(token_program)?; + + // The token program reports the length as return data. That buffer is a + // per-transaction global, so it's the program that last set it which makes + // the value trustworthy. Everything below is defensive: a token program + // that can't answer this query fails the CPI, which aborts the instruction + // without returning here at all. + let reported = get_return_data().ok_or(SettlementError::BufferSizeUnavailable)?; + if reported.program_id() != token_program { + return Err(SettlementError::BufferSizeUnavailable.into()); + } + let length: [u8; 8] = reported + .as_slice() + .try_into() + .map_err(|_| SettlementError::BufferSizeUnavailable)?; + let length = u64::from_le_bytes(length); + // A token account is at least its base layout, whatever its mint carries. + if length < BASE_TOKEN_ACCOUNT_LEN { + return Err(SettlementError::BufferSizeUnavailable.into()); + } + + Ok(length) +} + +/// The base-layout fields of a token account, as read by +/// [`read_token_account`]. +/// +/// Held by value rather than borrowed from the account so the caller can go on +/// to use the same account in a CPI that touches it: a live borrow would make +/// that CPI fail. +pub struct TokenAccount { + pub amount: u64, +} + +/// Read the base fields of the token account at `account`, which must be owned +/// by `token_program`. +/// +/// `token_program` must have come from [`validate_token_program`]; any other +/// address is rejected. The two programs share the base layout, and differ only +/// in what else may follow it, so which reader applies is decided by the +/// program rather than by the data: +/// +/// - under SPL Token the data is exactly the base layout; +/// - under Token-2022 extensions may follow it, and an account that carries any +/// is recognized by the account-type marker sitting just past the base. +pub fn read_token_account( + token_program: &Address, + account: &AccountView, +) -> Result { + if token_program == &SPL_TOKEN_PROGRAM_ID { + let account = pinocchio_token::state::Account::from_account_view(account)?; + Ok(TokenAccount { + amount: account.amount(), + }) + } else if token_program == &TOKEN_2022_PROGRAM_ID { + let account = pinocchio_token_2022::state::Account::from_account_view(account)?; + Ok(TokenAccount { + amount: account.amount(), + }) + } else { + Err(ProgramError::IncorrectProgramId) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cow_settlement_interface::instruction::fixtures::{fake_account, fake_account_owned_by}; + use cow_settlement_interface::token_program::SUPPORTED_TOKEN_PROGRAMS; + use pinocchio_token_2022::state::AccountType; + + /// An address that is not a token program. + const UNRELATED: Address = Address::new_from_array([0x99; 32]); + + /// The length of a token account holding nothing but the base layout. Both + /// programs share it: it is Token-2022's `BASE_LEN` and the whole of a + /// legacy account. + const BASE_LEN: usize = pinocchio_token::state::Account::LEN; + + /// The base layout of a token account holding `amount` of `mint` for + /// `owner`, with every other field left zeroed. + fn base_layout(mint: Address, owner: Address, amount: u64) -> Vec { + let mut data = vec![0u8; BASE_LEN]; + data[..32].copy_from_slice(mint.as_array()); + data[32..64].copy_from_slice(owner.as_array()); + data[64..72].copy_from_slice(&amount.to_le_bytes()); + data + } + + /// The addresses the off-chain crate offers are the ones the on-chain token + /// crates CPI into. Both sides name the same programs from their own + /// dependency, so this is what keeps them from drifting apart. + #[test] + fn interface_and_pinocchio_agree_on_the_program_ids() { + assert_eq!(SPL_TOKEN_PROGRAM_ID, pinocchio_token::ID); + assert_eq!(TOKEN_2022_PROGRAM_ID, pinocchio_token_2022::ID); + } + + /// The base layout is the same under both programs, so one reader's idea of + /// its length is the other's too. + #[test] + fn both_programs_share_the_base_layout_length() { + assert_eq!(BASE_LEN, pinocchio_token_2022::state::Account::BASE_LEN); + } + + /// A mint carrying no extension data — every legacy mint, and a Token-2022 + /// mint without extensions — needs only a base-layout token account, and + /// that is settled without asking the token program. + #[test] + fn token_account_len_is_the_base_layout_for_a_plain_mint() { + for program in SUPPORTED_TOKEN_PROGRAMS { + let mint = fake_account_owned_by(UNRELATED, program, &[0u8; Mint::LEN]); + assert_eq!( + token_account_len(&program, &mint), + Ok(BASE_TOKEN_ACCOUNT_LEN), + "a base-layout mint should need a base-layout account under {program}", + ); + } + } + + /// An account too short to be a mint at all still gets the base layout, + /// leaving `InitializeAccount3` to reject it — which is also how the native + /// mint works, since the token program knows it by address and litesvm + /// leaves the account itself absent. + #[test] + fn token_account_len_is_the_base_layout_for_a_too_short_account() { + let mint = fake_account(UNRELATED); + assert_eq!( + token_account_len(&SPL_TOKEN_PROGRAM_ID, &mint), + Ok(BASE_TOKEN_ACCOUNT_LEN), + ); + } + + /// A longer mint has to be asked about, and off-chain there is nobody to + /// ask: the CPI is a no-op and no return data comes back. On-chain a token + /// program that can't answer aborts the instruction instead of returning + /// here, so this is the error's only reachable path. + #[test] + fn token_account_len_reports_unavailable_without_an_answer() { + let mint = fake_account_owned_by(UNRELATED, TOKEN_2022_PROGRAM_ID, &[0u8; Mint::LEN + 1]); + assert_eq!( + token_account_len(&TOKEN_2022_PROGRAM_ID, &mint).err(), + Some(SettlementError::BufferSizeUnavailable.into()), + ); + } + + /// The query is a CPI, so an unsupported program is turned away before it + /// is handed the mint. + #[test] + fn token_account_len_rejects_an_unsupported_program() { + let mint = fake_account_owned_by(UNRELATED, UNRELATED, &[0u8; Mint::LEN + 1]); + assert_eq!( + token_account_len(&UNRELATED, &mint).err(), + Some(ProgramError::IncorrectProgramId), + ); + } + + #[test] + fn validate_token_program_accepts_every_supported_program() { + for program in SUPPORTED_TOKEN_PROGRAMS { + let account = fake_account(program); + assert_eq!(validate_token_program(&account), Ok(&program)); + } + } + + #[test] + fn validate_token_program_rejects_unrelated_program() { + let account = fake_account(UNRELATED); + assert_eq!( + validate_token_program(&account), + Err(ProgramError::IncorrectProgramId), + ); + } + + /// A plain account, the only shape the legacy program has and the shape a + /// Token-2022 account without extensions also takes, reads under either. + #[test] + fn read_token_account_reads_a_base_layout_account() { + let mint = Address::new_from_array([0x11; 32]); + let owner = Address::new_from_array([0x22; 32]); + for program in SUPPORTED_TOKEN_PROGRAMS { + let account = + fake_account_owned_by(UNRELATED, program, &base_layout(mint, owner, 4_200)); + let read = read_token_account(&program, &account) + .unwrap_or_else(|error| panic!("{program} account should read: {error:?}")); + assert_eq!(read.amount, 4_200); + } + } + + /// The point of the Token-2022 reader: an account whose extensions push it + /// past the base layout still reads, where the legacy reader's exact-length + /// check would have rejected it. + #[test] + fn read_token_account_reads_past_token_2022_extensions() { + let mint = Address::new_from_array([0x33; 32]); + let owner = Address::new_from_array([0x44; 32]); + let mut data = base_layout(mint, owner, 7); + // Extensions are preceded by the account-type marker, which is what + // distinguishes a longer account from a mint of the same size. + data.push(AccountType::Account as u8); + data.extend_from_slice(&[0xab; 16]); + + let account = fake_account_owned_by(UNRELATED, TOKEN_2022_PROGRAM_ID, &data); + let read = read_token_account(&TOKEN_2022_PROGRAM_ID, &account) + .expect("an extended Token-2022 account should read"); + assert_eq!(read.amount, 7); + } + + /// An over-long account marked as a mint rather than a token account is + /// still rejected, so the tolerance for extensions doesn't let a mint be + /// read as if it held a balance. + #[test] + fn read_token_account_rejects_an_extended_mint() { + let mut data = base_layout(UNRELATED, UNRELATED, 7); + data.push(AccountType::Mint as u8); + + let account = fake_account_owned_by(UNRELATED, TOKEN_2022_PROGRAM_ID, &data); + assert_eq!( + read_token_account(&TOKEN_2022_PROGRAM_ID, &account).err(), + Some(ProgramError::InvalidAccountData), + ); + } + + #[test] + fn read_token_account_rejects_unvalidated_program() { + // A well-formed legacy token account, so the rejection can only come + // from the program address. + let account = fake_account_owned_by( + UNRELATED, + SPL_TOKEN_PROGRAM_ID, + &base_layout(UNRELATED, UNRELATED, 0), + ); + assert_eq!( + read_token_account(&UNRELATED, &account).err(), + Some(ProgramError::IncorrectProgramId), + ); + } + + /// Each reader is tied to its own program: an otherwise well-formed account + /// owned by one token program can't be read as if it belonged to the other, + /// which is what stops an instruction from mixing the two. + #[test] + fn read_token_account_rejects_the_other_programs_account() { + for [program, other] in [ + [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID], + [TOKEN_2022_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID], + ] { + let account = + fake_account_owned_by(UNRELATED, other, &base_layout(UNRELATED, UNRELATED, 0)); + assert_eq!( + read_token_account(&program, &account).err(), + Some(ProgramError::InvalidAccountData), + "an account owned by {other} should not read under {program}", + ); + } + } +} From e499b86cc42dd277da6a0b7e4fc37846b6102b9c Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:23:35 +0900 Subject: [PATCH 02/36] Take the legacy program id from the Token-2022 crate `spl-token-interface` was a dependency of the interface crate for one thing: a 32-byte constant naming a program this code never calls directly. `spl-token-2022-interface` already carries that address in `inline_spl_token`, which exists precisely so a program that has to recognize both doesn't grow a second dependency for the one it only compares against. Both ids now come from there, and the legacy crate leaves the interface crate's dependency graph -- and with it the settlement program's. `instruction::settle` re-exported the same id straight from the legacy crate, so it moves to `token_program` too. That was the second import keeping the dependency alive, not a cosmetic change. The `.so` is byte-identical at 51,056 bytes: the constant was already inlined, so this narrows the dependency graph rather than the program. The program crate keeps `pinocchio-token`. Its state readers are not parameterized over the token program the way 0.7's instruction builders are -- `pinocchio_token_2022::state::Account` hardcodes an owner check against Token-2022 in all of its safe constructors -- so reading a legacy account without it means `from_bytes_unchecked` plus hand-rolled owner and length checks, in exchange for a crate `pinocchio-token-2022` depends on anyway. `test-cli` keeps the legacy crate as well. It talks to no other program, and the two crates' `native_mint` are different addresses, so swapping them there is a change to make deliberately rather than in passing. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 1 - interface/Cargo.toml | 1 - interface/src/instruction/settle/mod.rs | 2 +- interface/src/token_program.rs | 6 +++++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f41caff..005e230b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -911,7 +911,6 @@ dependencies = [ "solana-sha256-hasher", "solana-system-interface 3.2.0", "spl-token-2022-interface 3.1.1", - "spl-token-interface 3.0.0", ] [[package]] diff --git a/interface/Cargo.toml b/interface/Cargo.toml index a922b381..da903ed4 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -31,7 +31,6 @@ solana-sdk-ids.workspace = true solana-sha256-hasher.workspace = true solana-system-interface.workspace = true spl-token-2022-interface.workspace = true -spl-token-interface.workspace = true [dev-dependencies] hex-literal.workspace = true diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index 85ce25e0..a8450ee0 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -3,8 +3,8 @@ use solana_program_error::ProgramError; +pub use crate::token_program::SPL_TOKEN_PROGRAM_ID; pub use solana_sdk_ids::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID; -pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; mod begin; mod finalize; diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index 5a72adad..498dfc32 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -14,7 +14,11 @@ use crate::Pubkey; /// The legacy SPL Token program. -pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; +/// +/// Taken from the Token-2022 crate, which carries the address precisely so a +/// program that has to recognize both doesn't grow a second dependency for the +/// one it never calls directly. +pub use spl_token_2022_interface::inline_spl_token::ID as SPL_TOKEN_PROGRAM_ID; /// The SPL Token-2022 program. Its instruction encoding is a superset of the /// legacy program's, so the instructions this program issues are byte-identical From 881178bd6fc5a2c85b3f858c957fcd1255482dfa Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:47:00 +0900 Subject: [PATCH 03/36] fixes from final manual review --- interface/src/lib.rs | 3 +- interface/src/token_program.rs | 26 +---- programs/settlement/src/create_buffer.rs | 4 - programs/settlement/src/token.rs | 115 +++-------------------- 4 files changed, 15 insertions(+), 133 deletions(-) diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 3e133bd0..ca3606b9 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -236,8 +236,7 @@ pub enum SettlementError { OrderCreatedOnChainMismatch = 38, /// `CreateBuffer` asked the token program how long a token account for a /// mint has to be and couldn't read the answer, so it can't size the - /// buffer. Defensive: a token program that fails this query aborts the - /// instruction on its own. + /// buffer. BufferSizeUnavailable = 39, } diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index 498dfc32..1e7ee58a 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -1,36 +1,18 @@ -//! The token programs a buffer may be created under. -//! -//! `CreateBuffer` and `ReclaimBuffer` take a single `token_program` account and -//! issue every one of their CPIs against it. That account has to be one of -//! [`SUPPORTED_TOKEN_PROGRAMS`], which is what [`is_supported`] checks. -//! -//! Because the program account is shared by the whole instruction, the mints an -//! instruction touches must all live under the same token program: a legacy SPL -//! mint and a Token-2022 mint can't have their buffers created by one -//! `CreateBuffer`. Splitting them across two is the caller's job. -//! -//! `BeginSettle` and `FinalizeSettle` still accept only the legacy program. +//! Utilities related to the token programs supported by the settlement program. use crate::Pubkey; /// The legacy SPL Token program. -/// -/// Taken from the Token-2022 crate, which carries the address precisely so a -/// program that has to recognize both doesn't grow a second dependency for the -/// one it never calls directly. pub use spl_token_2022_interface::inline_spl_token::ID as SPL_TOKEN_PROGRAM_ID; -/// The SPL Token-2022 program. Its instruction encoding is a superset of the -/// legacy program's, so the instructions this program issues are byte-identical -/// either way and only the CPI target changes. +/// The SPL Token-2022 program. pub use spl_token_2022_interface::ID as TOKEN_2022_PROGRAM_ID; /// Every token program a token-moving instruction accepts, in no particular /// order. pub const SUPPORTED_TOKEN_PROGRAMS: [Pubkey; 2] = [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]; -/// Whether `address` is a token program buffers may be created under, that is, -/// whether it is one of [`SUPPORTED_TOKEN_PROGRAMS`]. +/// Whether `address` is a supported token program pub fn is_supported(address: &Pubkey) -> bool { SUPPORTED_TOKEN_PROGRAMS.contains(address) } @@ -52,8 +34,6 @@ mod tests { assert!(!is_supported(&pubkey_from_seed("not a token program"))); } - /// The list is two distinct programs, so it can't have been built from one - /// program repeated. #[test] fn supported_programs_are_distinct() { assert_ne!(SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID); diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/create_buffer.rs index 653fb40a..e39d32c7 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/create_buffer.rs @@ -38,10 +38,6 @@ pub fn process_create_buffer( // is a token account, so it's assigned to the token program rather than // to the settlement program. // - // The buffer is sized for the mint it will hold: a Token-2022 mint - // whose extensions require matching ones on its token accounts needs - // room for them, and `token_account_len` is what works out how much. - // // We don't validate `mint` here. `InitializeAccount3` requires a real, // token-program-owned mint (and special-cases the native mint), so a // check of our own would be redundant. diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 8ed52bf9..1458fe18 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -1,17 +1,4 @@ -//! Token-program validation and token-account reads shared by `CreateBuffer` -//! and `ReclaimBuffer`. -//! -//! Each takes one `token_program` account, validates it with -//! [`validate_token_program`], and issues all of its CPIs against the address -//! that returns. Token-2022 encodes the instructions this program issues -//! exactly as the legacy program does, so only the CPI target changes; nothing -//! else about them depends on which program it is. -//! -//! What does differ is the account data. A Token-2022 account carrying -//! extensions is longer than the base layout, so the legacy reader (which -//! insists on an exact length and the legacy owner) rejects it. Read token -//! accounts through [`read_token_account`], which dispatches on the validated -//! program. +//! Token-program validation and token-account reads use cow_settlement_interface::{ token_program::{is_supported, SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID}, @@ -20,17 +7,13 @@ use cow_settlement_interface::{ use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView, Address}; use pinocchio_token::{instructions::GetAccountDataSize, state::Mint}; -/// The length of a token account holding nothing but the base layout, which is -/// every legacy token account and a Token-2022 one carrying no extensions. +/// The length of a SPL token program account. Token2022 extensions may make +/// the actual token account longer than this. const BASE_TOKEN_ACCOUNT_LEN: u64 = pinocchio_token::state::Account::LEN as u64; /// Validate that `token_program_account` is a token program this program may /// issue CPIs against, returning its address for the instruction to target. -/// -/// This is the single gate in front of every token CPI: the callers pass the -/// address it returns to `invoke_*_with_unverified_program`, which skips the -/// program check that this already made. -#[must_use = "the returned address is the program the transfers must target"] +#[must_use = "not consuming skips validation"] pub fn validate_token_program( token_program_account: &AccountView, ) -> Result<&Address, ProgramError> { @@ -41,51 +24,18 @@ pub fn validate_token_program( Ok(address) } -/// The data length a token account holding `mint` has to be allocated at, -/// under `token_program`. -/// -/// A mint carrying no extension data needs no extension space on the accounts -/// that hold it, which is the base layout. Every legacy mint is exactly that -/// long, and so is a Token-2022 mint without extensions; a Token-2022 mint that -/// carries any is padded out past a token account's base layout to make room -/// for its account-type marker, so nothing that short can be one. Anything -/// shorter still isn't a mint at all — including the native mint, which the -/// token program recognizes by address without reading an account — and -/// `InitializeAccount3` is what rejects the ones that matter, as it always has. -/// -/// Otherwise the token program is asked, the way the associated-token-account -/// program asks it. That keeps the answer authoritative at run time instead of -/// freezing a mint-extension-to-account-extension table into this program, -/// which would need a redeploy every time Token-2022 grows one. -/// -/// Nothing trusts this length for safety, only for liveness: too short and -/// `InitializeAccount3` rejects the account, reverting the whole instruction; -/// too long and the only cost is rent, paid by this instruction's own payer and -/// recovered when the buffer is reclaimed. -// `get_return_data` returns its 1 KiB buffer by value, so keep it in a leaf -// frame of its own rather than the caller's: SBF stack frames are 4 KiB and -// don't grow. +/// The data length a token account holding `mint` has to be allocated at. +/// It is assumed that `token_program` has already been validated with [`validate_token_program`]. #[inline(never)] pub fn token_account_len(token_program: &Address, mint: &AccountView) -> Result { + // If the mint is of base SPL Mint length, the token accounts must be of base length accordingly. if mint.data_len() <= Mint::LEN { return Ok(BASE_TOKEN_ACCOUNT_LEN); } - // The CPI below targets whatever address it's handed, so re-establish that - // it is a token program at all before handing it the mint. Callers have - // validated it already; this keeps the guarantee local, as - // `read_token_account` does. - if !is_supported(token_program) { - return Err(ProgramError::IncorrectProgramId); - } - + // SPL token provides a function to get the actual required account data size GetAccountDataSize::new(mint).invoke_with_unverified_program(token_program)?; - // The token program reports the length as return data. That buffer is a - // per-transaction global, so it's the program that last set it which makes - // the value trustworthy. Everything below is defensive: a token program - // that can't answer this query fails the CPI, which aborts the instruction - // without returning here at all. let reported = get_return_data().ok_or(SettlementError::BufferSizeUnavailable)?; if reported.program_id() != token_program { return Err(SettlementError::BufferSizeUnavailable.into()); @@ -105,25 +55,15 @@ pub fn token_account_len(token_program: &Address, mint: &AccountView) -> Result< /// The base-layout fields of a token account, as read by /// [`read_token_account`]. -/// -/// Held by value rather than borrowed from the account so the caller can go on -/// to use the same account in a CPI that touches it: a live borrow would make -/// that CPI fail. +/// For our purposes, we only need the `amount`. pub struct TokenAccount { pub amount: u64, } /// Read the base fields of the token account at `account`, which must be owned /// by `token_program`. -/// -/// `token_program` must have come from [`validate_token_program`]; any other -/// address is rejected. The two programs share the base layout, and differ only -/// in what else may follow it, so which reader applies is decided by the -/// program rather than by the data: -/// -/// - under SPL Token the data is exactly the base layout; -/// - under Token-2022 extensions may follow it, and an account that carries any -/// is recognized by the account-type marker sitting just past the base. +/// It is assumed that `token_program` has already been validated with [`validate_token_program`], +/// or else a program error will be thrown. pub fn read_token_account( token_program: &Address, account: &AccountView, @@ -184,9 +124,6 @@ mod tests { assert_eq!(BASE_LEN, pinocchio_token_2022::state::Account::BASE_LEN); } - /// A mint carrying no extension data — every legacy mint, and a Token-2022 - /// mint without extensions — needs only a base-layout token account, and - /// that is settled without asking the token program. #[test] fn token_account_len_is_the_base_layout_for_a_plain_mint() { for program in SUPPORTED_TOKEN_PROGRAMS { @@ -199,10 +136,6 @@ mod tests { } } - /// An account too short to be a mint at all still gets the base layout, - /// leaving `InitializeAccount3` to reject it — which is also how the native - /// mint works, since the token program knows it by address and litesvm - /// leaves the account itself absent. #[test] fn token_account_len_is_the_base_layout_for_a_too_short_account() { let mint = fake_account(UNRELATED); @@ -212,10 +145,6 @@ mod tests { ); } - /// A longer mint has to be asked about, and off-chain there is nobody to - /// ask: the CPI is a no-op and no return data comes back. On-chain a token - /// program that can't answer aborts the instruction instead of returning - /// here, so this is the error's only reachable path. #[test] fn token_account_len_reports_unavailable_without_an_answer() { let mint = fake_account_owned_by(UNRELATED, TOKEN_2022_PROGRAM_ID, &[0u8; Mint::LEN + 1]); @@ -225,17 +154,6 @@ mod tests { ); } - /// The query is a CPI, so an unsupported program is turned away before it - /// is handed the mint. - #[test] - fn token_account_len_rejects_an_unsupported_program() { - let mint = fake_account_owned_by(UNRELATED, UNRELATED, &[0u8; Mint::LEN + 1]); - assert_eq!( - token_account_len(&UNRELATED, &mint).err(), - Some(ProgramError::IncorrectProgramId), - ); - } - #[test] fn validate_token_program_accepts_every_supported_program() { for program in SUPPORTED_TOKEN_PROGRAMS { @@ -253,8 +171,6 @@ mod tests { ); } - /// A plain account, the only shape the legacy program has and the shape a - /// Token-2022 account without extensions also takes, reads under either. #[test] fn read_token_account_reads_a_base_layout_account() { let mint = Address::new_from_array([0x11; 32]); @@ -268,9 +184,6 @@ mod tests { } } - /// The point of the Token-2022 reader: an account whose extensions push it - /// past the base layout still reads, where the legacy reader's exact-length - /// check would have rejected it. #[test] fn read_token_account_reads_past_token_2022_extensions() { let mint = Address::new_from_array([0x33; 32]); @@ -287,9 +200,6 @@ mod tests { assert_eq!(read.amount, 7); } - /// An over-long account marked as a mint rather than a token account is - /// still rejected, so the tolerance for extensions doesn't let a mint be - /// read as if it held a balance. #[test] fn read_token_account_rejects_an_extended_mint() { let mut data = base_layout(UNRELATED, UNRELATED, 7); @@ -317,9 +227,6 @@ mod tests { ); } - /// Each reader is tied to its own program: an otherwise well-formed account - /// owned by one token program can't be read as if it belonged to the other, - /// which is what stops an instruction from mixing the two. #[test] fn read_token_account_rejects_the_other_programs_account() { for [program, other] in [ From d690f26d11b3f55b2674e815cc84af0696fac700 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:05:18 +0900 Subject: [PATCH 04/36] switch fully away from legacy spl interface --- Cargo.lock | 2 +- Cargo.toml | 1 - DESIGN.md | 6 +- test-cli/Cargo.toml | 2 +- test-cli/src/cmd/create_order.rs | 2 +- test-cli/src/instructions.rs | 14 +- test-cli/src/token.rs | 279 ++++++++++++++++++++++++------- 7 files changed, 233 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a2f1f67..7c2a46fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,7 +930,7 @@ dependencies = [ "solana-system-interface 3.2.0", "spl-associated-token-account-interface", "spl-token", - "spl-token-interface 3.0.0", + "spl-token-2022-interface 3.1.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0e30342a..127d1923 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,6 @@ solana-system-interface = "3" spl-associated-token-account-interface = "2" spl-token = "9" spl-token-2022-interface = "3" -spl-token-interface = "3" [workspace.metadata.cli] # Used to help solana-verify identify the correct build image. diff --git a/DESIGN.md b/DESIGN.md index bd79007a..e0412bc2 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -363,4 +363,8 @@ The main reason to prefer the SOL flow described here is the handling of the ren ## Token 2022 -The settlement program will natively support [Token-2022](https://www.solana-program.com/docs/token-2022) tokens. All operations available to standard tokens will be usable for tokens based on this standard, and no major front-end or back-end changes are expected in order to support the majority of tokens based on this standard. +The settlement program natively supports [Token-2022](https://www.solana-program.com/docs/token-2022) tokens. All token +extensions will be supported. + +Only one token program can be supplied to an instruction at a time. If mints from two separate token programs are required, +the client needs to divide it into separate instructions. diff --git a/test-cli/Cargo.toml b/test-cli/Cargo.toml index 0938df22..b4aa0dc2 100644 --- a/test-cli/Cargo.toml +++ b/test-cli/Cargo.toml @@ -35,7 +35,7 @@ solana-sdk.workspace = true solana-system-interface = { workspace = true, features = ["bincode"] } spl-associated-token-account-interface.workspace = true spl-token.workspace = true -spl-token-interface.workspace = true +spl-token-2022-interface.workspace = true [lints] workspace = true diff --git a/test-cli/src/cmd/create_order.rs b/test-cli/src/cmd/create_order.rs index 34652034..3db66ec2 100644 --- a/test-cli/src/cmd/create_order.rs +++ b/test-cli/src/cmd/create_order.rs @@ -150,7 +150,7 @@ fn execute(ctx: Context, parsed: ParsedOrder, common: CommonArgs) -> anyhow::Res // Approve the settlement state PDA to pull sell tokens on the user's behalf. ixs.push(crate::instructions::approve( &ctx.program_id, - &sell.ta, + &sell, &ctx.payer.pubkey(), sell_amount, )?); diff --git a/test-cli/src/instructions.rs b/test-cli/src/instructions.rs index 665b373a..828fe281 100644 --- a/test-cli/src/instructions.rs +++ b/test-cli/src/instructions.rs @@ -5,7 +5,7 @@ use anyhow::Context as _; use cow_settlement_client::cow_settlement_interface::{pda::state::find_state_pda, Pubkey}; use solana_instruction::Instruction; use solana_rpc_client::rpc_client::RpcClient; -use spl_token_interface::instruction::{self as token_ix}; +use spl_token_2022_interface::instruction::{self as token_ix}; /// Build instructions that wrap `amount` lamports into the payer's WSOL ATA. /// @@ -27,26 +27,26 @@ pub fn wrap_sol( )); ixs.push( - token_ix::sync_native(&spl_token_interface::id(), &wsol.ta) + token_ix::sync_native(&wsol.token_program, &wsol.ta) .context("failed to build SyncNative instruction")?, ); Ok((wsol.ta, ixs)) } -/// Build an `Approve` instruction delegating `amount` tokens on `token_account` -/// to the PDA derived from `program_id`. +/// Build an `Approve` instruction delegating `amount` of `token` to the PDA +/// derived from `program_id`. pub fn approve( program_id: &Pubkey, - token_account: &Pubkey, + token: &token::ResolvedToken, owner: &Pubkey, amount: u64, ) -> anyhow::Result { let (settlement_pda, _) = find_state_pda(program_id); token_ix::approve( - &spl_token_interface::id(), - token_account, + &token.token_program, + &token.ta, &settlement_pda, owner, &[], diff --git a/test-cli/src/token.rs b/test-cli/src/token.rs index 89505100..5023599d 100644 --- a/test-cli/src/token.rs +++ b/test-cli/src/token.rs @@ -1,21 +1,20 @@ //! Token resolution helpers: converts a user-supplied token string (alias, mint address, -//! or token-account address) into an SPL token account address and decimal count. +//! or token-account address) into an SPL token account address, the token program that +//! owns it, and the decoded mint. //! //! Entry point: [`resolve`]. use anyhow::Context as _; -use cow_settlement_client::cow_settlement_interface::Pubkey; +use cow_settlement_client::cow_settlement_interface::{token_program, Pubkey}; use solana_instruction::Instruction; -use solana_program_pack::Pack; use solana_pubkey::pubkey; use solana_rpc_client::rpc_client::RpcClient; -use solana_sdk::account::ReadableAccount; -use spl_associated_token_account_interface::address::{ - get_associated_token_address, get_associated_token_address_with_program_id, -}; +use solana_sdk::account::{Account, ReadableAccount}; +use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; use spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent; -use spl_token_interface::native_mint; -use spl_token_interface::state::{Account as TokenAccount, Mint}; +use spl_token_2022_interface::extension::StateWithExtensions; +use spl_token_2022_interface::native_mint; +use spl_token_2022_interface::state::{Account as TokenAccount, Mint}; /// Inline registry of recognised token symbols. /// Avoids an RPC round-trip for well-known mints whose decimals are fixed. @@ -50,6 +49,11 @@ pub struct ResolvedToken { pub mint: Pubkey, /// The actual mint data pub mint_data: Mint, + /// The token program owning both `mint` and `ta` — one of + /// [`token_program::SUPPORTED_TOKEN_PROGRAMS`]. Any instruction touching + /// `ta` has to be built against it, so it travels with the resolved token + /// rather than being assumed. + pub token_program: Pubkey, /// `Some(owner)` when `ta` does not yet exist on-chain. Call with the /// transaction fee payer to build the instruction that creates it. create_ata: Option, @@ -64,7 +68,7 @@ impl ResolvedToken { payer, &owner, &self.mint, - &spl_token_interface::id(), + &self.token_program, )) } } @@ -73,20 +77,9 @@ impl ResolvedToken { pub fn resolve(rpc: &RpcClient, owner: &Pubkey, token_str: &str) -> anyhow::Result { let upper = token_str.to_uppercase(); - // 1. `"SOL"` / `"WSOL"` — payer's WSOL ATA, 9 decimals, no RPC call needed. + // 1. `"SOL"` / `"WSOL"` — payer's ATA for the native mint. if matches!(upper.as_str(), "SOL" | "WSOL") { - let wsol_mint: Pubkey = native_mint::id(); - let wsol_ata = get_associated_token_address_with_program_id( - owner, - &wsol_mint, - &spl_token_interface::id(), - ); - return Ok(ResolvedToken { - ta: wsol_ata, - mint: wsol_mint, - create_ata: determine_create_ata(rpc, &wsol_mint, owner)?, - mint_data: fetch_mint_data(rpc, &wsol_mint)?, - }); + return resolve_from_mint(rpc, owner, &native_mint::ID); } // 2. Base58 mint or token-account address — fetches decimals from the mint, and possibly the token account owner. @@ -100,17 +93,7 @@ pub fn resolve(rpc: &RpcClient, owner: &Pubkey, token_str: &str) -> anyhow::Resu .with_context(|| "failed to fetch genesis hash (is the RPC URL correct?)")? .to_string(); if let Some(known) = known_token(&genesis_hash, &upper) { - let ata = get_associated_token_address_with_program_id( - owner, - &known.mint, - &spl_token_interface::id(), - ); - return Ok(ResolvedToken { - ta: ata, - create_ata: determine_create_ata(rpc, &known.mint, owner)?, - mint: known.mint, - mint_data: fetch_mint_data(rpc, &known.mint)?, - }); + return resolve_from_mint(rpc, owner, &known.mint); } anyhow::bail!( @@ -132,13 +115,15 @@ pub fn resolve_from_token_account( ) })?; - let decoded_account = TokenAccount::unpack(account.data()) + let token_program = token_program_of(&account, token_account)?; + let decoded_account = unpack_token_account(account.data()) .with_context(|| format!("account {token_account} is not a token account"))?; Ok(ResolvedToken { ta: *token_account, mint: decoded_account.mint, - mint_data: fetch_mint_data(rpc, &decoded_account.mint)?, + mint_data: fetch_mint(rpc, &decoded_account.mint)?.1, + token_program, // The account was just fetched and unpacked above, so it already exists. create_ata: None, }) @@ -157,31 +142,32 @@ pub fn interpret_token_from_user_input( .get_account(token_account_or_mint) .with_context(|| format!("account {token_account_or_mint} not found on-chain"))?; - anyhow::ensure!( - account.owner == spl_token_interface::id(), - "{token_account_or_mint} is not owned by the token program (owner: {})", - account.owner - ); + let token_program = token_program_of(&account, token_account_or_mint)?; - if let Ok(token_account) = TokenAccount::unpack(&account.data) { + // Token accounts are tried first: a mint carrying enough extension data to + // reach the token account length is only told apart from an account by the + // account-type byte, which `unpack_token_account` checks. + if let Some(token_account) = unpack_token_account(account.data()) { Ok(ResolvedToken { ta: *token_account_or_mint, mint: token_account.mint, - mint_data: fetch_mint_data(rpc, &token_account.mint)?, + mint_data: fetch_mint(rpc, &token_account.mint)?.1, + token_program, // The account was just fetched and unpacked above, so it already exists. create_ata: None, }) - } else if let Ok(mint) = Mint::unpack(&account.data) { - let ata = get_associated_token_address_with_program_id( + } else if let Some(mint) = unpack_mint(account.data()) { + let ta = get_associated_token_address_with_program_id( owner, token_account_or_mint, - &spl_token_interface::id(), + &token_program, ); Ok(ResolvedToken { - ta: ata, + ta, mint_data: mint, mint: *token_account_or_mint, - create_ata: determine_create_ata(rpc, token_account_or_mint, owner)?, + token_program, + create_ata: determine_create_ata(rpc, &ta, owner)?, }) } else { anyhow::bail!( @@ -192,31 +178,202 @@ pub fn interpret_token_from_user_input( } } +/// Resolve `mint` to `owner`'s associated token account, derived under whichever +/// token program owns the mint. +fn resolve_from_mint( + rpc: &RpcClient, + owner: &Pubkey, + mint: &Pubkey, +) -> anyhow::Result { + let (token_program, mint_data) = fetch_mint(rpc, mint)?; + let ta = get_associated_token_address_with_program_id(owner, mint, &token_program); + + Ok(ResolvedToken { + ta, + mint: *mint, + mint_data, + token_program, + create_ata: determine_create_ata(rpc, &ta, owner)?, + }) +} + +/// The token program owning `account`, rejecting anything the settlement +/// program cannot move tokens with. +fn token_program_of(account: &Account, address: &Pubkey) -> anyhow::Result { + let owner = *account.owner(); + anyhow::ensure!( + token_program::is_supported(&owner), + "{address} is not owned by a supported token program (owner: {owner})", + ); + Ok(owner) +} + /// Used to set `create_ata` on `ResolvedToken`. Returns the ATA `owner` when the /// account still needs to be created. fn determine_create_ata( rpc: &RpcClient, - mint: &Pubkey, + token_account_address: &Pubkey, owner: &Pubkey, ) -> anyhow::Result> { - let token_account_address = get_associated_token_address(owner, mint); - let Ok(data) = rpc.get_account_data(&token_account_address) else { + let Ok(data) = rpc.get_account_data(token_account_address) else { return Ok(Some(*owner)); }; - TokenAccount::unpack(&data) - .map(|_| None) - .map_err(|_| anyhow::anyhow!("account {token_account_address} is not a token account")) + anyhow::ensure!( + unpack_token_account(&data).is_some(), + "account {token_account_address} is not a token account" + ); + Ok(None) } -fn fetch_mint_data(rpc: &RpcClient, mint: &Pubkey) -> anyhow::Result { - let data = rpc - .get_account_data(mint) +/// Fetch `mint` and return the token program owning it alongside its decoded state. +fn fetch_mint(rpc: &RpcClient, mint: &Pubkey) -> anyhow::Result<(Pubkey, Mint)> { + let account = rpc + .get_account(mint) .with_context(|| format!("mint account {mint} not found"))?; - if let Ok(mint_data) = Mint::unpack(&data) { - Ok(mint_data) - } else { - Err(anyhow::anyhow!("account {mint} is not a mint")) + let token_program = token_program_of(&account, mint)?; + let mint_data = + unpack_mint(account.data()).with_context(|| format!("account {mint} is not a mint"))?; + + Ok((token_program, mint_data)) +} + +/// Decode the base token-account state, skipping over any Token-2022 extensions. +/// The legacy layout is the same data without the extension suffix, so this +/// covers both token programs. +fn unpack_token_account(data: &[u8]) -> Option { + StateWithExtensions::::unpack(data) + .ok() + .map(|state| state.base) +} + +/// Decode the base mint state, skipping over any Token-2022 extensions. See +/// [`unpack_token_account`]. +fn unpack_mint(data: &[u8]) -> Option { + StateWithExtensions::::unpack(data) + .ok() + .map(|state| state.base) +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_program_pack::Pack as _; + use spl_token_2022_interface::extension::mint_close_authority::MintCloseAuthority; + use spl_token_2022_interface::extension::{ + BaseStateWithExtensionsMut as _, ExtensionType, StateWithExtensionsMut, + }; + use spl_token_2022_interface::state::AccountState; + + /// A mint as the legacy token program stores it: exactly `Mint::LEN` bytes. + fn legacy_mint(decimals: u8) -> Vec { + let mint = Mint { + decimals, + is_initialized: true, + ..Default::default() + }; + let mut data = vec![0u8; Mint::LEN]; + mint.pack_into_slice(&mut data); + data + } + + /// A token account as the legacy token program stores it. + fn legacy_token_account(mint: Pubkey) -> Vec { + let account = TokenAccount { + mint, + owner: Pubkey::new_unique(), + state: AccountState::Initialized, + ..Default::default() + }; + let mut data = vec![0u8; TokenAccount::LEN]; + account.pack_into_slice(&mut data); + data + } + + /// A Token-2022 mint carrying one extension, which pads it past + /// `TokenAccount::LEN` and appends the account-type byte. + fn extended_mint(decimals: u8) -> Vec { + let len = + ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority]) + .expect("mint length with a close authority"); + let mut data = vec![0u8; len]; + + let mut state = + StateWithExtensionsMut::::unpack_uninitialized(&mut data).expect("empty mint"); + state + .init_extension::(true) + .expect("close authority extension"); + state.base = Mint { + decimals, + is_initialized: true, + ..Default::default() + }; + state.pack_base(); + state.init_account_type().expect("account type"); + + data + } + + #[test] + fn unpacks_legacy_mint_and_token_account() { + assert_eq!(unpack_mint(&legacy_mint(6)).expect("mint").decimals, 6); + + let mint = Pubkey::new_unique(); + assert_eq!( + unpack_token_account(&legacy_token_account(mint)) + .expect("token account") + .mint, + mint, + ); + } + + #[test] + fn unpacks_token_2022_mint_with_extensions() { + // `Mint::unpack` rejects this outright: it insists on exactly `Mint::LEN`. + assert_eq!(unpack_mint(&extended_mint(2)).expect("mint").decimals, 2); + } + + #[test] + fn extended_mint_is_not_mistaken_for_a_token_account() { + // It is longer than `TokenAccount::LEN`, so only the account-type byte + // tells the two apart — which is why `interpret_token_from_user_input` + // may try the token account first. + let data = extended_mint(2); + assert!(data.len() > TokenAccount::LEN); + assert!(unpack_token_account(&data).is_none()); + } + + #[test] + fn legacy_mint_is_not_mistaken_for_a_token_account() { + assert!(unpack_token_account(&legacy_mint(9)).is_none()); + } + + #[test] + fn token_program_of_accepts_every_supported_program() { + let address = Pubkey::new_unique(); + for program in token_program::SUPPORTED_TOKEN_PROGRAMS { + let account = Account { + owner: program, + ..Default::default() + }; + assert_eq!(token_program_of(&account, &address).unwrap(), program); + } + } + + #[test] + fn token_program_of_rejects_other_owners() { + let address = Pubkey::new_unique(); + let account = Account { + owner: Pubkey::new_unique(), + ..Default::default() + }; + let err = token_program_of(&account, &address) + .expect_err("a non-token program is not a token program") + .to_string(); + assert!( + err.contains("not owned by a supported token program"), + "{err}" + ); } } From a056888148ef9d5edace5ff299765f2e9bbc1f98 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:28:13 +0900 Subject: [PATCH 05/36] fix inconsistencies in token program support should work on interface as well as the program itself. that was almost bad! --- client/src/instructions.rs | 4 ++ client/src/parse.rs | 2 + interface/src/instruction/create_buffer.rs | 50 ++++++++++++--------- interface/src/instruction/reclaim_buffer.rs | 21 +++++++-- programs/settlement/src/settle/begin.rs | 9 ++-- programs/settlement/src/settle/finalize.rs | 9 ++-- programs/settlement/src/settle/mod.rs | 18 ++------ programs/settlement/tests/common/buffer.rs | 2 + programs/settlement/tests/create_buffer.rs | 20 ++++++++- programs/settlement/tests/reclaim_buffer.rs | 12 ++++- test-cli/src/cmd/settle.rs | 28 +++++++----- 11 files changed, 117 insertions(+), 58 deletions(-) diff --git a/client/src/instructions.rs b/client/src/instructions.rs index e1c7976c..849c92c3 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -148,6 +148,7 @@ impl From> for Instruction { pub struct CreateBuffers<'a> { pub program_id: Pubkey, pub payer: Pubkey, + pub token_program: Pubkey, pub mints: &'a [Pubkey], } @@ -161,6 +162,7 @@ impl From> for Instruction { cow_settlement_interface::instruction::create_buffer::CreateBuffers { program_id: builder.program_id, payer: builder.payer, + token_program: builder.token_program, buffers: &buffers, } .into() @@ -199,6 +201,7 @@ pub struct ReclaimBuffer<'a> { pub program_id: Pubkey, pub reclaim_authority: Pubkey, pub reclaim_recipient: Pubkey, + pub token_program: Pubkey, pub mints: &'a [Pubkey], } @@ -218,6 +221,7 @@ impl From> for Instruction { state_pda, reclaim_authority: builder.reclaim_authority, reclaim_recipient: builder.reclaim_recipient, + token_program: builder.token_program, buffers: &buffers, } .into() diff --git a/client/src/parse.rs b/client/src/parse.rs index 60885466..ce151009 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -110,6 +110,7 @@ mod tests { SettlementInstruction::CreateBuffer => CreateBuffers { program_id, payer, + token_program: pubkey_from_seed("token program"), mints: &[pubkey_from_seed("mint")], } .into(), @@ -141,6 +142,7 @@ mod tests { state_pda: pubkey_from_seed("state pda"), reclaim_authority: payer, reclaim_recipient: payer, + token_program: pubkey_from_seed("token program"), buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"))], } .into(), diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index 77d69cec..e6f01732 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -14,11 +14,6 @@ pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; use super::InstructionInputParsing; use crate::SettlementInstruction; -/// The legacy SPL Token program, the default this builder places in the -/// `token_program` account. Buffers are created as token accounts owned by -/// whichever of [`crate::token_program::SUPPORTED_TOKEN_PROGRAMS`] is passed. -pub use crate::token_program::SPL_TOKEN_PROGRAM_ID; - /// Builder for a `CreateBuffer` instruction that creates one buffer per /// `(buffer_pda, mint)` pair in `buffers`. /// @@ -41,6 +36,10 @@ pub use crate::token_program::SPL_TOKEN_PROGRAM_ID; pub struct CreateBuffers<'a> { pub program_id: Pubkey, pub payer: Pubkey, + /// The token program that will own every buffer this instruction creates. + /// Must be one of [`crate::token_program::SUPPORTED_TOKEN_PROGRAMS`], and + /// must be the program owning every `mint` in `buffers`. + pub token_program: Pubkey, pub buffers: &'a [(Pubkey, Pubkey)], } @@ -49,7 +48,7 @@ impl From> for Instruction { let mut accounts = vec![ AccountMeta::new(builder.payer, true), AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(builder.token_program, false), ]; for (buffer_pda, mint) in builder.buffers { accounts.push(AccountMeta::new(*buffer_pda, false)); @@ -137,6 +136,7 @@ pub mod fixtures { Instruction::from(CreateBuffers { program_id: zero, payer: zero, + token_program: zero, buffers: &[(zero, zero)], }) .data @@ -160,14 +160,15 @@ mod tests { fn create_buffer_input_parses_valid_input() { let program_id: Address = Address::new_from_array([1; 32]); let payer: Address = Address::new_from_array([2; 32]); - let system_program = fake_account_from_array([4; 32]); let token_program = Address::new_from_array([3; 32]); + let system_program = fake_account_from_array([4; 32]); let buffer_pda = Address::new_from_array([5; 32]); let mint = Address::new_from_array([6; 32]); let data = Instruction::from(CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_pda, mint)], }) .data; @@ -194,14 +195,15 @@ mod tests { let program_id = Address::new_from_array([1; 32]); let payer = Address::new_from_array([2; 32]); let token_program = Address::new_from_array([3; 32]); - let buffer_a = Address::new_from_array([5; 32]); - let mint_a = Address::new_from_array([6; 32]); - let buffer_b = Address::new_from_array([7; 32]); - let mint_b = Address::new_from_array([8; 32]); + let buffer_a = Address::new_from_array([4; 32]); + let mint_a = Address::new_from_array([5; 32]); + let buffer_b = Address::new_from_array([6; 32]); + let mint_b = Address::new_from_array([7; 32]); let data = Instruction::from(CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], }) .data; @@ -277,11 +279,13 @@ mod tests { fn instruction_data_has_expected_layout() { let program_id = Pubkey::new_from_array([1; 32]); let payer = Pubkey::new_from_array([2; 32]); - let buffer_pda = Pubkey::new_from_array([3; 32]); - let mint = Pubkey::new_from_array([4; 32]); + let token_program = Pubkey::new_from_array([3; 32]); + let buffer_pda = Pubkey::new_from_array([4; 32]); + let mint = Pubkey::new_from_array([5; 32]); let Instruction { data, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -295,11 +299,13 @@ mod tests { fn single_buffer_has_expected_accounts() { let program_id = Pubkey::new_from_array([1; 32]); let payer = Pubkey::new_from_array([2; 32]); - let buffer_pda = Pubkey::new_from_array([3; 32]); - let mint = Pubkey::new_from_array([4; 32]); + let token_program = Pubkey::new_from_array([3; 32]); + let buffer_pda = Pubkey::new_from_array([4; 32]); + let mint = Pubkey::new_from_array([5; 32]); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -310,7 +316,7 @@ mod tests { // untouched. assert_writable_signer(&accounts[0], payer); assert_readonly_nonsigner(&accounts[1], SYSTEM_PROGRAM_ID); - assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[2], token_program); assert_writable_nonsigner(&accounts[3], buffer_pda); assert_readonly_nonsigner(&accounts[4], mint); } @@ -319,13 +325,15 @@ mod tests { fn multiple_buffers_append_pairs_after_shared_accounts() { let program_id = Pubkey::new_from_array([1; 32]); let payer = Pubkey::new_from_array([2; 32]); - let buffer_a = Pubkey::new_from_array([3; 32]); - let mint_a = Pubkey::new_from_array([4; 32]); - let buffer_b = Pubkey::new_from_array([5; 32]); - let mint_b = Pubkey::new_from_array([6; 32]); + let token_program = Pubkey::new_from_array([3; 32]); + let buffer_a = Pubkey::new_from_array([4; 32]); + let mint_a = Pubkey::new_from_array([5; 32]); + let buffer_b = Pubkey::new_from_array([6; 32]); + let mint_b = Pubkey::new_from_array([7; 32]); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], } .into(); @@ -342,9 +350,11 @@ mod tests { fn empty_buffers_has_only_shared_accounts() { let program_id = Pubkey::new_from_array([1; 32]); let payer = Pubkey::new_from_array([2; 32]); + let token_program = Pubkey::new_from_array([3; 32]); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[], } .into(); diff --git a/interface/src/instruction/reclaim_buffer.rs b/interface/src/instruction/reclaim_buffer.rs index 5a8559e9..5699f04b 100644 --- a/interface/src/instruction/reclaim_buffer.rs +++ b/interface/src/instruction/reclaim_buffer.rs @@ -16,7 +16,6 @@ use solana_program_error::ProgramError; use solana_pubkey::Pubkey; use super::InstructionInputParsing; -pub use crate::instruction::create_buffer::SPL_TOKEN_PROGRAM_ID; use crate::SettlementInstruction; /// Builder for a `ReclaimBuffer` instruction that closes one buffer per @@ -37,6 +36,9 @@ pub struct ReclaimBuffer<'a> { pub state_pda: Pubkey, pub reclaim_authority: Pubkey, pub reclaim_recipient: Pubkey, + /// The token program owning every buffer this instruction closes. Must be + /// one of [`crate::token_program::SUPPORTED_TOKEN_PROGRAMS`]. + pub token_program: Pubkey, pub buffers: &'a [(Pubkey, Pubkey)], } @@ -46,7 +48,7 @@ impl From> for Instruction { AccountMeta::new_readonly(builder.state_pda, false), AccountMeta::new_readonly(builder.reclaim_authority, true), AccountMeta::new(builder.reclaim_recipient, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(builder.token_program, false), ]; for (buffer_pda, mint) in builder.buffers { accounts.push(AccountMeta::new(*buffer_pda, false)); @@ -126,6 +128,7 @@ pub mod fixtures { state_pda: zero, reclaim_authority: zero, reclaim_recipient: zero, + token_program: zero, buffers: &[(zero, zero)], }) .data @@ -148,6 +151,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_pda = pubkey_from_seed("buffer pda"); let mint = pubkey_from_seed("mint"); @@ -156,6 +160,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_pda, mint)], }) .data; @@ -201,6 +206,7 @@ mod tests { let data = Instruction::from(ReclaimBuffer { program_id, state_pda, + token_program, reclaim_authority, reclaim_recipient, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], @@ -282,6 +288,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_pda = pubkey_from_seed("buffer pda"); let mint = pubkey_from_seed("mint"); let Instruction { data, .. } = ReclaimBuffer { @@ -289,6 +296,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -304,6 +312,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_pda = pubkey_from_seed("buffer pda"); let mint = pubkey_from_seed("mint"); let Instruction { accounts, .. } = ReclaimBuffer { @@ -311,6 +320,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -319,7 +329,7 @@ mod tests { assert_readonly_nonsigner(&accounts[0], state_pda); assert_readonly_signer(&accounts[1], reclaim_authority); assert_writable_nonsigner(&accounts[2], reclaim_recipient); - assert_readonly_nonsigner(&accounts[3], SPL_TOKEN_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[3], token_program); assert_writable_nonsigner(&accounts[4], buffer_pda); assert_readonly_nonsigner(&accounts[5], mint); } @@ -332,6 +342,7 @@ mod tests { state_pda: pubkey_from_seed("state pda"), reclaim_authority, reclaim_recipient: reclaim_authority, + token_program: pubkey_from_seed("token program"), buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"))], } .into(); @@ -346,6 +357,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_a = pubkey_from_seed("buffer a"); let mint_a = pubkey_from_seed("mint a"); let buffer_b = pubkey_from_seed("buffer b"); @@ -355,6 +367,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], } .into(); @@ -373,11 +386,13 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let Instruction { accounts, .. } = ReclaimBuffer { program_id, state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[], } .into(); diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 4100403f..19d6927b 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -29,11 +29,12 @@ use pinocchio::{ }; use pinocchio_token::{instructions::Transfer, state::Account as TokenAccount}; -use crate::processor::{ - check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump, +use crate::{ + processor::{check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump}, + token::validate_token_program, }; -use super::{validate_counterpart, validate_token_program_account}; +use super::validate_counterpart; pub fn process_begin_settle( program_id: &Address, @@ -75,7 +76,7 @@ pub fn process_begin_settle( let finalize_ix = instructions.load_instruction_at(usize::from(input.finalize_ix_index))?; - validate_token_program_account(input.token_program_account)?; + validate_token_program(input.token_program_account)?; with_state_pda_signer_from_bump(state_bump, |signer| { settle_orders( diff --git a/programs/settlement/src/settle/finalize.rs b/programs/settlement/src/settle/finalize.rs index df190ee8..ea20b01a 100644 --- a/programs/settlement/src/settle/finalize.rs +++ b/programs/settlement/src/settle/finalize.rs @@ -12,9 +12,12 @@ use pinocchio::{ }; use pinocchio_token::instructions::Transfer; -use crate::processor::{is_cpi_call, with_state_pda_signer}; +use crate::{ + processor::{is_cpi_call, with_state_pda_signer}, + token::validate_token_program, +}; -use super::{validate_counterpart, validate_token_program_account}; +use super::validate_counterpart; pub fn process_finalize_settle( program_id: &Address, @@ -44,7 +47,7 @@ pub fn process_finalize_settle( // the canonical buffer for the order's buy mint. Nothing is left to check // here, so `push_funds` only executes the transfers. - validate_token_program_account(input.token_program_account)?; + validate_token_program(input.token_program_account)?; with_state_pda_signer(program_id, input.state_pda_account, |state_pda_signer| { push_funds(input.state_pda_account, state_pda_signer, input.pushes) diff --git a/programs/settlement/src/settle/mod.rs b/programs/settlement/src/settle/mod.rs index 25a01d7a..f76737a1 100644 --- a/programs/settlement/src/settle/mod.rs +++ b/programs/settlement/src/settle/mod.rs @@ -3,12 +3,10 @@ use std::ops::Deref; use cow_settlement_interface::{ - instruction::{create_buffer::SPL_TOKEN_PROGRAM_ID, settle::recover_counterpart}, - recover_discriminator, SettlementError, SettlementInstruction, -}; -use pinocchio::{ - error::ProgramError, sysvars::instructions::Instructions, AccountView, Address, ProgramResult, + instruction::settle::recover_counterpart, recover_discriminator, SettlementError, + SettlementInstruction, }; +use pinocchio::{sysvars::instructions::Instructions, Address, ProgramResult}; mod begin; mod finalize; @@ -44,13 +42,3 @@ fn validate_counterpart>( } Ok(()) } - -/// Validate that `token_program_account` is the legacy SPL Token program, which -/// every settlement transfer is issued against. -#[must_use = "ignoring the output may lead to an unintended on-chain state"] -fn validate_token_program_account(token_program_account: &AccountView) -> ProgramResult { - if token_program_account.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index 2dddd41e..c081e5ba 100644 --- a/programs/settlement/tests/common/buffer.rs +++ b/programs/settlement/tests/common/buffer.rs @@ -1,6 +1,7 @@ //! Buffer-account helpers for the settlement integration tests. use cow_settlement_client::cow_settlement_interface::pda::buffer::find_buffer_pda; +use cow_settlement_client::cow_settlement_interface::token_program::SPL_TOKEN_PROGRAM_ID; use cow_settlement_client::cow_settlement_interface::Instruction; use cow_settlement_client::instructions::CreateBuffers; use litesvm::LiteSVM; @@ -33,6 +34,7 @@ pub fn ensure_buffer_exists( let ix = Instruction::from(CreateBuffers { program_id: *program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[*mint], }); let tx = Transaction::new_signed_with_payer( diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index f0aee179..17dd2c3f 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -1,9 +1,10 @@ use cow_settlement_client::cow_settlement_interface::{ - instruction::create_buffer::{CreateBuffers as CreateBuffersRaw, SPL_TOKEN_PROGRAM_ID}, + instruction::create_buffer::CreateBuffers as CreateBuffersRaw, pda::{ buffer::{buffer_pda_seeds, find_buffer_pda}, state::find_state_pda, }, + token_program::SPL_TOKEN_PROGRAM_ID, }; use cow_settlement_client::instructions::CreateBuffers; use litesvm::LiteSVM; @@ -43,6 +44,7 @@ fn happy_path_creates_initialized_buffer_token_account() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -110,6 +112,7 @@ fn buffer_can_receive_tokens() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -148,6 +151,7 @@ fn happy_path_creates_native_token_buffer() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[native_mint::ID], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -183,6 +187,7 @@ fn happy_path_creates_multiple_buffers_in_one_instruction() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &mints, }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -227,6 +232,7 @@ fn rejects_no_buffers() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -255,6 +261,7 @@ fn rejects_arbitrary_wrong_buffer_pda() { let ix = CreateBuffersRaw { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, buffers: &[(wrong_pda, mint)], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -275,6 +282,7 @@ fn rejects_non_canonical_bump_pda() { let ix = CreateBuffersRaw { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, buffers: &[(non_canonical_pda, mint)], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -291,6 +299,7 @@ fn rejects_non_spl_token_program() { let mut ix: Instruction = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], } .into(); @@ -334,6 +343,7 @@ fn rejects_invalid_mint() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[not_a_mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -367,6 +377,7 @@ fn creates_buffer_when_address_is_prefunded() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; common::signed_tx(svm, &payer, &payer, ix) @@ -383,6 +394,7 @@ fn recreating_same_buffer_is_idempotent() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; common::signed_tx(svm, &payer, &payer, ix) @@ -400,6 +412,7 @@ fn batch_with_existing_buffer_passes_with_no_changes() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[existing], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -414,6 +427,7 @@ fn batch_with_existing_buffer_passes_with_no_changes() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[fresh, existing], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -449,6 +463,7 @@ fn one_failing_buffer_reverts_the_whole_batch() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[fresh, not_a_mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -483,6 +498,7 @@ fn same_mint_twice_in_one_instruction_is_idempotent() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint, mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -508,6 +524,7 @@ fn max_buffers_via_lookup_table(svm: &mut LiteSVM, program_id: &Pubkey, payer: & let ix = CreateBuffersRaw { program_id: *program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, buffers: &buffers, }; common::lookup_table::lookup_table_tx(svm, payer, ix) @@ -575,6 +592,7 @@ fn max_buffers_in_one_instruction() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &mints, }; let tx = common::lookup_table::lookup_table_tx(&mut svm, &payer, ix); diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index ad5607d9..be614c64 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -2,7 +2,7 @@ use cow_settlement_client::instructions::ReclaimBuffer; use cow_settlement_interface::Instruction; use cow_settlement_interface::{ instruction::reclaim_buffer::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, - pda::state::find_state_pda, SettlementError, + pda::state::find_state_pda, token_program::SPL_TOKEN_PROGRAM_ID, SettlementError, }; use litesvm::LiteSVM; use solana_sdk::{ @@ -46,6 +46,7 @@ fn happy_path_reclaims_to_a_recipient_chosen_by_the_authority() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -93,6 +94,7 @@ fn happy_path_reclaims_empty_buffer_to_the_authority_itself() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -132,6 +134,7 @@ fn funded_buffer_is_skipped() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -177,6 +180,7 @@ fn reclaims_to_the_settlements_own_state_pda() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -225,6 +229,7 @@ fn reclaims_multiple_buffers_skipping_funded() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint_a, mint_b], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -261,6 +266,7 @@ fn rejects_the_same_buffer_twice_in_one_instruction() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint, mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -290,6 +296,7 @@ fn rejects_when_signer_is_not_the_configured_reclaim_authority() { program_id, reclaim_authority: impostor.pubkey(), reclaim_recipient: impostor.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &impostor, ix); @@ -321,6 +328,7 @@ fn rejects_when_the_reclaim_authority_does_not_sign() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: SPL_TOKEN_PROGRAM_ID, mints: &[mint], }); @@ -360,6 +368,7 @@ fn max_buffers_reclaim_via_lookup_table( state_pda, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, buffers: &buffers, }; common::lookup_table::lookup_table_tx(svm, reclaim_authority, ix) @@ -438,6 +447,7 @@ fn max_buffers_in_one_instruction() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: SPL_TOKEN_PROGRAM_ID, mints: &mints, }; let tx = common::lookup_table::lookup_table_tx(&mut svm, &reclaim_authority, ix); diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 2d7bb5a6..23b6e421 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -235,16 +235,18 @@ fn resolve_intents(ctx: &Context, args: &SettleArgs) -> anyhow::Result, - mint_buffers_to_create: &mut HashSet, - mint: Pubkey, + mint_buffers_to_create: &mut HashMap>, + token: &ResolvedToken, amount: u64, ) -> anyhow::Result<()> { + let mint = token.mint; match tally.get(&mint) { Some(cur_amount) => { let new_amount = cur_amount @@ -255,7 +257,10 @@ fn tally_and_register_buffer( None => { let (buffer_pda, _) = find_buffer_pda(&ctx.program_id, &mint); if ctx.rpc.get_account(&buffer_pda).is_err() { - mint_buffers_to_create.insert(mint); + mint_buffers_to_create + .entry(token.token_program) + .or_default() + .insert(mint); } tally.insert(mint, amount); } @@ -273,7 +278,7 @@ fn prepare_setup_ixs( ) -> anyhow::Result<()> { let mut sell_amount_pulled: HashMap = HashMap::new(); let mut buy_amount_pushed: HashMap = HashMap::new(); - let mut mint_buffers_to_create: HashSet = HashSet::new(); + let mut mint_buffers_to_create: HashMap> = HashMap::new(); for intent in intents { // for both the buy and sell token: we need to tally the total transfer amounts @@ -282,26 +287,27 @@ fn prepare_setup_ixs( ctx, &mut sell_amount_pulled, &mut mint_buffers_to_create, - intent.sell.mint, + &intent.sell, intent.data.sell_amount, )?; tally_and_register_buffer( ctx, &mut buy_amount_pushed, &mut mint_buffers_to_create, - intent.buy.mint, + &intent.buy, intent.data.buy_amount, )?; } ensure_cow_balance(&sell_amount_pulled, &buy_amount_pushed)?; - if !mint_buffers_to_create.is_empty() { + for (token_program, mints) in mint_buffers_to_create { all_ixs.push( CreateBuffers { program_id: ctx.program_id, payer: ctx.payer.pubkey(), - mints: &mint_buffers_to_create.into_iter().collect::>(), + token_program, + mints: &mints.into_iter().collect::>(), } .into(), ); From 8248ade9e92e00538eb0d6b2128827f389d0f2d3 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:34:31 +0900 Subject: [PATCH 06/36] Keep the test CLI's Token-2022 support out of this PR The CLI's token resolution now has to know which token program owns a mint, which is a self-contained change with its own tests; it lands separately. All that stays here is the one field `CreateBuffers` gained, pinned to the legacy program, and the workspace entry for the legacy interface the CLI still builds against. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 2 +- Cargo.toml | 3 + test-cli/Cargo.toml | 2 +- test-cli/src/cmd/create_order.rs | 2 +- test-cli/src/cmd/settle.rs | 32 ++-- test-cli/src/instructions.rs | 14 +- test-cli/src/token.rs | 279 +++++++------------------------ 7 files changed, 88 insertions(+), 246 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c2a46fe..8a2f1f67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,7 +930,7 @@ dependencies = [ "solana-system-interface 3.2.0", "spl-associated-token-account-interface", "spl-token", - "spl-token-2022-interface 3.1.1", + "spl-token-interface 3.0.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 127d1923..a974d630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,9 @@ solana-system-interface = "3" spl-associated-token-account-interface = "2" spl-token = "9" spl-token-2022-interface = "3" +# Only the test CLI still builds against the legacy interface; it moves over +# together with its Token-2022 support. +spl-token-interface = "3" [workspace.metadata.cli] # Used to help solana-verify identify the correct build image. diff --git a/test-cli/Cargo.toml b/test-cli/Cargo.toml index b4aa0dc2..0938df22 100644 --- a/test-cli/Cargo.toml +++ b/test-cli/Cargo.toml @@ -35,7 +35,7 @@ solana-sdk.workspace = true solana-system-interface = { workspace = true, features = ["bincode"] } spl-associated-token-account-interface.workspace = true spl-token.workspace = true -spl-token-2022-interface.workspace = true +spl-token-interface.workspace = true [lints] workspace = true diff --git a/test-cli/src/cmd/create_order.rs b/test-cli/src/cmd/create_order.rs index 3db66ec2..34652034 100644 --- a/test-cli/src/cmd/create_order.rs +++ b/test-cli/src/cmd/create_order.rs @@ -150,7 +150,7 @@ fn execute(ctx: Context, parsed: ParsedOrder, common: CommonArgs) -> anyhow::Res // Approve the settlement state PDA to pull sell tokens on the user's behalf. ixs.push(crate::instructions::approve( &ctx.program_id, - &sell, + &sell.ta, &ctx.payer.pubkey(), sell_amount, )?); diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 23b6e421..ead67e0f 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -4,7 +4,7 @@ use cow_settlement_client::{ cow_settlement_interface::{ data::{intent::OrderIntent, order::OrderAccount}, pda::buffer::find_buffer_pda, - Pubkey, + token_program, Pubkey, }, instructions::{ BeginSettle, CreateBuffers, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, @@ -235,18 +235,16 @@ fn resolve_intents(ctx: &Context, args: &SettleArgs) -> anyhow::Result, - mint_buffers_to_create: &mut HashMap>, - token: &ResolvedToken, + mint_buffers_to_create: &mut HashSet, + mint: Pubkey, amount: u64, ) -> anyhow::Result<()> { - let mint = token.mint; match tally.get(&mint) { Some(cur_amount) => { let new_amount = cur_amount @@ -257,10 +255,7 @@ fn tally_and_register_buffer( None => { let (buffer_pda, _) = find_buffer_pda(&ctx.program_id, &mint); if ctx.rpc.get_account(&buffer_pda).is_err() { - mint_buffers_to_create - .entry(token.token_program) - .or_default() - .insert(mint); + mint_buffers_to_create.insert(mint); } tally.insert(mint, amount); } @@ -278,7 +273,7 @@ fn prepare_setup_ixs( ) -> anyhow::Result<()> { let mut sell_amount_pulled: HashMap = HashMap::new(); let mut buy_amount_pushed: HashMap = HashMap::new(); - let mut mint_buffers_to_create: HashMap> = HashMap::new(); + let mut mint_buffers_to_create: HashSet = HashSet::new(); for intent in intents { // for both the buy and sell token: we need to tally the total transfer amounts @@ -287,27 +282,28 @@ fn prepare_setup_ixs( ctx, &mut sell_amount_pulled, &mut mint_buffers_to_create, - &intent.sell, + intent.sell.mint, intent.data.sell_amount, )?; tally_and_register_buffer( ctx, &mut buy_amount_pushed, &mut mint_buffers_to_create, - &intent.buy, + intent.buy.mint, intent.data.buy_amount, )?; } ensure_cow_balance(&sell_amount_pulled, &buy_amount_pushed)?; - for (token_program, mints) in mint_buffers_to_create { + if !mint_buffers_to_create.is_empty() { all_ixs.push( CreateBuffers { program_id: ctx.program_id, payer: ctx.payer.pubkey(), - token_program, - mints: &mints.into_iter().collect::>(), + // The CLI only resolves tokens on the legacy program for now. + token_program: token_program::SPL_TOKEN_PROGRAM_ID, + mints: &mint_buffers_to_create.into_iter().collect::>(), } .into(), ); diff --git a/test-cli/src/instructions.rs b/test-cli/src/instructions.rs index 828fe281..665b373a 100644 --- a/test-cli/src/instructions.rs +++ b/test-cli/src/instructions.rs @@ -5,7 +5,7 @@ use anyhow::Context as _; use cow_settlement_client::cow_settlement_interface::{pda::state::find_state_pda, Pubkey}; use solana_instruction::Instruction; use solana_rpc_client::rpc_client::RpcClient; -use spl_token_2022_interface::instruction::{self as token_ix}; +use spl_token_interface::instruction::{self as token_ix}; /// Build instructions that wrap `amount` lamports into the payer's WSOL ATA. /// @@ -27,26 +27,26 @@ pub fn wrap_sol( )); ixs.push( - token_ix::sync_native(&wsol.token_program, &wsol.ta) + token_ix::sync_native(&spl_token_interface::id(), &wsol.ta) .context("failed to build SyncNative instruction")?, ); Ok((wsol.ta, ixs)) } -/// Build an `Approve` instruction delegating `amount` of `token` to the PDA -/// derived from `program_id`. +/// Build an `Approve` instruction delegating `amount` tokens on `token_account` +/// to the PDA derived from `program_id`. pub fn approve( program_id: &Pubkey, - token: &token::ResolvedToken, + token_account: &Pubkey, owner: &Pubkey, amount: u64, ) -> anyhow::Result { let (settlement_pda, _) = find_state_pda(program_id); token_ix::approve( - &token.token_program, - &token.ta, + &spl_token_interface::id(), + token_account, &settlement_pda, owner, &[], diff --git a/test-cli/src/token.rs b/test-cli/src/token.rs index 5023599d..89505100 100644 --- a/test-cli/src/token.rs +++ b/test-cli/src/token.rs @@ -1,20 +1,21 @@ //! Token resolution helpers: converts a user-supplied token string (alias, mint address, -//! or token-account address) into an SPL token account address, the token program that -//! owns it, and the decoded mint. +//! or token-account address) into an SPL token account address and decimal count. //! //! Entry point: [`resolve`]. use anyhow::Context as _; -use cow_settlement_client::cow_settlement_interface::{token_program, Pubkey}; +use cow_settlement_client::cow_settlement_interface::Pubkey; use solana_instruction::Instruction; +use solana_program_pack::Pack; use solana_pubkey::pubkey; use solana_rpc_client::rpc_client::RpcClient; -use solana_sdk::account::{Account, ReadableAccount}; -use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; +use solana_sdk::account::ReadableAccount; +use spl_associated_token_account_interface::address::{ + get_associated_token_address, get_associated_token_address_with_program_id, +}; use spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent; -use spl_token_2022_interface::extension::StateWithExtensions; -use spl_token_2022_interface::native_mint; -use spl_token_2022_interface::state::{Account as TokenAccount, Mint}; +use spl_token_interface::native_mint; +use spl_token_interface::state::{Account as TokenAccount, Mint}; /// Inline registry of recognised token symbols. /// Avoids an RPC round-trip for well-known mints whose decimals are fixed. @@ -49,11 +50,6 @@ pub struct ResolvedToken { pub mint: Pubkey, /// The actual mint data pub mint_data: Mint, - /// The token program owning both `mint` and `ta` — one of - /// [`token_program::SUPPORTED_TOKEN_PROGRAMS`]. Any instruction touching - /// `ta` has to be built against it, so it travels with the resolved token - /// rather than being assumed. - pub token_program: Pubkey, /// `Some(owner)` when `ta` does not yet exist on-chain. Call with the /// transaction fee payer to build the instruction that creates it. create_ata: Option, @@ -68,7 +64,7 @@ impl ResolvedToken { payer, &owner, &self.mint, - &self.token_program, + &spl_token_interface::id(), )) } } @@ -77,9 +73,20 @@ impl ResolvedToken { pub fn resolve(rpc: &RpcClient, owner: &Pubkey, token_str: &str) -> anyhow::Result { let upper = token_str.to_uppercase(); - // 1. `"SOL"` / `"WSOL"` — payer's ATA for the native mint. + // 1. `"SOL"` / `"WSOL"` — payer's WSOL ATA, 9 decimals, no RPC call needed. if matches!(upper.as_str(), "SOL" | "WSOL") { - return resolve_from_mint(rpc, owner, &native_mint::ID); + let wsol_mint: Pubkey = native_mint::id(); + let wsol_ata = get_associated_token_address_with_program_id( + owner, + &wsol_mint, + &spl_token_interface::id(), + ); + return Ok(ResolvedToken { + ta: wsol_ata, + mint: wsol_mint, + create_ata: determine_create_ata(rpc, &wsol_mint, owner)?, + mint_data: fetch_mint_data(rpc, &wsol_mint)?, + }); } // 2. Base58 mint or token-account address — fetches decimals from the mint, and possibly the token account owner. @@ -93,7 +100,17 @@ pub fn resolve(rpc: &RpcClient, owner: &Pubkey, token_str: &str) -> anyhow::Resu .with_context(|| "failed to fetch genesis hash (is the RPC URL correct?)")? .to_string(); if let Some(known) = known_token(&genesis_hash, &upper) { - return resolve_from_mint(rpc, owner, &known.mint); + let ata = get_associated_token_address_with_program_id( + owner, + &known.mint, + &spl_token_interface::id(), + ); + return Ok(ResolvedToken { + ta: ata, + create_ata: determine_create_ata(rpc, &known.mint, owner)?, + mint: known.mint, + mint_data: fetch_mint_data(rpc, &known.mint)?, + }); } anyhow::bail!( @@ -115,15 +132,13 @@ pub fn resolve_from_token_account( ) })?; - let token_program = token_program_of(&account, token_account)?; - let decoded_account = unpack_token_account(account.data()) + let decoded_account = TokenAccount::unpack(account.data()) .with_context(|| format!("account {token_account} is not a token account"))?; Ok(ResolvedToken { ta: *token_account, mint: decoded_account.mint, - mint_data: fetch_mint(rpc, &decoded_account.mint)?.1, - token_program, + mint_data: fetch_mint_data(rpc, &decoded_account.mint)?, // The account was just fetched and unpacked above, so it already exists. create_ata: None, }) @@ -142,32 +157,31 @@ pub fn interpret_token_from_user_input( .get_account(token_account_or_mint) .with_context(|| format!("account {token_account_or_mint} not found on-chain"))?; - let token_program = token_program_of(&account, token_account_or_mint)?; + anyhow::ensure!( + account.owner == spl_token_interface::id(), + "{token_account_or_mint} is not owned by the token program (owner: {})", + account.owner + ); - // Token accounts are tried first: a mint carrying enough extension data to - // reach the token account length is only told apart from an account by the - // account-type byte, which `unpack_token_account` checks. - if let Some(token_account) = unpack_token_account(account.data()) { + if let Ok(token_account) = TokenAccount::unpack(&account.data) { Ok(ResolvedToken { ta: *token_account_or_mint, mint: token_account.mint, - mint_data: fetch_mint(rpc, &token_account.mint)?.1, - token_program, + mint_data: fetch_mint_data(rpc, &token_account.mint)?, // The account was just fetched and unpacked above, so it already exists. create_ata: None, }) - } else if let Some(mint) = unpack_mint(account.data()) { - let ta = get_associated_token_address_with_program_id( + } else if let Ok(mint) = Mint::unpack(&account.data) { + let ata = get_associated_token_address_with_program_id( owner, token_account_or_mint, - &token_program, + &spl_token_interface::id(), ); Ok(ResolvedToken { - ta, + ta: ata, mint_data: mint, mint: *token_account_or_mint, - token_program, - create_ata: determine_create_ata(rpc, &ta, owner)?, + create_ata: determine_create_ata(rpc, token_account_or_mint, owner)?, }) } else { anyhow::bail!( @@ -178,202 +192,31 @@ pub fn interpret_token_from_user_input( } } -/// Resolve `mint` to `owner`'s associated token account, derived under whichever -/// token program owns the mint. -fn resolve_from_mint( - rpc: &RpcClient, - owner: &Pubkey, - mint: &Pubkey, -) -> anyhow::Result { - let (token_program, mint_data) = fetch_mint(rpc, mint)?; - let ta = get_associated_token_address_with_program_id(owner, mint, &token_program); - - Ok(ResolvedToken { - ta, - mint: *mint, - mint_data, - token_program, - create_ata: determine_create_ata(rpc, &ta, owner)?, - }) -} - -/// The token program owning `account`, rejecting anything the settlement -/// program cannot move tokens with. -fn token_program_of(account: &Account, address: &Pubkey) -> anyhow::Result { - let owner = *account.owner(); - anyhow::ensure!( - token_program::is_supported(&owner), - "{address} is not owned by a supported token program (owner: {owner})", - ); - Ok(owner) -} - /// Used to set `create_ata` on `ResolvedToken`. Returns the ATA `owner` when the /// account still needs to be created. fn determine_create_ata( rpc: &RpcClient, - token_account_address: &Pubkey, + mint: &Pubkey, owner: &Pubkey, ) -> anyhow::Result> { - let Ok(data) = rpc.get_account_data(token_account_address) else { + let token_account_address = get_associated_token_address(owner, mint); + let Ok(data) = rpc.get_account_data(&token_account_address) else { return Ok(Some(*owner)); }; - anyhow::ensure!( - unpack_token_account(&data).is_some(), - "account {token_account_address} is not a token account" - ); - Ok(None) + TokenAccount::unpack(&data) + .map(|_| None) + .map_err(|_| anyhow::anyhow!("account {token_account_address} is not a token account")) } -/// Fetch `mint` and return the token program owning it alongside its decoded state. -fn fetch_mint(rpc: &RpcClient, mint: &Pubkey) -> anyhow::Result<(Pubkey, Mint)> { - let account = rpc - .get_account(mint) +fn fetch_mint_data(rpc: &RpcClient, mint: &Pubkey) -> anyhow::Result { + let data = rpc + .get_account_data(mint) .with_context(|| format!("mint account {mint} not found"))?; - let token_program = token_program_of(&account, mint)?; - let mint_data = - unpack_mint(account.data()).with_context(|| format!("account {mint} is not a mint"))?; - - Ok((token_program, mint_data)) -} - -/// Decode the base token-account state, skipping over any Token-2022 extensions. -/// The legacy layout is the same data without the extension suffix, so this -/// covers both token programs. -fn unpack_token_account(data: &[u8]) -> Option { - StateWithExtensions::::unpack(data) - .ok() - .map(|state| state.base) -} - -/// Decode the base mint state, skipping over any Token-2022 extensions. See -/// [`unpack_token_account`]. -fn unpack_mint(data: &[u8]) -> Option { - StateWithExtensions::::unpack(data) - .ok() - .map(|state| state.base) -} - -#[cfg(test)] -mod tests { - use super::*; - use solana_program_pack::Pack as _; - use spl_token_2022_interface::extension::mint_close_authority::MintCloseAuthority; - use spl_token_2022_interface::extension::{ - BaseStateWithExtensionsMut as _, ExtensionType, StateWithExtensionsMut, - }; - use spl_token_2022_interface::state::AccountState; - - /// A mint as the legacy token program stores it: exactly `Mint::LEN` bytes. - fn legacy_mint(decimals: u8) -> Vec { - let mint = Mint { - decimals, - is_initialized: true, - ..Default::default() - }; - let mut data = vec![0u8; Mint::LEN]; - mint.pack_into_slice(&mut data); - data - } - - /// A token account as the legacy token program stores it. - fn legacy_token_account(mint: Pubkey) -> Vec { - let account = TokenAccount { - mint, - owner: Pubkey::new_unique(), - state: AccountState::Initialized, - ..Default::default() - }; - let mut data = vec![0u8; TokenAccount::LEN]; - account.pack_into_slice(&mut data); - data - } - - /// A Token-2022 mint carrying one extension, which pads it past - /// `TokenAccount::LEN` and appends the account-type byte. - fn extended_mint(decimals: u8) -> Vec { - let len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority]) - .expect("mint length with a close authority"); - let mut data = vec![0u8; len]; - - let mut state = - StateWithExtensionsMut::::unpack_uninitialized(&mut data).expect("empty mint"); - state - .init_extension::(true) - .expect("close authority extension"); - state.base = Mint { - decimals, - is_initialized: true, - ..Default::default() - }; - state.pack_base(); - state.init_account_type().expect("account type"); - - data - } - - #[test] - fn unpacks_legacy_mint_and_token_account() { - assert_eq!(unpack_mint(&legacy_mint(6)).expect("mint").decimals, 6); - - let mint = Pubkey::new_unique(); - assert_eq!( - unpack_token_account(&legacy_token_account(mint)) - .expect("token account") - .mint, - mint, - ); - } - - #[test] - fn unpacks_token_2022_mint_with_extensions() { - // `Mint::unpack` rejects this outright: it insists on exactly `Mint::LEN`. - assert_eq!(unpack_mint(&extended_mint(2)).expect("mint").decimals, 2); - } - - #[test] - fn extended_mint_is_not_mistaken_for_a_token_account() { - // It is longer than `TokenAccount::LEN`, so only the account-type byte - // tells the two apart — which is why `interpret_token_from_user_input` - // may try the token account first. - let data = extended_mint(2); - assert!(data.len() > TokenAccount::LEN); - assert!(unpack_token_account(&data).is_none()); - } - - #[test] - fn legacy_mint_is_not_mistaken_for_a_token_account() { - assert!(unpack_token_account(&legacy_mint(9)).is_none()); - } - - #[test] - fn token_program_of_accepts_every_supported_program() { - let address = Pubkey::new_unique(); - for program in token_program::SUPPORTED_TOKEN_PROGRAMS { - let account = Account { - owner: program, - ..Default::default() - }; - assert_eq!(token_program_of(&account, &address).unwrap(), program); - } - } - - #[test] - fn token_program_of_rejects_other_owners() { - let address = Pubkey::new_unique(); - let account = Account { - owner: Pubkey::new_unique(), - ..Default::default() - }; - let err = token_program_of(&account, &address) - .expect_err("a non-token program is not a token program") - .to_string(); - assert!( - err.contains("not owned by a supported token program"), - "{err}" - ); + if let Ok(mint_data) = Mint::unpack(&data) { + Ok(mint_data) + } else { + Err(anyhow::anyhow!("account {mint} is not a mint")) } } From 4ed9541f0ea08d62f3352b2b654232352fd41687 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:14:46 +0900 Subject: [PATCH 07/36] switch to using an enum based approach, which seems clean --- interface/src/instruction/create_buffer.rs | 2 +- interface/src/instruction/reclaim_buffer.rs | 2 +- interface/src/instruction/settle/mod.rs | 4 +- interface/src/token_program.rs | 70 +++++++--- programs/settlement/src/create_buffer.rs | 5 +- programs/settlement/src/reclaim_buffer.rs | 7 +- programs/settlement/src/token.rs | 136 +++++++++----------- programs/settlement/tests/common/buffer.rs | 3 +- programs/settlement/tests/common/mod.rs | 5 + programs/settlement/tests/create_buffer.rs | 3 +- programs/settlement/tests/reclaim_buffer.rs | 3 +- test-cli/src/cmd/settle.rs | 5 +- 12 files changed, 138 insertions(+), 107 deletions(-) diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index e6f01732..b0146e0b 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -37,7 +37,7 @@ pub struct CreateBuffers<'a> { pub program_id: Pubkey, pub payer: Pubkey, /// The token program that will own every buffer this instruction creates. - /// Must be one of [`crate::token_program::SUPPORTED_TOKEN_PROGRAMS`], and + /// Must be the address of a [`crate::token_program::TokenProgram`], and /// must be the program owning every `mint` in `buffers`. pub token_program: Pubkey, pub buffers: &'a [(Pubkey, Pubkey)], diff --git a/interface/src/instruction/reclaim_buffer.rs b/interface/src/instruction/reclaim_buffer.rs index 5699f04b..dc633e46 100644 --- a/interface/src/instruction/reclaim_buffer.rs +++ b/interface/src/instruction/reclaim_buffer.rs @@ -37,7 +37,7 @@ pub struct ReclaimBuffer<'a> { pub reclaim_authority: Pubkey, pub reclaim_recipient: Pubkey, /// The token program owning every buffer this instruction closes. Must be - /// one of [`crate::token_program::SUPPORTED_TOKEN_PROGRAMS`]. + /// the address of a [`crate::token_program::TokenProgram`]. pub token_program: Pubkey, pub buffers: &'a [(Pubkey, Pubkey)], } diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index 54433418..8ba08590 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -1,9 +1,11 @@ //! `BeginSettle`/`FinalizeSettle` instruction tools, the instructions-sysvar //! account ID they all reference, and the off-chain instruction builders. +use crate::{token_program::TokenProgram, Pubkey}; use solana_program_error::ProgramError; -pub use crate::token_program::SPL_TOKEN_PROGRAM_ID; +/// The legacy SPL Token program, which the builders below target by default. +pub const SPL_TOKEN_PROGRAM_ID: Pubkey = TokenProgram::SplToken.address(); pub use solana_sdk_ids::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID; mod begin; diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index 1e7ee58a..df70efd4 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -1,20 +1,42 @@ //! Utilities related to the token programs supported by the settlement program. use crate::Pubkey; +use solana_program_error::ProgramError; -/// The legacy SPL Token program. -pub use spl_token_2022_interface::inline_spl_token::ID as SPL_TOKEN_PROGRAM_ID; +/// A token program a token-moving instruction accepts. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TokenProgram { + /// The legacy SPL Token program. + SplToken, + /// The SPL Token-2022 program. + Token2022, +} -/// The SPL Token-2022 program. -pub use spl_token_2022_interface::ID as TOKEN_2022_PROGRAM_ID; +impl TokenProgram { + /// Every supported token program, in no particular order. The single list + /// [`TryFrom`] resolves addresses against. + pub const ALL: [Self; 2] = [Self::SplToken, Self::Token2022]; -/// Every token program a token-moving instruction accepts, in no particular -/// order. -pub const SUPPORTED_TOKEN_PROGRAMS: [Pubkey; 2] = [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]; + /// The address the program is deployed at. + pub const fn address(self) -> Pubkey { + match self { + Self::SplToken => spl_token_2022_interface::inline_spl_token::ID, + Self::Token2022 => spl_token_2022_interface::ID, + } + } +} -/// Whether `address` is a supported token program -pub fn is_supported(address: &Pubkey) -> bool { - SUPPORTED_TOKEN_PROGRAMS.contains(address) +impl TryFrom<&Pubkey> for TokenProgram { + type Error = ProgramError; + + /// Resolves a program address to the token program it identifies, + /// rejecting any address that isn't a supported token program. + fn try_from(address: &Pubkey) -> Result { + Self::ALL + .into_iter() + .find(|program| program.address() == *address) + .ok_or(ProgramError::IncorrectProgramId) + } } #[cfg(test)] @@ -22,20 +44,34 @@ mod tests { use super::*; use crate::fixtures::pubkey_from_seed; + /// Adding a variant breaks this match, which is the reminder to add it to + /// [`TokenProgram::ALL`] as well #[test] - fn supported_programs_are_supported() { - for program in SUPPORTED_TOKEN_PROGRAMS { - assert!(is_supported(&program), "{program} should be supported"); + fn all_lists_every_variant() { + for program in TokenProgram::ALL { + match program { + TokenProgram::SplToken | TokenProgram::Token2022 => {} + } } } #[test] - fn unrelated_program_is_not_supported() { - assert!(!is_supported(&pubkey_from_seed("not a token program"))); + fn every_program_resolves_from_its_own_address() { + for program in TokenProgram::ALL { + let address = program.address(); + assert_eq!( + TokenProgram::try_from(&address), + Ok(program), + "{program:?} should resolve from {address}", + ); + } } #[test] - fn supported_programs_are_distinct() { - assert_ne!(SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID); + fn unrelated_program_cannot_be_resolved_as_token_program() { + assert_eq!( + TokenProgram::try_from(&pubkey_from_seed("not a token program")), + Err(ProgramError::IncorrectProgramId), + ); } } diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/create_buffer.rs index e39d32c7..40d22404 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/create_buffer.rs @@ -26,6 +26,7 @@ pub fn process_create_buffer( // it was handed, so reject an unsupported one up front rather than at the // first CPI. let token_program = validate_token_program(input.token_program)?; + let token_program_id = token_program.address(); // The buffers' token authority is the settlement state PDA, the single // authority over every buffer. Derive it once for all buffers. @@ -47,7 +48,7 @@ pub fn process_create_buffer( payer: input.payer, pda: buffer_pda, size: token_account_len(token_program, mint)?, - owner: token_program, + owner: &token_program_id, seeds: buffer_pda_seeds(mint_key), } .create_idempotent()?; @@ -56,7 +57,7 @@ pub fn process_create_buffer( // initialize a freshly created one. if created { InitializeAccount3::new(buffer_pda, mint, &state_pda) - .invoke_with_unverified_program(token_program)?; + .invoke_with_unverified_program(&token_program_id)?; } } diff --git a/programs/settlement/src/reclaim_buffer.rs b/programs/settlement/src/reclaim_buffer.rs index 11d128c8..9c5961e2 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/reclaim_buffer.rs @@ -33,6 +33,7 @@ pub fn process_reclaim_buffer( } = ReclaimBufferInput::parse(instruction_data, accounts)?; let token_program = validate_token_program(token_program)?; + let token_program_id = token_program.address(); with_state_pda_signer(program_id, state_pda, |state_signer| { let reclaim_authority_pubkey: Pubkey = @@ -62,7 +63,7 @@ pub fn process_reclaim_buffer( CloseAccount::new(buffer_pda, reclaim_recipient, state_pda) .invoke_signed_with_unverified_program( core::slice::from_ref(state_signer), - token_program, + &token_program_id, )?; } @@ -81,7 +82,7 @@ mod tests { reclaim_buffer_data, NUM_SHARED_ACCOUNTS, }; use cow_settlement_interface::pda::state::state_pda_seeds; - use cow_settlement_interface::token_program::SPL_TOKEN_PROGRAM_ID; + use cow_settlement_interface::token_program::TokenProgram; use litesvm_token::spl_token::state::{Account as SplTokenAccount, AccountState}; use pinocchio::error::ProgramError; use solana_program_pack::Pack; @@ -92,6 +93,8 @@ mod tests { const AUTHORITY: Address = Address::new_from_array([101; 32]); const MANAGER: Address = Address::new_from_array([102; 32]); const UNRELATED: Address = Address::new_from_array([254; 32]); + /// The token program these fixtures build their buffers under. + const SPL_TOKEN_PROGRAM_ID: Address = TokenProgram::SplToken.address(); /// Number of accounts in a one-buffer reclaim: the shared ones plus a /// single `(buffer_pda, mint)` pair. diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 1458fe18..d41fe766 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -1,10 +1,7 @@ //! Token-program validation and token-account reads -use cow_settlement_interface::{ - token_program::{is_supported, SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID}, - SettlementError, -}; -use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView, Address}; +use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; +use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; use pinocchio_token::{instructions::GetAccountDataSize, state::Mint}; /// The length of a SPL token program account. Token2022 extensions may make @@ -12,32 +9,30 @@ use pinocchio_token::{instructions::GetAccountDataSize, state::Mint}; const BASE_TOKEN_ACCOUNT_LEN: u64 = pinocchio_token::state::Account::LEN as u64; /// Validate that `token_program_account` is a token program this program may -/// issue CPIs against, returning its address for the instruction to target. +/// issue CPIs against, returning the program for the instruction to target. #[must_use = "not consuming skips validation"] pub fn validate_token_program( token_program_account: &AccountView, -) -> Result<&Address, ProgramError> { - let address = token_program_account.address(); - if !is_supported(address) { - return Err(ProgramError::IncorrectProgramId); - } - Ok(address) +) -> Result { + TokenProgram::try_from(token_program_account.address()) } /// The data length a token account holding `mint` has to be allocated at. -/// It is assumed that `token_program` has already been validated with [`validate_token_program`]. -#[inline(never)] -pub fn token_account_len(token_program: &Address, mint: &AccountView) -> Result { +pub fn token_account_len( + token_program: TokenProgram, + mint: &AccountView, +) -> Result { // If the mint is of base SPL Mint length, the token accounts must be of base length accordingly. if mint.data_len() <= Mint::LEN { return Ok(BASE_TOKEN_ACCOUNT_LEN); } + let token_program = token_program.address(); // SPL token provides a function to get the actual required account data size - GetAccountDataSize::new(mint).invoke_with_unverified_program(token_program)?; + GetAccountDataSize::new(mint).invoke_with_unverified_program(&token_program)?; let reported = get_return_data().ok_or(SettlementError::BufferSizeUnavailable)?; - if reported.program_id() != token_program { + if reported.program_id() != &token_program { return Err(SettlementError::BufferSizeUnavailable.into()); } let length: [u8; 8] = reported @@ -62,32 +57,26 @@ pub struct TokenAccount { /// Read the base fields of the token account at `account`, which must be owned /// by `token_program`. -/// It is assumed that `token_program` has already been validated with [`validate_token_program`], -/// or else a program error will be thrown. pub fn read_token_account( - token_program: &Address, + token_program: TokenProgram, account: &AccountView, ) -> Result { - if token_program == &SPL_TOKEN_PROGRAM_ID { - let account = pinocchio_token::state::Account::from_account_view(account)?; - Ok(TokenAccount { - amount: account.amount(), - }) - } else if token_program == &TOKEN_2022_PROGRAM_ID { - let account = pinocchio_token_2022::state::Account::from_account_view(account)?; - Ok(TokenAccount { - amount: account.amount(), - }) - } else { - Err(ProgramError::IncorrectProgramId) - } + let amount = match token_program { + TokenProgram::SplToken => { + pinocchio_token::state::Account::from_account_view(account)?.amount() + } + TokenProgram::Token2022 => { + pinocchio_token_2022::state::Account::from_account_view(account)?.amount() + } + }; + Ok(TokenAccount { amount }) } #[cfg(test)] mod tests { use super::*; use cow_settlement_interface::instruction::fixtures::{fake_account, fake_account_owned_by}; - use cow_settlement_interface::token_program::SUPPORTED_TOKEN_PROGRAMS; + use pinocchio::Address; use pinocchio_token_2022::state::AccountType; /// An address that is not a token program. @@ -113,8 +102,8 @@ mod tests { /// dependency, so this is what keeps them from drifting apart. #[test] fn interface_and_pinocchio_agree_on_the_program_ids() { - assert_eq!(SPL_TOKEN_PROGRAM_ID, pinocchio_token::ID); - assert_eq!(TOKEN_2022_PROGRAM_ID, pinocchio_token_2022::ID); + assert_eq!(TokenProgram::SplToken.address(), pinocchio_token::ID); + assert_eq!(TokenProgram::Token2022.address(), pinocchio_token_2022::ID); } /// The base layout is the same under both programs, so one reader's idea of @@ -126,12 +115,12 @@ mod tests { #[test] fn token_account_len_is_the_base_layout_for_a_plain_mint() { - for program in SUPPORTED_TOKEN_PROGRAMS { - let mint = fake_account_owned_by(UNRELATED, program, &[0u8; Mint::LEN]); + for program in TokenProgram::ALL { + let mint = fake_account_owned_by(UNRELATED, program.address(), &[0u8; Mint::LEN]); assert_eq!( - token_account_len(&program, &mint), + token_account_len(program, &mint), Ok(BASE_TOKEN_ACCOUNT_LEN), - "a base-layout mint should need a base-layout account under {program}", + "a base-layout mint should need a base-layout account under {program:?}", ); } } @@ -140,25 +129,29 @@ mod tests { fn token_account_len_is_the_base_layout_for_a_too_short_account() { let mint = fake_account(UNRELATED); assert_eq!( - token_account_len(&SPL_TOKEN_PROGRAM_ID, &mint), + token_account_len(TokenProgram::SplToken, &mint), Ok(BASE_TOKEN_ACCOUNT_LEN), ); } #[test] fn token_account_len_reports_unavailable_without_an_answer() { - let mint = fake_account_owned_by(UNRELATED, TOKEN_2022_PROGRAM_ID, &[0u8; Mint::LEN + 1]); + let mint = fake_account_owned_by( + UNRELATED, + TokenProgram::Token2022.address(), + &[0u8; Mint::LEN + 1], + ); assert_eq!( - token_account_len(&TOKEN_2022_PROGRAM_ID, &mint).err(), + token_account_len(TokenProgram::Token2022, &mint).err(), Some(SettlementError::BufferSizeUnavailable.into()), ); } #[test] fn validate_token_program_accepts_every_supported_program() { - for program in SUPPORTED_TOKEN_PROGRAMS { - let account = fake_account(program); - assert_eq!(validate_token_program(&account), Ok(&program)); + for program in TokenProgram::ALL { + let account = fake_account(program.address()); + assert_eq!(validate_token_program(&account), Ok(program)); } } @@ -175,11 +168,14 @@ mod tests { fn read_token_account_reads_a_base_layout_account() { let mint = Address::new_from_array([0x11; 32]); let owner = Address::new_from_array([0x22; 32]); - for program in SUPPORTED_TOKEN_PROGRAMS { - let account = - fake_account_owned_by(UNRELATED, program, &base_layout(mint, owner, 4_200)); - let read = read_token_account(&program, &account) - .unwrap_or_else(|error| panic!("{program} account should read: {error:?}")); + for program in TokenProgram::ALL { + let account = fake_account_owned_by( + UNRELATED, + program.address(), + &base_layout(mint, owner, 4_200), + ); + let read = read_token_account(program, &account) + .unwrap_or_else(|error| panic!("{program:?} account should read: {error:?}")); assert_eq!(read.amount, 4_200); } } @@ -194,8 +190,8 @@ mod tests { data.push(AccountType::Account as u8); data.extend_from_slice(&[0xab; 16]); - let account = fake_account_owned_by(UNRELATED, TOKEN_2022_PROGRAM_ID, &data); - let read = read_token_account(&TOKEN_2022_PROGRAM_ID, &account) + let account = fake_account_owned_by(UNRELATED, TokenProgram::Token2022.address(), &data); + let read = read_token_account(TokenProgram::Token2022, &account) .expect("an extended Token-2022 account should read"); assert_eq!(read.amount, 7); } @@ -205,40 +201,28 @@ mod tests { let mut data = base_layout(UNRELATED, UNRELATED, 7); data.push(AccountType::Mint as u8); - let account = fake_account_owned_by(UNRELATED, TOKEN_2022_PROGRAM_ID, &data); + let account = fake_account_owned_by(UNRELATED, TokenProgram::Token2022.address(), &data); assert_eq!( - read_token_account(&TOKEN_2022_PROGRAM_ID, &account).err(), + read_token_account(TokenProgram::Token2022, &account).err(), Some(ProgramError::InvalidAccountData), ); } - #[test] - fn read_token_account_rejects_unvalidated_program() { - // A well-formed legacy token account, so the rejection can only come - // from the program address. - let account = fake_account_owned_by( - UNRELATED, - SPL_TOKEN_PROGRAM_ID, - &base_layout(UNRELATED, UNRELATED, 0), - ); - assert_eq!( - read_token_account(&UNRELATED, &account).err(), - Some(ProgramError::IncorrectProgramId), - ); - } - #[test] fn read_token_account_rejects_the_other_programs_account() { for [program, other] in [ - [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID], - [TOKEN_2022_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID], + [TokenProgram::SplToken, TokenProgram::Token2022], + [TokenProgram::Token2022, TokenProgram::SplToken], ] { - let account = - fake_account_owned_by(UNRELATED, other, &base_layout(UNRELATED, UNRELATED, 0)); + let account = fake_account_owned_by( + UNRELATED, + other.address(), + &base_layout(UNRELATED, UNRELATED, 0), + ); assert_eq!( - read_token_account(&program, &account).err(), + read_token_account(program, &account).err(), Some(ProgramError::InvalidAccountData), - "an account owned by {other} should not read under {program}", + "an account owned by {other:?} should not read under {program:?}", ); } } diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index c081e5ba..b6638321 100644 --- a/programs/settlement/tests/common/buffer.rs +++ b/programs/settlement/tests/common/buffer.rs @@ -1,7 +1,6 @@ //! Buffer-account helpers for the settlement integration tests. use cow_settlement_client::cow_settlement_interface::pda::buffer::find_buffer_pda; -use cow_settlement_client::cow_settlement_interface::token_program::SPL_TOKEN_PROGRAM_ID; use cow_settlement_client::cow_settlement_interface::Instruction; use cow_settlement_client::instructions::CreateBuffers; use litesvm::LiteSVM; @@ -11,7 +10,7 @@ use solana_sdk::{ transaction::Transaction, }; -use super::token; +use super::{token, SPL_TOKEN_PROGRAM_ID}; /// The canonical buffer PDA for `mint`. pub fn buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> Pubkey { diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index cb54dcc2..be3d52a3 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -16,6 +16,7 @@ pub mod token; use cow_settlement_client::instructions::{AddSolver, Initialize}; use cow_settlement_interface::pda::state::find_state_pda; +use cow_settlement_interface::token_program::TokenProgram; use cow_settlement_interface::Instruction; use cow_settlement_interface::SettlementError; use litesvm::{types::TransactionMetadata, LiteSVM}; @@ -34,6 +35,10 @@ pub const PROGRAM_SO: &str = concat!( "/../../target/deploy/cow_settlement.so" ); +/// The legacy SPL Token program, which the tests create their buffers and +/// token accounts under unless they exercise Token-2022 specifically. +pub const SPL_TOKEN_PROGRAM_ID: Pubkey = TokenProgram::SplToken.address(); + pub const CPI_CALLER_SO: &str = concat!( env!("CARGO_MANIFEST_DIR"), "/../../target/deploy/test_cpi_caller.so" diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index 17dd2c3f..30a2bd3c 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -4,7 +4,6 @@ use cow_settlement_client::cow_settlement_interface::{ buffer::{buffer_pda_seeds, find_buffer_pda}, state::find_state_pda, }, - token_program::SPL_TOKEN_PROGRAM_ID, }; use cow_settlement_client::instructions::CreateBuffers; use litesvm::LiteSVM; @@ -29,7 +28,7 @@ use solana_sdk::{ use crate::common::{ benchmark::{send_transaction_metered, BenchLabel}, - unique_keypair, unique_pubkey, + unique_keypair, unique_pubkey, SPL_TOKEN_PROGRAM_ID, }; mod common; diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index be614c64..5231ed19 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -2,7 +2,7 @@ use cow_settlement_client::instructions::ReclaimBuffer; use cow_settlement_interface::Instruction; use cow_settlement_interface::{ instruction::reclaim_buffer::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, - pda::state::find_state_pda, token_program::SPL_TOKEN_PROGRAM_ID, SettlementError, + pda::state::find_state_pda, SettlementError, }; use litesvm::LiteSVM; use solana_sdk::{ @@ -15,6 +15,7 @@ use crate::common::benchmark::{send_transaction_metered, BenchLabel}; use crate::common::buffer::ensure_buffer_exists; use crate::common::{ assert_instruction_error, to_instruction_error, unique_pubkey, InitializedParams, + SPL_TOKEN_PROGRAM_ID, }; mod common; diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index ead67e0f..e0a25f8f 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -4,7 +4,8 @@ use cow_settlement_client::{ cow_settlement_interface::{ data::{intent::OrderIntent, order::OrderAccount}, pda::buffer::find_buffer_pda, - token_program, Pubkey, + token_program::TokenProgram, + Pubkey, }, instructions::{ BeginSettle, CreateBuffers, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, @@ -302,7 +303,7 @@ fn prepare_setup_ixs( program_id: ctx.program_id, payer: ctx.payer.pubkey(), // The CLI only resolves tokens on the legacy program for now. - token_program: token_program::SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken.address(), mints: &mint_buffers_to_create.into_iter().collect::>(), } .into(), From 5fd2f6ab5b4e9578d7e76cfa980dd5c330f6de41 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:26:22 +0900 Subject: [PATCH 08/36] idl update --- interface/src/lib.rs | 2 +- programs/settlement/idl/cow_settlement.json | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/interface/src/lib.rs b/interface/src/lib.rs index b510750a..2b1ecd06 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -28,7 +28,7 @@ pub enum SettlementInstruction { CreateOrder = 2, /// Creates the singleton settlement state PDA. Succeeds only once. Initialize = 3, - /// Creates one or more per-token buffer PDAs (SPL token accounts) in a + /// Creates one or more per-token buffer PDAs (token accounts) in a /// single instruction. /// /// Each buffer_pda_i must be the canonical PDA for seeds diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json index bb11b0d6..19a8f583 100644 --- a/programs/settlement/idl/cow_settlement.json +++ b/programs/settlement/idl/cow_settlement.json @@ -85,7 +85,8 @@ { "name": "create_buffer", "docs": [ - "Creates one or more per-token buffer PDAs (SPL token accounts) in a single instruction.", + "Creates one or more per-token buffer PDAs (token accounts) in a single instruction.", + "Every buffer created by one instruction is owned by the single token_program the instruction is handed, so mints spread across both supported token programs need one instruction each.", "IDL LIMITATION: the real instruction accepts an unbounded number of (buffer_pda, mint) pairs as remaining accounts, one pair per buffer, with at least one pair required (CreateBuffer rejects zero buffers). IDL grammar has no 'repeated group' construct, so this file only declares the guaranteed index-0 template (buffer_pda_0/mint_0).", "Each buffer_pda_i must be the canonical PDA for seeds [SETTLEMENT_SEED, mint_i, \"buffer\"]." ], @@ -104,7 +105,9 @@ }, { "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "docs": [ + "The token program that will own the created buffer PDAs. Must be one of the supported token accounts." + ] }, { "name": "buffer_pda_0", @@ -330,6 +333,7 @@ "name": "reclaim_buffer", "docs": [ "Closes one or more buffer PDAs and sends each closed buffer's rent lamports to a reclaim_recipient of the caller's choosing. Only the current holder of the ReclaimAuthority role recorded in the state PDA may authorize this. A buffer that still holds tokens is skipped, not closed, and the instruction still succeeds.", + "Every buffer closed by one instruction must be owned by the single token_program the instruction is handed, so buffers spread across both supported token programs need one instruction each.", "IDL LIMITATION: the real instruction accepts an unbounded number of (buffer_pda, mint) pairs as remaining accounts, one pair per buffer, with at least one pair required (ReclaimBuffer rejects zero buffers). IDL grammar has no 'repeated group' construct, so this file only declares the guaranteed index-0 template (buffer_pda_0/mint_0).", "Each buffer_pda_i must be the canonical PDA for seeds [SETTLEMENT_SEED, mint_i, \"buffer\"]; mint_i is passed only so that derivation can be checked on-chain." ], @@ -387,7 +391,9 @@ }, { "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "docs": [ + "The token program that owns the created buffer PDAs. Must be one of the supported token accounts." + ] }, { "name": "buffer_pda_0", @@ -966,6 +972,11 @@ "code": 38, "name": "OrderCreatedOnChainMismatch", "msg": "A created order's intent isn't set with the created_on_chain flag corresponding to the behavior of the invoked order creation instruction." + }, + { + "code": 39, + "name": "BufferSizeUnavailable", + "msg": "CreateBuffer asked the token program how long a token account for a mint has to be and couldn't read the answer, so it can't size the buffer." } ] } From 7836f892b40572729990b08afeb15cb9390904de Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:27:50 +0900 Subject: [PATCH 09/36] remove the unnecessary early return which skips the length CPI --- programs/settlement/src/token.rs | 29 ++-------------------- programs/settlement/tests/common/token.rs | 25 ++++++++++++++++++- programs/settlement/tests/create_buffer.rs | 8 +++--- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index d41fe766..a491ebe5 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -2,7 +2,7 @@ use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; -use pinocchio_token::{instructions::GetAccountDataSize, state::Mint}; +use pinocchio_token::instructions::GetAccountDataSize; /// The length of a SPL token program account. Token2022 extensions may make /// the actual token account longer than this. @@ -22,11 +22,6 @@ pub fn token_account_len( token_program: TokenProgram, mint: &AccountView, ) -> Result { - // If the mint is of base SPL Mint length, the token accounts must be of base length accordingly. - if mint.data_len() <= Mint::LEN { - return Ok(BASE_TOKEN_ACCOUNT_LEN); - } - let token_program = token_program.address(); // SPL token provides a function to get the actual required account data size GetAccountDataSize::new(mint).invoke_with_unverified_program(&token_program)?; @@ -77,6 +72,7 @@ mod tests { use super::*; use cow_settlement_interface::instruction::fixtures::{fake_account, fake_account_owned_by}; use pinocchio::Address; + use pinocchio_token::state::Mint; use pinocchio_token_2022::state::AccountType; /// An address that is not a token program. @@ -113,27 +109,6 @@ mod tests { assert_eq!(BASE_LEN, pinocchio_token_2022::state::Account::BASE_LEN); } - #[test] - fn token_account_len_is_the_base_layout_for_a_plain_mint() { - for program in TokenProgram::ALL { - let mint = fake_account_owned_by(UNRELATED, program.address(), &[0u8; Mint::LEN]); - assert_eq!( - token_account_len(program, &mint), - Ok(BASE_TOKEN_ACCOUNT_LEN), - "a base-layout mint should need a base-layout account under {program:?}", - ); - } - } - - #[test] - fn token_account_len_is_the_base_layout_for_a_too_short_account() { - let mint = fake_account(UNRELATED); - assert_eq!( - token_account_len(TokenProgram::SplToken, &mint), - Ok(BASE_TOKEN_ACCOUNT_LEN), - ); - } - #[test] fn token_account_len_reports_unavailable_without_an_answer() { let mint = fake_account_owned_by( diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index a6e7b893..6889c467 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -3,7 +3,7 @@ use cow_settlement_client::cow_settlement_interface::pda::state::find_state_pda; use litesvm::{types::TransactionMetadata, LiteSVM}; use litesvm_token::{ - spl_token::{instruction::initialize_mint2, state::Mint}, + spl_token::{instruction::initialize_mint2, native_mint, state::Mint}, Approve, CreateAccount, CreateAssociatedTokenAccount, MintTo, Transfer, TOKEN_ID, }; use solana_program_pack::Pack; @@ -48,6 +48,29 @@ pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { mint.pubkey() } +/// Seed the wrapped-SOL mint account, which `LiteSVM` does not create. +/// +/// On chain the native mint is a real, token-program-owned mint account, and +/// the program queries it with `GetAccountDataSize` to size a buffer. That +/// query rejects an account the token program does not own, so a test touching +/// the native mint has to put the account there first. +pub fn create_native_mint(svm: &mut LiteSVM) { + /// The native mint's fixed decimals, matching `spl_token::native_mint`. + const DECIMALS: u8 = 9; + + let mut data = vec![0u8; Mint::LEN]; + Mint { + mint_authority: None.into(), + supply: 0, + decimals: DECIMALS, + is_initialized: true, + freeze_authority: None.into(), + } + .pack_into_slice(&mut data); + let token_program = Pubkey::new_from_array(TOKEN_ID.to_bytes()); + super::create_account_at(svm, native_mint::ID, &token_program, &data); +} + /// Create an initialized SPL token account for `mint` whose SPL owner is /// `owner`, funded by `payer`, and return its address. Each call produces a /// fresh account, so the same `owner` can hold several accounts for one `mint`. diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index 30a2bd3c..a826093d 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -140,11 +140,11 @@ fn buffer_can_receive_tokens() { #[test] fn happy_path_creates_native_token_buffer() { - // The native mint is special-cased by the token program: it's recognized by - // key (no mint-account validation) and the buffer is initialized as a - // wrapped-SOL account. Since we fund exactly the rent-exempt minimum, the - // wrapped balance starts at zero. + // `InitializeAccount` special-cases the native mint: it's recognized by key + // and the buffer is initialized as a wrapped-SOL account. Since we fund + // exactly the rent-exempt minimum, the wrapped balance starts at zero. let (mut svm, program_id, payer) = common::setup(); + common::token::create_native_mint(&mut svm); let (buffer_pda, _bump) = find_buffer_pda(&program_id, &native_mint::ID); let ix = CreateBuffers { From 421a92669feb061f8e9167e47ffbc12647778887 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:10:34 +0900 Subject: [PATCH 10/36] update documentation and design --- DESIGN.md | 8 +++----- interface/src/instruction/create_buffer.rs | 5 +++++ interface/src/instruction/reclaim_buffer.rs | 5 +++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index e0412bc2..a3283aed 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -363,8 +363,6 @@ The main reason to prefer the SOL flow described here is the handling of the ren ## Token 2022 -The settlement program natively supports [Token-2022](https://www.solana-program.com/docs/token-2022) tokens. All token -extensions will be supported. - -Only one token program can be supplied to an instruction at a time. If mints from two separate token programs are required, -the client needs to divide it into separate instructions. +The settlement program natively supports [Token-2022](https://www.solana-program.com/docs/token-2022) tokens. The settlement program does not explicitly block any +token extensions, but if the extension interferes with the settlement process such that it causes a failure (such as locked tokens), then any affected orders will +be unsettleable. diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index b0146e0b..30a67e54 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -25,6 +25,11 @@ use crate::SettlementInstruction; /// Buffer creation is idempotent: an already-existing `buffer_pda` is left /// unchanged and the instruction still succeeds, so two parties racing to /// create the same buffer both succeed. +/// +/// The token_program supplied to this instruction must be the owner of all mints +/// supplied. Only one token program can be supplied to this instruction at a time. +/// If mints from two separate token programs are required, the client needs to +/// divide it into separate instructions. /// /// Wire format: `[discriminator=4]`, 1 byte. The tokens are implied by the /// `mint` accounts, so no further data is needed. diff --git a/interface/src/instruction/reclaim_buffer.rs b/interface/src/instruction/reclaim_buffer.rs index dc633e46..65f40188 100644 --- a/interface/src/instruction/reclaim_buffer.rs +++ b/interface/src/instruction/reclaim_buffer.rs @@ -6,6 +6,11 @@ //! [`crate::data::state::StateAccount`]) may authorize this. //! //! A buffer that still holds tokens is skipped, not closed. +//! +//! The token_program supplied to this instruction must be the owner of all mints +//! supplied. Only one token program can be supplied to this instruction at a time. +//! If mints from two separate token programs are required, the client needs to +//! divide it into separate instructions. //! //! Wire format: `[discriminator=6]`, 1 byte. //! Required accounts: From ce75edfe1cd690ba4871e6c577f8dd06cab70b45 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:22:15 +0900 Subject: [PATCH 11/36] update tests --- interface/src/instruction/create_buffer.rs | 80 +++++++++++----------- programs/settlement/src/token.rs | 40 ++++++----- 2 files changed, 63 insertions(+), 57 deletions(-) diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index 30a67e54..976d94d8 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -25,10 +25,10 @@ use crate::SettlementInstruction; /// Buffer creation is idempotent: an already-existing `buffer_pda` is left /// unchanged and the instruction still succeeds, so two parties racing to /// create the same buffer both succeed. -/// +/// /// The token_program supplied to this instruction must be the owner of all mints -/// supplied. Only one token program can be supplied to this instruction at a time. -/// If mints from two separate token programs are required, the client needs to +/// supplied. Only one token program can be supplied to this instruction at a time. +/// If mints from two separate token programs are required, the client needs to /// divide it into separate instructions. /// /// Wire format: `[discriminator=4]`, 1 byte. The tokens are implied by the @@ -152,23 +152,21 @@ pub mod fixtures { mod tests { use super::fixtures::{create_buffer_data, NUM_SHARED_ACCOUNTS}; use super::*; - use crate::instruction::fixtures::{ - fake_account, fake_account_from_array, fake_sequential_accounts, - }; + use crate::fixtures::pubkey_from_seed; + use crate::instruction::fixtures::{fake_account, fake_sequential_accounts}; use crate::instruction::tests::{ assert_readonly_nonsigner, assert_writable_nonsigner, assert_writable_signer, }; use solana_account_view::AccountView; - use solana_address::Address; #[test] fn create_buffer_input_parses_valid_input() { - let program_id: Address = Address::new_from_array([1; 32]); - let payer: Address = Address::new_from_array([2; 32]); - let token_program = Address::new_from_array([3; 32]); - let system_program = fake_account_from_array([4; 32]); - let buffer_pda = Address::new_from_array([5; 32]); - let mint = Address::new_from_array([6; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let system_program = fake_account(pubkey_from_seed("system program")); + let buffer_pda = pubkey_from_seed("buffer pda"); + let mint = pubkey_from_seed("mint"); let data = Instruction::from(CreateBuffers { program_id, @@ -197,13 +195,13 @@ mod tests { #[test] fn create_buffer_input_parses_multiple_buffers() { - let program_id = Address::new_from_array([1; 32]); - let payer = Address::new_from_array([2; 32]); - let token_program = Address::new_from_array([3; 32]); - let buffer_a = Address::new_from_array([4; 32]); - let mint_a = Address::new_from_array([5; 32]); - let buffer_b = Address::new_from_array([6; 32]); - let mint_b = Address::new_from_array([7; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_a = pubkey_from_seed("buffer a"); + let mint_a = pubkey_from_seed("mint a"); + let buffer_b = pubkey_from_seed("buffer b"); + let mint_b = pubkey_from_seed("mint b"); let data = Instruction::from(CreateBuffers { program_id, @@ -214,7 +212,7 @@ mod tests { .data; let accounts = [ fake_account(payer), - fake_account_from_array([4; 32]), + fake_account(pubkey_from_seed("system program")), fake_account(token_program), fake_account(buffer_a), fake_account(mint_a), @@ -282,11 +280,11 @@ mod tests { #[test] fn instruction_data_has_expected_layout() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); - let token_program = Pubkey::new_from_array([3; 32]); - let buffer_pda = Pubkey::new_from_array([4; 32]); - let mint = Pubkey::new_from_array([5; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_pda = pubkey_from_seed("buffer pda"); + let mint = pubkey_from_seed("mint"); let Instruction { data, .. } = CreateBuffers { program_id, payer, @@ -302,11 +300,11 @@ mod tests { #[test] fn single_buffer_has_expected_accounts() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); - let token_program = Pubkey::new_from_array([3; 32]); - let buffer_pda = Pubkey::new_from_array([4; 32]); - let mint = Pubkey::new_from_array([5; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_pda = pubkey_from_seed("buffer pda"); + let mint = pubkey_from_seed("mint"); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, @@ -328,13 +326,13 @@ mod tests { #[test] fn multiple_buffers_append_pairs_after_shared_accounts() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); - let token_program = Pubkey::new_from_array([3; 32]); - let buffer_a = Pubkey::new_from_array([4; 32]); - let mint_a = Pubkey::new_from_array([5; 32]); - let buffer_b = Pubkey::new_from_array([6; 32]); - let mint_b = Pubkey::new_from_array([7; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_a = pubkey_from_seed("buffer a"); + let mint_a = pubkey_from_seed("mint a"); + let buffer_b = pubkey_from_seed("buffer b"); + let mint_b = pubkey_from_seed("mint b"); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, @@ -353,9 +351,9 @@ mod tests { #[test] fn empty_buffers_has_only_shared_accounts() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); - let token_program = Pubkey::new_from_array([3; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index a491ebe5..f7afe1ea 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -70,14 +70,14 @@ pub fn read_token_account( #[cfg(test)] mod tests { use super::*; - use cow_settlement_interface::instruction::fixtures::{fake_account, fake_account_owned_by}; + use cow_settlement_interface::{ + fixtures::pubkey_from_seed, + instruction::fixtures::{fake_account, fake_account_owned_by}, + }; use pinocchio::Address; use pinocchio_token::state::Mint; use pinocchio_token_2022::state::AccountType; - /// An address that is not a token program. - const UNRELATED: Address = Address::new_from_array([0x99; 32]); - /// The length of a token account holding nothing but the base layout. Both /// programs share it: it is Token-2022's `BASE_LEN` and the whole of a /// legacy account. @@ -112,7 +112,7 @@ mod tests { #[test] fn token_account_len_reports_unavailable_without_an_answer() { let mint = fake_account_owned_by( - UNRELATED, + pubkey_from_seed("mint"), TokenProgram::Token2022.address(), &[0u8; Mint::LEN + 1], ); @@ -132,7 +132,7 @@ mod tests { #[test] fn validate_token_program_rejects_unrelated_program() { - let account = fake_account(UNRELATED); + let account = fake_account(pubkey_from_seed("not a token program")); assert_eq!( validate_token_program(&account), Err(ProgramError::IncorrectProgramId), @@ -141,11 +141,11 @@ mod tests { #[test] fn read_token_account_reads_a_base_layout_account() { - let mint = Address::new_from_array([0x11; 32]); - let owner = Address::new_from_array([0x22; 32]); + let mint = pubkey_from_seed("mint"); + let owner = pubkey_from_seed("owner"); for program in TokenProgram::ALL { let account = fake_account_owned_by( - UNRELATED, + pubkey_from_seed("token account"), program.address(), &base_layout(mint, owner, 4_200), ); @@ -157,15 +157,19 @@ mod tests { #[test] fn read_token_account_reads_past_token_2022_extensions() { - let mint = Address::new_from_array([0x33; 32]); - let owner = Address::new_from_array([0x44; 32]); + let mint = pubkey_from_seed("extended mint"); + let owner = pubkey_from_seed("extended owner"); let mut data = base_layout(mint, owner, 7); // Extensions are preceded by the account-type marker, which is what // distinguishes a longer account from a mint of the same size. data.push(AccountType::Account as u8); data.extend_from_slice(&[0xab; 16]); - let account = fake_account_owned_by(UNRELATED, TokenProgram::Token2022.address(), &data); + let account = fake_account_owned_by( + pubkey_from_seed("token account"), + TokenProgram::Token2022.address(), + &data, + ); let read = read_token_account(TokenProgram::Token2022, &account) .expect("an extended Token-2022 account should read"); assert_eq!(read.amount, 7); @@ -173,10 +177,14 @@ mod tests { #[test] fn read_token_account_rejects_an_extended_mint() { - let mut data = base_layout(UNRELATED, UNRELATED, 7); + let mut data = base_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 7); data.push(AccountType::Mint as u8); - let account = fake_account_owned_by(UNRELATED, TokenProgram::Token2022.address(), &data); + let account = fake_account_owned_by( + pubkey_from_seed("mint account"), + TokenProgram::Token2022.address(), + &data, + ); assert_eq!( read_token_account(TokenProgram::Token2022, &account).err(), Some(ProgramError::InvalidAccountData), @@ -190,9 +198,9 @@ mod tests { [TokenProgram::Token2022, TokenProgram::SplToken], ] { let account = fake_account_owned_by( - UNRELATED, + pubkey_from_seed("token account"), other.address(), - &base_layout(UNRELATED, UNRELATED, 0), + &base_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 0), ); assert_eq!( read_token_account(program, &account).err(), From 88b1c485a049664793342ee3f2731457538f6e9f Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:35:27 +0900 Subject: [PATCH 12/36] fix undo changes and lint --- interface/src/instruction/reclaim_buffer.rs | 6 ++-- programs/settlement/src/token.rs | 33 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/interface/src/instruction/reclaim_buffer.rs b/interface/src/instruction/reclaim_buffer.rs index 65f40188..ad400f43 100644 --- a/interface/src/instruction/reclaim_buffer.rs +++ b/interface/src/instruction/reclaim_buffer.rs @@ -6,10 +6,10 @@ //! [`crate::data::state::StateAccount`]) may authorize this. //! //! A buffer that still holds tokens is skipped, not closed. -//! +//! //! The token_program supplied to this instruction must be the owner of all mints -//! supplied. Only one token program can be supplied to this instruction at a time. -//! If mints from two separate token programs are required, the client needs to +//! supplied. Only one token program can be supplied to this instruction at a time. +//! If mints from two separate token programs are required, the client needs to //! divide it into separate instructions. //! //! Wire format: `[discriminator=6]`, 1 byte. diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index f7afe1ea..545b8820 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -2,7 +2,7 @@ use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; -use pinocchio_token::instructions::GetAccountDataSize; +use pinocchio_token::{instructions::GetAccountDataSize, state::Mint}; /// The length of a SPL token program account. Token2022 extensions may make /// the actual token account longer than this. @@ -22,6 +22,11 @@ pub fn token_account_len( token_program: TokenProgram, mint: &AccountView, ) -> Result { + // If the mint is of base SPL Mint length, the token accounts must be of base length accordingly. + if mint.data_len() <= Mint::LEN { + return Ok(BASE_TOKEN_ACCOUNT_LEN); + } + let token_program = token_program.address(); // SPL token provides a function to get the actual required account data size GetAccountDataSize::new(mint).invoke_with_unverified_program(&token_program)?; @@ -75,7 +80,6 @@ mod tests { instruction::fixtures::{fake_account, fake_account_owned_by}, }; use pinocchio::Address; - use pinocchio_token::state::Mint; use pinocchio_token_2022::state::AccountType; /// The length of a token account holding nothing but the base layout. Both @@ -109,6 +113,31 @@ mod tests { assert_eq!(BASE_LEN, pinocchio_token_2022::state::Account::BASE_LEN); } + #[test] + fn token_account_len_is_the_base_layout_for_a_plain_mint() { + for program in TokenProgram::ALL { + let mint = fake_account_owned_by( + pubkey_from_seed("mint"), + program.address(), + &[0u8; Mint::LEN], + ); + assert_eq!( + token_account_len(program, &mint), + Ok(BASE_TOKEN_ACCOUNT_LEN), + "a base-layout mint should need a base-layout account under {program:?}", + ); + } + } + + #[test] + fn token_account_len_is_the_base_layout_for_a_too_short_account() { + let mint = fake_account(pubkey_from_seed("mint")); + assert_eq!( + token_account_len(TokenProgram::SplToken, &mint), + Ok(BASE_TOKEN_ACCOUNT_LEN), + ); + } + #[test] fn token_account_len_reports_unavailable_without_an_answer() { let mint = fake_account_owned_by( From 90a87834b79cdb919f66ab5e42298025265011d2 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:38:58 +0900 Subject: [PATCH 13/36] fix bench --- bench-report.json | 52 +++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/bench-report.json b/bench-report.json index aeb8109e..cb301ab9 100644 --- a/bench-report.json +++ b/bench-report.json @@ -30,34 +30,34 @@ "transfer_authority/reclaim_authority_can_transfer_itself": 4 }, "compute_units": { - "add_solver/add_with_many_existing_solvers": 5066, - "add_solver/adds_a_solver": 4614, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 7367, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17305, - "create_buffers/max_buffers_in_one_instruction": 170142, - "create_order/happy_path_creates_order_pda_with_expected_body": 4981, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4523, - "reclaim_buffer/funded_buffer_is_skipped": 4854, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 6003, - "reclaim_buffer/max_buffers_in_one_instruction": 125165, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7618, - "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2196, - "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2066, - "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2074, + "add_solver/add_with_many_existing_solvers": 5074, + "add_solver/adds_a_solver": 4622, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 7376, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17290, + "create_buffers/max_buffers_in_one_instruction": 169803, + "create_order/happy_path_creates_order_pda_with_expected_body": 4985, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4529, + "reclaim_buffer/funded_buffer_is_skipped": 4859, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 6009, + "reclaim_buffer/max_buffers_in_one_instruction": 124823, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7611, + "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2202, + "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2072, + "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2079, "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": null, "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, - "settle/finalizes_with_no_pushes": 7126, - "settle/pulls_from_multiple_orders": 19997, - "settle/pulls_funds_to_destination": 13595, - "settle/pulls_to_multiple_destinations": 14735, - "settle/pushes_a_single_order": 12451, - "settle/pushes_several_orders_from_different_buffers": 17707, - "settle/pushes_several_orders_from_one_buffer": 17706, - "settle/settles_a_single_order": 12469, - "settle/settles_multiple_orders": 23010, - "transfer_authority/manager_can_transfer_manager": 3169, - "transfer_authority/manager_can_transfer_reclaim_authority": 3171, - "transfer_authority/reclaim_authority_can_transfer_itself": 3175 + "settle/finalizes_with_no_pushes": 7171, + "settle/pulls_from_multiple_orders": 20076, + "settle/pulls_funds_to_destination": 13657, + "settle/pulls_to_multiple_destinations": 14798, + "settle/pushes_a_single_order": 12512, + "settle/pushes_several_orders_from_different_buffers": 17784, + "settle/pushes_several_orders_from_one_buffer": 17783, + "settle/settles_a_single_order": 12530, + "settle/settles_multiple_orders": 23103, + "transfer_authority/manager_can_transfer_manager": 3174, + "transfer_authority/manager_can_transfer_reclaim_authority": 3176, + "transfer_authority/reclaim_authority_can_transfer_itself": 3180 }, "transaction_bytes": { "add_solver/add_with_many_existing_solvers": 366, From 1d5ac4600d633222cfbe5174fdad32f9bc10b2a0 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:25:17 +0900 Subject: [PATCH 14/36] Update programs/settlement/src/token.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/src/token.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 545b8820..5758e25c 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -102,8 +102,13 @@ mod tests { /// dependency, so this is what keeps them from drifting apart. #[test] fn interface_and_pinocchio_agree_on_the_program_ids() { - assert_eq!(TokenProgram::SplToken.address(), pinocchio_token::ID); - assert_eq!(TokenProgram::Token2022.address(), pinocchio_token_2022::ID); + for program in TokenProgram::ALL { + let pinocchio_id = match program { + TokenProgram::SplToken => pinocchio_token::ID, + TokenProgram::Token2022 => pinocchio_token_2022::ID, + }; + assert_eq!(program.address(), pinocchio_id); + } } /// The base layout is the same under both programs, so one reader's idea of From 08fc979b6bfcb20cf15a2fc910403b5931f09ed6 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:50:13 +0900 Subject: [PATCH 15/36] Update Cargo.toml Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dfddf517..e691330d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,8 +60,6 @@ solana-system-interface = "3" spl-associated-token-account-interface = "2" spl-token = "9" spl-token-2022-interface = "3" -# Only the test CLI still builds against the legacy interface; it moves over -# together with its Token-2022 support. spl-token-interface = "3" syn = { version = "2", features = ["full"] } From b427f49adef40928e8222aa9d11a92c3c360ac08 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:50:20 +0900 Subject: [PATCH 16/36] Update programs/settlement/src/reclaim_buffer.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/src/reclaim_buffer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/settlement/src/reclaim_buffer.rs b/programs/settlement/src/reclaim_buffer.rs index 9c5961e2..b7046698 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/reclaim_buffer.rs @@ -93,7 +93,6 @@ mod tests { const AUTHORITY: Address = Address::new_from_array([101; 32]); const MANAGER: Address = Address::new_from_array([102; 32]); const UNRELATED: Address = Address::new_from_array([254; 32]); - /// The token program these fixtures build their buffers under. const SPL_TOKEN_PROGRAM_ID: Address = TokenProgram::SplToken.address(); /// Number of accounts in a one-buffer reclaim: the shared ones plus a From a0910475a9c3d1fff6cdf737cb982a433b89cf42 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:50:52 +0900 Subject: [PATCH 17/36] Update programs/settlement/tests/common/token.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/common/token.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 6889c467..6d505397 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -49,11 +49,6 @@ pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { } /// Seed the wrapped-SOL mint account, which `LiteSVM` does not create. -/// -/// On chain the native mint is a real, token-program-owned mint account, and -/// the program queries it with `GetAccountDataSize` to size a buffer. That -/// query rejects an account the token program does not own, so a test touching -/// the native mint has to put the account there first. pub fn create_native_mint(svm: &mut LiteSVM) { /// The native mint's fixed decimals, matching `spl_token::native_mint`. const DECIMALS: u8 = 9; From 641c08cb03461a80ee086d922387c474a5663a9c Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:51:24 +0900 Subject: [PATCH 18/36] Update interface/src/token_program.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/token_program.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index df70efd4..c84b2983 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -44,16 +44,11 @@ mod tests { use super::*; use crate::fixtures::pubkey_from_seed; - /// Adding a variant breaks this match, which is the reminder to add it to - /// [`TokenProgram::ALL`] as well - #[test] - fn all_lists_every_variant() { - for program in TokenProgram::ALL { - match program { - TokenProgram::SplToken | TokenProgram::Token2022 => {} - } - } - } + /// Adding a variant makes this match non-exhaustive, the compile-time reminder + /// to add it to [`TokenProgram::ALL`] as well. + const _: () = match TokenProgram::SplToken { + TokenProgram::SplToken | TokenProgram::Token2022 => (), + }; #[test] fn every_program_resolves_from_its_own_address() { From e3ccd22ee7465b50c5d70a32bb2ba1d8e03975ed Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:56:40 +0900 Subject: [PATCH 19/36] use new tokenprgoram enum in client instructions --- client/src/instructions.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/client/src/instructions.rs b/client/src/instructions.rs index 849c92c3..7e0c40e2 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -6,9 +6,7 @@ //! by the function, making it more suitable for off-chain use. use cow_settlement_interface::{ - data::intent::{EncodedOrderIntent, OrderIntent}, - pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, - Instruction, Pubkey, Role, + Instruction, Pubkey, Role, data::intent::{EncodedOrderIntent, OrderIntent}, pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, token_program::TokenProgram, }; // Reexport the instruction builders that don't change from the interface. @@ -148,7 +146,7 @@ impl From> for Instruction { pub struct CreateBuffers<'a> { pub program_id: Pubkey, pub payer: Pubkey, - pub token_program: Pubkey, + pub token_program: TokenProgram, pub mints: &'a [Pubkey], } @@ -162,7 +160,7 @@ impl From> for Instruction { cow_settlement_interface::instruction::create_buffer::CreateBuffers { program_id: builder.program_id, payer: builder.payer, - token_program: builder.token_program, + token_program: builder.token_program.address(), buffers: &buffers, } .into() @@ -201,7 +199,7 @@ pub struct ReclaimBuffer<'a> { pub program_id: Pubkey, pub reclaim_authority: Pubkey, pub reclaim_recipient: Pubkey, - pub token_program: Pubkey, + pub token_program: TokenProgram, pub mints: &'a [Pubkey], } @@ -221,7 +219,7 @@ impl From> for Instruction { state_pda, reclaim_authority: builder.reclaim_authority, reclaim_recipient: builder.reclaim_recipient, - token_program: builder.token_program, + token_program: builder.token_program.address(), buffers: &buffers, } .into() From 1d2106b3b77c16a9924a1724ad228e148f0d3c0e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:35:48 +0900 Subject: [PATCH 20/36] switch to using tokenProgram in the client caused other changes --- client/src/parse.rs | 9 ++--- programs/settlement/tests/common/buffer.rs | 5 ++- programs/settlement/tests/create_buffer.rs | 44 ++++++++++----------- programs/settlement/tests/reclaim_buffer.rs | 22 +++++------ test-cli/src/cmd/settle.rs | 2 +- 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/client/src/parse.rs b/client/src/parse.rs index ce151009..45462d6b 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -77,13 +77,10 @@ mod tests { InitializedIntent, }; use cow_settlement_interface::{ - data::intent::fixtures::sample_intent, - fixtures::pubkey_from_seed, - instruction::{ + Instruction, Role, data::intent::fixtures::sample_intent, fixtures::pubkey_from_seed, instruction::{ fixtures::fake_account_from_array, reclaim_buffer::ReclaimBuffer, reclaim_order::ReclaimOrder, transfer_authority::TransferAuthority, - }, - Instruction, Role, + }, token_program::TokenProgram, }; /// One buildable instruction per discriminator. The exhaustive match makes @@ -110,7 +107,7 @@ mod tests { SettlementInstruction::CreateBuffer => CreateBuffers { program_id, payer, - token_program: pubkey_from_seed("token program"), + token_program: TokenProgram::SplToken, mints: &[pubkey_from_seed("mint")], } .into(), diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index b6638321..07b629d3 100644 --- a/programs/settlement/tests/common/buffer.rs +++ b/programs/settlement/tests/common/buffer.rs @@ -3,6 +3,7 @@ use cow_settlement_client::cow_settlement_interface::pda::buffer::find_buffer_pda; use cow_settlement_client::cow_settlement_interface::Instruction; use cow_settlement_client::instructions::CreateBuffers; +use cow_settlement_interface::token_program::TokenProgram; use litesvm::LiteSVM; use solana_sdk::{ pubkey::Pubkey, @@ -10,7 +11,7 @@ use solana_sdk::{ transaction::Transaction, }; -use super::{token, SPL_TOKEN_PROGRAM_ID}; +use super::token; /// The canonical buffer PDA for `mint`. pub fn buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> Pubkey { @@ -33,7 +34,7 @@ pub fn ensure_buffer_exists( let ix = Instruction::from(CreateBuffers { program_id: *program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[*mint], }); let tx = Transaction::new_signed_with_payer( diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index a826093d..0dcede6d 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -6,6 +6,7 @@ use cow_settlement_client::cow_settlement_interface::{ }, }; use cow_settlement_client::instructions::CreateBuffers; +use cow_settlement_interface::token_program::TokenProgram; use litesvm::LiteSVM; use litesvm_token::{ get_spl_account, @@ -28,7 +29,7 @@ use solana_sdk::{ use crate::common::{ benchmark::{send_transaction_metered, BenchLabel}, - unique_keypair, unique_pubkey, SPL_TOKEN_PROGRAM_ID, + unique_keypair, unique_pubkey, }; mod common; @@ -43,7 +44,7 @@ fn happy_path_creates_initialized_buffer_token_account() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -54,7 +55,7 @@ fn happy_path_creates_initialized_buffer_token_account() { .get_account(&buffer_pda) .expect("buffer PDA should exist after create_buffer"); assert_eq!( - account.owner, SPL_TOKEN_PROGRAM_ID, + account.owner, TokenProgram::SplToken.address(), "buffer must be owned by the SPL Token program" ); assert_eq!( @@ -111,7 +112,7 @@ fn buffer_can_receive_tokens() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -144,13 +145,12 @@ fn happy_path_creates_native_token_buffer() { // and the buffer is initialized as a wrapped-SOL account. Since we fund // exactly the rent-exempt minimum, the wrapped balance starts at zero. let (mut svm, program_id, payer) = common::setup(); - common::token::create_native_mint(&mut svm); let (buffer_pda, _bump) = find_buffer_pda(&program_id, &native_mint::ID); let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[native_mint::ID], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -186,7 +186,7 @@ fn happy_path_creates_multiple_buffers_in_one_instruction() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &mints, }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -199,7 +199,7 @@ fn happy_path_creates_multiple_buffers_in_one_instruction() { .get_account(&buffer_pda) .expect("each buffer PDA should exist after create_buffers"); assert_eq!( - account.owner, SPL_TOKEN_PROGRAM_ID, + account.owner, TokenProgram::SplToken.address(), "each buffer must be owned by the SPL Token program" ); assert_eq!( @@ -231,7 +231,7 @@ fn rejects_no_buffers() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -260,7 +260,7 @@ fn rejects_arbitrary_wrong_buffer_pda() { let ix = CreateBuffersRaw { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken.address(), buffers: &[(wrong_pda, mint)], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -281,7 +281,7 @@ fn rejects_non_canonical_bump_pda() { let ix = CreateBuffersRaw { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken.address(), buffers: &[(non_canonical_pda, mint)], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -298,13 +298,13 @@ fn rejects_non_spl_token_program() { let mut ix: Instruction = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], } .into(); let token_program_index = 2; assert_eq!( - ix.accounts[token_program_index].pubkey, SPL_TOKEN_PROGRAM_ID, + ix.accounts[token_program_index].pubkey, TokenProgram::SplToken.address(), "sanity: should replace token program" ); ix.accounts[token_program_index].pubkey = unique_pubkey(); @@ -342,7 +342,7 @@ fn rejects_invalid_mint() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[not_a_mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -376,7 +376,7 @@ fn creates_buffer_when_address_is_prefunded() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; common::signed_tx(svm, &payer, &payer, ix) @@ -393,7 +393,7 @@ fn recreating_same_buffer_is_idempotent() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; common::signed_tx(svm, &payer, &payer, ix) @@ -411,7 +411,7 @@ fn batch_with_existing_buffer_passes_with_no_changes() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[existing], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -426,7 +426,7 @@ fn batch_with_existing_buffer_passes_with_no_changes() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[fresh, existing], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -462,7 +462,7 @@ fn one_failing_buffer_reverts_the_whole_batch() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[fresh, not_a_mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -497,7 +497,7 @@ fn same_mint_twice_in_one_instruction_is_idempotent() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint, mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -523,7 +523,7 @@ fn max_buffers_via_lookup_table(svm: &mut LiteSVM, program_id: &Pubkey, payer: & let ix = CreateBuffersRaw { program_id: *program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken.address(), buffers: &buffers, }; common::lookup_table::lookup_table_tx(svm, payer, ix) @@ -591,7 +591,7 @@ fn max_buffers_in_one_instruction() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &mints, }; let tx = common::lookup_table::lookup_table_tx(&mut svm, &payer, ix); diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index 5231ed19..3bc58b39 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -1,5 +1,6 @@ use cow_settlement_client::instructions::ReclaimBuffer; use cow_settlement_interface::Instruction; +use cow_settlement_interface::token_program::TokenProgram; use cow_settlement_interface::{ instruction::reclaim_buffer::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, pda::state::find_state_pda, SettlementError, @@ -15,7 +16,6 @@ use crate::common::benchmark::{send_transaction_metered, BenchLabel}; use crate::common::buffer::ensure_buffer_exists; use crate::common::{ assert_instruction_error, to_instruction_error, unique_pubkey, InitializedParams, - SPL_TOKEN_PROGRAM_ID, }; mod common; @@ -47,7 +47,7 @@ fn happy_path_reclaims_to_a_recipient_chosen_by_the_authority() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -95,7 +95,7 @@ fn happy_path_reclaims_empty_buffer_to_the_authority_itself() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -135,7 +135,7 @@ fn funded_buffer_is_skipped() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -181,7 +181,7 @@ fn reclaims_to_the_settlements_own_state_pda() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -230,7 +230,7 @@ fn reclaims_multiple_buffers_skipping_funded() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint_a, mint_b], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -267,7 +267,7 @@ fn rejects_the_same_buffer_twice_in_one_instruction() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint, mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -297,7 +297,7 @@ fn rejects_when_signer_is_not_the_configured_reclaim_authority() { program_id, reclaim_authority: impostor.pubkey(), reclaim_recipient: impostor.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &impostor, ix); @@ -329,7 +329,7 @@ fn rejects_when_the_reclaim_authority_does_not_sign() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &[mint], }); @@ -369,7 +369,7 @@ fn max_buffers_reclaim_via_lookup_table( state_pda, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken.address(), buffers: &buffers, }; common::lookup_table::lookup_table_tx(svm, reclaim_authority, ix) @@ -448,7 +448,7 @@ fn max_buffers_in_one_instruction() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), - token_program: SPL_TOKEN_PROGRAM_ID, + token_program: TokenProgram::SplToken, mints: &mints, }; let tx = common::lookup_table::lookup_table_tx(&mut svm, &reclaim_authority, ix); diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index e0a25f8f..9efb0b86 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -303,7 +303,7 @@ fn prepare_setup_ixs( program_id: ctx.program_id, payer: ctx.payer.pubkey(), // The CLI only resolves tokens on the legacy program for now. - token_program: TokenProgram::SplToken.address(), + token_program: TokenProgram::SplToken, mints: &mint_buffers_to_create.into_iter().collect::>(), } .into(), From ada2e5a886f3e17d472f995fa222c61a32865827 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:09:32 +0900 Subject: [PATCH 21/36] remove the check --- client/src/instructions.rs | 5 ++++- client/src/parse.rs | 8 ++++++-- programs/settlement/src/token.rs | 4 ---- programs/settlement/tests/create_buffer.rs | 9 ++++++--- programs/settlement/tests/reclaim_buffer.rs | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/client/src/instructions.rs b/client/src/instructions.rs index 7e0c40e2..bd8f951c 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -6,7 +6,10 @@ //! by the function, making it more suitable for off-chain use. use cow_settlement_interface::{ - Instruction, Pubkey, Role, data::intent::{EncodedOrderIntent, OrderIntent}, pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, token_program::TokenProgram, + data::intent::{EncodedOrderIntent, OrderIntent}, + pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, + token_program::TokenProgram, + Instruction, Pubkey, Role, }; // Reexport the instruction builders that don't change from the interface. diff --git a/client/src/parse.rs b/client/src/parse.rs index 45462d6b..6341077f 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -77,10 +77,14 @@ mod tests { InitializedIntent, }; use cow_settlement_interface::{ - Instruction, Role, data::intent::fixtures::sample_intent, fixtures::pubkey_from_seed, instruction::{ + data::intent::fixtures::sample_intent, + fixtures::pubkey_from_seed, + instruction::{ fixtures::fake_account_from_array, reclaim_buffer::ReclaimBuffer, reclaim_order::ReclaimOrder, transfer_authority::TransferAuthority, - }, token_program::TokenProgram, + }, + token_program::TokenProgram, + Instruction, Role, }; /// One buildable instruction per discriminator. The exhaustive match makes diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 545b8820..530856ab 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -40,10 +40,6 @@ pub fn token_account_len( .try_into() .map_err(|_| SettlementError::BufferSizeUnavailable)?; let length = u64::from_le_bytes(length); - // A token account is at least its base layout, whatever its mint carries. - if length < BASE_TOKEN_ACCOUNT_LEN { - return Err(SettlementError::BufferSizeUnavailable.into()); - } Ok(length) } diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index 0dcede6d..ee106367 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -55,7 +55,8 @@ fn happy_path_creates_initialized_buffer_token_account() { .get_account(&buffer_pda) .expect("buffer PDA should exist after create_buffer"); assert_eq!( - account.owner, TokenProgram::SplToken.address(), + account.owner, + TokenProgram::SplToken.address(), "buffer must be owned by the SPL Token program" ); assert_eq!( @@ -199,7 +200,8 @@ fn happy_path_creates_multiple_buffers_in_one_instruction() { .get_account(&buffer_pda) .expect("each buffer PDA should exist after create_buffers"); assert_eq!( - account.owner, TokenProgram::SplToken.address(), + account.owner, + TokenProgram::SplToken.address(), "each buffer must be owned by the SPL Token program" ); assert_eq!( @@ -304,7 +306,8 @@ fn rejects_non_spl_token_program() { .into(); let token_program_index = 2; assert_eq!( - ix.accounts[token_program_index].pubkey, TokenProgram::SplToken.address(), + ix.accounts[token_program_index].pubkey, + TokenProgram::SplToken.address(), "sanity: should replace token program" ); ix.accounts[token_program_index].pubkey = unique_pubkey(); diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index 3bc58b39..3267353c 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -1,6 +1,6 @@ use cow_settlement_client::instructions::ReclaimBuffer; -use cow_settlement_interface::Instruction; use cow_settlement_interface::token_program::TokenProgram; +use cow_settlement_interface::Instruction; use cow_settlement_interface::{ instruction::reclaim_buffer::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, pda::state::find_state_pda, SettlementError, From 11947620f30fbfc0a5014a1a4c0c1002fa666091 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:04:30 +0900 Subject: [PATCH 22/36] remove superfluous test --- programs/settlement/src/token.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 530856ab..b3042cc1 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -125,15 +125,6 @@ mod tests { } } - #[test] - fn token_account_len_is_the_base_layout_for_a_too_short_account() { - let mint = fake_account(pubkey_from_seed("mint")); - assert_eq!( - token_account_len(TokenProgram::SplToken, &mint), - Ok(BASE_TOKEN_ACCOUNT_LEN), - ); - } - #[test] fn token_account_len_reports_unavailable_without_an_answer() { let mint = fake_account_owned_by( From f74498bfb6a2d30773790942f79cab37ecef6773 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:08:18 +0900 Subject: [PATCH 23/36] refactor `base_layout` to use the actual encoder, and rename for clarity --- programs/settlement/src/token.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 20aa89b0..fff870d7 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -75,8 +75,10 @@ mod tests { fixtures::pubkey_from_seed, instruction::fixtures::{fake_account, fake_account_owned_by}, }; + use litesvm_token::spl_token::state::{Account as SplTokenAccount, AccountState}; use pinocchio::Address; use pinocchio_token_2022::state::AccountType; + use solana_program_pack::Pack; /// The length of a token account holding nothing but the base layout. Both /// programs share it: it is Token-2022's `BASE_LEN` and the whole of a @@ -84,12 +86,18 @@ mod tests { const BASE_LEN: usize = pinocchio_token::state::Account::LEN; /// The base layout of a token account holding `amount` of `mint` for - /// `owner`, with every other field left zeroed. - fn base_layout(mint: Address, owner: Address, amount: u64) -> Vec { + /// `owner`, encoded by the SPL token program's own packer so the fixture + /// cannot drift from the layout the readers parse. + fn base_account_layout(mint: Address, owner: Address, amount: u64) -> Vec { let mut data = vec![0u8; BASE_LEN]; - data[..32].copy_from_slice(mint.as_array()); - data[32..64].copy_from_slice(owner.as_array()); - data[64..72].copy_from_slice(&amount.to_le_bytes()); + SplTokenAccount { + mint, + owner, + amount, + state: AccountState::Initialized, + ..Default::default() + } + .pack_into_slice(&mut data); data } @@ -168,7 +176,7 @@ mod tests { let account = fake_account_owned_by( pubkey_from_seed("token account"), program.address(), - &base_layout(mint, owner, 4_200), + &base_account_layout(mint, owner, 4_200), ); let read = read_token_account(program, &account) .unwrap_or_else(|error| panic!("{program:?} account should read: {error:?}")); @@ -180,7 +188,7 @@ mod tests { fn read_token_account_reads_past_token_2022_extensions() { let mint = pubkey_from_seed("extended mint"); let owner = pubkey_from_seed("extended owner"); - let mut data = base_layout(mint, owner, 7); + let mut data = base_account_layout(mint, owner, 7); // Extensions are preceded by the account-type marker, which is what // distinguishes a longer account from a mint of the same size. data.push(AccountType::Account as u8); @@ -198,7 +206,7 @@ mod tests { #[test] fn read_token_account_rejects_an_extended_mint() { - let mut data = base_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 7); + let mut data = base_account_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 7); data.push(AccountType::Mint as u8); let account = fake_account_owned_by( @@ -221,7 +229,7 @@ mod tests { let account = fake_account_owned_by( pubkey_from_seed("token account"), other.address(), - &base_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 0), + &base_account_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 0), ); assert_eq!( read_token_account(program, &account).err(), From 2907aaa50253348a156167a6224bd1be37b75c41 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:14:35 +0900 Subject: [PATCH 24/36] update test and spl token mint behavior --- programs/settlement/src/token.rs | 36 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index fff870d7..5f284c77 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -1,8 +1,11 @@ //! Token-program validation and token-account reads -use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; +use cow_settlement_interface::{ + token_program::TokenProgram::{self, SplToken}, + SettlementError, +}; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; -use pinocchio_token::{instructions::GetAccountDataSize, state::Mint}; +use pinocchio_token::instructions::GetAccountDataSize; /// The length of a SPL token program account. Token2022 extensions may make /// the actual token account longer than this. @@ -22,8 +25,8 @@ pub fn token_account_len( token_program: TokenProgram, mint: &AccountView, ) -> Result { - // If the mint is of base SPL Mint length, the token accounts must be of base length accordingly. - if mint.data_len() <= Mint::LEN { + // Early return for SPL token (saves the GetAccountDataSize CPI call) + if token_program == SplToken { return Ok(BASE_TOKEN_ACCOUNT_LEN); } @@ -77,6 +80,7 @@ mod tests { }; use litesvm_token::spl_token::state::{Account as SplTokenAccount, AccountState}; use pinocchio::Address; + use pinocchio_token::state::Mint; use pinocchio_token_2022::state::AccountType; use solana_program_pack::Pack; @@ -123,19 +127,17 @@ mod tests { } #[test] - fn token_account_len_is_the_base_layout_for_a_plain_mint() { - for program in TokenProgram::ALL { - let mint = fake_account_owned_by( - pubkey_from_seed("mint"), - program.address(), - &[0u8; Mint::LEN], - ); - assert_eq!( - token_account_len(program, &mint), - Ok(BASE_TOKEN_ACCOUNT_LEN), - "a base-layout mint should need a base-layout account under {program:?}", - ); - } + fn token_account_len_is_base_length_for_spl_program() { + let mint = fake_account_owned_by( + pubkey_from_seed("mint"), + SplToken.address(), + &[0u8; Mint::LEN], + ); + assert_eq!( + token_account_len(SplToken, &mint), + Ok(BASE_TOKEN_ACCOUNT_LEN), + "SPL owned mint should yield ase length", + ); } #[test] From 62776ff499ec4a89717c9552053509f8b74f6fa0 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:09:57 +0900 Subject: [PATCH 25/36] add some tests for wierd edge cases with recreating mints --- Cargo.lock | 1 + programs/settlement/Cargo.toml | 1 + programs/settlement/tests/common/buffer.rs | 14 +- programs/settlement/tests/common/mod.rs | 1 + programs/settlement/tests/common/token.rs | 10 +- .../settlement/tests/common/token_2022.rs | 187 ++++++++++++++++++ programs/settlement/tests/create_buffer.rs | 44 +++++ programs/settlement/tests/reclaim_buffer.rs | 136 +++++++++++++ 8 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 programs/settlement/tests/common/token_2022.rs diff --git a/Cargo.lock b/Cargo.lock index b4c55403..74280091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3341,6 +3341,7 @@ dependencies = [ "solana-sdk", "solana-sha256-hasher", "solana-system-interface 3.2.0", + "spl-token-2022-interface 3.1.1", "syn 2.0.119", ] diff --git a/programs/settlement/Cargo.toml b/programs/settlement/Cargo.toml index 00a3ccda..9f9e01d5 100644 --- a/programs/settlement/Cargo.toml +++ b/programs/settlement/Cargo.toml @@ -42,6 +42,7 @@ solana-program-pack.workspace = true solana-sdk.workspace = true solana-sha256-hasher.workspace = true solana-system-interface.workspace = true +spl-token-2022-interface.workspace = true syn.workspace = true [lints] diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index 07b629d3..b8f90700 100644 --- a/programs/settlement/tests/common/buffer.rs +++ b/programs/settlement/tests/common/buffer.rs @@ -26,6 +26,18 @@ pub fn ensure_buffer_exists( program_id: &Pubkey, payer: &Keypair, mint: &Pubkey, +) -> Pubkey { + ensure_buffer_exists_for(svm, program_id, payer, mint, TokenProgram::SplToken) +} + +/// [`ensure_buffer_exists`] under a token program of the caller's choosing, for +/// the tests that need a buffer belonging to Token-2022. +pub fn ensure_buffer_exists_for( + svm: &mut LiteSVM, + program_id: &Pubkey, + payer: &Keypair, + mint: &Pubkey, + token_program: TokenProgram, ) -> Pubkey { let pda = buffer_pda(program_id, mint); if svm.get_account(&pda).is_some() { @@ -34,7 +46,7 @@ pub fn ensure_buffer_exists( let ix = Instruction::from(CreateBuffers { program_id: *program_id, payer: payer.pubkey(), - token_program: TokenProgram::SplToken, + token_program, mints: &[*mint], }); let tx = Transaction::new_signed_with_payer( diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index be3d52a3..995cc7ba 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -13,6 +13,7 @@ pub mod pda; pub mod settlement; pub mod state; pub mod token; +pub mod token_2022; use cow_settlement_client::instructions::{AddSolver, Initialize}; use cow_settlement_interface::pda::state::find_state_pda; diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 6d505397..709b229e 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -24,10 +24,16 @@ use super::unique_keypair; /// buffer PDA, so a random one makes buffer bumps — and the compute cost of /// deriving them — vary between runs. See [`super::unique_pubkey`]. pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { + create_mint_at(svm, payer, &unique_keypair()) +} + +/// [`create_mint`] at `mint`'s address rather than a fresh one. Lets a test +/// reclaim an address a Token-2022 mint was just closed at, which is the only +/// way a legacy mint can end up where a Token-2022 one used to be. +pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pubkey { /// `litesvm_token::CreateMint`'s default, kept so the two agree. const DECIMALS: u8 = 8; - let mint = unique_keypair(); let create = system_create_account( &payer.pubkey(), &mint.pubkey(), @@ -40,7 +46,7 @@ pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { let tx = Transaction::new_signed_with_payer( &[create, initialize], Some(&payer.pubkey()), - &[payer, &mint], + &[payer, mint], svm.latest_blockhash(), ); svm.send_transaction(tx) diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs new file mode 100644 index 00000000..b13e52bd --- /dev/null +++ b/programs/settlement/tests/common/token_2022.rs @@ -0,0 +1,187 @@ +//! Token-2022 helpers for the settlement integration tests. +//! +//! Unlike the legacy program, Token-2022 lets a mint be closed and its address +//! reused for something else entirely. A buffer PDA is derived from the mint +//! address alone, so a buffer outlives the mint it was created for. These +//! helpers drive that lifecycle: create a mint under a chosen extension set, +//! close it, and put a different mint at the same address. + +use cow_settlement_interface::token_program::TokenProgram; +use litesvm::LiteSVM; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_system_interface::instruction::create_account as system_create_account; +use spl_token_2022_interface::{ + extension::{transfer_fee::instruction::initialize_transfer_fee_config, ExtensionType}, + instruction::{ + close_account, initialize_mint2, initialize_mint_close_authority, + initialize_non_transferable_mint, + }, + state::{Account, Mint}, +}; + +/// The Token-2022 program, the counterpart of [`super::SPL_TOKEN_PROGRAM_ID`]. +const TOKEN_2022_PROGRAM_ID: Pubkey = TokenProgram::Token2022.address(); + +/// Decimals every test mint carries, matching [`super::token::create_mint`] so +/// a legacy and a Token-2022 mint differ only in their program. +const DECIMALS: u8 = 8; + +/// Transfer-fee parameters for [`Extensions::WithTransferFee`]. Arbitrary; +/// nothing reads them back, but `InitializeTransferFeeConfig` demands values. +const FEE_BASIS_POINTS: u16 = 50; +const MAXIMUM_FEE: u64 = 1_000; + +/// The extension set a test mint is created with. +#[derive(Clone, Copy, Debug)] +pub enum Extensions { + CloseAuthorityOnly, + /// Non-transferable + CloseAuthority + WithNonTransferable, + /// Transfer Fee + CloseAuthority + WithTransferFee, +} + +impl Extensions { + /// The extensions the mint itself is initialized with. + fn mint(self) -> &'static [ExtensionType] { + match self { + Self::CloseAuthorityOnly => &[ExtensionType::MintCloseAuthority], + Self::WithNonTransferable => &[ + ExtensionType::MintCloseAuthority, + ExtensionType::NonTransferable, + ], + Self::WithTransferFee => &[ + ExtensionType::MintCloseAuthority, + ExtensionType::TransferFeeConfig, + ], + } + } + + /// The extensions Token-2022 requires of a token account holding the mint. + /// Spelled out rather than derived from [`Self::mint`], so the length a test + /// expects is stated independently of the program's own bookkeeping. + fn token_account(self) -> &'static [ExtensionType] { + match self { + Self::CloseAuthorityOnly => &[], + Self::WithNonTransferable => &[ + ExtensionType::NonTransferableAccount, + ExtensionType::ImmutableOwner, + ], + Self::WithTransferFee => &[ExtensionType::TransferFeeAmount], + } + } + + /// The data length a token account holding the mint has to be allocated at, + /// which is what `create_buffer` asks the token program for. + pub fn token_account_len(self) -> usize { + ExtensionType::try_calculate_account_len::(self.token_account()) + .expect("every account extension used here has a fixed length") + } + + /// The instructions initializing the extensions on `mint`, with `authority` + /// filling every authority they ask for. Token-2022 requires all of them to + /// run before `InitializeMint`, and insists the mint be allocated at exactly + /// the length they need. + fn initializers(self, mint: &Pubkey, authority: &Pubkey) -> Vec { + self.mint() + .iter() + .map(|extension| { + match extension { + ExtensionType::MintCloseAuthority => initialize_mint_close_authority( + &TOKEN_2022_PROGRAM_ID, + mint, + Some(authority), + ), + ExtensionType::NonTransferable => { + initialize_non_transferable_mint(&TOKEN_2022_PROGRAM_ID, mint) + } + ExtensionType::TransferFeeConfig => initialize_transfer_fee_config( + &TOKEN_2022_PROGRAM_ID, + mint, + Some(authority), + Some(authority), + FEE_BASIS_POINTS, + MAXIMUM_FEE, + ), + other => panic!("no initializer is wired up for {other:?}"), + } + .expect("extension initializer should build") + }) + .collect() + } +} + +/// Create a Token-2022 mint at `mint`'s address carrying `extensions`, with +/// `payer` as both its mint authority and its close authority, and return the +/// address. Taking the keypair rather than generating one lets a test close the +/// mint and put something else back at the same address. +pub fn create_mint( + svm: &mut LiteSVM, + payer: &Keypair, + mint: &Keypair, + extensions: Extensions, +) -> Pubkey { + let space = ExtensionType::try_calculate_account_len::(extensions.mint()) + .expect("every mint extension used here has a fixed length"); + let mut instructions = vec![system_create_account( + &payer.pubkey(), + &mint.pubkey(), + svm.minimum_balance_for_rent_exemption(space), + space as u64, + &TOKEN_2022_PROGRAM_ID, + )]; + instructions.extend(extensions.initializers(&mint.pubkey(), &payer.pubkey())); + instructions.push( + initialize_mint2( + &TOKEN_2022_PROGRAM_ID, + &mint.pubkey(), + &payer.pubkey(), + None, + DECIMALS, + ) + .expect("initialize_mint2 should build"), + ); + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, mint], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .expect("Token-2022 mint creation should succeed"); + mint.pubkey() +} + +/// Close `mint`, whose close authority must be `payer`, refunding its rent to +/// `payer`. Token-2022 hands the emptied account back to the System program, so +/// the address is free for [`create_mint`] or [`super::token::create_mint_at`] +/// to claim again. +pub fn close_mint(svm: &mut LiteSVM, payer: &Keypair, mint: &Pubkey) { + let ix = close_account( + &TOKEN_2022_PROGRAM_ID, + mint, + &payer.pubkey(), + &payer.pubkey(), + &[], + ) + .expect("close_account should build"); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .expect("closing the mint should succeed"); + assert!( + svm.get_account(mint) + .is_none_or(|account| account.data.is_empty()), + "a closed mint must leave no data behind at its address", + ); +} diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index ee106367..0c7546e0 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -29,6 +29,7 @@ use solana_sdk::{ use crate::common::{ benchmark::{send_transaction_metered, BenchLabel}, + token_2022::Extensions, unique_keypair, unique_pubkey, }; @@ -513,6 +514,49 @@ fn same_mint_twice_in_one_instruction_is_idempotent() { ); } +#[test] +fn sizes_a_token_2022_buffer_to_the_extensions_its_mint_forces() { + let (mut svm, program_id, payer) = common::setup(); + + for extensions in [ + Extensions::CloseAuthorityOnly, + Extensions::WithNonTransferable, + Extensions::WithTransferFee, + ] { + let mint = common::token_2022::create_mint(&mut svm, &payer, &unique_keypair(), extensions); + let (buffer_pda, _bump) = find_buffer_pda(&program_id, &mint); + + let ix = CreateBuffers { + program_id, + payer: payer.pubkey(), + token_program: TokenProgram::Token2022, + mints: &[mint], + }; + let tx = common::signed_tx(&svm, &payer, &payer, ix); + svm.send_transaction(tx).unwrap_or_else(|err| { + panic!( + "create_buffer should succeed for {extensions:?}: {:?}", + err.err + ) + }); + + let account = svm + .get_account(&buffer_pda) + .expect("buffer PDA should exist after create_buffer"); + assert_eq!( + account.owner, + TokenProgram::Token2022.address(), + "a {extensions:?} buffer must be owned by the Token-2022 program" + ); + assert_eq!( + account.data.len(), + extensions.token_account_len(), + "a {extensions:?} buffer must be allocated at the length its extensions imply" + ); + common::assert_rent_exempt(&svm, &account); + } +} + /// Largest number of buffers a single ALT-backed `create_buffers` transaction /// can carry, bounded by the transaction account-lock limit (litesvm and current /// mainnet both cap this at 64). diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index 3267353c..13c5413e 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -14,6 +14,7 @@ use solana_sdk::{ use crate::common::benchmark::{send_transaction_metered, BenchLabel}; use crate::common::buffer::ensure_buffer_exists; +use crate::common::token_2022::Extensions; use crate::common::{ assert_instruction_error, to_instruction_error, unique_pubkey, InitializedParams, }; @@ -355,6 +356,141 @@ fn rejects_when_the_reclaim_authority_does_not_sign() { ); } +/// Set up a Token-2022 mint with a close authority and its buffer, then close +/// the mint so `reopen` can claim the address. Returns the mint and its buffer. +fn buffer_whose_mint_was_reopened( + svm: &mut LiteSVM, + program_id: &Pubkey, + payer: &Keypair, + reopen: impl FnOnce(&mut LiteSVM, &Keypair, &Keypair), +) -> (Pubkey, Pubkey) { + let mint_keypair = common::unique_keypair(); + let mint = + common::token_2022::create_mint(svm, payer, &mint_keypair, Extensions::CloseAuthorityOnly); + let buffer_pda = common::buffer::ensure_buffer_exists_for( + svm, + program_id, + payer, + &mint, + TokenProgram::Token2022, + ); + + common::token_2022::close_mint(svm, payer, &mint); + reopen(svm, payer, &mint_keypair); + + (mint, buffer_pda) +} + +#[test] +fn reclaims_a_buffer_whose_mint_was_reopened_with_another_extension() { + let ( + mut svm, + InitializedParams { + program_id, + payer, + reclaim: reclaim_authority, + .. + }, + ) = common::setup_init(); + + let (mint, buffer_pda) = buffer_whose_mint_was_reopened( + &mut svm, + &program_id, + &payer, + |svm, payer, mint_keypair| { + common::token_2022::create_mint( + svm, + payer, + mint_keypair, + Extensions::WithNonTransferable, + ); + }, + ); + + let buffer_lamports_before = svm + .get_account(&buffer_pda) + .expect("buffer must exist before reclaim") + .lamports; + let recipient_before = common::lamports(&svm, &reclaim_authority.pubkey()); + + let ix = ReclaimBuffer { + program_id, + reclaim_authority: reclaim_authority.pubkey(), + reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::Token2022, + mints: &[mint], + }; + let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); + svm.send_transaction(tx) + .expect("reclaim_buffer should succeed for a reopened mint"); + + assert!( + svm.get_account(&buffer_pda).is_none(), + "buffer PDA must be closed after reclaim" + ); + assert_eq!( + common::lamports(&svm, &reclaim_authority.pubkey()) - recipient_before, + buffer_lamports_before, + "the rent of a buffer stranded by a reopened mint must still be recoverable" + ); +} + +#[test] +fn reclaims_a_buffer_whose_mint_was_reopened_as_a_legacy_mint() { + let ( + mut svm, + InitializedParams { + program_id, + payer, + reclaim: reclaim_authority, + .. + }, + ) = common::setup_init(); + + let (mint, buffer_pda) = buffer_whose_mint_was_reopened( + &mut svm, + &program_id, + &payer, + |svm, payer, mint_keypair| { + common::token::create_mint_at(svm, payer, mint_keypair); + }, + ); + assert_eq!( + svm.get_account(&mint) + .expect("the reopened mint should exist") + .owner, + common::SPL_TOKEN_PROGRAM_ID, + "sanity: the mint must now belong to the legacy program" + ); + + let buffer_lamports_before = svm + .get_account(&buffer_pda) + .expect("buffer must exist before reclaim") + .lamports; + let recipient_before = common::lamports(&svm, &reclaim_authority.pubkey()); + + let ix = ReclaimBuffer { + program_id, + reclaim_authority: reclaim_authority.pubkey(), + reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::Token2022, + mints: &[mint], + }; + let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); + svm.send_transaction(tx) + .expect("reclaim_buffer should succeed for a mint reopened as legacy"); + + assert!( + svm.get_account(&buffer_pda).is_none(), + "buffer PDA must be closed after reclaim" + ); + assert_eq!( + common::lamports(&svm, &reclaim_authority.pubkey()) - recipient_before, + buffer_lamports_before, + "the rent of a buffer whose mint turned legacy must still be recoverable" + ); +} + fn max_buffers_reclaim_via_lookup_table( svm: &mut LiteSVM, program_id: &Pubkey, From 00312dff7ab880a8be4f126629c3f9e1b47ae72f Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:19:23 +0900 Subject: [PATCH 26/36] fix idl --- programs/settlement/idl/cow_settlement.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json index ab5fce55..48c19b6c 100644 --- a/programs/settlement/idl/cow_settlement.json +++ b/programs/settlement/idl/cow_settlement.json @@ -1049,7 +1049,7 @@ "msg": "A created order's intent isn't set with the created_on_chain flag corresponding to the behavior of the invoked order creation instruction." }, { - "code": 39, + "code": 40, "name": "BufferSizeUnavailable", "msg": "CreateBuffer asked the token program how long a token account for a mint has to be and couldn't read the answer, so it can't size the buffer." } From 3c8a10c0b07d4429817afd02d2d9d892490e1fdd Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:00:31 +0900 Subject: [PATCH 27/36] Update programs/settlement/tests/common/token_2022.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/common/token_2022.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index b13e52bd..57065234 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -40,10 +40,8 @@ const MAXIMUM_FEE: u64 = 1_000; #[derive(Clone, Copy, Debug)] pub enum Extensions { CloseAuthorityOnly, - /// Non-transferable + CloseAuthority - WithNonTransferable, - /// Transfer Fee + CloseAuthority - WithTransferFee, + CloseAuthorityAndNonTransferable, + CloseAuthorityAndTransferFee, } impl Extensions { From 06e41e1d26e764a97d5b8fb479013aa1a9484b3e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:18:16 +0900 Subject: [PATCH 28/36] Update programs/settlement/src/token.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/src/token.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 5f284c77..4990c12c 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -122,7 +122,7 @@ mod tests { /// The base layout is the same under both programs, so one reader's idea of /// its length is the other's too. #[test] - fn both_programs_share_the_base_layout_length() { + fn sanity_check_both_programs_share_the_base_layout_length() { assert_eq!(BASE_LEN, pinocchio_token_2022::state::Account::BASE_LEN); } From d8c0815db20b3e4b5ecba1f7d5906a1efffa910b Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:25:24 +0900 Subject: [PATCH 29/36] Update programs/settlement/src/token.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/src/token.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 4990c12c..aa71000b 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -1,7 +1,7 @@ //! Token-program validation and token-account reads use cow_settlement_interface::{ - token_program::TokenProgram::{self, SplToken}, + token_program::TokenProgram, SettlementError, }; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; From b392dbba7f4c14864d0a049bdeb3c944dd9afa18 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:28:15 +0900 Subject: [PATCH 30/36] Update programs/settlement/src/token.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/src/token.rs | 41 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index aa71000b..c3eb44ed 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -25,26 +25,29 @@ pub fn token_account_len( token_program: TokenProgram, mint: &AccountView, ) -> Result { - // Early return for SPL token (saves the GetAccountDataSize CPI call) - if token_program == SplToken { - return Ok(BASE_TOKEN_ACCOUNT_LEN); - } - - let token_program = token_program.address(); - // SPL token provides a function to get the actual required account data size - GetAccountDataSize::new(mint).invoke_with_unverified_program(&token_program)?; - - let reported = get_return_data().ok_or(SettlementError::BufferSizeUnavailable)?; - if reported.program_id() != &token_program { - return Err(SettlementError::BufferSizeUnavailable.into()); + match token_program { + // SPL token accounts are always the base length, so skip the CPI. + TokenProgram::SplToken => Ok(BASE_TOKEN_ACCOUNT_LEN), + // Token-2022 accounts vary with the mint's extensions. This mirrors the + // SPL Associated Token Account program's `get_account_len`: + // https://github.com/solana-program/associated-token-account/blob/2dc55ee1009d787eea7e1c401b8f27e6892bff4b/program/src/tools/account.rs#L72-L97 + TokenProgram::Token2022 => { + GetAccountDataSize::new(mint) + .invoke_with_unverified_program(&TokenProgram::Token2022.address())?; + get_return_data() + .ok_or(SettlementError::BufferSizeUnavailable.into()) + .and_then(|reported| { + if reported.program_id() != &TokenProgram::Token2022.address() { + return Err(SettlementError::BufferSizeUnavailable.into()); + } + reported + .as_slice() + .try_into() + .map(u64::from_le_bytes) + .map_err(|_| SettlementError::BufferSizeUnavailable.into()) + }) + } } - let length: [u8; 8] = reported - .as_slice() - .try_into() - .map_err(|_| SettlementError::BufferSizeUnavailable)?; - let length = u64::from_le_bytes(length); - - Ok(length) } /// The base-layout fields of a token account, as read by From 479771fed42bb35cb3cd5b23663932679be4b890 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:43:43 +0900 Subject: [PATCH 31/36] remove unneeded function --- programs/settlement/tests/common/token.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 709b229e..d6bad1ba 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -54,24 +54,6 @@ pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pub mint.pubkey() } -/// Seed the wrapped-SOL mint account, which `LiteSVM` does not create. -pub fn create_native_mint(svm: &mut LiteSVM) { - /// The native mint's fixed decimals, matching `spl_token::native_mint`. - const DECIMALS: u8 = 9; - - let mut data = vec![0u8; Mint::LEN]; - Mint { - mint_authority: None.into(), - supply: 0, - decimals: DECIMALS, - is_initialized: true, - freeze_authority: None.into(), - } - .pack_into_slice(&mut data); - let token_program = Pubkey::new_from_array(TOKEN_ID.to_bytes()); - super::create_account_at(svm, native_mint::ID, &token_program, &data); -} - /// Create an initialized SPL token account for `mint` whose SPL owner is /// `owner`, funded by `payer`, and return its address. Each call produces a /// fresh account, so the same `owner` can hold several accounts for one `mint`. From a40dcf1babc743984f19000716dab78b67b03f34 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:57:58 +0900 Subject: [PATCH 32/36] switch to using the account state builder provided by spl_token_2022_interface --- programs/settlement/src/token.rs | 55 ++++++++++++++----- .../settlement/tests/common/token_2022.rs | 10 ++-- programs/settlement/tests/create_buffer.rs | 4 +- programs/settlement/tests/reclaim_buffer.rs | 2 +- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index c3eb44ed..efe82e07 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -1,9 +1,6 @@ //! Token-program validation and token-account reads -use cow_settlement_interface::{ - token_program::TokenProgram, - SettlementError, -}; +use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; use pinocchio_token::instructions::GetAccountDataSize; @@ -86,6 +83,13 @@ mod tests { use pinocchio_token::state::Mint; use pinocchio_token_2022::state::AccountType; use solana_program_pack::Pack; + use spl_token_2022_interface::{ + extension::{ + transfer_fee::TransferFeeAmount, BaseStateWithExtensionsMut, ExtensionType, + StateWithExtensionsMut, + }, + state::{Account as Token2022TokenAccount, AccountState as Token2022AccountState}, + }; /// The length of a token account holding nothing but the base layout. Both /// programs share it: it is Token-2022's `BASE_LEN` and the whole of a @@ -108,6 +112,37 @@ mod tests { data } + /// A Token-2022 account holding `amount` of `mint` for `owner`, extended + /// with the `TransferFeeAmount` extension. Built through the token program's own TLV + /// writers. + fn extended_token_2022_account_layout(mint: Address, owner: Address, amount: u64) -> Vec { + let len = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::TransferFeeAmount, + ]) + .expect("TransferFeeAmount has a fixed length"); + let mut data = vec![0u8; len]; + + let mut state = + StateWithExtensionsMut::::unpack_uninitialized(&mut data) + .expect("a zeroed buffer of the right length is an uninitialized account"); + state + .init_extension::(true) + .expect("the buffer is sized for the extension"); + state.base = Token2022TokenAccount { + mint, + owner, + amount, + state: Token2022AccountState::Initialized, + ..Default::default() + }; + state.pack_base(); + state + .init_account_type() + .expect("the extension belongs to a token account"); + + data + } + /// The addresses the off-chain crate offers are the ones the on-chain token /// crates CPI into. Both sides name the same programs from their own /// dependency, so this is what keeps them from drifting apart. @@ -133,11 +168,11 @@ mod tests { fn token_account_len_is_base_length_for_spl_program() { let mint = fake_account_owned_by( pubkey_from_seed("mint"), - SplToken.address(), + TokenProgram::SplToken.address(), &[0u8; Mint::LEN], ); assert_eq!( - token_account_len(SplToken, &mint), + token_account_len(TokenProgram::SplToken, &mint), Ok(BASE_TOKEN_ACCOUNT_LEN), "SPL owned mint should yield ase length", ); @@ -193,16 +228,10 @@ mod tests { fn read_token_account_reads_past_token_2022_extensions() { let mint = pubkey_from_seed("extended mint"); let owner = pubkey_from_seed("extended owner"); - let mut data = base_account_layout(mint, owner, 7); - // Extensions are preceded by the account-type marker, which is what - // distinguishes a longer account from a mint of the same size. - data.push(AccountType::Account as u8); - data.extend_from_slice(&[0xab; 16]); - let account = fake_account_owned_by( pubkey_from_seed("token account"), TokenProgram::Token2022.address(), - &data, + &extended_token_2022_account_layout(mint, owner, 7), ); let read = read_token_account(TokenProgram::Token2022, &account) .expect("an extended Token-2022 account should read"); diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index 57065234..23c2ef3f 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -31,7 +31,7 @@ const TOKEN_2022_PROGRAM_ID: Pubkey = TokenProgram::Token2022.address(); /// a legacy and a Token-2022 mint differ only in their program. const DECIMALS: u8 = 8; -/// Transfer-fee parameters for [`Extensions::WithTransferFee`]. Arbitrary; +/// Transfer-fee parameters for [`Extensions::CloseAuthorityAndTransferFee`]. Arbitrary; /// nothing reads them back, but `InitializeTransferFeeConfig` demands values. const FEE_BASIS_POINTS: u16 = 50; const MAXIMUM_FEE: u64 = 1_000; @@ -49,11 +49,11 @@ impl Extensions { fn mint(self) -> &'static [ExtensionType] { match self { Self::CloseAuthorityOnly => &[ExtensionType::MintCloseAuthority], - Self::WithNonTransferable => &[ + Self::CloseAuthorityAndNonTransferable => &[ ExtensionType::MintCloseAuthority, ExtensionType::NonTransferable, ], - Self::WithTransferFee => &[ + Self::CloseAuthorityAndTransferFee => &[ ExtensionType::MintCloseAuthority, ExtensionType::TransferFeeConfig, ], @@ -66,11 +66,11 @@ impl Extensions { fn token_account(self) -> &'static [ExtensionType] { match self { Self::CloseAuthorityOnly => &[], - Self::WithNonTransferable => &[ + Self::CloseAuthorityAndNonTransferable => &[ ExtensionType::NonTransferableAccount, ExtensionType::ImmutableOwner, ], - Self::WithTransferFee => &[ExtensionType::TransferFeeAmount], + Self::CloseAuthorityAndTransferFee => &[ExtensionType::TransferFeeAmount], } } diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index 0c7546e0..919a6d8c 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -520,8 +520,8 @@ fn sizes_a_token_2022_buffer_to_the_extensions_its_mint_forces() { for extensions in [ Extensions::CloseAuthorityOnly, - Extensions::WithNonTransferable, - Extensions::WithTransferFee, + Extensions::CloseAuthorityAndNonTransferable, + Extensions::CloseAuthorityAndTransferFee, ] { let mint = common::token_2022::create_mint(&mut svm, &payer, &unique_keypair(), extensions); let (buffer_pda, _bump) = find_buffer_pda(&program_id, &mint); diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index 13c5413e..fd4a51b5 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -402,7 +402,7 @@ fn reclaims_a_buffer_whose_mint_was_reopened_with_another_extension() { svm, payer, mint_keypair, - Extensions::WithNonTransferable, + Extensions::CloseAuthorityAndNonTransferable, ); }, ); From 2de525fc133e1762cfcd67ce25d1e525f00645b5 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:28:26 +0900 Subject: [PATCH 33/36] fix lint and test confirm --- programs/settlement/tests/common/token.rs | 2 +- programs/settlement/tests/common/token_2022.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index d6bad1ba..a71dd3ef 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -3,7 +3,7 @@ use cow_settlement_client::cow_settlement_interface::pda::state::find_state_pda; use litesvm::{types::TransactionMetadata, LiteSVM}; use litesvm_token::{ - spl_token::{instruction::initialize_mint2, native_mint, state::Mint}, + spl_token::{instruction::initialize_mint2, state::Mint}, Approve, CreateAccount, CreateAssociatedTokenAccount, MintTo, Transfer, TOKEN_ID, }; use solana_program_pack::Pack; diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index 23c2ef3f..f2654b5c 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -36,18 +36,20 @@ const DECIMALS: u8 = 8; const FEE_BASIS_POINTS: u16 = 50; const MAXIMUM_FEE: u64 = 1_000; -/// The extension set a test mint is created with. +/// Defines a set of token account/mint configurations we are interested in testing #[derive(Clone, Copy, Debug)] pub enum Extensions { + None, CloseAuthorityOnly, CloseAuthorityAndNonTransferable, CloseAuthorityAndTransferFee, } impl Extensions { - /// The extensions the mint itself is initialized with. + /// The extensions which should be configured on the mint fn mint(self) -> &'static [ExtensionType] { match self { + Self::None => &[], Self::CloseAuthorityOnly => &[ExtensionType::MintCloseAuthority], Self::CloseAuthorityAndNonTransferable => &[ ExtensionType::MintCloseAuthority, @@ -60,11 +62,12 @@ impl Extensions { } } - /// The extensions Token-2022 requires of a token account holding the mint. - /// Spelled out rather than derived from [`Self::mint`], so the length a test - /// expects is stated independently of the program's own bookkeeping. + /// The extensions which should be configured on the token account + /// Since we are currently only interested in testing the accounts required + /// by the mint, we only use fn token_account(self) -> &'static [ExtensionType] { match self { + Self::None => &[], Self::CloseAuthorityOnly => &[], Self::CloseAuthorityAndNonTransferable => &[ ExtensionType::NonTransferableAccount, From ac77442b597ea077cd8d955ba985f5815012c969 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:00:14 +0900 Subject: [PATCH 34/36] finally --- .../settlement/tests/common/token_2022.rs | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index f2654b5c..284b5e06 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -45,6 +45,25 @@ pub enum Extensions { CloseAuthorityAndTransferFee, } +pub struct RequiredInitAccountExtensionType(ExtensionType); + +impl RequiredInitAccountExtensionType { + /// Copied from the unnecessarily private function in spl_token_2022_interface + /// https://docs.rs/spl-token-2022-interface/latest/src/spl_token_2022_interface/extension/mod.rs.html#1296 + pub fn required_init_account_extensions(&self) -> &'static [ExtensionType] { + match self.0 { + ExtensionType::TransferFeeConfig => &[ExtensionType::TransferFeeAmount], + ExtensionType::NonTransferable => &[ + ExtensionType::NonTransferableAccount, + ExtensionType::ImmutableOwner, + ], + ExtensionType::TransferHook => &[ExtensionType::TransferHookAccount], + ExtensionType::Pausable => &[ExtensionType::PausableAccount], + _ => &[], + } + } +} + impl Extensions { /// The extensions which should be configured on the mint fn mint(self) -> &'static [ExtensionType] { @@ -63,24 +82,33 @@ impl Extensions { } /// The extensions which should be configured on the token account - /// Since we are currently only interested in testing the accounts required - /// by the mint, we only use - fn token_account(self) -> &'static [ExtensionType] { - match self { - Self::None => &[], - Self::CloseAuthorityOnly => &[], - Self::CloseAuthorityAndNonTransferable => &[ - ExtensionType::NonTransferableAccount, - ExtensionType::ImmutableOwner, - ], - Self::CloseAuthorityAndTransferFee => &[ExtensionType::TransferFeeAmount], + /// Includes the required mint accounts by default, plus any additionally + /// specified optional token account extensions + fn token_account(self) -> Vec { + let mut extensions = vec![]; + + // required extensions by mint + for mint_extension in self.mint() { + extensions.extend_from_slice( + RequiredInitAccountExtensionType(*mint_extension) + .required_init_account_extensions(), + ); } + + // additional extensions for this configuration + // more will be added in the future + #[allow(clippy::match_single_binding)] + extensions.extend_from_slice(match self { + _ => &[], + }); + + extensions } /// The data length a token account holding the mint has to be allocated at, /// which is what `create_buffer` asks the token program for. pub fn token_account_len(self) -> usize { - ExtensionType::try_calculate_account_len::(self.token_account()) + ExtensionType::try_calculate_account_len::(&self.token_account()) .expect("every account extension used here has a fixed length") } From d438a63f1cc2cea7a5327f58a246a2d22b5e5799 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:24:42 +0900 Subject: [PATCH 35/36] fix bench --- bench-report.json | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/bench-report.json b/bench-report.json index 192c1445..e0a0e2a6 100644 --- a/bench-report.json +++ b/bench-report.json @@ -3,14 +3,21 @@ "add_solver/add_with_many_existing_solvers": 5, "add_solver/adds_a_solver": 5, "create_buffers/happy_path_creates_initialized_buffer_token_account": 6, + "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 6, "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 10, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 10, "create_buffers/max_buffers_in_one_instruction": 64, + "create_buffers/max_buffers_in_one_instruction_token_2022": 46, "create_order/happy_path_creates_order_pda_with_expected_body": 4, "initialize/happy_path_initializes_state_pda_with_expected_data": 4, "reclaim_buffer/funded_buffer_is_skipped": 7, + "reclaim_buffer/funded_buffer_is_skipped_token_2022": 7, "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 7, "reclaim_buffer/max_buffers_in_one_instruction": 64, + "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 64, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 9, "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 4, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 3, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 3, @@ -34,20 +41,29 @@ "compute_units": { "add_solver/add_with_many_existing_solvers": 5074, "add_solver/adds_a_solver": 4622, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 7376, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17290, - "create_buffers/max_buffers_in_one_instruction": 169803, - "create_order/happy_path_creates_order_pda_with_expected_body": 4985, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4529, - "reclaim_buffer/funded_buffer_is_skipped": 4859, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 6009, - "reclaim_buffer/max_buffers_in_one_instruction": 124823, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7611, - "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2202, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 7373, + "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 12188, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17283, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 31720, + "create_buffers/max_buffers_in_one_instruction": 169742, + "create_buffers/max_buffers_in_one_instruction_token_2022": 214956, + "create_order/happy_path_creates_order_pda_with_expected_body": 4986, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4530, + "reclaim_buffer/funded_buffer_is_skipped": 4860, + "reclaim_buffer/funded_buffer_is_skipped_token_2022": 4868, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 6010, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 7499, + "reclaim_buffer/max_buffers_in_one_instruction": 124824, + "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 169378, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7612, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 9105, + "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2203, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2072, - "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2079, + "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2080, "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": null, "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, + "remove_solver/remove_with_many_existing_solvers": 3758, + "remove_solver/removes_a_solver": 3493, "settle/finalizes_with_no_pushes": 7171, "settle/pulls_from_multiple_orders": 20076, "settle/pulls_funds_to_destination": 13657, @@ -65,14 +81,21 @@ "add_solver/add_with_many_existing_solvers": 366, "add_solver/adds_a_solver": 366, "create_buffers/happy_path_creates_initialized_buffer_token_account": 303, + "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 303, "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 435, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 435, "create_buffers/max_buffers_in_one_instruction": 331, + "create_buffers/max_buffers_in_one_instruction_token_2022": 295, "create_order/happy_path_creates_order_pda_with_expected_body": 452, "initialize/happy_path_initializes_state_pda_with_expected_data": 301, "reclaim_buffer/funded_buffer_is_skipped": 400, + "reclaim_buffer/funded_buffer_is_skipped_token_2022": 400, "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 400, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 400, "reclaim_buffer/max_buffers_in_one_instruction": 332, + "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 332, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 466, "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 236, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 204, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 204, From 1f0eb5fec46d7203d40798daa7537775798597e7 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:28:40 +0900 Subject: [PATCH 36/36] remove new tests from bench --- bench-report.json | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/bench-report.json b/bench-report.json index e0a0e2a6..80489801 100644 --- a/bench-report.json +++ b/bench-report.json @@ -3,21 +3,14 @@ "add_solver/add_with_many_existing_solvers": 5, "add_solver/adds_a_solver": 5, "create_buffers/happy_path_creates_initialized_buffer_token_account": 6, - "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 6, "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 10, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 10, "create_buffers/max_buffers_in_one_instruction": 64, - "create_buffers/max_buffers_in_one_instruction_token_2022": 46, "create_order/happy_path_creates_order_pda_with_expected_body": 4, "initialize/happy_path_initializes_state_pda_with_expected_data": 4, "reclaim_buffer/funded_buffer_is_skipped": 7, - "reclaim_buffer/funded_buffer_is_skipped_token_2022": 7, "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 7, "reclaim_buffer/max_buffers_in_one_instruction": 64, - "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 64, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 9, "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 4, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 3, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 3, @@ -42,21 +35,14 @@ "add_solver/add_with_many_existing_solvers": 5074, "add_solver/adds_a_solver": 4622, "create_buffers/happy_path_creates_initialized_buffer_token_account": 7373, - "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 12188, "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17283, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 31720, "create_buffers/max_buffers_in_one_instruction": 169742, - "create_buffers/max_buffers_in_one_instruction_token_2022": 214956, "create_order/happy_path_creates_order_pda_with_expected_body": 4986, "initialize/happy_path_initializes_state_pda_with_expected_data": 4530, "reclaim_buffer/funded_buffer_is_skipped": 4860, - "reclaim_buffer/funded_buffer_is_skipped_token_2022": 4868, "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 6010, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 7499, "reclaim_buffer/max_buffers_in_one_instruction": 124824, - "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 169378, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7612, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 9105, "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2203, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2072, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2080, @@ -81,21 +67,14 @@ "add_solver/add_with_many_existing_solvers": 366, "add_solver/adds_a_solver": 366, "create_buffers/happy_path_creates_initialized_buffer_token_account": 303, - "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 303, "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 435, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 435, "create_buffers/max_buffers_in_one_instruction": 331, - "create_buffers/max_buffers_in_one_instruction_token_2022": 295, "create_order/happy_path_creates_order_pda_with_expected_body": 452, "initialize/happy_path_initializes_state_pda_with_expected_data": 301, "reclaim_buffer/funded_buffer_is_skipped": 400, - "reclaim_buffer/funded_buffer_is_skipped_token_2022": 400, "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 400, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 400, "reclaim_buffer/max_buffers_in_one_instruction": 332, - "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 332, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 466, "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 236, "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 204, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 204,