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