Skip to main content

cow_types/
unsigned_order.rs

1//! [`UnsignedOrder`] — the canonical `CoW` Protocol order struct before signing.
2//!
3//! This type used to live in `cow-signing::types`, but it is referenced by
4//! both `cow-signing` (for EIP-712 hashing) and `cow-settlement` (for encoder
5//! and trade building). Keeping it in an L2 sibling crate would require a
6//! sibling dependency between the two, so it has been pushed down to L1.
7//!
8//! The former convenience method `UnsignedOrder::hash` (which delegated to
9//! `cow_signing::order_hash`) has been dropped during the move. Call
10//! [`cow_signing::order_hash`](https://docs.rs/cow-signing) directly instead.
11
12use std::fmt;
13
14use alloy_primitives::{Address, B256, U256};
15
16use crate::{OrderKind, TokenBalance};
17
18/// An unsigned `CoW` Protocol order ready to be hashed and signed.
19#[derive(Debug, Clone)]
20pub struct UnsignedOrder {
21    /// Token to sell.
22    pub sell_token: Address,
23    /// Token to buy.
24    pub buy_token: Address,
25    /// Address that receives the bought tokens.
26    pub receiver: Address,
27    /// Amount of `sell_token` to sell (after fee, in atoms).
28    pub sell_amount: U256,
29    /// Minimum amount of `buy_token` to receive (in atoms).
30    pub buy_amount: U256,
31    /// Order expiry as Unix timestamp.
32    pub valid_to: u32,
33    /// App-data hash (`bytes32`).
34    pub app_data: B256,
35    /// Protocol fee included in `sell_amount` (in atoms).
36    pub fee_amount: U256,
37    /// Sell or buy direction.
38    pub kind: OrderKind,
39    /// Whether the order may be partially filled.
40    pub partially_fillable: bool,
41    /// Source of sell funds.
42    pub sell_token_balance: TokenBalance,
43    /// Destination of buy funds.
44    pub buy_token_balance: TokenBalance,
45}
46
47impl UnsignedOrder {
48    /// Construct a **sell** order with defaults: ERC-20 balances, `fee_amount = 0`,
49    /// `app_data = B256::ZERO`, `valid_to = 0`, `receiver = Address::ZERO`.
50    ///
51    /// Use the builder methods to override any field before signing.
52    ///
53    /// # Arguments
54    ///
55    /// * `sell_token` - Address of the token to sell.
56    /// * `buy_token` - Address of the token to buy.
57    /// * `sell_amount` - Amount of `sell_token` to sell (in atoms).
58    /// * `buy_amount` - Minimum amount of `buy_token` to receive (in atoms).
59    ///
60    /// # Returns
61    ///
62    /// A new [`UnsignedOrder`] with [`OrderKind::Sell`] and sensible defaults.
63    #[must_use]
64    pub const fn sell(
65        sell_token: Address,
66        buy_token: Address,
67        sell_amount: U256,
68        buy_amount: U256,
69    ) -> Self {
70        Self {
71            sell_token,
72            buy_token,
73            receiver: Address::ZERO,
74            sell_amount,
75            buy_amount,
76            valid_to: 0,
77            app_data: B256::ZERO,
78            fee_amount: U256::ZERO,
79            kind: OrderKind::Sell,
80            partially_fillable: false,
81            sell_token_balance: TokenBalance::Erc20,
82            buy_token_balance: TokenBalance::Erc20,
83        }
84    }
85
86    /// Construct a **buy** order with defaults: ERC-20 balances, `fee_amount = 0`,
87    /// `app_data = B256::ZERO`, `valid_to = 0`, `receiver = Address::ZERO`.
88    ///
89    /// # Arguments
90    ///
91    /// * `sell_token` - Address of the token to sell.
92    /// * `buy_token` - Address of the token to buy.
93    /// * `sell_amount` - Maximum amount of `sell_token` willing to sell (in atoms).
94    /// * `buy_amount` - Amount of `buy_token` to buy (in atoms).
95    ///
96    /// # Returns
97    ///
98    /// A new [`UnsignedOrder`] with [`OrderKind::Buy`] and sensible defaults.
99    #[must_use]
100    pub const fn buy(
101        sell_token: Address,
102        buy_token: Address,
103        sell_amount: U256,
104        buy_amount: U256,
105    ) -> Self {
106        Self {
107            sell_token,
108            buy_token,
109            receiver: Address::ZERO,
110            sell_amount,
111            buy_amount,
112            valid_to: 0,
113            app_data: B256::ZERO,
114            fee_amount: U256::ZERO,
115            kind: OrderKind::Buy,
116            partially_fillable: false,
117            sell_token_balance: TokenBalance::Erc20,
118            buy_token_balance: TokenBalance::Erc20,
119        }
120    }
121
122    /// Override the receiver address.
123    #[must_use]
124    pub const fn with_receiver(mut self, receiver: Address) -> Self {
125        self.receiver = receiver;
126        self
127    }
128
129    /// Set the order expiry as a Unix timestamp.
130    #[must_use]
131    pub const fn with_valid_to(mut self, valid_to: u32) -> Self {
132        self.valid_to = valid_to;
133        self
134    }
135
136    /// Set the app-data hash.
137    #[must_use]
138    pub const fn with_app_data(mut self, app_data: B256) -> Self {
139        self.app_data = app_data;
140        self
141    }
142
143    /// Override the fee amount (defaults to zero).
144    #[must_use]
145    pub const fn with_fee_amount(mut self, fee_amount: U256) -> Self {
146        self.fee_amount = fee_amount;
147        self
148    }
149
150    /// Allow partial fills.
151    #[must_use]
152    pub const fn with_partially_fillable(mut self) -> Self {
153        self.partially_fillable = true;
154        self
155    }
156
157    /// Override the sell-token balance source.
158    #[must_use]
159    pub const fn with_sell_token_balance(mut self, balance: TokenBalance) -> Self {
160        self.sell_token_balance = balance;
161        self
162    }
163
164    /// Override the buy-token balance destination.
165    #[must_use]
166    pub const fn with_buy_token_balance(mut self, balance: TokenBalance) -> Self {
167        self.buy_token_balance = balance;
168        self
169    }
170
171    /// Returns `true` if the order has expired at the given Unix timestamp.
172    ///
173    /// An order is expired when `timestamp > valid_to`.
174    ///
175    /// ```
176    /// use alloy_primitives::{Address, U256};
177    /// use cow_types::UnsignedOrder;
178    ///
179    /// let order = UnsignedOrder::sell(Address::ZERO, Address::ZERO, U256::ZERO, U256::ZERO)
180    ///     .with_valid_to(1_000_000);
181    /// assert!(!order.is_expired(999_999));
182    /// assert!(!order.is_expired(1_000_000)); // valid_to is inclusive
183    /// assert!(order.is_expired(1_000_001));
184    /// ```
185    #[must_use]
186    pub const fn is_expired(&self, timestamp: u64) -> bool {
187        timestamp > self.valid_to as u64
188    }
189
190    /// Returns `true` if this is a sell-direction order.
191    #[must_use]
192    pub const fn is_sell(&self) -> bool {
193        self.kind.is_sell()
194    }
195
196    /// Returns `true` if this is a buy-direction order.
197    #[must_use]
198    pub const fn is_buy(&self) -> bool {
199        self.kind.is_buy()
200    }
201
202    /// Returns `true` if a non-zero receiver address is set.
203    #[must_use]
204    pub fn has_custom_receiver(&self) -> bool {
205        !self.receiver.is_zero()
206    }
207
208    /// Returns `true` if a non-zero app-data hash is attached.
209    #[must_use]
210    pub fn has_app_data(&self) -> bool {
211        !self.app_data.is_zero()
212    }
213
214    /// Returns `true` if the fee amount is non-zero.
215    #[must_use]
216    pub fn has_fee(&self) -> bool {
217        !self.fee_amount.is_zero()
218    }
219
220    /// Returns `true` if this order allows partial fills.
221    #[must_use]
222    pub const fn is_partially_fillable(&self) -> bool {
223        self.partially_fillable
224    }
225
226    /// Returns the total token amount at stake: `sell_amount + buy_amount`.
227    ///
228    /// Uses saturating addition to avoid overflow on extreme values.
229    #[must_use]
230    pub const fn total_amount(&self) -> U256 {
231        self.sell_amount.saturating_add(self.buy_amount)
232    }
233}
234
235impl fmt::Display for UnsignedOrder {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        write!(f, "{} {:#x} → {:#x}", self.kind, self.sell_token, self.buy_token)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    fn base() -> UnsignedOrder {
246        UnsignedOrder::sell(Address::ZERO, Address::ZERO, U256::from(1_000u64), U256::from(500u64))
247    }
248
249    #[test]
250    fn with_partially_fillable_sets_flag() {
251        let order = base().with_partially_fillable();
252        assert!(order.is_partially_fillable());
253    }
254
255    #[test]
256    fn with_sell_token_balance_overrides_default() {
257        let order = base().with_sell_token_balance(TokenBalance::External);
258        assert_eq!(order.sell_token_balance, TokenBalance::External);
259    }
260
261    #[test]
262    fn with_buy_token_balance_overrides_default() {
263        let order = base().with_buy_token_balance(TokenBalance::Internal);
264        assert_eq!(order.buy_token_balance, TokenBalance::Internal);
265    }
266
267    #[test]
268    fn total_amount_saturates_on_overflow() {
269        // saturating_add must clamp to U256::MAX rather than panic.
270        let order = UnsignedOrder::sell(Address::ZERO, Address::ZERO, U256::MAX, U256::from(1u64));
271        assert_eq!(order.total_amount(), U256::MAX);
272    }
273
274    #[test]
275    fn total_amount_sums_sell_and_buy() {
276        let order =
277            UnsignedOrder::sell(Address::ZERO, Address::ZERO, U256::from(7u64), U256::from(11u64));
278        assert_eq!(order.total_amount(), U256::from(18u64));
279    }
280
281    #[test]
282    fn display_renders_kind_and_token_addresses() {
283        let order = base();
284        let rendered = format!("{order}");
285        assert!(rendered.contains("sell"));
286        assert!(rendered.contains("0x0000000000000000000000000000000000000000"));
287    }
288}