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