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_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}