Skip to main content

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