signet_types/signing/
fill.rs

1use crate::agg::AggregateOrders;
2use crate::signing::{permit_signing_info, SignedPermitError, SigningError};
3use alloy::{
4    network::TransactionBuilder, primitives::Address, rpc::types::TransactionRequest,
5    signers::Signer, sol_types::SolCall,
6};
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9use signet_zenith::{
10    BundleHelper::{FillPermit2, IOrders},
11    RollupOrders::{fillPermit2Call, Output, Permit2Batch, TokenPermissions},
12};
13use std::{borrow::Cow, collections::HashMap};
14
15/// SignedFill type is constructed by Fillers to fill a batch of Orders.
16/// It represents the Orders' Outputs after they have been permit2-encoded and signed.
17///
18/// A SignedFill corresponds to the parameters for `fillPermit2` on the OrderDestination contract,
19/// and thus contains all necessary information to fill the Order.
20///
21/// SignedFill is an optional part of the SignetEthBundle type.
22/// Fillers sign & send bundles which contain Order initiations & fills.
23/// Filler bundles contain:
24/// - optionally, a host SignedFill (if any Orders contain host Outputs)
25/// - optionally, a rollup transaction that submits a SignedFill (if any Orders contain rollup Outputs)
26/// - rollup transactions that submit the SignedOrders
27///
28/// # Warning ⚠️
29/// A SignedFill *must* remain private until it is mined, as there is no guarantee
30/// that desired Order Inputs will be received in return for the Outputs offered by the signed Permit2Batch.
31/// SignetEthBundles are used to submit SignedFills because they *must* be submitted atomically
32/// with the corresponding SignedOrder(s) in order to claim the Inputs.
33/// It is important to use private transaction relays to send bundles containing SignedFill(s) to Builders.
34/// Bundles can be sent to a *trusted* Signet Node's `signet_sendBundle` endpoint.
35///
36/// TODO: Link to docs.
37#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
38pub struct SignedFill {
39    /// The permit batch.
40    #[serde(flatten)]
41    pub permit: Permit2Batch,
42    /// The desired outputs.
43    pub outputs: Vec<Output>,
44}
45
46impl SignedFill {
47    /// Creates a new signed fill.
48    pub const fn new(permit: Permit2Batch, outputs: Vec<Output>) -> Self {
49        Self { permit, outputs }
50    }
51
52    /// Check that this can be syntactically used as a fill.
53    ///
54    /// For it to be valid:
55    /// - Deadline must be in the future.
56    /// - The permits must exactly match the ordering, token, and amount of the outputs.
57    pub fn validate(&self, timestamp: u64) -> Result<(), SignedPermitError> {
58        let deadline = self.permit.permit.deadline.saturating_to::<u64>();
59        if timestamp > deadline {
60            return Err(SignedPermitError::DeadlinePassed { current: timestamp, deadline });
61        }
62
63        // ensure Permits exactly match Outputs
64        if self.outputs.len() != self.permit.permit.permitted.len() {
65            return Err(SignedPermitError::PermitMismatch);
66        }
67
68        for (output, permit) in self.outputs.iter().zip(self.permit.permit.permitted.iter()) {
69            // check that the token is the same
70            if output.token != permit.token {
71                return Err(SignedPermitError::PermitMismatch);
72            }
73            // check that the amount is exactly equal
74            if output.amount != permit.amount {
75                return Err(SignedPermitError::PermitMismatch);
76            }
77        }
78
79        Ok(())
80    }
81
82    /// Generate a TransactionRequest to `fill` the SignedFill.
83    pub fn to_fill_tx(&self, order_contract: Address) -> TransactionRequest {
84        // encode fill data
85        let fill_data =
86            fillPermit2Call { outputs: self.outputs.clone(), permit2: self.permit.clone() }
87                .abi_encode();
88
89        // construct fill tx request
90        TransactionRequest::default().with_input(fill_data).with_to(order_contract)
91    }
92}
93
94impl From<SignedFill> for FillPermit2 {
95    fn from(fill: SignedFill) -> Self {
96        FillPermit2 {
97            permit2: fill.permit.into(),
98            outputs: fill.outputs.into_iter().map(IOrders::Output::from).collect(),
99        }
100    }
101}
102
103impl From<&SignedFill> for FillPermit2 {
104    fn from(fill: &SignedFill) -> Self {
105        FillPermit2 {
106            permit2: (&fill.permit).into(),
107            outputs: fill.outputs.iter().map(IOrders::Output::from).collect(),
108        }
109    }
110}
111
112/// An UnsignedFill is a helper type used to easily transform an AggregateOrder into a single SignedFill with correct permit2 semantics.
113#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
114pub struct UnsignedFill<'a> {
115    /// The rollup chain id from which the Orders originated.
116    ru_chain_id: Option<u64>,
117    /// The set of Orders to fill. Multiple Orders can be aggregated into a single Fill,
118    /// but they MUST all originate on the same rollup chain indicated by `ru_chain_id`.
119    orders: Cow<'a, AggregateOrders>,
120    /// The deadline for the Fill, after which it cannot be mined.
121    deadline: Option<u64>,
122    /// The Permit2 nonce for the Fill, used to prevent replay attacks.
123    nonce: Option<u64>,
124    /// The (chain_id, order_contract_address) for each target chain on which Fills will be submitted.
125    target_chains: HashMap<u64, Address>,
126}
127
128impl<'a> From<&'a AggregateOrders> for UnsignedFill<'a> {
129    fn from(orders: &'a AggregateOrders) -> Self {
130        UnsignedFill::new(orders)
131    }
132}
133
134impl<'a> UnsignedFill<'a> {
135    /// Get a new UnsignedFill from a set of AggregateOrders.
136    pub fn new(orders: &'a AggregateOrders) -> Self {
137        Self {
138            ru_chain_id: None,
139            orders: orders.into(),
140            deadline: None,
141            nonce: None,
142            target_chains: HashMap::new(),
143        }
144    }
145
146    /// Add a Permit2 nonce to the UnsignedFill.
147    pub fn with_nonce(self, nonce: u64) -> Self {
148        Self { nonce: Some(nonce), ..self }
149    }
150
151    /// Add a deadline to the UnsignedFill, after which it cannot be mined.
152    pub fn with_deadline(self, deadline: u64) -> Self {
153        Self { deadline: Some(deadline), ..self }
154    }
155
156    /// Add the rollup chain id to the UnsignedFill.
157    /// This is the rollup chain id from which the Orders originated,
158    /// to which the Fill should be credited.
159    /// MUST call this before signing, cannot be inferred.
160    pub fn with_ru_chain_id(self, ru_chain_id: u64) -> Self {
161        Self { ru_chain_id: Some(ru_chain_id), ..self }
162    }
163
164    /// Add the chain id and Order contract address to the UnsignedFill.
165    pub fn with_chain(mut self, chain_id: u64, order_contract_address: Address) -> Self {
166        self.target_chains.insert(chain_id, order_contract_address);
167        self
168    }
169
170    /// Sign the UnsignedFill, generating a SignedFill for each target chain.
171    /// Use if Filling Orders with the same signing key on every chain.
172    pub async fn sign<S: Signer>(
173        &self,
174        signer: &S,
175    ) -> Result<HashMap<u64, SignedFill>, SigningError> {
176        let mut fills = HashMap::new();
177
178        // loop through each target chain and sign the fills
179        for target_chain_id in self.orders.target_chain_ids() {
180            let signed_fill = self.sign_for(target_chain_id, signer).await?;
181            fills.insert(target_chain_id, signed_fill);
182        }
183
184        // return the fills
185        Ok(fills)
186    }
187
188    /// Sign the UnsignedFill for a specific target chain.
189    /// Use if Filling Orders with different signing keys on respective target chains.
190    /// # Warning ⚠️
191    /// *All* Outputs MUST be filled on all target chains, else the Order Inputs will not be transferred on the rollup.
192    /// Take care when using this function to produce SignedFills for every target chain.
193    pub async fn sign_for<S: Signer>(
194        &self,
195        target_chain_id: u64,
196        signer: &S,
197    ) -> Result<SignedFill, SigningError> {
198        let now = Utc::now();
199        // if nonce is are None, populate it as the current timestamp in microseconds
200        let nonce = self.nonce.unwrap_or(now.timestamp_micros() as u64);
201        // if deadline is None, populate it as now + 12 seconds (can only mine within the current block)
202        let deadline = self.deadline.unwrap_or(now.timestamp() as u64 + 12);
203
204        // get the target order address
205        let target_order_address = self
206            .target_chains
207            .get(&target_chain_id)
208            .ok_or(SigningError::MissingOrderContract(target_chain_id))?;
209
210        // get the rollup chain id, or throw an error if not set
211        let ru_chain_id = self.ru_chain_id.ok_or(SigningError::MissingRollupChainId)?;
212
213        // get the outputs for the target chain from the AggregateOrders
214        let outputs = self.orders.outputs_for(target_chain_id, ru_chain_id);
215        // generate the permitted tokens from the Outputs
216        let permitted: Vec<TokenPermissions> = outputs.iter().map(Into::into).collect();
217
218        // generate the permit2 signing info
219        let permit = permit_signing_info(
220            outputs,
221            permitted,
222            deadline,
223            nonce,
224            target_chain_id,
225            *target_order_address,
226        );
227
228        // sign it
229        let signature = signer.sign_hash(&permit.signing_hash).await?;
230
231        // return as a SignedFill
232        Ok(SignedFill {
233            permit: Permit2Batch {
234                permit: permit.permit,
235                owner: signer.address(),
236                signature: signature.as_bytes().into(),
237            },
238            outputs: permit.outputs,
239        })
240    }
241}