Skip to main content

cow_composable/
gat.rs

1//! `GoodAfterTime` (`GAT`) conditional order handler.
2//!
3//! A GAT order is not valid before `start_time` and must be executed before
4//! `tx_deadline`.  It wraps a standard `GPv2Order` and adds time-gating.
5//!
6//! The GAT handler uses the same contract address as the `TWAP` handler:
7//! `0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5`.
8
9use alloy_primitives::{Address, B256, U256, keccak256};
10
11use cow_errors::CowError;
12
13use super::types::{ConditionalOrderParams, GpV2OrderStruct, TWAP_HANDLER_ADDRESS};
14
15// ── Handler address ───────────────────────────────────────────────────────────
16
17/// `GoodAfterTime` handler contract address.
18///
19/// This is the same address as the `TWAP` handler —
20/// `0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5`.
21pub const GAT_HANDLER_ADDRESS: Address = TWAP_HANDLER_ADDRESS;
22
23// ── GatData ───────────────────────────────────────────────────────────────────
24
25/// Parameters for a `GoodAfterTime` (`GAT`) conditional order.
26///
27/// A `GAT` order wraps a regular `GPv2Order` and adds two time constraints:
28/// - The order is **not** valid before `start_time`.
29/// - The settling transaction must be submitted before `tx_deadline` to be accepted by the handler.
30#[derive(Debug, Clone)]
31pub struct GatData {
32    /// The underlying `GPv2Order` that will be submitted when the time window
33    /// is active.
34    pub order: GpV2OrderStruct,
35    /// Unix timestamp before which the order is not valid.
36    pub start_time: u32,
37    /// Absolute Unix timestamp by which the settlement transaction must be
38    /// included (`block.timestamp ≤ tx_deadline`).
39    pub tx_deadline: u32,
40}
41
42// ── GatOrder ──────────────────────────────────────────────────────────────────
43
44/// A `GoodAfterTime` conditional order ready for submission to `ComposableCow`.
45#[derive(Debug, Clone)]
46pub struct GatOrder {
47    /// `GAT` configuration.
48    pub data: GatData,
49    /// 32-byte salt uniquely identifying this order instance.
50    pub salt: B256,
51}
52
53impl GatOrder {
54    /// Create a new `GAT` order with a deterministic salt derived from the
55    /// order parameters.
56    ///
57    /// # Returns
58    ///
59    /// A [`GatOrder`] whose salt is the `keccak256` hash of the key order
60    /// fields (`sell_token`, `buy_token`, `sell_amount`, `start_time`,
61    /// `tx_deadline`).
62    #[must_use]
63    pub fn new(data: GatData) -> Self {
64        let salt = deterministic_salt(&data);
65        Self { data, salt }
66    }
67
68    /// Create a `GAT` order with an explicit salt.
69    ///
70    /// # Arguments
71    ///
72    /// * `data` - The [`GatData`] containing the underlying `GPv2Order` and time-gating parameters.
73    /// * `salt` - A caller-chosen 32-byte salt that uniquely identifies this order instance
74    ///   on-chain.
75    ///
76    /// # Returns
77    ///
78    /// A [`GatOrder`] using the provided `salt` verbatim.
79    #[must_use]
80    pub const fn with_salt(data: GatData, salt: B256) -> Self {
81        Self { data, salt }
82    }
83
84    /// Returns `true` if the order parameters are logically valid:
85    ///
86    /// - `sell_amount > 0`
87    /// - `buy_amount > 0`
88    /// - `sell_token != buy_token`
89    /// - `start_time <= tx_deadline`
90    #[must_use]
91    pub fn is_valid(&self) -> bool {
92        let d = &self.data;
93        let o = &d.order;
94        !o.sell_amount.is_zero() &&
95            !o.buy_amount.is_zero() &&
96            o.sell_token != o.buy_token &&
97            d.start_time <= d.tx_deadline
98    }
99
100    /// Build the on-chain [`ConditionalOrderParams`] for this order.
101    ///
102    /// # Returns
103    ///
104    /// A [`ConditionalOrderParams`] with `handler` set to
105    /// [`GAT_HANDLER_ADDRESS`], the stored `salt`, and the ABI-encoded
106    /// `static_input` bytes.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`CowError::AppData`] if ABI encoding fails.
111    pub fn to_params(&self) -> Result<ConditionalOrderParams, CowError> {
112        Ok(ConditionalOrderParams {
113            handler: GAT_HANDLER_ADDRESS,
114            salt: self.salt,
115            static_input: encode_gat_struct(&self.data),
116        })
117    }
118
119    /// Returns a reference to the `GAT` data.
120    ///
121    /// # Returns
122    ///
123    /// A shared reference to the inner [`GatData`], giving access to the
124    /// underlying `GPv2Order`, `start_time`, and `tx_deadline`.
125    #[must_use]
126    pub const fn data_ref(&self) -> &GatData {
127        &self.data
128    }
129
130    /// Returns a reference to the 32-byte salt.
131    ///
132    /// # Returns
133    ///
134    /// A shared reference to the [`B256`] salt that uniquely identifies this
135    /// order instance on-chain.
136    #[must_use]
137    pub const fn salt_ref(&self) -> &B256 {
138        &self.salt
139    }
140}
141
142// ── ABI encoding ──────────────────────────────────────────────────────────────
143
144/// ABI-encode a [`GatData`] struct into the `staticInput` bytes expected by
145/// the on-chain `GoodAfterTime` handler.
146///
147/// Encoding layout (each word is 32 bytes, big-endian):
148/// - Words 0-11: `GpV2OrderStruct` fields (`sell_token`, `buy_token`, `receiver`, `sell_amount`,
149///   `buy_amount`, `valid_to`, `app_data`, `fee_amount`, `kind`, `partially_fillable`,
150///   `sell_token_balance`, `buy_token_balance`)
151/// - Word 12: `start_time` (uint32)
152/// - Word 13: `tx_deadline` (uint32)
153///
154/// Total: 14 × 32 = 448 bytes.
155///
156/// ```
157/// use alloy_primitives::{Address, B256, U256};
158/// use cow_composable::{GatData, GpV2OrderStruct, encode_gat_struct};
159///
160/// let order = GpV2OrderStruct {
161///     sell_token: Address::ZERO,
162///     buy_token: Address::ZERO,
163///     receiver: Address::ZERO,
164///     sell_amount: U256::from(1_000u64),
165///     buy_amount: U256::from(900u64),
166///     valid_to: 9_999_999,
167///     app_data: B256::ZERO,
168///     fee_amount: U256::ZERO,
169///     kind: B256::ZERO,
170///     partially_fillable: false,
171///     sell_token_balance: B256::ZERO,
172///     buy_token_balance: B256::ZERO,
173/// };
174/// let data = GatData { order, start_time: 1_000_000, tx_deadline: 2_000_000 };
175/// let encoded = encode_gat_struct(&data);
176/// assert_eq!(encoded.len(), 448);
177/// ```
178#[must_use]
179pub fn encode_gat_struct(d: &GatData) -> Vec<u8> {
180    let o = &d.order;
181    let mut buf = Vec::with_capacity(14 * 32);
182    // GPv2Order fields
183    buf.extend_from_slice(&pad_address(o.sell_token.as_slice()));
184    buf.extend_from_slice(&pad_address(o.buy_token.as_slice()));
185    buf.extend_from_slice(&pad_address(o.receiver.as_slice()));
186    buf.extend_from_slice(&u256_bytes(o.sell_amount));
187    buf.extend_from_slice(&u256_bytes(o.buy_amount));
188    buf.extend_from_slice(&u256_be(u64::from(o.valid_to)));
189    buf.extend_from_slice(o.app_data.as_slice());
190    buf.extend_from_slice(&u256_bytes(o.fee_amount));
191    buf.extend_from_slice(o.kind.as_slice());
192    buf.extend_from_slice(&bool_word(o.partially_fillable));
193    buf.extend_from_slice(o.sell_token_balance.as_slice());
194    buf.extend_from_slice(o.buy_token_balance.as_slice());
195    // GAT-specific fields
196    buf.extend_from_slice(&u256_be(u64::from(d.start_time)));
197    buf.extend_from_slice(&u256_be(u64::from(d.tx_deadline)));
198    buf
199}
200
201/// ABI-decode a 448-byte `staticInput` buffer into a [`GatData`].
202///
203/// # Errors
204///
205/// Returns [`CowError::AppData`] if `bytes` is shorter than 448 bytes.
206///
207/// ```
208/// use alloy_primitives::{Address, B256, U256};
209/// use cow_composable::{GatData, GpV2OrderStruct, decode_gat_static_input, encode_gat_struct};
210///
211/// let order = GpV2OrderStruct {
212///     sell_token: Address::repeat_byte(0x01),
213///     buy_token: Address::repeat_byte(0x02),
214///     receiver: Address::ZERO,
215///     sell_amount: U256::from(500u64),
216///     buy_amount: U256::from(400u64),
217///     valid_to: 1_234_567,
218///     app_data: B256::ZERO,
219///     fee_amount: U256::ZERO,
220///     kind: B256::ZERO,
221///     partially_fillable: false,
222///     sell_token_balance: B256::ZERO,
223///     buy_token_balance: B256::ZERO,
224/// };
225/// let data = GatData { order, start_time: 1_000_000, tx_deadline: 2_000_000 };
226/// let encoded = encode_gat_struct(&data);
227/// let decoded = decode_gat_static_input(&encoded).unwrap();
228/// assert_eq!(decoded.start_time, data.start_time);
229/// assert_eq!(decoded.tx_deadline, data.tx_deadline);
230/// assert_eq!(decoded.order.sell_amount, data.order.sell_amount);
231/// ```
232pub fn decode_gat_static_input(bytes: &[u8]) -> Result<GatData, CowError> {
233    if bytes.len() < 14 * 32 {
234        return Err(CowError::AppData(format!(
235            "GAT static input too short: {} bytes (need 448)",
236            bytes.len()
237        )));
238    }
239    let addr = |off: usize| -> Address {
240        let mut a = [0u8; 20];
241        a.copy_from_slice(&bytes[off + 12..off + 32]);
242        Address::new(a)
243    };
244    let u256 = |off: usize| -> U256 { U256::from_be_slice(&bytes[off..off + 32]) };
245    let u32v = |off: usize| -> u32 {
246        u32::from_be_bytes([bytes[off + 28], bytes[off + 29], bytes[off + 30], bytes[off + 31]])
247    };
248    let bool_v = |off: usize| -> bool { bytes[off + 31] != 0 };
249    let b256 = |off: usize| -> B256 {
250        let mut arr = [0u8; 32];
251        arr.copy_from_slice(&bytes[off..off + 32]);
252        B256::new(arr)
253    };
254
255    let order = GpV2OrderStruct {
256        sell_token: addr(0),
257        buy_token: addr(32),
258        receiver: addr(64),
259        sell_amount: u256(96),
260        buy_amount: u256(128),
261        valid_to: u32v(160),
262        app_data: b256(192),
263        fee_amount: u256(224),
264        kind: b256(256),
265        partially_fillable: bool_v(288),
266        sell_token_balance: b256(320),
267        buy_token_balance: b256(352),
268    };
269
270    Ok(GatData { order, start_time: u32v(384), tx_deadline: u32v(416) })
271}
272
273// ── Private helpers ───────────────────────────────────────────────────────────
274
275/// Left-pad an address (or shorter slice) to 32 bytes.
276///
277/// # Arguments
278///
279/// * `bytes` - A 20-byte (or shorter) slice to right-align within a 32-byte ABI word.
280///
281/// # Returns
282///
283/// A 32-byte array with the input placed in the last `bytes.len()` positions
284/// and the leading bytes zeroed.
285fn pad_address(bytes: &[u8]) -> [u8; 32] {
286    let mut out = [0u8; 32];
287    out[12..].copy_from_slice(bytes);
288    out
289}
290
291/// Convert a `U256` to its 32-byte big-endian representation.
292///
293/// # Arguments
294///
295/// * `v` - The [`U256`] value to convert.
296///
297/// # Returns
298///
299/// A 32-byte array containing the big-endian encoding of `v`.
300const fn u256_bytes(v: U256) -> [u8; 32] {
301    v.to_be_bytes()
302}
303
304/// Encode a `u64` as a 32-byte big-endian ABI word.
305///
306/// # Arguments
307///
308/// * `v` - The `u64` value to encode.
309///
310/// # Returns
311///
312/// A 32-byte array with the big-endian `u64` right-aligned (bytes 24..32)
313/// and the leading 24 bytes zeroed.
314fn u256_be(v: u64) -> [u8; 32] {
315    let mut out = [0u8; 32];
316    out[24..].copy_from_slice(&v.to_be_bytes());
317    out
318}
319
320/// Encode a `bool` as a 32-byte ABI word (0 or 1).
321///
322/// # Arguments
323///
324/// * `v` - The boolean value to encode.
325///
326/// # Returns
327///
328/// A 32-byte array where byte 31 is `1` when `v` is `true` and `0`
329/// otherwise; all other bytes are zero.
330const fn bool_word(v: bool) -> [u8; 32] {
331    let mut out = [0u8; 32];
332    out[31] = if v { 1 } else { 0 };
333    out
334}
335
336/// Derive a deterministic salt by hashing all GAT parameters.
337///
338/// # Arguments
339///
340/// * `d` - The [`GatData`] whose key fields (`sell_token`, `buy_token`, `sell_amount`,
341///   `start_time`, `tx_deadline`) are concatenated and hashed.
342///
343/// # Returns
344///
345/// A [`B256`] salt computed as `keccak256(sell_token || buy_token ||
346/// sell_amount || start_time || tx_deadline)`.
347fn deterministic_salt(d: &GatData) -> B256 {
348    let o = &d.order;
349    let mut buf = Vec::with_capacity(20 + 20 + 32 + 4 + 4);
350    buf.extend_from_slice(o.sell_token.as_slice());
351    buf.extend_from_slice(o.buy_token.as_slice());
352    buf.extend_from_slice(&u256_bytes(o.sell_amount));
353    buf.extend_from_slice(&d.start_time.to_be_bytes());
354    buf.extend_from_slice(&d.tx_deadline.to_be_bytes());
355    keccak256(&buf)
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    fn make_order() -> GpV2OrderStruct {
363        GpV2OrderStruct {
364            sell_token: Address::repeat_byte(0x01),
365            buy_token: Address::repeat_byte(0x02),
366            receiver: Address::ZERO,
367            sell_amount: U256::from(1_000u64),
368            buy_amount: U256::from(900u64),
369            valid_to: 9_999_999,
370            app_data: B256::ZERO,
371            fee_amount: U256::ZERO,
372            kind: B256::ZERO,
373            partially_fillable: false,
374            sell_token_balance: B256::ZERO,
375            buy_token_balance: B256::ZERO,
376        }
377    }
378
379    fn make_data() -> GatData {
380        GatData { order: make_order(), start_time: 1_000_000, tx_deadline: 2_000_000 }
381    }
382
383    #[test]
384    fn encode_is_448_bytes() {
385        let data = make_data();
386        let encoded = encode_gat_struct(&data);
387        assert_eq!(encoded.len(), 448);
388    }
389
390    #[test]
391    fn encode_decode_roundtrip() {
392        let data = make_data();
393        let encoded = encode_gat_struct(&data);
394        let decoded = decode_gat_static_input(&encoded).unwrap();
395        assert_eq!(decoded.start_time, data.start_time);
396        assert_eq!(decoded.tx_deadline, data.tx_deadline);
397        assert_eq!(decoded.order.sell_token, data.order.sell_token);
398        assert_eq!(decoded.order.buy_token, data.order.buy_token);
399        assert_eq!(decoded.order.sell_amount, data.order.sell_amount);
400        assert_eq!(decoded.order.buy_amount, data.order.buy_amount);
401        assert_eq!(decoded.order.valid_to, data.order.valid_to);
402        assert_eq!(decoded.order.partially_fillable, data.order.partially_fillable);
403    }
404
405    #[test]
406    fn is_valid_returns_true_for_valid_order() {
407        let order = GatOrder::new(make_data());
408        assert!(order.is_valid());
409    }
410
411    #[test]
412    fn is_valid_returns_false_for_same_tokens() {
413        let mut data = make_data();
414        data.order.buy_token = data.order.sell_token;
415        let order = GatOrder::new(data);
416        assert!(!order.is_valid());
417    }
418
419    #[test]
420    fn is_valid_returns_false_when_deadline_before_start() {
421        let mut data = make_data();
422        data.tx_deadline = data.start_time - 1;
423        let order = GatOrder::new(data);
424        assert!(!order.is_valid());
425    }
426
427    #[test]
428    fn to_params_sets_correct_handler() {
429        let order = GatOrder::new(make_data());
430        let params = order.to_params().unwrap();
431        assert_eq!(params.handler, GAT_HANDLER_ADDRESS);
432    }
433
434    #[test]
435    fn decode_too_short_returns_error() {
436        let result = decode_gat_static_input(&[0u8; 100]);
437        assert!(result.is_err());
438    }
439}