cow_settlement_interface/instruction/mod.rs
1//! Off-chain instruction builders for the settlement program.
2//!
3//! Each submodule builds the [`solana_instruction::Instruction`] for specific
4//! settlement instructions, encoding their discriminator (see
5//! [`crate::SettlementInstruction`]) and laying out the required accounts.
6
7use solana_program_error::ProgramError;
8
9pub mod add_solver;
10pub mod cancel_order;
11pub mod create_buffer;
12pub mod create_order;
13pub mod create_settlement_owned_order;
14pub mod initialize;
15pub mod reclaim_buffer;
16pub mod reclaim_order;
17pub mod remove_solver;
18pub mod settle;
19pub mod transfer_authority;
20pub mod withdraw_native_sol;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)]
23#[repr(u8)]
24#[num_enum(error_type(
25 name = ProgramError,
26 constructor = SettlementInstruction::unknown_discriminator,
27))]
28pub enum SettlementInstruction {
29 /// Pulls funds for a batch of orders. Must be paired in the same
30 /// transaction with a `FinalizeSettle` at `finalize_ix_index`.
31 BeginSettle = 0,
32 /// Validates that a `BeginSettle` at `begin_ix_index` exists and points
33 /// back at this instruction. Must not be called via CPI.
34 FinalizeSettle = 1,
35 /// Allocates a per-order PDA and writes the initial `OrderAccount` body.
36 CreateOrder = 2,
37 /// Creates the singleton settlement state PDA and the native SOL buffer.
38 /// Succeeds only once, and only when signed by the hardcoded initializer.
39 Initialize = 3,
40 /// Creates one or more per-token buffer PDAs (token accounts) in a
41 /// single instruction.
42 ///
43 /// Each buffer_pda_i must be the canonical PDA for seeds
44 /// [SETTLEMENT_SEED, mint_i, "buffer"].
45 CreateBuffer = 4,
46 /// Closes an order PDA and returns its rent lamports to the created_by
47 /// account recorded in the order body.
48 ///
49 /// Anyone may reclaim after now > valid_to, or before expiry if the order
50 /// is completely filled.
51 ///
52 /// Reclaiming a cancelled order that is not completely filled while
53 /// now <= valid_to requires the stored intent owner's signature.
54 ReclaimOrder = 5,
55 /// Closes one or more buffer PDAs and sends each closed buffer's rent
56 /// lamports to a reclaim_recipient of the caller's choosing.
57 ReclaimBuffer = 6,
58 /// Transfers a Role to another account: the signer names the new holder and
59 /// the role's holder is updated immediately.
60 TransferAuthority = 7,
61 /// Registers a solver, allowing that account to execute settlements.
62 AddSolver = 8,
63 /// Removes a solver, preventing that account from executing settlements.
64 RemoveSolver = 9,
65 /// Create an order on behalf of the state PDA. Used to sell fees
66 /// accumulated in the buffer accounts through a regular settlement.
67 CreateSettlementOwnedOrder = 10,
68 /// Cancels an order so it can no longer be settled. Only the order owner
69 /// may cancel.
70 /// If the order PDA doesn't exist yet, it's created already cancelled. For
71 /// this, some arbitrary account needs to pay the rent and sign.
72 CancelOrder = 11,
73 /// Moves lamports out of the native SOL buffer to a recipient of the
74 /// caller's choosing. Only the current holder of the
75 /// SettlementOwnedOrderAuthority role may authorize this, and the buffer's
76 /// rent can't be withdrawn.
77 WithdrawNativeSol = 12,
78}
79
80impl SettlementInstruction {
81 pub fn discriminator(self) -> u8 {
82 self as u8
83 }
84
85 fn unknown_discriminator(_: u8) -> ProgramError {
86 ProgramError::InvalidInstructionData
87 }
88}
89
90/// Recover the discriminator from the first byte of the payload and the
91/// remaining bytes to parse.
92/// Returns `InvalidInstructionData` for an insufficient length or an
93/// unknown discriminator.
94pub fn recover_discriminator(
95 instruction_data: &[u8],
96) -> Result<(SettlementInstruction, &[u8]), ProgramError> {
97 let discriminator = instruction_data
98 .first()
99 .copied()
100 .ok_or(ProgramError::InvalidInstructionData)
101 .and_then(SettlementInstruction::try_from)?;
102 Ok((discriminator, &instruction_data[1..]))
103}
104
105/// Shared components for parsing an instruction's input (data fields and
106/// accounts).
107///
108/// Implementations declare which [`SettlementInstruction`] discriminator they
109/// belong to and parse the remaining instruction data and accounts. The
110/// discriminator check is shared via the default [`Self::parse`] implementation;
111/// an impl only needs to provide [`Self::parse_body`].
112///
113/// Parsing accounts is purely positional: they are picked out by their index in
114/// the slice, never by inspecting them. The account type `A` is left generic so
115/// the same layout can be parsed from any account representation, on-chain
116/// (`solana_account_view::AccountView`) or off-chain.
117pub trait InstructionInputParsing<'a, A>: Sized {
118 const DISCRIMINATOR: SettlementInstruction;
119
120 fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError>;
121
122 fn parse(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
123 match recover_discriminator(instruction_data)? {
124 (discriminator, remaining_data) if discriminator == Self::DISCRIMINATOR => {
125 Self::parse_body(remaining_data, accounts)
126 }
127 _ => Err(ProgramError::InvalidInstructionData),
128 }
129 }
130}
131
132/// Account-building scaffolding shared by the parser unit tests in this crate
133/// and the settlement program's own tests.
134///
135/// Exposed under the `test-fixtures` feature (and unconditionally for this
136/// crate's own `cargo test`) so both crates can build
137/// [`AccountView`](solana_account_view::AccountView)s without duplicating the
138/// unsafe initializer below.
139#[cfg(any(test, feature = "test-fixtures"))]
140pub mod fixtures {
141 use solana_account_view::{AccountView, RuntimeAccount};
142 use solana_address::Address;
143 use std::{mem, ptr};
144
145 /// Build an `AccountView` based on the input `RuntimeAccount` and whose
146 /// data region is empty.
147 ///
148 /// This is trickier to do than it should be. There's no safe initializer for
149 /// `AccountView` in Pinocchio. The only initializer is:
150 /// <https://docs.rs/solana-account-view/2.0.0/solana_account_view/struct.AccountView.html#method.new_unchecked>
151 ///
152 /// `AccountView::new_unchecked` requires (1) a pointer to an initialized
153 /// `RuntimeAccount`, (2) immediately followed by exactly `data_len` bytes of
154 /// data. We satisfy (1) via `Box::new(RuntimeAccount::default())` (every
155 /// field is zero-initialized, then we overwrite `address`), and (2) by
156 /// setting `data_len = 0` so the trailing-data clause is vacuously true
157 /// regardless of what's actually in memory after the box.
158 ///
159 /// [`Box::leak`] keeps the backing alive for the rest of the test process:
160 /// a dropped `Box` or a returned stack slot would leave the pointer
161 /// dangling. We ignore the memory leak since this function is only intended to
162 /// use in tests.
163 /// <https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak>
164 ///
165 /// Every `AccountView` method is safe to call on the result. Header
166 /// accessors read fields out of the `RuntimeAccount`. Data-region accessors
167 /// hand out a zero-length slice, which [`core::slice::from_raw_parts`] (the
168 /// primitive underneath them) defines as sound for any non-null, aligned
169 /// pointer. This is true for us because the pointer itself comes boxed data
170 /// and not some manual allocation.
171 /// <https://docs.rs/crate/solana-account-view/2.0.0/source/src/lib.rs#98-295>
172 /// <https://doc.rust-lang.org/beta/core/slice/fn.from_raw_parts.html>
173 pub fn fake_account_from(runtime_account: RuntimeAccount) -> AccountView {
174 let backing = Box::leak(Box::new(runtime_account));
175 unsafe { AccountView::new_unchecked(backing as *mut RuntimeAccount) }
176 }
177
178 /// Adapted from pinocchio's crate-private test helper; kept structurally
179 /// close for comparison:
180 /// <https://docs.rs/crate/pinocchio/0.11.1/source/src/sysvars/slot_hashes/test_utils.rs#120-160>
181 ///
182 /// Allocate a heap-backed `AccountView` whose data region is initialized with
183 /// `data`, whose address is `address`, whose owning program is `owner`, and
184 /// whose borrow flag is `borrow_state`.
185 ///
186 /// The function also returns the backing `Vec<u64>` so the caller can keep it
187 /// alive for the duration of the test (otherwise the memory would be freed and
188 /// the raw pointer inside `AccountView` would dangle).
189 ///
190 /// # Safety
191 /// The caller must ensure the returned `AccountView` is used only for reading
192 /// or according to borrow rules because the Solana runtime invariants are not
193 /// fully enforced in this hand-rolled representation.
194 #[allow(
195 clippy::arithmetic_side_effects,
196 reason = "the function is mostly vendored and we don't want to introduce unnecessary changes"
197 )]
198 pub unsafe fn make_account_view(
199 address: Address,
200 owner: Address,
201 data: &[u8],
202 borrow_state: u8,
203 ) -> (AccountView, Vec<u64>) {
204 // pinocchio writes a hand-rolled `AccountLayout` mirror and casts the
205 // pointer to `*mut RuntimeAccount`, because inside that crate the
206 // account struct's fields are private and its tests cannot set them by
207 // name. Here we instead write a real `RuntimeAccount`, since
208 // `solana_account_view::RuntimeAccount` has public fields and a
209 // `Default` impl.
210 let hdr_size = mem::size_of::<RuntimeAccount>();
211 let total = hdr_size + data.len();
212 let words = total.div_ceil(8);
213 let mut backing: Vec<u64> = vec![0u64; words];
214 assert!(
215 mem::align_of::<u64>() >= mem::align_of::<RuntimeAccount>(),
216 "`backing` should be properly aligned to store a `RuntimeAccount` instance"
217 );
218
219 let hdr_ptr = backing.as_mut_ptr() as *mut RuntimeAccount;
220 ptr::write(
221 hdr_ptr,
222 RuntimeAccount {
223 address,
224 owner,
225 borrow_state,
226 data_len: data.len() as u64,
227 ..Default::default()
228 },
229 );
230
231 ptr::copy_nonoverlapping(
232 data.as_ptr(),
233 (hdr_ptr as *mut u8).add(hdr_size),
234 data.len(),
235 );
236
237 (AccountView::new_unchecked(hdr_ptr), backing)
238 }
239
240 /// Create an account view storing the input data, owned by `owner`.
241 pub fn fake_account_owned_by(address: Address, owner: Address, data: &[u8]) -> AccountView {
242 let (account, backing) =
243 unsafe { make_account_view(address, owner, data, solana_account_view::NOT_BORROWED) };
244 core::mem::forget(backing);
245 account
246 }
247
248 /// Create an account view storing the input data.
249 pub fn fake_account_with_data(address: Address, data: &[u8]) -> AccountView {
250 fake_account_owned_by(address, Address::default(), data)
251 }
252
253 pub fn fake_account(address: Address) -> AccountView {
254 fake_account_with_data(address, &[])
255 }
256
257 pub fn fake_account_from_array(address_array: [u8; 32]) -> AccountView {
258 fake_account(Address::new_from_array(address_array))
259 }
260
261 pub fn fake_signer(address: Address) -> AccountView {
262 fake_account_from(RuntimeAccount {
263 address,
264 is_signer: 1,
265 ..Default::default()
266 })
267 }
268
269 /// Build `N` fake accounts with sequential addresses (`[1; 32]`, `[2; 32]`, …).
270 pub fn fake_sequential_accounts<const N: usize>() -> [AccountView; N] {
271 core::array::from_fn(|i| fake_account_from_array([(i as u8).wrapping_add(1); 32]))
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::{AccountMeta, Pubkey};
279 use solana_account_view::AccountView;
280
281 #[track_caller]
282 pub(crate) fn assert_writable_signer(account: &AccountMeta, pubkey: Pubkey) {
283 assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
284 assert!(account.is_writable, "{} should be writable", account.pubkey);
285 assert!(account.is_signer, "{} should be a signer", account.pubkey);
286 }
287
288 #[track_caller]
289 pub(crate) fn assert_writable_nonsigner(account: &AccountMeta, pubkey: Pubkey) {
290 assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
291 assert!(account.is_writable, "{} should be writable", account.pubkey);
292 assert!(
293 !account.is_signer,
294 "{} should not be a signer",
295 account.pubkey
296 );
297 }
298
299 #[track_caller]
300 pub(crate) fn assert_readonly_signer(account: &AccountMeta, pubkey: Pubkey) {
301 assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
302 assert!(
303 !account.is_writable,
304 "{} should be read-only",
305 account.pubkey
306 );
307 assert!(account.is_signer, "{} should be a signer", account.pubkey);
308 }
309
310 #[track_caller]
311 pub(crate) fn assert_readonly_nonsigner(account: &AccountMeta, pubkey: Pubkey) {
312 assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
313 assert!(
314 !account.is_writable,
315 "{} should be read-only",
316 account.pubkey
317 );
318 assert!(
319 !account.is_signer,
320 "{} should not be a signer",
321 account.pubkey
322 );
323 }
324
325 #[test]
326 fn input_parsing_rejects_different_discriminator() {
327 struct TestInputParsing {}
328 impl<'a> InstructionInputParsing<'a, AccountView> for TestInputParsing {
329 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::BeginSettle;
330
331 fn parse_body(
332 _instruction_data: &'a [u8],
333 _accounts: &'a [AccountView],
334 ) -> Result<Self, ProgramError> {
335 Ok(Self {})
336 }
337 }
338
339 let mut data = [0; 42];
340 let different_discriminator = SettlementInstruction::CreateOrder;
341 assert_ne!(TestInputParsing::DISCRIMINATOR, different_discriminator);
342 data[0] = different_discriminator.discriminator();
343 assert_eq!(
344 TestInputParsing::parse(&data, &[]).err(),
345 Some(ProgramError::InvalidInstructionData),
346 );
347 }
348
349 #[test]
350 fn rejects_empty_payload() {
351 assert_eq!(
352 recover_discriminator(&[]),
353 Err(ProgramError::InvalidInstructionData),
354 );
355 }
356
357 #[test]
358 fn rejects_unknown_discriminator() {
359 // 42 is outside the set of valid discriminators.
360 assert_eq!(
361 recover_discriminator(&[42]),
362 Err(ProgramError::InvalidInstructionData),
363 );
364 }
365
366 #[test]
367 fn forwards_trailing_bytes() {
368 assert!(matches!(
369 recover_discriminator(&[
370 SettlementInstruction::BeginSettle.discriminator(),
371 42 // unused
372 ]),
373 Ok((SettlementInstruction::BeginSettle, [42])),
374 ));
375 }
376
377 #[test]
378 fn settlement_instruction_try_from_partitions_all_bytes() {
379 for i in u8::MIN..=u8::MAX {
380 match SettlementInstruction::try_from(i) {
381 Ok(ix) => assert_eq!(ix as u8, i),
382 Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData),
383 }
384 }
385 }
386
387 #[test]
388 fn settlement_instruction_try_from_matches_begin_settle() {
389 assert_eq!(
390 SettlementInstruction::try_from(0),
391 Ok(SettlementInstruction::BeginSettle)
392 );
393 }
394}