Skip to main content

cow_settlement_interface/
lib.rs

1//! Shared types and instruction builders for the CoW Protocol settlement program.
2
3pub use solana_instruction::{AccountMeta, Instruction};
4use solana_program_error::ProgramError;
5pub use solana_pubkey::Pubkey;
6
7solana_pubkey::declare_id!("MooohhPEAAHwAwEozL7JPEmnDvaahuUpccYN4Yb8ccK");
8
9pub mod data;
10pub mod instruction;
11pub mod pda;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)]
14#[repr(u8)]
15#[num_enum(error_type(
16    name = ProgramError,
17    constructor = SettlementInstruction::unknown_discriminator,
18))]
19pub enum SettlementInstruction {
20    BeginSettle = 0,
21    FinalizeSettle = 1,
22    CreateOrder = 2,
23    Initialize = 3,
24    CreateBuffer = 4,
25    ReclaimOrder = 5,
26}
27
28impl SettlementInstruction {
29    pub fn discriminator(self) -> u8 {
30        self as u8
31    }
32
33    fn unknown_discriminator(_: u8) -> ProgramError {
34        ProgramError::InvalidInstructionData
35    }
36}
37
38/// Identifies the account type a given account's data belongs to, via the
39/// single discriminator byte stored at its front. Starts at 128 to keep
40/// account discriminators visually distinct from instruction discriminators.
41#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)]
42#[repr(u8)]
43#[num_enum(error_type(
44    name = ProgramError,
45    constructor = SettlementAccount::unknown_discriminator,
46))]
47pub enum SettlementAccount {
48    OrderAccount = 128,
49    SettlementState = 129,
50}
51
52impl SettlementAccount {
53    pub const fn discriminator(self) -> u8 {
54        self as u8
55    }
56
57    fn unknown_discriminator(_: u8) -> ProgramError {
58        ProgramError::InvalidAccountData
59    }
60}
61
62/// Recover the discriminator from the first byte of the payload and the
63/// remaining bytes to parse.
64/// Returns `InvalidInstructionData` for an insufficient length or an
65/// unknown discriminator.
66pub fn recover_discriminator(
67    instruction_data: &[u8],
68) -> Result<(SettlementInstruction, &[u8]), ProgramError> {
69    let discriminator = instruction_data
70        .first()
71        .copied()
72        .ok_or(ProgramError::InvalidInstructionData)
73        .and_then(SettlementInstruction::try_from)?;
74    Ok((discriminator, &instruction_data[1..]))
75}
76
77/// Program-side errors surfaced by the settlement program.
78/// The discriminant value is the on-chain `ProgramError::Custom` code.
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80#[repr(u32)]
81pub enum SettlementError {
82    /// The `FinalizeSettle` included as input to `BeginSettle` isn't before
83    /// the actual `BeginSettle` index.
84    FinalizeBeforeInitialize = 0,
85    /// Another `BeginSettle`/`FinalizeSettle` of this program appears strictly
86    /// between this pair's bounds, nesting or overlapping two settlements.
87    BeginFinalizePairOverlap = 1,
88    /// The counterpart index points past the end of the transaction's
89    /// instruction list, so no instruction sits there.
90    MissingCounterpartInstruction = 2,
91    /// The instruction at the counterpart index belongs to a different program.
92    CounterpartIsExternal = 3,
93    /// The counterpart instruction's discriminator byte couldn't be recovered
94    /// from its data.
95    InvalidCounterpartDiscriminator = 4,
96    /// The counterpart instruction's own counterpart index couldn't be
97    /// recovered from its data.
98    InvalidCounterpartCounterpart = 5,
99    /// The counterpart's discriminator isn't the expected
100    /// `BeginSettle`/`FinalizeSettle` kind, or its counterpart index doesn't
101    /// point back at this instruction.
102    MismatchedCounterpartDiscriminator = 6,
103    /// `CreateOrder` instruction wasn't signed by the created `OrderIntent`
104    /// owner.
105    OwnerMismatch = 7,
106    /// An account was provided that cannot be derived from the seeds recognized by the program
107    AccountNotDerivable = 8,
108    /// `BeginSettle`'s order accounts aren't passed strictly increasing by
109    /// address.
110    OrdersNotStrictlyIncreasing = 9,
111    /// A `BeginSettle` sell token account doesn't match the
112    /// `sell_token_account` recorded in the order's intent.
113    SellTokenAccountMismatch = 10,
114    /// A `BeginSettle` sell token account isn't a valid SPL token account
115    /// (wrong data length or not owned by the token program).
116    SellTokenAccountInvalid = 11,
117    /// A `BeginSettle` sell token account's SPL owner isn't the order's intent
118    /// owner.
119    SellTokenOwnerMismatch = 12,
120    /// `BeginSettle`'s order-account count doesn't match the structure its
121    /// instruction data expects: `n` orders each contribute an order PDA and a
122    /// sell token account, plus one destination account per transfer.
123    AccountCountNotMatchingOrderCount = 13,
124    /// `BeginSettle` or `FinalizeSettle` was invoked via CPI rather than as a
125    /// top-level transaction instruction.
126    CalledViaCpi = 14,
127    /// A `BeginSettle` order has been cancelled by its owner and can no longer
128    /// be settled.
129    OrderCancelled = 15,
130    /// A `BeginSettle` order's `valid_to` lies in the past: the order has
131    /// expired and can no longer be settled.
132    OrderExpired = 16,
133    /// The transfer counts in `BeginSettle` don't sum to the number of transfer
134    /// amounts, so destinations and amounts can't be paired up exactly.
135    TransferCountMismatch = 17,
136    /// `BeginSettle`'s state account isn't the canonical settlement state PDA,
137    /// which must sign the pulls as the user's token delegate.
138    StateAccountMismatch = 18,
139    /// `FinalizeSettle`'s push-account count doesn't match its instruction
140    /// data: each push contributes a source buffer and a destination account,
141    /// so the count must be twice the number of push amounts.
142    AccountCountNotMatchingPushCount = 19,
143    /// `BeginSettle`: the number of pushes carried by the paired `FinalizeSettle`
144    /// doesn't equal the number of settled orders. Each order must be paid by
145    /// exactly one push.
146    SettledOrderPushCountMismatch = 20,
147    /// `BeginSettle`: a paired `FinalizeSettle` push doesn't send its proceeds
148    /// to the order's buy token account; its destination differs from the
149    /// `buy_token_account` in the order's intent.
150    PushDestinationMismatch = 21,
151    /// `FinalizeSettle`: a push doesn't draw funds from the canonical buffer
152    /// for its destination's mint.
153    PushSourceNotBuffer = 22,
154    /// `FinalizeSettle`: a push's destination isn't a valid SPL token account
155    /// (wrong data length or not owned by the token program), so its mint can't
156    /// be read to derive the buffer.
157    InvalidBuyTokenAccount = 23,
158    /// `BeginSettle`: a settled order's executed price (`amount_out/amount_in`)
159    /// is worse than the order's limit price (`buy_amount/sell_amount`).
160    LimitPriceViolated = 24,
161    /// `BeginSettle`: an order's pull amounts sum to more than `u64::MAX`.
162    PullAmountOverflow = 25,
163    /// `BeginSettle`: filling this order would consume more tokens than the
164    /// maximum the user is willing to trade on this intent.
165    /// Sell: `amount_in > sell_amount`; buy: `amount_out > buy_amount`.
166    FillExceedsOrderAmount = 26,
167    /// `BeginSettle`: a non-`partially_fillable` order isn't filled exactly to
168    /// its amount (either under- or over-filled).
169    /// Sell: `amount_in != sell_amount`; buy: total `amount_out != buy_amount`.
170    OrderNotExactlyFilled = 27,
171    /// `BeginSettle`: the order's cumulative `amount_withdrawn` would exceed
172    /// `u64::MAX` once this settlement's pulls are added.
173    AmountWithdrawnOverflow = 28,
174    /// `BeginSettle`: the order's cumulative `amount_received` would exceed
175    /// `u64::MAX` once this settlement's push is added.
176    AmountReceivedOverflow = 29,
177    /// `ReclaimOrder` was called before the order's `valid_to` has elapsed.
178    OrderNotExpired = 30,
179    /// `ReclaimOrder`'s `reclaim_recipient` account doesn't match the
180    /// `created_by` address recorded in the order.
181    ReclaimRecipientMismatch = 31,
182}
183
184impl From<SettlementError> for u32 {
185    fn from(e: SettlementError) -> Self {
186        e as u32
187    }
188}
189
190impl From<SettlementError> for solana_program_error::ProgramError {
191    fn from(e: SettlementError) -> Self {
192        Self::Custom(e.into())
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn rejects_empty_payload() {
202        assert_eq!(
203            recover_discriminator(&[]),
204            Err(ProgramError::InvalidInstructionData),
205        );
206    }
207
208    #[test]
209    fn rejects_unknown_discriminator() {
210        // 42 is outside the set of valid discriminators.
211        assert_eq!(
212            recover_discriminator(&[42]),
213            Err(ProgramError::InvalidInstructionData),
214        );
215    }
216
217    #[test]
218    fn forwards_trailing_bytes() {
219        assert!(matches!(
220            recover_discriminator(&[
221                SettlementInstruction::BeginSettle.discriminator(),
222                42 // unused
223            ]),
224            Ok((SettlementInstruction::BeginSettle, [42])),
225        ));
226    }
227
228    #[test]
229    fn settlement_instruction_try_from_partitions_all_bytes() {
230        for i in u8::MIN..=u8::MAX {
231            match SettlementInstruction::try_from(i) {
232                Ok(ix) => assert_eq!(ix as u8, i),
233                Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData),
234            }
235        }
236    }
237
238    #[test]
239    fn settlement_instruction_try_from_matches_begin_settle() {
240        assert_eq!(
241            SettlementInstruction::try_from(0),
242            Ok(SettlementInstruction::BeginSettle)
243        );
244    }
245
246    #[test]
247    fn settlement_account_try_from_partitions_all_bytes() {
248        for i in u8::MIN..=u8::MAX {
249            match SettlementAccount::try_from(i) {
250                Ok(account) => assert_eq!(account as u8, i),
251                Err(err) => assert_eq!(err, ProgramError::InvalidAccountData),
252            }
253        }
254    }
255
256    #[test]
257    fn settlement_account_discriminators_are_distinct() {
258        assert_ne!(
259            SettlementAccount::OrderAccount.discriminator(),
260            SettlementAccount::SettlementState.discriminator(),
261        );
262    }
263}