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//! Each relaimed buffer takes a burn limit. Up to this amount of balance is
9//! burned, so that rent reclamation remains possible when the tokens can't be
10//! otherwise be disposed of. A zero limit prevents any burning and is the
11//! expected standard way to reclaim buffers, since most tokens should rather
12//! be sold in advance than burned.
13//!
14//! The token_program supplied to this instruction must be the owner of all mints
15//! supplied. Only one token program can be supplied to this instruction at a time.
16//! If mints from two separate token programs are required, the client needs to
17//! divide it into separate instructions.
18//!
19//! Wire format (with `n` buffers): `[discriminator=6][burn_limit: u64 LE ×n]`.
20//! Required accounts:
21//! `[state_pda (R), reclaim_authority (R,S), reclaim_recipient (W),
22//! token_program (R), (buffer_pda (W), mint (W))...]`.
23
24use solana_instruction::{AccountMeta, Instruction};
25use solana_program_error::ProgramError;
26use solana_pubkey::Pubkey;
27
28use super::InstructionInputParsing;
29use crate::SettlementInstruction;
30
31/// Number of accounts each buffer contributes to the instruction: its buffer
32/// PDA and its mint.
33pub const ACCOUNTS_PER_BUFFER: usize = 2;
34
35/// Builder for a `ReclaimBuffer` instruction that closes one buffer per
36/// `(buffer_pda, mint, burn_limit)` entry in `buffers`.
37///
38/// `state_pda` must be [`crate::pda::state::STATE_PDA`]. `reclaim_authority`
39/// must sign and must match the `reclaim_authority` recorded in the state PDA's data.
40/// `reclaim_recipient` is the account receiving the closed buffer's lamports.
41/// Each `buffer_pda` must be the canonical PDA returned by
42/// [`crate::pda::buffer::find_buffer_pda`] for its paired `mint`, which is
43/// passed both so that derivation can be checked on-chain and so the balance can
44/// be burned when clearing the buffer.
45///
46/// `burn_limit` caps how much of a buffer's balance may be burned to clear it:
47/// a buffer whose balance doesn't exceed its limit is burned to zero (when
48/// non-empty) and closed, while one that exceeds its limit is skipped so the
49/// rest of the batch still closes; the instruction reverts only when every
50/// buffer is over its limit. A zero limit prevents any burning.
51pub struct ReclaimBuffer<'a> {
52    pub program_id: Pubkey,
53    pub state_pda: Pubkey,
54    pub reclaim_authority: Pubkey,
55    pub reclaim_recipient: Pubkey,
56    /// The token program owning every buffer this instruction closes. Must be
57    /// the address of a [`crate::token_program::TokenProgram`].
58    pub token_program: Pubkey,
59    /// One `(buffer_pda, mint, burn_limit)` entry per buffer to close.
60    pub buffers: &'a [(Pubkey, Pubkey, u64)],
61}
62
63impl From<ReclaimBuffer<'_>> for Instruction {
64    fn from(builder: ReclaimBuffer<'_>) -> Self {
65        let mut accounts = vec![
66            AccountMeta::new_readonly(builder.state_pda, false),
67            AccountMeta::new_readonly(builder.reclaim_authority, true),
68            AccountMeta::new(builder.reclaim_recipient, false),
69            AccountMeta::new_readonly(builder.token_program, false),
70        ];
71        let mut data = vec![SettlementInstruction::ReclaimBuffer.discriminator()];
72        for (buffer_pda, mint, burn_limit) in builder.buffers {
73            accounts.push(AccountMeta::new(*buffer_pda, false));
74            // The mint is writable so the handler can burn the buffer's
75            // balance to clear it, which decrements the mint's supply.
76            accounts.push(AccountMeta::new(*mint, false));
77            data.extend_from_slice(&burn_limit.to_le_bytes());
78        }
79        Instruction {
80            program_id: builder.program_id,
81            accounts,
82            data,
83        }
84    }
85}
86
87/// Decoded info for a single buffer to reclaim
88pub struct Buffer<'a, A> {
89    pub buffer_pda: &'a A,
90    pub mint: &'a A,
91    pub burn_limit: u64,
92}
93
94/// The `(buffer_pda, mint)` account pairs of a `ReclaimBuffer` and their
95/// parallel per-buffer burn limits. Parsing guarantees the two have the same
96/// length.
97pub struct Buffers<'a, A> {
98    accounts: &'a [[A; ACCOUNTS_PER_BUFFER]],
99    burn_limits: &'a [[u8; core::mem::size_of::<u64>()]],
100}
101
102impl<'a, A> Buffers<'a, A> {
103    pub fn iter(&self) -> impl Iterator<Item = Buffer<'a, A>> + '_ {
104        self.accounts
105            .iter()
106            .zip(self.burn_limits)
107            .map(|([buffer_pda, mint], burn_limit)| Buffer {
108                buffer_pda,
109                mint,
110                burn_limit: u64::from_le_bytes(*burn_limit),
111            })
112    }
113}
114
115/// Parsed inputs of a `ReclaimBuffer` instruction.
116pub struct ReclaimBufferInput<'a, A> {
117    pub state_pda: &'a A,
118    pub reclaim_authority: &'a A,
119    pub reclaim_recipient: &'a A,
120    pub buffers: Buffers<'a, A>,
121}
122
123impl<'a, A> InstructionInputParsing<'a, A> for ReclaimBufferInput<'a, A> {
124    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::ReclaimBuffer;
125
126    fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
127        // Accounts: [state_pda (R), reclaim_authority (R,S), reclaim_recipient
128        // (W), token_program (R), (buffer_pda (W), mint (W))...]. The four
129        // shared accounts come first; the per-buffer pairs follow, one pair per
130        // buffer. The token program is skipped rather than read: each buffer is
131        // closed by the program that owns it, so the account is only there to
132        // put that program in the transaction.
133        let [state_pda, reclaim_authority, reclaim_recipient, _token_program, rest @ ..] = accounts
134        else {
135            return Err(ProgramError::NotEnoughAccountKeys);
136        };
137        // Group the trailing accounts into `[buffer_pda, mint]` pairs. Each
138        // buffer needs both, so a stray leftover account is a malformed
139        // instruction. There must be at least one pair: an instruction that
140        // reclaims no buffers is rejected as a likely encoding issue.
141        let (buffer_pairs, remainder) = rest.as_chunks::<ACCOUNTS_PER_BUFFER>();
142        if !remainder.is_empty() || buffer_pairs.is_empty() {
143            return Err(ProgramError::NotEnoughAccountKeys);
144        }
145
146        // The data carries one little-endian `u64` burn limit per buffer, in
147        // buffer order. A trailing partial limit, or a limit count that doesn't
148        // match the buffers, is a malformed instruction.
149        let (burn_limits, remainder) =
150            instruction_data.as_chunks::<{ core::mem::size_of::<u64>() }>();
151        if !remainder.is_empty() || burn_limits.len() != buffer_pairs.len() {
152            return Err(ProgramError::InvalidInstructionData);
153        }
154
155        Ok(Self {
156            state_pda,
157            reclaim_authority,
158            reclaim_recipient,
159            buffers: Buffers {
160                accounts: buffer_pairs,
161                burn_limits,
162            },
163        })
164    }
165}
166
167/// Test scaffolding for `ReclaimBuffer` parsing and handling, shared by this
168/// crate's tests and the settlement program's via the `test-fixtures` feature.
169#[cfg(any(test, feature = "test-fixtures"))]
170pub mod fixtures {
171    use solana_address::Address;
172
173    use super::{Instruction, ReclaimBuffer};
174
175    /// Number of accounts that don't depend on the number of buffers
176    /// reclaimed: state PDA, reclaim authority, reclaim recipient, and token
177    /// program.
178    pub const NUM_SHARED_ACCOUNTS: usize = 4;
179
180    /// `ReclaimBuffer` instruction data with placeholder addresses and data,
181    /// for failure cases where the input is irrelevant.
182    pub fn reclaim_buffer_data() -> Vec<u8> {
183        let zero = Address::new_from_array([0; 32]);
184        Instruction::from(ReclaimBuffer {
185            program_id: zero,
186            state_pda: zero,
187            reclaim_authority: zero,
188            reclaim_recipient: zero,
189            token_program: zero,
190            buffers: &[(zero, zero, 0)],
191        })
192        .data
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::fixtures::{reclaim_buffer_data, NUM_SHARED_ACCOUNTS};
199    use super::*;
200    use crate::fixtures::pubkey_from_seed;
201    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
202    use crate::instruction::tests::{
203        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
204    };
205
206    #[test]
207    fn reclaim_buffer_input_parses_valid_input() {
208        let program_id = pubkey_from_seed("program id");
209        let state_pda = pubkey_from_seed("state pda");
210        let reclaim_authority = pubkey_from_seed("reclaim authority");
211        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
212        let token_program = pubkey_from_seed("token program");
213        let buffer_pda = pubkey_from_seed("buffer pda");
214        let mint = pubkey_from_seed("mint");
215
216        let data = Instruction::from(ReclaimBuffer {
217            program_id,
218            state_pda,
219            reclaim_authority,
220            reclaim_recipient,
221            token_program,
222            buffers: &[(buffer_pda, mint, 1337)],
223        })
224        .data;
225        let token_program = pubkey_from_seed("token program");
226        let accounts = [
227            fake_account(state_pda),
228            fake_account(reclaim_authority),
229            fake_account(reclaim_recipient),
230            fake_account(token_program),
231            fake_account(buffer_pda),
232            fake_account(mint),
233        ];
234
235        let ReclaimBufferInput {
236            state_pda: parsed_state_pda,
237            reclaim_authority: parsed_reclaim_authority,
238            reclaim_recipient: parsed_reclaim_recipient,
239            buffers,
240        } = ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
241
242        assert_eq!(*parsed_state_pda.address(), state_pda);
243        assert_eq!(*parsed_reclaim_authority.address(), reclaim_authority);
244        assert_eq!(*parsed_reclaim_recipient.address(), reclaim_recipient);
245        let parsed: Vec<_> = buffers.iter().collect();
246        let [Buffer {
247            buffer_pda: parsed_buffer_pda,
248            mint: parsed_mint,
249            burn_limit,
250        }] = parsed.as_slice()
251        else {
252            panic!("expected exactly one parsed buffer");
253        };
254        assert_eq!(*parsed_buffer_pda.address(), buffer_pda);
255        assert_eq!(*parsed_mint.address(), mint);
256        assert_eq!(*burn_limit, 1337);
257    }
258
259    #[test]
260    fn reclaim_buffer_input_parses_multiple_buffers() {
261        let program_id = pubkey_from_seed("program id");
262        let state_pda = pubkey_from_seed("state pda");
263        let reclaim_authority = pubkey_from_seed("reclaim authority");
264        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
265        let token_program = pubkey_from_seed("token program");
266        let buffer_a = pubkey_from_seed("buffer a");
267        let mint_a = pubkey_from_seed("mint a");
268        let buffer_b = pubkey_from_seed("buffer b");
269        let mint_b = pubkey_from_seed("mint b");
270
271        let data = Instruction::from(ReclaimBuffer {
272            program_id,
273            state_pda,
274            token_program,
275            reclaim_authority,
276            reclaim_recipient,
277            buffers: &[(buffer_a, mint_a, 42), (buffer_b, mint_b, 1337)],
278        })
279        .data;
280        let accounts = [
281            fake_account(state_pda),
282            fake_account(reclaim_authority),
283            fake_account(reclaim_recipient),
284            fake_account(token_program),
285            fake_account(buffer_a),
286            fake_account(mint_a),
287            fake_account(buffer_b),
288            fake_account(mint_b),
289        ];
290
291        let ReclaimBufferInput { buffers, .. } =
292            ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
293
294        let parsed: Vec<_> = buffers.iter().collect();
295        let [Buffer {
296            buffer_pda: parsed_buffer_a,
297            mint: parsed_mint_a,
298            burn_limit: limit_a,
299        }, Buffer {
300            buffer_pda: parsed_buffer_b,
301            mint: parsed_mint_b,
302            burn_limit: limit_b,
303        }] = parsed.as_slice()
304        else {
305            panic!("expected exactly two parsed buffers");
306        };
307        assert_eq!(*parsed_buffer_a.address(), buffer_a);
308        assert_eq!(*parsed_mint_a.address(), mint_a);
309        assert_eq!(*limit_a, 42);
310        assert_eq!(*parsed_buffer_b.address(), buffer_b);
311        assert_eq!(*parsed_mint_b.address(), mint_b);
312        assert_eq!(*limit_b, 1337);
313    }
314
315    #[test]
316    fn reclaim_buffer_input_rejects_zero_buffers() {
317        let data = reclaim_buffer_data();
318        // Only the four shared accounts, no buffer pairs.
319        let accounts = fake_sequential_accounts::<NUM_SHARED_ACCOUNTS>();
320        assert_eq!(
321            ReclaimBufferInput::parse(&data, &accounts).err(),
322            Some(ProgramError::NotEnoughAccountKeys),
323            "an instruction that reclaims no buffers is rejected",
324        );
325    }
326
327    #[test]
328    fn reclaim_buffer_input_rejects_a_partial_trailing_limit() {
329        let mut data = reclaim_buffer_data();
330        data.push(0); // trailing byte
331        let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + ACCOUNTS_PER_BUFFER }>();
332        assert_eq!(
333            ReclaimBufferInput::parse(&data, &accounts).err(),
334            Some(ProgramError::InvalidInstructionData),
335        );
336    }
337
338    #[test]
339    fn reclaim_buffer_input_rejects_a_limit_count_that_does_not_match_the_buffers() {
340        // Data for one buffer, but two buffer pairs in the accounts.
341        let data = reclaim_buffer_data();
342        let accounts =
343            fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + 2 * ACCOUNTS_PER_BUFFER }>();
344        assert_eq!(
345            ReclaimBufferInput::parse(&data, &accounts).err(),
346            Some(ProgramError::InvalidInstructionData),
347        );
348    }
349
350    #[test]
351    fn reclaim_buffer_input_rejects_missing_accounts() {
352        let data = reclaim_buffer_data();
353        // Fewer than the four shared accounts.
354        let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS - 1 }>();
355        assert_eq!(
356            ReclaimBufferInput::parse(&data, &accounts).err(),
357            Some(ProgramError::NotEnoughAccountKeys),
358        );
359    }
360
361    #[test]
362    fn reclaim_buffer_input_rejects_incomplete_pair() {
363        let data = reclaim_buffer_data();
364        // The shared accounts plus one dangling account that can't form a
365        // full pair.
366        let accounts =
367            fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + ACCOUNTS_PER_BUFFER + 1 }>();
368        assert_eq!(
369            ReclaimBufferInput::parse(&data, &accounts).err(),
370            Some(ProgramError::NotEnoughAccountKeys),
371        );
372    }
373
374    #[test]
375    fn instruction_data_has_expected_layout() {
376        let program_id = pubkey_from_seed("program id");
377        let state_pda = pubkey_from_seed("state pda");
378        let reclaim_authority = pubkey_from_seed("reclaim authority");
379        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
380        let token_program = pubkey_from_seed("token program");
381        let buffer_pda = pubkey_from_seed("buffer pda");
382        let mint = pubkey_from_seed("mint");
383        let Instruction { data, .. } = ReclaimBuffer {
384            program_id,
385            state_pda,
386            reclaim_authority,
387            reclaim_recipient,
388            token_program,
389            buffers: &[(buffer_pda, mint, 0x1337)],
390        }
391        .into();
392        let mut expected = vec![SettlementInstruction::ReclaimBuffer.discriminator()];
393        expected.extend_from_slice(&[0x37, 0x13, 0, 0, 0, 0, 0, 0]);
394        assert_eq!(data, expected);
395    }
396
397    #[test]
398    fn single_buffer_has_expected_accounts() {
399        let program_id = pubkey_from_seed("program id");
400        let state_pda = pubkey_from_seed("state pda");
401        let reclaim_authority = pubkey_from_seed("reclaim authority");
402        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
403        let token_program = pubkey_from_seed("token program");
404        let buffer_pda = pubkey_from_seed("buffer pda");
405        let mint = pubkey_from_seed("mint");
406        let Instruction { accounts, .. } = ReclaimBuffer {
407            program_id,
408            state_pda,
409            reclaim_authority,
410            reclaim_recipient,
411            token_program,
412            buffers: &[(buffer_pda, mint, 0)],
413        }
414        .into();
415
416        assert_eq!(accounts.len(), 6);
417        assert_readonly_nonsigner(&accounts[0], state_pda);
418        assert_readonly_signer(&accounts[1], reclaim_authority);
419        assert_writable_nonsigner(&accounts[2], reclaim_recipient);
420        assert_readonly_nonsigner(&accounts[3], token_program);
421        assert_writable_nonsigner(&accounts[4], buffer_pda);
422        assert_writable_nonsigner(&accounts[5], mint);
423    }
424
425    #[test]
426    fn recipient_may_be_the_reclaim_authority_itself() {
427        let reclaim_authority = pubkey_from_seed("reclaim authority");
428        let Instruction { accounts, .. } = ReclaimBuffer {
429            program_id: pubkey_from_seed("program id"),
430            state_pda: pubkey_from_seed("state pda"),
431            reclaim_authority,
432            reclaim_recipient: reclaim_authority,
433            token_program: pubkey_from_seed("token program"),
434            buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"), 0)],
435        }
436        .into();
437
438        assert_readonly_signer(&accounts[1], reclaim_authority);
439        assert_writable_nonsigner(&accounts[2], reclaim_authority);
440    }
441
442    #[test]
443    fn multiple_buffers_append_pairs_after_shared_accounts() {
444        let program_id = pubkey_from_seed("program id");
445        let state_pda = pubkey_from_seed("state pda");
446        let reclaim_authority = pubkey_from_seed("reclaim authority");
447        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
448        let token_program = pubkey_from_seed("token program");
449        let buffer_a = pubkey_from_seed("buffer a");
450        let mint_a = pubkey_from_seed("mint a");
451        let buffer_b = pubkey_from_seed("buffer b");
452        let mint_b = pubkey_from_seed("mint b");
453        let Instruction { accounts, .. } = ReclaimBuffer {
454            program_id,
455            state_pda,
456            reclaim_authority,
457            reclaim_recipient,
458            token_program,
459            buffers: &[(buffer_a, mint_a, 0), (buffer_b, mint_b, 0)],
460        }
461        .into();
462
463        // The shared accounts followed by two (buffer, mint) pairs.
464        assert_eq!(
465            accounts.len(),
466            NUM_SHARED_ACCOUNTS + 2 * ACCOUNTS_PER_BUFFER
467        );
468        assert_writable_nonsigner(&accounts[4], buffer_a);
469        assert_writable_nonsigner(&accounts[5], mint_a);
470        assert_writable_nonsigner(&accounts[6], buffer_b);
471        assert_writable_nonsigner(&accounts[7], mint_b);
472    }
473
474    #[test]
475    fn empty_buffers_has_only_shared_accounts() {
476        let program_id = pubkey_from_seed("program id");
477        let state_pda = pubkey_from_seed("state pda");
478        let reclaim_authority = pubkey_from_seed("reclaim authority");
479        let reclaim_recipient = pubkey_from_seed("reclaim recipient");
480        let token_program = pubkey_from_seed("token program");
481        let Instruction { accounts, .. } = ReclaimBuffer {
482            program_id,
483            state_pda,
484            reclaim_authority,
485            reclaim_recipient,
486            token_program,
487            buffers: &[],
488        }
489        .into();
490        assert_eq!(accounts.len(), NUM_SHARED_ACCOUNTS);
491    }
492}