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}