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}