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