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