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