Skip to main content

cow_settlement_interface/instruction/
remove_solver.rs

1//! `RemoveSolver` instruction builder and parser.
2//!
3//! It removes a solver from the sorted solver list stored in the state PDA (see
4//! [`crate::data::state`]). Only the manager may authorize it. The state PDA
5//! shrinks by one solver and the freed rent is paid to `rent_recipient`.
6
7use core::mem::size_of;
8
9use solana_instruction::{AccountMeta, Instruction};
10use solana_program_error::ProgramError;
11use solana_pubkey::Pubkey;
12
13use crate::instruction::InstructionInputParsing;
14use crate::SettlementInstruction;
15
16/// Builder for a `RemoveSolver` instruction.
17///
18/// `manager` authorizes the change and must be the state PDA's current manager;
19/// it signs but doesn't receive anything. `rent_recipient` receives the freed
20/// rent. `solver` is removed from the sorted solver list; removing one that
21/// isn't present fails.
22///
23/// Wire format: `[discriminator=9, solver (32 bytes)]`.
24/// Required accounts: `[manager (S), rent_recipient (W), state_pda (W)]`.
25pub struct RemoveSolver {
26    pub program_id: Pubkey,
27    pub manager: Pubkey,
28    pub rent_recipient: Pubkey,
29    pub state_pda: Pubkey,
30    pub solver: Pubkey,
31}
32
33impl From<RemoveSolver> for Instruction {
34    fn from(builder: RemoveSolver) -> Self {
35        let mut data = vec![SettlementInstruction::RemoveSolver.discriminator()];
36        data.extend_from_slice(&builder.solver.to_bytes());
37        Instruction {
38            program_id: builder.program_id,
39            accounts: vec![
40                AccountMeta::new_readonly(builder.manager, true),
41                AccountMeta::new(builder.rent_recipient, false),
42                AccountMeta::new(builder.state_pda, false),
43            ],
44            data,
45        }
46    }
47}
48
49/// Parsed inputs of a `RemoveSolver` instruction.
50pub struct RemoveSolverInput<'a, A> {
51    pub manager: &'a A,
52    pub rent_recipient: &'a A,
53    pub state_pda: &'a A,
54    pub solver: Pubkey,
55}
56
57impl<'a, A> InstructionInputParsing<'a, A> for RemoveSolverInput<'a, A> {
58    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::RemoveSolver;
59
60    fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
61        let solver: &[u8; size_of::<Pubkey>()] = instruction_data
62            .try_into()
63            .map_err(|_| ProgramError::InvalidInstructionData)?;
64        let solver = Pubkey::new_from_array(*solver);
65
66        // Accounts: [manager (S), rent_recipient (W), state_pda (W)].
67        let [manager, rent_recipient, state_pda, ..] = accounts else {
68            return Err(ProgramError::NotEnoughAccountKeys);
69        };
70
71        Ok(Self {
72            manager,
73            rent_recipient,
74            state_pda,
75            solver,
76        })
77    }
78}
79
80/// Test scaffolding for `RemoveSolver` parsing and handling, shared by this
81/// crate's tests and the settlement program's via the `test-fixtures` feature.
82#[cfg(any(test, feature = "test-fixtures"))]
83pub mod fixtures {
84    use solana_address::Address;
85
86    use super::{Instruction, RemoveSolver};
87
88    /// Number of accounts `RemoveSolver` expects: manager, rent recipient, and
89    /// state PDA.
90    pub const NUM_ACCOUNTS: usize = 3;
91
92    /// `RemoveSolver` instruction data with placeholder addresses, for failure
93    /// cases where the actual addresses don't matter.
94    pub fn remove_solver_data() -> Vec<u8> {
95        let zero = Address::new_from_array([0; 32]);
96        Instruction::from(RemoveSolver {
97            program_id: zero,
98            manager: zero,
99            rent_recipient: zero,
100            state_pda: zero,
101            solver: zero,
102        })
103        .data
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::fixtures::{remove_solver_data, NUM_ACCOUNTS};
110    use super::*;
111    use crate::fixtures::pubkey_from_seed;
112    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
113    use crate::instruction::tests::{assert_readonly_signer, assert_writable_nonsigner};
114    use solana_account_view::AccountView;
115
116    #[test]
117    fn remove_solver_input_parses_valid_input() {
118        let program_id = pubkey_from_seed("program id");
119        let manager = fake_account(pubkey_from_seed("manager"));
120        let rent_recipient = fake_account(pubkey_from_seed("rent recipient"));
121        let state_pda = fake_account(pubkey_from_seed("state pda"));
122        let solver = pubkey_from_seed("solver");
123
124        let data = Instruction::from(RemoveSolver {
125            program_id,
126            manager: *manager.address(),
127            rent_recipient: *rent_recipient.address(),
128            state_pda: *state_pda.address(),
129            solver,
130        })
131        .data;
132        let accounts = [manager, rent_recipient, state_pda];
133
134        let RemoveSolverInput {
135            manager: parsed_manager,
136            rent_recipient: parsed_rent_recipient,
137            state_pda: parsed_state_pda,
138            solver: parsed_solver,
139        } = RemoveSolverInput::parse(&data, &accounts).expect("parse should succeed");
140
141        assert_eq!(parsed_manager.address(), accounts[0].address());
142        assert_eq!(parsed_rent_recipient.address(), accounts[1].address());
143        assert_eq!(parsed_state_pda.address(), accounts[2].address());
144        assert_eq!(parsed_solver, solver);
145    }
146
147    #[test]
148    fn remove_solver_input_rejects_long_data() {
149        let mut data = remove_solver_data();
150        data.push(0); // trailing byte
151        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
152        assert_eq!(
153            RemoveSolverInput::parse(&data, &accounts).err(),
154            Some(ProgramError::InvalidInstructionData),
155        );
156    }
157
158    #[test]
159    fn remove_solver_input_rejects_short_data() {
160        let mut data = remove_solver_data();
161        data.pop(); // one byte short
162        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
163        assert_eq!(
164            RemoveSolverInput::parse(&data, &accounts).err(),
165            Some(ProgramError::InvalidInstructionData),
166        );
167    }
168
169    #[test]
170    fn remove_solver_input_rejects_missing_accounts() {
171        let data = remove_solver_data();
172        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
173        accounts.pop();
174        assert_eq!(
175            RemoveSolverInput::parse(&data, &accounts).err(),
176            Some(ProgramError::NotEnoughAccountKeys),
177        );
178    }
179
180    #[test]
181    fn instruction_data_has_expected_layout() {
182        let solver = pubkey_from_seed("solver");
183        let Instruction { data, .. } = RemoveSolver {
184            program_id: pubkey_from_seed("program id"),
185            manager: pubkey_from_seed("manager"),
186            rent_recipient: pubkey_from_seed("rent recipient"),
187            state_pda: pubkey_from_seed("state pda"),
188            solver,
189        }
190        .into();
191
192        assert_eq!(data.len(), 1 + size_of::<Pubkey>());
193        assert_eq!(data[0], SettlementInstruction::RemoveSolver.discriminator());
194        assert_eq!(&data[1..], &solver.to_bytes());
195    }
196
197    #[test]
198    fn instruction_data_regression() {
199        let solver = Pubkey::new_from_array([0x11; 32]);
200        let Instruction { data, .. } = RemoveSolver {
201            program_id: pubkey_from_seed("program id"),
202            manager: pubkey_from_seed("manager"),
203            rent_recipient: pubkey_from_seed("rent recipient"),
204            state_pda: pubkey_from_seed("state pda"),
205            solver,
206        }
207        .into();
208
209        #[rustfmt::skip]
210        let expected: [u8; 1 + size_of::<Pubkey>()] = [
211            // discriminator (RemoveSolver = 9)
212            0x09,
213            // solver
214            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
215            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
216            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
217            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
218        ];
219        assert_eq!(data, expected);
220    }
221
222    #[test]
223    fn instruction_has_expected_accounts() {
224        let manager = pubkey_from_seed("manager");
225        let rent_recipient = pubkey_from_seed("rent recipient");
226        let state_pda = pubkey_from_seed("state pda");
227        let Instruction { accounts, .. } = RemoveSolver {
228            program_id: pubkey_from_seed("program id"),
229            manager,
230            rent_recipient,
231            state_pda,
232            solver: pubkey_from_seed("solver"),
233        }
234        .into();
235
236        assert_eq!(accounts.len(), 3);
237        // The manager authorizes the change; the freed rent is paid to the
238        // recipient; the state PDA is shrunk and written.
239        assert_readonly_signer(&accounts[0], manager);
240        assert_writable_nonsigner(&accounts[1], rent_recipient);
241        assert_writable_nonsigner(&accounts[2], state_pda);
242    }
243}