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}