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