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!("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}