Skip to main content

cow_settlement_interface/instruction/
withdraw_native_sol.rs

1//! `WithdrawNativeSol` instruction builder and parser.
2//!
3//! Moves lamports out of the native SOL buffer (see
4//! [`crate::pda::buffer::find_native_sol_buffer_pda`]) to a `recipient` of the
5//! caller's choosing. Only the settlement's configured
6//! `settlement_owned_order_authority` (see
7//! [`crate::data::state::StateAccount`]) may authorize this.
8//!
9//! The `amount` is a maximum: it's capped to the lamports above the buffer's
10//! rent-exempt minimum, so the buffer always stays alive (only `Initialize` can
11//! create it) and a withdrawal never reverts for asking too much.
12//!
13//! Wire format: `[discriminator=12, amount (u64 LE)]`, 9 bytes.
14//! Required accounts:
15//! `[state_pda (R), authority (R,S), native_sol_buffer (W), recipient (W)]`.
16
17use core::mem::size_of;
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 `WithdrawNativeSol` instruction.
27///
28/// `state_pda` must be the settlement state PDA (see
29/// [`crate::pda::state::find_state_pda`]). `authority` must sign and must match
30/// the `settlement_owned_order_authority` recorded in the state PDA's data.
31/// `native_sol_buffer` must be the native SOL buffer PDA (see
32/// [`crate::pda::buffer::find_native_sol_buffer_pda`]). `recipient` receives up
33/// to `amount` lamports, capped to the buffer's balance above rent.
34pub struct WithdrawNativeSol {
35    pub program_id: Pubkey,
36    pub state_pda: Pubkey,
37    pub authority: Pubkey,
38    pub native_sol_buffer: Pubkey,
39    pub recipient: Pubkey,
40    pub amount: u64,
41}
42
43impl From<WithdrawNativeSol> for Instruction {
44    fn from(builder: WithdrawNativeSol) -> Self {
45        let mut data = vec![SettlementInstruction::WithdrawNativeSol.discriminator()];
46        data.extend_from_slice(&builder.amount.to_le_bytes());
47        Instruction {
48            program_id: builder.program_id,
49            accounts: vec![
50                AccountMeta::new_readonly(builder.state_pda, false),
51                AccountMeta::new_readonly(builder.authority, true),
52                AccountMeta::new(builder.native_sol_buffer, false),
53                AccountMeta::new(builder.recipient, false),
54            ],
55            data,
56        }
57    }
58}
59
60/// Parsed inputs of a `WithdrawNativeSol` instruction.
61pub struct WithdrawNativeSolInput<'a, A> {
62    pub state_pda: &'a A,
63    pub authority: &'a A,
64    pub native_sol_buffer: &'a A,
65    pub recipient: &'a A,
66    pub amount: u64,
67}
68
69impl<'a, A> InstructionInputParsing<'a, A> for WithdrawNativeSolInput<'a, A> {
70    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::WithdrawNativeSol;
71
72    fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
73        let amount: &[u8; size_of::<u64>()] = instruction_data
74            .try_into()
75            .map_err(|_| ProgramError::InvalidInstructionData)?;
76        let amount = u64::from_le_bytes(*amount);
77
78        // Accounts: [state_pda (R), authority (R,S), native_sol_buffer (W),
79        // recipient (W)].
80        let [state_pda, authority, native_sol_buffer, recipient, ..] = accounts else {
81            return Err(ProgramError::NotEnoughAccountKeys);
82        };
83
84        Ok(Self {
85            state_pda,
86            authority,
87            native_sol_buffer,
88            recipient,
89            amount,
90        })
91    }
92}
93
94/// Test scaffolding for `WithdrawNativeSol` parsing and handling, shared by
95/// this crate's tests and the settlement program's via the `test-fixtures`
96/// feature.
97#[cfg(any(test, feature = "test-fixtures"))]
98pub mod fixtures {
99    use solana_address::Address;
100
101    use super::{Instruction, WithdrawNativeSol};
102
103    /// Number of accounts `WithdrawNativeSol` expects: state PDA, authority,
104    /// native SOL buffer, and recipient.
105    pub const NUM_ACCOUNTS: usize = 4;
106
107    /// `WithdrawNativeSol` instruction data withdrawing `amount`, with
108    /// placeholder addresses since they don't end up in the data.
109    pub fn withdraw_native_sol_data(amount: u64) -> Vec<u8> {
110        let zero = Address::new_from_array([0; 32]);
111        Instruction::from(WithdrawNativeSol {
112            program_id: zero,
113            state_pda: zero,
114            authority: zero,
115            native_sol_buffer: zero,
116            recipient: zero,
117            amount,
118        })
119        .data
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::fixtures::{withdraw_native_sol_data, NUM_ACCOUNTS};
126    use super::*;
127    use crate::fixtures::pubkey_from_seed;
128    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
129    use crate::instruction::tests::{
130        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
131    };
132
133    fn sample() -> WithdrawNativeSol {
134        WithdrawNativeSol {
135            program_id: pubkey_from_seed("program id"),
136            state_pda: pubkey_from_seed("state pda"),
137            authority: pubkey_from_seed("authority"),
138            native_sol_buffer: pubkey_from_seed("native sol buffer"),
139            recipient: pubkey_from_seed("recipient"),
140            amount: 0x0102_0304_0506_0708,
141        }
142    }
143
144    #[test]
145    fn withdraw_native_sol_input_parses_valid_input() {
146        let builder = sample();
147        let accounts = [
148            fake_account(builder.state_pda),
149            fake_account(builder.authority),
150            fake_account(builder.native_sol_buffer),
151            fake_account(builder.recipient),
152        ];
153        let (state_pda, authority, native_sol_buffer, recipient, amount) = (
154            builder.state_pda,
155            builder.authority,
156            builder.native_sol_buffer,
157            builder.recipient,
158            builder.amount,
159        );
160        let data = Instruction::from(builder).data;
161
162        let WithdrawNativeSolInput {
163            state_pda: parsed_state_pda,
164            authority: parsed_authority,
165            native_sol_buffer: parsed_native_sol_buffer,
166            recipient: parsed_recipient,
167            amount: parsed_amount,
168        } = WithdrawNativeSolInput::parse(&data, &accounts).expect("parse should succeed");
169
170        assert_eq!(*parsed_state_pda.address(), state_pda);
171        assert_eq!(*parsed_authority.address(), authority);
172        assert_eq!(*parsed_native_sol_buffer.address(), native_sol_buffer);
173        assert_eq!(*parsed_recipient.address(), recipient);
174        assert_eq!(parsed_amount, amount);
175    }
176
177    #[test]
178    fn withdraw_native_sol_input_rejects_short_data() {
179        let mut data = withdraw_native_sol_data(1);
180        data.pop();
181        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
182        assert_eq!(
183            WithdrawNativeSolInput::parse(&data, &accounts).err(),
184            Some(ProgramError::InvalidInstructionData),
185        );
186    }
187
188    #[test]
189    fn withdraw_native_sol_input_rejects_long_data() {
190        let mut data = withdraw_native_sol_data(1);
191        data.push(0);
192        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
193        assert_eq!(
194            WithdrawNativeSolInput::parse(&data, &accounts).err(),
195            Some(ProgramError::InvalidInstructionData),
196        );
197    }
198
199    #[test]
200    fn withdraw_native_sol_input_rejects_missing_accounts() {
201        let data = withdraw_native_sol_data(1);
202        let accounts = fake_sequential_accounts::<{ NUM_ACCOUNTS - 1 }>();
203        assert_eq!(
204            WithdrawNativeSolInput::parse(&data, &accounts).err(),
205            Some(ProgramError::NotEnoughAccountKeys),
206        );
207    }
208
209    #[test]
210    fn instruction_data_has_expected_layout() {
211        let Instruction { data, .. } = WithdrawNativeSol {
212            amount: 0x0102_0304_0506_0708,
213            ..sample()
214        }
215        .into();
216        assert_eq!(
217            data,
218            [
219                SettlementInstruction::WithdrawNativeSol.discriminator(),
220                8,
221                7,
222                6,
223                5,
224                4,
225                3,
226                2,
227                1,
228            ]
229        );
230    }
231
232    #[test]
233    fn instruction_has_expected_accounts() {
234        let builder = sample();
235        let ix = Instruction::from(sample());
236
237        assert_eq!(ix.program_id, builder.program_id);
238        assert_eq!(ix.accounts.len(), NUM_ACCOUNTS);
239        assert_readonly_nonsigner(&ix.accounts[0], builder.state_pda);
240        assert_readonly_signer(&ix.accounts[1], builder.authority);
241        assert_writable_nonsigner(&ix.accounts[2], builder.native_sol_buffer);
242        assert_writable_nonsigner(&ix.accounts[3], builder.recipient);
243    }
244}