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}