From d4c19acd16f3f5ac97794bb951d78aec64ecc009 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:10:57 +0900 Subject: [PATCH 01/38] Support Token-2022 in `BeginSettle` and `FinalizeSettle` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BeginSettle` and `FinalizeSettle` each took a `token_program` account and rejected anything that wasn't the legacy SPL Token program, so the buffers `CreateBuffer` can now open under Token-2022 had no way to be settled. They accept it too, and issue every transfer against whichever of the two they were handed, through the same `token::validate_token_program` gate the buffer instructions use. Token-2022 encodes `Transfer` exactly as the legacy program does, so only the CPI target changes. What differs is the account data: a Token-2022 account carrying extensions is longer than the base layout, and the legacy reader insists on an exact length. Both sides now read through `token::read_token_account`, which dispatches on the validated program — the sell account's owner in `BeginSettle`, the destination's mint in `FinalizeSettle`. That reader grows the `mint` and `owner` fields the settlement needs and the buffer instructions didn't, which costs `ReclaimBuffer` a little: its `max_buffers_in_one_instruction` goes 137,046 -> 138,392 CU for the wider read. The settle benchmarks rise 0.3-1.6% from the added dispatch. The `token_program` account is shared by the whole instruction, so every token one settlement touches must live under the same program; a mixed settlement still needs two instruction pairs. Co-Authored-By: Claude Opus 5 (1M context) --- programs/settlement/src/settle/begin.rs | 47 +++++++++++++--------- programs/settlement/src/settle/finalize.rs | 16 ++++++-- programs/settlement/src/token.rs | 25 +++++++++--- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 19d6927b..416e9b4b 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -15,7 +15,9 @@ use cow_settlement_interface::{ InstructionInputParsing, }, pda::buffer::validate_buffer_pda, - recover_discriminator, SettlementError, SettlementInstruction, + recover_discriminator, + token_program::TokenProgram, + SettlementError, SettlementInstruction, }; use pinocchio::{ cpi::Signer, @@ -27,11 +29,11 @@ use pinocchio::{ }, AccountView, Address, ProgramResult, }; -use pinocchio_token::{instructions::Transfer, state::Account as TokenAccount}; +use pinocchio_token::instructions::Transfer; use crate::{ processor::{check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump}, - token::validate_token_program, + token::{read_token_account, validate_token_program}, }; use super::validate_counterpart; @@ -76,7 +78,7 @@ pub fn process_begin_settle( let finalize_ix = instructions.load_instruction_at(usize::from(input.finalize_ix_index))?; - validate_token_program(input.token_program_account)?; + let token_program = validate_token_program(input.token_program_account)?; with_state_pda_signer_from_bump(state_bump, |signer| { settle_orders( @@ -85,6 +87,7 @@ pub fn process_begin_settle( signer, &input.orders, &finalize_ix, + token_program, ) }) } @@ -205,6 +208,7 @@ fn settle_orders( state_pda_signer: &Signer, orders: &SettledOrders<'_, AccountView>, finalize_ix: &IntrospectedInstruction, + token_program: TokenProgram, ) -> ProgramResult { // Orders must be passed strictly increasing by address; this rejects // duplicates (settling the same order twice) without a separate scan. @@ -235,6 +239,7 @@ fn settle_orders( now, state_pda_account, state_pda_signer, + token_program, )?; } @@ -258,6 +263,7 @@ fn process_order( now: i64, state_account: &AccountView, state_pda_signer: &Signer, + token_program: TokenProgram, ) -> ProgramResult { let SettledOrder { order_pda, @@ -297,21 +303,19 @@ fn process_order( } // Assert the order intent owner and sell mint match those of the sell token // account. - { - // `from_account_view` confirms this is a real SPL token account - // (right length, owned by the token program) before we read its - // owner and mint. The borrow it holds is released at the end of this - // block, before the transfers below touch the same account. - let token_account = TokenAccount::from_account_view(sell_token_account) - .map_err(|_| SettlementError::SellTokenAccountInvalid)?; - if token_account.owner() != &intent.owner { - return Err(SettlementError::SellTokenOwnerMismatch.into()); - } - // Like the buy side, the account could have been recreated for another - // mint after the order was created. - if token_account.mint() != &intent.sell_mint { - return Err(SettlementError::SellMintMismatch.into()); - } + // `read_token_account` confirms this is a real token account of the + // instruction's token program before we read its owner and mint, and reads + // by value, so nothing is left borrowing the account when the transfers + // below touch it. + let sell_token = read_token_account(token_program, sell_token_account) + .map_err(|_| SettlementError::SellTokenAccountInvalid)?; + if sell_token.owner != intent.owner { + return Err(SettlementError::SellTokenOwnerMismatch.into()); + } + // Like the buy side, the account could have been recreated for another + // mint after the order was created. + if sell_token.mint != intent.sell_mint { + return Err(SettlementError::SellMintMismatch.into()); } // Pull the configured amounts out of the sell token account, summing them @@ -324,7 +328,10 @@ fn process_order( .checked_add(amount) .ok_or(SettlementError::PullAmountOverflow)?; Transfer::new(sell_token_account, destination, state_account, amount) - .invoke_signed(core::slice::from_ref(state_pda_signer))?; + .invoke_signed_with_unverified_program( + core::slice::from_ref(state_pda_signer), + &token_program.address(), + )?; } validate_limit_price(intent, amount_in, push.amount)?; diff --git a/programs/settlement/src/settle/finalize.rs b/programs/settlement/src/settle/finalize.rs index ea20b01a..3bd2564e 100644 --- a/programs/settlement/src/settle/finalize.rs +++ b/programs/settlement/src/settle/finalize.rs @@ -5,6 +5,7 @@ use cow_settlement_interface::{ settle::{FinalizeSettleInput, Pushes}, InstructionInputParsing, }, + token_program::TokenProgram, SettlementError, SettlementInstruction, }; use pinocchio::{ @@ -47,10 +48,15 @@ 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(input.token_program_account)?; + let token_program = 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) + push_funds( + input.state_pda_account, + state_pda_signer, + input.pushes, + token_program, + ) }) } @@ -69,6 +75,7 @@ fn push_funds<'a>( state_pda_account: &AccountView, state_pda_signer: &Signer, pushes: Pushes<'a, AccountView>, + token_program: TokenProgram, ) -> ProgramResult { for push in pushes.iter() { Transfer::new( @@ -77,7 +84,10 @@ fn push_funds<'a>( state_pda_account, push.amount, ) - .invoke_signed(core::slice::from_ref(state_pda_signer))?; + .invoke_signed_with_unverified_program( + core::slice::from_ref(state_pda_signer), + &token_program.address(), + )?; } Ok(()) diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index efe82e07..25148d27 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, SettlementError}; -use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; +use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView, Address}; use pinocchio_token::instructions::GetAccountDataSize; /// The length of a SPL token program account. Token2022 extensions may make @@ -51,6 +51,8 @@ pub fn token_account_len( /// [`read_token_account`]. /// For our purposes, we only need the `amount`. pub struct TokenAccount { + pub mint: Address, + pub owner: Address, pub amount: u64, } @@ -60,15 +62,24 @@ pub fn read_token_account( token_program: TokenProgram, account: &AccountView, ) -> Result { - let amount = match token_program { + Ok(match token_program { TokenProgram::SplToken => { - pinocchio_token::state::Account::from_account_view(account)?.amount() + let decoded = pinocchio_token::state::Account::from_account_view(account)?; + TokenAccount { + amount: decoded.amount(), + mint: *decoded.mint(), + owner: *decoded.owner(), + } } TokenProgram::Token2022 => { - pinocchio_token_2022::state::Account::from_account_view(account)?.amount() + let decoded = pinocchio_token_2022::state::Account::from_account_view(account)?; + TokenAccount { + amount: decoded.amount(), + mint: *decoded.mint(), + owner: *decoded.owner(), + } } - }; - Ok(TokenAccount { amount }) + }) } #[cfg(test)] @@ -235,6 +246,8 @@ mod tests { ); let read = read_token_account(TokenProgram::Token2022, &account) .expect("an extended Token-2022 account should read"); + assert_eq!(read.mint, mint); + assert_eq!(read.owner, owner); assert_eq!(read.amount, 7); } From ac413194fc4b7f9f774c3d748c0eb6451bb2d4bc Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:03:33 +0900 Subject: [PATCH 02/38] Carry both token programs in a settlement pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BeginSettle` and `FinalizeSettle` took one `token_program` account, so every token an instruction touched had to live under the same program: settling a legacy SPL mint and a Token-2022 mint meant two instruction pairs. They now take one account per supported program, at fixed positions after the state PDA, and issue each transfer against the program that owns the account it moves. One pair can settle both, and the two sides of a single order need not agree — the pull follows the sell account's owner, the push the buy account's. A program the settlement doesn't touch is left out by putting the system program in its slot. The transfers still need their program named by the transaction, so the placeholder is how an instruction says this one isn't; in a transaction that already references the system program it costs an account index instead of another 32-byte address. Nothing new goes into the instruction data: the owner is the authority on which program an account belongs to, and the slots only decide whether the settlement can reach it. Resolving an account gives one of three answers: - owned by a carried program: its transfers CPI into that program; - owned by a supported program whose slot holds the placeholder: the new `SettlementError::TokenProgramNotProvided` (36), so a forgotten slot reads as itself rather than as a malformed account; - owned by neither: the existing `SellTokenAccountInvalid` / `InvalidBuyTokenAccount`, unchanged. The slots are positional. Each holds its own program or the placeholder; anything else, swapping the two included, is `IncorrectProgramId`. `FINALIZE_FIXED_ACCOUNTS` becomes 4, which `push_destinations` follows on its own. `CreateBuffer` and `ReclaimBuffer` keep their single `token_program` account: each works on one mint at a time, so there is nothing to mix. The settle benchmarks each gain one account, 34 transaction bytes, and 50-170 CU. The one- and two-unit drift on the unrelated create/reclaim lines is codegen, not behaviour. Co-Authored-By: Claude Opus 5 (1M context) --- client/src/instructions.rs | 24 +- client/src/parse.rs | 2 + interface/src/instruction/settle/begin.rs | 120 +++-- interface/src/instruction/settle/finalize.rs | 109 ++++- interface/src/instruction/settle/mod.rs | 2 + interface/src/lib.rs | 6 + interface/src/token_program.rs | 149 +++++- programs/settlement/src/settle/begin.rs | 37 +- programs/settlement/src/settle/finalize.rs | 19 +- programs/settlement/src/token.rs | 117 +++++ .../settlement/tests/begin_settle_orders.rs | 36 +- programs/settlement/tests/common/buffer.rs | 10 +- .../settlement/tests/common/settlement.rs | 5 +- programs/settlement/tests/common/token.rs | 136 +++++- .../tests/finalize_settle_pushes.rs | 22 +- .../tests/matching_begin_finalize.rs | 12 +- .../settlement/tests/program_deployment.rs | 4 +- .../settlement/tests/settle_token_programs.rs | 429 ++++++++++++++++++ test-cli/src/cmd/settle.rs | 5 + 19 files changed, 1124 insertions(+), 120 deletions(-) create mode 100644 programs/settlement/tests/settle_token_programs.rs diff --git a/client/src/instructions.rs b/client/src/instructions.rs index eaec2398..c21339d5 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -14,7 +14,7 @@ use cow_settlement_interface::{ // Reexport the instruction builders that don't change from the interface. // We want the client to provide all instruction builders. -pub use cow_settlement_interface::instruction::settle::Pull; +pub use cow_settlement_interface::instruction::settle::{Pull, TokenPrograms}; /// An order ready to be settled, together with the funds to pull from it: /// `intent` identifies the order and `pulls` lists the [`Pull`]s to make from @@ -32,6 +32,10 @@ pub struct BeginSettle<'a> { /// The off-chain auction this settlement executes, carried so it can be tied /// back to its auction off-chain. pub auction_id: i64, + /// The token programs owning the accounts this settlement pulls from and + /// pays into. Leaving one out makes its accounts unsettleable here, so this + /// has to cover every one of them. + pub token_programs: TokenPrograms, pub orders: &'a [InitializedIntent<'a>], } @@ -53,6 +57,7 @@ impl From> for Instruction { solver: builder.solver, finalize_ix_index: builder.finalize_ix_index, auction_id: builder.auction_id, + token_programs: builder.token_programs, order_pdas: &order_pdas, sell_token_accounts: &sell_token_accounts, pulls: &pull_lists, @@ -81,6 +86,9 @@ pub struct FinalizedIntent<'a> { pub struct FinalizeSettle<'a> { pub program_id: Pubkey, pub begin_ix_index: u16, + /// The token programs owning the buffers and buy token accounts this + /// settlement pushes between, filled the same way as [`BeginSettle`]'s. + pub token_programs: TokenPrograms, pub orders: &'a [FinalizedIntent<'a>], } @@ -114,6 +122,7 @@ impl From> for Instruction { program_id: builder.program_id, state_pda, begin_ix_index: builder.begin_ix_index, + token_programs: builder.token_programs, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, @@ -309,7 +318,8 @@ mod tests { instruction::{ fixtures::fake_account_from_array, settle::{ - BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID, + BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, + SPL_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, }, InstructionInputParsing, }, @@ -337,6 +347,7 @@ mod tests { solver: pubkey_from_seed("solver"), finalize_ix_index, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &orders, }); @@ -401,6 +412,7 @@ mod tests { let ix = Instruction::from(FinalizeSettle { program_id, begin_ix_index, + token_programs: TokenPrograms::SPL_TOKEN, orders: &orders, }); @@ -446,9 +458,15 @@ mod tests { let (state_pda, _bump) = find_state_pda(&program_id); prop_assert_eq!(parsed.state_pda_account.address(), &state_pda); prop_assert_eq!( - parsed.token_program_account.address(), + parsed.spl_token_program_account.address(), &SPL_TOKEN_PROGRAM_ID, ); + // These settlements are legacy-only, so Token-2022's slot stands + // empty. + prop_assert_eq!( + parsed.token_2022_program_account.address(), + &SYSTEM_PROGRAM_ID, + ); let parsed_pushes: Vec<_> = parsed.pushes.iter().collect(); prop_assert_eq!(parsed_pushes.len(), expected.len()); diff --git a/client/src/parse.rs b/client/src/parse.rs index 6d5eeaa1..6f7d7e48 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -125,6 +125,7 @@ mod tests { solver: payer, finalize_ix_index: 1, auction_id: 42, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[InitializedIntent { intent: &intent, pulls: &[], @@ -134,6 +135,7 @@ mod tests { SettlementInstruction::FinalizeSettle => FinalizeSettle { program_id, begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], } .into(), diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index a4284f89..76ce8382 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -9,7 +9,7 @@ use solana_pubkey::Pubkey; use crate::instruction::InstructionInputParsing; use crate::{SettlementError, SettlementInstruction}; -use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID}; +use super::{recover_counterpart, TokenPrograms, INSTRUCTIONS_SYSVAR_ID}; /// A single transfer made when settling an order: `amount` tokens sent from the /// order's sell token account to `destination`. @@ -34,8 +34,11 @@ pub struct Pull { /// `[discriminator=0][finalize_ix_index: u16 LE][auction_id: i64 LE][n: u8] /// [transfer_count×n][amount: u64 LE ×T]`. /// Required accounts: `[solver (S,R), instructions_sysvar (R), state_pda (R), -/// token_program (R)]` followed, per order, by `[order_pda (W), -/// sell_token_account (W), destination (W)...]`. +/// spl_token_program (R), token_2022_program (R)]` followed, per order, by +/// `[order_pda (W), sell_token_account (W), destination (W)...]`. The two token +/// programs are the slots [`TokenPrograms`] describes: each transfer is issued +/// against the program that owns the account it moves, and a program this +/// settlement doesn't touch is left out with the system program. /// /// `solver` must sign, and the solver must be registered in the state pda. /// @@ -52,6 +55,9 @@ pub struct BeginSettle<'a> { /// instruction data so the settlement can be tied back to its auction /// off-chain, unused on-chain. pub auction_id: i64, + /// The token programs this settlement carries, one slot each; see + /// [`TokenPrograms`]. + pub token_programs: TokenPrograms, pub order_pdas: &'a [Pubkey], pub sell_token_accounts: &'a [Pubkey], pub pulls: &'a [&'a [Pull]], @@ -65,6 +71,7 @@ impl From> for Instruction { solver, finalize_ix_index, auction_id, + token_programs, order_pdas, sell_token_accounts, pulls, @@ -93,13 +100,18 @@ impl From> for Instruction { .concat(); // The signing solver, followed by read-only accounts for instruction - // introspection, settlement state, and the SPL token program. + // introspection, settlement state, and one slot per supported token + // program. let mut accounts = vec![ AccountMeta::new_readonly(solver, true), AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), AccountMeta::new_readonly(state_pda, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), ]; + accounts.extend( + token_programs + .addresses() + .map(|address| AccountMeta::new_readonly(address, false)), + ); for &i in &order { // Writable account for the order: `BeginSettle` updates its filled // amounts (`amount_withdrawn`/`amount_received`). @@ -202,7 +214,11 @@ pub struct BeginSettleInput<'a, A> { pub solver_account: &'a A, pub instructions_sysvar_account: &'a A, pub state_pda_account: &'a A, - pub token_program_account: &'a A, + /// The legacy SPL Token program's slot: the program itself, or the + /// placeholder where this settlement moves no token under it. + pub spl_token_program_account: &'a A, + /// Token-2022's slot, filled the same way. + pub token_2022_program_account: &'a A, pub orders: SettledOrders<'a, A>, } @@ -215,7 +231,7 @@ impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> { fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result { let (finalize_ix_index, body) = recover_counterpart(instruction_data)?; - let [solver_account, instructions_sysvar_account, state_pda_account, token_program_account, order_accounts @ ..] = + let [solver_account, instructions_sysvar_account, state_pda_account, spl_token_program_account, token_2022_program_account, order_accounts @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -269,7 +285,8 @@ impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> { auction_id, instructions_sysvar_account, state_pda_account, - token_program_account, + spl_token_program_account, + token_2022_program_account, solver_account, orders: SettledOrders { order_accounts, @@ -288,15 +305,17 @@ mod tests { fake_account, fake_account_from_array, fake_sequential_accounts, }; use crate::instruction::settle::tests::ix_data; + use crate::instruction::settle::SPL_TOKEN_PROGRAM_ID; use crate::instruction::tests::{assert_readonly_nonsigner, assert_readonly_signer}; + use crate::token_program::{TokenProgram, SYSTEM_PROGRAM_ID}; use hex_literal::hex; use solana_account_view::AccountView; use solana_address::Address; /// The fixed accounts every `BeginSettle` carries before its order accounts: /// the signing solver, the instructions sysvar, the settlement state PDA, and - /// the token program. - const FIXED_ACCOUNTS: usize = 4; + /// one slot per supported token program. + const FIXED_ACCOUNTS: usize = 5; /// A placeholder auction id for the tests where its specific value is /// incidental. The wire-layout tests spell out the literal bytes instead. @@ -317,6 +336,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: 0x0102_0304_0506_0708, + token_programs: TokenPrograms::SPL_TOKEN, order_pdas: &[], sell_token_accounts: &[], pulls: &[], @@ -332,14 +352,47 @@ mod tests { [0], // order count ], ); - // No orders: the four fixed accounts (solver, sysvar, state PDA, token - // program). Only the solver signs; the rest don't play an active role in - // the base instruction (the state PDA CPI signature isn't relevant here). - assert_eq!(accounts.len(), 4); + // No orders: the fixed accounts (solver, sysvar, state PDA, and a slot + // per token program). Only the solver signs; the rest don't play an + // active role in the base instruction (the state PDA CPI signature isn't + // relevant here). This settlement carries only the legacy program, so + // Token-2022's slot holds the placeholder. + assert_eq!(accounts.len(), FIXED_ACCOUNTS); assert_readonly_signer(&accounts[0], solver); assert_readonly_nonsigner(&accounts[1], INSTRUCTIONS_SYSVAR_ID); assert_readonly_nonsigner(&accounts[2], state_pda); assert_readonly_nonsigner(&accounts[3], SPL_TOKEN_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[4], SYSTEM_PROGRAM_ID); + } + + /// The token-program slots are whatever [`TokenPrograms`] says, in its own + /// order, so a settlement can carry both programs — or leave either one out. + #[test] + fn begin_settle_carries_the_token_program_slots_it_is_given() { + for token_programs in [ + TokenPrograms::SPL_TOKEN, + TokenPrograms::TOKEN_2022, + TokenPrograms::BOTH, + TokenPrograms::NONE, + ] { + let Instruction { accounts, .. } = Instruction::from(BeginSettle { + program_id: Pubkey::new_unique(), + state_pda: Pubkey::new_unique(), + solver: Pubkey::new_unique(), + finalize_ix_index: 0, + auction_id: 0, + token_programs, + order_pdas: &[], + sell_token_accounts: &[], + pulls: &[], + }); + let slots: Vec = accounts[3..].iter().map(|meta| meta.pubkey).collect(); + assert_eq!( + slots, + token_programs.addresses(), + "{token_programs:?} should be laid out as its own addresses", + ); + } } #[test] @@ -359,6 +412,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: AUCTION_ID, + token_programs: TokenPrograms::SPL_TOKEN, order_pdas: &[high_order_pda, low_order_pda], sell_token_accounts: &[high_sell_token_account, low_sell_token_account], pulls: &[&[], &[]], @@ -381,6 +435,7 @@ mod tests { INSTRUCTIONS_SYSVAR_ID, state_pda, SPL_TOKEN_PROGRAM_ID, + SYSTEM_PROGRAM_ID, low_order_pda, low_sell_token_account, high_order_pda, @@ -428,6 +483,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: AUCTION_ID, + token_programs: TokenPrograms::BOTH, order_pdas: &[order_a, order_b], sell_token_accounts: &[sell_a, sell_b], pulls: &[ @@ -469,6 +525,7 @@ mod tests { INSTRUCTIONS_SYSVAR_ID, state_pda, SPL_TOKEN_PROGRAM_ID, + TokenProgram::Token2022.address(), order_a, sell_a, dest_a0, @@ -498,13 +555,15 @@ mod tests { fn begin_settle_input_parses_valid_input() { let sysvar = pubkey_from_seed("sysvar"); let state = pubkey_from_seed("state pda"); - let token_program = pubkey_from_seed("token program"); + let spl_token_program = pubkey_from_seed("spl token program"); + let token_2022_program = pubkey_from_seed("token 2022 program"); let solver = pubkey_from_seed("solver"); let accounts = [ fake_account(solver), fake_account(sysvar), fake_account(state), - fake_account(token_program), + fake_account(spl_token_program), + fake_account(token_2022_program), ]; let data = ix_data![ [SettlementInstruction::BeginSettle.discriminator()], @@ -518,14 +577,16 @@ mod tests { solver_account, instructions_sysvar_account, orders, - token_program_account, + spl_token_program_account, + token_2022_program_account, state_pda_account, } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(finalize_ix_index, 0x1337); assert_eq!(auction_id, 0x0102_0304_0506_0708); assert_eq!(instructions_sysvar_account.address(), &sysvar); assert_eq!(orders.iter().count(), 0); - assert_eq!(token_program_account.address(), &token_program); + assert_eq!(spl_token_program_account.address(), &spl_token_program); + assert_eq!(token_2022_program_account.address(), &token_2022_program); assert_eq!(state_pda_account.address(), &state); assert_eq!(solver_account.address(), &solver); } @@ -576,7 +637,8 @@ mod tests { fn begin_settle_input_pairs_orders_with_their_accounts() { let sysvar = pubkey_from_seed("sysvar"); let state = pubkey_from_seed("state pda"); - let token_program = pubkey_from_seed("token program"); + let spl_token_program = pubkey_from_seed("spl token program"); + let token_2022_program = pubkey_from_seed("token 2022 program"); let solver = pubkey_from_seed("solver"); let order_pda = pubkey_from_seed("order pda"); let sell_token = pubkey_from_seed("sell token"); @@ -584,7 +646,8 @@ mod tests { fake_account(solver), fake_account(sysvar), fake_account(state), - fake_account(token_program), + fake_account(spl_token_program), + fake_account(token_2022_program), fake_account(order_pda), fake_account(sell_token), ]; @@ -602,12 +665,14 @@ mod tests { instructions_sysvar_account, orders, state_pda_account, - token_program_account, + spl_token_program_account, + token_2022_program_account, } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(finalize_ix_index, 0x1337); assert_eq!(auction_id, AUCTION_ID); assert_eq!(instructions_sysvar_account.address(), &sysvar); - assert_eq!(token_program_account.address(), &token_program); + assert_eq!(spl_token_program_account.address(), &spl_token_program); + assert_eq!(token_2022_program_account.address(), &token_2022_program); assert_eq!(state_pda_account.address(), &state); assert_eq!(solver_account.address(), &solver); @@ -623,7 +688,8 @@ mod tests { fn begin_settle_input_parses_transfers() { let sysvar = pubkey_from_seed("sysvar"); let state = pubkey_from_seed("state pda"); - let token_program = pubkey_from_seed("token program"); + let spl_token_program = pubkey_from_seed("spl token program"); + let token_2022_program = pubkey_from_seed("token 2022 program"); let solver = pubkey_from_seed("solver"); let order_pda = pubkey_from_seed("order pda"); let sell_token = pubkey_from_seed("sell token"); @@ -633,7 +699,8 @@ mod tests { fake_account(solver), fake_account(sysvar), fake_account(state), - fake_account(token_program), + fake_account(spl_token_program), + fake_account(token_2022_program), fake_account(order_pda), fake_account(sell_token), fake_account(dest0), @@ -677,13 +744,14 @@ mod tests { expected.push((order_pda, sell_token)); } - // The four fixed accounts (`[0xff..]`, `[0xfe..]`, `[0xfd..]`, `[0xfc..]`) - // differ from every order/token address above. + // The fixed accounts (`[0xff..]` down to `[0xfb..]`) differ from every + // order/token address above. let mut accounts = vec![ fake_account_from_array([0xff; 32]), fake_account_from_array([0xfe; 32]), fake_account_from_array([0xfd; 32]), fake_account_from_array([0xfc; 32]), + fake_account_from_array([0xfb; 32]), ]; for &(order_pda, sell_token) in &expected { accounts.push(fake_account(order_pda)); diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index b6ed462a..58d4ae89 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -9,12 +9,12 @@ use solana_pubkey::Pubkey; use crate::instruction::InstructionInputParsing; use crate::{recover_discriminator, SettlementError, SettlementInstruction}; -use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID}; +use super::{recover_counterpart, TokenPrograms, INSTRUCTIONS_SYSVAR_ID}; /// The number of fixed accounts every `FinalizeSettle` carries before its push -/// accounts: the instructions sysvar, the settlement state PDA, and the token -/// program. -pub const FINALIZE_FIXED_ACCOUNTS: usize = 3; +/// accounts: the instructions sysvar, the settlement state PDA, and one slot per +/// supported token program. +pub const FINALIZE_FIXED_ACCOUNTS: usize = 4; /// Split the instruction bytes from `FinalizeSettle` that remain after all /// constant-size data has been extracted into the per-push bump bytes and the @@ -80,8 +80,10 @@ pub fn finalize_push_data( /// Wire format (with `n` total pushes): /// `[discriminator=1][begin_ix_index: u16 LE][bump: u8 ×n][amount: u64 LE ×n]`. /// Required accounts: -/// `[instructions_sysvar (R), state_pda (R), token_program (R)]` followed, per -/// push, by `[source_buffer (W), destination (W)]`. +/// `[instructions_sysvar (R), state_pda (R), spl_token_program (R), +/// token_2022_program (R)]` followed, per push, by `[source_buffer (W), +/// destination (W)]`. The two token programs are the slots [`TokenPrograms`] +/// describes; the matching `BeginSettle` carries the same ones. /// /// `FinalizeSettle` only executes the transfers. Every push is validated by /// `BeginSettle`, which reads this instruction through introspection. @@ -89,6 +91,9 @@ pub struct FinalizeSettle<'a> { pub program_id: Pubkey, pub state_pda: Pubkey, pub begin_ix_index: u16, + /// The token programs this settlement carries, one slot each; see + /// [`TokenPrograms`]. + pub token_programs: TokenPrograms, pub source_buffers: &'a [Pubkey], pub destinations: &'a [Pubkey], pub bumps: &'a [u8], @@ -101,6 +106,7 @@ impl From> for Instruction { program_id, state_pda, begin_ix_index, + token_programs, source_buffers, destinations, bumps, @@ -116,8 +122,12 @@ impl From> for Instruction { let mut accounts = vec![ AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), AccountMeta::new_readonly(state_pda, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), ]; + accounts.extend( + token_programs + .addresses() + .map(|address| AccountMeta::new_readonly(address, false)), + ); for (source, destination) in source_buffers.iter().zip(destinations) { accounts.push(AccountMeta::new(*source, false)); accounts.push(AccountMeta::new(*destination, false)); @@ -198,7 +208,11 @@ pub struct FinalizeSettleInput<'a, A> { pub begin_ix_index: u16, pub instructions_sysvar_account: &'a A, pub state_pda_account: &'a A, - pub token_program_account: &'a A, + /// The legacy SPL Token program's slot: the program itself, or the + /// placeholder where this settlement moves no token under it. + pub spl_token_program_account: &'a A, + /// Token-2022's slot, filled the same way. + pub token_2022_program_account: &'a A, pub pushes: Pushes<'a, A>, } @@ -211,7 +225,7 @@ impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> { fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result { let (begin_ix_index, body) = recover_counterpart(instruction_data)?; - let [instructions_sysvar_account, state_pda_account, token_program_account, push_accounts @ ..] = + let [instructions_sysvar_account, state_pda_account, spl_token_program_account, token_2022_program_account, push_accounts @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -233,7 +247,8 @@ impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> { begin_ix_index, instructions_sysvar_account, state_pda_account, - token_program_account, + spl_token_program_account, + token_2022_program_account, pushes: Pushes { push_accounts, bumps, @@ -251,7 +266,9 @@ mod tests { fake_account, fake_account_from_array, fake_sequential_accounts, }; use crate::instruction::settle::tests::ix_data; + use crate::instruction::settle::SPL_TOKEN_PROGRAM_ID; use crate::instruction::tests::assert_readonly_nonsigner; + use crate::token_program::{TokenProgram, SYSTEM_PROGRAM_ID}; use hex_literal::hex; use proptest::prelude::*; use solana_account_view::AccountView; @@ -265,6 +282,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[], destinations: &[], bumps: &[], @@ -285,6 +303,7 @@ mod tests { program_id, state_pda, begin_ix_index: 0x1337, + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[], destinations: &[], bumps: &[], @@ -299,14 +318,45 @@ mod tests { hex!("3713"), // counterpart index (little-endian) ], ); - // No orders: the three fixed accounts (sysvar, state PDA, token - // program). They are all generic accounts that don't play an active - // role in the base instruction (the state PDA CPI signature isn't - // relevant here). - assert_eq!(accounts.len(), 3); + // No orders: the fixed accounts (sysvar, state PDA, and a slot per token + // program). They are all generic accounts that don't play an active role + // in the base instruction (the state PDA CPI signature isn't relevant + // here). This settlement carries only the legacy program, so Token-2022's + // slot holds the placeholder. + assert_eq!(accounts.len(), FINALIZE_FIXED_ACCOUNTS); assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID); assert_readonly_nonsigner(&accounts[1], state_pda); assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID); + } + + /// The token-program slots are whatever [`TokenPrograms`] says, in its own + /// order, so a settlement can carry both programs — or leave either one out. + #[test] + fn finalize_settle_carries_the_token_program_slots_it_is_given() { + for token_programs in [ + TokenPrograms::SPL_TOKEN, + TokenPrograms::TOKEN_2022, + TokenPrograms::BOTH, + TokenPrograms::NONE, + ] { + let ix = Instruction::from(FinalizeSettle { + program_id: Pubkey::new_unique(), + state_pda: Pubkey::new_unique(), + begin_ix_index: 0, + token_programs, + source_buffers: &[], + destinations: &[], + bumps: &[], + amounts: &[], + }); + let slots: Vec = ix.accounts[2..].iter().map(|meta| meta.pubkey).collect(); + assert_eq!( + slots, + token_programs.addresses(), + "{token_programs:?} should be laid out as its own addresses", + ); + } } #[test] @@ -322,6 +372,7 @@ mod tests { program_id, state_pda, begin_ix_index: 0x1337, + token_programs: TokenPrograms::BOTH, source_buffers: &[source_a, source_b], destinations: &[dest_a, dest_b], bumps: &[0xa1, 0xb1], @@ -347,6 +398,7 @@ mod tests { INSTRUCTIONS_SYSVAR_ID, state_pda, SPL_TOKEN_PROGRAM_ID, + TokenProgram::Token2022.address(), source_a, dest_a, source_b, @@ -369,11 +421,13 @@ mod tests { fn finalize_settle_input_parses_no_pushes() { let sysvar = pubkey_from_seed("sysvar"); let state = pubkey_from_seed("state pda"); - let token_program = pubkey_from_seed("token program"); + let spl_token_program = pubkey_from_seed("spl token program"); + let token_2022_program = pubkey_from_seed("token 2022 program"); let accounts = [ fake_account(sysvar), fake_account(state), - fake_account(token_program), + fake_account(spl_token_program), + fake_account(token_2022_program), ]; let data = ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], @@ -383,13 +437,15 @@ mod tests { begin_ix_index, instructions_sysvar_account, state_pda_account, - token_program_account, + spl_token_program_account, + token_2022_program_account, pushes, } = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(begin_ix_index, 0x1337); assert_eq!(instructions_sysvar_account.address(), &sysvar); assert_eq!(state_pda_account.address(), &state); - assert_eq!(token_program_account.address(), &token_program); + assert_eq!(spl_token_program_account.address(), &spl_token_program); + assert_eq!(token_2022_program_account.address(), &token_2022_program); assert_eq!(pushes.iter().count(), 0); } @@ -397,7 +453,8 @@ mod tests { fn finalize_settle_input_parses_pushes() { let sysvar = pubkey_from_seed("sysvar"); let state = pubkey_from_seed("state pda"); - let token_program = pubkey_from_seed("token program"); + let spl_token_program = pubkey_from_seed("spl token program"); + let token_2022_program = pubkey_from_seed("token 2022 program"); // The same source buffer funds both pushes: parsing makes no uniqueness // assumption about source buffers. let source = pubkey_from_seed("source buffer"); @@ -406,7 +463,8 @@ mod tests { let accounts = [ fake_account(sysvar), fake_account(state), - fake_account(token_program), + fake_account(spl_token_program), + fake_account(token_2022_program), fake_account(source), fake_account(dest0), fake_account(source), @@ -467,12 +525,13 @@ mod tests { }); } - // The three fixed accounts (`[0xff..]`, `[0xfe..]`, `[0xfd..]`) differ - // from every source/destination address above. + // The fixed accounts (`[0xff..]` down to `[0xfc..]`) differ from every + // source/destination address above. let mut accounts = vec![ fake_account_from_array([0xff; 32]), fake_account_from_array([0xfe; 32]), fake_account_from_array([0xfd; 32]), + fake_account_from_array([0xfc; 32]), ]; let mut bump_bytes = Vec::new(); let mut amount_bytes = Vec::new(); @@ -578,6 +637,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0x1337, + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[ pubkey_from_seed("source buffer 0"), pubkey_from_seed("source buffer 1"), @@ -601,6 +661,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[], destinations: &[], bumps: &[], @@ -616,6 +677,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[pubkey_from_seed("source buffer")], destinations: &[pubkey_from_seed("destination")], bumps: &[0xff], @@ -650,6 +712,7 @@ mod tests { program_id, state_pda, begin_ix_index, + token_programs: TokenPrograms::BOTH, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index 8ba08590..53014458 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -6,6 +6,8 @@ use solana_program_error::ProgramError; /// The legacy SPL Token program, which the builders below target by default. pub const SPL_TOKEN_PROGRAM_ID: Pubkey = TokenProgram::SplToken.address(); + +pub use crate::token_program::TokenPrograms; pub use solana_sdk_ids::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID; mod begin; diff --git a/interface/src/lib.rs b/interface/src/lib.rs index efd2cf06..a6a6ad0d 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -259,6 +259,12 @@ pub enum SettlementError { /// mint has to be and couldn't read the answer, so it can't size the /// buffer. BufferSizeUnavailable = 40, + /// `BeginSettle`/`FinalizeSettle`: a token account it has to move is owned + /// by a supported token program whose slot carries the system-program + /// placeholder, so there is no program to issue that transfer against. The + /// settlement has to carry every token program its accounts live under; see + /// [`token_program::TokenPrograms`]. + TokenProgramNotProvided = 41, } impl From for u32 { diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index c84b2983..aadcf942 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -1,8 +1,27 @@ -//! Utilities related to the token programs supported by the settlement program. +//! The token programs settlement transfers may be issued against. +//! +//! An instruction that moves tokens has to name the program to issue its +//! transfers against, and that program has to be one of [`TokenProgram::ALL`], +//! which is what [`TokenProgram::try_from`] resolves an address against. How it +//! names them differs by instruction: +//! +//! - `CreateBuffer` and `ReclaimBuffer` take a single `token_program` account. +//! Each works on one program's accounts at a time, so a mint under the other +//! needs its own instruction. +//! - `BeginSettle` and `FinalizeSettle` take one account per supported program, +//! described by [`TokenPrograms`], and issue each transfer against the +//! program that owns the account it moves. One settlement can therefore mix +//! tokens from both programs. use crate::Pubkey; use solana_program_error::ProgramError; +/// The program a [`TokenPrograms`] slot carries when the settlement moves no +/// token under that program. The system program is named by nearly every +/// settlement transaction already, so standing it in costs one more account +/// index rather than another 32-byte address. +pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; + /// A token program a token-moving instruction accepts. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TokenProgram { @@ -13,8 +32,9 @@ pub enum TokenProgram { } impl TokenProgram { - /// Every supported token program, in no particular order. The single list - /// [`TryFrom`] resolves addresses against. + /// Every supported token program. The single list [`TryFrom`] resolves + /// addresses against, and the order `BeginSettle` and `FinalizeSettle` lay + /// their token-program accounts out in; see [`TokenPrograms::addresses`]. pub const ALL: [Self; 2] = [Self::SplToken, Self::Token2022]; /// The address the program is deployed at. @@ -39,6 +59,80 @@ impl TryFrom<&Pubkey> for TokenProgram { } } +/// Which of [`TokenProgram::ALL`] a `BeginSettle`/`FinalizeSettle` pair +/// carries. +/// +/// Both instructions take one account per supported program, at fixed positions +/// and in [`TokenProgram::ALL`] order, and issue each transfer against the +/// program that owns the account it moves — so a single settlement may mix +/// tokens from both. A program the settlement doesn't touch is left out by +/// putting [`SYSTEM_PROGRAM_ID`] in its slot: the transfers still need their +/// program to be named by the transaction, and the placeholder says this one +/// isn't. A token account under a left-out program has nothing to be settled +/// against and is rejected. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct TokenPrograms { + /// Whether the legacy SPL Token program's slot carries the program rather + /// than the placeholder. + pub spl_token: bool, + /// Whether Token-2022's slot carries the program rather than the + /// placeholder. + pub token_2022: bool, +} + +impl TokenPrograms { + /// The legacy SPL Token program alone. + pub const SPL_TOKEN: Self = Self { + spl_token: true, + token_2022: false, + }; + + /// Token-2022 alone. + pub const TOKEN_2022: Self = Self { + spl_token: false, + token_2022: true, + }; + + /// Both programs, for a settlement mixing tokens from each. + pub const BOTH: Self = Self { + spl_token: true, + token_2022: true, + }; + + /// Neither program: every slot is the placeholder. Only a settlement that + /// moves no tokens at all can be built this way. + pub const NONE: Self = Self { + spl_token: false, + token_2022: false, + }; + + /// The addresses to pass, one per entry of [`TokenProgram::ALL`] and in + /// that order: the program itself where the settlement needs it, and + /// [`SYSTEM_PROGRAM_ID`] where it doesn't. + pub const fn addresses(self) -> [Pubkey; TokenProgram::ALL.len()] { + let [spl_token, token_2022] = TokenProgram::ALL; + [self.slot(spl_token), self.slot(token_2022)] + } + + /// The address `program`'s own slot holds. + const fn slot(self, program: TokenProgram) -> Pubkey { + if self.carries(program) { + program.address() + } else { + SYSTEM_PROGRAM_ID + } + } + + /// Whether `program`'s slot carries it rather than the placeholder. The one + /// place a new [`TokenProgram`] variant has to be given a slot. + const fn carries(self, program: TokenProgram) -> bool { + match program { + TokenProgram::SplToken => self.spl_token, + TokenProgram::Token2022 => self.token_2022, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -69,4 +163,53 @@ mod tests { Err(ProgramError::IncorrectProgramId), ); } + + /// The placeholder has to be something no token account can be owned by, + /// or a slot carrying it would still dispatch transfers somewhere. + #[test] + fn the_placeholder_is_not_a_token_program() { + assert_eq!( + TokenProgram::try_from(&SYSTEM_PROGRAM_ID), + Err(ProgramError::IncorrectProgramId), + ); + } + + /// Every combination puts each program in its own slot, and the placeholder + /// wherever the settlement said it isn't needed. + #[test] + fn addresses_fill_each_slot_with_its_program_or_the_placeholder() { + let spl_token = TokenProgram::SplToken.address(); + let token_2022 = TokenProgram::Token2022.address(); + assert_eq!(TokenPrograms::BOTH.addresses(), [spl_token, token_2022]); + assert_eq!( + TokenPrograms::SPL_TOKEN.addresses(), + [spl_token, SYSTEM_PROGRAM_ID], + ); + assert_eq!( + TokenPrograms::TOKEN_2022.addresses(), + [SYSTEM_PROGRAM_ID, token_2022], + ); + assert_eq!( + TokenPrograms::NONE.addresses(), + [SYSTEM_PROGRAM_ID, SYSTEM_PROGRAM_ID], + ); + } + + /// The slots are laid out in [`TokenProgram::ALL`] order, which is what + /// lets the on-chain side pair a slot with the program it stands for by + /// position alone. + #[test] + fn addresses_follow_the_supported_program_order() { + assert_eq!( + TokenPrograms::BOTH.addresses(), + TokenProgram::ALL.map(TokenProgram::address), + ); + } + + /// Carrying nothing is the default, so a builder that forgets its token + /// programs settles no tokens rather than silently picking one. + #[test] + fn no_program_is_carried_by_default() { + assert_eq!(TokenPrograms::default(), TokenPrograms::NONE); + } } diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 416e9b4b..3a4f5d83 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -15,9 +15,7 @@ use cow_settlement_interface::{ InstructionInputParsing, }, pda::buffer::validate_buffer_pda, - recover_discriminator, - token_program::TokenProgram, - SettlementError, SettlementInstruction, + recover_discriminator, SettlementError, SettlementInstruction, }; use pinocchio::{ cpi::Signer, @@ -33,7 +31,7 @@ use pinocchio_token::instructions::Transfer; use crate::{ processor::{check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump}, - token::{read_token_account, validate_token_program}, + token::{read_token_account, TokenPrograms}, }; use super::validate_counterpart; @@ -78,7 +76,10 @@ pub fn process_begin_settle( let finalize_ix = instructions.load_instruction_at(usize::from(input.finalize_ix_index))?; - let token_program = validate_token_program(input.token_program_account)?; + let token_programs = TokenPrograms::validate( + input.spl_token_program_account, + input.token_2022_program_account, + )?; with_state_pda_signer_from_bump(state_bump, |signer| { settle_orders( @@ -87,7 +88,7 @@ pub fn process_begin_settle( signer, &input.orders, &finalize_ix, - token_program, + &token_programs, ) }) } @@ -208,7 +209,7 @@ fn settle_orders( state_pda_signer: &Signer, orders: &SettledOrders<'_, AccountView>, finalize_ix: &IntrospectedInstruction, - token_program: TokenProgram, + token_programs: &TokenPrograms, ) -> ProgramResult { // Orders must be passed strictly increasing by address; this rejects // duplicates (settling the same order twice) without a separate scan. @@ -239,7 +240,7 @@ fn settle_orders( now, state_pda_account, state_pda_signer, - token_program, + token_programs, )?; } @@ -263,7 +264,7 @@ fn process_order( now: i64, state_account: &AccountView, state_pda_signer: &Signer, - token_program: TokenProgram, + token_programs: &TokenPrograms, ) -> ProgramResult { let SettledOrder { order_pda, @@ -301,12 +302,17 @@ fn process_order( if sell_token_account.address() != &intent.sell_token_account { return Err(SettlementError::SellTokenAccountMismatch.into()); } + // The pulls below move this account's tokens, so they are issued against + // the token program that owns it — the one this settlement has to be + // carrying. An account under neither program isn't a token account at all. + let token_program = token_programs + .program_for(sell_token_account)? + .ok_or(SettlementError::SellTokenAccountInvalid)?; // Assert the order intent owner and sell mint match those of the sell token // account. - // `read_token_account` confirms this is a real token account of the - // instruction's token program before we read its owner and mint, and reads - // by value, so nothing is left borrowing the account when the transfers - // below touch it. + // `read_token_account` confirms this is a real token account of that token + // program before we read its owner and mint, and reads by value, so nothing + // is left borrowing the account when the transfers below touch it. let sell_token = read_token_account(token_program, sell_token_account) .map_err(|_| SettlementError::SellTokenAccountInvalid)?; if sell_token.owner != intent.owner { @@ -424,7 +430,9 @@ mod tests { use cow_settlement_interface::data::intent::Flags; use cow_settlement_interface::instruction::fixtures::fake_account; use cow_settlement_interface::instruction::settle::fixtures::arb_pushes; - use cow_settlement_interface::instruction::settle::{FinalizeSettle, FinalizeSettleInput}; + use cow_settlement_interface::instruction::settle::{ + FinalizeSettle, FinalizeSettleInput, TokenPrograms, + }; use cow_settlement_interface::instruction::InstructionInputParsing; use cow_settlement_interface::Pubkey; use proptest::prelude::*; @@ -1029,6 +1037,7 @@ mod tests { program_id: Pubkey::new_from_array(program_id), state_pda: Pubkey::new_from_array(state_pda), begin_ix_index, + token_programs: TokenPrograms::BOTH, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, diff --git a/programs/settlement/src/settle/finalize.rs b/programs/settlement/src/settle/finalize.rs index 3bd2564e..b52d8f70 100644 --- a/programs/settlement/src/settle/finalize.rs +++ b/programs/settlement/src/settle/finalize.rs @@ -5,7 +5,6 @@ use cow_settlement_interface::{ settle::{FinalizeSettleInput, Pushes}, InstructionInputParsing, }, - token_program::TokenProgram, SettlementError, SettlementInstruction, }; use pinocchio::{ @@ -15,7 +14,7 @@ use pinocchio_token::instructions::Transfer; use crate::{ processor::{is_cpi_call, with_state_pda_signer}, - token::validate_token_program, + token::TokenPrograms, }; use super::validate_counterpart; @@ -48,14 +47,17 @@ 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. - let token_program = validate_token_program(input.token_program_account)?; + let token_programs = TokenPrograms::validate( + input.spl_token_program_account, + input.token_2022_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, - token_program, + &token_programs, ) }) } @@ -75,9 +77,16 @@ fn push_funds<'a>( state_pda_account: &AccountView, state_pda_signer: &Signer, pushes: Pushes<'a, AccountView>, - token_program: TokenProgram, + token_programs: &TokenPrograms, ) -> ProgramResult { for push in pushes.iter() { + // The push moves this destination's tokens, so it is issued against the + // token program that owns it — the one this settlement has to be + // carrying. An account under neither program isn't a token account at + // all. + let token_program = token_programs + .program_for(push.destination)? + .ok_or(SettlementError::PushDestinationInvalid)?; Transfer::new( push.source_buffer, push.destination, diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 25148d27..408dbcf8 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -202,6 +202,123 @@ mod tests { ); } + /// The settlement's own two slots, each holding the program it stands for. + fn both_slots() -> [AccountView; 2] { + [ + fake_account(SPL_TOKEN_PROGRAM_ID), + fake_account(TOKEN_2022_PROGRAM_ID), + ] + } + + /// A token account of `program`, well-formed but empty of interest: only + /// its owner decides which program its transfers go to. + fn token_account_of(program: Address) -> AccountView { + fake_account_owned_by(UNRELATED, program, &base_layout(UNRELATED, UNRELATED, 0)) + } + + /// A settlement carrying both programs settles accounts under either, each + /// against the program that owns it. This is what one instruction pair + /// mixing the two token programs rests on. + #[test] + fn program_for_dispatches_on_the_accounts_owner() { + let [spl_token, token_2022] = both_slots(); + let programs = + TokenPrograms::validate(&spl_token, &token_2022).expect("both slots hold a program"); + + for program in SUPPORTED_TOKEN_PROGRAMS { + assert_eq!( + programs.program_for(&token_account_of(program)), + Ok(Some(&program)), + "an account owned by {program} should be settled against it", + ); + } + } + + /// An account under neither program is no token account at all, which the + /// caller reports as whatever the account failed to be. + #[test] + fn program_for_returns_nothing_for_an_unowned_account() { + let [spl_token, token_2022] = both_slots(); + let programs = + TokenPrograms::validate(&spl_token, &token_2022).expect("both slots hold a program"); + + assert_eq!(programs.program_for(&token_account_of(UNRELATED)), Ok(None)); + } + + /// A settlement that left a program out can't reach it, so an account under + /// it is refused by name rather than mistaken for a malformed one. + #[test] + fn program_for_rejects_an_account_under_a_left_out_program() { + let placeholder = fake_account(SYSTEM_PROGRAM_ID); + for [carried, left_out] in [ + [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID], + [TOKEN_2022_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID], + ] { + let carried_account = fake_account(carried); + let (spl_token, token_2022) = if carried == SPL_TOKEN_PROGRAM_ID { + (&carried_account, &placeholder) + } else { + (&placeholder, &carried_account) + }; + let programs = + TokenPrograms::validate(spl_token, token_2022).expect("the placeholder is allowed"); + + assert_eq!( + programs.program_for(&token_account_of(left_out)), + Err(SettlementError::TokenProgramNotProvided), + "{left_out} was left out, so its accounts have nothing to settle against", + ); + // The program that *is* carried still settles its own accounts. + assert_eq!( + programs.program_for(&token_account_of(carried)), + Ok(Some(&carried)), + ); + } + } + + /// Leaving both programs out is allowed — it only makes every token account + /// unsettleable, which is exactly what a settlement moving no tokens wants. + #[test] + fn validate_accepts_two_placeholders() { + let placeholder = fake_account(SYSTEM_PROGRAM_ID); + let programs = TokenPrograms::validate(&placeholder, &placeholder) + .expect("two placeholders are allowed"); + + for program in SUPPORTED_TOKEN_PROGRAMS { + assert_eq!( + programs.program_for(&token_account_of(program)), + Err(SettlementError::TokenProgramNotProvided), + ); + } + } + + /// The slots are positional: each one holds its own program or the + /// placeholder, so the two programs can't be swapped between them. + #[test] + fn validate_rejects_swapped_slots() { + let [spl_token, token_2022] = both_slots(); + assert_eq!( + TokenPrograms::validate(&token_2022, &spl_token).err(), + Some(ProgramError::IncorrectProgramId), + ); + } + + /// Anything that is neither the slot's program nor the placeholder is a + /// caller mistake, not an opt-out. + #[test] + fn validate_rejects_an_unrelated_account_in_a_slot() { + let unrelated = fake_account(UNRELATED); + let [spl_token, token_2022] = both_slots(); + assert_eq!( + TokenPrograms::validate(&unrelated, &token_2022).err(), + Some(ProgramError::IncorrectProgramId), + ); + assert_eq!( + TokenPrograms::validate(&spl_token, &unrelated).err(), + Some(ProgramError::IncorrectProgramId), + ); + } + #[test] fn validate_token_program_accepts_every_supported_program() { for program in TokenProgram::ALL { diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 3a2965f6..bf7ffaa7 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -29,14 +29,14 @@ use crate::common::{ use cow_settlement_client::cow_settlement_interface::{ data::order::{EncodedOrderAccount, OrderAccount}, instruction::settle::{ - BeginSettle as BeginSettleRaw, FinalizeSettle as FinalizeSettleRaw, INSTRUCTIONS_SYSVAR_ID, - SPL_TOKEN_PROGRAM_ID, + BeginSettle as BeginSettleRaw, FinalizeSettle as FinalizeSettleRaw, + FINALIZE_FIXED_ACCOUNTS, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID, }, pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, Instruction, SettlementError, SettlementInstruction, }; use cow_settlement_client::instructions::{ - BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, + BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, }; use cow_settlement_interface::data::intent::OrderIntent; use litesvm::LiteSVM; @@ -137,11 +137,13 @@ fn settle_and_pay_amounts( solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders, }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &settled, }; vec![begin.into(), finalize.into()] @@ -254,6 +256,7 @@ fn rejects_fabricated_program_owned_account() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, order_pdas: &[fake_order], sell_token_accounts: &[sell_token], pulls: &no_pulls(1), @@ -264,6 +267,7 @@ fn rejects_fabricated_program_owned_account() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[unique_pubkey()], destinations: &[intent.buy_token_account], bumps: &[0], @@ -294,6 +298,7 @@ fn rejects_non_order_account_in_order_slot() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, order_pdas: &[sell_token], sell_token_accounts: &[sell_token], pulls: &no_pulls(1), @@ -303,6 +308,7 @@ fn rejects_non_order_account_in_order_slot() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[unique_pubkey()], destinations: &[unique_pubkey()], bumps: &[0], @@ -479,10 +485,10 @@ fn rejects_orders_in_wrong_address_order() { // instructions by hand in the current wire format. Begin data is // `[discriminator, finalize_ix_index (LE), order_count, transfer_count×n]` // (no transfers here) and begin accounts are `[solver, instructions_sysvar, - // state_pda, token_program, (order_pda, sell_token_account)...]`. The - // finalize's push destinations are laid out in the same decreasing order, - // so the first order's destination check passes and the second order trips - // the ordering check. + // state_pda, spl_token_program, token_2022_program, (order_pda, + // sell_token_account)...]`. The finalize's push destinations are laid out in + // the same decreasing order, so the first order's destination check passes + // and the second order trips the ordering check. let mut orders = [(first_pda, &first), (second_pda, &second)]; orders.sort_by_key(|&(pda, ..)| std::cmp::Reverse(pda)); @@ -498,8 +504,12 @@ fn rejects_orders_in_wrong_address_order() { AccountMeta::new_readonly(solver.pubkey(), true), AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), AccountMeta::new_readonly(find_state_pda(&program_id).0, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), ]; + accounts.extend( + TokenPrograms::SPL_TOKEN + .addresses() + .map(|program| AccountMeta::new_readonly(program, false)), + ); for (order_pda, intent) in orders { accounts.push(AccountMeta::new_readonly(order_pda, false)); accounts.push(AccountMeta::new(intent.sell_token_account, false)); @@ -528,6 +538,7 @@ fn rejects_orders_in_wrong_address_order() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, @@ -1076,11 +1087,12 @@ fn rejects_push_to_wrong_destination() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &orders, }); // Redirect the push to an account that isn't the order's buy token account. - // Finalize accounts: `[sysvar, state, token_program, source, destination]`. - let destination_index = 4; + // Finalize accounts: `[FINALIZE_FIXED_ACCOUNTS..., source, destination]`. + let destination_index = FINALIZE_FIXED_ACCOUNTS + 1; finalize.accounts[destination_index].pubkey = unique_pubkey(); let instructions = build_settlement(&program_id, &solver.pubkey(), &orders, finalize); @@ -1109,6 +1121,7 @@ fn rejects_push_if_buffer_does_not_match_buy_mint() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, source_buffers: &[other_buffer], destinations: &[intent.buy_token_account], bumps: &[other_bump], @@ -1135,6 +1148,7 @@ fn rejects_fewer_pushes_than_orders() { let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }; @@ -1155,6 +1169,7 @@ fn rejects_more_pushes_than_orders() { let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &[FinalizedIntent { intent: &intent, amount: 0, @@ -1180,6 +1195,7 @@ fn rejects_partial_push_amount_in_finalize_settle() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &orders, }); // Drop one byte from the finalize intstruction so the trailing amount is no diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index b8f90700..76767c19 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 cow_settlement_interface::token_program::TokenProgram; @@ -11,7 +12,7 @@ use solana_sdk::{ transaction::Transaction, }; -use super::token; +use super::{replace_first_matching_account, token}; /// The canonical buffer PDA for `mint`. pub fn buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> Pubkey { @@ -43,12 +44,17 @@ pub fn ensure_buffer_exists_for( if svm.get_account(&pda).is_some() { return pda; } - let ix = Instruction::from(CreateBuffers { + let mut ix = Instruction::from(CreateBuffers { program_id: *program_id, payer: payer.pubkey(), token_program, mints: &[*mint], }); + // A buffer is a token account of its mint, so it has to be created under the + // mint's own program. The builder can only name the legacy one, so point the + // instruction at whichever program the mint actually lives under — a no-op + // for a legacy mint. + replace_first_matching_account(&mut ix, &SPL_TOKEN_PROGRAM_ID, token::program_of(svm, mint)); let tx = Transaction::new_signed_with_payer( &[ix], Some(&payer.pubkey()), diff --git a/programs/settlement/tests/common/settlement.rs b/programs/settlement/tests/common/settlement.rs index c3065bf2..ad19f25a 100644 --- a/programs/settlement/tests/common/settlement.rs +++ b/programs/settlement/tests/common/settlement.rs @@ -1,7 +1,7 @@ //! Scaffolding for building `[BeginSettle, FinalizeSettle]` settlement pairs. use cow_settlement_client::instructions::{ - BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, + BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, }; use cow_settlement_interface::{data::intent::OrderIntent, Instruction}; use litesvm::LiteSVM; @@ -40,6 +40,7 @@ pub fn build_settlement( solver: *solver, finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &begin_orders, }; vec![begin.into(), finalize.into()] @@ -131,11 +132,13 @@ pub fn build_staged_settlement( solver: *solver, finalize_ix_index: finalize_index(between.len()), auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &begin_orders, }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &finalize_orders, }; diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index a71dd3ef..97818c2e 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -1,10 +1,19 @@ //! SPL Token helpers for the settlement integration tests. +//! +//! Every helper that acts on an existing token works under whichever token +//! program owns it, read back with [`program_of`], so a test settling +//! Token-2022 accounts uses the same calls as one settling legacy ones. Only +//! [`create_mint_under`] has to be told, there being nothing yet to read it +//! from. -use cow_settlement_client::cow_settlement_interface::pda::state::find_state_pda; +use cow_settlement_client::cow_settlement_interface::{pda::state::find_state_pda, Instruction}; use litesvm::{types::TransactionMetadata, LiteSVM}; use litesvm_token::{ - spl_token::{instruction::initialize_mint2, state::Mint}, - Approve, CreateAccount, CreateAssociatedTokenAccount, MintTo, Transfer, TOKEN_ID, + spl_token::{ + instruction::{approve, initialize_account3, initialize_mint2, mint_to as mint_to_ix}, + state::{Account, Mint}, + }, + CreateAssociatedTokenAccount, Transfer, TOKEN_ID, }; use solana_program_pack::Pack; use solana_sdk::{ @@ -16,7 +25,58 @@ use solana_system_interface::instruction::create_account as system_create_accoun use super::unique_keypair; -/// Create a fresh mint owned by `payer` and return its address. +/// The token program that owns `account`. +/// +/// A token account always lives under its mint's program, so this answers for a +/// mint and for the accounts holding it alike — which is what lets the helpers +/// below take the program from the tokens a test already built. +pub fn program_of(svm: &LiteSVM, account: &Pubkey) -> Pubkey { + svm.get_account(account) + .unwrap_or_else(|| panic!("{account} should exist on-chain")) + .owner +} + +/// Re-target a token instruction at `token_program`. +/// +/// The SPL Token builders refuse to emit an instruction for any program but +/// their own, so the helpers below build against the legacy program and re-point +/// the result. Token-2022 encodes each of these instructions exactly as the +/// legacy program does — the same fact that lets the settlement program issue +/// one transfer against either — so only the program id needs replacing. +fn under(mut instruction: Instruction, token_program: &Pubkey) -> Instruction { + instruction.program_id = *token_program; + instruction +} + +/// Submit `instructions` as one transaction signed by `payer` and `extra`. +fn send_token_tx( + svm: &mut LiteSVM, + payer: &Keypair, + extra: &[&Keypair], + instructions: &[Instruction], + what: &str, +) { + let mut signers = vec![payer]; + signers.extend_from_slice(extra); + let tx = Transaction::new_signed_with_payer( + instructions, + Some(&payer.pubkey()), + &signers, + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .unwrap_or_else(|error| panic!("{what} should succeed: {error:?}")); +} + +/// Create a fresh mint under the legacy SPL Token program, owned by `payer`, +/// and return its address. +pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { + create_mint_under(svm, payer, &TOKEN_ID) +} + +/// Create a fresh mint under `token_program`, whose mint authority is `payer`, +/// and return its address. Every later helper reads the program back off the +/// mint, so this is the only place a test names it. /// /// This open-codes what [`litesvm_token::CreateMint`] does rather than calling /// it, because that builder generates the mint keypair with `Keypair::new()` @@ -39,7 +99,7 @@ pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pub &mint.pubkey(), svm.minimum_balance_for_rent_exemption(Mint::LEN), Mint::LEN as u64, - &TOKEN_ID, + token_program, ); let initialize = initialize_mint2(&TOKEN_ID, &mint.pubkey(), &payer.pubkey(), None, DECIMALS) .expect("initialize_mint2 should build"); @@ -49,27 +109,46 @@ pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pub &[payer, mint], svm.latest_blockhash(), ); - svm.send_transaction(tx) - .expect("mint creation should succeed"); + send_token_tx(svm, payer, &[&mint], &[create, initialize], "mint creation"); mint.pubkey() } -/// 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`. +/// Create an initialized token account for `mint` whose token owner is `owner`, +/// funded by `payer`, and return its address. The account is created under +/// `mint`'s own token program. Each call produces a fresh account, so the same +/// `owner` can hold several accounts for one `mint`. +/// +/// Open-coded for the same reason as [`create_mint_under`]: the builder picks +/// the account address itself, and it would build against the legacy program +/// whatever the mint lives under. pub fn create_token_account( svm: &mut LiteSVM, payer: &Keypair, mint: &Pubkey, owner: &Pubkey, ) -> Pubkey { - CreateAccount::new(svm, payer, mint) - .owner(owner) - // Without this the builder generates the address with `Keypair::new()`; - // see [`create_mint`]. - .account_kp(unique_keypair()) - .send() - .expect("token account creation should succeed") + let token_program = program_of(svm, mint); + let account = unique_keypair(); + let create = system_create_account( + &payer.pubkey(), + &account.pubkey(), + svm.minimum_balance_for_rent_exemption(Account::LEN), + Account::LEN as u64, + &token_program, + ); + let initialize = under( + initialize_account3(&TOKEN_ID, &account.pubkey(), mint, owner) + .expect("initialize_account3 should build"), + &token_program, + ); + send_token_tx( + svm, + payer, + &[&account], + &[create, initialize], + "token account creation", + ); + account.pubkey() } /// Create `owner`'s associated token account for `mint`, funded by `payer`, and @@ -96,9 +175,13 @@ pub fn mint_to( destination: &Pubkey, amount: u64, ) { - MintTo::new(svm, payer, mint, destination, amount) - .send() - .expect("mint_to should succeed"); + let token_program = program_of(svm, mint); + let instruction = under( + mint_to_ix(&TOKEN_ID, mint, destination, &payer.pubkey(), &[], amount) + .expect("mint_to should build"), + &token_program, + ); + send_token_tx(svm, payer, &[], &[instruction], "mint_to"); } /// Transfer `amount` of `mint` from `owner`'s associated token account into @@ -124,9 +207,13 @@ pub fn delegate( delegate: &Pubkey, amount: u64, ) { - Approve::new(svm, owner, delegate, source, amount) - .send() - .expect("approving a delegate should succeed"); + let token_program = program_of(svm, source); + let instruction = under( + approve(&TOKEN_ID, source, delegate, &owner.pubkey(), &[], amount) + .expect("approve should build"), + &token_program, + ); + send_token_tx(svm, owner, &[], &[instruction], "approving a delegate"); } /// Fund `sell_token` with `amount` of its mint and approve the settlement state @@ -149,7 +236,8 @@ pub fn fund_and_delegate( ); } -/// Read the SPL token balance of `account`. +/// Read the token balance of `account`. The two programs share the base layout +/// this reads, so it answers for an account under either. pub fn balance(svm: &LiteSVM, account: &Pubkey) -> u64 { litesvm_token::get_spl_account::(svm, account) .expect("account should exist and be a valid SPL token account") diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index daa291f3..6393b9ef 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -21,7 +21,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::intent::OrderIntent, instruction::settle::SPL_TOKEN_PROGRAM_ID, pda::state::find_state_pda, Instruction, SettlementError, }; -use cow_settlement_client::instructions::{FinalizeSettle, FinalizedIntent}; +use cow_settlement_client::instructions::{FinalizeSettle, FinalizedIntent, TokenPrograms}; use litesvm_token::spl_token::error::TokenError; use solana_sdk::{ instruction::InstructionError, program_error::ProgramError, pubkey::Pubkey, signer::Signer, @@ -45,6 +45,7 @@ fn finalize(program_id: &Pubkey, solver: &Pubkey, orders: &[FinalizedIntent]) -> let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders, }; build_settlement(program_id, solver, orders, finalize) @@ -253,6 +254,7 @@ fn rejects_push_account_count_mismatch() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &orders, }); // ...with another push's worth of data bytes appended but no matching @@ -278,9 +280,10 @@ fn rejects_too_few_accounts() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }); - // ...with one of its three fixed accounts popped. `BeginSettle` runs first + // ...with one of its fixed accounts popped. `BeginSettle` runs first // but only reads push destinations off the accounts (finding none, matching // its zero orders) so it passes. The finalize then can't even destructure // its fixed accounts and raises `NotEnoughAccountKeys`. @@ -301,6 +304,9 @@ fn rejects_too_few_accounts() { ); } +/// An account that isn't a token account at all is owned by no token program, +/// so the push has nothing to be issued against and `FinalizeSettle` says so +/// itself rather than handing the transfer to a token program. #[test] fn rejects_invalid_buy_token_account() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); @@ -319,7 +325,7 @@ fn rejects_invalid_buy_token_account() { let instructions = finalize(&program_id, &solver.pubkey(), &orders); assert_finalize_error( send(&mut svm, &solver, instructions), - InstructionError::InvalidAccountData, + to_instruction_error(SettlementError::PushDestinationInvalid), ); } @@ -334,9 +340,9 @@ fn rejects_buy_token_account_owned_by_wrong_program() { .data; let impostor = create_account(&mut svm, &unique_pubkey(), &token_shaped); - // As above, the impostor passes both instructions' checks (the push pays - // `intent.buy_token_account` from `intent.buy_mint`'s buffer) and is left - // for the SPL token program, which rejects a destination it doesn't own. + // As above, the impostor passes both instructions' push checks (the push + // pays `intent.buy_token_account` from `intent.buy_mint`'s buffer), but its + // owner is no token program, so there is nothing to issue the push against. let intent = OrderIntent { buy_token_account: impostor, ..settlable @@ -351,7 +357,7 @@ fn rejects_buy_token_account_owned_by_wrong_program() { let instructions = finalize(&program_id, &solver.pubkey(), &orders); assert_finalize_error( send(&mut svm, &solver, instructions), - InstructionError::IncorrectProgramId, + to_instruction_error(SettlementError::PushDestinationInvalid), ); } @@ -372,6 +378,7 @@ fn rejects_two_too_few_accounts() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &orders, }); // ...with that push's whole (source, destination) pair popped, so the data @@ -401,6 +408,7 @@ fn rejects_partial_push_amount() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::SPL_TOKEN, orders: &orders, }); // Drop one byte so the trailing amount is no longer a whole `u64`. diff --git a/programs/settlement/tests/matching_begin_finalize.rs b/programs/settlement/tests/matching_begin_finalize.rs index 958cea0c..df4e3d66 100644 --- a/programs/settlement/tests/matching_begin_finalize.rs +++ b/programs/settlement/tests/matching_begin_finalize.rs @@ -1,5 +1,5 @@ use cow_settlement_client::cow_settlement_interface::{SettlementError, SettlementInstruction}; -use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle}; +use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle, TokenPrograms}; use litesvm::{types::FailedTransactionMetadata, LiteSVM}; use solana_sdk::{ instruction::{AccountMeta, Instruction, InstructionError}, @@ -40,12 +40,14 @@ fn run_sequence( solver: solver.pubkey(), finalize_ix_index: *idx, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], } .into(), AbstractInstruction::Fin(idx) => FinalizeSettle { program_id: *program_id, begin_ix_index: *idx, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], } .into(), @@ -193,6 +195,7 @@ fn rejects_non_instructions_sysvar_account_at_position_one() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], } .into(); @@ -200,6 +203,7 @@ fn rejects_non_instructions_sysvar_account_at_position_one() { let finalize = FinalizeSettle { program_id, begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }; @@ -231,6 +235,7 @@ fn rejects_counterpart_instruction_in_different_program() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }; // We build a transaction that looks like a valid finalize_settle but @@ -239,6 +244,7 @@ fn rejects_counterpart_instruction_in_different_program() { let stranger = FinalizeSettle { program_id: solana_system_interface::program::ID, begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }; @@ -293,6 +299,7 @@ fn rejects_cpi_call_to_begin_settle() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }, ); @@ -325,6 +332,7 @@ fn rejects_cpi_call_to_finalize_settle() { FinalizeSettle { program_id: settlement_id, begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }, ); @@ -358,6 +366,7 @@ fn rejects_counterpart_with_unrecoverable_discriminator() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }; // Uses the settlement program, but no data: `recover_discriminator` fails @@ -400,6 +409,7 @@ fn rejects_counterpart_with_unrecoverable_counterpart_index() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], }; // Same program as `begin`, with a valid discriminator but no trailing diff --git a/programs/settlement/tests/program_deployment.rs b/programs/settlement/tests/program_deployment.rs index bbd3a8d6..e7779d67 100644 --- a/programs/settlement/tests/program_deployment.rs +++ b/programs/settlement/tests/program_deployment.rs @@ -1,4 +1,4 @@ -use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle}; +use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle, TokenPrograms}; use solana_sdk::{ instruction::{Instruction, InstructionError}, signature::Signer, @@ -34,12 +34,14 @@ fn program_can_be_invoked() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], } .into(), FinalizeSettle { program_id, begin_ix_index: 0, + token_programs: TokenPrograms::SPL_TOKEN, orders: &[], } .into(), diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs new file mode 100644 index 00000000..6306d920 --- /dev/null +++ b/programs/settlement/tests/settle_token_programs.rs @@ -0,0 +1,429 @@ +//! Integration tests for the token-program slots a `BeginSettle` / +//! `FinalizeSettle` pair carries. +//! +//! Both instructions take one account per supported token program and issue +//! each transfer against the program that owns the account it moves, so a +//! single pair can settle legacy SPL Token and Token-2022 orders together. A +//! program the settlement doesn't need is left out by putting the system +//! program in its slot; a token account under a left-out program then has +//! nothing to be settled against. + +use crate::common::{ + assert_settlement_error, buffer, + order::OrderBuilder, + settlement::{BEGIN_INDEX, FINALIZE_INDEX}, + setup, token, unique_pubkey, +}; +use cow_settlement_client::cow_settlement_interface::{ + data::intent::OrderIntent, + token_program::{SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID}, + Instruction, SettlementError, +}; +use cow_settlement_client::instructions::{ + BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, +}; +use litesvm::LiteSVM; +use solana_sdk::{ + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, +}; + +mod common; + +/// What each order in a settlement sells and buys: `amount_in` of its sell +/// token pulled out, `amount_out` of its buy token pushed in. +struct Settled<'a> { + intent: &'a OrderIntent, + amount_in: u64, + amount_out: u64, +} + +/// Fund and settle `orders` in one `[BeginSettle, FinalizeSettle]` pair, with +/// each instruction carrying the token-program slots it is given. +/// +/// Every account involved is set up under its own mint's program, so the only +/// thing a test varies is which programs the settlement says it carries. +fn settle_with( + svm: &mut LiteSVM, + program_id: &Pubkey, + payer: &Keypair, + orders: &[Settled], + begin_programs: TokenPrograms, + finalize_programs: TokenPrograms, +) -> Result<(), TransactionError> { + let mut initialized: Vec = vec![]; + let mut finalized: Vec = vec![]; + for order in orders { + let intent = order.intent; + // Sell side: fund the account and delegate the pull to the state PDA, + // then pull into a throwaway account of the same mint. + token::fund_and_delegate( + svm, + program_id, + payer, + &intent.sell_token_account, + order.amount_in, + ); + let sell_mint = token::mint_of(svm, &intent.sell_token_account); + let destination = token::create_token_account(svm, payer, &sell_mint, &unique_pubkey()); + let pulls: &[Pull] = Box::leak(Box::new([Pull { + destination, + amount: order.amount_in, + }])); + initialized.push(InitializedIntent { intent, pulls }); + + // Buy side: fund the buffer so the push has something to draw from. + let buy_mint = token::mint_of(svm, &intent.buy_token_account); + buffer::ensure_funded(svm, program_id, payer, &buy_mint, order.amount_out); + finalized.push(FinalizedIntent { + intent, + mint: buy_mint, + amount: order.amount_out, + }); + } + + let begin = BeginSettle { + program_id: *program_id, + finalize_ix_index: FINALIZE_INDEX.into(), + auction_id: 0, + token_programs: begin_programs, + orders: &initialized, + }; + let finalize = FinalizeSettle { + program_id: *program_id, + begin_ix_index: BEGIN_INDEX.into(), + token_programs: finalize_programs, + orders: &finalized, + }; + let tx = Transaction::new_signed_with_payer( + &[begin.into(), finalize.into()], + Some(&payer.pubkey()), + &[payer], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .map(|_| ()) + .map_err(|error| error.err) +} + +/// An order selling a token under `sell_program` and buying one under +/// `buy_program`, priced 1:1 and partially fillable. +fn order_across( + svm: &mut LiteSVM, + program_id: &Pubkey, + payer: &Keypair, + salt: u8, + sell_program: &Pubkey, + buy_program: &Pubkey, +) -> OrderIntent { + let sell_mint = token::create_mint_under(svm, payer, sell_program); + let buy_mint = token::create_mint_under(svm, payer, buy_program); + let intent = OrderBuilder::new(svm, program_id, payer) + .salt(salt) + .sell_mint(&sell_mint) + .buy_mint(&buy_mint) + .sell_amount(1_000) + .buy_amount(1_000) + .build(); + // The order's accounts have to have landed under the programs asked for, or + // a test meant to settle Token-2022 would quietly be settling legacy tokens. + assert_eq!( + token::program_of(svm, &intent.sell_token_account), + *sell_program, + ); + assert_eq!( + token::program_of(svm, &intent.buy_token_account), + *buy_program, + ); + intent +} + +/// The headline capability: one settlement pair moving tokens under both +/// programs, each transfer issued against the program that owns the account. +#[test] +fn settles_orders_under_both_token_programs() { + let (mut svm, program_id, payer) = setup(); + + let legacy = order_across( + &mut svm, + &program_id, + &payer, + 0, + &SPL_TOKEN_PROGRAM_ID, + &SPL_TOKEN_PROGRAM_ID, + ); + let token_2022 = order_across( + &mut svm, + &program_id, + &payer, + 1, + &TOKEN_2022_PROGRAM_ID, + &TOKEN_2022_PROGRAM_ID, + ); + + settle_with( + &mut svm, + &program_id, + &payer, + &[ + Settled { + intent: &legacy, + amount_in: 400, + amount_out: 400, + }, + Settled { + intent: &token_2022, + amount_in: 700, + amount_out: 700, + }, + ], + TokenPrograms::BOTH, + TokenPrograms::BOTH, + ) + .expect("a settlement carrying both programs should settle orders under either"); + + assert_eq!(token::balance(&svm, &legacy.buy_token_account), 400); + assert_eq!(token::balance(&svm, &token_2022.buy_token_account), 700); + // Both sell sides were drained by their own program's transfer. + assert_eq!(token::balance(&svm, &legacy.sell_token_account), 0); + assert_eq!(token::balance(&svm, &token_2022.sell_token_account), 0); +} + +/// The two sides of one order need not share a program: the pull follows the +/// sell account's owner and the push the buy account's, independently. +#[test] +fn settles_an_order_that_crosses_token_programs() { + let (mut svm, program_id, payer) = setup(); + + let intent = order_across( + &mut svm, + &program_id, + &payer, + 0, + &SPL_TOKEN_PROGRAM_ID, + &TOKEN_2022_PROGRAM_ID, + ); + + settle_with( + &mut svm, + &program_id, + &payer, + &[Settled { + intent: &intent, + amount_in: 250, + amount_out: 250, + }], + TokenPrograms::BOTH, + TokenPrograms::BOTH, + ) + .expect("an order selling under one program and buying under the other should settle"); + + assert_eq!(token::balance(&svm, &intent.buy_token_account), 250); + assert_eq!(token::balance(&svm, &intent.sell_token_account), 0); +} + +/// A settlement that carries only Token-2022 still settles Token-2022 orders: +/// the legacy slot holding the placeholder costs it nothing it needs. +#[test] +fn settles_token_2022_orders_without_carrying_the_legacy_program() { + let (mut svm, program_id, payer) = setup(); + + let intent = order_across( + &mut svm, + &program_id, + &payer, + 0, + &TOKEN_2022_PROGRAM_ID, + &TOKEN_2022_PROGRAM_ID, + ); + + settle_with( + &mut svm, + &program_id, + &payer, + &[Settled { + intent: &intent, + amount_in: 300, + amount_out: 300, + }], + TokenPrograms::TOKEN_2022, + TokenPrograms::TOKEN_2022, + ) + .expect("a Token-2022-only settlement should settle Token-2022 orders"); + + assert_eq!(token::balance(&svm, &intent.buy_token_account), 300); +} + +/// `BeginSettle` pulls from the sell account, so leaving that account's program +/// out is what it refuses — by name, rather than as a malformed account. +#[test] +fn rejects_a_sell_account_under_a_left_out_program() { + let (mut svm, program_id, payer) = setup(); + + let intent = order_across( + &mut svm, + &program_id, + &payer, + 0, + &TOKEN_2022_PROGRAM_ID, + &SPL_TOKEN_PROGRAM_ID, + ); + + assert_settlement_error( + BEGIN_INDEX, + settle_with( + &mut svm, + &program_id, + &payer, + &[Settled { + intent: &intent, + amount_in: 100, + amount_out: 100, + }], + TokenPrograms::SPL_TOKEN, + TokenPrograms::SPL_TOKEN, + ), + SettlementError::TokenProgramNotProvided, + ); +} + +/// `FinalizeSettle` pushes into the buy account, so it is the one that refuses +/// a settlement whose slots leave that account's program out. `BeginSettle` +/// runs first and passes: it only pulls, and this order's sell side is legacy. +#[test] +fn rejects_a_buy_account_under_a_left_out_program() { + let (mut svm, program_id, payer) = setup(); + + let intent = order_across( + &mut svm, + &program_id, + &payer, + 0, + &SPL_TOKEN_PROGRAM_ID, + &TOKEN_2022_PROGRAM_ID, + ); + + assert_settlement_error( + FINALIZE_INDEX, + settle_with( + &mut svm, + &program_id, + &payer, + &[Settled { + intent: &intent, + amount_in: 100, + amount_out: 100, + }], + TokenPrograms::BOTH, + TokenPrograms::SPL_TOKEN, + ), + SettlementError::TokenProgramNotProvided, + ); +} + +/// The slots are positional. Handing each one the other's program isn't a way +/// to carry both: each slot takes its own program or the placeholder, nothing +/// else. +#[test] +fn rejects_swapped_token_program_slots() { + let (mut svm, program_id, payer) = setup(); + + let intent = order_across( + &mut svm, + &program_id, + &payer, + 0, + &SPL_TOKEN_PROGRAM_ID, + &SPL_TOKEN_PROGRAM_ID, + ); + token::fund_and_delegate( + &mut svm, + &program_id, + &payer, + &intent.sell_token_account, + 100, + ); + let sell_mint = token::mint_of(&svm, &intent.sell_token_account); + let buy_mint = token::mint_of(&svm, &intent.buy_token_account); + buffer::ensure_funded(&mut svm, &program_id, &payer, &buy_mint, 100); + let destination = token::create_token_account(&mut svm, &payer, &sell_mint, &unique_pubkey()); + + let pulls = [Pull { + destination, + amount: 100, + }]; + let mut begin = Instruction::from(BeginSettle { + program_id, + finalize_ix_index: FINALIZE_INDEX.into(), + auction_id: 0, + token_programs: TokenPrograms::BOTH, + orders: &[InitializedIntent { + intent: &intent, + pulls: &pulls, + }], + }); + // `BeginSettle`'s accounts are `[sysvar, state, spl_token, token_2022, ...]`, + // so exchanging the two slots leaves both programs present but each in the + // other's position. + begin.accounts.swap(2, 3); + let finalize = FinalizeSettle { + program_id, + begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::BOTH, + orders: &[FinalizedIntent { + intent: &intent, + mint: buy_mint, + amount: 100, + }], + }; + + let tx = Transaction::new_signed_with_payer( + &[begin, finalize.into()], + Some(&payer.pubkey()), + &[&payer], + svm.latest_blockhash(), + ); + let error = svm + .send_transaction(tx) + .expect_err("swapped slots should be rejected") + .err; + assert_eq!( + error, + TransactionError::InstructionError(BEGIN_INDEX, InstructionError::IncorrectProgramId), + ); +} + +/// Every settlement in the rest of the suite leaves Token-2022's slot empty, so +/// the placeholder has to be accepted for a legacy-only settlement — and it is +/// only the accounts under the left-out program that become unsettleable. +#[test] +fn accepts_the_placeholder_for_a_legacy_only_settlement() { + let (mut svm, program_id, payer) = setup(); + + let intent = order_across( + &mut svm, + &program_id, + &payer, + 0, + &SPL_TOKEN_PROGRAM_ID, + &SPL_TOKEN_PROGRAM_ID, + ); + + settle_with( + &mut svm, + &program_id, + &payer, + &[Settled { + intent: &intent, + amount_in: 500, + amount_out: 500, + }], + TokenPrograms::SPL_TOKEN, + TokenPrograms::SPL_TOKEN, + ) + .expect("a legacy-only settlement should not have to carry Token-2022"); + + assert_eq!(token::balance(&svm, &intent.buy_token_account), 500); +} diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 9efb0b86..ddd6ea96 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -9,6 +9,7 @@ use cow_settlement_client::{ }, instructions::{ BeginSettle, CreateBuffers, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, + TokenPrograms, }, }; use solana_hash::Hash; @@ -116,6 +117,9 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { program_id: ctx.program_id, solver, finalize_ix_index, + // Token resolution builds legacy SPL accounts throughout (see + // `crate::token`), so Token-2022's slot stays empty. + token_programs: TokenPrograms::SPL_TOKEN, orders: &initialized_intents, auction_id: 0, }; @@ -132,6 +136,7 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { let finalize_ix = FinalizeSettle { program_id: ctx.program_id, begin_ix_index, + token_programs: TokenPrograms::SPL_TOKEN, orders: &settled, }; From ce87d3907f1a31e2581597c41ae3e5825960c29f Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:00:17 +0900 Subject: [PATCH 03/38] fix errors in compilation --- client/src/instructions.rs | 4 +- client/src/parse.rs | 2 +- interface/src/lib.rs | 4 + programs/settlement/idl/cow_settlement.json | 10 ++ programs/settlement/src/token.rs | 130 +++++++++++++++--- programs/settlement/tests/common/buffer.rs | 18 +-- programs/settlement/tests/common/mod.rs | 18 ++- programs/settlement/tests/common/token.rs | 46 ++++--- .../settlement/tests/common/token_2022.rs | 15 +- programs/settlement/tests/reclaim_buffer.rs | 2 +- .../settlement/tests/settle_limit_prices.rs | 15 +- .../settlement/tests/settle_solver_auth.rs | 5 +- .../settlement/tests/settle_token_programs.rs | 75 +++++----- 13 files changed, 226 insertions(+), 118 deletions(-) diff --git a/client/src/instructions.rs b/client/src/instructions.rs index c21339d5..8291435e 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -318,12 +318,12 @@ mod tests { instruction::{ fixtures::fake_account_from_array, settle::{ - BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, - SPL_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, + BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID, }, InstructionInputParsing, }, pda::order::find_order_pda, + token_program::SYSTEM_PROGRAM_ID, }; proptest! { diff --git a/client/src/parse.rs b/client/src/parse.rs index 6f7d7e48..d0526877 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -79,7 +79,7 @@ mod tests { use super::*; use crate::instructions::{ AddSolver, BeginSettle, CreateBuffers, CreateOrder, FinalizeSettle, Initialize, - InitializedIntent, RemoveSolver, + InitializedIntent, RemoveSolver, TokenPrograms, }; use cow_settlement_interface::{ data::intent::fixtures::sample_intent, diff --git a/interface/src/lib.rs b/interface/src/lib.rs index a6a6ad0d..b5382c31 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -265,6 +265,10 @@ pub enum SettlementError { /// settlement has to carry every token program its accounts live under; see /// [`token_program::TokenPrograms`]. TokenProgramNotProvided = 41, + /// `FinalizeSettle`: a push's destination isn't owned by a supported token + /// program, so it is no token account at all and there is nothing to issue + /// its transfer against. + PushDestinationInvalid = 42, } impl From for u32 { diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json index 48c19b6c..8bd47cf9 100644 --- a/programs/settlement/idl/cow_settlement.json +++ b/programs/settlement/idl/cow_settlement.json @@ -1052,6 +1052,16 @@ "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." + }, + { + "code": 41, + "name": "TokenProgramNotProvided", + "msg": "BeginSettle/FinalizeSettle: a token account it has to move is owned by a supported token program whose slot carries the system-program placeholder, so there is no program to issue that transfer against. The settlement has to carry every token program its accounts live under; see token_program::TokenPrograms." + }, + { + "code": 42, + "name": "PushDestinationInvalid", + "msg": "FinalizeSettle: a push's destination isn't owned by a supported token program, so it is no token account at all and there is nothing to issue its transfer against." } ] } diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 408dbcf8..c6379adb 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -1,6 +1,9 @@ //! Token-program validation and token-account reads -use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; +use cow_settlement_interface::{ + token_program::{TokenProgram, SYSTEM_PROGRAM_ID}, + SettlementError, +}; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView, Address}; use pinocchio_token::instructions::GetAccountDataSize; @@ -17,6 +20,87 @@ pub fn validate_token_program( TokenProgram::try_from(token_program_account.address()) } +/// The token programs a `BeginSettle`/`FinalizeSettle` was handed: one slot per +/// entry of [`TokenProgram::ALL`], holding either that program or the system +/// program standing in for "this settlement moves no token under it". +/// +/// Built by [`TokenPrograms::validate`] and asked, per account, +/// [`which program owns it`](TokenPrograms::program_for). The instruction issues +/// that account's transfers against the answer, which is what lets a single +/// settlement mix tokens from both programs. +pub struct TokenPrograms { + /// Whether the legacy SPL Token program's slot held the program rather than + /// the placeholder. + spl_token: bool, + /// Token-2022's slot, filled the same way. + token_2022: bool, +} + +impl TokenPrograms { + /// Validate a settlement's two token-program slots, in the order the + /// instruction lays them out. + /// + /// Each slot has to hold either the program it stands for or + /// [`SYSTEM_PROGRAM_ID`]; anything else is a caller mistake rather than an + /// opt-out. A slot holding its program is also what puts that program in the + /// transaction, which is what makes the transfers' CPIs dispatchable at all. + #[must_use = "the returned slots decide which program each transfer targets"] + pub fn validate( + spl_token_account: &AccountView, + token_2022_account: &AccountView, + ) -> Result { + Ok(Self { + spl_token: validate_slot(spl_token_account, TokenProgram::SplToken)?, + token_2022: validate_slot(token_2022_account, TokenProgram::Token2022)?, + }) + } + + /// The token program `account`'s transfers must be issued against: the one + /// that owns it. + /// + /// `None` when `account` isn't owned by a supported token program, which + /// means it is no token account at all and the caller reports it as whatever + /// it failed to be. An account owned by a supported program whose slot held + /// the placeholder is a different matter: the settlement can't reach that + /// program, so it says so with [`SettlementError::TokenProgramNotProvided`] + /// rather than pretending the account is malformed. + pub fn program_for( + &self, + account: &AccountView, + ) -> Result, SettlementError> { + let Ok(owner) = TokenProgram::try_from(account.owner()) else { + return Ok(None); + }; + if self.carries(owner) { + Ok(Some(owner)) + } else { + Err(SettlementError::TokenProgramNotProvided) + } + } + + /// Whether `program`'s slot held it rather than the placeholder. The one + /// place a new [`TokenProgram`] variant has to be given a slot. + fn carries(&self, program: TokenProgram) -> bool { + match program { + TokenProgram::SplToken => self.spl_token, + TokenProgram::Token2022 => self.token_2022, + } + } +} + +/// Whether `program`'s slot holds it rather than the placeholder, rejecting an +/// address that is neither. +fn validate_slot(account: &AccountView, program: TokenProgram) -> Result { + let address = account.address(); + if address == &program.address() { + Ok(true) + } else if address == &SYSTEM_PROGRAM_ID { + Ok(false) + } else { + Err(ProgramError::IncorrectProgramId) + } +} + /// The data length a token account holding `mint` has to be allocated at. pub fn token_account_len( token_program: TokenProgram, @@ -204,16 +288,17 @@ mod tests { /// The settlement's own two slots, each holding the program it stands for. fn both_slots() -> [AccountView; 2] { - [ - fake_account(SPL_TOKEN_PROGRAM_ID), - fake_account(TOKEN_2022_PROGRAM_ID), - ] + TokenProgram::ALL.map(|program| fake_account(program.address())) } /// A token account of `program`, well-formed but empty of interest: only /// its owner decides which program its transfers go to. fn token_account_of(program: Address) -> AccountView { - fake_account_owned_by(UNRELATED, program, &base_layout(UNRELATED, UNRELATED, 0)) + fake_account_owned_by( + pubkey_from_seed("token account"), + program, + &base_account_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 0), + ) } /// A settlement carrying both programs settles accounts under either, each @@ -225,11 +310,11 @@ mod tests { let programs = TokenPrograms::validate(&spl_token, &token_2022).expect("both slots hold a program"); - for program in SUPPORTED_TOKEN_PROGRAMS { + for program in TokenProgram::ALL { assert_eq!( - programs.program_for(&token_account_of(program)), - Ok(Some(&program)), - "an account owned by {program} should be settled against it", + programs.program_for(&token_account_of(program.address())), + Ok(Some(program)), + "an account owned by {program:?} should be settled against it", ); } } @@ -242,7 +327,8 @@ mod tests { let programs = TokenPrograms::validate(&spl_token, &token_2022).expect("both slots hold a program"); - assert_eq!(programs.program_for(&token_account_of(UNRELATED)), Ok(None)); + let unrelated = pubkey_from_seed("not a token program"); + assert_eq!(programs.program_for(&token_account_of(unrelated)), Ok(None)); } /// A settlement that left a program out can't reach it, so an account under @@ -251,11 +337,11 @@ mod tests { fn program_for_rejects_an_account_under_a_left_out_program() { let placeholder = fake_account(SYSTEM_PROGRAM_ID); for [carried, left_out] 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 carried_account = fake_account(carried); - let (spl_token, token_2022) = if carried == SPL_TOKEN_PROGRAM_ID { + let carried_account = fake_account(carried.address()); + let (spl_token, token_2022) = if carried == TokenProgram::SplToken { (&carried_account, &placeholder) } else { (&placeholder, &carried_account) @@ -264,14 +350,14 @@ mod tests { TokenPrograms::validate(spl_token, token_2022).expect("the placeholder is allowed"); assert_eq!( - programs.program_for(&token_account_of(left_out)), + programs.program_for(&token_account_of(left_out.address())), Err(SettlementError::TokenProgramNotProvided), - "{left_out} was left out, so its accounts have nothing to settle against", + "{left_out:?} was left out, so its accounts have nothing to settle against", ); // The program that *is* carried still settles its own accounts. assert_eq!( - programs.program_for(&token_account_of(carried)), - Ok(Some(&carried)), + programs.program_for(&token_account_of(carried.address())), + Ok(Some(carried)), ); } } @@ -284,9 +370,9 @@ mod tests { let programs = TokenPrograms::validate(&placeholder, &placeholder) .expect("two placeholders are allowed"); - for program in SUPPORTED_TOKEN_PROGRAMS { + for program in TokenProgram::ALL { assert_eq!( - programs.program_for(&token_account_of(program)), + programs.program_for(&token_account_of(program.address())), Err(SettlementError::TokenProgramNotProvided), ); } @@ -307,7 +393,7 @@ mod tests { /// caller mistake, not an opt-out. #[test] fn validate_rejects_an_unrelated_account_in_a_slot() { - let unrelated = fake_account(UNRELATED); + let unrelated = fake_account(pubkey_from_seed("not a token program")); let [spl_token, token_2022] = both_slots(); assert_eq!( TokenPrograms::validate(&unrelated, &token_2022).err(), diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index 76767c19..54b6f480 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 cow_settlement_interface::token_program::TokenProgram; @@ -12,7 +11,7 @@ use solana_sdk::{ transaction::Transaction, }; -use super::{replace_first_matching_account, token}; +use super::token; /// The canonical buffer PDA for `mint`. pub fn buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> Pubkey { @@ -22,13 +21,19 @@ pub fn buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> Pubkey { /// Create the canonical buffer for `mint`, paid for by `payer`, unless it /// already exists, and return its address. Idempotent so several orders can /// share one buy mint. +/// +/// A buffer is a token account of its mint, so it is created under whichever +/// program owns the mint; [`ensure_buffer_exists_for`] is for the tests that +/// name a program of their own instead. pub fn ensure_buffer_exists( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, mint: &Pubkey, ) -> Pubkey { - ensure_buffer_exists_for(svm, program_id, payer, mint, TokenProgram::SplToken) + let token_program = TokenProgram::try_from(&token::program_of(svm, mint)) + .expect("a mint lives under a supported token program"); + ensure_buffer_exists_for(svm, program_id, payer, mint, token_program) } /// [`ensure_buffer_exists`] under a token program of the caller's choosing, for @@ -44,17 +49,12 @@ pub fn ensure_buffer_exists_for( if svm.get_account(&pda).is_some() { return pda; } - let mut ix = Instruction::from(CreateBuffers { + let ix = Instruction::from(CreateBuffers { program_id: *program_id, payer: payer.pubkey(), token_program, mints: &[*mint], }); - // A buffer is a token account of its mint, so it has to be created under the - // mint's own program. The builder can only name the legacy one, so point the - // instruction at whichever program the mint actually lives under — a no-op - // for a legacy mint. - replace_first_matching_account(&mut ix, &SPL_TOKEN_PROGRAM_ID, token::program_of(svm, mint)); let tx = Transaction::new_signed_with_payer( &[ix], Some(&payer.pubkey()), diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index 995cc7ba..b694a27c 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -17,7 +17,6 @@ pub mod token_2022; 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}; @@ -36,10 +35,6 @@ 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" @@ -207,6 +202,19 @@ pub fn assert_instruction_error_at( ); } +/// Convenience wrapper around [`assert_instruction_error_at`] for asserting a +/// specific [`SettlementError`] at the instruction that produced it: settlements +/// run as a `[BeginSettle, FinalizeSettle]` pair, so the failing instruction +/// isn't always the first. +#[track_caller] +pub fn assert_settlement_error( + ix_idx: u8, + result: Result, + expected: SettlementError, +) { + assert_instruction_error_at(ix_idx, result, to_instruction_error(expected)); +} + pub fn create_account_at(svm: &mut LiteSVM, address: Pubkey, owner: &Pubkey, data: &[u8]) { let lamports = svm.minimum_balance_for_rent_exemption(data.len()); svm.set_account( diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 97818c2e..46aca1ce 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -74,23 +74,34 @@ pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { create_mint_under(svm, payer, &TOKEN_ID) } -/// Create a fresh mint under `token_program`, whose mint authority is `payer`, -/// and return its address. Every later helper reads the program back off the -/// mint, so this is the only place a test names it. +/// [`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 { + create_mint_at_under(svm, payer, mint, &TOKEN_ID) +} + +/// [`create_mint`] under `token_program` rather than the legacy program, for +/// the tests that build mints under both at once. +pub fn create_mint_under(svm: &mut LiteSVM, payer: &Keypair, token_program: &Pubkey) -> Pubkey { + create_mint_at_under(svm, payer, &unique_keypair(), token_program) +} + +/// Create a mint at `mint`'s address under `token_program`, whose mint authority +/// is `payer`, and return its address. Every later helper reads the program back +/// off the mint, so the wrappers above are the only place a test names it. /// /// This open-codes what [`litesvm_token::CreateMint`] does rather than calling /// it, because that builder generates the mint keypair with `Keypair::new()` /// internally and offers no way to supply one. A mint address is a seed of its /// 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 { +fn create_mint_at_under( + svm: &mut LiteSVM, + payer: &Keypair, + mint: &Keypair, + token_program: &Pubkey, +) -> Pubkey { /// `litesvm_token::CreateMint`'s default, kept so the two agree. const DECIMALS: u8 = 8; @@ -101,15 +112,12 @@ pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pub Mint::LEN as u64, token_program, ); - let initialize = initialize_mint2(&TOKEN_ID, &mint.pubkey(), &payer.pubkey(), None, DECIMALS) - .expect("initialize_mint2 should build"); - let tx = Transaction::new_signed_with_payer( - &[create, initialize], - Some(&payer.pubkey()), - &[payer, mint], - svm.latest_blockhash(), + let initialize = under( + initialize_mint2(&TOKEN_ID, &mint.pubkey(), &payer.pubkey(), None, DECIMALS) + .expect("initialize_mint2 should build"), + token_program, ); - send_token_tx(svm, payer, &[&mint], &[create, initialize], "mint creation"); + send_token_tx(svm, payer, &[mint], &[create, initialize], "mint creation"); mint.pubkey() } diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index 284b5e06..92eb744a 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -24,9 +24,6 @@ use spl_token_2022_interface::{ 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; @@ -122,15 +119,15 @@ impl Extensions { .map(|extension| { match extension { ExtensionType::MintCloseAuthority => initialize_mint_close_authority( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), mint, Some(authority), ), ExtensionType::NonTransferable => { - initialize_non_transferable_mint(&TOKEN_2022_PROGRAM_ID, mint) + initialize_non_transferable_mint(&TokenProgram::Token2022.address(), mint) } ExtensionType::TransferFeeConfig => initialize_transfer_fee_config( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), mint, Some(authority), Some(authority), @@ -162,12 +159,12 @@ pub fn create_mint( &mint.pubkey(), svm.minimum_balance_for_rent_exemption(space), space as u64, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), )]; instructions.extend(extensions.initializers(&mint.pubkey(), &payer.pubkey())); instructions.push( initialize_mint2( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), &mint.pubkey(), &payer.pubkey(), None, @@ -193,7 +190,7 @@ pub fn create_mint( /// to claim again. pub fn close_mint(svm: &mut LiteSVM, payer: &Keypair, mint: &Pubkey) { let ix = close_account( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), mint, &payer.pubkey(), &payer.pubkey(), diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index fd4a51b5..5050f602 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -459,7 +459,7 @@ fn reclaims_a_buffer_whose_mint_was_reopened_as_a_legacy_mint() { svm.get_account(&mint) .expect("the reopened mint should exist") .owner, - common::SPL_TOKEN_PROGRAM_ID, + TokenProgram::SplToken.address(), "sanity: the mint must now belong to the legacy program" ); diff --git a/programs/settlement/tests/settle_limit_prices.rs b/programs/settlement/tests/settle_limit_prices.rs index fbd39d29..a54ce96e 100644 --- a/programs/settlement/tests/settle_limit_prices.rs +++ b/programs/settlement/tests/settle_limit_prices.rs @@ -7,7 +7,7 @@ //! succeeds or is rejected with the expected error. use crate::common::{ - assert_instruction_error_at, + assert_settlement_error, order::OrderBuilder, send, settlement::{build_staged_settlement, stage_order, StagedOrder, BEGIN_INDEX}, @@ -28,19 +28,6 @@ use solana_sdk::{ mod common; -/// Convenience wrapper around [`assert_instruction_error_at`] for asserting a -/// specific [`SettlementError`] at the instruction that produced it: settlements -/// run as a `[BeginSettle, FinalizeSettle]` pair, so the failing instruction -/// isn't always the first. -#[track_caller] -fn assert_settlement_error( - ix_idx: u8, - result: Result, - expected: SettlementError, -) { - assert_instruction_error_at(ix_idx, result, to_instruction_error(expected)); -} - /// Read `intent`'s order PDA and return its persisted `(amount_withdrawn, /// amount_received)` cumulative fill totals. fn order_fill(svm: &LiteSVM, program_id: &Pubkey, intent: &OrderIntent) -> (u64, u64) { diff --git a/programs/settlement/tests/settle_solver_auth.rs b/programs/settlement/tests/settle_solver_auth.rs index 82e87428..20396ec8 100644 --- a/programs/settlement/tests/settle_solver_auth.rs +++ b/programs/settlement/tests/settle_solver_auth.rs @@ -4,7 +4,7 @@ //! unauthorized caller is rejected before any settlement work happens. use cow_settlement_client::cow_settlement_interface::{Instruction, SettlementError}; -use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle}; +use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle, TokenPrograms}; use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; use crate::common::{ @@ -24,11 +24,13 @@ fn noop_settlement(program_id: &Pubkey, solver: &Pubkey) -> Vec { solver: *solver, finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, + token_programs: TokenPrograms::NONE, orders: &[], }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::NONE, orders: &[], }; vec![begin.into(), finalize.into()] @@ -99,6 +101,7 @@ fn non_signing_solver_may_not_settle() { solver: solver.pubkey(), finalize_ix_index: 0, auction_id: 0, + token_programs: TokenPrograms::NONE, orders: &[], } .into(); diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index 6306d920..33202eaf 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -12,12 +12,10 @@ use crate::common::{ assert_settlement_error, buffer, order::OrderBuilder, settlement::{BEGIN_INDEX, FINALIZE_INDEX}, - setup, token, unique_pubkey, + setup_settle_ready, token, unique_pubkey, }; use cow_settlement_client::cow_settlement_interface::{ - data::intent::OrderIntent, - token_program::{SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID}, - Instruction, SettlementError, + data::intent::OrderIntent, token_program::TokenProgram, Instruction, SettlementError, }; use cow_settlement_client::instructions::{ BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, @@ -49,6 +47,7 @@ fn settle_with( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, + solver: &Keypair, orders: &[Settled], begin_programs: TokenPrograms, finalize_programs: TokenPrograms, @@ -79,13 +78,13 @@ fn settle_with( buffer::ensure_funded(svm, program_id, payer, &buy_mint, order.amount_out); finalized.push(FinalizedIntent { intent, - mint: buy_mint, amount: order.amount_out, }); } let begin = BeginSettle { program_id: *program_id, + solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, token_programs: begin_programs, @@ -100,7 +99,7 @@ fn settle_with( let tx = Transaction::new_signed_with_payer( &[begin.into(), finalize.into()], Some(&payer.pubkey()), - &[payer], + &[payer, solver], svm.latest_blockhash(), ); svm.send_transaction(tx) @@ -144,29 +143,30 @@ fn order_across( /// programs, each transfer issued against the program that owns the account. #[test] fn settles_orders_under_both_token_programs() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let legacy = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::SplToken.address(), ); let token_2022 = order_across( &mut svm, &program_id, &payer, 1, - &TOKEN_2022_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), + &TokenProgram::Token2022.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[ Settled { intent: &legacy, @@ -195,21 +195,22 @@ fn settles_orders_under_both_token_programs() { /// sell account's owner and the push the buy account's, independently. #[test] fn settles_an_order_that_crosses_token_programs() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::Token2022.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 250, @@ -228,21 +229,22 @@ fn settles_an_order_that_crosses_token_programs() { /// the legacy slot holding the placeholder costs it nothing it needs. #[test] fn settles_token_2022_orders_without_carrying_the_legacy_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &TOKEN_2022_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), + &TokenProgram::Token2022.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 300, @@ -260,15 +262,15 @@ fn settles_token_2022_orders_without_carrying_the_legacy_program() { /// out is what it refuses — by name, rather than as a malformed account. #[test] fn rejects_a_sell_account_under_a_left_out_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &TOKEN_2022_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::Token2022.address(), + &TokenProgram::SplToken.address(), ); assert_settlement_error( @@ -277,6 +279,7 @@ fn rejects_a_sell_account_under_a_left_out_program() { &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 100, @@ -294,15 +297,15 @@ fn rejects_a_sell_account_under_a_left_out_program() { /// runs first and passes: it only pulls, and this order's sell side is legacy. #[test] fn rejects_a_buy_account_under_a_left_out_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::Token2022.address(), ); assert_settlement_error( @@ -311,6 +314,7 @@ fn rejects_a_buy_account_under_a_left_out_program() { &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 100, @@ -328,15 +332,15 @@ fn rejects_a_buy_account_under_a_left_out_program() { /// else. #[test] fn rejects_swapped_token_program_slots() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::SplToken.address(), ); token::fund_and_delegate( &mut svm, @@ -356,6 +360,7 @@ fn rejects_swapped_token_program_slots() { }]; let mut begin = Instruction::from(BeginSettle { program_id, + solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, token_programs: TokenPrograms::BOTH, @@ -364,17 +369,16 @@ fn rejects_swapped_token_program_slots() { pulls: &pulls, }], }); - // `BeginSettle`'s accounts are `[sysvar, state, spl_token, token_2022, ...]`, - // so exchanging the two slots leaves both programs present but each in the - // other's position. - begin.accounts.swap(2, 3); + // `BeginSettle`'s accounts are `[solver, sysvar, state, spl_token, + // token_2022, ...]`, so exchanging the two slots leaves both programs + // present but each in the other's position. + begin.accounts.swap(3, 4); let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), token_programs: TokenPrograms::BOTH, orders: &[FinalizedIntent { intent: &intent, - mint: buy_mint, amount: 100, }], }; @@ -382,7 +386,7 @@ fn rejects_swapped_token_program_slots() { let tx = Transaction::new_signed_with_payer( &[begin, finalize.into()], Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); let error = svm @@ -400,21 +404,22 @@ fn rejects_swapped_token_program_slots() { /// only the accounts under the left-out program that become unsettleable. #[test] fn accepts_the_placeholder_for_a_legacy_only_settlement() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::SplToken.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 500, From 6ecb26c5aacd605827ae247641d9bc0b426c2e88 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:08:59 +0900 Subject: [PATCH 04/38] feat: Save CU by removing parsing/validation of the token program account args (#144) Co-authored-by: Claude Opus 5 (1M context) --- client/src/instructions.rs | 20 +-- client/src/parse.rs | 2 +- interface/src/instruction/create_buffer.rs | 12 +- interface/src/instruction/reclaim_buffer.rs | 10 +- interface/src/instruction/settle/begin.rs | 27 +-- interface/src/instruction/settle/finalize.rs | 20 +-- interface/src/lib.rs | 10 +- interface/src/token_program.rs | 23 +-- programs/settlement/idl/cow_settlement.json | 13 +- programs/settlement/src/create_buffer.rs | 20 +-- programs/settlement/src/reclaim_buffer.rs | 25 +-- programs/settlement/src/settle/begin.rs | 20 +-- programs/settlement/src/settle/finalize.rs | 25 +-- programs/settlement/src/token.rs | 156 +++++------------- .../settlement/tests/begin_settle_orders.rs | 35 +--- programs/settlement/tests/common/buffer.rs | 18 +- programs/settlement/tests/common/mod.rs | 18 +- programs/settlement/tests/common/token.rs | 64 ++++--- .../settlement/tests/common/token_2022.rs | 15 +- programs/settlement/tests/create_buffer.rs | 26 +-- .../tests/finalize_settle_pushes.rs | 7 +- programs/settlement/tests/reclaim_buffer.rs | 7 +- .../settlement/tests/settle_limit_prices.rs | 15 +- .../settlement/tests/settle_solver_auth.rs | 5 +- .../settlement/tests/settle_token_programs.rs | 137 ++++++++------- 25 files changed, 313 insertions(+), 417 deletions(-) diff --git a/client/src/instructions.rs b/client/src/instructions.rs index c21339d5..1d8116ec 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -318,12 +318,12 @@ mod tests { instruction::{ fixtures::fake_account_from_array, settle::{ - BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, - SPL_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, + BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID, }, InstructionInputParsing, }, pda::order::find_order_pda, + token_program::SYSTEM_PROGRAM_ID, }; proptest! { @@ -457,16 +457,12 @@ mod tests { ); let (state_pda, _bump) = find_state_pda(&program_id); prop_assert_eq!(parsed.state_pda_account.address(), &state_pda); - prop_assert_eq!( - parsed.spl_token_program_account.address(), - &SPL_TOKEN_PROGRAM_ID, - ); - // These settlements are legacy-only, so Token-2022's slot stands - // empty. - prop_assert_eq!( - parsed.token_2022_program_account.address(), - &SYSTEM_PROGRAM_ID, - ); + // The token-program slots aren't parsed, so the instruction's own + // account list is where they are checked: the legacy program in its + // own slot, and — these settlements being legacy-only — the + // placeholder in Token-2022's. + prop_assert_eq!(ix.accounts[2].pubkey, SPL_TOKEN_PROGRAM_ID); + prop_assert_eq!(ix.accounts[3].pubkey, SYSTEM_PROGRAM_ID); let parsed_pushes: Vec<_> = parsed.pushes.iter().collect(); prop_assert_eq!(parsed_pushes.len(), expected.len()); diff --git a/client/src/parse.rs b/client/src/parse.rs index 6f7d7e48..d0526877 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -79,7 +79,7 @@ mod tests { use super::*; use crate::instructions::{ AddSolver, BeginSettle, CreateBuffers, CreateOrder, FinalizeSettle, Initialize, - InitializedIntent, RemoveSolver, + InitializedIntent, RemoveSolver, TokenPrograms, }; use cow_settlement_interface::{ data::intent::fixtures::sample_intent, diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index 976d94d8..e017196c 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -76,7 +76,6 @@ pub struct BufferAccounts<'a, A> { /// Parsed inputs of a `CreateBuffer` instruction. pub struct CreateBufferInput<'a, A> { pub payer: &'a A, - pub token_program: &'a A, buffer_pairs: &'a [[A; 2]], } @@ -98,10 +97,11 @@ impl<'a, A> InstructionInputParsing<'a, A> for CreateBufferInput<'a, A> { } // Accounts: [payer (W,S), system_program (R), token_program (R), // (buffer_pda (W), mint (R))...]. The three shared accounts come first; - // the per-buffer pairs follow, one pair per buffer. The system program - // needs to be present for the `CreateAccount` CPI but isn't dereferenced - // here. - let [payer, _system, token_program, rest @ ..] = accounts else { + // the per-buffer pairs follow, one pair per buffer. Neither program is + // dereferenced here: they need to be present for the `CreateAccount` + // and `InitializeAccount3` CPIs to dispatch, and each buffer's program + // is the one that owns its mint. + let [payer, _system, _token_program, rest @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; // Group the trailing accounts into `[buffer_pda, mint]` pairs. Each @@ -116,7 +116,6 @@ impl<'a, A> InstructionInputParsing<'a, A> for CreateBufferInput<'a, A> { Ok(Self { payer, - token_program, buffer_pairs: buffers, }) } @@ -186,7 +185,6 @@ mod tests { let input = CreateBufferInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(*input.payer.address(), payer); - assert_eq!(*input.token_program.address(), token_program); let buffers: Vec<_> = input.buffers().collect(); assert_eq!(buffers.len(), 1, "one buffer is one (pda, mint) pair"); assert_eq!(*buffers[0].buffer_pda.address(), buffer_pda); diff --git a/interface/src/instruction/reclaim_buffer.rs b/interface/src/instruction/reclaim_buffer.rs index ad400f43..e62c472f 100644 --- a/interface/src/instruction/reclaim_buffer.rs +++ b/interface/src/instruction/reclaim_buffer.rs @@ -72,7 +72,6 @@ pub struct ReclaimBufferInput<'a, A> { pub state_pda: &'a A, pub reclaim_authority: &'a A, pub reclaim_recipient: &'a A, - pub token_program: &'a A, /// One `[buffer_pda, mint]` pair per buffer to close. pub buffers: &'a [[A; 2]], } @@ -87,8 +86,10 @@ impl<'a, A> InstructionInputParsing<'a, A> for ReclaimBufferInput<'a, A> { // Accounts: [state_pda (R), reclaim_authority (R,S), reclaim_recipient // (W), token_program (R), (buffer_pda (W), mint (R))...]. The four // shared accounts come first; the per-buffer pairs follow, one pair per - // buffer. - let [state_pda, reclaim_authority, reclaim_recipient, token_program, rest @ ..] = accounts + // buffer. The token program is skipped rather than read: each buffer is + // closed by the program that owns it, so the account is only there to + // put that program in the transaction. + let [state_pda, reclaim_authority, reclaim_recipient, _token_program, rest @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -105,7 +106,6 @@ impl<'a, A> InstructionInputParsing<'a, A> for ReclaimBufferInput<'a, A> { state_pda, reclaim_authority, reclaim_recipient, - token_program, buffers, }) } @@ -183,14 +183,12 @@ mod tests { state_pda: parsed_state_pda, reclaim_authority: parsed_reclaim_authority, reclaim_recipient: parsed_reclaim_recipient, - token_program: parsed_token_program, buffers, } = ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(*parsed_state_pda.address(), state_pda); assert_eq!(*parsed_reclaim_authority.address(), reclaim_authority); assert_eq!(*parsed_reclaim_recipient.address(), reclaim_recipient); - assert_eq!(*parsed_token_program.address(), token_program); assert_eq!(buffers.len(), 1, "one buffer is one pair"); assert_eq!(*buffers[0][0].address(), buffer_pda); assert_eq!(*buffers[0][1].address(), mint); diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 76ce8382..22200e82 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -37,8 +37,10 @@ pub struct Pull { /// spl_token_program (R), token_2022_program (R)]` followed, per order, by /// `[order_pda (W), sell_token_account (W), destination (W)...]`. The two token /// programs are the slots [`TokenPrograms`] describes: each transfer is issued -/// against the program that owns the account it moves, and a program this -/// settlement doesn't touch is left out with the system program. +/// against the program that owns the account it moves, so the slots are there +/// to name those programs — a CPI can only dispatch to a program the +/// instruction names. A program this settlement doesn't touch is left out with +/// the system program. /// /// `solver` must sign, and the solver must be registered in the state pda. /// @@ -214,11 +216,6 @@ pub struct BeginSettleInput<'a, A> { pub solver_account: &'a A, pub instructions_sysvar_account: &'a A, pub state_pda_account: &'a A, - /// The legacy SPL Token program's slot: the program itself, or the - /// placeholder where this settlement moves no token under it. - pub spl_token_program_account: &'a A, - /// Token-2022's slot, filled the same way. - pub token_2022_program_account: &'a A, pub orders: SettledOrders<'a, A>, } @@ -231,7 +228,11 @@ impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> { fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result { let (finalize_ix_index, body) = recover_counterpart(instruction_data)?; - let [solver_account, instructions_sysvar_account, state_pda_account, spl_token_program_account, token_2022_program_account, order_accounts @ ..] = + // The two token-program slots are skipped rather than read: every + // transfer is issued against the program that owns the account it + // moves, so naming the programs is all the slots do. They still take up + // their positions, which is what the order accounts are counted from. + let [solver_account, instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, order_accounts @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -285,8 +286,6 @@ impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> { auction_id, instructions_sysvar_account, state_pda_account, - spl_token_program_account, - token_2022_program_account, solver_account, orders: SettledOrders { order_accounts, @@ -577,16 +576,12 @@ mod tests { solver_account, instructions_sysvar_account, orders, - spl_token_program_account, - token_2022_program_account, state_pda_account, } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(finalize_ix_index, 0x1337); assert_eq!(auction_id, 0x0102_0304_0506_0708); assert_eq!(instructions_sysvar_account.address(), &sysvar); assert_eq!(orders.iter().count(), 0); - assert_eq!(spl_token_program_account.address(), &spl_token_program); - assert_eq!(token_2022_program_account.address(), &token_2022_program); assert_eq!(state_pda_account.address(), &state); assert_eq!(solver_account.address(), &solver); } @@ -665,14 +660,10 @@ mod tests { instructions_sysvar_account, orders, state_pda_account, - spl_token_program_account, - token_2022_program_account, } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(finalize_ix_index, 0x1337); assert_eq!(auction_id, AUCTION_ID); assert_eq!(instructions_sysvar_account.address(), &sysvar); - assert_eq!(spl_token_program_account.address(), &spl_token_program); - assert_eq!(token_2022_program_account.address(), &token_2022_program); assert_eq!(state_pda_account.address(), &state); assert_eq!(solver_account.address(), &solver); diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index 58d4ae89..d7e3d29c 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -83,7 +83,8 @@ pub fn finalize_push_data( /// `[instructions_sysvar (R), state_pda (R), spl_token_program (R), /// token_2022_program (R)]` followed, per push, by `[source_buffer (W), /// destination (W)]`. The two token programs are the slots [`TokenPrograms`] -/// describes; the matching `BeginSettle` carries the same ones. +/// describes, there to name the programs this instruction's pushes are issued +/// against; the matching `BeginSettle` carries the ones its pulls need. /// /// `FinalizeSettle` only executes the transfers. Every push is validated by /// `BeginSettle`, which reads this instruction through introspection. @@ -208,11 +209,6 @@ pub struct FinalizeSettleInput<'a, A> { pub begin_ix_index: u16, pub instructions_sysvar_account: &'a A, pub state_pda_account: &'a A, - /// The legacy SPL Token program's slot: the program itself, or the - /// placeholder where this settlement moves no token under it. - pub spl_token_program_account: &'a A, - /// Token-2022's slot, filled the same way. - pub token_2022_program_account: &'a A, pub pushes: Pushes<'a, A>, } @@ -225,7 +221,11 @@ impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> { fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result { let (begin_ix_index, body) = recover_counterpart(instruction_data)?; - let [instructions_sysvar_account, state_pda_account, spl_token_program_account, token_2022_program_account, push_accounts @ ..] = + // The two token-program slots are skipped rather than read: every push + // is issued against the program that owns its destination, so naming + // the programs is all the slots do. They still take up their positions, + // which is what the push accounts are counted from. + let [instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, push_accounts @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -247,8 +247,6 @@ impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> { begin_ix_index, instructions_sysvar_account, state_pda_account, - spl_token_program_account, - token_2022_program_account, pushes: Pushes { push_accounts, bumps, @@ -437,15 +435,11 @@ mod tests { begin_ix_index, instructions_sysvar_account, state_pda_account, - spl_token_program_account, - token_2022_program_account, pushes, } = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed"); assert_eq!(begin_ix_index, 0x1337); assert_eq!(instructions_sysvar_account.address(), &sysvar); assert_eq!(state_pda_account.address(), &state); - assert_eq!(spl_token_program_account.address(), &spl_token_program); - assert_eq!(token_2022_program_account.address(), &token_2022_program); assert_eq!(pushes.iter().count(), 0); } diff --git a/interface/src/lib.rs b/interface/src/lib.rs index a6a6ad0d..1b9b2aed 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -259,12 +259,10 @@ pub enum SettlementError { /// mint has to be and couldn't read the answer, so it can't size the /// buffer. BufferSizeUnavailable = 40, - /// `BeginSettle`/`FinalizeSettle`: a token account it has to move is owned - /// by a supported token program whose slot carries the system-program - /// placeholder, so there is no program to issue that transfer against. The - /// settlement has to carry every token program its accounts live under; see - /// [`token_program::TokenPrograms`]. - TokenProgramNotProvided = 41, + /// `FinalizeSettle`: a push's destination isn't owned by a supported token + /// program, so it is no token account at all and there is nothing to issue + /// its transfer against. + PushDestinationInvalid = 41, } impl From for u32 { diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index aadcf942..9672a33a 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -1,13 +1,16 @@ //! The token programs settlement transfers may be issued against. //! //! An instruction that moves tokens has to name the program to issue its -//! transfers against, and that program has to be one of [`TokenProgram::ALL`], -//! which is what [`TokenProgram::try_from`] resolves an address against. How it -//! names them differs by instruction: +//! transfers against — a CPI can only dispatch to a program its instruction +//! names — and the program it targets is the one owning the account it moves, +//! which [`TokenProgram::try_from`] resolves from that account's owner. Naming +//! is all the accounts below do; none of them is read on-chain. How an +//! instruction names them differs: //! //! - `CreateBuffer` and `ReclaimBuffer` take a single `token_program` account. -//! Each works on one program's accounts at a time, so a mint under the other -//! needs its own instruction. +//! Each buffer is created under, and closed by, the program owning its mint, +//! so a mint under the program the instruction didn't name needs its own +//! instruction. //! - `BeginSettle` and `FinalizeSettle` take one account per supported program, //! described by [`TokenPrograms`], and issue each transfer against the //! program that owns the account it moves. One settlement can therefore mix @@ -65,11 +68,11 @@ impl TryFrom<&Pubkey> for TokenProgram { /// Both instructions take one account per supported program, at fixed positions /// and in [`TokenProgram::ALL`] order, and issue each transfer against the /// program that owns the account it moves — so a single settlement may mix -/// tokens from both. A program the settlement doesn't touch is left out by -/// putting [`SYSTEM_PROGRAM_ID`] in its slot: the transfers still need their -/// program to be named by the transaction, and the placeholder says this one -/// isn't. A token account under a left-out program has nothing to be settled -/// against and is rejected. +/// tokens from both. The slots are what name those programs; they are not read +/// on-chain, and a program the settlement doesn't touch is left out by putting +/// [`SYSTEM_PROGRAM_ID`] in its slot. A transfer of a token account under a +/// left-out program then has no program to dispatch to, and the runtime refuses +/// the instruction. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct TokenPrograms { /// Whether the legacy SPL Token program's slot carries the program rather diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json index 48c19b6c..954eee85 100644 --- a/programs/settlement/idl/cow_settlement.json +++ b/programs/settlement/idl/cow_settlement.json @@ -86,7 +86,7 @@ "name": "create_buffer", "docs": [ "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.", + "Every buffer is created under the token program that owns its mint, and the instruction names a single token_program for those CPIs to dispatch to, 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\"]." ], @@ -106,7 +106,7 @@ { "name": "token_program", "docs": [ - "The token program that will own the created buffer PDAs. Must be one of the supported token accounts." + "The token program the created buffer PDAs are initialized through. Must be one of the supported token programs, and the one owning every mint this instruction is handed." ] }, { @@ -333,7 +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.", + "Every buffer is closed by the token program that owns it, and the instruction names a single token_program for those CPIs to dispatch to, 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." ], @@ -392,7 +392,7 @@ { "name": "token_program", "docs": [ - "The token program that owns the created buffer PDAs. Must be one of the supported token accounts." + "The token program the buffer PDAs are closed through. Must be one of the supported token programs, and the one owning every buffer this instruction is handed." ] }, { @@ -1052,6 +1052,11 @@ "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." + }, + { + "code": 41, + "name": "PushDestinationInvalid", + "msg": "FinalizeSettle: a push's destination isn't owned by a supported token program, so it is no token account at all and there is nothing to issue its transfer against." } ] } diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/create_buffer.rs index fd9318ff..d0e86372 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/create_buffer.rs @@ -12,7 +12,7 @@ use pinocchio_token::instructions::InitializeAccount3; use crate::{ processor::CanonicalPda, - token::{token_account_len, validate_token_program}, + token::{owning_token_program, token_account_len}, }; pub fn process_create_buffer( @@ -22,12 +22,6 @@ pub fn process_create_buffer( ) -> ProgramResult { let input = CreateBufferInput::parse(instruction_data, accounts)?; - // 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)?; - 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. let (state_pda, _) = Address::find_program_address(&state_pda_seeds(), program_id); @@ -39,9 +33,15 @@ pub fn process_create_buffer( // is a token account, so it's assigned to the token program rather than // to the settlement program. // - // 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. + // A buffer belongs to the same program as the mint it holds, so the + // mint's owner is what says which program to allocate it to and + // initialize it with. That is also the only check `mint` needs here: + // `InitializeAccount3` requires a real mint of that program (and + // special-cases the native mint), so a check of our own would be + // redundant. + let token_program = owning_token_program(mint)?; + let token_program_id = token_program.address(); + let mint_key = mint.address().as_array(); let (created, _) = CanonicalPda { program_id, diff --git a/programs/settlement/src/reclaim_buffer.rs b/programs/settlement/src/reclaim_buffer.rs index 151e6d53..33c1659d 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/reclaim_buffer.rs @@ -16,7 +16,7 @@ use pinocchio_token::instructions::CloseAccount; use crate::{ processor::with_state_pda_signer, - token::{read_token_account, validate_token_program}, + token::{owning_token_program, read_token_account}, }; pub fn process_reclaim_buffer( @@ -28,13 +28,9 @@ pub fn process_reclaim_buffer( state_pda, reclaim_authority, reclaim_recipient, - token_program, buffers, } = 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 = StateAccount::from_account(state_pda)?.authority(Role::ReclaimAuthority); @@ -51,6 +47,9 @@ pub fn process_reclaim_buffer( return Err(SettlementError::ReclaimBufferNotCanonical.into()); } + // A buffer is closed by the program that owns it, which is the one + // that created it in the first place. + let token_program = owning_token_program(buffer_pda)?; 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 @@ -63,7 +62,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_id, + &token_program.address(), )?; } @@ -101,7 +100,6 @@ mod tests { // Positions within [`base_accounts`], for the tests that swap one entry. const STATE_PDA: usize = 0; const RECLAIM_AUTHORITY: usize = 1; - const TOKEN_PROGRAM: usize = 3; const BUFFER_PDA: usize = 4; /// State account bytes for planting a well-formed state PDA in tests. @@ -179,10 +177,14 @@ mod tests { .unwrap_or_else(|err| panic!("reclaim buffer happy path should succeed: {err}")); } + /// The buffer's own owner is what says which program closes it, so one + /// owned by neither token program is refused: there is nothing to close it + /// with. #[test] - fn process_reclaim_buffer_rejects_wrong_token_program() { + fn process_reclaim_buffer_rejects_a_buffer_under_an_unrelated_program() { let mut accounts = base_accounts(); - accounts[TOKEN_PROGRAM] = fake_account(UNRELATED); + let buffer_pda = *accounts[BUFFER_PDA].address(); + accounts[BUFFER_PDA] = fake_account_owned_by(buffer_pda, UNRELATED, &[]); assert_rejects(accounts, ProgramError::IncorrectProgramId); } @@ -243,6 +245,9 @@ mod tests { assert_rejects(accounts, SettlementError::ReclaimBufferNotCanonical.into()); } + /// A buffer that was never created is owned by the system program, so it + /// is refused as an account no token program can close rather than read as + /// a malformed token account. #[test] fn process_reclaim_buffer_rejects_uninitialized_buffer_pda() { let mut accounts = base_accounts(); @@ -250,6 +255,6 @@ mod tests { let buffer_pda = *accounts[BUFFER_PDA].address(); accounts[BUFFER_PDA] = fake_account(buffer_pda); - assert_rejects(accounts, ProgramError::InvalidAccountData); + assert_rejects(accounts, ProgramError::IncorrectProgramId); } } diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 3a4f5d83..a52aabe7 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -31,7 +31,7 @@ use pinocchio_token::instructions::Transfer; use crate::{ processor::{check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump}, - token::{read_token_account, TokenPrograms}, + token::{owning_token_program, read_token_account}, }; use super::validate_counterpart; @@ -76,11 +76,6 @@ pub fn process_begin_settle( let finalize_ix = instructions.load_instruction_at(usize::from(input.finalize_ix_index))?; - let token_programs = TokenPrograms::validate( - input.spl_token_program_account, - input.token_2022_program_account, - )?; - with_state_pda_signer_from_bump(state_bump, |signer| { settle_orders( program_id, @@ -88,7 +83,6 @@ pub fn process_begin_settle( signer, &input.orders, &finalize_ix, - &token_programs, ) }) } @@ -209,7 +203,6 @@ fn settle_orders( state_pda_signer: &Signer, orders: &SettledOrders<'_, AccountView>, finalize_ix: &IntrospectedInstruction, - token_programs: &TokenPrograms, ) -> ProgramResult { // Orders must be passed strictly increasing by address; this rejects // duplicates (settling the same order twice) without a separate scan. @@ -240,7 +233,6 @@ fn settle_orders( now, state_pda_account, state_pda_signer, - token_programs, )?; } @@ -264,7 +256,6 @@ fn process_order( now: i64, state_account: &AccountView, state_pda_signer: &Signer, - token_programs: &TokenPrograms, ) -> ProgramResult { let SettledOrder { order_pda, @@ -303,11 +294,10 @@ fn process_order( return Err(SettlementError::SellTokenAccountMismatch.into()); } // The pulls below move this account's tokens, so they are issued against - // the token program that owns it — the one this settlement has to be - // carrying. An account under neither program isn't a token account at all. - let token_program = token_programs - .program_for(sell_token_account)? - .ok_or(SettlementError::SellTokenAccountInvalid)?; + // the token program that owns it. An account under neither program isn't a + // token account at all. + let token_program = owning_token_program(sell_token_account) + .map_err(|_| SettlementError::SellTokenAccountInvalid)?; // Assert the order intent owner and sell mint match those of the sell token // account. // `read_token_account` confirms this is a real token account of that token diff --git a/programs/settlement/src/settle/finalize.rs b/programs/settlement/src/settle/finalize.rs index b52d8f70..7005b512 100644 --- a/programs/settlement/src/settle/finalize.rs +++ b/programs/settlement/src/settle/finalize.rs @@ -14,7 +14,7 @@ use pinocchio_token::instructions::Transfer; use crate::{ processor::{is_cpi_call, with_state_pda_signer}, - token::TokenPrograms, + token::owning_token_program, }; use super::validate_counterpart; @@ -47,18 +47,8 @@ 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. - let token_programs = TokenPrograms::validate( - input.spl_token_program_account, - input.token_2022_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, - &token_programs, - ) + push_funds(input.state_pda_account, state_pda_signer, input.pushes) }) } @@ -77,16 +67,13 @@ fn push_funds<'a>( state_pda_account: &AccountView, state_pda_signer: &Signer, pushes: Pushes<'a, AccountView>, - token_programs: &TokenPrograms, ) -> ProgramResult { for push in pushes.iter() { // The push moves this destination's tokens, so it is issued against the - // token program that owns it — the one this settlement has to be - // carrying. An account under neither program isn't a token account at - // all. - let token_program = token_programs - .program_for(push.destination)? - .ok_or(SettlementError::PushDestinationInvalid)?; + // token program that owns it. An account under neither program isn't a + // token account at all. + let token_program = owning_token_program(push.destination) + .map_err(|_| SettlementError::PushDestinationInvalid)?; Transfer::new( push.source_buffer, push.destination, diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs index 408dbcf8..b6355651 100644 --- a/programs/settlement/src/token.rs +++ b/programs/settlement/src/token.rs @@ -1,4 +1,4 @@ -//! Token-program validation and token-account reads +//! Token-program dispatch and token-account reads use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView, Address}; @@ -8,13 +8,21 @@ use pinocchio_token::instructions::GetAccountDataSize; /// 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 the program for the instruction to target. -#[must_use = "not consuming skips validation"] -pub fn validate_token_program( - token_program_account: &AccountView, -) -> Result { - TokenProgram::try_from(token_program_account.address()) +/// The token program that owns `account`, and so the one every transfer of its +/// tokens has to be issued against. +/// +/// Reading the owner is what lets one instruction move tokens under either +/// program without being told which: the account itself says. An account under +/// anything else is no token account at all, and there is nothing to issue a +/// transfer against. +/// +/// The program the answer names still has to be one of the calling +/// instruction's own accounts, or the CPI issued against it has nothing to +/// dispatch to. Naming it is the caller's job, and the runtime is what enforces +/// it. +#[must_use = "not consuming skips the owner check"] +pub fn owning_token_program(account: &AccountView) -> Result { + TokenProgram::try_from(account.owner()) } /// The data length a token account holding `mint` has to be allocated at. @@ -202,34 +210,26 @@ mod tests { ); } - /// The settlement's own two slots, each holding the program it stands for. - fn both_slots() -> [AccountView; 2] { - [ - fake_account(SPL_TOKEN_PROGRAM_ID), - fake_account(TOKEN_2022_PROGRAM_ID), - ] - } - /// A token account of `program`, well-formed but empty of interest: only /// its owner decides which program its transfers go to. fn token_account_of(program: Address) -> AccountView { - fake_account_owned_by(UNRELATED, program, &base_layout(UNRELATED, UNRELATED, 0)) + fake_account_owned_by( + pubkey_from_seed("token account"), + program, + &base_account_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 0), + ) } - /// A settlement carrying both programs settles accounts under either, each - /// against the program that owns it. This is what one instruction pair - /// mixing the two token programs rests on. + /// Every token account dispatches to the program that owns it. This is what + /// one instruction moving tokens under both programs rests on: nothing has + /// to tell it which, each account already says. #[test] - fn program_for_dispatches_on_the_accounts_owner() { - let [spl_token, token_2022] = both_slots(); - let programs = - TokenPrograms::validate(&spl_token, &token_2022).expect("both slots hold a program"); - - for program in SUPPORTED_TOKEN_PROGRAMS { + fn owning_token_program_dispatches_on_the_accounts_owner() { + for program in TokenProgram::ALL { assert_eq!( - programs.program_for(&token_account_of(program)), - Ok(Some(&program)), - "an account owned by {program} should be settled against it", + owning_token_program(&token_account_of(program.address())), + Ok(program), + "an account owned by {program:?} should be settled against it", ); } } @@ -237,101 +237,21 @@ mod tests { /// An account under neither program is no token account at all, which the /// caller reports as whatever the account failed to be. #[test] - fn program_for_returns_nothing_for_an_unowned_account() { - let [spl_token, token_2022] = both_slots(); - let programs = - TokenPrograms::validate(&spl_token, &token_2022).expect("both slots hold a program"); - - assert_eq!(programs.program_for(&token_account_of(UNRELATED)), Ok(None)); - } - - /// A settlement that left a program out can't reach it, so an account under - /// it is refused by name rather than mistaken for a malformed one. - #[test] - fn program_for_rejects_an_account_under_a_left_out_program() { - let placeholder = fake_account(SYSTEM_PROGRAM_ID); - for [carried, left_out] in [ - [SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID], - [TOKEN_2022_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID], - ] { - let carried_account = fake_account(carried); - let (spl_token, token_2022) = if carried == SPL_TOKEN_PROGRAM_ID { - (&carried_account, &placeholder) - } else { - (&placeholder, &carried_account) - }; - let programs = - TokenPrograms::validate(spl_token, token_2022).expect("the placeholder is allowed"); - - assert_eq!( - programs.program_for(&token_account_of(left_out)), - Err(SettlementError::TokenProgramNotProvided), - "{left_out} was left out, so its accounts have nothing to settle against", - ); - // The program that *is* carried still settles its own accounts. - assert_eq!( - programs.program_for(&token_account_of(carried)), - Ok(Some(&carried)), - ); - } - } - - /// Leaving both programs out is allowed — it only makes every token account - /// unsettleable, which is exactly what a settlement moving no tokens wants. - #[test] - fn validate_accepts_two_placeholders() { - let placeholder = fake_account(SYSTEM_PROGRAM_ID); - let programs = TokenPrograms::validate(&placeholder, &placeholder) - .expect("two placeholders are allowed"); - - for program in SUPPORTED_TOKEN_PROGRAMS { - assert_eq!( - programs.program_for(&token_account_of(program)), - Err(SettlementError::TokenProgramNotProvided), - ); - } - } - - /// The slots are positional: each one holds its own program or the - /// placeholder, so the two programs can't be swapped between them. - #[test] - fn validate_rejects_swapped_slots() { - let [spl_token, token_2022] = both_slots(); - assert_eq!( - TokenPrograms::validate(&token_2022, &spl_token).err(), - Some(ProgramError::IncorrectProgramId), - ); - } - - /// Anything that is neither the slot's program nor the placeholder is a - /// caller mistake, not an opt-out. - #[test] - fn validate_rejects_an_unrelated_account_in_a_slot() { - let unrelated = fake_account(UNRELATED); - let [spl_token, token_2022] = both_slots(); + fn owning_token_program_rejects_an_account_under_an_unrelated_program() { + let unrelated = pubkey_from_seed("not a token program"); assert_eq!( - TokenPrograms::validate(&unrelated, &token_2022).err(), - Some(ProgramError::IncorrectProgramId), - ); - assert_eq!( - TokenPrograms::validate(&spl_token, &unrelated).err(), - Some(ProgramError::IncorrectProgramId), + owning_token_program(&token_account_of(unrelated)), + Err(ProgramError::IncorrectProgramId), ); } + /// An account that was never allocated is owned by the system program, so + /// it is refused like any other non-token account rather than read as one. #[test] - fn validate_token_program_accepts_every_supported_program() { - for program in TokenProgram::ALL { - let account = fake_account(program.address()); - assert_eq!(validate_token_program(&account), Ok(program)); - } - } - - #[test] - fn validate_token_program_rejects_unrelated_program() { - let account = fake_account(pubkey_from_seed("not a token program")); + fn owning_token_program_rejects_an_unallocated_account() { + let account = fake_account(pubkey_from_seed("never allocated")); assert_eq!( - validate_token_program(&account), + owning_token_program(&account), Err(ProgramError::IncorrectProgramId), ); } diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index bf7ffaa7..e526ef02 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -12,8 +12,7 @@ //! fully-working settlement, so every test here builds one with it and either //! sends it unmodified (when the rejection is already baked into the orders or //! accounts passed in) or mutates its `BeginSettle` instruction in place -//! afterwards (a wrong account, a wrong token program, a wrong state PDA, an -//! extra account). A few tests are the exception and build the raw instruction +//! afterwards (a wrong account, a wrong state PDA, an extra account). A few tests are the exception and build the raw instruction //! directly, because what they exercise can't come out of the client builder, //! whose output is a properly built instruction. @@ -30,7 +29,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::order::{EncodedOrderAccount, OrderAccount}, instruction::settle::{ BeginSettle as BeginSettleRaw, FinalizeSettle as FinalizeSettleRaw, - FINALIZE_FIXED_ACCOUNTS, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID, + FINALIZE_FIXED_ACCOUNTS, INSTRUCTIONS_SYSVAR_ID, }, pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, Instruction, SettlementError, SettlementInstruction, @@ -937,36 +936,6 @@ fn rejects_wrong_state_pda() { ); } -#[test] -fn rejects_wrong_token_program() { - let (mut svm, program_id, payer, solver) = setup_settle_ready(); - - let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); - let mut instructions = settle_and_pay( - &mut svm, - &program_id, - &payer, - &solver, - &[InitializedIntent { - intent: &intent, - pulls: &[], - }], - ); - - // Swap the SPL Token program account `BeginSettle` references for a bogus - // one. - replace_first_matching_account( - &mut instructions[usize::from(BEGIN_INDEX)], - &SPL_TOKEN_PROGRAM_ID, - unique_pubkey(), - ); - - assert_instruction_error( - send(&mut svm, &solver, instructions), - InstructionError::IncorrectProgramId, - ); -} - #[test] fn rejects_pull_delegated_to_incorrect_address() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index 76767c19..54b6f480 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 cow_settlement_interface::token_program::TokenProgram; @@ -12,7 +11,7 @@ use solana_sdk::{ transaction::Transaction, }; -use super::{replace_first_matching_account, token}; +use super::token; /// The canonical buffer PDA for `mint`. pub fn buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> Pubkey { @@ -22,13 +21,19 @@ pub fn buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> Pubkey { /// Create the canonical buffer for `mint`, paid for by `payer`, unless it /// already exists, and return its address. Idempotent so several orders can /// share one buy mint. +/// +/// A buffer is a token account of its mint, so it is created under whichever +/// program owns the mint; [`ensure_buffer_exists_for`] is for the tests that +/// name a program of their own instead. pub fn ensure_buffer_exists( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, mint: &Pubkey, ) -> Pubkey { - ensure_buffer_exists_for(svm, program_id, payer, mint, TokenProgram::SplToken) + let token_program = TokenProgram::try_from(&token::program_of(svm, mint)) + .expect("a mint lives under a supported token program"); + ensure_buffer_exists_for(svm, program_id, payer, mint, token_program) } /// [`ensure_buffer_exists`] under a token program of the caller's choosing, for @@ -44,17 +49,12 @@ pub fn ensure_buffer_exists_for( if svm.get_account(&pda).is_some() { return pda; } - let mut ix = Instruction::from(CreateBuffers { + let ix = Instruction::from(CreateBuffers { program_id: *program_id, payer: payer.pubkey(), token_program, mints: &[*mint], }); - // A buffer is a token account of its mint, so it has to be created under the - // mint's own program. The builder can only name the legacy one, so point the - // instruction at whichever program the mint actually lives under — a no-op - // for a legacy mint. - replace_first_matching_account(&mut ix, &SPL_TOKEN_PROGRAM_ID, token::program_of(svm, mint)); let tx = Transaction::new_signed_with_payer( &[ix], Some(&payer.pubkey()), diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index 995cc7ba..b694a27c 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -17,7 +17,6 @@ pub mod token_2022; 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}; @@ -36,10 +35,6 @@ 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" @@ -207,6 +202,19 @@ pub fn assert_instruction_error_at( ); } +/// Convenience wrapper around [`assert_instruction_error_at`] for asserting a +/// specific [`SettlementError`] at the instruction that produced it: settlements +/// run as a `[BeginSettle, FinalizeSettle]` pair, so the failing instruction +/// isn't always the first. +#[track_caller] +pub fn assert_settlement_error( + ix_idx: u8, + result: Result, + expected: SettlementError, +) { + assert_instruction_error_at(ix_idx, result, to_instruction_error(expected)); +} + pub fn create_account_at(svm: &mut LiteSVM, address: Pubkey, owner: &Pubkey, data: &[u8]) { let lamports = svm.minimum_balance_for_rent_exemption(data.len()); svm.set_account( diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 97818c2e..0d3b3a6d 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -11,6 +11,7 @@ use litesvm::{types::TransactionMetadata, LiteSVM}; use litesvm_token::{ spl_token::{ instruction::{approve, initialize_account3, initialize_mint2, mint_to as mint_to_ix}, + native_mint, state::{Account, Mint}, }, CreateAssociatedTokenAccount, Transfer, TOKEN_ID, @@ -68,29 +69,57 @@ fn send_token_tx( .unwrap_or_else(|error| panic!("{what} should succeed: {error:?}")); } +/// Plant the native mint (wrapped SOL) at its well-known address, owned by the +/// legacy SPL Token program. +/// +/// Every cluster carries this mint already; LiteSVM starts without it, so a +/// test that works with wrapped SOL has to put it there. Its body is what a +/// real one holds: no authorities, no supply, and the native decimals. +pub fn create_native_mint(svm: &mut LiteSVM) { + let mut data = vec![0u8; Mint::LEN]; + Mint { + decimals: native_mint::DECIMALS, + is_initialized: true, + ..Default::default() + } + .pack_into_slice(&mut data); + super::create_account_at(svm, native_mint::ID, &TOKEN_ID, &data); +} + /// Create a fresh mint under the legacy SPL Token program, owned by `payer`, /// and return its address. pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { create_mint_under(svm, payer, &TOKEN_ID) } -/// Create a fresh mint under `token_program`, whose mint authority is `payer`, -/// and return its address. Every later helper reads the program back off the -/// mint, so this is the only place a test names it. +/// [`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 { + create_mint_at_under(svm, payer, mint, &TOKEN_ID) +} + +/// [`create_mint`] under `token_program` rather than the legacy program, for +/// the tests that build mints under both at once. +pub fn create_mint_under(svm: &mut LiteSVM, payer: &Keypair, token_program: &Pubkey) -> Pubkey { + create_mint_at_under(svm, payer, &unique_keypair(), token_program) +} + +/// Create a mint at `mint`'s address under `token_program`, whose mint authority +/// is `payer`, and return its address. Every later helper reads the program back +/// off the mint, so the wrappers above are the only place a test names it. /// /// This open-codes what [`litesvm_token::CreateMint`] does rather than calling /// it, because that builder generates the mint keypair with `Keypair::new()` /// internally and offers no way to supply one. A mint address is a seed of its /// 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 { +fn create_mint_at_under( + svm: &mut LiteSVM, + payer: &Keypair, + mint: &Keypair, + token_program: &Pubkey, +) -> Pubkey { /// `litesvm_token::CreateMint`'s default, kept so the two agree. const DECIMALS: u8 = 8; @@ -101,15 +130,12 @@ pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pub Mint::LEN as u64, token_program, ); - let initialize = initialize_mint2(&TOKEN_ID, &mint.pubkey(), &payer.pubkey(), None, DECIMALS) - .expect("initialize_mint2 should build"); - let tx = Transaction::new_signed_with_payer( - &[create, initialize], - Some(&payer.pubkey()), - &[payer, mint], - svm.latest_blockhash(), + let initialize = under( + initialize_mint2(&TOKEN_ID, &mint.pubkey(), &payer.pubkey(), None, DECIMALS) + .expect("initialize_mint2 should build"), + token_program, ); - send_token_tx(svm, payer, &[&mint], &[create, initialize], "mint creation"); + send_token_tx(svm, payer, &[mint], &[create, initialize], "mint creation"); mint.pubkey() } diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index 284b5e06..92eb744a 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -24,9 +24,6 @@ use spl_token_2022_interface::{ 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; @@ -122,15 +119,15 @@ impl Extensions { .map(|extension| { match extension { ExtensionType::MintCloseAuthority => initialize_mint_close_authority( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), mint, Some(authority), ), ExtensionType::NonTransferable => { - initialize_non_transferable_mint(&TOKEN_2022_PROGRAM_ID, mint) + initialize_non_transferable_mint(&TokenProgram::Token2022.address(), mint) } ExtensionType::TransferFeeConfig => initialize_transfer_fee_config( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), mint, Some(authority), Some(authority), @@ -162,12 +159,12 @@ pub fn create_mint( &mint.pubkey(), svm.minimum_balance_for_rent_exemption(space), space as u64, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), )]; instructions.extend(extensions.initializers(&mint.pubkey(), &payer.pubkey())); instructions.push( initialize_mint2( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), &mint.pubkey(), &payer.pubkey(), None, @@ -193,7 +190,7 @@ pub fn create_mint( /// to claim again. pub fn close_mint(svm: &mut LiteSVM, payer: &Keypair, mint: &Pubkey) { let ix = close_account( - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), mint, &payer.pubkey(), &payer.pubkey(), diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index 919a6d8c..6a0a72da 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -147,6 +147,9 @@ 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(); + // The buffer is created under the program that owns its mint, so the native + // mint has to be on-chain here the way it is on a real cluster. + common::token::create_native_mint(&mut svm); let (buffer_pda, _bump) = find_buffer_pda(&program_id, &native_mint::ID); let ix = CreateBuffers { @@ -291,8 +294,12 @@ fn rejects_non_canonical_bump_pda() { common::pda::assert_rejected_as_noncanonical(&mut svm, tx, &non_canonical_pda); } +/// The token-program account isn't read: each buffer is created under the +/// program that owns its mint. What the account is for is naming that program, +/// and a CPI can only dispatch to a program its instruction names — so swapping +/// it out leaves `InitializeAccount3` with nowhere to go. #[test] -fn rejects_non_spl_token_program() { +fn rejects_a_token_program_the_instruction_doesnt_name() { let (mut svm, program_id, payer) = common::setup(); let mint = common::token::create_mint(&mut svm, &payer); let (buffer_pda, _bump) = find_buffer_pda(&program_id, &mint); @@ -316,13 +323,13 @@ fn rejects_non_spl_token_program() { let err = svm .send_transaction(tx) - .expect_err("a non-SPL-Token program must be rejected"); + .expect_err("a buffer whose token program isn't named must be rejected"); assert!( matches!( err.err, - TransactionError::InstructionError(0, InstructionError::IncorrectProgramId) + TransactionError::InstructionError(0, InstructionError::MissingAccount) ), - "expected instruction 0 to fail with IncorrectProgramId, got {:?}", + "expected instruction 0 to fail with MissingAccount, got {:?}", err.err, ); assert!( @@ -335,11 +342,10 @@ fn rejects_non_spl_token_program() { fn rejects_invalid_mint() { let (mut svm, program_id, payer) = common::setup(); - // An account that isn't an initialized SPL mint. The handler derives the - // buffer PDA from it and delegates mint validation to InitializeAccount3, - // which rejects it: a non-mint account isn't owned by the token program, so - // the CPI fails with IncorrectProgramId after the buffer was allocated, - // reverting the whole instruction. + // An account that isn't an initialized SPL mint. The handler reads the + // mint's owner to decide which program the buffer belongs to, and an + // account under no token program has no answer: it is rejected with + // IncorrectProgramId before anything is allocated. let not_a_mint = unique_pubkey(); let (buffer_pda, _bump) = find_buffer_pda(&program_id, ¬_a_mint); @@ -354,8 +360,6 @@ fn rejects_invalid_mint() { let err = svm .send_transaction(tx) .expect_err("a non-mint account must be rejected"); - // Expected failing line: - // https://github.com/solana-program/token/blob/7ed1aa8d9eb6d54c0084a9e8475c56a0a868b5bd/program/src/processor.rs#L115 assert!( matches!( err.err, diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index 6393b9ef..e23acaed 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -196,8 +196,11 @@ fn rejects_buy_token_account_recreated_for_another_mint() { ); } +/// The token-program account isn't read: every push is issued against the +/// program that owns its destination. The account is what names that program to +/// the runtime, and a CPI can only dispatch to a program its instruction names. #[test] -fn rejects_wrong_token_program() { +fn rejects_a_token_program_the_instruction_doesnt_name() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { @@ -214,7 +217,7 @@ fn rejects_wrong_token_program() { assert_finalize_error( send(&mut svm, &solver, instructions), - InstructionError::IncorrectProgramId, + InstructionError::MissingAccount, ); } diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index fd4a51b5..94fc164e 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -248,6 +248,9 @@ fn reclaims_multiple_buffers_skipping_funded() { ); } +/// The first pass closes the buffer, which hands it back to the system program. +/// The second pass then finds an account no token program owns and refuses to +/// close it. #[test] fn rejects_the_same_buffer_twice_in_one_instruction() { let ( @@ -274,7 +277,7 @@ fn rejects_the_same_buffer_twice_in_one_instruction() { let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); assert_instruction_error( svm.send_transaction(tx).map_err(|e| e.err), - InstructionError::InvalidAccountData, + InstructionError::IncorrectProgramId, ); } @@ -459,7 +462,7 @@ fn reclaims_a_buffer_whose_mint_was_reopened_as_a_legacy_mint() { svm.get_account(&mint) .expect("the reopened mint should exist") .owner, - common::SPL_TOKEN_PROGRAM_ID, + TokenProgram::SplToken.address(), "sanity: the mint must now belong to the legacy program" ); diff --git a/programs/settlement/tests/settle_limit_prices.rs b/programs/settlement/tests/settle_limit_prices.rs index fbd39d29..a54ce96e 100644 --- a/programs/settlement/tests/settle_limit_prices.rs +++ b/programs/settlement/tests/settle_limit_prices.rs @@ -7,7 +7,7 @@ //! succeeds or is rejected with the expected error. use crate::common::{ - assert_instruction_error_at, + assert_settlement_error, order::OrderBuilder, send, settlement::{build_staged_settlement, stage_order, StagedOrder, BEGIN_INDEX}, @@ -28,19 +28,6 @@ use solana_sdk::{ mod common; -/// Convenience wrapper around [`assert_instruction_error_at`] for asserting a -/// specific [`SettlementError`] at the instruction that produced it: settlements -/// run as a `[BeginSettle, FinalizeSettle]` pair, so the failing instruction -/// isn't always the first. -#[track_caller] -fn assert_settlement_error( - ix_idx: u8, - result: Result, - expected: SettlementError, -) { - assert_instruction_error_at(ix_idx, result, to_instruction_error(expected)); -} - /// Read `intent`'s order PDA and return its persisted `(amount_withdrawn, /// amount_received)` cumulative fill totals. fn order_fill(svm: &LiteSVM, program_id: &Pubkey, intent: &OrderIntent) -> (u64, u64) { diff --git a/programs/settlement/tests/settle_solver_auth.rs b/programs/settlement/tests/settle_solver_auth.rs index 82e87428..20396ec8 100644 --- a/programs/settlement/tests/settle_solver_auth.rs +++ b/programs/settlement/tests/settle_solver_auth.rs @@ -4,7 +4,7 @@ //! unauthorized caller is rejected before any settlement work happens. use cow_settlement_client::cow_settlement_interface::{Instruction, SettlementError}; -use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle}; +use cow_settlement_client::instructions::{BeginSettle, FinalizeSettle, TokenPrograms}; use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; use crate::common::{ @@ -24,11 +24,13 @@ fn noop_settlement(program_id: &Pubkey, solver: &Pubkey) -> Vec { solver: *solver, finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, + token_programs: TokenPrograms::NONE, orders: &[], }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), + token_programs: TokenPrograms::NONE, orders: &[], }; vec![begin.into(), finalize.into()] @@ -99,6 +101,7 @@ fn non_signing_solver_may_not_settle() { solver: solver.pubkey(), finalize_ix_index: 0, auction_id: 0, + token_programs: TokenPrograms::NONE, orders: &[], } .into(); diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index 6306d920..df2f74bc 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -3,21 +3,21 @@ //! //! Both instructions take one account per supported token program and issue //! each transfer against the program that owns the account it moves, so a -//! single pair can settle legacy SPL Token and Token-2022 orders together. A -//! program the settlement doesn't need is left out by putting the system -//! program in its slot; a token account under a left-out program then has -//! nothing to be settled against. +//! single pair can settle legacy SPL Token and Token-2022 orders together. The +//! slots are never read: all they do is name those programs, and a CPI can only +//! dispatch to a program its instruction names. A program the settlement +//! doesn't need is left out by putting the system program in its slot; a +//! transfer of a token account under a left-out program then has nothing to +//! dispatch to, and the runtime refuses it. use crate::common::{ - assert_settlement_error, buffer, + buffer, order::OrderBuilder, settlement::{BEGIN_INDEX, FINALIZE_INDEX}, - setup, token, unique_pubkey, + setup_settle_ready, token, unique_pubkey, }; use cow_settlement_client::cow_settlement_interface::{ - data::intent::OrderIntent, - token_program::{SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID}, - Instruction, SettlementError, + data::intent::OrderIntent, token_program::TokenProgram, Instruction, }; use cow_settlement_client::instructions::{ BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, @@ -49,6 +49,7 @@ fn settle_with( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, + solver: &Keypair, orders: &[Settled], begin_programs: TokenPrograms, finalize_programs: TokenPrograms, @@ -79,13 +80,13 @@ fn settle_with( buffer::ensure_funded(svm, program_id, payer, &buy_mint, order.amount_out); finalized.push(FinalizedIntent { intent, - mint: buy_mint, amount: order.amount_out, }); } let begin = BeginSettle { program_id: *program_id, + solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, token_programs: begin_programs, @@ -100,7 +101,7 @@ fn settle_with( let tx = Transaction::new_signed_with_payer( &[begin.into(), finalize.into()], Some(&payer.pubkey()), - &[payer], + &[payer, solver], svm.latest_blockhash(), ); svm.send_transaction(tx) @@ -144,29 +145,30 @@ fn order_across( /// programs, each transfer issued against the program that owns the account. #[test] fn settles_orders_under_both_token_programs() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let legacy = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::SplToken.address(), ); let token_2022 = order_across( &mut svm, &program_id, &payer, 1, - &TOKEN_2022_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), + &TokenProgram::Token2022.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[ Settled { intent: &legacy, @@ -195,21 +197,22 @@ fn settles_orders_under_both_token_programs() { /// sell account's owner and the push the buy account's, independently. #[test] fn settles_an_order_that_crosses_token_programs() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::Token2022.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 250, @@ -228,21 +231,22 @@ fn settles_an_order_that_crosses_token_programs() { /// the legacy slot holding the placeholder costs it nothing it needs. #[test] fn settles_token_2022_orders_without_carrying_the_legacy_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &TOKEN_2022_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::Token2022.address(), + &TokenProgram::Token2022.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 300, @@ -256,27 +260,30 @@ fn settles_token_2022_orders_without_carrying_the_legacy_program() { assert_eq!(token::balance(&svm, &intent.buy_token_account), 300); } -/// `BeginSettle` pulls from the sell account, so leaving that account's program -/// out is what it refuses — by name, rather than as a malformed account. +/// `BeginSettle` pulls from the sell account against the program that owns it, +/// so leaving that program out of the settlement leaves the pull's CPI with +/// nothing to dispatch to. The runtime is what refuses it: the program was +/// never told which programs the settlement carries, only which one owns the +/// account in front of it. #[test] fn rejects_a_sell_account_under_a_left_out_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &TOKEN_2022_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::Token2022.address(), + &TokenProgram::SplToken.address(), ); - assert_settlement_error( - BEGIN_INDEX, + assert_eq!( settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 100, @@ -285,32 +292,36 @@ fn rejects_a_sell_account_under_a_left_out_program() { TokenPrograms::SPL_TOKEN, TokenPrograms::SPL_TOKEN, ), - SettlementError::TokenProgramNotProvided, + Err(TransactionError::InstructionError( + BEGIN_INDEX, + InstructionError::MissingAccount, + )), ); } -/// `FinalizeSettle` pushes into the buy account, so it is the one that refuses -/// a settlement whose slots leave that account's program out. `BeginSettle` -/// runs first and passes: it only pulls, and this order's sell side is legacy. +/// `FinalizeSettle` pushes into the buy account, so it is the instruction whose +/// CPI has nothing to dispatch to when that account's program is left out. +/// `BeginSettle` runs first and passes: it only pulls, and this order's sell +/// side is legacy. #[test] fn rejects_a_buy_account_under_a_left_out_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &TOKEN_2022_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::Token2022.address(), ); - assert_settlement_error( - FINALIZE_INDEX, + assert_eq!( settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 100, @@ -319,24 +330,27 @@ fn rejects_a_buy_account_under_a_left_out_program() { TokenPrograms::BOTH, TokenPrograms::SPL_TOKEN, ), - SettlementError::TokenProgramNotProvided, + Err(TransactionError::InstructionError( + FINALIZE_INDEX, + InstructionError::MissingAccount, + )), ); } -/// The slots are positional. Handing each one the other's program isn't a way -/// to carry both: each slot takes its own program or the placeholder, nothing -/// else. +/// The slots aren't positional: nothing reads them, so a settlement naming both +/// programs settles either way round. All the slots decide is which programs +/// the instruction names. #[test] -fn rejects_swapped_token_program_slots() { - let (mut svm, program_id, payer) = setup(); +fn settles_with_the_token_program_slots_swapped() { + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::SplToken.address(), ); token::fund_and_delegate( &mut svm, @@ -356,6 +370,7 @@ fn rejects_swapped_token_program_slots() { }]; let mut begin = Instruction::from(BeginSettle { program_id, + solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, token_programs: TokenPrograms::BOTH, @@ -364,17 +379,16 @@ fn rejects_swapped_token_program_slots() { pulls: &pulls, }], }); - // `BeginSettle`'s accounts are `[sysvar, state, spl_token, token_2022, ...]`, - // so exchanging the two slots leaves both programs present but each in the - // other's position. - begin.accounts.swap(2, 3); + // `BeginSettle`'s accounts are `[solver, sysvar, state, spl_token, + // token_2022, ...]`, so exchanging the two slots leaves both programs + // present but each in the other's position. + begin.accounts.swap(3, 4); let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), token_programs: TokenPrograms::BOTH, orders: &[FinalizedIntent { intent: &intent, - mint: buy_mint, amount: 100, }], }; @@ -382,17 +396,13 @@ fn rejects_swapped_token_program_slots() { let tx = Transaction::new_signed_with_payer( &[begin, finalize.into()], Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); - let error = svm - .send_transaction(tx) - .expect_err("swapped slots should be rejected") - .err; - assert_eq!( - error, - TransactionError::InstructionError(BEGIN_INDEX, InstructionError::IncorrectProgramId), - ); + svm.send_transaction(tx) + .expect("the slots only name the programs, in either order"); + + assert_eq!(token::balance(&svm, &intent.buy_token_account), 100); } /// Every settlement in the rest of the suite leaves Token-2022's slot empty, so @@ -400,21 +410,22 @@ fn rejects_swapped_token_program_slots() { /// only the accounts under the left-out program that become unsettleable. #[test] fn accepts_the_placeholder_for_a_legacy_only_settlement() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( &mut svm, &program_id, &payer, 0, - &SPL_TOKEN_PROGRAM_ID, - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), + &TokenProgram::SplToken.address(), ); settle_with( &mut svm, &program_id, &payer, + &solver, &[Settled { intent: &intent, amount_in: 500, From f96c7bb631a0050fc55a2da31ecfdb9d62249d34 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:24:45 +0900 Subject: [PATCH 05/38] fixes from my own review need explicit tests confirming the behavior of passing in with the wrong token program --- bench-report.json | 104 +++++++++--------- interface/src/error.rs | 6 +- programs/settlement/idl/cow_settlement.json | 5 - .../src/processor/finalize_settle.rs | 2 +- .../settlement/tests/begin_settle_orders.rs | 42 +++++++ programs/settlement/tests/common/token.rs | 67 ++++++++++- .../tests/finalize_settle_pushes.rs | 23 ++-- 7 files changed, 171 insertions(+), 78 deletions(-) diff --git a/bench-report.json b/bench-report.json index 79e3cb63..fe3f2df5 100644 --- a/bench-report.json +++ b/bench-report.json @@ -25,57 +25,57 @@ "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 3, "remove_solver/remove_with_many_existing_solvers": 5, "remove_solver/removes_a_solver": 5, - "settle/finalizes_with_no_pushes": 5, - "settle/pulls_from_multiple_orders": 15, - "settle/pulls_funds_to_destination": 10, - "settle/pulls_to_multiple_destinations": 11, - "settle/pushes_a_single_order": 9, - "settle/pushes_several_orders_from_different_buffers": 13, - "settle/pushes_several_orders_from_one_buffer": 12, - "settle/settles_a_single_order": 9, - "settle/settles_multiple_orders": 17, + "settle/finalizes_with_no_pushes": 6, + "settle/pulls_from_multiple_orders": 16, + "settle/pulls_funds_to_destination": 11, + "settle/pulls_to_multiple_destinations": 12, + "settle/pushes_a_single_order": 10, + "settle/pushes_several_orders_from_different_buffers": 14, + "settle/pushes_several_orders_from_one_buffer": 13, + "settle/settles_a_single_order": 10, + "settle/settles_multiple_orders": 18, "transfer_authority/manager_can_transfer_manager": 4, "transfer_authority/manager_can_transfer_reclaim_authority": 4, "transfer_authority/reclaim_authority_can_transfer_itself": 4 }, "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": 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": 163742, - "create_buffers/max_buffers_in_one_instruction_token_2022": 207456, - "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, + "add_solver/add_with_many_existing_solvers": 5069, + "add_solver/adds_a_solver": 4617, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 7368, + "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 12175, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17344, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 31765, + "create_buffers/max_buffers_in_one_instruction": 164694, + "create_buffers/max_buffers_in_one_instruction_token_2022": 208023, + "create_order/happy_path_creates_order_pda_with_expected_body": 4981, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4525, + "reclaim_buffer/funded_buffer_is_skipped": 4847, + "reclaim_buffer/funded_buffer_is_skipped_token_2022": 4857, "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, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 7498, + "reclaim_buffer/max_buffers_in_one_instruction": 125926, + "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 170566, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7637, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 9135, + "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2200, + "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2069, + "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2077, "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, - "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 + "remove_solver/remove_with_many_existing_solvers": 3755, + "remove_solver/removes_a_solver": 3490, + "settle/finalizes_with_no_pushes": 7103, + "settle/pulls_from_multiple_orders": 20151, + "settle/pulls_funds_to_destination": 13667, + "settle/pulls_to_multiple_destinations": 14820, + "settle/pushes_a_single_order": 12511, + "settle/pushes_several_orders_from_different_buffers": 17834, + "settle/pushes_several_orders_from_one_buffer": 17834, + "settle/settles_a_single_order": 12529, + "settle/settles_multiple_orders": 23210, + "transfer_authority/manager_can_transfer_manager": 3171, + "transfer_authority/manager_can_transfer_reclaim_authority": 3173, + "transfer_authority/reclaim_authority_can_transfer_itself": 3177 }, "transaction_bytes": { "add_solver/add_with_many_existing_solvers": 366, @@ -103,15 +103,15 @@ "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 204, "remove_solver/remove_with_many_existing_solvers": 365, "remove_solver/removes_a_solver": 365, - "settle/finalizes_with_no_pushes": 290, - "settle/pulls_from_multiple_orders": 656, - "settle/pulls_funds_to_destination": 473, - "settle/pulls_to_multiple_destinations": 514, - "settle/pushes_a_single_order": 432, - "settle/pushes_several_orders_from_different_buffers": 574, - "settle/pushes_several_orders_from_one_buffer": 542, - "settle/settles_a_single_order": 432, - "settle/settles_multiple_orders": 716, + "settle/finalizes_with_no_pushes": 324, + "settle/pulls_from_multiple_orders": 690, + "settle/pulls_funds_to_destination": 507, + "settle/pulls_to_multiple_destinations": 548, + "settle/pushes_a_single_order": 466, + "settle/pushes_several_orders_from_different_buffers": 608, + "settle/pushes_several_orders_from_one_buffer": 576, + "settle/settles_a_single_order": 466, + "settle/settles_multiple_orders": 750, "transfer_authority/manager_can_transfer_manager": 333, "transfer_authority/manager_can_transfer_reclaim_authority": 333, "transfer_authority/reclaim_authority_can_transfer_itself": 333 diff --git a/interface/src/error.rs b/interface/src/error.rs index b96f471d..d7908aa6 100644 --- a/interface/src/error.rs +++ b/interface/src/error.rs @@ -132,12 +132,8 @@ pub enum SettlementError { /// mint has to be and couldn't read the answer, so it can't size the /// buffer. BufferSizeUnavailable = 40, - /// `FinalizeSettle`: a push's destination isn't owned by a supported token - /// program, so it is no token account at all and there is nothing to issue - /// its transfer against. - PushDestinationInvalid = 41, /// The token program for a given token or mint is not supported. - InvalidTokenProgram = 42, + InvalidTokenProgram = 41, } impl From for u32 { diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json index c2b51437..6b69049e 100644 --- a/programs/settlement/idl/cow_settlement.json +++ b/programs/settlement/idl/cow_settlement.json @@ -1055,11 +1055,6 @@ }, { "code": 41, - "name": "PushDestinationInvalid", - "msg": "FinalizeSettle: a push's destination isn't owned by a supported token program, so it is no token account at all and there is nothing to issue its transfer against." - }, - { - "code": 42, "name": "InvalidTokenProgram", "msg": "The token program for a given token or mint is not supported." } diff --git a/programs/settlement/src/processor/finalize_settle.rs b/programs/settlement/src/processor/finalize_settle.rs index f196b647..5b4f7721 100644 --- a/programs/settlement/src/processor/finalize_settle.rs +++ b/programs/settlement/src/processor/finalize_settle.rs @@ -71,7 +71,7 @@ fn push_funds<'a>( // token program that owns it. An account under neither program isn't a // token account at all. let token_program = owning_token_program(push.destination) - .map_err(|_| SettlementError::PushDestinationInvalid)?; + .map_err(|_| SettlementError::InvalidTokenProgram)?; Transfer::new( push.source_buffer, push.destination, diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 4f9f54c4..43a5712b 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -416,6 +416,48 @@ fn rejects_non_token_sell_account() { ); } +/// Even if a token account parses, we should still correctly identify if its unsupported +#[test] +fn rejects_sell_account_under_a_unsupported_token_program() { + let (mut svm, program_id, payer, solver) = setup_settle_ready(); + + let amount = 1_000_000; + let (sell_mint, sell_token_account) = token::cloned_token_under_unsupported_program( + &mut svm, + &program_id, + &payer, + &payer.pubkey(), + amount, + ); + + let intent = OrderIntent { + sell_token_account, + sell_mint, + ..settlable_intent(&mut svm, &payer, payer.pubkey(), 1) + }; + create_order_pda(&mut svm, &program_id, &payer, &intent); + + let instructions = settle_and_pay( + &mut svm, + &program_id, + &payer, + &solver, + &[InitializedIntent { + intent: &intent, + pulls: &[], + }], + ); + assert_begin_error( + send(&mut svm, &solver, &instructions), + SettlementError::SellTokenAccountInvalid, + ); + assert_eq!( + token::balance(&svm, &sell_token_account), + amount, + "the clone's tokens must be left where they were" + ); +} + #[test] fn rejects_sell_token_account_recreated_for_another_mint() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 50995d48..ba4f12f5 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -9,7 +9,9 @@ //! running test is exercising, and [`create_mint_under`] names it outright, for //! the tests that build mints under both at once. -use super::{active_token, send_with_signers, token_2022::Extensions, unique_keypair}; +use super::{ + active_token, send_with_signers, token_2022::Extensions, unique_keypair, unique_pubkey, +}; use cow_settlement_client::cow_settlement_interface::{ pda::state::find_state_pda, token_program::TokenProgram, }; @@ -262,6 +264,69 @@ pub fn delegate( .unwrap_or_else(|error| panic!("approving a delegate should succeed: {error:?}")); } +/// A third-party token program: an SPL Token clone, serving the same +/// instructions over the same account layouts, that simply isn't one of the two +/// programs this settlement supports. +/// +/// Never deployed, because nothing here gets far enough to call it. An account's +/// owner decides which program its transfers are issued against, and this one is +/// refused at that step — long before there is a CPI to dispatch. +pub const CLONED_TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array([0x7c; 32]); + +/// Re-plant `account`'s bytes at a fresh address under +/// [`CLONED_TOKEN_PROGRAM_ID`], and return it. +/// +/// The copy is the account it was taken from in every respect that can be read +/// out of it — same layout, same length, same mint, owner, balance and delegate +/// — because it is the same bytes. All that differs is the program they sit +/// under. +pub fn clone_under_unsupported_program(svm: &mut LiteSVM, account: &Pubkey) -> Pubkey { + let data = svm + .get_account(account) + .unwrap_or_else(|| panic!("{account} should exist on-chain")) + .data; + let clone = unique_pubkey(); + super::create_account_at(svm, clone, &CLONED_TOKEN_PROGRAM_ID, &data); + clone +} + +/// A mint and one of its token accounts, both under +/// [`CLONED_TOKEN_PROGRAM_ID`], returned as `(mint, token_account)`. +/// +/// Both are byte-for-byte copies of a genuine SPL Token mint and a genuine, +/// funded token account of it, held by `owner` and delegated to the settlement +/// state PDA for the whole `amount` — everything a settleable sell account is. +/// The copies are self-consistent under the clone: the account's mint field +/// names the cloned mint, so under that program this is a whole, well-formed +/// token. Only the owning program marks it out. +pub fn cloned_token_under_unsupported_program( + svm: &mut LiteSVM, + program_id: &Pubkey, + payer: &Keypair, + owner: &Pubkey, + amount: u64, +) -> (Pubkey, Pubkey) { + // Build the genuine article first, so what gets cloned is a real token's + // bytes rather than a test's idea of them. + let mint = create_mint_under(svm, payer, &TokenProgram::SplToken.address()); + let account = create_token_account(svm, payer, &mint, owner); + fund_and_delegate(svm, program_id, payer, &account, amount); + + let cloned_mint = clone_under_unsupported_program(svm, &mint); + // Repoint the copy at the cloned mint, so the pair stands on its own under + // the clone instead of borrowing the real mint. + let mut token = + litesvm_token::get_spl_account::(svm, &account) + .expect("the freshly delegated account is a valid token account"); + token.mint = cloned_mint; + let mut data = vec![0u8; litesvm_token::spl_token::state::Account::LEN]; + token.pack_into_slice(&mut data); + let cloned_account = unique_pubkey(); + super::create_account_at(svm, cloned_account, &CLONED_TOKEN_PROGRAM_ID, &data); + + (cloned_mint, cloned_account) +} + /// Fund `sell_token` with `amount` of its mint and approve the settlement state /// PDA as its delegate for the same `amount`, so the program can pull from it. pub fn fund_and_delegate( diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index 3ca1c6a7..eb343254 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -11,7 +11,7 @@ use crate::common::{ benchmark::BenchLabel, - buffer, create_account, + buffer, order::{create_order_pda, settlable_intent, OrderBuilder}, replace_first_matching_account, send, send_metered, settlement::{build_settlement, BEGIN_INDEX, FINALIZE_INDEX}, @@ -307,16 +307,15 @@ fn rejects_too_few_accounts() { ); } -/// An account that isn't a token account at all is owned by no token program, -/// so the push has nothing to be issued against and `FinalizeSettle` says so -/// itself rather than handing the transfer to a token program. #[test] fn rejects_invalid_buy_token_account() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); + let settlable = settlable_intent(&mut svm, &payer, payer.pubkey(), 0); + // The mint account is a convenient invalid account we can use let intent = OrderIntent { - buy_token_account: unique_pubkey(), - ..settlable_intent(&mut svm, &payer, payer.pubkey(), 0) + buy_token_account: settlable.buy_mint, + ..settlable }; create_order_pda(&mut svm, &program_id, &payer, &intent); buffer::ensure_funded(&mut svm, &program_id, &payer, &intent.buy_mint, 1_000); @@ -328,20 +327,16 @@ fn rejects_invalid_buy_token_account() { let instructions = finalize(&program_id, &solver.pubkey(), &orders); assert_finalize_error( send(&mut svm, &solver, &instructions), - to_instruction_error(SettlementError::PushDestinationInvalid), + InstructionError::InvalidAccountData, ); } #[test] -fn rejects_buy_token_account_owned_by_wrong_program() { +fn rejects_buy_account_under_a_unsupported_token_program() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); let settlable = settlable_intent(&mut svm, &payer, payer.pubkey(), 0); - let token_shaped = svm - .get_account(&settlable.buy_token_account) - .expect("the settlable order's buy token account exists") - .data; - let impostor = create_account(&mut svm, &unique_pubkey(), &token_shaped); + let impostor = token::clone_under_unsupported_program(&mut svm, &settlable.buy_token_account); // As above, the impostor passes both instructions' push checks (the push // pays `intent.buy_token_account` from `intent.buy_mint`'s buffer), but its @@ -360,7 +355,7 @@ fn rejects_buy_token_account_owned_by_wrong_program() { let instructions = finalize(&program_id, &solver.pubkey(), &orders); assert_finalize_error( send(&mut svm, &solver, &instructions), - to_instruction_error(SettlementError::PushDestinationInvalid), + to_instruction_error(SettlementError::InvalidTokenProgram), ); } From e72f305a9eae50600f40fe0766b05d8e5c7ab3d4 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:54:47 +0900 Subject: [PATCH 06/38] safari to remove incorrect description of CPI calls as "dispatch" --- client/src/instruction/finalize_settle.rs | 9 +- interface/src/instruction/create_buffer.rs | 4 +- interface/src/instruction/settle/begin.rs | 16 +-- interface/src/token_program.rs | 22 +-- .../settlement/src/processor/utils/token.rs | 8 +- programs/settlement/tests/common/token.rs | 21 +-- programs/settlement/tests/create_buffer.rs | 4 - .../tests/finalize_settle_pushes.rs | 3 - .../settlement/tests/settle_token_programs.rs | 133 +++--------------- 9 files changed, 35 insertions(+), 185 deletions(-) diff --git a/client/src/instruction/finalize_settle.rs b/client/src/instruction/finalize_settle.rs index 0b07f153..3f86e78e 100644 --- a/client/src/instruction/finalize_settle.rs +++ b/client/src/instruction/finalize_settle.rs @@ -85,10 +85,9 @@ mod tests { fixtures::pubkey_from_seed, instruction::{ fixtures::fake_account_from_array, - settle::{FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID}, + settle::{FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID}, InstructionInputParsing, }, - token_program::SYSTEM_PROGRAM_ID, }; proptest! { @@ -160,12 +159,6 @@ mod tests { ); let (state_pda, _bump) = find_state_pda(&program_id); prop_assert_eq!(parsed.state_pda_account.address(), &state_pda); - // The token-program slots aren't parsed, so the instruction's own - // account list is where they are checked: the legacy program in its - // own slot, and — these settlements being legacy-only — the - // placeholder in Token-2022's. - prop_assert_eq!(ix.accounts[2].pubkey, SPL_TOKEN_PROGRAM_ID); - prop_assert_eq!(ix.accounts[3].pubkey, SYSTEM_PROGRAM_ID); let parsed_pushes: Vec<_> = parsed.pushes.iter().collect(); prop_assert_eq!(parsed_pushes.len(), expected.len()); diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index e017196c..c87ed5f7 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -37,7 +37,7 @@ use crate::SettlementInstruction; /// `[payer (W,S), system_program (R), token_program (R), (buffer_pda (W), mint (R))...]`. /// The three shared accounts come first and are read positionally; the /// per-buffer pairs follow. The system program only has to be present so the -/// `CreateAccount` CPI can dispatch; it isn't read by index. +/// `CreateAccount` CPI can execute; it isn't read by index. pub struct CreateBuffers<'a> { pub program_id: Pubkey, pub payer: Pubkey, @@ -99,7 +99,7 @@ impl<'a, A> InstructionInputParsing<'a, A> for CreateBufferInput<'a, A> { // (buffer_pda (W), mint (R))...]. The three shared accounts come first; // the per-buffer pairs follow, one pair per buffer. Neither program is // dereferenced here: they need to be present for the `CreateAccount` - // and `InitializeAccount3` CPIs to dispatch, and each buffer's program + // and `InitializeAccount3` CPIs to execute, and each buffer's program // is the one that owns its mint. let [payer, _system, _token_program, rest @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 22200e82..236f5260 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -35,12 +35,10 @@ pub struct Pull { /// [transfer_count×n][amount: u64 LE ×T]`. /// Required accounts: `[solver (S,R), instructions_sysvar (R), state_pda (R), /// spl_token_program (R), token_2022_program (R)]` followed, per order, by -/// `[order_pda (W), sell_token_account (W), destination (W)...]`. The two token -/// programs are the slots [`TokenPrograms`] describes: each transfer is issued -/// against the program that owns the account it moves, so the slots are there -/// to name those programs — a CPI can only dispatch to a program the -/// instruction names. A program this settlement doesn't touch is left out with -/// the system program. +/// `[order_pda (W), sell_token_account (W), destination (W)...]`. The token +/// program accounts are there to allow CPI calls against the corresponding token +/// program, and are otherwise not parsed or validated, so it is possible to replace +/// these accounts with the system program (or any other program) if they are unused. /// /// `solver` must sign, and the solver must be registered in the state pda. /// @@ -228,10 +226,8 @@ impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> { fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result { let (finalize_ix_index, body) = recover_counterpart(instruction_data)?; - // The two token-program slots are skipped rather than read: every - // transfer is issued against the program that owns the account it - // moves, so naming the programs is all the slots do. They still take up - // their positions, which is what the order accounts are counted from. + // The two token-program slots are skipped rather than read since they are only + // used for program invocation. let [solver_account, instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, order_accounts @ ..] = accounts else { diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index 9672a33a..b5beead3 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -1,20 +1,4 @@ //! The token programs settlement transfers may be issued against. -//! -//! An instruction that moves tokens has to name the program to issue its -//! transfers against — a CPI can only dispatch to a program its instruction -//! names — and the program it targets is the one owning the account it moves, -//! which [`TokenProgram::try_from`] resolves from that account's owner. Naming -//! is all the accounts below do; none of them is read on-chain. How an -//! instruction names them differs: -//! -//! - `CreateBuffer` and `ReclaimBuffer` take a single `token_program` account. -//! Each buffer is created under, and closed by, the program owning its mint, -//! so a mint under the program the instruction didn't name needs its own -//! instruction. -//! - `BeginSettle` and `FinalizeSettle` take one account per supported program, -//! described by [`TokenPrograms`], and issue each transfer against the -//! program that owns the account it moves. One settlement can therefore mix -//! tokens from both programs. use crate::Pubkey; use solana_program_error::ProgramError; @@ -70,9 +54,7 @@ impl TryFrom<&Pubkey> for TokenProgram { /// program that owns the account it moves — so a single settlement may mix /// tokens from both. The slots are what name those programs; they are not read /// on-chain, and a program the settlement doesn't touch is left out by putting -/// [`SYSTEM_PROGRAM_ID`] in its slot. A transfer of a token account under a -/// left-out program then has no program to dispatch to, and the runtime refuses -/// the instruction. +/// [`SYSTEM_PROGRAM_ID`] in its slot. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct TokenPrograms { /// Whether the legacy SPL Token program's slot carries the program rather @@ -168,7 +150,7 @@ mod tests { } /// The placeholder has to be something no token account can be owned by, - /// or a slot carrying it would still dispatch transfers somewhere. + /// or a slot carrying it would still execute transfers somewhere. #[test] fn the_placeholder_is_not_a_token_program() { assert_eq!( diff --git a/programs/settlement/src/processor/utils/token.rs b/programs/settlement/src/processor/utils/token.rs index b6355651..953e6a00 100644 --- a/programs/settlement/src/processor/utils/token.rs +++ b/programs/settlement/src/processor/utils/token.rs @@ -1,4 +1,4 @@ -//! Token-program dispatch and token-account reads +//! Token-program execution and token-account reads use cow_settlement_interface::{token_program::TokenProgram, SettlementError}; use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView, Address}; @@ -18,7 +18,7 @@ const BASE_TOKEN_ACCOUNT_LEN: u64 = pinocchio_token::state::Account::LEN as u64; /// /// The program the answer names still has to be one of the calling /// instruction's own accounts, or the CPI issued against it has nothing to -/// dispatch to. Naming it is the caller's job, and the runtime is what enforces +/// execute into. Naming it is the caller's job, and the runtime is what enforces /// it. #[must_use = "not consuming skips the owner check"] pub fn owning_token_program(account: &AccountView) -> Result { @@ -220,11 +220,11 @@ mod tests { ) } - /// Every token account dispatches to the program that owns it. This is what + /// Every token account executes against the program that owns it. This is what /// one instruction moving tokens under both programs rests on: nothing has /// to tell it which, each account already says. #[test] - fn owning_token_program_dispatches_on_the_accounts_owner() { + fn owning_token_program_returns_on_the_accounts_owner() { for program in TokenProgram::ALL { assert_eq!( owning_token_program(&token_account_of(program.address())), diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index ba4f12f5..68c8710f 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -264,22 +264,14 @@ pub fn delegate( .unwrap_or_else(|error| panic!("approving a delegate should succeed: {error:?}")); } -/// A third-party token program: an SPL Token clone, serving the same -/// instructions over the same account layouts, that simply isn't one of the two -/// programs this settlement supports. +/// An SPL Token clone, serving the same instructions over the same account layouts. +/// Used to validate unsupported token programs. /// -/// Never deployed, because nothing here gets far enough to call it. An account's -/// owner decides which program its transfers are issued against, and this one is -/// refused at that step — long before there is a CPI to dispatch. +/// Never deployed, because nothing here gets far enough to call it. pub const CLONED_TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array([0x7c; 32]); /// Re-plant `account`'s bytes at a fresh address under /// [`CLONED_TOKEN_PROGRAM_ID`], and return it. -/// -/// The copy is the account it was taken from in every respect that can be read -/// out of it — same layout, same length, same mint, owner, balance and delegate -/// — because it is the same bytes. All that differs is the program they sit -/// under. pub fn clone_under_unsupported_program(svm: &mut LiteSVM, account: &Pubkey) -> Pubkey { let data = svm .get_account(account) @@ -292,13 +284,6 @@ pub fn clone_under_unsupported_program(svm: &mut LiteSVM, account: &Pubkey) -> P /// A mint and one of its token accounts, both under /// [`CLONED_TOKEN_PROGRAM_ID`], returned as `(mint, token_account)`. -/// -/// Both are byte-for-byte copies of a genuine SPL Token mint and a genuine, -/// funded token account of it, held by `owner` and delegated to the settlement -/// state PDA for the whole `amount` — everything a settleable sell account is. -/// The copies are self-consistent under the clone: the account's mint field -/// names the cloned mint, so under that program this is a whole, well-formed -/// token. Only the owning program marks it out. pub fn cloned_token_under_unsupported_program( svm: &mut LiteSVM, program_id: &Pubkey, diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index 0f732473..4cfe9028 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -316,10 +316,6 @@ fn rejects_non_canonical_bump_pda() { common::pda::assert_rejected_as_noncanonical(&mut svm, tx, &non_canonical_pda); } -/// The token-program account isn't read: each buffer is created under the -/// program that owns its mint. What the account is for is naming that program, -/// and a CPI can only dispatch to a program its instruction names — so swapping -/// it out leaves `InitializeAccount3` with nowhere to go. #[test] fn rejects_a_token_program_the_instruction_doesnt_name() { let (mut svm, program_id, payer) = common::setup(); diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index eb343254..746bf2f9 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -196,9 +196,6 @@ fn rejects_buy_token_account_recreated_for_another_mint() { ); } -/// The token-program account isn't read: every push is issued against the -/// program that owns its destination. The account is what names that program to -/// the runtime, and a CPI can only dispatch to a program its instruction names. #[test] fn rejects_a_token_program_the_instruction_doesnt_name() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index d5141050..a85b1316 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -1,14 +1,5 @@ -//! Integration tests for the token-program slots a `BeginSettle` / -//! `FinalizeSettle` pair carries. -//! -//! Both instructions take one account per supported token program and issue -//! each transfer against the program that owns the account it moves, so a -//! single pair can settle legacy SPL Token and Token-2022 orders together. The -//! slots are never read: all they do is name those programs, and a CPI can only -//! dispatch to a program its instruction names. A program the settlement -//! doesn't need is left out by putting the system program in its slot; a -//! transfer of a token account under a left-out program then has nothing to -//! dispatch to, and the runtime refuses it. +//! Integration tests to verify the behavior of multiple token programs +//! within a single settlement. use crate::common::{ buffer, @@ -24,7 +15,6 @@ use cow_settlement_client::instruction::{ }; use litesvm::LiteSVM; use solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, transaction::{Transaction, TransactionError}, @@ -141,10 +131,8 @@ fn order_across( intent } -/// The headline capability: one settlement pair moving tokens under both -/// programs, each transfer issued against the program that owns the account. #[test] -fn settles_orders_under_both_token_programs() { +fn settles_orders_under_both_token_programs_simultaneously() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); let legacy = order_across( @@ -193,8 +181,6 @@ fn settles_orders_under_both_token_programs() { assert_eq!(token::balance(&svm, &token_2022.sell_token_account), 0); } -/// The two sides of one order need not share a program: the pull follows the -/// sell account's owner and the push the buy account's, independently. #[test] fn settles_an_order_that_crosses_token_programs() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); @@ -227,8 +213,6 @@ fn settles_an_order_that_crosses_token_programs() { assert_eq!(token::balance(&svm, &intent.sell_token_account), 0); } -/// A settlement that carries only Token-2022 still settles Token-2022 orders: -/// the legacy slot holding the placeholder costs it nothing it needs. #[test] fn settles_token_2022_orders_without_carrying_the_legacy_program() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); @@ -260,13 +244,8 @@ fn settles_token_2022_orders_without_carrying_the_legacy_program() { assert_eq!(token::balance(&svm, &intent.buy_token_account), 300); } -/// `BeginSettle` pulls from the sell account against the program that owns it, -/// so leaving that program out of the settlement leaves the pull's CPI with -/// nothing to dispatch to. The runtime is what refuses it: the program was -/// never told which programs the settlement carries, only which one owns the -/// account in front of it. #[test] -fn rejects_a_sell_account_under_a_left_out_program() { +fn settles_legacy_orders_without_carrying_the_token_2022_program() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = order_across( @@ -274,72 +253,28 @@ fn rejects_a_sell_account_under_a_left_out_program() { &program_id, &payer, 0, - &TokenProgram::Token2022.address(), + &TokenProgram::SplToken.address(), &TokenProgram::SplToken.address(), ); - assert_eq!( - settle_with( - &mut svm, - &program_id, - &payer, - &solver, - &[Settled { - intent: &intent, - amount_in: 100, - amount_out: 100, - }], - TokenPrograms::SPL_TOKEN, - TokenPrograms::SPL_TOKEN, - ), - Err(TransactionError::InstructionError( - BEGIN_INDEX, - InstructionError::MissingAccount, - )), - ); -} - -/// `FinalizeSettle` pushes into the buy account, so it is the instruction whose -/// CPI has nothing to dispatch to when that account's program is left out. -/// `BeginSettle` runs first and passes: it only pulls, and this order's sell -/// side is legacy. -#[test] -fn rejects_a_buy_account_under_a_left_out_program() { - let (mut svm, program_id, payer, solver) = setup_settle_ready(); - - let intent = order_across( + settle_with( &mut svm, &program_id, &payer, - 0, - &TokenProgram::SplToken.address(), - &TokenProgram::Token2022.address(), - ); + &solver, + &[Settled { + intent: &intent, + amount_in: 500, + amount_out: 500, + }], + TokenPrograms::SPL_TOKEN, + TokenPrograms::SPL_TOKEN, + ) + .expect("a legacy-only settlement should not have to carry Token-2022"); - assert_eq!( - settle_with( - &mut svm, - &program_id, - &payer, - &solver, - &[Settled { - intent: &intent, - amount_in: 100, - amount_out: 100, - }], - TokenPrograms::BOTH, - TokenPrograms::SPL_TOKEN, - ), - Err(TransactionError::InstructionError( - FINALIZE_INDEX, - InstructionError::MissingAccount, - )), - ); + assert_eq!(token::balance(&svm, &intent.buy_token_account), 500); } -/// The slots aren't positional: nothing reads them, so a settlement naming both -/// programs settles either way round. All the slots decide is which programs -/// the instruction names. #[test] fn settles_with_the_token_program_slots_swapped() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); @@ -404,37 +339,3 @@ fn settles_with_the_token_program_slots_swapped() { assert_eq!(token::balance(&svm, &intent.buy_token_account), 100); } - -/// Every settlement in the rest of the suite leaves Token-2022's slot empty, so -/// the placeholder has to be accepted for a legacy-only settlement — and it is -/// only the accounts under the left-out program that become unsettleable. -#[test] -fn accepts_the_placeholder_for_a_legacy_only_settlement() { - let (mut svm, program_id, payer, solver) = setup_settle_ready(); - - let intent = order_across( - &mut svm, - &program_id, - &payer, - 0, - &TokenProgram::SplToken.address(), - &TokenProgram::SplToken.address(), - ); - - settle_with( - &mut svm, - &program_id, - &payer, - &solver, - &[Settled { - intent: &intent, - amount_in: 500, - amount_out: 500, - }], - TokenPrograms::SPL_TOKEN, - TokenPrograms::SPL_TOKEN, - ) - .expect("a legacy-only settlement should not have to carry Token-2022"); - - assert_eq!(token::balance(&svm, &intent.buy_token_account), 500); -} From 80cf0926deb3e79d92a4345e4f4a10121c572efd Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:59:12 +0900 Subject: [PATCH 07/38] reduce unnecessary comment --- programs/settlement/src/processor/utils/token.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/programs/settlement/src/processor/utils/token.rs b/programs/settlement/src/processor/utils/token.rs index 953e6a00..4a148f87 100644 --- a/programs/settlement/src/processor/utils/token.rs +++ b/programs/settlement/src/processor/utils/token.rs @@ -8,18 +8,8 @@ use pinocchio_token::instructions::GetAccountDataSize; /// the actual token account longer than this. const BASE_TOKEN_ACCOUNT_LEN: u64 = pinocchio_token::state::Account::LEN as u64; -/// The token program that owns `account`, and so the one every transfer of its -/// tokens has to be issued against. -/// -/// Reading the owner is what lets one instruction move tokens under either -/// program without being told which: the account itself says. An account under -/// anything else is no token account at all, and there is nothing to issue a -/// transfer against. -/// -/// The program the answer names still has to be one of the calling -/// instruction's own accounts, or the CPI issued against it has nothing to -/// execute into. Naming it is the caller's job, and the runtime is what enforces -/// it. +/// Resolve the token program behind the given token account. +/// Throws if the owning token program isn't supported. #[must_use = "not consuming skips the owner check"] pub fn owning_token_program(account: &AccountView) -> Result { TokenProgram::try_from(account.owner()) From 7af61d968bd97c1d2dc5d194fdaf38f9197a084f Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:14:46 +0900 Subject: [PATCH 08/38] ensure the fake account is still unrelated --- programs/settlement/src/processor/utils/token.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/programs/settlement/src/processor/utils/token.rs b/programs/settlement/src/processor/utils/token.rs index 4a148f87..578af0d7 100644 --- a/programs/settlement/src/processor/utils/token.rs +++ b/programs/settlement/src/processor/utils/token.rs @@ -204,9 +204,13 @@ mod tests { /// its owner decides which program its transfers go to. fn token_account_of(program: Address) -> AccountView { fake_account_owned_by( - pubkey_from_seed("token account"), + pubkey_from_seed("UNRELATED token account"), program, - &base_account_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 0), + &base_account_layout( + pubkey_from_seed("UNRELATED mint"), + pubkey_from_seed("UNRELATED owner"), + 0, + ), ) } From 09d1316d93103e54562da776b69b22c18aa4d55e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:00:03 +0900 Subject: [PATCH 09/38] refactor of the token test helpers --- programs/settlement/tests/common/token.rs | 159 +++++++++++++----- .../settlement/tests/common/token_2022.rs | 80 +-------- programs/settlement/tests/create_buffer.rs | 11 +- programs/settlement/tests/reclaim_buffer.rs | 22 ++- .../settlement/tests/settle_token_programs.rs | 11 +- 5 files changed, 158 insertions(+), 125 deletions(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 68c8710f..6596180e 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -9,9 +9,9 @@ //! running test is exercising, and [`create_mint_under`] names it outright, for //! the tests that build mints under both at once. -use super::{ - active_token, send_with_signers, token_2022::Extensions, unique_keypair, unique_pubkey, -}; +use crate::common::{active_token, token_2022::Extensions}; + +use super::{send_with_signers, unique_keypair, unique_pubkey}; use cow_settlement_client::cow_settlement_interface::{ pda::state::find_state_pda, token_program::TokenProgram, }; @@ -24,13 +24,14 @@ use solana_program_pack::Pack; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, + transaction::Transaction, }; use solana_system_interface::instruction::create_account as system_create_account; use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; use spl_token_2022_interface::{ - extension::StateWithExtensions, + extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions}, instruction::{ - approve, initialize_account3, initialize_mint2, mint_to as mint_to_ix, + approve, close_account, initialize_account3, initialize_mint2, mint_to as mint_to_ix, transfer_checked as transfer_checked_ix, }, state::{Account, Mint as Mint2022}, @@ -69,68 +70,139 @@ pub fn create_native_mint(svm: &mut LiteSVM) { /// Create a fresh mint under [`active_token::program`], whose mint authority is /// `payer`, and return its address. pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { - create_mint_at(svm, payer, &unique_keypair()) + create_mint_at_under( + svm, + payer, + &unique_keypair(), + &active_token::program().address(), + Extensions::default(), + ) } -/// [`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. -/// -/// Under Token-2022 the mint carries [`Extensions::DEFAULT`] rather than being -/// bare, so every generated test exercises the longer accounts its extensions -/// force. [`create_mint_under`] is the way to a bare one. -pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pubkey { - match active_token::program() { - TokenProgram::SplToken => { - create_mint_at_under(svm, payer, mint, &TokenProgram::SplToken.address()) - } - TokenProgram::Token2022 => { - super::token_2022::create_mint(svm, payer, mint, Extensions::default()) - } - } +/// Create a mint with the specified instructions. Extensions are not added if the token program is SPL. +pub fn create_mint_with_extensions( + svm: &mut LiteSVM, + payer: &Keypair, + extensions: Extensions, +) -> Pubkey { + create_mint_at_under( + svm, + payer, + &unique_keypair(), + &active_token::program().address(), + extensions, + ) } /// [`create_mint`] under `token_program` rather than under /// [`active_token::program`], for the tests that build mints under both /// programs at once. -pub fn create_mint_under(svm: &mut LiteSVM, payer: &Keypair, token_program: &Pubkey) -> Pubkey { - create_mint_at_under(svm, payer, &unique_keypair(), token_program) +pub fn create_mint_under( + svm: &mut LiteSVM, + payer: &Keypair, + token_program: &Pubkey, + extensions: Extensions, +) -> Pubkey { + create_mint_at_under(svm, payer, &unique_keypair(), token_program, extensions) } /// Create a mint at `mint`'s address under `token_program`, whose mint authority /// is `payer`, and return its address. -fn create_mint_at_under( +pub fn create_mint_at_under( svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair, token_program: &Pubkey, + extensions: Extensions, ) -> Pubkey { /// `litesvm_token::CreateMint`'s default, kept so the two agree. const DECIMALS: u8 = 8; - let create = system_create_account( + // The legacy program has no extensions to make room for, so its mints are + // always the base length; Token-2022 insists on exactly the length the + // extensions initialized below need. + let space = if token_program == &TokenProgram::SplToken.address() { + Mint::LEN + } else { + extensions.mint_len() + }; + let mut instructions = vec![system_create_account( &payer.pubkey(), &mint.pubkey(), - svm.minimum_balance_for_rent_exemption(Mint::LEN), - Mint::LEN as u64, + svm.minimum_balance_for_rent_exemption(space), + space as u64, token_program, + )]; + + if token_program != &TokenProgram::SplToken.address() { + instructions.extend(extensions.initializers(&mint.pubkey(), &payer.pubkey())); + } + + instructions.push( + initialize_mint2( + token_program, + &mint.pubkey(), + &payer.pubkey(), + None, + DECIMALS, + ) + .expect("initialize_mint2 should build"), ); - // A mint with no extension data, which is every legacy mint and the shape a - // Token-2022 mint takes when nothing asks for more. That is what keeps a - // buffer for it at the base layout under either program. - let initialize = initialize_mint2( - token_program, - &mint.pubkey(), - &payer.pubkey(), - None, - DECIMALS, - ) - .expect("initialize_mint2 should build"); - send_with_signers(svm, payer, &[mint], &[create, initialize]) + + send_with_signers(svm, payer, &[mint], &instructions) .unwrap_or_else(|error| panic!("mint creation should succeed: {error:?}")); mint.pubkey() } +/// Close `mint`, whose close authority must be `payer`, refunding its rent to `payer`. +pub fn close_mint(svm: &mut LiteSVM, payer: &Keypair, mint: &Pubkey) { + let ix = close_account( + &TokenProgram::Token2022.address(), + 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", + ); +} + +/// The length a token account for `mint` has to be allocated at. +/// +/// A legacy account is always the base layout; a Token-2022 one has to make room +/// for whatever account extensions its mint's own extensions force, which is +/// read back off the mint rather than being passed in, so this answers for a +/// mint created anywhere. +fn token_account_len_for(svm: &LiteSVM, mint: &Pubkey, token_program: &Pubkey) -> usize { + if token_program == &TokenProgram::SplToken.address() { + return Account::LEN; + } + let data = svm + .get_account(mint) + .unwrap_or_else(|| panic!("{mint} should exist on-chain")) + .data; + let mint_extensions = StateWithExtensions::::unpack(&data) + .expect("the mint should be a valid mint account") + .get_extension_types() + .expect("the mint's extension list should be readable"); + ExtensionType::try_calculate_account_len::( + &ExtensionType::get_required_init_account_extensions(&mint_extensions), + ) + .expect("every account extension a test mint forces has a fixed length") +} + /// 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`. @@ -141,12 +213,13 @@ pub fn create_token_account( owner: &Pubkey, ) -> Pubkey { let token_program = program_of(svm, mint); + let space = token_account_len_for(svm, mint, &token_program); let account = unique_keypair(); let create = system_create_account( &payer.pubkey(), &account.pubkey(), - svm.minimum_balance_for_rent_exemption(Account::LEN), - Account::LEN as u64, + svm.minimum_balance_for_rent_exemption(space), + space as u64, &token_program, ); let initialize = initialize_account3(&token_program, &account.pubkey(), mint, owner) @@ -293,7 +366,7 @@ pub fn cloned_token_under_unsupported_program( ) -> (Pubkey, Pubkey) { // Build the genuine article first, so what gets cloned is a real token's // bytes rather than a test's idea of them. - let mint = create_mint_under(svm, payer, &TokenProgram::SplToken.address()); + let mint = create_mint(svm, payer); let account = create_token_account(svm, payer, &mint, owner); fund_and_delegate(svm, program_id, payer, &account, amount); diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index b1a54b8e..bf6278a3 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -106,6 +106,14 @@ impl Extensions { extensions } + /// The data length the mint itself has to be allocated at. Token-2022 + /// insists on exactly the length its extensions need, so this is what + /// [`super::token::create_mint_at_under`] allocates a mint under it at. + pub(crate) fn mint_len(self) -> usize { + ExtensionType::try_calculate_account_len::(self.mint()) + .expect("every mint extension used here has a fixed length") + } + /// 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 { @@ -117,7 +125,7 @@ impl Extensions { /// 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 { + pub(crate) fn initializers(self, mint: &Pubkey, authority: &Pubkey) -> Vec { self.mint() .iter() .map(|extension| { @@ -145,73 +153,3 @@ impl Extensions { .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 4cfe9028..902d41be 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -551,7 +551,12 @@ fn sizes_a_token_2022_buffer_to_the_extensions_its_mint_forces() { Extensions::CloseAuthorityAndNonTransferable, Extensions::CloseAuthorityAndTransferFee, ] { - let mint = common::token_2022::create_mint(&mut svm, &payer, &unique_keypair(), extensions); + let mint = common::token::create_mint_under( + &mut svm, + &payer, + &TokenProgram::Token2022.address(), + extensions, + ); let (buffer_pda, _bump) = find_buffer_pda(&program_id, &mint); let ix = CreateBuffers { @@ -647,10 +652,10 @@ fn bench_assert_known_max_token_2022_buffer_count() { let probe = loop { let mints: Vec = (0..n) .map(|_| { - common::token_2022::create_mint( + common::token::create_mint_under( &mut svm, &payer, - &unique_keypair(), + &TokenProgram::Token2022.address(), Extensions::default(), ) }) diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index 4c9dd4c4..b1a6c88f 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -377,11 +377,16 @@ fn buffer_whose_mint_was_reopened( 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 mint = common::token::create_mint_at_under( + svm, + payer, + &mint_keypair, + &TokenProgram::Token2022.address(), + Extensions::CloseAuthorityOnly, + ); let buffer_pda = common::buffer::ensure_buffer_exists(svm, program_id, payer, &mint); - common::token_2022::close_mint(svm, payer, &mint); + common::token::close_mint(svm, payer, &mint); reopen(svm, payer, &mint_keypair); (mint, buffer_pda) @@ -404,10 +409,11 @@ fn reclaims_a_buffer_whose_mint_was_reopened_with_another_extension() { &program_id, &payer, |svm, payer, mint_keypair| { - common::token_2022::create_mint( + common::token::create_mint_at_under( svm, payer, mint_keypair, + &TokenProgram::Token2022.address(), Extensions::CloseAuthorityAndNonTransferable, ); }, @@ -458,7 +464,13 @@ fn reclaims_a_buffer_whose_mint_was_reopened_as_a_legacy_mint() { &program_id, &payer, |svm, payer, mint_keypair| { - common::token::create_mint_at(svm, payer, mint_keypair); + common::token::create_mint_at_under( + svm, + payer, + mint_keypair, + &TokenProgram::SplToken.address(), + Extensions::default(), + ); }, ); assert_eq!( diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index a85b1316..16921636 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -5,7 +5,9 @@ use crate::common::{ buffer, order::OrderBuilder, settlement::{BEGIN_INDEX, FINALIZE_INDEX}, - setup_settle_ready, token, unique_pubkey, + setup_settle_ready, token, + token_2022::Extensions, + unique_pubkey, }; use cow_settlement_client::cow_settlement_interface::{ data::intent::OrderIntent, token_program::TokenProgram, Instruction, @@ -109,8 +111,11 @@ fn order_across( sell_program: &Pubkey, buy_program: &Pubkey, ) -> OrderIntent { - let sell_mint = token::create_mint_under(svm, payer, sell_program); - let buy_mint = token::create_mint_under(svm, payer, buy_program); + // Bare mints: what these tests vary is which program a token lives under, + // and a transfer-fee mint would refuse the unchecked `Transfer` the program + // settles with before the crossing under test got a chance to matter. + let sell_mint = token::create_mint_under(svm, payer, sell_program, Extensions::None); + let buy_mint = token::create_mint_under(svm, payer, buy_program, Extensions::None); let intent = OrderBuilder::new(svm, program_id, payer) .salt(salt) .sell_mint(&sell_mint) From c86904830f8768a9b03f33fb4f0ba88671e84c8b Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:10:38 +0900 Subject: [PATCH 10/38] lint fixes --- programs/settlement/tests/common/token.rs | 12 +++--------- programs/settlement/tests/common/token_2022.rs | 18 ++---------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 6596180e..6e0bbc45 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -29,7 +29,7 @@ use solana_sdk::{ use solana_system_interface::instruction::create_account as system_create_account; use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; use spl_token_2022_interface::{ - extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions}, + extension::{account_len::try_calculate_account_len_from_mint_data, StateWithExtensions}, instruction::{ approve, close_account, initialize_account3, initialize_mint2, mint_to as mint_to_ix, transfer_checked as transfer_checked_ix, @@ -193,14 +193,8 @@ fn token_account_len_for(svm: &LiteSVM, mint: &Pubkey, token_program: &Pubkey) - .get_account(mint) .unwrap_or_else(|| panic!("{mint} should exist on-chain")) .data; - let mint_extensions = StateWithExtensions::::unpack(&data) - .expect("the mint should be a valid mint account") - .get_extension_types() - .expect("the mint's extension list should be readable"); - ExtensionType::try_calculate_account_len::( - &ExtensionType::get_required_init_account_extensions(&mint_extensions), - ) - .expect("every account extension a test mint forces has a fixed length") + try_calculate_account_len_from_mint_data(&data, &[]) + .expect("the mint should be a valid mint whose extensions have a fixed length") } /// Create an initialized SPL token account for `mint` whose SPL owner is diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs index bf6278a3..5bd7ea62 100644 --- a/programs/settlement/tests/common/token_2022.rs +++ b/programs/settlement/tests/common/token_2022.rs @@ -7,30 +7,16 @@ //! 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 solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 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, - }, + instruction::{initialize_mint_close_authority, initialize_non_transferable_mint}, state::{Account, Mint}, }; /// The Token-2022 program, spelled once so the builders below can take it. 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::CloseAuthorityAndTransferFee`]. /// nothing reads them back, but `InitializeTransferFeeConfig` demands values. pub const FEE_BASIS_POINTS: u64 = 50; From 4ee6eccab439328cfff6672a7a9455dc11d07b72 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:42:40 +0900 Subject: [PATCH 11/38] simplify token program dependency logic a bit --- client/src/instruction/begin_settle.rs | 16 +- client/src/instruction/finalize_settle.rs | 14 +- client/src/instruction/mod.rs | 2 +- client/src/parse.rs | 6 +- interface/src/instruction/settle/begin.rs | 39 ++--- interface/src/instruction/settle/finalize.rs | 54 +++--- interface/src/instruction/settle/mod.rs | 4 +- interface/src/token_program.rs | 164 ++++++------------ .../settlement/src/processor/begin_settle.rs | 6 +- .../settlement/tests/begin_settle_orders.rs | 29 ++-- .../settlement/tests/common/settlement.rs | 8 +- .../tests/finalize_settle_pushes.rs | 12 +- .../tests/matching_begin_finalize.rs | 22 +-- .../settlement/tests/program_deployment.rs | 6 +- .../settlement/tests/settle_solver_auth.rs | 8 +- .../settlement/tests/settle_token_programs.rs | 39 ++--- test-cli/src/cmd/settle.rs | 5 +- 17 files changed, 189 insertions(+), 245 deletions(-) diff --git a/client/src/instruction/begin_settle.rs b/client/src/instruction/begin_settle.rs index 241f6d27..a57c5cee 100644 --- a/client/src/instruction/begin_settle.rs +++ b/client/src/instruction/begin_settle.rs @@ -6,9 +6,9 @@ use cow_settlement_interface::{ Instruction, Pubkey, }; -// Reexport the interface's `Pull` and `TokenPrograms` so the client provides +// Reexport the interface's `Pull` and `TokenProgram` so the client provides // all the types a caller needs to build a settlement. -pub use cow_settlement_interface::instruction::settle::{Pull, TokenPrograms}; +pub use cow_settlement_interface::instruction::settle::{Pull, TokenProgram}; /// An order ready to be settled, together with the funds to pull from it: /// `intent` identifies the order and `pulls` lists the [`Pull`]s to make from @@ -26,10 +26,10 @@ pub struct BeginSettle<'a> { /// The off-chain auction this settlement executes, carried so it can be tied /// back to its auction off-chain. pub auction_id: i64, - /// The token programs owning the accounts this settlement pulls from and - /// pays into. Leaving one out makes its accounts unsettleable here, so this - /// has to cover every one of them. - pub token_programs: TokenPrograms, + /// Replaces any token program not corresponding with what is given + /// with the system program. Reduces the total number of accounts + /// depended upon by this instruction. + pub only_token_program: Option, pub orders: &'a [InitializedIntent<'a>], } @@ -51,7 +51,7 @@ impl From> for Instruction { solver: builder.solver, finalize_ix_index: builder.finalize_ix_index, auction_id: builder.auction_id, - token_programs: builder.token_programs, + only_token_program: builder.only_token_program, order_pdas: &order_pdas, sell_token_accounts: &sell_token_accounts, pulls: &pull_lists, @@ -95,7 +95,7 @@ mod tests { solver: pubkey_from_seed("solver"), finalize_ix_index, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &orders, }); diff --git a/client/src/instruction/finalize_settle.rs b/client/src/instruction/finalize_settle.rs index 3f86e78e..3583fa0b 100644 --- a/client/src/instruction/finalize_settle.rs +++ b/client/src/instruction/finalize_settle.rs @@ -6,7 +6,7 @@ use cow_settlement_interface::{ Instruction, Pubkey, }; -use super::begin_settle::TokenPrograms; +use super::begin_settle::TokenProgram; /// A settled order whose proceeds are pushed to it: `intent` identifies the /// order (its `buy_token_account` is the push destination and its `buy_mint` @@ -29,10 +29,10 @@ pub struct FinalizedIntent<'a> { pub struct FinalizeSettle<'a> { pub program_id: Pubkey, pub begin_ix_index: u16, - /// The token programs owning the buffers and buy token accounts this - /// settlement pushes between, filled the same way as - /// [`BeginSettle`](super::begin_settle::BeginSettle)'s. - pub token_programs: TokenPrograms, + /// Replaces any token program not corresponding with what is given + /// with the system program. Reduces the total number of accounts + /// depended upon by this instruction. + pub only_token_program: Option, pub orders: &'a [FinalizedIntent<'a>], } @@ -66,7 +66,7 @@ impl From> for Instruction { program_id: builder.program_id, state_pda, begin_ix_index: builder.begin_ix_index, - token_programs: builder.token_programs, + only_token_program: builder.only_token_program, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, @@ -114,7 +114,7 @@ mod tests { let ix = Instruction::from(FinalizeSettle { program_id, begin_ix_index, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &orders, }); diff --git a/client/src/instruction/mod.rs b/client/src/instruction/mod.rs index 0bbc331a..adfc144f 100644 --- a/client/src/instruction/mod.rs +++ b/client/src/instruction/mod.rs @@ -16,7 +16,7 @@ pub mod remove_solver; pub mod transfer_authority; pub use add_solver::AddSolver; -pub use begin_settle::{BeginSettle, InitializedIntent, Pull, TokenPrograms}; +pub use begin_settle::{BeginSettle, InitializedIntent, Pull, TokenProgram}; pub use create_buffer::CreateBuffers; pub use create_order::CreateOrder; pub use finalize_settle::{FinalizeSettle, FinalizedIntent}; diff --git a/client/src/parse.rs b/client/src/parse.rs index fb017c41..4075a007 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -79,7 +79,7 @@ mod tests { use super::*; use crate::instruction::{ AddSolver, BeginSettle, CreateBuffers, CreateOrder, FinalizeSettle, Initialize, - InitializedIntent, RemoveSolver, TokenPrograms, + InitializedIntent, RemoveSolver, }; use cow_settlement_interface::{ data::intent::fixtures::sample_intent, @@ -125,7 +125,7 @@ mod tests { solver: payer, finalize_ix_index: 1, auction_id: 42, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[InitializedIntent { intent: &intent, pulls: &[], @@ -135,7 +135,7 @@ mod tests { SettlementInstruction::FinalizeSettle => FinalizeSettle { program_id, begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], } .into(), diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 236f5260..eee76f3f 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -9,7 +9,7 @@ use solana_pubkey::Pubkey; use crate::instruction::InstructionInputParsing; use crate::{SettlementError, SettlementInstruction}; -use super::{recover_counterpart, TokenPrograms, INSTRUCTIONS_SYSVAR_ID}; +use super::{recover_counterpart, TokenProgram, INSTRUCTIONS_SYSVAR_ID}; /// A single transfer made when settling an order: `amount` tokens sent from the /// order's sell token account to `destination`. @@ -55,9 +55,9 @@ pub struct BeginSettle<'a> { /// instruction data so the settlement can be tied back to its auction /// off-chain, unused on-chain. pub auction_id: i64, - /// The token programs this settlement carries, one slot each; see - /// [`TokenPrograms`]. - pub token_programs: TokenPrograms, + /// The only token program this settlement's transfers are issued against, + /// or `None` to name every supported one; see [`TokenProgram::addresses`]. + pub only_token_program: Option, pub order_pdas: &'a [Pubkey], pub sell_token_accounts: &'a [Pubkey], pub pulls: &'a [&'a [Pull]], @@ -71,7 +71,7 @@ impl From> for Instruction { solver, finalize_ix_index, auction_id, - token_programs, + only_token_program, order_pdas, sell_token_accounts, pulls, @@ -108,8 +108,7 @@ impl From> for Instruction { AccountMeta::new_readonly(state_pda, false), ]; accounts.extend( - token_programs - .addresses() + TokenProgram::addresses(only_token_program) .map(|address| AccountMeta::new_readonly(address, false)), ); for &i in &order { @@ -331,7 +330,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: 0x0102_0304_0506_0708, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), order_pdas: &[], sell_token_accounts: &[], pulls: &[], @@ -360,15 +359,15 @@ mod tests { assert_readonly_nonsigner(&accounts[4], SYSTEM_PROGRAM_ID); } - /// The token-program slots are whatever [`TokenPrograms`] says, in its own - /// order, so a settlement can carry both programs — or leave either one out. + /// The token-program slots are the addresses the settlement's + /// `only_token_program` names, in [`TokenProgram::ALL`] order, so a + /// settlement can name both programs — or leave either one out. #[test] fn begin_settle_carries_the_token_program_slots_it_is_given() { - for token_programs in [ - TokenPrograms::SPL_TOKEN, - TokenPrograms::TOKEN_2022, - TokenPrograms::BOTH, - TokenPrograms::NONE, + for only_token_program in [ + None, + Some(TokenProgram::SplToken), + Some(TokenProgram::Token2022), ] { let Instruction { accounts, .. } = Instruction::from(BeginSettle { program_id: Pubkey::new_unique(), @@ -376,7 +375,7 @@ mod tests { solver: Pubkey::new_unique(), finalize_ix_index: 0, auction_id: 0, - token_programs, + only_token_program, order_pdas: &[], sell_token_accounts: &[], pulls: &[], @@ -384,8 +383,8 @@ mod tests { let slots: Vec = accounts[3..].iter().map(|meta| meta.pubkey).collect(); assert_eq!( slots, - token_programs.addresses(), - "{token_programs:?} should be laid out as its own addresses", + TokenProgram::addresses(only_token_program), + "{only_token_program:?} should be laid out as its own addresses", ); } } @@ -407,7 +406,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: AUCTION_ID, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), order_pdas: &[high_order_pda, low_order_pda], sell_token_accounts: &[high_sell_token_account, low_sell_token_account], pulls: &[&[], &[]], @@ -478,7 +477,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: AUCTION_ID, - token_programs: TokenPrograms::BOTH, + only_token_program: None, order_pdas: &[order_a, order_b], sell_token_accounts: &[sell_a, sell_b], pulls: &[ diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index d7e3d29c..e1d8a5c3 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -9,7 +9,7 @@ use solana_pubkey::Pubkey; use crate::instruction::InstructionInputParsing; use crate::{recover_discriminator, SettlementError, SettlementInstruction}; -use super::{recover_counterpart, TokenPrograms, INSTRUCTIONS_SYSVAR_ID}; +use super::{recover_counterpart, TokenProgram, INSTRUCTIONS_SYSVAR_ID}; /// The number of fixed accounts every `FinalizeSettle` carries before its push /// accounts: the instructions sysvar, the settlement state PDA, and one slot per @@ -82,9 +82,10 @@ pub fn finalize_push_data( /// Required accounts: /// `[instructions_sysvar (R), state_pda (R), spl_token_program (R), /// token_2022_program (R)]` followed, per push, by `[source_buffer (W), -/// destination (W)]`. The two token programs are the slots [`TokenPrograms`] -/// describes, there to name the programs this instruction's pushes are issued -/// against; the matching `BeginSettle` carries the ones its pulls need. +/// destination (W)]`. The two token programs are the slots +/// [`TokenProgram::addresses`] describes, there to name the programs this +/// instruction's pushes are issued against; the matching `BeginSettle` carries +/// the ones its pulls need. /// /// `FinalizeSettle` only executes the transfers. Every push is validated by /// `BeginSettle`, which reads this instruction through introspection. @@ -92,9 +93,9 @@ pub struct FinalizeSettle<'a> { pub program_id: Pubkey, pub state_pda: Pubkey, pub begin_ix_index: u16, - /// The token programs this settlement carries, one slot each; see - /// [`TokenPrograms`]. - pub token_programs: TokenPrograms, + /// The only token program this settlement's transfers are issued against, + /// or `None` to name every supported one; see [`TokenProgram::addresses`]. + pub only_token_program: Option, pub source_buffers: &'a [Pubkey], pub destinations: &'a [Pubkey], pub bumps: &'a [u8], @@ -107,7 +108,7 @@ impl From> for Instruction { program_id, state_pda, begin_ix_index, - token_programs, + only_token_program, source_buffers, destinations, bumps, @@ -125,8 +126,7 @@ impl From> for Instruction { AccountMeta::new_readonly(state_pda, false), ]; accounts.extend( - token_programs - .addresses() + TokenProgram::addresses(only_token_program) .map(|address| AccountMeta::new_readonly(address, false)), ); for (source, destination) in source_buffers.iter().zip(destinations) { @@ -280,7 +280,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[], destinations: &[], bumps: &[], @@ -301,7 +301,7 @@ mod tests { program_id, state_pda, begin_ix_index: 0x1337, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[], destinations: &[], bumps: &[], @@ -328,21 +328,21 @@ mod tests { assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID); } - /// The token-program slots are whatever [`TokenPrograms`] says, in its own - /// order, so a settlement can carry both programs — or leave either one out. + /// The token-program slots are the addresses the settlement's + /// `only_token_program` names, in [`TokenProgram::ALL`] order, so a + /// settlement can name both programs — or leave either one out. #[test] fn finalize_settle_carries_the_token_program_slots_it_is_given() { - for token_programs in [ - TokenPrograms::SPL_TOKEN, - TokenPrograms::TOKEN_2022, - TokenPrograms::BOTH, - TokenPrograms::NONE, + for only_token_program in [ + None, + Some(TokenProgram::SplToken), + Some(TokenProgram::Token2022), ] { let ix = Instruction::from(FinalizeSettle { program_id: Pubkey::new_unique(), state_pda: Pubkey::new_unique(), begin_ix_index: 0, - token_programs, + only_token_program, source_buffers: &[], destinations: &[], bumps: &[], @@ -351,8 +351,8 @@ mod tests { let slots: Vec = ix.accounts[2..].iter().map(|meta| meta.pubkey).collect(); assert_eq!( slots, - token_programs.addresses(), - "{token_programs:?} should be laid out as its own addresses", + TokenProgram::addresses(only_token_program), + "{only_token_program:?} should be laid out as its own addresses", ); } } @@ -370,7 +370,7 @@ mod tests { program_id, state_pda, begin_ix_index: 0x1337, - token_programs: TokenPrograms::BOTH, + only_token_program: None, source_buffers: &[source_a, source_b], destinations: &[dest_a, dest_b], bumps: &[0xa1, 0xb1], @@ -631,7 +631,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0x1337, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[ pubkey_from_seed("source buffer 0"), pubkey_from_seed("source buffer 1"), @@ -655,7 +655,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[], destinations: &[], bumps: &[], @@ -671,7 +671,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[pubkey_from_seed("source buffer")], destinations: &[pubkey_from_seed("destination")], bumps: &[0xff], @@ -706,7 +706,7 @@ mod tests { program_id, state_pda, begin_ix_index, - token_programs: TokenPrograms::BOTH, + only_token_program: None, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index 53014458..6301560f 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -1,13 +1,13 @@ //! `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 crate::Pubkey; use solana_program_error::ProgramError; /// The legacy SPL Token program, which the builders below target by default. pub const SPL_TOKEN_PROGRAM_ID: Pubkey = TokenProgram::SplToken.address(); -pub use crate::token_program::TokenPrograms; +pub use crate::token_program::TokenProgram; 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 b5beead3..f21e3f21 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -3,10 +3,6 @@ use crate::Pubkey; use solana_program_error::ProgramError; -/// The program a [`TokenPrograms`] slot carries when the settlement moves no -/// token under that program. The system program is named by nearly every -/// settlement transaction already, so standing it in costs one more account -/// index rather than another 32-byte address. pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; /// A token program a token-moving instruction accepts. @@ -21,7 +17,7 @@ pub enum TokenProgram { impl TokenProgram { /// Every supported token program. The single list [`TryFrom`] resolves /// addresses against, and the order `BeginSettle` and `FinalizeSettle` lay - /// their token-program accounts out in; see [`TokenPrograms::addresses`]. + /// their token-program accounts out in; see [`Self::addresses`]. pub const ALL: [Self; 2] = [Self::SplToken, Self::Token2022]; /// The address the program is deployed at. @@ -31,6 +27,38 @@ impl TokenProgram { Self::Token2022 => spl_token_2022_interface::ID, } } + + /// The addresses a `BeginSettle`/`FinalizeSettle` pair puts in its + /// token-program slots, one per entry of [`Self::ALL`] and in that order. + /// + /// Both instructions take one account per supported program, at fixed + /// positions, and issue each transfer against the program that owns the + /// account it moves — so a settlement naming every program may mix tokens + /// from both. `only_token_program` is what narrows that: `None` names them + /// all, and `Some(program)` names just that one, leaving + /// [`SYSTEM_PROGRAM_ID`] in every other slot. + pub const fn addresses(only_token_program: Option) -> [Pubkey; Self::ALL.len()] { + let [spl_token, token_2022] = Self::ALL; + [ + spl_token.slot(only_token_program), + token_2022.slot(only_token_program), + ] + } + + /// The address this program's own slot holds. The slots are not read + /// on-chain, so a program the settlement doesn't touch is left out by + /// standing [`SYSTEM_PROGRAM_ID`] in: nearly every settlement transaction + /// names the system program already, so it costs one more account index + /// rather than another 32-byte address. + const fn slot(self, only_token_program: Option) -> Pubkey { + match only_token_program { + // Compared as discriminants because `PartialEq` isn't const. That + // keeps the narrowing correct for any variant added to `ALL`, + // rather than making this a second place to list them. + Some(only) if only as u8 != self as u8 => SYSTEM_PROGRAM_ID, + _ => self.address(), + } + } } impl TryFrom<&Pubkey> for TokenProgram { @@ -46,78 +74,6 @@ impl TryFrom<&Pubkey> for TokenProgram { } } -/// Which of [`TokenProgram::ALL`] a `BeginSettle`/`FinalizeSettle` pair -/// carries. -/// -/// Both instructions take one account per supported program, at fixed positions -/// and in [`TokenProgram::ALL`] order, and issue each transfer against the -/// program that owns the account it moves — so a single settlement may mix -/// tokens from both. The slots are what name those programs; they are not read -/// on-chain, and a program the settlement doesn't touch is left out by putting -/// [`SYSTEM_PROGRAM_ID`] in its slot. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct TokenPrograms { - /// Whether the legacy SPL Token program's slot carries the program rather - /// than the placeholder. - pub spl_token: bool, - /// Whether Token-2022's slot carries the program rather than the - /// placeholder. - pub token_2022: bool, -} - -impl TokenPrograms { - /// The legacy SPL Token program alone. - pub const SPL_TOKEN: Self = Self { - spl_token: true, - token_2022: false, - }; - - /// Token-2022 alone. - pub const TOKEN_2022: Self = Self { - spl_token: false, - token_2022: true, - }; - - /// Both programs, for a settlement mixing tokens from each. - pub const BOTH: Self = Self { - spl_token: true, - token_2022: true, - }; - - /// Neither program: every slot is the placeholder. Only a settlement that - /// moves no tokens at all can be built this way. - pub const NONE: Self = Self { - spl_token: false, - token_2022: false, - }; - - /// The addresses to pass, one per entry of [`TokenProgram::ALL`] and in - /// that order: the program itself where the settlement needs it, and - /// [`SYSTEM_PROGRAM_ID`] where it doesn't. - pub const fn addresses(self) -> [Pubkey; TokenProgram::ALL.len()] { - let [spl_token, token_2022] = TokenProgram::ALL; - [self.slot(spl_token), self.slot(token_2022)] - } - - /// The address `program`'s own slot holds. - const fn slot(self, program: TokenProgram) -> Pubkey { - if self.carries(program) { - program.address() - } else { - SYSTEM_PROGRAM_ID - } - } - - /// Whether `program`'s slot carries it rather than the placeholder. The one - /// place a new [`TokenProgram`] variant has to be given a slot. - const fn carries(self, program: TokenProgram) -> bool { - match program { - TokenProgram::SplToken => self.spl_token, - TokenProgram::Token2022 => self.token_2022, - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -159,42 +115,36 @@ mod tests { ); } - /// Every combination puts each program in its own slot, and the placeholder - /// wherever the settlement said it isn't needed. + /// Naming every program puts each of them in its own slot, in the order + /// the on-chain side pairs a slot with the program it stands for by. #[test] - fn addresses_fill_each_slot_with_its_program_or_the_placeholder() { - let spl_token = TokenProgram::SplToken.address(); - let token_2022 = TokenProgram::Token2022.address(); - assert_eq!(TokenPrograms::BOTH.addresses(), [spl_token, token_2022]); - assert_eq!( - TokenPrograms::SPL_TOKEN.addresses(), - [spl_token, SYSTEM_PROGRAM_ID], - ); + fn every_program_is_named_when_the_settlement_is_not_narrowed() { assert_eq!( - TokenPrograms::TOKEN_2022.addresses(), - [SYSTEM_PROGRAM_ID, token_2022], - ); - assert_eq!( - TokenPrograms::NONE.addresses(), - [SYSTEM_PROGRAM_ID, SYSTEM_PROGRAM_ID], - ); - } - - /// The slots are laid out in [`TokenProgram::ALL`] order, which is what - /// lets the on-chain side pair a slot with the program it stands for by - /// position alone. - #[test] - fn addresses_follow_the_supported_program_order() { - assert_eq!( - TokenPrograms::BOTH.addresses(), + TokenProgram::addresses(None), TokenProgram::ALL.map(TokenProgram::address), ); } - /// Carrying nothing is the default, so a builder that forgets its token - /// programs settles no tokens rather than silently picking one. + /// Narrowing to one program keeps that program in its own slot and leaves + /// the placeholder everywhere else, so a settlement pays for the addresses + /// of only the programs it touches. #[test] - fn no_program_is_carried_by_default() { - assert_eq!(TokenPrograms::default(), TokenPrograms::NONE); + fn narrowing_to_one_program_leaves_the_placeholder_in_every_other_slot() { + for (named, only) in TokenProgram::ALL.into_iter().enumerate() { + let addresses = TokenProgram::addresses(Some(only)); + for (slot, (address, program)) in + addresses.into_iter().zip(TokenProgram::ALL).enumerate() + { + let expected = if slot == named { + only.address() + } else { + SYSTEM_PROGRAM_ID + }; + assert_eq!( + address, expected, + "a settlement narrowed to {only:?} should not name {program:?}", + ); + } + } } } diff --git a/programs/settlement/src/processor/begin_settle.rs b/programs/settlement/src/processor/begin_settle.rs index b3abf51e..2517c475 100644 --- a/programs/settlement/src/processor/begin_settle.rs +++ b/programs/settlement/src/processor/begin_settle.rs @@ -420,9 +420,7 @@ mod tests { use cow_settlement_interface::data::intent::Flags; use cow_settlement_interface::instruction::fixtures::fake_account; use cow_settlement_interface::instruction::settle::fixtures::arb_pushes; - use cow_settlement_interface::instruction::settle::{ - FinalizeSettle, FinalizeSettleInput, TokenPrograms, - }; + use cow_settlement_interface::instruction::settle::{FinalizeSettle, FinalizeSettleInput}; use cow_settlement_interface::instruction::InstructionInputParsing; use cow_settlement_interface::Pubkey; use proptest::prelude::*; @@ -1027,7 +1025,7 @@ mod tests { program_id: Pubkey::new_from_array(program_id), state_pda: Pubkey::new_from_array(state_pda), begin_ix_index, - token_programs: TokenPrograms::BOTH, + only_token_program: None, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 43a5712b..48c35ff0 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -37,7 +37,7 @@ use cow_settlement_client::cow_settlement_interface::{ Instruction, SettlementError, SettlementInstruction, }; use cow_settlement_client::instruction::{ - BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, + BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenProgram, }; use cow_settlement_interface::data::intent::OrderIntent; use litesvm::LiteSVM; @@ -138,13 +138,13 @@ fn settle_and_pay_amounts( solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders, }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &settled, }; vec![begin.into(), finalize.into()] @@ -257,7 +257,7 @@ fn rejects_fabricated_program_owned_account() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), order_pdas: &[fake_order], sell_token_accounts: &[sell_token], pulls: &no_pulls(1), @@ -268,7 +268,7 @@ fn rejects_fabricated_program_owned_account() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[unique_pubkey()], destinations: &[intent.buy_token_account], bumps: &[0], @@ -299,7 +299,7 @@ fn rejects_non_order_account_in_order_slot() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), order_pdas: &[sell_token], sell_token_accounts: &[sell_token], pulls: &no_pulls(1), @@ -309,7 +309,7 @@ fn rejects_non_order_account_in_order_slot() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[unique_pubkey()], destinations: &[unique_pubkey()], bumps: &[0], @@ -549,8 +549,7 @@ fn rejects_orders_in_wrong_address_order() { AccountMeta::new_readonly(find_state_pda(&program_id).0, false), ]; accounts.extend( - TokenPrograms::SPL_TOKEN - .addresses() + TokenProgram::addresses(Some(TokenProgram::SplToken)) .map(|program| AccountMeta::new_readonly(program, false)), ); for (order_pda, intent) in orders { @@ -581,7 +580,7 @@ fn rejects_orders_in_wrong_address_order() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, @@ -1151,7 +1150,7 @@ fn rejects_push_to_wrong_destination() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &orders, }); // Redirect the push to an account that isn't the order's buy token account. @@ -1185,7 +1184,7 @@ fn rejects_push_if_buffer_does_not_match_buy_mint() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), source_buffers: &[other_buffer], destinations: &[intent.buy_token_account], bumps: &[other_bump], @@ -1212,7 +1211,7 @@ fn rejects_fewer_pushes_than_orders() { let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }; @@ -1233,7 +1232,7 @@ fn rejects_more_pushes_than_orders() { let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[FinalizedIntent { intent: &intent, amount: 0, @@ -1259,7 +1258,7 @@ fn rejects_partial_push_amount_in_finalize_settle() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &orders, }); // Drop one byte from the finalize intstruction so the trailing amount is no diff --git a/programs/settlement/tests/common/settlement.rs b/programs/settlement/tests/common/settlement.rs index cf2b2735..59701229 100644 --- a/programs/settlement/tests/common/settlement.rs +++ b/programs/settlement/tests/common/settlement.rs @@ -1,7 +1,7 @@ //! Scaffolding for building `[BeginSettle, FinalizeSettle]` settlement pairs. use cow_settlement_client::instruction::{ - BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, + BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenProgram, }; use cow_settlement_interface::{data::intent::OrderIntent, Instruction}; use litesvm::LiteSVM; @@ -40,7 +40,7 @@ pub fn build_settlement( solver: *solver, finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &begin_orders, }; vec![begin.into(), finalize.into()] @@ -132,13 +132,13 @@ pub fn build_staged_settlement( solver: *solver, finalize_ix_index: finalize_index(between.len()), auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &begin_orders, }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &finalize_orders, }; diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index 746bf2f9..b450e70a 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -21,7 +21,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::intent::OrderIntent, instruction::settle::SPL_TOKEN_PROGRAM_ID, pda::state::find_state_pda, Instruction, SettlementError, }; -use cow_settlement_client::instruction::{FinalizeSettle, FinalizedIntent, TokenPrograms}; +use cow_settlement_client::instruction::{FinalizeSettle, FinalizedIntent, TokenProgram}; use litesvm_token::spl_token::error::TokenError; use solana_sdk::{ instruction::InstructionError, program_error::ProgramError, pubkey::Pubkey, signer::Signer, @@ -45,7 +45,7 @@ fn finalize(program_id: &Pubkey, solver: &Pubkey, orders: &[FinalizedIntent]) -> let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders, }; build_settlement(program_id, solver, orders, finalize) @@ -254,7 +254,7 @@ fn rejects_push_account_count_mismatch() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &orders, }); // ...with another push's worth of data bytes appended but no matching @@ -280,7 +280,7 @@ fn rejects_too_few_accounts() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }); // ...with one of its fixed accounts popped. `BeginSettle` runs first @@ -373,7 +373,7 @@ fn rejects_two_too_few_accounts() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &orders, }); // ...with that push's whole (source, destination) pair popped, so the data @@ -403,7 +403,7 @@ fn rejects_partial_push_amount() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &orders, }); // Drop one byte so the trailing amount is no longer a whole `u64`. diff --git a/programs/settlement/tests/matching_begin_finalize.rs b/programs/settlement/tests/matching_begin_finalize.rs index d92d40e9..ab6ffac6 100644 --- a/programs/settlement/tests/matching_begin_finalize.rs +++ b/programs/settlement/tests/matching_begin_finalize.rs @@ -1,5 +1,5 @@ use cow_settlement_client::cow_settlement_interface::{SettlementError, SettlementInstruction}; -use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle, TokenPrograms}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle, TokenProgram}; use litesvm::{types::FailedTransactionMetadata, LiteSVM}; use solana_sdk::{ instruction::{AccountMeta, Instruction, InstructionError}, @@ -40,14 +40,14 @@ fn run_sequence( solver: solver.pubkey(), finalize_ix_index: *idx, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], } .into(), AbstractInstruction::Fin(idx) => FinalizeSettle { program_id: *program_id, begin_ix_index: *idx, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], } .into(), @@ -195,7 +195,7 @@ fn rejects_non_instructions_sysvar_account_at_position_one() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], } .into(); @@ -203,7 +203,7 @@ fn rejects_non_instructions_sysvar_account_at_position_one() { let finalize = FinalizeSettle { program_id, begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }; @@ -235,7 +235,7 @@ fn rejects_counterpart_instruction_in_different_program() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }; // We build a transaction that looks like a valid finalize_settle but @@ -244,7 +244,7 @@ fn rejects_counterpart_instruction_in_different_program() { let stranger = FinalizeSettle { program_id: solana_system_interface::program::ID, begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }; @@ -299,7 +299,7 @@ fn rejects_cpi_call_to_begin_settle() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }, ); @@ -332,7 +332,7 @@ fn rejects_cpi_call_to_finalize_settle() { FinalizeSettle { program_id: settlement_id, begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }, ); @@ -366,7 +366,7 @@ fn rejects_counterpart_with_unrecoverable_discriminator() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }; // Uses the settlement program, but no data: `recover_discriminator` fails @@ -409,7 +409,7 @@ fn rejects_counterpart_with_unrecoverable_counterpart_index() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], }; // Same program as `begin`, with a valid discriminator but no trailing diff --git a/programs/settlement/tests/program_deployment.rs b/programs/settlement/tests/program_deployment.rs index 7606ff85..40ab3c3f 100644 --- a/programs/settlement/tests/program_deployment.rs +++ b/programs/settlement/tests/program_deployment.rs @@ -1,4 +1,4 @@ -use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle, TokenPrograms}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle, TokenProgram}; use solana_sdk::{ instruction::{Instruction, InstructionError}, signature::Signer, @@ -34,14 +34,14 @@ fn program_can_be_invoked() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], } .into(), FinalizeSettle { program_id, begin_ix_index: 0, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &[], } .into(), diff --git a/programs/settlement/tests/settle_solver_auth.rs b/programs/settlement/tests/settle_solver_auth.rs index 4edd3322..09f84c2f 100644 --- a/programs/settlement/tests/settle_solver_auth.rs +++ b/programs/settlement/tests/settle_solver_auth.rs @@ -4,7 +4,7 @@ //! unauthorized caller is rejected before any settlement work happens. use cow_settlement_client::cow_settlement_interface::{Instruction, SettlementError}; -use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle, TokenPrograms}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle}; use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; use crate::common::{ @@ -24,13 +24,13 @@ fn noop_settlement(program_id: &Pubkey, solver: &Pubkey) -> Vec { solver: *solver, finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, - token_programs: TokenPrograms::NONE, + only_token_program: None, orders: &[], }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::NONE, + only_token_program: None, orders: &[], }; vec![begin.into(), finalize.into()] @@ -101,7 +101,7 @@ fn non_signing_solver_may_not_settle() { solver: solver.pubkey(), finalize_ix_index: 0, auction_id: 0, - token_programs: TokenPrograms::NONE, + only_token_program: None, orders: &[], } .into(); diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index 16921636..597595bd 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -9,11 +9,9 @@ use crate::common::{ token_2022::Extensions, unique_pubkey, }; -use cow_settlement_client::cow_settlement_interface::{ - data::intent::OrderIntent, token_program::TokenProgram, Instruction, -}; +use cow_settlement_client::cow_settlement_interface::{data::intent::OrderIntent, Instruction}; use cow_settlement_client::instruction::{ - BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenPrograms, + BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenProgram, }; use litesvm::LiteSVM; use solana_sdk::{ @@ -33,18 +31,19 @@ struct Settled<'a> { } /// Fund and settle `orders` in one `[BeginSettle, FinalizeSettle]` pair, with -/// each instruction carrying the token-program slots it is given. +/// each instruction narrowed to the token program it is given, or naming every +/// one of them when given `None`. /// /// Every account involved is set up under its own mint's program, so the only -/// thing a test varies is which programs the settlement says it carries. +/// thing a test varies is which programs the settlement says it names. fn settle_with( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, solver: &Keypair, orders: &[Settled], - begin_programs: TokenPrograms, - finalize_programs: TokenPrograms, + begin_program: Option, + finalize_program: Option, ) -> Result<(), TransactionError> { let mut initialized: Vec = vec![]; let mut finalized: Vec = vec![]; @@ -81,13 +80,13 @@ fn settle_with( solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, - token_programs: begin_programs, + only_token_program: begin_program, orders: &initialized, }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: finalize_programs, + only_token_program: finalize_program, orders: &finalized, }; let tx = Transaction::new_signed_with_payer( @@ -174,8 +173,8 @@ fn settles_orders_under_both_token_programs_simultaneously() { amount_out: 700, }, ], - TokenPrograms::BOTH, - TokenPrograms::BOTH, + None, + None, ) .expect("a settlement carrying both programs should settle orders under either"); @@ -209,8 +208,8 @@ fn settles_an_order_that_crosses_token_programs() { amount_in: 250, amount_out: 250, }], - TokenPrograms::BOTH, - TokenPrograms::BOTH, + None, + None, ) .expect("an order selling under one program and buying under the other should settle"); @@ -241,8 +240,8 @@ fn settles_token_2022_orders_without_carrying_the_legacy_program() { amount_in: 300, amount_out: 300, }], - TokenPrograms::TOKEN_2022, - TokenPrograms::TOKEN_2022, + Some(TokenProgram::Token2022), + Some(TokenProgram::Token2022), ) .expect("a Token-2022-only settlement should settle Token-2022 orders"); @@ -272,8 +271,8 @@ fn settles_legacy_orders_without_carrying_the_token_2022_program() { amount_in: 500, amount_out: 500, }], - TokenPrograms::SPL_TOKEN, - TokenPrograms::SPL_TOKEN, + Some(TokenProgram::SplToken), + Some(TokenProgram::SplToken), ) .expect("a legacy-only settlement should not have to carry Token-2022"); @@ -313,7 +312,7 @@ fn settles_with_the_token_program_slots_swapped() { solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, - token_programs: TokenPrograms::BOTH, + only_token_program: None, orders: &[InitializedIntent { intent: &intent, pulls: &pulls, @@ -326,7 +325,7 @@ fn settles_with_the_token_program_slots_swapped() { let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - token_programs: TokenPrograms::BOTH, + only_token_program: None, orders: &[FinalizedIntent { intent: &intent, amount: 100, diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 4c9b40ed..db36fd1b 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -9,7 +9,6 @@ use cow_settlement_client::{ }, instruction::{ BeginSettle, CreateBuffers, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, - TokenPrograms, }, }; use solana_hash::Hash; @@ -119,7 +118,7 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { finalize_ix_index, // Token resolution builds legacy SPL accounts throughout (see // `crate::token`), so Token-2022's slot stays empty. - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &initialized_intents, auction_id: 0, }; @@ -136,7 +135,7 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { let finalize_ix = FinalizeSettle { program_id: ctx.program_id, begin_ix_index, - token_programs: TokenPrograms::SPL_TOKEN, + only_token_program: Some(TokenProgram::SplToken), orders: &settled, }; From b86385e98306b61ff7e39ecbe234a7be37013f9f Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:46:24 +0900 Subject: [PATCH 12/38] use destructuring --- programs/settlement/src/processor/utils/token.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/programs/settlement/src/processor/utils/token.rs b/programs/settlement/src/processor/utils/token.rs index 578af0d7..58fd147c 100644 --- a/programs/settlement/src/processor/utils/token.rs +++ b/programs/settlement/src/processor/utils/token.rs @@ -275,11 +275,15 @@ mod tests { TokenProgram::Token2022.address(), &extended_token_2022_account_layout(mint, owner, 7), ); - let read = read_token_account(TokenProgram::Token2022, &account) + let TokenAccount { + mint: read_mint, + owner: read_owner, + amount, + } = read_token_account(TokenProgram::Token2022, &account) .expect("an extended Token-2022 account should read"); - assert_eq!(read.mint, mint); - assert_eq!(read.owner, owner); - assert_eq!(read.amount, 7); + assert_eq!(read_mint, mint); + assert_eq!(read_owner, owner); + assert_eq!(amount, 7); } #[test] From e29a45b8cd7b1ce37fed3d9b2bf7caa4cc8cc361 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:49:38 +0900 Subject: [PATCH 13/38] simplify comment --- programs/settlement/tests/common/token.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 6e0bbc45..aeec8302 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -181,10 +181,7 @@ pub fn close_mint(svm: &mut LiteSVM, payer: &Keypair, mint: &Pubkey) { /// The length a token account for `mint` has to be allocated at. /// -/// A legacy account is always the base layout; a Token-2022 one has to make room -/// for whatever account extensions its mint's own extensions force, which is -/// read back off the mint rather than being passed in, so this answers for a -/// mint created anywhere. +/// The `token_program` should be the owner of the provided mint account. fn token_account_len_for(svm: &LiteSVM, mint: &Pubkey, token_program: &Pubkey) -> usize { if token_program == &TokenProgram::SplToken.address() { return Account::LEN; From 193bbe99da80b7bc4f01b1cb641a7d6b31ee84ea Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:50:21 +0900 Subject: [PATCH 14/38] simplify comment --- programs/settlement/tests/common/token.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index aeec8302..181075ef 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -1,13 +1,4 @@ //! Token helpers for the settlement integration tests. -//! -//! Every helper that acts on an existing token works under whichever token -//! program owns it, read back with [`program_of`], so a test settling -//! Token-2022 accounts uses the same calls as one settling legacy ones. -//! -//! Creating a mint is the one thing with nothing to read the program from. -//! [`create_mint`] takes it from [`active_token::program`], the program the -//! running test is exercising, and [`create_mint_under`] names it outright, for -//! the tests that build mints under both at once. use crate::common::{active_token, token_2022::Extensions}; From d3eb3f61e72d95b8cf42ed3d45bba0ae809cc1ac Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:53:44 +0900 Subject: [PATCH 15/38] simplify comment --- programs/settlement/src/processor/utils/token.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/programs/settlement/src/processor/utils/token.rs b/programs/settlement/src/processor/utils/token.rs index 58fd147c..cb5c02f2 100644 --- a/programs/settlement/src/processor/utils/token.rs +++ b/programs/settlement/src/processor/utils/token.rs @@ -200,8 +200,7 @@ mod tests { ); } - /// A token account of `program`, well-formed but empty of interest: only - /// its owner decides which program its transfers go to. + /// Creates a legacy SPL-compliant token account of `program` fn token_account_of(program: Address) -> AccountView { fake_account_owned_by( pubkey_from_seed("UNRELATED token account"), From 10b5529ffa1334c8181f2fdc96a75070456d210d Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:00:31 +0900 Subject: [PATCH 16/38] fix tests --- programs/settlement/tests/common/mod.rs | 4 ++-- programs/settlement/tests/finalize_settle_pushes.rs | 2 +- programs/settlement/tests/settle_limit_prices.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index f8ca36e6..3fb72a11 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -204,9 +204,9 @@ pub fn assert_instruction_error_at( pub fn assert_settlement_error( ix_idx: u8, result: Result, - expected: SettlementError, + expected: impl Into, ) { - assert_instruction_error_at(ix_idx, result, to_instruction_error(expected)); + assert_instruction_error_at(ix_idx, result, expected); } pub fn create_account_at(svm: &mut LiteSVM, address: Pubkey, owner: &Pubkey, data: &[u8]) { diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index bb3724d0..990e3319 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -354,7 +354,7 @@ fn rejects_buy_account_under_a_unsupported_token_program() { let instructions = finalize(&program_id, &solver.pubkey(), &orders); assert_finalize_error( send(&mut svm, &solver, &instructions), - to_instruction_error(SettlementError::InvalidTokenProgram), + SettlementError::InvalidTokenProgram, ); } diff --git a/programs/settlement/tests/settle_limit_prices.rs b/programs/settlement/tests/settle_limit_prices.rs index 8a375ae3..992459d7 100644 --- a/programs/settlement/tests/settle_limit_prices.rs +++ b/programs/settlement/tests/settle_limit_prices.rs @@ -7,7 +7,7 @@ //! succeeds or is rejected with the expected error. use crate::common::{ - assert_settlement_error, + assert_instruction_error_at, order::OrderBuilder, send, settlement::{build_staged_settlement, stage_order, StagedOrder, BEGIN_INDEX}, From e27e40e9369da0c4cd47ea9ee89541749f07f118 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:03:31 +0900 Subject: [PATCH 17/38] update bench --- bench-report.json | 70 +++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/bench-report.json b/bench-report.json index fe3f2df5..74283ef4 100644 --- a/bench-report.json +++ b/bench-report.json @@ -39,43 +39,43 @@ "transfer_authority/reclaim_authority_can_transfer_itself": 4 }, "compute_units": { - "add_solver/add_with_many_existing_solvers": 5069, - "add_solver/adds_a_solver": 4617, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 7368, - "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 12175, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17344, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 31765, - "create_buffers/max_buffers_in_one_instruction": 164694, - "create_buffers/max_buffers_in_one_instruction_token_2022": 208023, - "create_order/happy_path_creates_order_pda_with_expected_body": 4981, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4525, - "reclaim_buffer/funded_buffer_is_skipped": 4847, - "reclaim_buffer/funded_buffer_is_skipped_token_2022": 4857, - "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": 7498, - "reclaim_buffer/max_buffers_in_one_instruction": 125926, - "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 170566, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7637, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 9135, - "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2200, - "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2069, - "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2077, + "add_solver/add_with_many_existing_solvers": 5074, + "add_solver/adds_a_solver": 4622, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 7370, + "create_buffers/happy_path_creates_initialized_buffer_token_account_token_2022": 12177, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17346, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction_token_2022": 31767, + "create_buffers/max_buffers_in_one_instruction": 164696, + "create_buffers/max_buffers_in_one_instruction_token_2022": 208025, + "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": 4849, + "reclaim_buffer/funded_buffer_is_skipped_token_2022": 4859, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 6012, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself_token_2022": 7500, + "reclaim_buffer/max_buffers_in_one_instruction": 125928, + "reclaim_buffer/max_buffers_in_one_instruction_token_2022": 170568, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7639, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded_token_2022": 9137, + "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, "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": 3755, - "remove_solver/removes_a_solver": 3490, - "settle/finalizes_with_no_pushes": 7103, - "settle/pulls_from_multiple_orders": 20151, - "settle/pulls_funds_to_destination": 13667, - "settle/pulls_to_multiple_destinations": 14820, - "settle/pushes_a_single_order": 12511, - "settle/pushes_several_orders_from_different_buffers": 17834, - "settle/pushes_several_orders_from_one_buffer": 17834, - "settle/settles_a_single_order": 12529, - "settle/settles_multiple_orders": 23210, - "transfer_authority/manager_can_transfer_manager": 3171, - "transfer_authority/manager_can_transfer_reclaim_authority": 3173, - "transfer_authority/reclaim_authority_can_transfer_itself": 3177 + "remove_solver/remove_with_many_existing_solvers": 3758, + "remove_solver/removes_a_solver": 3493, + "settle/finalizes_with_no_pushes": 7128, + "settle/pulls_from_multiple_orders": 20184, + "settle/pulls_funds_to_destination": 13695, + "settle/pulls_to_multiple_destinations": 14847, + "settle/pushes_a_single_order": 12540, + "settle/pushes_several_orders_from_different_buffers": 17869, + "settle/pushes_several_orders_from_one_buffer": 17869, + "settle/settles_a_single_order": 12558, + "settle/settles_multiple_orders": 23251, + "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 d3d7ac9b571419f40c6d357c29de2bff0011e1e1 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:16:12 +0900 Subject: [PATCH 18/38] improved clarity of only_token_program comments --- client/src/instruction/begin_settle.rs | 10 ++++++---- client/src/instruction/finalize_settle.rs | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/client/src/instruction/begin_settle.rs b/client/src/instruction/begin_settle.rs index a57c5cee..e5c64ab8 100644 --- a/client/src/instruction/begin_settle.rs +++ b/client/src/instruction/begin_settle.rs @@ -25,10 +25,12 @@ pub struct BeginSettle<'a> { pub finalize_ix_index: u16, /// The off-chain auction this settlement executes, carried so it can be tied /// back to its auction off-chain. - pub auction_id: i64, - /// Replaces any token program not corresponding with what is given - /// with the system program. Reduces the total number of accounts - /// depended upon by this instruction. + pub auction_id: i64, + /// By default, a settlement support both token programs at the same time. + /// If you know you only need a single token program, you can make the byte + /// size of the settlement transaction a bit smaller and reduce the total + /// accounts used in the transaction by specifying the + /// only token program you need here. pub only_token_program: Option, pub orders: &'a [InitializedIntent<'a>], } diff --git a/client/src/instruction/finalize_settle.rs b/client/src/instruction/finalize_settle.rs index 3583fa0b..345ae98c 100644 --- a/client/src/instruction/finalize_settle.rs +++ b/client/src/instruction/finalize_settle.rs @@ -29,9 +29,11 @@ pub struct FinalizedIntent<'a> { pub struct FinalizeSettle<'a> { pub program_id: Pubkey, pub begin_ix_index: u16, - /// Replaces any token program not corresponding with what is given - /// with the system program. Reduces the total number of accounts - /// depended upon by this instruction. + /// By default, a settlement support both token programs at the same time. + /// If you know you only need a single token program, you can make the byte + /// size of the settlement transaction a bit smaller and reduce the total + /// accounts used in the transaction by specifying the + /// only token program you need here. pub only_token_program: Option, pub orders: &'a [FinalizedIntent<'a>], } From 6767484941e9ccafc5ad2d17c29edd51748b79a9 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:24:21 +0900 Subject: [PATCH 19/38] Update interface/src/instruction/settle/begin.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/instruction/settle/begin.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index eee76f3f..fe7bd278 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -107,10 +107,17 @@ impl From> for Instruction { AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), AccountMeta::new_readonly(state_pda, false), ]; - accounts.extend( - TokenProgram::addresses(only_token_program) - .map(|address| AccountMeta::new_readonly(address, false)), - ); + // One account per supported token program. If `only_token_program`, + // replace the other program in the instruction with an account that's + // already present (and so doesn't take extra space in the tx). + accounts.extend(TokenProgram::ALL.map(|program| { + let address = if only_token_program.is_none_or(|only| only == program) { + program.address() + } else { + INSTRUCTIONS_SYSVAR_ID + }; + AccountMeta::new_readonly(address, false) + })); for &i in &order { // Writable account for the order: `BeginSettle` updates its filled // amounts (`amount_withdrawn`/`amount_received`). From 9ca531f759562616c3adc526064a79285dd3191e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:37:57 +0900 Subject: [PATCH 20/38] remove dependency on SPL_TOKEN_PROGRAM_ID and use None instead of Some --- client/src/instruction/begin_settle.rs | 4 +-- client/src/instruction/finalize_settle.rs | 2 +- client/src/parse.rs | 4 +-- interface/src/instruction/settle/begin.rs | 17 ++++++----- interface/src/instruction/settle/finalize.rs | 19 ++++++------- interface/src/instruction/settle/mod.rs | 4 --- .../settlement/tests/begin_settle_orders.rs | 28 +++++++++---------- .../settlement/tests/common/settlement.rs | 8 +++--- .../tests/finalize_settle_pushes.rs | 22 ++++++++------- .../tests/matching_begin_finalize.rs | 22 +++++++-------- .../settlement/tests/program_deployment.rs | 6 ++-- test-cli/src/cmd/settle.rs | 4 +-- 12 files changed, 68 insertions(+), 72 deletions(-) diff --git a/client/src/instruction/begin_settle.rs b/client/src/instruction/begin_settle.rs index e5c64ab8..b89bfbf8 100644 --- a/client/src/instruction/begin_settle.rs +++ b/client/src/instruction/begin_settle.rs @@ -25,7 +25,7 @@ pub struct BeginSettle<'a> { pub finalize_ix_index: u16, /// The off-chain auction this settlement executes, carried so it can be tied /// back to its auction off-chain. - pub auction_id: i64, + pub auction_id: i64, /// By default, a settlement support both token programs at the same time. /// If you know you only need a single token program, you can make the byte /// size of the settlement transaction a bit smaller and reduce the total @@ -97,7 +97,7 @@ mod tests { solver: pubkey_from_seed("solver"), finalize_ix_index, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &orders, }); diff --git a/client/src/instruction/finalize_settle.rs b/client/src/instruction/finalize_settle.rs index 345ae98c..e715e1c9 100644 --- a/client/src/instruction/finalize_settle.rs +++ b/client/src/instruction/finalize_settle.rs @@ -116,7 +116,7 @@ mod tests { let ix = Instruction::from(FinalizeSettle { program_id, begin_ix_index, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &orders, }); diff --git a/client/src/parse.rs b/client/src/parse.rs index 4075a007..520b2736 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -125,7 +125,7 @@ mod tests { solver: payer, finalize_ix_index: 1, auction_id: 42, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[InitializedIntent { intent: &intent, pulls: &[], @@ -135,7 +135,7 @@ mod tests { SettlementInstruction::FinalizeSettle => FinalizeSettle { program_id, begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], } .into(), diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index eee76f3f..1f25f998 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -299,9 +299,8 @@ mod tests { fake_account, fake_account_from_array, fake_sequential_accounts, }; use crate::instruction::settle::tests::ix_data; - use crate::instruction::settle::SPL_TOKEN_PROGRAM_ID; use crate::instruction::tests::{assert_readonly_nonsigner, assert_readonly_signer}; - use crate::token_program::{TokenProgram, SYSTEM_PROGRAM_ID}; + use crate::token_program::TokenProgram; use hex_literal::hex; use solana_account_view::AccountView; use solana_address::Address; @@ -330,7 +329,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: 0x0102_0304_0506_0708, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, order_pdas: &[], sell_token_accounts: &[], pulls: &[], @@ -355,8 +354,8 @@ mod tests { assert_readonly_signer(&accounts[0], solver); assert_readonly_nonsigner(&accounts[1], INSTRUCTIONS_SYSVAR_ID); assert_readonly_nonsigner(&accounts[2], state_pda); - assert_readonly_nonsigner(&accounts[3], SPL_TOKEN_PROGRAM_ID); - assert_readonly_nonsigner(&accounts[4], SYSTEM_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[3], TokenProgram::SplToken.address()); + assert_readonly_nonsigner(&accounts[4], TokenProgram::Token2022.address()); } /// The token-program slots are the addresses the settlement's @@ -406,7 +405,7 @@ mod tests { solver, finalize_ix_index: 0x1337, auction_id: AUCTION_ID, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, order_pdas: &[high_order_pda, low_order_pda], sell_token_accounts: &[high_sell_token_account, low_sell_token_account], pulls: &[&[], &[]], @@ -428,8 +427,8 @@ mod tests { solver, INSTRUCTIONS_SYSVAR_ID, state_pda, - SPL_TOKEN_PROGRAM_ID, - SYSTEM_PROGRAM_ID, + TokenProgram::SplToken.address(), + TokenProgram::Token2022.address(), low_order_pda, low_sell_token_account, high_order_pda, @@ -518,7 +517,7 @@ mod tests { solver, INSTRUCTIONS_SYSVAR_ID, state_pda, - SPL_TOKEN_PROGRAM_ID, + TokenProgram::SplToken.address(), TokenProgram::Token2022.address(), order_a, sell_a, diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index e1d8a5c3..900059d9 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -264,9 +264,8 @@ mod tests { fake_account, fake_account_from_array, fake_sequential_accounts, }; use crate::instruction::settle::tests::ix_data; - use crate::instruction::settle::SPL_TOKEN_PROGRAM_ID; use crate::instruction::tests::assert_readonly_nonsigner; - use crate::token_program::{TokenProgram, SYSTEM_PROGRAM_ID}; + use crate::token_program::TokenProgram; use hex_literal::hex; use proptest::prelude::*; use solana_account_view::AccountView; @@ -280,7 +279,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[], destinations: &[], bumps: &[], @@ -301,7 +300,7 @@ mod tests { program_id, state_pda, begin_ix_index: 0x1337, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[], destinations: &[], bumps: &[], @@ -324,8 +323,8 @@ mod tests { assert_eq!(accounts.len(), FINALIZE_FIXED_ACCOUNTS); assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID); assert_readonly_nonsigner(&accounts[1], state_pda); - assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID); - assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[2], TokenProgram::SplToken.address()); + assert_readonly_nonsigner(&accounts[3], TokenProgram::Token2022.address()); } /// The token-program slots are the addresses the settlement's @@ -395,7 +394,7 @@ mod tests { vec![ INSTRUCTIONS_SYSVAR_ID, state_pda, - SPL_TOKEN_PROGRAM_ID, + TokenProgram::SplToken.address(), TokenProgram::Token2022.address(), source_a, dest_a, @@ -631,7 +630,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0x1337, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[ pubkey_from_seed("source buffer 0"), pubkey_from_seed("source buffer 1"), @@ -655,7 +654,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[], destinations: &[], bumps: &[], @@ -671,7 +670,7 @@ mod tests { program_id: pubkey_from_seed("program id"), state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[pubkey_from_seed("source buffer")], destinations: &[pubkey_from_seed("destination")], bumps: &[0xff], diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index 6301560f..0a8c78ba 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -1,12 +1,8 @@ //! `BeginSettle`/`FinalizeSettle` instruction tools, the instructions-sysvar //! account ID they all reference, and the off-chain instruction builders. -use crate::Pubkey; use solana_program_error::ProgramError; -/// The legacy SPL Token program, which the builders below target by default. -pub const SPL_TOKEN_PROGRAM_ID: Pubkey = TokenProgram::SplToken.address(); - pub use crate::token_program::TokenProgram; pub use solana_sdk_ids::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID; diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 05e37a08..84037ef5 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -31,7 +31,7 @@ use cow_settlement_client::cow_settlement_interface::{ data::order::{EncodedOrderAccount, OrderAccount}, instruction::settle::{ BeginSettle as BeginSettleRaw, FinalizeSettle as FinalizeSettleRaw, - FINALIZE_FIXED_ACCOUNTS, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID, + FINALIZE_FIXED_ACCOUNTS, INSTRUCTIONS_SYSVAR_ID, }, pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, Instruction, SettlementError, SettlementInstruction, @@ -136,13 +136,13 @@ fn settle_and_pay_amounts( solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders, }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &settled, }; vec![begin.into(), finalize.into()] @@ -255,7 +255,7 @@ fn rejects_fabricated_program_owned_account() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, order_pdas: &[fake_order], sell_token_accounts: &[sell_token], pulls: &no_pulls(1), @@ -266,7 +266,7 @@ fn rejects_fabricated_program_owned_account() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[unique_pubkey()], destinations: &[intent.buy_token_account], bumps: &[0], @@ -297,7 +297,7 @@ fn rejects_non_order_account_in_order_slot() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, order_pdas: &[sell_token], sell_token_accounts: &[sell_token], pulls: &no_pulls(1), @@ -307,7 +307,7 @@ fn rejects_non_order_account_in_order_slot() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[unique_pubkey()], destinations: &[unique_pubkey()], bumps: &[0], @@ -578,7 +578,7 @@ fn rejects_orders_in_wrong_address_order() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &source_buffers, destinations: &destinations, bumps: &bumps, @@ -1013,7 +1013,7 @@ fn rejects_a_token_program_the_instruction_doesnt_name() { // Swap the SPL Token program account `BeginSettle` references for an invalid one. replace_first_matching_account( &mut instructions[usize::from(BEGIN_INDEX)], - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), unique_pubkey(), ); @@ -1148,7 +1148,7 @@ fn rejects_push_to_wrong_destination() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &orders, }); // Redirect the push to an account that isn't the order's buy token account. @@ -1182,7 +1182,7 @@ fn rejects_push_if_buffer_does_not_match_buy_mint() { program_id, state_pda: find_state_pda(&program_id).0, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, source_buffers: &[other_buffer], destinations: &[intent.buy_token_account], bumps: &[other_bump], @@ -1209,7 +1209,7 @@ fn rejects_fewer_pushes_than_orders() { let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }; @@ -1230,7 +1230,7 @@ fn rejects_more_pushes_than_orders() { let finalize = FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[FinalizedIntent { intent: &intent, amount: 0, @@ -1256,7 +1256,7 @@ fn rejects_partial_push_amount_in_finalize_settle() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &orders, }); // Drop one byte from the finalize intstruction so the trailing amount is no diff --git a/programs/settlement/tests/common/settlement.rs b/programs/settlement/tests/common/settlement.rs index 59701229..baea5a82 100644 --- a/programs/settlement/tests/common/settlement.rs +++ b/programs/settlement/tests/common/settlement.rs @@ -1,7 +1,7 @@ //! Scaffolding for building `[BeginSettle, FinalizeSettle]` settlement pairs. use cow_settlement_client::instruction::{ - BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, TokenProgram, + BeginSettle, FinalizeSettle, FinalizedIntent, InitializedIntent, Pull, }; use cow_settlement_interface::{data::intent::OrderIntent, Instruction}; use litesvm::LiteSVM; @@ -40,7 +40,7 @@ pub fn build_settlement( solver: *solver, finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &begin_orders, }; vec![begin.into(), finalize.into()] @@ -132,13 +132,13 @@ pub fn build_staged_settlement( solver: *solver, finalize_ix_index: finalize_index(between.len()), auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &begin_orders, }; let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &finalize_orders, }; diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index 990e3319..24d2d5ec 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -18,11 +18,13 @@ use crate::common::{ settlement::{build_settlement, BEGIN_INDEX, FINALIZE_INDEX}, setup_settle_ready, token, unique_pubkey, }; -use cow_settlement_client::cow_settlement_interface::{ - data::intent::OrderIntent, instruction::settle::SPL_TOKEN_PROGRAM_ID, - pda::state::find_state_pda, Instruction, SettlementError, +use cow_settlement_client::instruction::{FinalizeSettle, FinalizedIntent}; +use cow_settlement_client::{ + cow_settlement_interface::{ + data::intent::OrderIntent, pda::state::find_state_pda, Instruction, SettlementError, + }, + instruction::TokenProgram, }; -use cow_settlement_client::instruction::{FinalizeSettle, FinalizedIntent, TokenProgram}; use litesvm_token::spl_token::error::TokenError; use solana_sdk::{ instruction::InstructionError, program_error::ProgramError, pubkey::Pubkey, signer::Signer, @@ -47,7 +49,7 @@ fn finalize(program_id: &Pubkey, solver: &Pubkey, orders: &[FinalizedIntent]) -> let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders, }; build_settlement(program_id, solver, orders, finalize) @@ -210,7 +212,7 @@ fn rejects_a_token_program_the_instruction_doesnt_name() { let mut instructions = finalize(&program_id, &solver.pubkey(), &orders); replace_first_matching_account( &mut instructions[usize::from(FINALIZE_INDEX)], - &SPL_TOKEN_PROGRAM_ID, + &TokenProgram::SplToken.address(), unique_pubkey(), ); @@ -256,7 +258,7 @@ fn rejects_push_account_count_mismatch() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &orders, }); // ...with another push's worth of data bytes appended but no matching @@ -282,7 +284,7 @@ fn rejects_too_few_accounts() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }); // ...with one of its fixed accounts popped. `BeginSettle` runs first @@ -375,7 +377,7 @@ fn rejects_two_too_few_accounts() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &orders, }); // ...with that push's whole (source, destination) pair popped, so the data @@ -405,7 +407,7 @@ fn rejects_partial_push_amount() { let mut finalize = Instruction::from(FinalizeSettle { program_id, begin_ix_index: BEGIN_INDEX.into(), - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &orders, }); // Drop one byte so the trailing amount is no longer a whole `u64`. diff --git a/programs/settlement/tests/matching_begin_finalize.rs b/programs/settlement/tests/matching_begin_finalize.rs index 8980ac8c..a5bd20d8 100644 --- a/programs/settlement/tests/matching_begin_finalize.rs +++ b/programs/settlement/tests/matching_begin_finalize.rs @@ -1,5 +1,5 @@ use cow_settlement_client::cow_settlement_interface::{SettlementError, SettlementInstruction}; -use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle, TokenProgram}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle}; use litesvm::{types::FailedTransactionMetadata, LiteSVM}; use solana_sdk::{ instruction::{AccountMeta, Instruction, InstructionError}, @@ -38,14 +38,14 @@ fn run_sequence( solver: solver.pubkey(), finalize_ix_index: *idx, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], } .into(), AbstractInstruction::Fin(idx) => FinalizeSettle { program_id: *program_id, begin_ix_index: *idx, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], } .into(), @@ -193,7 +193,7 @@ fn rejects_non_instructions_sysvar_account_at_position_one() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], } .into(); @@ -201,7 +201,7 @@ fn rejects_non_instructions_sysvar_account_at_position_one() { let finalize = FinalizeSettle { program_id, begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }; @@ -233,7 +233,7 @@ fn rejects_counterpart_instruction_in_different_program() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }; // We build a transaction that looks like a valid finalize_settle but @@ -242,7 +242,7 @@ fn rejects_counterpart_instruction_in_different_program() { let stranger = FinalizeSettle { program_id: solana_system_interface::program::ID, begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }; @@ -297,7 +297,7 @@ fn rejects_cpi_call_to_begin_settle() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }, ); @@ -330,7 +330,7 @@ fn rejects_cpi_call_to_finalize_settle() { FinalizeSettle { program_id: settlement_id, begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }, ); @@ -364,7 +364,7 @@ fn rejects_counterpart_with_unrecoverable_discriminator() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }; // Uses the settlement program, but no data: `recover_discriminator` fails @@ -407,7 +407,7 @@ fn rejects_counterpart_with_unrecoverable_counterpart_index() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], }; // Same program as `begin`, with a valid discriminator but no trailing diff --git a/programs/settlement/tests/program_deployment.rs b/programs/settlement/tests/program_deployment.rs index 40ab3c3f..d8066ffb 100644 --- a/programs/settlement/tests/program_deployment.rs +++ b/programs/settlement/tests/program_deployment.rs @@ -1,4 +1,4 @@ -use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle, TokenProgram}; +use cow_settlement_client::instruction::{BeginSettle, FinalizeSettle}; use solana_sdk::{ instruction::{Instruction, InstructionError}, signature::Signer, @@ -34,14 +34,14 @@ fn program_can_be_invoked() { solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], } .into(), FinalizeSettle { program_id, begin_ix_index: 0, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &[], } .into(), diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index db36fd1b..bd26d960 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -118,7 +118,7 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { finalize_ix_index, // Token resolution builds legacy SPL accounts throughout (see // `crate::token`), so Token-2022's slot stays empty. - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &initialized_intents, auction_id: 0, }; @@ -135,7 +135,7 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { let finalize_ix = FinalizeSettle { program_id: ctx.program_id, begin_ix_index, - only_token_program: Some(TokenProgram::SplToken), + only_token_program: None, orders: &settled, }; From 7a3ba541e86d31103007d9fc4c281bec4ed45c37 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:39:45 +0900 Subject: [PATCH 21/38] Update interface/src/instruction/settle/finalize.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/instruction/settle/finalize.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index 900059d9..13d07756 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -221,10 +221,8 @@ impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> { fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result { let (begin_ix_index, body) = recover_counterpart(instruction_data)?; - // The two token-program slots are skipped rather than read: every push - // is issued against the program that owns its destination, so naming - // the programs is all the slots do. They still take up their positions, - // which is what the push accounts are counted from. + // The two token-program slots are skipped rather than read since they are only + // used for program invocation. let [instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, push_accounts @ ..] = accounts else { From 813d6d769a7906da54e1d0ba20dc9076e8a7d6b2 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:40:14 +0900 Subject: [PATCH 22/38] Update interface/src/token_program.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/token_program.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index f21e3f21..8a4c0b8a 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -15,9 +15,7 @@ pub enum TokenProgram { } impl TokenProgram { - /// Every supported token program. The single list [`TryFrom`] resolves - /// addresses against, and the order `BeginSettle` and `FinalizeSettle` lay - /// their token-program accounts out in; see [`Self::addresses`]. + /// Every supported token program. pub const ALL: [Self; 2] = [Self::SplToken, Self::Token2022]; /// The address the program is deployed at. From ab7a5e1e6176c8cea943309d4fee1f8b60e6c81d Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:42:57 +0900 Subject: [PATCH 23/38] Update programs/settlement/src/processor/utils/token.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/src/processor/utils/token.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/programs/settlement/src/processor/utils/token.rs b/programs/settlement/src/processor/utils/token.rs index cb5c02f2..5cc6bb3c 100644 --- a/programs/settlement/src/processor/utils/token.rs +++ b/programs/settlement/src/processor/utils/token.rs @@ -203,11 +203,11 @@ mod tests { /// Creates a legacy SPL-compliant token account of `program` fn token_account_of(program: Address) -> AccountView { fake_account_owned_by( - pubkey_from_seed("UNRELATED token account"), + pubkey_from_seed("token_account_of's token account"), program, &base_account_layout( - pubkey_from_seed("UNRELATED mint"), - pubkey_from_seed("UNRELATED owner"), + pubkey_from_seed("token_account_of's mint"), + pubkey_from_seed("token_account_of's owner"), 0, ), ) From 6b522c602ee87cc39b37d08143ef43dfb5e749db Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:46:31 +0900 Subject: [PATCH 24/38] refactor cloned_token_under_unsupported_program --- .../settlement/tests/begin_settle_orders.rs | 25 +++++++++++----- programs/settlement/tests/common/token.rs | 30 ------------------- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 84037ef5..b5ec6470 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -420,13 +420,24 @@ fn rejects_sell_account_under_a_unsupported_token_program() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); let amount = 1_000_000; - let (sell_mint, sell_token_account) = token::cloned_token_under_unsupported_program( - &mut svm, - &program_id, - &payer, - &payer.pubkey(), - amount, - ); + + // Build the genuine article first, so what gets cloned is a real token's + // bytes rather than a test's idea of them. + let mint = create_mint(svm, payer); + let account = create_token_account(svm, payer, &mint, owner); + fund_and_delegate(svm, program_id, payer, &account, amount); + + let sell_mint = clone_under_unsupported_program(svm, &mint); + // Repoint the copy at the cloned mint, so the pair stands on its own under + // the clone instead of borrowing the real mint. + let mut token = + litesvm_token::get_spl_account::(svm, &account) + .expect("the freshly delegated account is a valid token account"); + token.mint = cloned_mint; + let mut data = vec![0u8; litesvm_token::spl_token::state::Account::LEN]; + token.pack_into_slice(&mut data); + let sell_token_account = unique_pubkey(); + super::create_account_at(svm, cloned_account, &CLONED_TOKEN_PROGRAM_ID, &data); let intent = OrderIntent { sell_token_account, diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 181075ef..f48f6a9c 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -337,36 +337,6 @@ pub fn clone_under_unsupported_program(svm: &mut LiteSVM, account: &Pubkey) -> P clone } -/// A mint and one of its token accounts, both under -/// [`CLONED_TOKEN_PROGRAM_ID`], returned as `(mint, token_account)`. -pub fn cloned_token_under_unsupported_program( - svm: &mut LiteSVM, - program_id: &Pubkey, - payer: &Keypair, - owner: &Pubkey, - amount: u64, -) -> (Pubkey, Pubkey) { - // Build the genuine article first, so what gets cloned is a real token's - // bytes rather than a test's idea of them. - let mint = create_mint(svm, payer); - let account = create_token_account(svm, payer, &mint, owner); - fund_and_delegate(svm, program_id, payer, &account, amount); - - let cloned_mint = clone_under_unsupported_program(svm, &mint); - // Repoint the copy at the cloned mint, so the pair stands on its own under - // the clone instead of borrowing the real mint. - let mut token = - litesvm_token::get_spl_account::(svm, &account) - .expect("the freshly delegated account is a valid token account"); - token.mint = cloned_mint; - let mut data = vec![0u8; litesvm_token::spl_token::state::Account::LEN]; - token.pack_into_slice(&mut data); - let cloned_account = unique_pubkey(); - super::create_account_at(svm, cloned_account, &CLONED_TOKEN_PROGRAM_ID, &data); - - (cloned_mint, cloned_account) -} - /// Fund `sell_token` with `amount` of its mint and approve the settlement state /// PDA as its delegate for the same `amount`, so the program can pull from it. pub fn fund_and_delegate( From b8861fc0158e1eb3a9d9fad766ea7fa3d35ff7c4 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:19:06 +0900 Subject: [PATCH 25/38] remove unneeded constant and add test to verify intended effect of narrowing --- interface/src/token_program.rs | 13 ++- .../settlement/tests/begin_settle_orders.rs | 15 ++-- .../settlement/tests/settle_token_programs.rs | 87 +++++++++++++++++++ 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index f21e3f21..8ae3a648 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -2,8 +2,7 @@ use crate::Pubkey; use solana_program_error::ProgramError; - -pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; +pub use solana_sdk_ids::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID; /// A token program a token-moving instruction accepts. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -36,7 +35,7 @@ impl TokenProgram { /// account it moves — so a settlement naming every program may mix tokens /// from both. `only_token_program` is what narrows that: `None` names them /// all, and `Some(program)` names just that one, leaving - /// [`SYSTEM_PROGRAM_ID`] in every other slot. + /// [`INSTRUCTIONS_SYSVAR_ID`] in every other slot. pub const fn addresses(only_token_program: Option) -> [Pubkey; Self::ALL.len()] { let [spl_token, token_2022] = Self::ALL; [ @@ -47,7 +46,7 @@ impl TokenProgram { /// The address this program's own slot holds. The slots are not read /// on-chain, so a program the settlement doesn't touch is left out by - /// standing [`SYSTEM_PROGRAM_ID`] in: nearly every settlement transaction + /// standing [`INSTRUCTIONS_SYSVAR_ID`] in: nearly every settlement transaction /// names the system program already, so it costs one more account index /// rather than another 32-byte address. const fn slot(self, only_token_program: Option) -> Pubkey { @@ -55,7 +54,7 @@ impl TokenProgram { // Compared as discriminants because `PartialEq` isn't const. That // keeps the narrowing correct for any variant added to `ALL`, // rather than making this a second place to list them. - Some(only) if only as u8 != self as u8 => SYSTEM_PROGRAM_ID, + Some(only) if only as u8 != self as u8 => INSTRUCTIONS_SYSVAR_ID, _ => self.address(), } } @@ -110,7 +109,7 @@ mod tests { #[test] fn the_placeholder_is_not_a_token_program() { assert_eq!( - TokenProgram::try_from(&SYSTEM_PROGRAM_ID), + TokenProgram::try_from(&INSTRUCTIONS_SYSVAR_ID), Err(ProgramError::IncorrectProgramId), ); } @@ -138,7 +137,7 @@ mod tests { let expected = if slot == named { only.address() } else { - SYSTEM_PROGRAM_ID + INSTRUCTIONS_SYSVAR_ID }; assert_eq!( address, expected, diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index b5ec6470..6656409e 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -42,6 +42,7 @@ use cow_settlement_client::instruction::{ use cow_settlement_interface::data::intent::OrderIntent; use litesvm::LiteSVM; use litesvm_token::spl_token::error::TokenError; +use solana_program_pack::Pack; use solana_sdk::{ instruction::{AccountMeta, InstructionError}, pubkey::Pubkey, @@ -423,21 +424,21 @@ fn rejects_sell_account_under_a_unsupported_token_program() { // Build the genuine article first, so what gets cloned is a real token's // bytes rather than a test's idea of them. - let mint = create_mint(svm, payer); - let account = create_token_account(svm, payer, &mint, owner); - fund_and_delegate(svm, program_id, payer, &account, amount); + let mint = common::token::create_mint(&mut svm, &payer); + let account = common::token::create_token_account(&mut svm, &payer, &mint, &payer.pubkey()); + common::token::fund_and_delegate(&mut svm, &program_id, &payer, &account, amount); - let sell_mint = clone_under_unsupported_program(svm, &mint); + let sell_mint = common::token::clone_under_unsupported_program(&mut svm, &mint); // Repoint the copy at the cloned mint, so the pair stands on its own under // the clone instead of borrowing the real mint. let mut token = - litesvm_token::get_spl_account::(svm, &account) + litesvm_token::get_spl_account::(&mut svm, &account) .expect("the freshly delegated account is a valid token account"); - token.mint = cloned_mint; + token.mint = sell_mint; let mut data = vec![0u8; litesvm_token::spl_token::state::Account::LEN]; token.pack_into_slice(&mut data); let sell_token_account = unique_pubkey(); - super::create_account_at(svm, cloned_account, &CLONED_TOKEN_PROGRAM_ID, &data); + common::create_account_at(&mut svm, sell_token_account, &common::token::CLONED_TOKEN_PROGRAM_ID, &data); let intent = OrderIntent { sell_token_account, diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index 597595bd..ad6d75f9 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -343,3 +343,90 @@ fn settles_with_the_token_program_slots_swapped() { assert_eq!(token::balance(&svm, &intent.buy_token_account), 100); } + +#[test] +fn narrowing_begin_settle_drops_one_account_from_the_transaction() { + /// What the order settles for. Any amount does; it just has to be the same + /// in both transactions, so the two differ only in what they name. + const AMOUNT: u64 = 100; + + let (mut svm, program_id, payer, solver) = setup_settle_ready(); + + let intent = order_across( + &mut svm, + &program_id, + &payer, + 0, + &TokenProgram::Token2022.address(), + &TokenProgram::Token2022.address(), + ); + token::fund_and_delegate( + &mut svm, + &program_id, + &payer, + &intent.sell_token_account, + AMOUNT, + ); + let sell_mint = token::mint_of(&svm, &intent.sell_token_account); + let buy_mint = token::mint_of(&svm, &intent.buy_token_account); + buffer::ensure_funded(&mut svm, &program_id, &payer, &buy_mint, AMOUNT); + let destination = token::create_token_account(&mut svm, &payer, &sell_mint, &unique_pubkey()); + let pulls = [Pull { + destination, + amount: AMOUNT, + }]; + let blockhash = svm.latest_blockhash(); + + let verify_narrowed = |only_token_program| { + let initialized = [InitializedIntent { + intent: &intent, + pulls: &pulls, + }]; + let finalized = [FinalizedIntent { + intent: &intent, + amount: AMOUNT, + }]; + let begin = BeginSettle { + program_id, + solver: solver.pubkey(), + finalize_ix_index: FINALIZE_INDEX.into(), + auction_id: 0, + only_token_program, + orders: &initialized, + }; + let finalize = FinalizeSettle { + program_id, + begin_ix_index: BEGIN_INDEX.into(), + only_token_program, + orders: &finalized, + }; + Transaction::new_signed_with_payer( + &[begin.into(), finalize.into()], + Some(&payer.pubkey()), + &[&payer, &solver], + blockhash, + ) + }; + + let both = verify_narrowed(None); + let narrowed_legacy = verify_narrowed(Some(TokenProgram::SplToken)); + let narrowed_2022 = verify_narrowed(Some(TokenProgram::Token2022)); + + assert_eq!( + narrowed_legacy.message.account_keys.len() + 1, + both.message.account_keys.len(), + "narrowing `BeginSettle` to the SPL token program should cost the transaction \ + one account fewer", + ); + assert_eq!( + narrowed_2022.message.account_keys.len() + 1, + both.message.account_keys.len(), + "narrowing `BeginSettle` to the Token2022 token program should cost the transaction \ + one account fewer", + ); + + // And the shorter transaction is still one that settles. + svm.send_transaction(narrowed_2022) + .expect("a settlement narrowed to the program it uses should settle"); + assert_eq!(token::balance(&svm, &intent.buy_token_account), AMOUNT); +} From 26291d2a94aed4812f3c3e1cb558fb272da3ec62 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:20:33 +0900 Subject: [PATCH 26/38] Update programs/settlement/src/processor/finalize_settle.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/src/processor/finalize_settle.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/programs/settlement/src/processor/finalize_settle.rs b/programs/settlement/src/processor/finalize_settle.rs index 5b4f7721..02875861 100644 --- a/programs/settlement/src/processor/finalize_settle.rs +++ b/programs/settlement/src/processor/finalize_settle.rs @@ -67,9 +67,6 @@ fn push_funds<'a>( pushes: Pushes<'a, AccountView>, ) -> ProgramResult { for push in pushes.iter() { - // The push moves this destination's tokens, so it is issued against the - // token program that owns it. An account under neither program isn't a - // token account at all. let token_program = owning_token_program(push.destination) .map_err(|_| SettlementError::InvalidTokenProgram)?; Transfer::new( From 99a2f3536e45f932fbc208dc59a7dc69e48f4fbf Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:21:16 +0900 Subject: [PATCH 27/38] Update programs/settlement/tests/begin_settle_orders.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/begin_settle_orders.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 6656409e..f4496358 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -13,10 +13,9 @@ //! sends it unmodified (when the rejection is already baked into the orders or //! accounts passed in) or mutates its `BeginSettle` instruction in place //! afterwards (a wrong account, a wrong token program, a wrong state PDA, an -//! extra account). A few -//! tests are the exception and build the raw instruction directly, because -//! what they exercise can't come out of the client builder, whose output is a -//! properly built instruction. +//! extra account). A few tests are the exception and build the raw instruction +//! directly, because what they exercise can't come out of the client builder, +//! whose output is a properly built instruction. use crate::common::{ assert_instruction_error_at, From 3390c9ae95667a94615fb54b01f8804f54e0fc2d Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:34:09 +0900 Subject: [PATCH 28/38] remove token program helpers for determining necessary instruction addrs --- interface/src/instruction/settle/begin.rs | 27 +++++--- interface/src/instruction/settle/finalize.rs | 44 +++++++++---- interface/src/token_program.rs | 65 ------------------- .../settlement/tests/begin_settle_orders.rs | 5 +- 4 files changed, 53 insertions(+), 88 deletions(-) diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 9e5f1e78..77a1a3ca 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -56,7 +56,7 @@ pub struct BeginSettle<'a> { /// off-chain, unused on-chain. pub auction_id: i64, /// The only token program this settlement's transfers are issued against, - /// or `None` to name every supported one; see [`TokenProgram::addresses`]. + /// or `None` to name every supported one; see [`TokenProgram::ALL`]. pub only_token_program: Option, pub order_pdas: &'a [Pubkey], pub sell_token_accounts: &'a [Pubkey], @@ -370,10 +370,22 @@ mod tests { /// settlement can name both programs — or leave either one out. #[test] fn begin_settle_carries_the_token_program_slots_it_is_given() { - for only_token_program in [ - None, - Some(TokenProgram::SplToken), - Some(TokenProgram::Token2022), + for (only_token_program, expected) in [ + ( + None, + [ + TokenProgram::SplToken.address(), + TokenProgram::Token2022.address(), + ], + ), + ( + Some(TokenProgram::SplToken), + [TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID], + ), + ( + Some(TokenProgram::Token2022), + [INSTRUCTIONS_SYSVAR_ID, TokenProgram::Token2022.address()], + ), ] { let Instruction { accounts, .. } = Instruction::from(BeginSettle { program_id: Pubkey::new_unique(), @@ -388,9 +400,8 @@ mod tests { }); let slots: Vec = accounts[3..].iter().map(|meta| meta.pubkey).collect(); assert_eq!( - slots, - TokenProgram::addresses(only_token_program), - "{only_token_program:?} should be laid out as its own addresses", + slots, expected, + "{only_token_program:?} should name just the programs it settles against", ); } } diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index 13d07756..bfb62b27 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -83,7 +83,7 @@ pub fn finalize_push_data( /// `[instructions_sysvar (R), state_pda (R), spl_token_program (R), /// token_2022_program (R)]` followed, per push, by `[source_buffer (W), /// destination (W)]`. The two token programs are the slots -/// [`TokenProgram::addresses`] describes, there to name the programs this +/// [`TokenProgram::ALL`] describes, there to name the programs this /// instruction's pushes are issued against; the matching `BeginSettle` carries /// the ones its pulls need. /// @@ -94,7 +94,7 @@ pub struct FinalizeSettle<'a> { pub state_pda: Pubkey, pub begin_ix_index: u16, /// The only token program this settlement's transfers are issued against, - /// or `None` to name every supported one; see [`TokenProgram::addresses`]. + /// or `None` to name every supported one; see [`TokenProgram::ALL`]. pub only_token_program: Option, pub source_buffers: &'a [Pubkey], pub destinations: &'a [Pubkey], @@ -125,10 +125,17 @@ impl From> for Instruction { AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), AccountMeta::new_readonly(state_pda, false), ]; - accounts.extend( - TokenProgram::addresses(only_token_program) - .map(|address| AccountMeta::new_readonly(address, false)), - ); + // One account per supported token program. If `only_token_program`, + // replace the other program in the instruction with an account that's + // already present (and so doesn't take extra space in the tx). + accounts.extend(TokenProgram::ALL.map(|program| { + let address = if only_token_program.is_none_or(|only| only == program) { + program.address() + } else { + INSTRUCTIONS_SYSVAR_ID + }; + AccountMeta::new_readonly(address, false) + })); for (source, destination) in source_buffers.iter().zip(destinations) { accounts.push(AccountMeta::new(*source, false)); accounts.push(AccountMeta::new(*destination, false)); @@ -330,10 +337,22 @@ mod tests { /// settlement can name both programs — or leave either one out. #[test] fn finalize_settle_carries_the_token_program_slots_it_is_given() { - for only_token_program in [ - None, - Some(TokenProgram::SplToken), - Some(TokenProgram::Token2022), + for (only_token_program, expected) in [ + ( + None, + [ + TokenProgram::SplToken.address(), + TokenProgram::Token2022.address(), + ], + ), + ( + Some(TokenProgram::SplToken), + [TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID], + ), + ( + Some(TokenProgram::Token2022), + [INSTRUCTIONS_SYSVAR_ID, TokenProgram::Token2022.address()], + ), ] { let ix = Instruction::from(FinalizeSettle { program_id: Pubkey::new_unique(), @@ -347,9 +366,8 @@ mod tests { }); let slots: Vec = ix.accounts[2..].iter().map(|meta| meta.pubkey).collect(); assert_eq!( - slots, - TokenProgram::addresses(only_token_program), - "{only_token_program:?} should be laid out as its own addresses", + slots, expected, + "{only_token_program:?} should name just the programs it settles against", ); } } diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index 60668ae4..62ee4fd7 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -24,38 +24,6 @@ impl TokenProgram { Self::Token2022 => spl_token_2022_interface::ID, } } - - /// The addresses a `BeginSettle`/`FinalizeSettle` pair puts in its - /// token-program slots, one per entry of [`Self::ALL`] and in that order. - /// - /// Both instructions take one account per supported program, at fixed - /// positions, and issue each transfer against the program that owns the - /// account it moves — so a settlement naming every program may mix tokens - /// from both. `only_token_program` is what narrows that: `None` names them - /// all, and `Some(program)` names just that one, leaving - /// [`INSTRUCTIONS_SYSVAR_ID`] in every other slot. - pub const fn addresses(only_token_program: Option) -> [Pubkey; Self::ALL.len()] { - let [spl_token, token_2022] = Self::ALL; - [ - spl_token.slot(only_token_program), - token_2022.slot(only_token_program), - ] - } - - /// The address this program's own slot holds. The slots are not read - /// on-chain, so a program the settlement doesn't touch is left out by - /// standing [`INSTRUCTIONS_SYSVAR_ID`] in: nearly every settlement transaction - /// names the system program already, so it costs one more account index - /// rather than another 32-byte address. - const fn slot(self, only_token_program: Option) -> Pubkey { - match only_token_program { - // Compared as discriminants because `PartialEq` isn't const. That - // keeps the narrowing correct for any variant added to `ALL`, - // rather than making this a second place to list them. - Some(only) if only as u8 != self as u8 => INSTRUCTIONS_SYSVAR_ID, - _ => self.address(), - } - } } impl TryFrom<&Pubkey> for TokenProgram { @@ -111,37 +79,4 @@ mod tests { Err(ProgramError::IncorrectProgramId), ); } - - /// Naming every program puts each of them in its own slot, in the order - /// the on-chain side pairs a slot with the program it stands for by. - #[test] - fn every_program_is_named_when_the_settlement_is_not_narrowed() { - assert_eq!( - TokenProgram::addresses(None), - TokenProgram::ALL.map(TokenProgram::address), - ); - } - - /// Narrowing to one program keeps that program in its own slot and leaves - /// the placeholder everywhere else, so a settlement pays for the addresses - /// of only the programs it touches. - #[test] - fn narrowing_to_one_program_leaves_the_placeholder_in_every_other_slot() { - for (named, only) in TokenProgram::ALL.into_iter().enumerate() { - let addresses = TokenProgram::addresses(Some(only)); - for (slot, (address, program)) in - addresses.into_iter().zip(TokenProgram::ALL).enumerate() - { - let expected = if slot == named { - only.address() - } else { - INSTRUCTIONS_SYSVAR_ID - }; - assert_eq!( - address, expected, - "a settlement narrowed to {only:?} should not name {program:?}", - ); - } - } - } } diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 6656409e..755e3a00 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -558,9 +558,10 @@ fn rejects_orders_in_wrong_address_order() { AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), AccountMeta::new_readonly(find_state_pda(&program_id).0, false), ]; + // Narrowed to the legacy program, so Token-2022's slot holds the placeholder. accounts.extend( - TokenProgram::addresses(Some(TokenProgram::SplToken)) - .map(|program| AccountMeta::new_readonly(program, false)), + [TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID] + .map(|address| AccountMeta::new_readonly(address, false)), ); for (order_pda, intent) in orders { accounts.push(AccountMeta::new_readonly(order_pda, false)); From 0a315b5bff52e1378090f207e49dec5e4c79b7a3 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:10:16 +0900 Subject: [PATCH 29/38] fix another lint --- programs/settlement/tests/begin_settle_orders.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index f46f6c77..1833c609 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -432,7 +432,7 @@ fn rejects_sell_account_under_a_unsupported_token_program() { // Repoint the copy at the cloned mint, so the pair stands on its own under // the clone instead of borrowing the real mint. let mut token = litesvm_token::get_spl_account::( - &mut svm, &account, + &svm, &account, ) .expect("the freshly delegated account is a valid token account"); token.mint = sell_mint; From 2666d5d71fdb267d5c89d0d59b4681b8147bab14 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:29:05 +0900 Subject: [PATCH 30/38] fmt --- programs/settlement/tests/begin_settle_orders.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 1833c609..e4451327 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -431,10 +431,9 @@ fn rejects_sell_account_under_a_unsupported_token_program() { let sell_mint = common::token::clone_under_new_program(&mut svm, &mint, &fake_token_program); // Repoint the copy at the cloned mint, so the pair stands on its own under // the clone instead of borrowing the real mint. - let mut token = litesvm_token::get_spl_account::( - &svm, &account, - ) - .expect("the freshly delegated account is a valid token account"); + let mut token = + litesvm_token::get_spl_account::(&svm, &account) + .expect("the freshly delegated account is a valid token account"); token.mint = sell_mint; let mut data = vec![0u8; litesvm_token::spl_token::state::Account::LEN]; token.pack_into_slice(&mut data); From 88f1e37c46a9f79437a8d9a2becdf09dc0bac366 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:21:07 +0900 Subject: [PATCH 31/38] Update programs/settlement/tests/begin_settle_orders.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/begin_settle_orders.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index e4451327..d706c21a 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -421,16 +421,15 @@ fn rejects_sell_account_under_a_unsupported_token_program() { let amount = 1_000_000; - // Build the genuine article first, so what gets cloned is a real token's - // bytes rather than a test's idea of them. + // We set up a standard token account, ready to trade let mint = common::token::create_mint(&mut svm, &payer); let account = common::token::create_token_account(&mut svm, &payer, &mint, &payer.pubkey()); common::token::fund_and_delegate(&mut svm, &program_id, &payer, &account, amount); + // We clone the previous mint but assign it to a fake program let fake_token_program = create_account(&mut svm, &payer.pubkey(), &[]); let sell_mint = common::token::clone_under_new_program(&mut svm, &mint, &fake_token_program); - // Repoint the copy at the cloned mint, so the pair stands on its own under - // the clone instead of borrowing the real mint. + // We replace the mint of the previous account with the new mint let mut token = litesvm_token::get_spl_account::(&svm, &account) .expect("the freshly delegated account is a valid token account"); From c511b67e243fc90634ec96fe36e001a573d3c270 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:22:21 +0900 Subject: [PATCH 32/38] Update programs/settlement/tests/settle_token_programs.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/settle_token_programs.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index ad6d75f9..5f3ab29f 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -367,10 +367,9 @@ fn narrowing_begin_settle_drops_one_account_from_the_transaction() { &intent.sell_token_account, AMOUNT, ); - let sell_mint = token::mint_of(&svm, &intent.sell_token_account); - let buy_mint = token::mint_of(&svm, &intent.buy_token_account); - buffer::ensure_funded(&mut svm, &program_id, &payer, &buy_mint, AMOUNT); - let destination = token::create_token_account(&mut svm, &payer, &sell_mint, &unique_pubkey()); + buffer::ensure_funded(&mut svm, &program_id, &payer, &intent.buy_mint, AMOUNT); + let destination = + token::create_token_account(&mut svm, &payer, &intent.sell_mint, &unique_pubkey()); let pulls = [Pull { destination, amount: AMOUNT, From eec9376b2763d5c48a7bd87357c8788054abb636 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:22:39 +0900 Subject: [PATCH 33/38] Update programs/settlement/tests/settle_token_programs.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/settle_token_programs.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index 5f3ab29f..b0f24d88 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -346,8 +346,6 @@ fn settles_with_the_token_program_slots_swapped() { #[test] fn narrowing_begin_settle_drops_one_account_from_the_transaction() { - /// What the order settles for. Any amount does; it just has to be the same - /// in both transactions, so the two differ only in what they name. const AMOUNT: u64 = 100; let (mut svm, program_id, payer, solver) = setup_settle_ready(); From 26068b5ec1cb7d8b38d11765a1d50af6ad0d72e1 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:23:43 +0900 Subject: [PATCH 34/38] Update programs/settlement/tests/settle_token_programs.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/settle_token_programs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index b0f24d88..ab4cb515 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -374,7 +374,7 @@ fn narrowing_begin_settle_drops_one_account_from_the_transaction() { }]; let blockhash = svm.latest_blockhash(); - let verify_narrowed = |only_token_program| { + let settle_tx = |only_token_program| { let initialized = [InitializedIntent { intent: &intent, pulls: &pulls, From 782d319d1d7cf1400deb98a3a55555cd90b2f9c7 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:24:14 +0900 Subject: [PATCH 35/38] Update test-cli/src/cmd/settle.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- test-cli/src/cmd/settle.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index bd26d960..6e861d65 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -116,8 +116,6 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { program_id: ctx.program_id, solver, finalize_ix_index, - // Token resolution builds legacy SPL accounts throughout (see - // `crate::token`), so Token-2022's slot stays empty. only_token_program: None, orders: &initialized_intents, auction_id: 0, From d91701b46a57f9f4ccdc5a56aaa00c163d04b1f8 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:32:36 +0900 Subject: [PATCH 36/38] assert settlement error is unneeded --- programs/settlement/tests/common/mod.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index 25d71691..1176cdda 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -200,19 +200,6 @@ pub fn assert_instruction_error_at( ); } -/// Convenience wrapper around [`assert_instruction_error_at`] for asserting a -/// specific [`SettlementError`] at the instruction that produced it: settlements -/// run as a `[BeginSettle, FinalizeSettle]` pair, so the failing instruction -/// isn't always the first. -#[track_caller] -pub fn assert_settlement_error( - ix_idx: u8, - result: Result, - expected: impl Into, -) { - assert_instruction_error_at(ix_idx, result, expected); -} - pub fn create_account_at(svm: &mut LiteSVM, address: Pubkey, owner: &Pubkey, data: &[u8]) { let lamports = svm.minimum_balance_for_rent_exemption(data.len()); svm.set_account( From e44ea53fd06fe06d16f3444b8b2d580171e017b8 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:48:09 +0900 Subject: [PATCH 37/38] remove unnecessary test --- interface/src/token_program.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs index 62ee4fd7..27a7bd9f 100644 --- a/interface/src/token_program.rs +++ b/interface/src/token_program.rs @@ -69,14 +69,4 @@ mod tests { Err(ProgramError::IncorrectProgramId), ); } - - /// The placeholder has to be something no token account can be owned by, - /// or a slot carrying it would still execute transfers somewhere. - #[test] - fn the_placeholder_is_not_a_token_program() { - assert_eq!( - TokenProgram::try_from(&INSTRUCTIONS_SYSVAR_ID), - Err(ProgramError::IncorrectProgramId), - ); - } } From 6feaeb502312a3fa9295865fa123d48f527d04a6 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:53:14 +0900 Subject: [PATCH 38/38] lint fix and fmt --- programs/settlement/tests/begin_settle_orders.rs | 4 ++-- programs/settlement/tests/settle_token_programs.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index d706c21a..efa6c346 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -426,10 +426,10 @@ fn rejects_sell_account_under_a_unsupported_token_program() { let account = common::token::create_token_account(&mut svm, &payer, &mint, &payer.pubkey()); common::token::fund_and_delegate(&mut svm, &program_id, &payer, &account, amount); - // We clone the previous mint but assign it to a fake program + // We clone the previous mint but assign it to a fake program let fake_token_program = create_account(&mut svm, &payer.pubkey(), &[]); let sell_mint = common::token::clone_under_new_program(&mut svm, &mint, &fake_token_program); - // We replace the mint of the previous account with the new mint + // We replace the mint of the previous account with the new mint let mut token = litesvm_token::get_spl_account::(&svm, &account) .expect("the freshly delegated account is a valid token account"); diff --git a/programs/settlement/tests/settle_token_programs.rs b/programs/settlement/tests/settle_token_programs.rs index ab4cb515..15432e39 100644 --- a/programs/settlement/tests/settle_token_programs.rs +++ b/programs/settlement/tests/settle_token_programs.rs @@ -405,9 +405,9 @@ fn narrowing_begin_settle_drops_one_account_from_the_transaction() { ) }; - let both = verify_narrowed(None); - let narrowed_legacy = verify_narrowed(Some(TokenProgram::SplToken)); - let narrowed_2022 = verify_narrowed(Some(TokenProgram::Token2022)); + let both = settle_tx(None); + let narrowed_legacy = settle_tx(Some(TokenProgram::SplToken)); + let narrowed_2022 = settle_tx(Some(TokenProgram::Token2022)); assert_eq!( narrowed_legacy.message.account_keys.len() + 1,