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