Skip to main content

cow_settlement_interface/instruction/
reclaim_buffer.rs

1//! `ReclaimBuffer` instruction builder.
2//!
3//! Closes one or more buffer PDAs (see [`crate::pda::buffer`]) and sends each
4//! closed buffer's rent lamports to a `reclaim_recipient` of the caller's
5//! choosing. Only the settlement's configured `reclaim_authority` (see
6//! [`crate::data::state::StateAccount`]) may authorize this.
7//!
8//! A buffer that still holds tokens is skipped, not closed.
9//!
10//! The token_program supplied to this instruction must be the owner of all mints
11//! supplied. Only one token program can be supplied to this instruction at a time.
12//! If mints from two separate token programs are required, the client needs to
13//! divide it into separate instructions.
14//!
15//! Wire format: `[discriminator=6]`, 1 byte.
16//! Required accounts:
17//! `[state_pda (R), reclaim_authority (R,S), reclaim_recipient (W), token_program (R), (buffer_pda (W), mint (R))...]`.
18
19use solana_instruction::{AccountMeta, Instruction};
20use solana_program_error::ProgramError;
21use solana_pubkey::Pubkey;
22
23use super::InstructionInputParsing;
24use crate::SettlementInstruction;
25
26/// Builder for a `ReclaimBuffer` instruction that closes one buffer per
27/// `(buffer_pda, mint)` pair in `buffers`.
28///
29/// `state_pda` must be [`crate::pda::state::STATE_PDA`]. `reclaim_authority`
30/// must sign and must match the `reclaim_authority` recorded in the state PDA's data.
31/// `reclaim_recipient` is the account receiving the closed buffer's lamports.
32/// Each `buffer_pda` must be the canonical PDA returned by
33/// [`crate::pda::buffer::find_buffer_pda`] for its paired `mint`, which is
34/// passed only so that derivation can be checked on-chain.
35///
36/// Buffers that still hold a token balance are skipped without failing the
37/// instruction.
38pub struct ReclaimBuffer<'a> {
39    pub program_id: Pubkey,
40    pub state_pda: Pubkey,
41    pub reclaim_authority: Pubkey,
42    pub reclaim_recipient: Pubkey,
43    /// The token program owning every buffer this instruction closes. Must be
44    /// the address of a [`crate::token_program::TokenProgram`].
45    pub token_program: Pubkey,
46    pub buffers: &'a [(Pubkey, Pubkey)],
47}
48
49impl From<ReclaimBuffer<'_>> for Instruction {
50    fn from(builder: ReclaimBuffer<'_>) -> Self {
51        let mut accounts = vec![
52            AccountMeta::new_readonly(builder.state_pda, false),
53            AccountMeta::new_readonly(builder.reclaim_authority, true),
54            AccountMeta::new(builder.reclaim_recipient, false),
55            AccountMeta::new_readonly(builder.token_program, false),
56        ];
57        for (buffer_pda, mint) in builder.buffers {
58            accounts.push(AccountMeta::new(*buffer_pda, false));
59            accounts.push(AccountMeta::new_readonly(*mint, false));
60        }
61        Instruction {
62            program_id: builder.program_id,
63            accounts,
64            data: vec![SettlementInstruction::ReclaimBuffer.discriminator()],
65        }
66    }
67}
68
69/// Parsed inputs of a `ReclaimBuffer` instruction.
70pub struct ReclaimBufferInput<'a, A> {
71    pub state_pda: &'a A,
72    pub reclaim_authority: &'a A,
73    pub reclaim_recipient: &'a A,
74    /// One `[buffer_pda, mint]` pair per buffer to close.
75    pub buffers: &'a [[A; 2]],
76}
77
78impl<'a, A> InstructionInputParsing<'a, A> for ReclaimBufferInput<'a, A> {
79    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::ReclaimBuffer;
80
81    fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
82        if !instruction_data.is_empty() {
83            return Err(ProgramError::InvalidInstructionData);
84        }
85        // Accounts: [state_pda (R), reclaim_authority (R,S), reclaim_recipient
86        // (W), token_program (R), (buffer_pda (W), mint (R))...]. The four
87        // shared accounts come first; the per-buffer pairs follow, one pair per
88        // buffer. The token program is skipped rather than read: each buffer is
89        // closed by the program that owns it, so the account is only there to
90        // put that program in the transaction.
91        let [state_pda, reclaim_authority, reclaim_recipient, _token_program, rest @ ..] = accounts
92        else {
93            return Err(ProgramError::NotEnoughAccountKeys);
94        };
95        // Group the trailing accounts into `[buffer_pda, mint]` pairs. Each
96        // buffer needs both, so a stray leftover account is a malformed
97        // instruction. There must be at least one pair: an instruction that
98        // reclaims no buffers is rejected as a likely encoding issue.
99        let (buffers, remainder) = rest.as_chunks::<2>();
100        if !remainder.is_empty() || buffers.is_empty() {
101            return Err(ProgramError::NotEnoughAccountKeys);
102        }
103
104        Ok(Self {
105            state_pda,
106            reclaim_authority,
107            reclaim_recipient,
108            buffers,
109        })
110    }
111}
112
113/// Test scaffolding for `ReclaimBuffer` parsing and handling, shared by this
114/// crate's tests and the settlement program's via the `test-fixtures` feature.
115#[cfg(any(test, feature = "test-fixtures"))]
116pub mod fixtures {
117    use solana_address::Address;
118
119    use super::{Instruction, ReclaimBuffer};
120
121    /// Number of accounts that don't depend on the number of buffers
122    /// reclaimed: state PDA, reclaim authority, reclaim recipient, and token
123    /// program.
124    pub const NUM_SHARED_ACCOUNTS: usize = 4;
125
126    /// `ReclaimBuffer` instruction data with placeholder addresses, for
127    /// failure cases where the input is irrelevant.
128    pub fn reclaim_buffer_data() -> Vec<u8> {
129        let zero = Address::new_from_array([0; 32]);
130        Instruction::from(ReclaimBuffer {
131            program_id: zero,
132            state_pda: zero,
133            reclaim_authority: zero,
134            reclaim_recipient: zero,
135            token_program: zero,
136            buffers: &[(zero, zero)],
137        })
138        .data
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::fixtures::{reclaim_buffer_data, NUM_SHARED_ACCOUNTS};
145    use super::*;
146    use crate::fixtures::pubkey_from_seed;
147    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
148    use crate::instruction::tests::{
149        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
150    };
151
152    #[test]
153    fn reclaim_buffer_input_parses_valid_input() {
154        let program_id = pubkey_from_seed("program id");
155        let state_pda = pubkey_from_seed("state pda");
156        let reclaim_authority = pubkey_from_seed("reclaim authority");
157        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
158        let token_program = pubkey_from_seed("token program");
159        let buffer_pda = pubkey_from_seed("buffer pda");
160        let mint = pubkey_from_seed("mint");
161
162        let data = Instruction::from(ReclaimBuffer {
163            program_id,
164            state_pda,
165            reclaim_authority,
166            reclaim_recipient,
167            token_program,
168            buffers: &[(buffer_pda, mint)],
169        })
170        .data;
171        let token_program = pubkey_from_seed("token program");
172        let accounts = [
173            fake_account(state_pda),
174            fake_account(reclaim_authority),
175            fake_account(reclaim_recipient),
176            fake_account(token_program),
177            fake_account(buffer_pda),
178            fake_account(mint),
179        ];
180
181        let ReclaimBufferInput {
182            state_pda: parsed_state_pda,
183            reclaim_authority: parsed_reclaim_authority,
184            reclaim_recipient: parsed_reclaim_recipient,
185            buffers,
186        } = ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
187
188        assert_eq!(*parsed_state_pda.address(), state_pda);
189        assert_eq!(*parsed_reclaim_authority.address(), reclaim_authority);
190        assert_eq!(*parsed_reclaim_recipient.address(), reclaim_recipient);
191        assert_eq!(buffers.len(), 1, "one buffer is one pair");
192        assert_eq!(*buffers[0][0].address(), buffer_pda);
193        assert_eq!(*buffers[0][1].address(), mint);
194    }
195
196    #[test]
197    fn reclaim_buffer_input_parses_multiple_buffers() {
198        let program_id = pubkey_from_seed("program id");
199        let state_pda = pubkey_from_seed("state pda");
200        let reclaim_authority = pubkey_from_seed("reclaim authority");
201        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
202        let token_program = pubkey_from_seed("token program");
203        let buffer_a = pubkey_from_seed("buffer a");
204        let mint_a = pubkey_from_seed("mint a");
205        let buffer_b = pubkey_from_seed("buffer b");
206        let mint_b = pubkey_from_seed("mint b");
207
208        let data = Instruction::from(ReclaimBuffer {
209            program_id,
210            state_pda,
211            token_program,
212            reclaim_authority,
213            reclaim_recipient,
214            buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
215        })
216        .data;
217        let accounts = [
218            fake_account(state_pda),
219            fake_account(reclaim_authority),
220            fake_account(reclaim_recipient),
221            fake_account(token_program),
222            fake_account(buffer_a),
223            fake_account(mint_a),
224            fake_account(buffer_b),
225            fake_account(mint_b),
226        ];
227
228        let ReclaimBufferInput { buffers, .. } =
229            ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
230
231        assert_eq!(buffers.len(), 2, "two buffers are two pairs");
232        assert_eq!(
233            buffers[0].each_ref().map(|a| *a.address()),
234            [buffer_a, mint_a]
235        );
236        assert_eq!(
237            buffers[1].each_ref().map(|a| *a.address()),
238            [buffer_b, mint_b]
239        );
240    }
241
242    #[test]
243    fn reclaim_buffer_input_rejects_zero_buffers() {
244        let data = reclaim_buffer_data();
245        // Only the four shared accounts, no buffer pairs.
246        let accounts = fake_sequential_accounts::<NUM_SHARED_ACCOUNTS>();
247        assert_eq!(
248            ReclaimBufferInput::parse(&data, &accounts).err(),
249            Some(ProgramError::NotEnoughAccountKeys),
250            "an instruction that reclaims no buffers is rejected",
251        );
252    }
253
254    #[test]
255    fn reclaim_buffer_input_rejects_long_data() {
256        let mut data = reclaim_buffer_data();
257        data.push(0); // trailing byte
258        assert_eq!(
259            ReclaimBufferInput::parse(&data, &[0]).err(),
260            Some(ProgramError::InvalidInstructionData),
261        );
262    }
263
264    #[test]
265    fn reclaim_buffer_input_rejects_missing_accounts() {
266        let data = reclaim_buffer_data();
267        // Fewer than the four shared accounts.
268        let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS - 1 }>();
269        assert_eq!(
270            ReclaimBufferInput::parse(&data, &accounts).err(),
271            Some(ProgramError::NotEnoughAccountKeys),
272        );
273    }
274
275    #[test]
276    fn reclaim_buffer_input_rejects_incomplete_pair() {
277        let data = reclaim_buffer_data();
278        // The shared accounts plus one dangling account that can't form a
279        // full pair.
280        let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + 3 }>();
281        assert_eq!(
282            ReclaimBufferInput::parse(&data, &accounts).err(),
283            Some(ProgramError::NotEnoughAccountKeys),
284        );
285    }
286
287    #[test]
288    fn instruction_data_has_expected_layout() {
289        let program_id = pubkey_from_seed("program id");
290        let state_pda = pubkey_from_seed("state pda");
291        let reclaim_authority = pubkey_from_seed("reclaim authority");
292        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
293        let token_program = pubkey_from_seed("token program");
294        let buffer_pda = pubkey_from_seed("buffer pda");
295        let mint = pubkey_from_seed("mint");
296        let Instruction { data, .. } = ReclaimBuffer {
297            program_id,
298            state_pda,
299            reclaim_authority,
300            reclaim_recipient,
301            token_program,
302            buffers: &[(buffer_pda, mint)],
303        }
304        .into();
305        assert_eq!(
306            data,
307            vec![SettlementInstruction::ReclaimBuffer.discriminator()]
308        );
309    }
310
311    #[test]
312    fn single_buffer_has_expected_accounts() {
313        let program_id = pubkey_from_seed("program id");
314        let state_pda = pubkey_from_seed("state pda");
315        let reclaim_authority = pubkey_from_seed("reclaim authority");
316        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
317        let token_program = pubkey_from_seed("token program");
318        let buffer_pda = pubkey_from_seed("buffer pda");
319        let mint = pubkey_from_seed("mint");
320        let Instruction { accounts, .. } = ReclaimBuffer {
321            program_id,
322            state_pda,
323            reclaim_authority,
324            reclaim_recipient,
325            token_program,
326            buffers: &[(buffer_pda, mint)],
327        }
328        .into();
329
330        assert_eq!(accounts.len(), 6);
331        assert_readonly_nonsigner(&accounts[0], state_pda);
332        assert_readonly_signer(&accounts[1], reclaim_authority);
333        assert_writable_nonsigner(&accounts[2], reclaim_recipient);
334        assert_readonly_nonsigner(&accounts[3], token_program);
335        assert_writable_nonsigner(&accounts[4], buffer_pda);
336        assert_readonly_nonsigner(&accounts[5], mint);
337    }
338
339    #[test]
340    fn recipient_may_be_the_reclaim_authority_itself() {
341        let reclaim_authority = pubkey_from_seed("reclaim authority");
342        let Instruction { accounts, .. } = ReclaimBuffer {
343            program_id: pubkey_from_seed("program id"),
344            state_pda: pubkey_from_seed("state pda"),
345            reclaim_authority,
346            reclaim_recipient: reclaim_authority,
347            token_program: pubkey_from_seed("token program"),
348            buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"))],
349        }
350        .into();
351
352        assert_readonly_signer(&accounts[1], reclaim_authority);
353        assert_writable_nonsigner(&accounts[2], reclaim_authority);
354    }
355
356    #[test]
357    fn multiple_buffers_append_pairs_after_shared_accounts() {
358        let program_id = pubkey_from_seed("program id");
359        let state_pda = pubkey_from_seed("state pda");
360        let reclaim_authority = pubkey_from_seed("reclaim authority");
361        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
362        let token_program = pubkey_from_seed("token program");
363        let buffer_a = pubkey_from_seed("buffer a");
364        let mint_a = pubkey_from_seed("mint a");
365        let buffer_b = pubkey_from_seed("buffer b");
366        let mint_b = pubkey_from_seed("mint b");
367        let Instruction { accounts, .. } = ReclaimBuffer {
368            program_id,
369            state_pda,
370            reclaim_authority,
371            reclaim_recipient,
372            token_program,
373            buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
374        }
375        .into();
376
377        // Four shared accounts followed by two (buffer, mint) pairs.
378        assert_eq!(accounts.len(), 4 + 2 * 2);
379        assert_writable_nonsigner(&accounts[4], buffer_a);
380        assert_readonly_nonsigner(&accounts[5], mint_a);
381        assert_writable_nonsigner(&accounts[6], buffer_b);
382        assert_readonly_nonsigner(&accounts[7], mint_b);
383    }
384
385    #[test]
386    fn empty_buffers_has_only_shared_accounts() {
387        let program_id = pubkey_from_seed("program id");
388        let state_pda = pubkey_from_seed("state pda");
389        let reclaim_authority = pubkey_from_seed("reclaim authority");
390        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
391        let token_program = pubkey_from_seed("token program");
392        let Instruction { accounts, .. } = ReclaimBuffer {
393            program_id,
394            state_pda,
395            reclaim_authority,
396            reclaim_recipient,
397            token_program,
398            buffers: &[],
399        }
400        .into();
401        assert_eq!(accounts.len(), NUM_SHARED_ACCOUNTS);
402    }
403}