diff --git a/bench-report.json b/bench-report.json index f7a86db..df7ad9f 100644 --- a/bench-report.json +++ b/bench-report.json @@ -33,23 +33,23 @@ "create_buffers/max_buffers_in_one_instruction": 177040, "create_order/happy_path_creates_order_pda_with_expected_body": 4978, "initialize/happy_path_initializes_state_pda_with_expected_data": 4529, - "reclaim_buffer/funded_buffer_is_skipped": 6333, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7481, - "reclaim_buffer/max_buffers_in_one_instruction": 136650, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18080, + "reclaim_buffer/funded_buffer_is_skipped": 6335, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7483, + "reclaim_buffer/max_buffers_in_one_instruction": 136652, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18082, "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2183, - "settle/finalizes_with_no_pushes": 7070, - "settle/pulls_from_multiple_orders": 19949, - "settle/pulls_funds_to_destination": 13542, - "settle/pulls_to_multiple_destinations": 14694, - "settle/pushes_a_single_order": 12380, - "settle/pushes_several_orders_from_different_buffers": 17632, - "settle/pushes_several_orders_from_one_buffer": 17631, - "settle/settles_a_single_order": 12398, - "settle/settles_multiple_orders": 22926, - "transfer_authority/manager_can_transfer_manager": 3175, - "transfer_authority/manager_can_transfer_reclaim_authority": 3177, - "transfer_authority/reclaim_authority_can_transfer_itself": 3181 + "settle/finalizes_with_no_pushes": 7152, + "settle/pulls_from_multiple_orders": 20041, + "settle/pulls_funds_to_destination": 13630, + "settle/pulls_to_multiple_destinations": 14771, + "settle/pushes_a_single_order": 12485, + "settle/pushes_several_orders_from_different_buffers": 17749, + "settle/pushes_several_orders_from_one_buffer": 17748, + "settle/settles_a_single_order": 12503, + "settle/settles_multiple_orders": 23060, + "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, @@ -64,15 +64,15 @@ "reclaim_buffer/max_buffers_in_one_instruction": 332, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466, "reclaim_order/happy_path_returns_lamports_and_closes_pda": 236, - "settle/finalizes_with_no_pushes": 289, - "settle/pulls_from_multiple_orders": 655, - "settle/pulls_funds_to_destination": 472, - "settle/pulls_to_multiple_destinations": 513, - "settle/pushes_a_single_order": 431, - "settle/pushes_several_orders_from_different_buffers": 573, - "settle/pushes_several_orders_from_one_buffer": 541, - "settle/settles_a_single_order": 431, - "settle/settles_multiple_orders": 715, + "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, "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/client/src/instructions.rs b/client/src/instructions.rs index 17cc5a9..e1c7976 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -26,6 +26,7 @@ pub struct InitializedIntent<'a> { /// Builder for a `BeginSettle` instruction settling the given orders. pub struct BeginSettle<'a> { pub program_id: Pubkey, + pub solver: Pubkey, pub finalize_ix_index: u16, /// The off-chain auction this settlement executes, carried so it can be tied /// back to its auction off-chain. @@ -48,6 +49,7 @@ impl From> for Instruction { cow_settlement_interface::instruction::settle::BeginSettle { program_id: builder.program_id, state_pda, + solver: builder.solver, finalize_ix_index: builder.finalize_ix_index, auction_id: builder.auction_id, order_pdas: &order_pdas, @@ -275,6 +277,7 @@ mod tests { use ::proptest::{prelude::*, test_runner::TestCaseError}; use cow_settlement_interface::{ data::intent::fixtures::arb_order_intent, + fixtures::pubkey_from_seed, instruction::{ fixtures::fake_account_from_array, settle::{ @@ -294,7 +297,7 @@ mod tests { finalize_ix_index in any::(), intents in prop::collection::vec(arb_order_intent(), 1..=5), ) { - let program_id = Pubkey::new_unique(); + let program_id = pubkey_from_seed("program id"); // No pulls here: this test only checks that orders are derived and // laid out correctly. let orders: Vec = intents @@ -303,6 +306,7 @@ mod tests { .collect(); let ix = Instruction::from(BeginSettle { program_id, + solver: pubkey_from_seed("solver"), finalize_ix_index, auction_id: 0, orders: &orders, @@ -358,7 +362,7 @@ mod tests { 1..=5, ), ) { - let program_id = Pubkey::new_unique(); + let program_id = pubkey_from_seed("program id"); let orders: Vec = cases .iter() .map(|(intent, amount)| FinalizedIntent { diff --git a/client/src/parse.rs b/client/src/parse.rs index d83ea15..6088546 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -115,6 +115,7 @@ mod tests { .into(), SettlementInstruction::BeginSettle => BeginSettle { program_id, + solver: payer, finalize_ix_index: 1, auction_id: 42, orders: &[InitializedIntent { diff --git a/interface/src/data/state.rs b/interface/src/data/state.rs index e869d71..3850384 100644 --- a/interface/src/data/state.rs +++ b/interface/src/data/state.rs @@ -146,6 +146,11 @@ impl> StateAccount { self.solver_region().binary_search(&seek) } + /// Whether `solver` is in the solver list. + pub fn is_solver(&self, solver: &Pubkey) -> bool { + self.solver_search(solver).is_ok() + } + /// The stored solvers, in order (sorted ascending by address). pub fn solvers(&self) -> impl Iterator + '_ { self.solver_region() @@ -481,6 +486,29 @@ mod tests { use super::*; proptest! { + /// `is_solver` is true for every stored solver and false for one that + /// isn't stored. + #[test] + fn is_solver_reflects_membership( + header in fixtures::arb_init_params(), + // Unique and already sorted, being a `BTreeSet`. + raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 0..50), + raw_absent in any::<[u8; 32]>(), + ) { + prop_assume!(!raw_solvers.contains(&raw_absent)); + let stored: Vec = + raw_solvers.into_iter().map(Pubkey::new_from_array).collect(); + let absent = Pubkey::new_from_array(raw_absent); + + let bytes = fixtures::state_account_bytes(&header, &stored); + let state = StateAccount::attach(&bytes[..]).expect("valid header"); + + for solver in &stored { + prop_assert!(state.is_solver(solver)); + } + prop_assert!(!state.is_solver(&absent)); + } + /// The encode roundtrip: any two role holders written with /// `initialize` read back unchanged. #[test] diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 4a9f525..a4284f8 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -33,9 +33,11 @@ pub struct Pull { /// Wire format (grouped, with `n` orders and `T` total transfers): /// `[discriminator=0][finalize_ix_index: u16 LE][auction_id: i64 LE][n: u8] /// [transfer_count×n][amount: u64 LE ×T]`. -/// Required accounts: `[instructions_sysvar (R), state_pda (R), token_program -/// (R)]` followed, per order, by `[order_pda (W), sell_token_account (W), -/// destination (W)...]`. +/// 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)...]`. +/// +/// `solver` must sign, and the solver must be registered in the state pda. /// /// The program requires the order PDAs to be strictly increasing by address. /// This builder establishes that ordering for the caller: it sorts the orders by @@ -44,6 +46,7 @@ pub struct Pull { pub struct BeginSettle<'a> { pub program_id: Pubkey, pub state_pda: Pubkey, + pub solver: Pubkey, pub finalize_ix_index: u16, /// The off-chain auction this settlement executes. Carried in the /// instruction data so the settlement can be tied back to its auction @@ -59,6 +62,7 @@ impl From> for Instruction { let BeginSettle { program_id, state_pda, + solver, finalize_ix_index, auction_id, order_pdas, @@ -88,9 +92,10 @@ impl From> for Instruction { ] .concat(); - // Read-only accounts for instruction introspection, settlement state, and - // the SPL token program. + // The signing solver, followed by read-only accounts for instruction + // introspection, settlement state, and the SPL 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), @@ -194,6 +199,7 @@ pub struct BeginSettleInput<'a, A> { /// data. Not validated on-chain: it's carried only so the settlement can be /// tied back to its auction off-chain. pub auction_id: i64, + pub solver_account: &'a A, pub instructions_sysvar_account: &'a A, pub state_pda_account: &'a A, pub token_program_account: &'a A, @@ -209,7 +215,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 [instructions_sysvar_account, state_pda_account, token_program_account, order_accounts @ ..] = + let [solver_account, instructions_sysvar_account, state_pda_account, token_program_account, order_accounts @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -264,6 +270,7 @@ impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> { instructions_sysvar_account, state_pda_account, token_program_account, + solver_account, orders: SettledOrders { order_accounts, counts, @@ -276,18 +283,20 @@ impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> { #[cfg(test)] mod tests { use super::*; + use crate::fixtures::pubkey_from_seed; use crate::instruction::fixtures::{ fake_account, fake_account_from_array, fake_sequential_accounts, }; use crate::instruction::settle::tests::ix_data; - use crate::instruction::tests::assert_readonly_nonsigner; + use crate::instruction::tests::{assert_readonly_nonsigner, assert_readonly_signer}; use hex_literal::hex; use solana_account_view::AccountView; use solana_address::Address; /// The fixed accounts every `BeginSettle` carries before its order accounts: - /// the instructions sysvar, the settlement state PDA, and the token program. - const FIXED_ACCOUNTS: usize = 3; + /// the signing solver, the instructions sysvar, the settlement state PDA, and + /// the token program. + const FIXED_ACCOUNTS: usize = 4; /// A placeholder auction id for the tests where its specific value is /// incidental. The wire-layout tests spell out the literal bytes instead. @@ -295,8 +304,9 @@ mod tests { #[test] fn expected_encoding_begin_settle_no_orders() { - let program_id = Pubkey::new_unique(); - let state_pda = Pubkey::new_unique(); + let program_id = pubkey_from_seed("program id"); + let state_pda = pubkey_from_seed("state pda"); + let solver = pubkey_from_seed("solver"); let Instruction { program_id: ix_program_id, accounts, @@ -304,6 +314,7 @@ mod tests { } = BeginSettle { program_id, state_pda, + solver, finalize_ix_index: 0x1337, auction_id: 0x0102_0304_0506_0708, order_pdas: &[], @@ -321,20 +332,21 @@ mod tests { [0], // order count ], ); - // 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); - assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID); - assert_readonly_nonsigner(&accounts[1], state_pda); - assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID); + // 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); + 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); } #[test] fn begin_settle_sorts_orders_by_pda() { - let program_id = Pubkey::new_unique(); - let state_pda = Pubkey::new_unique(); + let program_id = pubkey_from_seed("program id"); + let state_pda = pubkey_from_seed("state pda"); + let solver = pubkey_from_seed("solver"); // Two orders supplied in descending PDA order. All the other parameters // are chosen to sort in the opposite order. let high_order_pda = Pubkey::new_from_array([0xbb; 32]); @@ -344,6 +356,7 @@ mod tests { let Instruction { data, accounts, .. } = BeginSettle { program_id, state_pda, + solver, finalize_ix_index: 0x1337, auction_id: AUCTION_ID, order_pdas: &[high_order_pda, low_order_pda], @@ -364,6 +377,7 @@ mod tests { ); let expected: Vec = vec![ + solver, INSTRUCTIONS_SYSVAR_ID, state_pda, SPL_TOKEN_PROGRAM_ID, @@ -389,13 +403,16 @@ mod tests { high_sell_token_account, ], ); - assert!(accounts.iter().all(|account| !account.is_signer)); + // The solver is the sole signer; none of the order accounts signs. + let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS); + assert!(order_accounts.iter().all(|account| !account.is_signer)); } #[test] fn begin_settle_encodes_grouped_transfers() { - let program_id = Pubkey::new_unique(); - let state_pda = Pubkey::new_unique(); + let program_id = pubkey_from_seed("program id"); + let state_pda = pubkey_from_seed("state pda"); + let solver = pubkey_from_seed("solver"); let order_a = Pubkey::new_from_array([0x01; 32]); let sell_a = Pubkey::new_from_array([0x02; 32]); let order_b = Pubkey::new_from_array([0x03; 32]); @@ -408,6 +425,7 @@ mod tests { let Instruction { data, accounts, .. } = BeginSettle { program_id, state_pda, + solver, finalize_ix_index: 0x1337, auction_id: AUCTION_ID, order_pdas: &[order_a, order_b], @@ -447,6 +465,7 @@ mod tests { ); let expected: Vec = vec![ + solver, INSTRUCTIONS_SYSVAR_ID, state_pda, SPL_TOKEN_PROGRAM_ID, @@ -470,16 +489,19 @@ mod tests { writable, vec![order_a, sell_a, dest_a0, dest_a1, order_b, sell_b, dest_b0], ); - assert!(accounts.iter().all(|account| !account.is_signer)); + // The solver is the sole signer; none of the order accounts signs. + let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS); + assert!(order_accounts.iter().all(|account| !account.is_signer)); } #[test] fn begin_settle_input_parses_valid_input() { - let sysvar = Address::new_from_array([0x42u8; 32]); - // The state-PDA and token-program slots are reserved but not surfaced. - let state = Address::new_from_array([0x43u8; 32]); - let token_program = Address::new_from_array([0x44u8; 32]); + let sysvar = pubkey_from_seed("sysvar"); + let state = pubkey_from_seed("state pda"); + let token_program = pubkey_from_seed("token program"); + let solver = pubkey_from_seed("solver"); let accounts = [ + fake_account(solver), fake_account(sysvar), fake_account(state), fake_account(token_program), @@ -493,6 +515,7 @@ mod tests { let BeginSettleInput { finalize_ix_index, auction_id, + solver_account, instructions_sysvar_account, orders, token_program_account, @@ -504,6 +527,7 @@ mod tests { assert_eq!(orders.iter().count(), 0); assert_eq!(token_program_account.address(), &token_program); assert_eq!(state_pda_account.address(), &state); + assert_eq!(solver_account.address(), &solver); } #[test] @@ -550,12 +574,14 @@ mod tests { #[test] fn begin_settle_input_pairs_orders_with_their_accounts() { - let sysvar = Address::new_from_array([1u8; 32]); - let state = Address::new_from_array([0xa1u8; 32]); - let token_program = Address::new_from_array([0xa2u8; 32]); - let order_pda = Address::new_from_array([2u8; 32]); - let sell_token = Address::new_from_array([3u8; 32]); + let sysvar = pubkey_from_seed("sysvar"); + let state = pubkey_from_seed("state pda"); + let token_program = pubkey_from_seed("token program"); + let solver = pubkey_from_seed("solver"); + let order_pda = pubkey_from_seed("order pda"); + let sell_token = pubkey_from_seed("sell token"); let accounts = [ + fake_account(solver), fake_account(sysvar), fake_account(state), fake_account(token_program), @@ -572,6 +598,7 @@ mod tests { let BeginSettleInput { finalize_ix_index, auction_id, + solver_account, instructions_sysvar_account, orders, state_pda_account, @@ -582,6 +609,7 @@ mod tests { assert_eq!(instructions_sysvar_account.address(), &sysvar); assert_eq!(token_program_account.address(), &token_program); assert_eq!(state_pda_account.address(), &state); + assert_eq!(solver_account.address(), &solver); let mut orders = orders.iter(); let order = orders.next().expect("one settled order"); @@ -593,14 +621,16 @@ mod tests { #[test] fn begin_settle_input_parses_transfers() { - let sysvar = Address::new_from_array([1u8; 32]); - let state = Address::new_from_array([0xa1u8; 32]); - let token_program = Address::new_from_array([0xa2u8; 32]); - let order_pda = Address::new_from_array([2u8; 32]); - let sell_token = Address::new_from_array([3u8; 32]); - let dest0 = Address::new_from_array([4u8; 32]); - let dest1 = Address::new_from_array([5u8; 32]); + let sysvar = pubkey_from_seed("sysvar"); + let state = pubkey_from_seed("state pda"); + let token_program = pubkey_from_seed("token program"); + let solver = pubkey_from_seed("solver"); + let order_pda = pubkey_from_seed("order pda"); + let sell_token = pubkey_from_seed("sell token"); + let dest0 = pubkey_from_seed("destination 0"); + let dest1 = pubkey_from_seed("destination 1"); let accounts = [ + fake_account(solver), fake_account(sysvar), fake_account(state), fake_account(token_program), @@ -642,17 +672,18 @@ mod tests { let mut expected: Vec<(Address, Address)> = Vec::new(); for i in 0..ORDER_COUNT { - let order_pda = Address::new_from_array([i as u8; 32]); - let sell_token = Address::new_from_array([(i + ORDER_COUNT) as u8; 32]); + let order_pda = pubkey_from_seed(&format!("order pda {i}")); + let sell_token = pubkey_from_seed(&format!("sell token {i}")); expected.push((order_pda, sell_token)); } - // The three fixed accounts (`[0xff..]`, `[0xfe..]`, `[0xfd..]`) differ - // from every order/token address above. + // The four fixed accounts (`[0xff..]`, `[0xfe..]`, `[0xfd..]`, `[0xfc..]`) + // 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]), ]; 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 24ce576..b6ed462 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -246,6 +246,7 @@ impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> { #[cfg(test)] mod tests { use super::*; + use crate::fixtures::pubkey_from_seed; use crate::instruction::fixtures::{ fake_account, fake_account_from_array, fake_sequential_accounts, }; @@ -261,8 +262,8 @@ mod tests { // A no-push finalize carries exactly the fixed accounts, so the constant // must equal the account count the builder emits with no pushes. let ix = Instruction::from(FinalizeSettle { - program_id: Pubkey::new_unique(), - state_pda: Pubkey::new_unique(), + program_id: pubkey_from_seed("program id"), + state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, source_buffers: &[], destinations: &[], @@ -274,8 +275,8 @@ mod tests { #[test] fn expected_encoding_finalize_settle_no_pushes() { - let program_id = Pubkey::new_unique(); - let state_pda = Pubkey::new_unique(); + let program_id = pubkey_from_seed("program id"); + let state_pda = pubkey_from_seed("state pda"); let Instruction { program_id: ix_program_id, accounts, @@ -310,8 +311,8 @@ mod tests { #[test] fn finalize_settle_encodes_pushes() { - let program_id = Pubkey::new_unique(); - let state_pda = Pubkey::new_unique(); + let program_id = pubkey_from_seed("program id"); + let state_pda = pubkey_from_seed("state pda"); let source_a = Pubkey::new_from_array([0x01; 32]); let dest_a = Pubkey::new_from_array([0x02; 32]); let source_b = Pubkey::new_from_array([0x03; 32]); @@ -366,10 +367,9 @@ mod tests { #[test] fn finalize_settle_input_parses_no_pushes() { - let sysvar = Address::new_from_array([0x42u8; 32]); - // The state-PDA and token-program slots are reserved but not surfaced. - let state = Address::new_from_array([0x43u8; 32]); - let token_program = Address::new_from_array([0x44u8; 32]); + let sysvar = pubkey_from_seed("sysvar"); + let state = pubkey_from_seed("state pda"); + let token_program = pubkey_from_seed("token program"); let accounts = [ fake_account(sysvar), fake_account(state), @@ -395,14 +395,14 @@ mod tests { #[test] fn finalize_settle_input_parses_pushes() { - let sysvar = Address::new_from_array([1u8; 32]); - let state = Address::new_from_array([0xa1u8; 32]); - let token_program = Address::new_from_array([0xa2u8; 32]); + let sysvar = pubkey_from_seed("sysvar"); + let state = pubkey_from_seed("state pda"); + let token_program = pubkey_from_seed("token program"); // The same source buffer funds both pushes: parsing makes no uniqueness // assumption about source buffers. - let source = Address::new_from_array([3u8; 32]); - let dest0 = Address::new_from_array([4u8; 32]); - let dest1 = Address::new_from_array([5u8; 32]); + let source = pubkey_from_seed("source buffer"); + let dest0 = pubkey_from_seed("destination 0"); + let dest1 = pubkey_from_seed("destination 1"); let accounts = [ fake_account(sysvar), fake_account(state), @@ -455,8 +455,8 @@ mod tests { } let mut expected: Vec = Vec::new(); for i in 0..PUSH_COUNT { - let source = Address::new_from_array([i as u8; 32]); - let dest = Address::new_from_array([(i + PUSH_COUNT) as u8; 32]); + let source = pubkey_from_seed(&format!("source buffer {i}")); + let dest = pubkey_from_seed(&format!("destination {i}")); let bump = (i + 2 * PUSH_COUNT) as u8; let amount = u64::from_le_bytes([(i + 3 * PUSH_COUNT) as u8; 8]); expected.push(ExpectedPush { @@ -575,11 +575,17 @@ mod tests { let bumps = [0xa1, 0xb1]; let amounts = [0x0102, 0x0304]; let ix = Instruction::from(FinalizeSettle { - program_id: Pubkey::new_unique(), - state_pda: Pubkey::new_unique(), + program_id: pubkey_from_seed("program id"), + state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0x1337, - source_buffers: &[Pubkey::new_unique(), Pubkey::new_unique()], - destinations: &[Pubkey::new_unique(), Pubkey::new_unique()], + source_buffers: &[ + pubkey_from_seed("source buffer 0"), + pubkey_from_seed("source buffer 1"), + ], + destinations: &[ + pubkey_from_seed("destination 0"), + pubkey_from_seed("destination 1"), + ], bumps: &bumps, amounts: &amounts, }); @@ -592,8 +598,8 @@ mod tests { #[test] fn finalize_push_data_handles_no_pushes() { let ix = Instruction::from(FinalizeSettle { - program_id: Pubkey::new_unique(), - state_pda: Pubkey::new_unique(), + program_id: pubkey_from_seed("program id"), + state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, source_buffers: &[], destinations: &[], @@ -607,11 +613,11 @@ mod tests { #[test] fn finalize_push_data_rejects_incorrect_bytes() { let mut ix = Instruction::from(FinalizeSettle { - program_id: Pubkey::new_unique(), - state_pda: Pubkey::new_unique(), + program_id: pubkey_from_seed("program id"), + state_pda: pubkey_from_seed("state pda"), begin_ix_index: 0, - source_buffers: &[Pubkey::new_unique()], - destinations: &[Pubkey::new_unique()], + source_buffers: &[pubkey_from_seed("source buffer")], + destinations: &[pubkey_from_seed("destination")], bumps: &[0xff], amounts: &[31337], }); diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 182a1f4..9a294c8 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -227,6 +227,9 @@ pub enum SettlementError { UnauthorizedSolverManagement = 35, /// `AddSolver`'s solver is already in the state PDA's solver list. SolverAlreadyExists = 36, + /// `BeginSettle`/`FinalizeSettle`'s solver account isn't a signer or isn't + /// in the state PDA's solver list, so it may not settle. + UnauthorizedSolver = 37, } impl From for u32 { diff --git a/programs/settlement/src/processor.rs b/programs/settlement/src/processor.rs index 059a124..7f3246c 100644 --- a/programs/settlement/src/processor.rs +++ b/programs/settlement/src/processor.rs @@ -10,6 +10,7 @@ use pinocchio::{ use pinocchio_system::instructions::CreateAccountAllowPrefund; use cow_settlement_interface::{ + data::state::StateAccount, pda::state::{state_pda_seeds, state_pda_signer_seeds}, SettlementError, }; @@ -125,22 +126,51 @@ pub fn check_state_pda( Ok(state_bump) } -/// Validate that `state_pda_account` is the canonical state PDA and run `f` with -/// a signer for it. The state PDA is the authority over the accounts settlement -/// touches — user token delegations and the buffers — so it must sign every CPI -/// that acts on them. +/// Run `f` with a signer for the state PDA, given its already-derived canonical +/// `state_bump`. /// -/// The signer only borrows its seed buffers, which are local to this frame; -/// running `f` here rather than returning the signer keeps them alive for as -/// long as `f` needs it. +/// This function is to be used as an alternative for [`with_state_pda_signer`] +/// in the case where the state PDA has been checked in an earlier call. +/// The caller is responsible for having validated the bump against the state +/// PDA (e.g. via [`check_state_pda`] or [`require_solver`]). +/// +/// If state PDA validation is needed, use [`with_state_pda_signer`]. +pub fn with_state_pda_signer_from_bump( + state_bump: u8, + f: impl FnOnce(&Signer) -> ProgramResult, +) -> ProgramResult { + let state_bump = [state_bump]; + let signer_seeds = state_pda_signer_seeds(&state_bump).map(Seed::from); + f(&Signer::from(&signer_seeds)) +} +/// Validate that `state_pda_account` is the canonical state PDA and run `f` +/// with a signer for it, in one step. Use [`with_state_pda_signer_from_bump`] +/// directly when the bump has already been derived (as settling does, via +/// [`require_solver`]) to avoid re-deriving the PDA. pub fn with_state_pda_signer( program_id: &Address, state_pda_account: &AccountView, f: impl FnOnce(&Signer) -> ProgramResult, ) -> ProgramResult { - let state_bump = [check_state_pda(program_id, state_pda_account)?]; - let signer_seeds = state_pda_signer_seeds(&state_bump).map(Seed::from); - f(&Signer::from(&signer_seeds)) + with_state_pda_signer_from_bump(check_state_pda(program_id, state_pda_account)?, f) +} + +/// Confirm that the given account is a valid solver. +/// +/// Returns the state PDA's canonical bump, so the caller can build its signer via +/// [`with_state_pda_signer_from_bump`] without deriving the PDA a second time. +#[must_use = "ignoring the result skips solver authentication"] +pub fn require_solver( + program_id: &Address, + state_pda_account: &AccountView, + solver_account: &AccountView, +) -> Result { + let state_bump = check_state_pda(program_id, state_pda_account)?; + let state = StateAccount::attach(state_pda_account.try_borrow()?)?; + if !solver_account.is_signer() || !state.is_solver(solver_account.address()) { + return Err(SettlementError::UnauthorizedSolver.into()); + } + Ok(state_bump) } pub fn is_cpi_call() -> bool { diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 4c86cf3..498adff 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -29,7 +29,7 @@ use pinocchio::{ }; use pinocchio_token::{instructions::Transfer, state::Account as TokenAccount}; -use crate::processor::{is_cpi_call, with_state_pda_signer}; +use crate::processor::{is_cpi_call, require_solver, with_state_pda_signer_from_bump}; use super::{validate_counterpart, validate_token_program_account}; @@ -44,6 +44,10 @@ pub fn process_begin_settle( let input = BeginSettleInput::parse(instruction_data, accounts)?; + // Only an approved solver may settle. Reuse the bump this derives so the + // signer below doesn't re-derive the state PDA. + let state_bump = require_solver(program_id, input.state_pda_account, input.solver_account)?; + // We use `instructions_sysvar_account` from the input but this could be // any address since parsing doesn't validate the input. We rely on the // fact that the Pinocchio library already checks that the input account @@ -72,11 +76,11 @@ pub fn process_begin_settle( validate_token_program_account(input.token_program_account)?; - with_state_pda_signer(program_id, input.state_pda_account, |state_pda_signer| { + with_state_pda_signer_from_bump(state_bump, |signer| { settle_orders( program_id, input.state_pda_account, - state_pda_signer, + signer, &input.orders, &finalize_ix, ) diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index dfa5d34..3a2965f 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -24,7 +24,7 @@ use crate::common::{ order::{create_order_pda, sample_intent, settlable_intent, OrderBuilder}, replace_first_matching_account, send, set_unix_timestamp, settlement::{build_settlement, BEGIN_INDEX, FINALIZE_INDEX}, - setup, to_instruction_error, token, unique_pubkey, + setup_settle_ready, to_instruction_error, token, unique_pubkey, }; use cow_settlement_client::cow_settlement_interface::{ data::order::{EncodedOrderAccount, OrderAccount}, @@ -94,9 +94,17 @@ fn settle_and_pay( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, + solver: &Keypair, orders: &[InitializedIntent], ) -> Vec { - settle_and_pay_amounts(svm, program_id, payer, orders, &vec![0; orders.len()]) + settle_and_pay_amounts( + svm, + program_id, + payer, + solver, + orders, + &vec![0; orders.len()], + ) } /// Build the `[BeginSettle, FinalizeSettle]` instructions settling `orders` and @@ -108,6 +116,7 @@ fn settle_and_pay_amounts( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, + solver: &Keypair, orders: &[InitializedIntent], push_amounts: &[u64], ) -> Vec { @@ -125,6 +134,7 @@ fn settle_and_pay_amounts( let begin = BeginSettle { program_id: *program_id, + solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, orders, @@ -139,25 +149,26 @@ fn settle_and_pay_amounts( #[test] fn settles_a_single_order() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let instructions = settle_and_pay( &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("settlement should succeed"); } #[test] fn settles_multiple_orders() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let mut intents = Vec::new(); for salt in 0..3u8 { @@ -172,14 +183,14 @@ fn settles_multiple_orders() { .iter() .map(|intent| InitializedIntent { intent, pulls: &[] }) .collect(); - let instructions = settle_and_pay(&mut svm, &program_id, &payer, &orders); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + let instructions = settle_and_pay(&mut svm, &program_id, &payer, &solver, &orders); + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("multi-order settlement should succeed"); } #[test] fn rejects_wrong_stored_bump() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let (order_pda, bump) = find_order_pda(&program_id, &intent.uid()); @@ -200,20 +211,21 @@ fn rejects_wrong_stored_bump() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::AccountNotDerivable, ); } #[test] fn rejects_fabricated_program_owned_account() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let mint = token::create_mint(&mut svm, &payer); let sell_token = token::create_token_account(&mut svm, &payer, &mint, &payer.pubkey()); @@ -239,6 +251,7 @@ fn rejects_fabricated_program_owned_account() { let begin = BeginSettleRaw { program_id, state_pda: find_state_pda(&program_id).0, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, order_pdas: &[fake_order], @@ -259,14 +272,14 @@ fn rejects_fabricated_program_owned_account() { let instructions = vec![begin.into(), finalize.into()]; assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::AccountNotDerivable, ); } #[test] fn rejects_non_order_account_in_order_slot() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let mint = token::create_mint(&mut svm, &payer); let sell_token = token::create_token_account(&mut svm, &payer, &mint, &payer.pubkey()); @@ -278,6 +291,7 @@ fn rejects_non_order_account_in_order_slot() { let begin = BeginSettleRaw { program_id, state_pda: find_state_pda(&program_id).0, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, order_pdas: &[sell_token], @@ -297,14 +311,14 @@ fn rejects_non_order_account_in_order_slot() { let instructions = vec![begin.into(), finalize.into()]; assert_instruction_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::InvalidAccountData, ); } #[test] fn rejects_sell_token_account_mismatch() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let mint = token::create_mint(&mut svm, &payer); // Supply a different token account than the one the order's intent names. @@ -314,6 +328,7 @@ fn rejects_sell_token_account_mismatch() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], @@ -326,14 +341,14 @@ fn rejects_sell_token_account_mismatch() { ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::SellTokenAccountMismatch, ); } #[test] fn rejects_sell_token_owner_mismatch() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); let buy_mint = token::create_mint(&mut svm, &payer); @@ -354,20 +369,21 @@ fn rejects_sell_token_owner_mismatch() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::SellTokenOwnerMismatch, ); } #[test] fn rejects_non_token_sell_account() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let non_token = unique_pubkey(); @@ -381,20 +397,21 @@ fn rejects_non_token_sell_account() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::SellTokenAccountInvalid, ); } #[test] fn rejects_sell_token_account_recreated_for_another_mint() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let another_mint = token::create_mint(&mut svm, &payer); @@ -404,26 +421,28 @@ fn rejects_sell_token_account_recreated_for_another_mint() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::SellMintMismatch, ); } #[test] fn rejects_duplicate_orders() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let instructions = settle_and_pay( &mut svm, &program_id, &payer, + &solver, &[ InitializedIntent { intent: &intent, @@ -436,14 +455,14 @@ fn rejects_duplicate_orders() { ], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::OrdersNotStrictlyIncreasing, ); } #[test] fn rejects_orders_in_wrong_address_order() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let first = OrderBuilder::new(&mut svm, &program_id, &payer) .salt(0) @@ -459,10 +478,11 @@ fn rejects_orders_in_wrong_address_order() { // the program rejects. The interface builders would sort them, so build both // 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 `[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. + // (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. let mut orders = [(first_pda, &first), (second_pda, &second)]; orders.sort_by_key(|&(pda, ..)| std::cmp::Reverse(pda)); @@ -474,6 +494,8 @@ fn rejects_orders_in_wrong_address_order() { data.extend(orders.iter().map(|_| 0u8)); let mut accounts = vec![ + // The signing solver, registered by `setup_settle_ready`. + 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), @@ -513,14 +535,14 @@ fn rejects_orders_in_wrong_address_order() { }; let instructions = vec![begin, finalize.into()]; assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::OrdersNotStrictlyIncreasing, ); } #[test] fn rejects_cancelled_order() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); @@ -546,20 +568,21 @@ fn rejects_cancelled_order() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::OrderCancelled, ); } #[test] fn rejects_expired_order() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let valid_to = 1_000_000; let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -572,20 +595,21 @@ fn rejects_expired_order() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::OrderExpired, ); } #[test] fn settles_order_at_exact_valid_to() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let valid_to = 1_000_000; let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -597,17 +621,19 @@ fn settles_order_at_exact_valid_to() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], }], ); - send(&mut svm, &payer, instructions).expect("an order is still settleable at exactly valid_to"); + send(&mut svm, &solver, instructions) + .expect("an order is still settleable at exactly valid_to"); } #[test] fn pulls_funds_to_destination() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); // Sell 2_000_000 for at least 4_000_000 at limit price. @@ -627,6 +653,7 @@ fn pulls_funds_to_destination() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[Pull { @@ -636,7 +663,7 @@ fn pulls_funds_to_destination() { }], &[paid], ); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("a pull within the approved delegation, paid at the limit, should succeed"); assert_eq!(token::balance(&svm, &destination), amount); @@ -649,7 +676,7 @@ fn pulls_funds_to_destination() { #[test] fn pulls_to_multiple_destinations() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -669,6 +696,7 @@ fn pulls_to_multiple_destinations() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[ @@ -684,7 +712,7 @@ fn pulls_to_multiple_destinations() { }], &[paid], ); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("multiple pulls from one order should succeed"); assert_eq!(token::balance(&svm, &dest0), pulled0); @@ -701,7 +729,7 @@ fn pulls_to_multiple_destinations() { #[test] fn pulls_from_multiple_orders() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); // Two distinct orders, each selling from its own token account. @@ -742,6 +770,7 @@ fn pulls_from_multiple_orders() { &mut svm, &program_id, &payer, + &solver, &[ InitializedIntent { intent: &first, @@ -760,7 +789,7 @@ fn pulls_from_multiple_orders() { ], &[paid_first, paid_second], ); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("pulls from several orders should succeed"); assert_eq!(token::balance(&svm, &dest_first), pulled_first); @@ -777,7 +806,7 @@ fn pulls_from_multiple_orders() { #[test] fn rejects_pulls_summing_beyond_u64() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -795,6 +824,7 @@ fn rejects_pulls_summing_beyond_u64() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[ @@ -811,14 +841,14 @@ fn rejects_pulls_summing_beyond_u64() { &[0], ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::PullAmountOverflow, ); } #[test] fn zero_pulls_moves_nothing() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // The intent specifies a sell mint. We want to see that, when no pull is // specified, this account isn't touched in the transaction. let sell_mint = token::create_mint(&mut svm, &payer); @@ -842,6 +872,7 @@ fn zero_pulls_moves_nothing() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[], @@ -850,7 +881,7 @@ fn zero_pulls_moves_nothing() { let transaction = Transaction::new_signed_with_payer( &instructions, Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); let account_keys = transaction.message.account_keys.clone(); @@ -867,13 +898,14 @@ fn zero_pulls_moves_nothing() { #[test] fn rejects_wrong_state_pda() { - let (mut svm, program_id, payer) = setup(); + 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: &[], @@ -889,20 +921,21 @@ fn rejects_wrong_state_pda() { ); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::StateAccountMismatch, ); } #[test] fn rejects_wrong_token_program() { - let (mut svm, program_id, payer) = setup(); + 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: &[], @@ -918,14 +951,14 @@ fn rejects_wrong_token_program() { ); assert_instruction_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::IncorrectProgramId, ); } #[test] fn rejects_pull_delegated_to_incorrect_address() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -943,6 +976,7 @@ fn rejects_pull_delegated_to_incorrect_address() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[Pull { @@ -952,14 +986,14 @@ fn rejects_pull_delegated_to_incorrect_address() { }], ); assert_instruction_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::Custom(TokenError::OwnerMismatch as u32), ); } #[test] fn rejects_pull_exceeding_delegation() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -983,6 +1017,7 @@ fn rejects_pull_exceeding_delegation() { &mut svm, &program_id, &payer, + &solver, &[InitializedIntent { intent: &intent, pulls: &[Pull { @@ -992,7 +1027,7 @@ fn rejects_pull_exceeding_delegation() { }], ); assert_instruction_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::Custom(TokenError::InsufficientFunds as u32), ); assert_eq!(token::balance(&svm, &sell_token), initial_amount); @@ -1003,13 +1038,14 @@ fn rejects_pull_exceeding_delegation() { #[test] fn rejects_extra_account() { - let (mut svm, program_id, payer) = setup(); + 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: &[], @@ -1023,14 +1059,14 @@ fn rejects_extra_account() { .push(AccountMeta::new_readonly(unique_pubkey(), false)); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::AccountCountNotMatchingOrderCount, ); } #[test] fn rejects_push_to_wrong_destination() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, @@ -1043,20 +1079,20 @@ fn rejects_push_to_wrong_destination() { orders: &orders, }); // Redirect the push to an account that isn't the order's buy token account. - // Accounts: `[sysvar, state, token_program, source, destination]`. + // Finalize accounts: `[sysvar, state, token_program, source, destination]`. let destination_index = 4; finalize.accounts[destination_index].pubkey = unique_pubkey(); - let instructions = build_settlement(&program_id, &orders, finalize); + let instructions = build_settlement(&program_id, &solver.pubkey(), &orders, finalize); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::PushDestinationMismatch, ); } #[test] fn rejects_push_if_buffer_does_not_match_buy_mint() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let buy_mint = token::create_mint(&mut svm, &payer); let other_mint = token::create_mint(&mut svm, &payer); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -1079,16 +1115,16 @@ fn rejects_push_if_buffer_does_not_match_buy_mint() { amounts: &[100], }; - let instructions = build_settlement(&program_id, &orders, finalize); + let instructions = build_settlement(&program_id, &solver.pubkey(), &orders, finalize); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::PushSourceNotBuffer, ); } #[test] fn rejects_fewer_pushes_than_orders() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, @@ -1102,16 +1138,16 @@ fn rejects_fewer_pushes_than_orders() { orders: &[], }; - let instructions = build_settlement(&program_id, &orders, finalize); + let instructions = build_settlement(&program_id, &solver.pubkey(), &orders, finalize); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::SettledOrderPushCountMismatch, ); } #[test] fn rejects_more_pushes_than_orders() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); // A finalize that pushes to one order, paired with a begin that settles none, @@ -1125,16 +1161,16 @@ fn rejects_more_pushes_than_orders() { }], }; - let instructions = build_settlement(&program_id, &[], finalize); + let instructions = build_settlement(&program_id, &solver.pubkey(), &[], finalize); assert_begin_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), SettlementError::SettledOrderPushCountMismatch, ); } #[test] fn rejects_partial_push_amount_in_finalize_settle() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, @@ -1151,9 +1187,9 @@ fn rejects_partial_push_amount_in_finalize_settle() { // it rejects the malformed encoding before the finalize instruction runs. finalize.data.pop(); - let instructions = build_settlement(&program_id, &orders, finalize); + let instructions = build_settlement(&program_id, &solver.pubkey(), &orders, finalize); assert_eq!( - send(&mut svm, &payer, instructions).err(), + send(&mut svm, &solver, instructions).err(), Some(TransactionError::InstructionError( BEGIN_INDEX, InstructionError::InvalidInstructionData, diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index 08cc4f8..cb54dcc 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -14,7 +14,7 @@ pub mod settlement; pub mod state; pub mod token; -use cow_settlement_client::instructions::Initialize; +use cow_settlement_client::instructions::{AddSolver, Initialize}; use cow_settlement_interface::pda::state::find_state_pda; use cow_settlement_interface::Instruction; use cow_settlement_interface::SettlementError; @@ -124,6 +124,41 @@ pub fn setup_init() -> (LiteSVM, InitializedParams) { ) } +/// Register `solver` in the state PDA's solver list, authorized by the manager +/// and paid by the fee payer. +pub fn register_solver(svm: &mut LiteSVM, params: &InitializedParams, solver: &Pubkey) { + let tx = Transaction::new_signed_with_payer( + &[AddSolver { + program_id: params.program_id, + manager: params.manager.pubkey(), + payer: params.payer.pubkey(), + solver: *solver, + } + .into()], + Some(¶ms.payer.pubkey()), + &[¶ms.payer, ¶ms.manager], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .expect("registering a solver should succeed"); +} + +/// [`setup_init`] plus a freshly registered, funded `solver`, returned alongside +/// the fee `payer`. `payer` funds the test's setup transactions (creating orders, +/// funding buffers). `solver` authorizes settlements and must sign them; it is +/// airdropped so it can submit and pay for the settlement itself. +pub fn setup_settle_ready() -> (LiteSVM, Pubkey, Keypair, Keypair) { + let (mut svm, params) = setup_init(); + let solver = unique_keypair(); + register_solver(&mut svm, ¶ms, &solver.pubkey()); + svm.airdrop(&solver.pubkey(), 1_000_000_000) + .expect("airdrop to solver should succeed"); + let InitializedParams { + program_id, payer, .. + } = params; + (svm, program_id, payer, solver) +} + /// Adds CPI caller test helper to the given SVM pub fn setup_cpi_caller(svm: &mut LiteSVM) -> Pubkey { let cpi_caller_id = unique_pubkey(); diff --git a/programs/settlement/tests/common/settlement.rs b/programs/settlement/tests/common/settlement.rs index 26dbf7b..0d10eb9 100644 --- a/programs/settlement/tests/common/settlement.rs +++ b/programs/settlement/tests/common/settlement.rs @@ -16,6 +16,7 @@ pub const FINALIZE_INDEX: u8 = 1; /// to. Submit the result with [`send`](super::send). pub fn build_settlement( program_id: &Pubkey, + solver: &Pubkey, orders: &[FinalizedIntent], finalize: impl Into, ) -> Vec { @@ -28,6 +29,7 @@ pub fn build_settlement( .collect(); let begin = BeginSettle { program_id: *program_id, + solver: *solver, finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, orders: &begin_orders, diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index 6bcc74f..daa291f 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -15,7 +15,7 @@ use crate::common::{ order::{create_order_pda, settlable_intent, OrderBuilder}, replace_first_matching_account, send, settlement::{build_settlement, BEGIN_INDEX, FINALIZE_INDEX}, - setup, to_instruction_error, token, unique_pubkey, + setup_settle_ready, to_instruction_error, token, unique_pubkey, }; use cow_settlement_client::cow_settlement_interface::{ data::intent::OrderIntent, instruction::settle::SPL_TOKEN_PROGRAM_ID, @@ -41,27 +41,27 @@ fn assert_finalize_error(result: Result, expected: Instr /// Build the minimal `[BeginSettle, FinalizeSettle]` instructions that settle /// `orders` (begin) and push their proceeds (finalize). -fn finalize(program_id: &Pubkey, orders: &[FinalizedIntent]) -> Vec { +fn finalize(program_id: &Pubkey, solver: &Pubkey, orders: &[FinalizedIntent]) -> Vec { let finalize = FinalizeSettle { program_id: *program_id, begin_ix_index: BEGIN_INDEX.into(), orders, }; - build_settlement(program_id, orders, finalize) + build_settlement(program_id, solver, orders, finalize) } #[test] fn finalizes_with_no_pushes() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, _payer, solver) = setup_settle_ready(); - let instructions = finalize(&program_id, &[]); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + let instructions = finalize(&program_id, &solver.pubkey(), &[]); + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("a finalize with no pushes should succeed"); } #[test] fn pushes_a_single_order() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let mint = token::create_mint(&mut svm, &payer); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) .buy_mint(&mint) @@ -72,12 +72,13 @@ fn pushes_a_single_order() { let amount = 400; let instructions = finalize( &program_id, + &solver.pubkey(), &[FinalizedIntent { intent: &intent, amount, }], ); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("a single push should be paid"); assert_eq!(token::balance(&svm, &intent.buy_token_account), amount); @@ -86,7 +87,7 @@ fn pushes_a_single_order() { #[test] fn pushes_several_orders_from_one_buffer() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let mint = token::create_mint(&mut svm, &payer); // Distinct orders (each `OrderBuilder` makes fresh sell and buy token // accounts) sharing one buy mint, so both pushes draw from one buffer. @@ -105,6 +106,7 @@ fn pushes_several_orders_from_one_buffer() { let amount1 = 2_000; let instructions = finalize( &program_id, + &solver.pubkey(), &[ FinalizedIntent { intent: &intent0, @@ -116,7 +118,7 @@ fn pushes_several_orders_from_one_buffer() { }, ], ); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("several pushes from one buffer should be paid"); assert_eq!(token::balance(&svm, &intent0.buy_token_account), amount0); @@ -129,7 +131,7 @@ fn pushes_several_orders_from_one_buffer() { #[test] fn pushes_several_orders_from_different_buffers() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let mint0 = token::create_mint(&mut svm, &payer); let mint1 = token::create_mint(&mut svm, &payer); let intent0 = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -146,6 +148,7 @@ fn pushes_several_orders_from_different_buffers() { let amount1 = 2_000; let instructions = finalize( &program_id, + &solver.pubkey(), &[ FinalizedIntent { intent: &intent0, @@ -157,7 +160,7 @@ fn pushes_several_orders_from_different_buffers() { }, ], ); - send_metered(&mut svm, &payer, instructions, BenchLabel::Settle) + send_metered(&mut svm, &solver, instructions, BenchLabel::Settle) .expect("pushes from different buffers should be paid"); assert_eq!(token::balance(&svm, &intent0.buy_token_account), amount0); @@ -168,7 +171,7 @@ fn pushes_several_orders_from_different_buffers() { #[test] fn rejects_buy_token_account_recreated_for_another_mint() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let buy_mint = token::create_mint(&mut svm, &payer); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) .buy_mint(&buy_mint) @@ -180,27 +183,28 @@ fn rejects_buy_token_account_recreated_for_another_mint() { let instructions = finalize( &program_id, + &solver.pubkey(), &[FinalizedIntent { intent: &intent, amount: 100, }], ); assert_finalize_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::Custom(TokenError::MintMismatch as u32), ); } #[test] fn rejects_wrong_token_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, amount: 0, }]; - let mut instructions = finalize(&program_id, &orders); + let mut instructions = finalize(&program_id, &solver.pubkey(), &orders); replace_first_matching_account( &mut instructions[usize::from(FINALIZE_INDEX)], &SPL_TOKEN_PROGRAM_ID, @@ -208,21 +212,21 @@ fn rejects_wrong_token_program() { ); assert_finalize_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::IncorrectProgramId, ); } #[test] fn rejects_wrong_state_pda() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, amount: 0, }]; - let mut instructions = finalize(&program_id, &orders); + let mut instructions = finalize(&program_id, &solver.pubkey(), &orders); let (state_pda, _bump) = find_state_pda(&program_id); replace_first_matching_account( &mut instructions[usize::from(FINALIZE_INDEX)], @@ -231,14 +235,14 @@ fn rejects_wrong_state_pda() { ); assert_finalize_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), to_instruction_error(SettlementError::StateAccountMismatch), ); } #[test] fn rejects_push_account_count_mismatch() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, @@ -259,16 +263,16 @@ fn rejects_push_account_count_mismatch() { // account/data disagreement `BeginSettle` structurally can't see. finalize.data.extend_from_slice(&[0u8; 9]); - let instructions = build_settlement(&program_id, &orders, finalize); + let instructions = build_settlement(&program_id, &solver.pubkey(), &orders, finalize); assert_finalize_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), to_instruction_error(SettlementError::AccountCountNotMatchingPushCount), ); } #[test] fn rejects_too_few_accounts() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, _payer, solver) = setup_settle_ready(); // A well-formed no-push finalize... let mut finalize = Instruction::from(FinalizeSettle { @@ -282,8 +286,8 @@ fn rejects_too_few_accounts() { // its fixed accounts and raises `NotEnoughAccountKeys`. finalize.accounts.pop(); - let instructions = build_settlement(&program_id, &[], finalize); - let err = send(&mut svm, &payer, instructions) + let instructions = build_settlement(&program_id, &solver.pubkey(), &[], finalize); + let err = send(&mut svm, &solver, instructions) .expect_err("a finalize missing a fixed account must be rejected"); let TransactionError::InstructionError(FINALIZE_INDEX, ix_err) = err else { panic!("expected the finalize (index {FINALIZE_INDEX}) to fail, got {err:?}"); @@ -299,7 +303,7 @@ fn rejects_too_few_accounts() { #[test] fn rejects_invalid_buy_token_account() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderIntent { buy_token_account: unique_pubkey(), @@ -312,16 +316,16 @@ fn rejects_invalid_buy_token_account() { amount: 0, }]; - let instructions = finalize(&program_id, &orders); + let instructions = finalize(&program_id, &solver.pubkey(), &orders); assert_finalize_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::InvalidAccountData, ); } #[test] fn rejects_buy_token_account_owned_by_wrong_program() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let settlable = settlable_intent(&mut svm, &payer, payer.pubkey(), 0); let token_shaped = svm @@ -344,9 +348,9 @@ fn rejects_buy_token_account_owned_by_wrong_program() { amount: 0, }]; - let instructions = finalize(&program_id, &orders); + let instructions = finalize(&program_id, &solver.pubkey(), &orders); assert_finalize_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), InstructionError::IncorrectProgramId, ); } @@ -357,7 +361,7 @@ fn rejects_buy_token_account_owned_by_wrong_program() { // pairing rather than accounting for missing accounts. #[test] fn rejects_two_too_few_accounts() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, @@ -378,16 +382,16 @@ fn rejects_two_too_few_accounts() { // The paired `Begin` settles no orders, so it never checks the push // destinations: the inconsistency is left for the finalize's own // account-count check to reject. - let instructions = build_settlement(&program_id, &[], finalize); + let instructions = build_settlement(&program_id, &solver.pubkey(), &[], finalize); assert_finalize_error( - send(&mut svm, &payer, instructions), + send(&mut svm, &solver, instructions), to_instruction_error(SettlementError::AccountCountNotMatchingPushCount), ); } #[test] fn rejects_partial_push_amount() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, _solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer).build(); let orders = [FinalizedIntent { intent: &intent, diff --git a/programs/settlement/tests/matching_begin_finalize.rs b/programs/settlement/tests/matching_begin_finalize.rs index bce11ff..958cea0 100644 --- a/programs/settlement/tests/matching_begin_finalize.rs +++ b/programs/settlement/tests/matching_begin_finalize.rs @@ -29,7 +29,7 @@ enum AbstractInstruction { fn run_sequence( svm: &mut LiteSVM, program_id: &Pubkey, - payer: &Keypair, + solver: &Keypair, sequence: &[AbstractInstruction], ) -> Result<(), Box> { let instructions: Vec = sequence @@ -37,6 +37,7 @@ fn run_sequence( .map(|spec| match spec { AbstractInstruction::Init(idx) => BeginSettle { program_id: *program_id, + solver: solver.pubkey(), finalize_ix_index: *idx, auction_id: 0, orders: &[], @@ -52,15 +53,17 @@ fn run_sequence( // (unlike Compute Budget) Solana allows to appear multiple times // in the same transaction. AbstractInstruction::Other => { - system_instruction::transfer(&payer.pubkey(), &payer.pubkey(), 0) + system_instruction::transfer(&solver.pubkey(), &solver.pubkey(), 0) } }) .collect(); + // The solver settles and pays: it's the fee payer and the only signer any of + // these instructions needs (a `BeginSettle` names it as its solver-signer). let tx = Transaction::new_signed_with_payer( &instructions, - Some(&payer.pubkey()), - &[payer], + Some(&solver.pubkey()), + &[solver], svm.latest_blockhash(), ); svm.send_transaction(tx).map(|_| ()).map_err(Box::new) @@ -90,9 +93,9 @@ fn valid_sequences() { ], ]; - let (mut svm, program_id, payer) = common::setup(); + let (mut svm, program_id, _payer, solver) = common::setup_settle_ready(); for sequence in cases { - let result = run_sequence(&mut svm, &program_id, &payer, sequence); + let result = run_sequence(&mut svm, &program_id, &solver, sequence); assert!( result.is_ok(), "expected {sequence:?} to succeed, got {result:?}" @@ -165,9 +168,9 @@ fn invalid_sequences() { ), ]; - let (mut svm, program_id, payer) = common::setup(); + let (mut svm, program_id, _payer, solver) = common::setup_settle_ready(); for (sequence, (failing_index, expected)) in cases { - let err = run_sequence(&mut svm, &program_id, &payer, sequence) + let err = run_sequence(&mut svm, &program_id, &solver, sequence) .expect_err(&format!("expected {sequence:?} to fail, got Ok")); assert_eq!( err.err, @@ -182,17 +185,18 @@ fn invalid_sequences() { /// should reject the substitution with `UnsupportedSysvar` and revert the /// transaction. #[test] -fn rejects_non_instructions_sysvar_account_at_position_zero() { - let (mut svm, program_id, payer) = common::setup(); +fn rejects_non_instructions_sysvar_account_at_position_one() { + let (mut svm, program_id, payer, solver) = common::setup_settle_ready(); let mut begin: Instruction = BeginSettle { program_id, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, orders: &[], } .into(); - begin.accounts[0] = AccountMeta::new_readonly(payer.pubkey(), false); + begin.accounts[1] = AccountMeta::new_readonly(payer.pubkey(), false); let finalize = FinalizeSettle { program_id, begin_ix_index: 0, @@ -202,7 +206,7 @@ fn rejects_non_instructions_sysvar_account_at_position_zero() { let tx = Transaction::new_signed_with_payer( &[begin, finalize.into()], Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); let err = svm @@ -220,10 +224,11 @@ fn rejects_non_instructions_sysvar_account_at_position_zero() { /// filled with an instruction that has the same data shape as a begin/finalize /// settlement instruction but `init.get_program_id() != program_id`. fn rejects_counterpart_instruction_in_different_program() { - let (mut svm, program_id, payer) = common::setup(); + let (mut svm, program_id, payer, solver) = common::setup_settle_ready(); let begin = BeginSettle { program_id, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, orders: &[], @@ -242,7 +247,7 @@ fn rejects_counterpart_instruction_in_different_program() { let tx = Transaction::new_signed_with_payer( &instructions, Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); let err = svm @@ -278,13 +283,14 @@ fn as_cpi_call(cpi_caller_id: Pubkey, ix: impl Into) -> Instruction /// via CPI. The settlement program should reject it with `CalledViaCpi`. #[test] fn rejects_cpi_call_to_begin_settle() { - let (mut svm, settlement_id, payer) = common::setup(); + let (mut svm, settlement_id, payer, solver) = common::setup_settle_ready(); let cpi_caller_id = common::setup_cpi_caller(&mut svm); let cpi_caller_ix = as_cpi_call( cpi_caller_id, BeginSettle { program_id: settlement_id, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, orders: &[], @@ -294,7 +300,7 @@ fn rejects_cpi_call_to_begin_settle() { let tx = Transaction::new_signed_with_payer( &[cpi_caller_ix], Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); let err = svm @@ -310,7 +316,8 @@ fn rejects_cpi_call_to_begin_settle() { /// Same as `rejects_cpi_call_to_begin_settle` but for `finalize_settle`. #[test] fn rejects_cpi_call_to_finalize_settle() { - let (mut svm, settlement_id, payer) = common::setup(); + // `FinalizeSettle` isn't gated, so no solver signature is needed here. + let (mut svm, settlement_id, payer, _solver) = common::setup_settle_ready(); let cpi_caller_id = common::setup_cpi_caller(&mut svm); let cpi_caller_ix = as_cpi_call( @@ -344,10 +351,11 @@ fn rejects_cpi_call_to_finalize_settle() { /// valid discriminator, and `Other` belongs to a different program. #[test] fn rejects_counterpart_with_unrecoverable_discriminator() { - let (mut svm, program_id, payer) = common::setup(); + let (mut svm, program_id, payer, solver) = common::setup_settle_ready(); let begin = BeginSettle { program_id, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, orders: &[], @@ -363,7 +371,7 @@ fn rejects_counterpart_with_unrecoverable_discriminator() { let tx = Transaction::new_signed_with_payer( &[begin.into(), malformed], Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); let err = svm @@ -385,10 +393,11 @@ fn rejects_counterpart_with_unrecoverable_discriminator() { /// outside what the abstract harness can produce. #[test] fn rejects_counterpart_with_unrecoverable_counterpart_index() { - let (mut svm, program_id, payer) = common::setup(); + let (mut svm, program_id, payer, solver) = common::setup_settle_ready(); let begin = BeginSettle { program_id, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, orders: &[], @@ -405,7 +414,7 @@ fn rejects_counterpart_with_unrecoverable_counterpart_index() { let tx = Transaction::new_signed_with_payer( &[begin.into(), malformed], Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); let err = svm diff --git a/programs/settlement/tests/program_deployment.rs b/programs/settlement/tests/program_deployment.rs index 9c2ed66..bbd3a8d 100644 --- a/programs/settlement/tests/program_deployment.rs +++ b/programs/settlement/tests/program_deployment.rs @@ -23,7 +23,7 @@ fn program_is_deployed_with_code() { #[test] fn program_can_be_invoked() { - let (mut svm, program_id, payer) = common::setup(); + let (mut svm, program_id, payer, solver) = common::setup_settle_ready(); let tx = Transaction::new_signed_with_payer( // Indices encode the BeginSettle/FinalizeSettle pair @@ -31,6 +31,7 @@ fn program_can_be_invoked() { &[ BeginSettle { program_id, + solver: solver.pubkey(), finalize_ix_index: 1, auction_id: 0, orders: &[], @@ -44,7 +45,7 @@ fn program_can_be_invoked() { .into(), ], Some(&payer.pubkey()), - &[&payer], + &[&payer, &solver], svm.latest_blockhash(), ); diff --git a/programs/settlement/tests/settle_limit_prices.rs b/programs/settlement/tests/settle_limit_prices.rs index e29d36e..bda5827 100644 --- a/programs/settlement/tests/settle_limit_prices.rs +++ b/programs/settlement/tests/settle_limit_prices.rs @@ -10,7 +10,7 @@ use crate::common::{ assert_instruction_error_at, buffer, order::OrderBuilder, settlement::{BEGIN_INDEX, FINALIZE_INDEX}, - setup, to_instruction_error, token, unique_pubkey, + setup_settle_ready, to_instruction_error, token, unique_pubkey, }; use cow_settlement_client::cow_settlement_interface::{ data::intent::{OrderIntent, OrderKind}, @@ -61,6 +61,7 @@ fn settle( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, + solver: &Keypair, intent: &OrderIntent, amount_in: u64, amount_out: u64, @@ -69,6 +70,7 @@ fn settle( svm, program_id, payer, + solver, &[(intent, &[amount_in], amount_out)], ) } @@ -78,6 +80,7 @@ fn settle_all( svm: &mut LiteSVM, program_id: &Pubkey, payer: &Keypair, + solver: &Keypair, orders: &[(&OrderIntent, &[u64], u64)], ) -> Result<(), TransactionError> { let mut initialized: Vec = vec![]; @@ -118,6 +121,7 @@ fn settle_all( let begin = BeginSettle { program_id: *program_id, + solver: solver.pubkey(), finalize_ix_index: FINALIZE_INDEX.into(), auction_id: 0, orders: &initialized, @@ -127,10 +131,13 @@ fn settle_all( begin_ix_index: BEGIN_INDEX.into(), orders: &finalized, }; + // The solver settles and pays: it's the fee payer and the only signer the + // pair needs (`BeginSettle` names it as its solver-signer). `payer` above + // only funds the order/buffer setup. let tx = Transaction::new_signed_with_payer( &[begin.into(), finalize.into()], - Some(&payer.pubkey()), - &[payer], + Some(&solver.pubkey()), + &[solver], svm.latest_blockhash(), ); svm.send_transaction(tx).map(|_| ()).map_err(|e| e.err) @@ -140,7 +147,7 @@ fn settle_all( #[test] fn sell_order_succeeds_at_limit_price() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Sell up to 1_200_000 for at least 3_600_000: a 3:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -149,14 +156,22 @@ fn sell_order_succeeds_at_limit_price() { .build(); // Sell 400_000, receive exactly the proportional minimum 1_200_000. - settle(&mut svm, &program_id, &payer, &intent, 400_000, 1_200_000) - .expect("a price exactly at the limit should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 400_000, + 1_200_000, + ) + .expect("a price exactly at the limit should be accepted"); assert_eq!(token::balance(&svm, &intent.buy_token_account), 1_200_000); } #[test] fn sell_order_below_limit_price_is_rejected() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Sell up to 900_000 for at least 1_800_000: a 2:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -167,14 +182,22 @@ fn sell_order_below_limit_price_is_rejected() { // One token short of the proportional minimum (600_000) for the pull. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 300_000, 599_999), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 300_000, + 599_999, + ), SettlementError::LimitPriceViolated, ); } #[test] fn sell_order_above_limit_price_succeeds() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Sell up to 500_000 for at least 2_500_000: a 5:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -183,14 +206,22 @@ fn sell_order_above_limit_price_succeeds() { .build(); // A better-than-limit price (more buy than the 500_000 minimum) is fine. - settle(&mut svm, &program_id, &payer, &intent, 100_000, 750_000) - .expect("a better-than-limit price should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 100_000, + 750_000, + ) + .expect("a better-than-limit price should be accepted"); assert_eq!(token::balance(&svm, &intent.buy_token_account), 750_000); } #[test] fn buy_order_succeeds_at_limit_price() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Buy up to 800_000, spending at most 2_400_000: a 3:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -200,14 +231,22 @@ fn buy_order_succeeds_at_limit_price() { .build(); // Buy 400_000 for exactly the proportional maximum spend of 1_200_000. - settle(&mut svm, &program_id, &payer, &intent, 1_200_000, 400_000) - .expect("a price exactly at the limit should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 1_200_000, + 400_000, + ) + .expect("a price exactly at the limit should be accepted"); assert_eq!(token::balance(&svm, &intent.buy_token_account), 400_000); } #[test] fn buy_order_above_limit_price_succeeds() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Buy up to 800_000, spending at most 2_400_000: a 3:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -217,14 +256,22 @@ fn buy_order_above_limit_price_succeeds() { .build(); // A better-than-limit price (less spent than the 1_200_000 maximum) is fine. - settle(&mut svm, &program_id, &payer, &intent, 900_000, 400_000) - .expect("a better-than-limit price should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 900_000, + 400_000, + ) + .expect("a better-than-limit price should be accepted"); assert_eq!(token::balance(&svm, &intent.buy_token_account), 400_000); } #[test] fn buy_order_below_limit_price_is_rejected() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Buy up to 500_000, spending at most 2_000_000: a 4:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -236,7 +283,15 @@ fn buy_order_below_limit_price_is_rejected() { // One token more than the proportional maximum spend (800_000) for the buy. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 800_001, 200_000), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 800_001, + 200_000, + ), SettlementError::LimitPriceViolated, ); } @@ -251,7 +306,7 @@ fn assert_locality_rejected(generous_kind: OrderKind, violating_kind: OrderKind) const SELL_AMOUNT: u64 = 900_000; const BUY_AMOUNT: u64 = 1_800_000; - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let sell_mint = token::create_mint(&mut svm, &payer); let buy_mint = token::create_mint(&mut svm, &payer); @@ -274,6 +329,7 @@ fn assert_locality_rejected(generous_kind: OrderKind, violating_kind: OrderKind) &mut svm, &program_id, &payer, + &solver, &[ // Far above the 2:1 limit. (&generous, &[250_000], 1_000_000), @@ -306,7 +362,7 @@ fn settlement_rejected_when_one_order_below_limit() { #[test] fn multiple_pulls_clearing_the_limit_are_accepted() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Sell up to 1_000_000 for at least 2_000_000: a 2:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -320,6 +376,7 @@ fn multiple_pulls_clearing_the_limit_are_accepted() { &mut svm, &program_id, &payer, + &solver, &[(&intent, &[300_000, 200_000, 100_000], 1_200_000)], ) .expect("a payment clearing the limit for the summed pull should be accepted"); @@ -328,7 +385,7 @@ fn multiple_pulls_clearing_the_limit_are_accepted() { #[test] fn multiple_pulls_below_the_limit_are_rejected() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Sell up to 1_000_000 for at least 2_000_000: a 2:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -344,6 +401,7 @@ fn multiple_pulls_below_the_limit_are_rejected() { &mut svm, &program_id, &payer, + &solver, &[(&intent, &[300_000, 200_000, 100_000], 1_199_999)], ), SettlementError::LimitPriceViolated, @@ -354,7 +412,7 @@ fn multiple_pulls_below_the_limit_are_rejected() { #[test] fn sell_order_cannot_exceed_its_sell_amount() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) .kind(OrderKind::Sell) @@ -366,14 +424,22 @@ fn sell_order_cannot_exceed_its_sell_amount() { // so only the sell cap can reject it. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 1_500_000, 3_000_000), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 1_500_000, + 3_000_000, + ), SettlementError::FillExceedsOrderAmount, ); } #[test] fn buy_order_cannot_exceed_its_buy_amount() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Buy up to 1_000_000, spending at most 2_000_000. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -385,14 +451,22 @@ fn buy_order_cannot_exceed_its_buy_amount() { // Receive 1_500_000 > the 1_000_000 buy amount, spending within the limit. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 1_000_000, 1_500_000), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 1_000_000, + 1_500_000, + ), SettlementError::FillExceedsOrderAmount, ); } #[test] fn fill_or_kill_order_must_be_filled_completely() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) .kind(OrderKind::Sell) @@ -404,14 +478,22 @@ fn fill_or_kill_order_must_be_filled_completely() { // Selling only half a fill-or-kill order isn't allowed, even at the limit. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 500_000, 1_000_000), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 500_000, + 1_000_000, + ), SettlementError::OrderNotExactlyFilled, ); } #[test] fn partially_fillable_order_fills_across_settlements() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Sell up to 1_000_000 for at least 2_000_000: a 2:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -422,13 +504,29 @@ fn partially_fillable_order_fills_across_settlements() { .build(); // Two settlements that together fill the order exactly, each at the limit. - settle(&mut svm, &program_id, &payer, &intent, 400_000, 800_000) - .expect("the first partial fill should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 400_000, + 800_000, + ) + .expect("the first partial fill should be accepted"); // The first fill's totals are persisted to the order PDA. assert_eq!(order_fill(&svm, &program_id, &intent), (400_000, 800_000)); - settle(&mut svm, &program_id, &payer, &intent, 600_000, 1_200_000) - .expect("a second fill completing the order should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 600_000, + 1_200_000, + ) + .expect("a second fill completing the order should be accepted"); // The second fill accumulates onto the first. assert_eq!( order_fill(&svm, &program_id, &intent), @@ -441,7 +539,7 @@ fn partially_fillable_order_fills_across_settlements() { #[test] fn order_cannot_be_overfilled_across_settlements() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) .kind(OrderKind::Sell) @@ -451,20 +549,36 @@ fn order_cannot_be_overfilled_across_settlements() { .build(); // Fill 600_000 first; the order PDA now records 600_000 withdrawn. - settle(&mut svm, &program_id, &payer, &intent, 600_000, 1_200_000) - .expect("the first partial fill should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 600_000, + 1_200_000, + ) + .expect("the first partial fill should be accepted"); // A second 500_000 pull would take 1_100_000 in total, past the sell amount. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 500_000, 1_000_000), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 500_000, + 1_000_000, + ), SettlementError::FillExceedsOrderAmount, ); } #[test] fn buy_order_cannot_be_overfilled_across_settlements() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // Buy up to 1_000_000, spending at most 2_000_000: a 2:1 limit. let intent = OrderBuilder::new(&mut svm, &program_id, &payer) @@ -476,21 +590,37 @@ fn buy_order_cannot_be_overfilled_across_settlements() { // Buy 600_000 first (spending 1_200_000 at the limit); the order PDA now // records 600_000 received. - settle(&mut svm, &program_id, &payer, &intent, 1_200_000, 600_000) - .expect("the first partial buy should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 1_200_000, + 600_000, + ) + .expect("the first partial buy should be accepted"); // Buying another 500_000 would total 1_100_000, past the buy amount, even // though this settlement's spend stays within the limit price. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 1_000_000, 500_000), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 1_000_000, + 500_000, + ), SettlementError::FillExceedsOrderAmount, ); } #[test] fn fill_or_kill_order_cannot_be_settled_twice() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) .kind(OrderKind::Sell) @@ -500,8 +630,16 @@ fn fill_or_kill_order_cannot_be_settled_twice() { .build(); // A full fill-or-kill fill is accepted once. - settle(&mut svm, &program_id, &payer, &intent, 1_000_000, 2_000_000) - .expect("a full fill-or-kill fill should be accepted"); + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 1_000_000, + 2_000_000, + ) + .expect("a full fill-or-kill fill should be accepted"); // Advance the blockhash so the identical second attempt (same amounts to the // same accounts) isn't rejected as a duplicate transaction before it runs. @@ -512,14 +650,22 @@ fn fill_or_kill_order_cannot_be_settled_twice() { // amount, so it's rejected as not exactly filled. assert_settlement_error( BEGIN_INDEX, - settle(&mut svm, &program_id, &payer, &intent, 1_000_000, 2_000_000), + settle( + &mut svm, + &program_id, + &payer, + &solver, + &intent, + 1_000_000, + 2_000_000, + ), SettlementError::OrderNotExactlyFilled, ); } #[test] fn settlement_rejected_when_one_order_exceeds_its_amount() { - let (mut svm, program_id, payer) = setup(); + let (mut svm, program_id, payer, solver) = setup_settle_ready(); // The cap is per order: one order settled within its amount can't excuse // another pulled past its sell amount in the same settlement. @@ -542,6 +688,7 @@ fn settlement_rejected_when_one_order_exceeds_its_amount() { &mut svm, &program_id, &payer, + &solver, &[ // Within its amount and at the limit. (&ok, &[400_000], 800_000), diff --git a/programs/settlement/tests/settle_solver_auth.rs b/programs/settlement/tests/settle_solver_auth.rs new file mode 100644 index 0000000..82e8742 --- /dev/null +++ b/programs/settlement/tests/settle_solver_auth.rs @@ -0,0 +1,124 @@ +//! Integration tests for the solver gate on settling: `BeginSettle` and +//! `FinalizeSettle` run only for a solver that both signs the transaction and is +//! in the state PDA's solver list. The gate is checked first, so an +//! 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 solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; + +use crate::common::{ + assert_instruction_error_at, register_solver, + settlement::{BEGIN_INDEX, FINALIZE_INDEX}, + setup_init, to_instruction_error, unique_keypair, +}; + +mod common; + +/// A minimal no-op settlement: a `[BeginSettle, FinalizeSettle]` pair with no +/// orders, naming `solver` as `BeginSettle`'s settling solver. It moves no funds, +/// so it isolates the solver gate from the rest of settlement. +fn noop_settlement(program_id: &Pubkey, solver: &Pubkey) -> Vec { + let begin = BeginSettle { + program_id: *program_id, + solver: *solver, + finalize_ix_index: FINALIZE_INDEX.into(), + auction_id: 0, + orders: &[], + }; + let finalize = FinalizeSettle { + program_id: *program_id, + begin_ix_index: BEGIN_INDEX.into(), + orders: &[], + }; + vec![begin.into(), finalize.into()] +} + +#[test] +fn registered_signing_solver_may_settle() { + let (mut svm, params) = setup_init(); + let solver = unique_keypair(); + register_solver(&mut svm, ¶ms, &solver.pubkey()); + + let tx = Transaction::new_signed_with_payer( + &noop_settlement(¶ms.program_id, &solver.pubkey()), + Some(¶ms.payer.pubkey()), + &[¶ms.payer, &solver], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .expect("a registered, signing solver may settle"); +} + +#[test] +fn registered_signing_solver_may_settle_and_pay_for_tx_fee_itself() { + let (mut svm, params) = setup_init(); + let solver = unique_keypair(); + register_solver(&mut svm, ¶ms, &solver.pubkey()); + // The solver is the fee payer here, so it needs a balance of its own. + svm.airdrop(&solver.pubkey(), 1_000_000_000) + .expect("airdrop to solver should succeed"); + + let tx = Transaction::new_signed_with_payer( + &noop_settlement(¶ms.program_id, &solver.pubkey()), + Some(&solver.pubkey()), + &[&solver], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .expect("a registered, signing solver may settle and pay its own fee"); +} + +#[test] +fn unregistered_solver_may_not_settle() { + let (mut svm, params) = setup_init(); + // A solver that signs but was never added to the list. + let solver = unique_keypair(); + + let tx = Transaction::new_signed_with_payer( + &noop_settlement(¶ms.program_id, &solver.pubkey()), + Some(¶ms.payer.pubkey()), + &[¶ms.payer, &solver], + svm.latest_blockhash(), + ); + assert_instruction_error_at( + BEGIN_INDEX, + svm.send_transaction(tx).map(|_| ()).map_err(|e| e.err), + to_instruction_error(SettlementError::UnauthorizedSolver), + ); +} + +#[test] +fn non_signing_solver_may_not_settle() { + let (mut svm, params) = setup_init(); + let solver = unique_keypair(); + register_solver(&mut svm, ¶ms, &solver.pubkey()); + + let mut begin_settle: Instruction = BeginSettle { + program_id: params.program_id, + solver: solver.pubkey(), + finalize_ix_index: 0, + auction_id: 0, + orders: &[], + } + .into(); + + let solver_index = 0; + assert!( + begin_settle.accounts[solver_index].is_signer + && begin_settle.accounts[solver_index].pubkey == solver.pubkey(), + "sanity check: the solver account should start as a signer" + ); + begin_settle.accounts[solver_index].is_signer = false; + let tx = Transaction::new_signed_with_payer( + &[begin_settle], + Some(¶ms.payer.pubkey()), + &[¶ms.payer], + svm.latest_blockhash(), + ); + assert_instruction_error_at( + BEGIN_INDEX, + svm.send_transaction(tx).map(|_| ()).map_err(|e| e.err), + to_instruction_error(SettlementError::UnauthorizedSolver), + ); +} diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 18e9cf7..2d7bb5a 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -108,8 +108,12 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { .and_then(|begin| Some((begin, begin.checked_add(1)?))) .context("too many instructions: begin/finalize index overflow")?; + // `BeginSettle` is gated on a registered solver that signs. The fee payer + // is assumed to be a solver. + let solver = ctx.payer.pubkey(); let begin_ix = BeginSettle { program_id: ctx.program_id, + solver, finalize_ix_index, orders: &initialized_intents, auction_id: 0,