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!("FYp8R5K4B3B1Kfr7QuWzMz4TwoT7wptjYtxgCrY5sRXb");
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 TransferAuthority = 7,
28 AddSolver = 8,
29}
30
31impl SettlementInstruction {
32 pub fn discriminator(self) -> u8 {
33 self as u8
34 }
35
36 fn unknown_discriminator(_: u8) -> ProgramError {
37 ProgramError::InvalidInstructionData
38 }
39}
40
41/// A transferable authority stored in the state PDA.
42///
43/// The discriminant is the wire value carried by the authority-transfer
44/// instruction (see [`transfer_authority`](instruction::transfer_authority)).
45#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)]
46#[repr(u8)]
47#[num_enum(error_type(name = ProgramError, constructor = Role::unknown_role))]
48pub enum Role {
49 /// The account authorized to add and remove solvers and to transfer roles.
50 /// It is the highest authority: it may transfer any role.
51 Manager = 0,
52 /// The account authorized to close buffer accounts and reclaim their rent,
53 /// choosing where that rent goes.
54 ReclaimAuthority,
55}
56
57impl Role {
58 /// Every [`Role`] variant, in discriminant order.
59 pub const ALL: [Self; 2] = [Role::Manager, Role::ReclaimAuthority];
60
61 /// The single wire byte that selects this role in the authority-transfer
62 /// instruction.
63 pub fn discriminator(self) -> u8 {
64 self as u8
65 }
66
67 fn unknown_role(_: u8) -> ProgramError {
68 ProgramError::InvalidInstructionData
69 }
70}
71
72/// Identifies the account type a given account's data belongs to, via the
73/// single discriminator byte stored at its front. Starts at 128 to keep
74/// account discriminators visually distinct from instruction discriminators.
75#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)]
76#[repr(u8)]
77#[num_enum(error_type(
78 name = ProgramError,
79 constructor = SettlementAccount::unknown_discriminator,
80))]
81pub enum SettlementAccount {
82 OrderAccount = 128,
83 SettlementState = 129,
84}
85
86impl SettlementAccount {
87 pub const fn discriminator(self) -> u8 {
88 self as u8
89 }
90
91 fn unknown_discriminator(_: u8) -> ProgramError {
92 ProgramError::InvalidAccountData
93 }
94}
95
96/// Recover the discriminator from the first byte of the payload and the
97/// remaining bytes to parse.
98/// Returns `InvalidInstructionData` for an insufficient length or an
99/// unknown discriminator.
100pub fn recover_discriminator(
101 instruction_data: &[u8],
102) -> Result<(SettlementInstruction, &[u8]), ProgramError> {
103 let discriminator = instruction_data
104 .first()
105 .copied()
106 .ok_or(ProgramError::InvalidInstructionData)
107 .and_then(SettlementInstruction::try_from)?;
108 Ok((discriminator, &instruction_data[1..]))
109}
110
111/// Program-side errors surfaced by the settlement program.
112/// The discriminant value is the on-chain `ProgramError::Custom` code.
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114#[repr(u32)]
115pub enum SettlementError {
116 /// The `FinalizeSettle` included as input to `BeginSettle` isn't before
117 /// the actual `BeginSettle` index.
118 FinalizeBeforeInitialize = 0,
119 /// Another `BeginSettle`/`FinalizeSettle` of this program appears strictly
120 /// between this pair's bounds, nesting or overlapping two settlements.
121 BeginFinalizePairOverlap = 1,
122 /// The counterpart index points past the end of the transaction's
123 /// instruction list, so no instruction sits there.
124 MissingCounterpartInstruction = 2,
125 /// The instruction at the counterpart index belongs to a different program.
126 CounterpartIsExternal = 3,
127 /// The counterpart instruction's discriminator byte couldn't be recovered
128 /// from its data.
129 InvalidCounterpartDiscriminator = 4,
130 /// The counterpart instruction's own counterpart index couldn't be
131 /// recovered from its data.
132 InvalidCounterpartCounterpart = 5,
133 /// The counterpart's discriminator isn't the expected
134 /// `BeginSettle`/`FinalizeSettle` kind, or its counterpart index doesn't
135 /// point back at this instruction.
136 MismatchedCounterpartDiscriminator = 6,
137 /// `CreateOrder` instruction wasn't signed by the created `OrderIntent`
138 /// owner.
139 OwnerMismatch = 7,
140 /// An account was provided that cannot be derived from the seeds recognized by the program
141 AccountNotDerivable = 8,
142 /// `BeginSettle`'s order accounts aren't passed strictly increasing by
143 /// address.
144 OrdersNotStrictlyIncreasing = 9,
145 /// A `BeginSettle` sell token account doesn't match the
146 /// `sell_token_account` recorded in the order's intent.
147 SellTokenAccountMismatch = 10,
148 /// A `BeginSettle` sell token account isn't a valid SPL token account
149 /// (wrong data length or not owned by the token program).
150 SellTokenAccountInvalid = 11,
151 /// A `BeginSettle` sell token account's SPL owner isn't the order's intent
152 /// owner.
153 SellTokenOwnerMismatch = 12,
154 /// `BeginSettle`'s order-account count doesn't match the structure its
155 /// instruction data expects: `n` orders each contribute an order PDA and a
156 /// sell token account, plus one destination account per transfer.
157 AccountCountNotMatchingOrderCount = 13,
158 /// `BeginSettle` or `FinalizeSettle` was invoked via CPI rather than as a
159 /// top-level transaction instruction.
160 CalledViaCpi = 14,
161 /// A `BeginSettle` order has been cancelled by its owner and can no longer
162 /// be settled.
163 OrderCancelled = 15,
164 /// A `BeginSettle` order's `valid_to` lies in the past: the order has
165 /// expired and can no longer be settled.
166 OrderExpired = 16,
167 /// The transfer counts in `BeginSettle` don't sum to the number of transfer
168 /// amounts, so destinations and amounts can't be paired up exactly.
169 TransferCountMismatch = 17,
170 /// `BeginSettle`'s state account isn't the canonical settlement state PDA,
171 /// which must sign the pulls as the user's token delegate.
172 StateAccountMismatch = 18,
173 /// `FinalizeSettle`'s push-account count doesn't match its instruction
174 /// data: each push contributes a source buffer and a destination account,
175 /// so the count must be twice the number of push amounts.
176 AccountCountNotMatchingPushCount = 19,
177 /// `BeginSettle`: the number of pushes carried by the paired `FinalizeSettle`
178 /// doesn't equal the number of settled orders. Each order must be paid by
179 /// exactly one push.
180 SettledOrderPushCountMismatch = 20,
181 /// `BeginSettle`: a paired `FinalizeSettle` push doesn't send its proceeds
182 /// to the order's buy token account; its destination differs from the
183 /// `buy_token_account` in the order's intent.
184 PushDestinationMismatch = 21,
185 /// `BeginSettle`: a paired `FinalizeSettle` push doesn't draw funds from the
186 /// canonical buffer for the order's `buy_mint`.
187 PushSourceNotBuffer = 22,
188 /// `BeginSettle`: the OrderIntent `sell_token_account` holds a different
189 /// mint than the declared `sell_mint`.
190 SellMintMismatch = 23,
191 /// `BeginSettle`: a settled order's executed price (`amount_out/amount_in`)
192 /// is worse than the order's limit price (`buy_amount/sell_amount`).
193 LimitPriceViolated = 24,
194 /// `BeginSettle`: an order's pull amounts sum to more than `u64::MAX`.
195 PullAmountOverflow = 25,
196 /// `BeginSettle`: filling this order would consume more tokens than the
197 /// maximum the user is willing to trade on this intent.
198 /// Sell: `amount_in > sell_amount`; buy: `amount_out > buy_amount`.
199 FillExceedsOrderAmount = 26,
200 /// `BeginSettle`: a non-`partially_fillable` order isn't filled exactly to
201 /// its amount (either under- or over-filled).
202 /// Sell: `amount_in != sell_amount`; buy: total `amount_out != buy_amount`.
203 OrderNotExactlyFilled = 27,
204 /// `BeginSettle`: the order's cumulative `amount_withdrawn` would exceed
205 /// `u64::MAX` once this settlement's pulls are added.
206 AmountWithdrawnOverflow = 28,
207 /// `BeginSettle`: the order's cumulative `amount_received` would exceed
208 /// `u64::MAX` once this settlement's push is added.
209 AmountReceivedOverflow = 29,
210 /// `ReclaimOrder` was called on an order that has is not yet eligible for reclaim.
211 OrderNotReclaimable = 30,
212 /// `ReclaimOrder`'s `reclaim_recipient` account doesn't match the
213 /// `created_by` address recorded in the order.
214 ReclaimRecipientMismatch = 31,
215 /// `ReclaimBuffer`'s `reclaim_authority` account isn't a signer, or doesn't
216 /// match the `reclaim_authority` address recorded in the settlement state
217 /// PDA.
218 ReclaimAuthorityMismatch = 32,
219 /// A `ReclaimBuffer` `buffer_pda` doesn't sit at the canonical buffer PDA
220 /// derived from its paired `mint`.
221 ReclaimBufferNotCanonical = 33,
222 /// `TransferAuthority`'s signer is neither the manager nor the current
223 /// holder of the role being transferred, so it may not transfer it.
224 UnauthorizedAuthorityTransfer = 34,
225 /// `AddSolver`'s manager account isn't a signer, or doesn't match the
226 /// `manager` recorded in the settlement state PDA.
227 UnauthorizedSolverManagement = 35,
228 /// `AddSolver`'s solver is already in the state PDA's solver list.
229 SolverAlreadyExists = 36,
230 /// `BeginSettle`/`FinalizeSettle`'s solver account isn't a signer or isn't
231 /// in the state PDA's solver list, so it may not settle.
232 UnauthorizedSolver = 37,
233 /// A created order's intent isn't set with the `created_on_chain` flag corresponding
234 /// to the behavior of the invoked order creation instruction.
235 OrderCreatedOnChainMismatch = 38,
236}
237
238impl From<SettlementError> for u32 {
239 fn from(e: SettlementError) -> Self {
240 e as u32
241 }
242}
243
244impl From<SettlementError> for solana_program_error::ProgramError {
245 fn from(e: SettlementError) -> Self {
246 Self::Custom(e.into())
247 }
248}
249
250/// Test fixtures for building settlement values with stable, readable
251/// addresses. Exposed under the `test-fixtures` feature (and unconditionally
252/// for this crate's own `cargo test`) so other crates can reuse them.
253#[cfg(any(test, feature = "test-fixtures"))]
254pub mod fixtures {
255 use crate::Pubkey;
256
257 /// Deterministically generate a [`Pubkey`] by hashing a seed string, for
258 /// building fixtures with stable, readable addresses.
259 pub fn pubkey_from_seed(seed: &str) -> Pubkey {
260 Pubkey::new_from_array(solana_sha256_hasher::hash(seed.as_bytes()).to_bytes())
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn rejects_empty_payload() {
270 assert_eq!(
271 recover_discriminator(&[]),
272 Err(ProgramError::InvalidInstructionData),
273 );
274 }
275
276 #[test]
277 fn rejects_unknown_discriminator() {
278 // 42 is outside the set of valid discriminators.
279 assert_eq!(
280 recover_discriminator(&[42]),
281 Err(ProgramError::InvalidInstructionData),
282 );
283 }
284
285 #[test]
286 fn forwards_trailing_bytes() {
287 assert!(matches!(
288 recover_discriminator(&[
289 SettlementInstruction::BeginSettle.discriminator(),
290 42 // unused
291 ]),
292 Ok((SettlementInstruction::BeginSettle, [42])),
293 ));
294 }
295
296 #[test]
297 fn settlement_instruction_try_from_partitions_all_bytes() {
298 for i in u8::MIN..=u8::MAX {
299 match SettlementInstruction::try_from(i) {
300 Ok(ix) => assert_eq!(ix as u8, i),
301 Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData),
302 }
303 }
304 }
305
306 #[test]
307 fn settlement_instruction_try_from_matches_begin_settle() {
308 assert_eq!(
309 SettlementInstruction::try_from(0),
310 Ok(SettlementInstruction::BeginSettle)
311 );
312 }
313
314 #[test]
315 fn settlement_account_try_from_partitions_all_bytes() {
316 for i in u8::MIN..=u8::MAX {
317 match SettlementAccount::try_from(i) {
318 Ok(account) => assert_eq!(account as u8, i),
319 Err(err) => assert_eq!(err, ProgramError::InvalidAccountData),
320 }
321 }
322 }
323
324 #[test]
325 fn settlement_account_discriminators_are_distinct() {
326 assert_ne!(
327 SettlementAccount::OrderAccount.discriminator(),
328 SettlementAccount::SettlementState.discriminator(),
329 );
330 }
331
332 #[test]
333 fn role_try_from_partitions_all_bytes() {
334 for i in u8::MIN..=u8::MAX {
335 match Role::try_from(i) {
336 Ok(role) => assert_eq!(role as u8, i),
337 Err(err) => assert_eq!(err, ProgramError::InvalidInstructionData),
338 }
339 }
340 }
341
342 #[test]
343 fn role_try_from_matches_manager() {
344 assert_eq!(Role::try_from(0), Ok(Role::Manager));
345 }
346
347 #[test]
348 fn all_roles_lists_every_role_in_discriminator_order() {
349 // The roles `try_from` accepts, discovered independently of `Role::ALL`.
350 // The scan runs over ascending bytes, so this is every role that exists,
351 // in discriminant order.
352 let every_role: Vec<Role> = (u8::MIN..=u8::MAX)
353 .filter_map(|byte| Role::try_from(byte).ok())
354 .collect();
355
356 assert_eq!(Role::ALL.as_slice(), every_role.as_slice());
357 }
358}