alloy_zksync/provider/
deposit.rs

1//! Implementation of the deposit logic.
2
3use crate::{
4    contracts::{
5        common::erc20::{ERC20, encode_token_data_for_bridge},
6        l1::{
7            bridge_hub::{
8                Bridgehub::{self},
9                L2TransactionRequestDirect, L2TransactionRequestTwoBridges,
10            },
11            l1_bridge::{L1Bridge, encode_deposit_token_calldata},
12        },
13        l2::l2_bridge::encode_finalize_deposit_calldata,
14    },
15    network::{Zksync, transaction_request::TransactionRequest},
16    provider::{
17        L1CommunicationError, ZksyncProvider, l1_transaction_receipt::L1TransactionReceipt,
18    },
19    utils::{ETHER_L1_ADDRESS, apply_l1_to_l2_alias},
20};
21use alloy::{
22    eips::eip1559::Eip1559Estimation,
23    network::{Ethereum, NetworkWallet, TransactionBuilder},
24    primitives::{Address, Bytes, U256},
25    providers::{WalletProvider, utils::Eip1559Estimator},
26    rpc::types::eth::TransactionRequest as L1TransactionRequest,
27};
28use std::str::FromStr;
29
30pub const REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT: u64 = 800;
31
32/// Type for deposit request.
33/// This type only stores the required information for the deposit, while the deposit itself
34/// is performed via [`DepositExecutor`].
35#[derive(Clone, Debug)]
36pub struct DepositRequest {
37    /// Amount to deposit in Wei.
38    pub amount: U256,
39    /// Receiver of deposited assets. If None, the sender address will be used as a receiver.
40    pub receiver: Option<Address>,
41    /// L1 token address to deposit.
42    pub token: Address,
43    /// Bridge address for the deposit. If None, default shared bridge will be used.
44    pub bridge_address: Option<Address>,
45    /// Gas per pubdata limit to use in initiated transactions. If None,
46    /// REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT will be used.
47    pub gas_per_pubdata_limit: U256,
48    /// Enable or disable automatic submission of ERC20 approval transactions
49    /// if the allowance is not sufficient.
50    pub auto_approval: bool,
51}
52
53impl DepositRequest {
54    /// Initiates a new deposit request.
55    pub fn new(amount: U256) -> Self {
56        Self {
57            amount,
58            receiver: None,
59            token: ETHER_L1_ADDRESS,
60            bridge_address: None,
61            gas_per_pubdata_limit: U256::from(REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT),
62            auto_approval: true,
63        }
64    }
65
66    /// Returns the amount to deposit.
67    pub fn amount(&self) -> &U256 {
68        &self.amount
69    }
70
71    /// Sets the receiver for the deposit.
72    pub fn with_receiver(mut self, address: Address) -> Self {
73        self.receiver = Some(address);
74        self
75    }
76
77    /// Sets the token address for the deposit.
78    pub fn with_token(mut self, token: Address) -> Self {
79        self.token = token;
80        self
81    }
82
83    /// Sets the gas per pubdata limit for the transaction.
84    pub fn with_gas_per_pubdata_limit(mut self, value: U256) -> Self {
85        self.gas_per_pubdata_limit = value;
86        self
87    }
88
89    /// Sets the bridge address.
90    pub fn with_bridge_address(mut self, bridge_address: Address) -> Self {
91        self.bridge_address = Some(bridge_address);
92        self
93    }
94
95    /// Enables or disables auto-approval for ERC20 tokens.
96    pub fn with_auto_approval(mut self, auto_approval: bool) -> Self {
97        self.auto_approval = auto_approval;
98        self
99    }
100}
101
102#[derive(Clone, Debug, Copy)]
103struct FeeParams {
104    max_fee_per_gas: u128,
105    max_priority_fee_per_gas: u128,
106}
107
108#[derive(Clone, Debug, Copy)]
109struct BridgeL2TxFeeParams {
110    pub gas_limit: U256,
111    pub tx_base_cost: U256,
112}
113
114#[derive(Clone, Debug, Copy)]
115struct BridgeAddresses {
116    pub l1_bridge_address: Address,
117    pub l2_bridge_address: Address,
118}
119
120/// Scales the gas limit to ensure the transaction will be accepted.
121// Gas limit scaling logic is taken from the JS SDK:
122// https://github.com/zksync-sdk/zksync-ethers/blob/64763688d1bb5cee4a4c220c3841b803c74b0d05/src/utils.ts#L1451
123pub fn scale_l1_gas_limit(l1_gas_limit: u64) -> u64 {
124    /// Numerator used in scaling the gas limit to ensure acceptance of `L1->L2` transactions.
125    /// This constant is part of a coefficient calculation to adjust the gas limit to account for variations
126    /// in the SDK estimation, ensuring the transaction will be accepted.
127    const L1_FEE_ESTIMATION_COEF_NUMERATOR: u64 = 12;
128
129    /// Denominator used in scaling the gas limit to ensure acceptance of `L1->L2` transactions.
130    /// This constant is part of a coefficient calculation to adjust the gas limit to account for variations
131    /// in the SDK estimation, ensuring the transaction will be accepted.
132    const L1_FEE_ESTIMATION_COEF_DENOMINATOR: u64 = 10;
133    l1_gas_limit * L1_FEE_ESTIMATION_COEF_NUMERATOR / L1_FEE_ESTIMATION_COEF_DENOMINATOR
134}
135
136/// Type that handles deposit logic for various scenarios: deposit ETH, ERC20 etc.
137pub struct DepositExecutor<'a, P1, P2>
138where
139    P1: alloy::providers::Provider<Ethereum>,
140    P2: ZksyncProvider + WalletProvider<Zksync> + ?Sized,
141{
142    l1_provider: &'a P1,
143    l2_provider: &'a P2,
144    request: &'a DepositRequest,
145}
146
147impl<'a, P1, P2> DepositExecutor<'a, P1, P2>
148where
149    P1: alloy::providers::Provider<Ethereum>,
150    P2: ZksyncProvider + WalletProvider<Zksync> + ?Sized,
151{
152    /// Prepares an executor for a particular deposit request.
153    pub fn new(l1_provider: &'a P1, l2_provider: &'a P2, request: &'a DepositRequest) -> Self {
154        DepositExecutor {
155            l1_provider,
156            l2_provider,
157            request,
158        }
159    }
160
161    async fn get_bridge_addresses_for_deposit(
162        &self,
163        l2_chain_id: U256,
164    ) -> Result<(Address, Address), L1CommunicationError> {
165        let (l1_bridge_address, l2_bridge_address) = match self.request.bridge_address {
166            Some(l1_bridge_address) => {
167                let l1_bridge = L1Bridge::new(l1_bridge_address, self.l1_provider);
168                let l2_bridge_address = l1_bridge
169                    .l2BridgeAddress(l2_chain_id)
170                    .call()
171                    .await
172                    .map_err(|_| {
173                        L1CommunicationError::Custom("Error while getting L2 bridge address.")
174                    })?;
175                (l1_bridge_address, l2_bridge_address)
176            }
177            None => {
178                let bridge_addresses =
179                    self.l2_provider.get_bridge_contracts().await.map_err(|_| {
180                        L1CommunicationError::Custom(
181                            "Error occurred while fetching bridge contracts.",
182                        )
183                    })?;
184                (
185                    bridge_addresses.l1_shared_default_bridge.ok_or(
186                        L1CommunicationError::Custom(
187                            "L1 shared default bridge is not defined for the chain and bridge address is not specified in the deposit request.",
188                        ),
189                    )?,
190                    bridge_addresses.l2_shared_default_bridge.ok_or(
191                        L1CommunicationError::Custom(
192                            "L2 shared default bridge is not defined for the chain.",
193                        ),
194                    )?,
195                )
196            }
197        };
198        Ok((l1_bridge_address, l2_bridge_address))
199    }
200
201    async fn get_l1_fee_params(&self) -> Result<FeeParams, L1CommunicationError> {
202        let max_priority_fee_per_gas = self
203            .l1_provider
204            .get_max_priority_fee_per_gas()
205            .await
206            .map_err(|_| {
207                L1CommunicationError::Custom(
208                    "Error occurred while fetching L1 max_priority_fee_per_gas.",
209                )
210            })?;
211        // fees adjustment is taken from the JS SDK:
212        // https://github.com/zksync-sdk/zksync-ethers/blob/64763688d1bb5cee4a4c220c3841b803c74b0d05/src/adapters.ts#L2069
213        let base_l1_fees_data = self
214            .l1_provider
215            .estimate_eip1559_fees_with(Eip1559Estimator::new(|base_fee_per_gas, _| {
216                Eip1559Estimation {
217                    max_fee_per_gas: base_fee_per_gas * 3 / 2,
218                    max_priority_fee_per_gas: 0,
219                }
220            }))
221            .await
222            .map_err(|_| {
223                L1CommunicationError::Custom("Error occurred while estimating L1 base fees.")
224            })?;
225        let max_fee_per_gas = base_l1_fees_data.max_fee_per_gas + max_priority_fee_per_gas;
226
227        Ok(FeeParams {
228            max_fee_per_gas,
229            max_priority_fee_per_gas,
230        })
231    }
232
233    async fn get_l1_tx_gas_limit(
234        &self,
235        tx_request: &L1TransactionRequest,
236    ) -> Result<u64, L1CommunicationError> {
237        let l1_tx_gas_estimation = self
238            .l1_provider
239            .estimate_gas(tx_request.clone())
240            .await
241            .map_err(|_| {
242                L1CommunicationError::Custom(
243                    "Error occurred while estimating gas limit for the L1 transaction.",
244                )
245            })?;
246        let l1_gas_limit = scale_l1_gas_limit(l1_tx_gas_estimation);
247        Ok(l1_gas_limit)
248    }
249
250    async fn get_bridge_l2_tx_fee_params<P>(
251        &self,
252        bridge_hub_contract: &Bridgehub::BridgehubInstance<&P>,
253        l1_to_l2_tx: TransactionRequest,
254        l2_chain_id: U256,
255        fee_params: &FeeParams,
256    ) -> Result<BridgeL2TxFeeParams, L1CommunicationError>
257    where
258        P: alloy::providers::Provider<Ethereum>,
259    {
260        let gas_limit = self
261            .l2_provider
262            .estimate_gas_l1_to_l2(l1_to_l2_tx)
263            .await
264            .map_err(|_| {
265                L1CommunicationError::Custom(
266                    "Error occurred while estimating gas for L1 -> L2 transaction.",
267                )
268            })?;
269
270        let tx_base_cost = bridge_hub_contract
271            .l2TransactionBaseCost(
272                l2_chain_id,
273                U256::from(fee_params.max_fee_per_gas),
274                gas_limit,
275                self.request.gas_per_pubdata_limit,
276            )
277            .call()
278            .await
279            .map_err(|_| {
280                L1CommunicationError::Custom(
281                    "Error occurred while estimating L2 transaction base cost.",
282                )
283            })?;
284        Ok(BridgeL2TxFeeParams {
285            gas_limit,
286            tx_base_cost,
287        })
288    }
289
290    async fn get_l1_deposit_tx(
291        &self,
292        sender: Address,
293        receiver: Address,
294        bridge_addresses: Option<BridgeAddresses>,
295        l2_chain_id: U256,
296        fee_params: &FeeParams,
297    ) -> Result<L1TransactionRequest, L1CommunicationError> {
298        let bridge_hub_contract_address = self
299            .l2_provider
300            .get_bridgehub_contract()
301            .await
302            .map_err(|_| {
303                L1CommunicationError::Custom(
304                    "Error occurred while fetching the bridge hub contract address.",
305                )
306            })?
307            .unwrap();
308        let bridge_hub_contract = Bridgehub::new(bridge_hub_contract_address, self.l1_provider);
309
310        let l1_tx_request = if self.request.token == ETHER_L1_ADDRESS {
311            let l2_tx_fee = self
312                .get_bridge_l2_tx_fee_params(
313                    &bridge_hub_contract,
314                    TransactionRequest::default()
315                        .with_from(sender)
316                        .with_to(receiver)
317                        .with_value(self.request.amount)
318                        .with_gas_per_pubdata(self.request.gas_per_pubdata_limit)
319                        .with_input(Bytes::from("0x")),
320                    l2_chain_id,
321                    fee_params,
322                )
323                .await?;
324
325            let l1_value = l2_tx_fee.tx_base_cost + self.request.amount;
326            bridge_hub_contract
327                .requestL2TransactionDirect(L2TransactionRequestDirect {
328                    chainId: l2_chain_id,
329                    mintValue: l1_value,
330                    l2Contract: receiver,
331                    l2Value: self.request.amount,
332                    l2Calldata: Bytes::from_str("0x").unwrap(),
333                    l2GasLimit: l2_tx_fee.gas_limit,
334                    l2GasPerPubdataByteLimit: self.request.gas_per_pubdata_limit,
335                    factoryDeps: vec![],
336                    refundRecipient: sender,
337                })
338                .value(l1_value)
339                .into_transaction_request()
340        } else {
341            let bridge_addresses = bridge_addresses.unwrap();
342            let erc20_contract = ERC20::new(self.request.token, self.l1_provider);
343            let token_data = encode_token_data_for_bridge(&erc20_contract)
344                .await
345                .map_err(|_| {
346                    L1CommunicationError::Custom("Error while encoding ERC20 token data.")
347                })?;
348            let l2_finalize_deposit_calldata = encode_finalize_deposit_calldata(
349                sender,
350                receiver,
351                self.request.token,
352                self.request.amount,
353                token_data,
354            );
355
356            let l2_tx_fee = self
357                .get_bridge_l2_tx_fee_params(
358                    &bridge_hub_contract,
359                    TransactionRequest::default()
360                        .with_from(apply_l1_to_l2_alias(bridge_addresses.l1_bridge_address))
361                        .with_to(bridge_addresses.l2_bridge_address)
362                        .with_gas_per_pubdata(self.request.gas_per_pubdata_limit)
363                        .with_input(l2_finalize_deposit_calldata),
364                    l2_chain_id,
365                    fee_params,
366                )
367                .await?;
368
369            let bridge_calldata =
370                encode_deposit_token_calldata(self.request.token, self.request.amount, receiver);
371            bridge_hub_contract
372                .requestL2TransactionTwoBridges(L2TransactionRequestTwoBridges {
373                    chainId: l2_chain_id,
374                    mintValue: l2_tx_fee.tx_base_cost,
375                    l2Value: U256::from(0),
376                    l2GasLimit: l2_tx_fee.gas_limit,
377                    l2GasPerPubdataByteLimit: self.request.gas_per_pubdata_limit,
378                    refundRecipient: sender,
379                    secondBridgeAddress: bridge_addresses.l1_bridge_address,
380                    secondBridgeValue: U256::from(0),
381                    secondBridgeCalldata: bridge_calldata,
382                })
383                .from(sender)
384                .value(l2_tx_fee.tx_base_cost)
385                .into_transaction_request()
386        };
387        Ok(l1_tx_request
388            .max_fee_per_gas(fee_params.max_fee_per_gas)
389            .max_priority_fee_per_gas(fee_params.max_priority_fee_per_gas))
390    }
391
392    async fn approve_tokens(
393        &self,
394        sender: Address,
395        bridge_addresses: Option<BridgeAddresses>,
396        fee_params: &FeeParams,
397    ) -> Result<(), L1CommunicationError> {
398        if self.request.token == ETHER_L1_ADDRESS {
399            return Ok(());
400        }
401        let bridge_addresses = bridge_addresses.unwrap();
402        let erc20_contract = ERC20::new(self.request.token, self.l1_provider);
403        let token_allowance = erc20_contract
404            .allowance(sender, bridge_addresses.l1_bridge_address)
405            .call()
406            .await
407            .map_err(|_| {
408                L1CommunicationError::Custom(
409                    "Error occurred while fetching token allowance for the bridge.",
410                )
411            })?;
412
413        let allowance_deficit = self.request.amount - token_allowance;
414        if allowance_deficit > U256::from(0) {
415            if !self.request.auto_approval {
416                return Err(L1CommunicationError::Custom(
417                    "Deposit request auto_approval is disabled and the current token allowance won't cover the deposit. Consider enabling deposit request auto_approval or approving tokens manually before the deposit.",
418                ));
419            }
420            let approve_tx_builder = erc20_contract
421                .approve(bridge_addresses.l1_bridge_address, allowance_deficit)
422                .from(sender);
423            let approve_tx = approve_tx_builder.into_transaction_request();
424            let approve_tx_gas_limit = self.get_l1_tx_gas_limit(&approve_tx).await?;
425
426            self
427                .l1_provider
428                .send_transaction( approve_tx
429                .max_fee_per_gas(fee_params.max_fee_per_gas)
430                .max_priority_fee_per_gas(fee_params.max_priority_fee_per_gas)
431                .gas_limit(approve_tx_gas_limit))
432                .await.map_err(|_| {
433                    L1CommunicationError::Custom(
434                        "Error occurred while approving tokens for the bridge address",
435                    )
436                })?
437                .watch()
438                .await
439                .map_err(|_| {
440                    L1CommunicationError::Custom(
441                                "Error occurred while approving tokens for the bridge address. Approve transaction has failed.",
442                )
443                })?;
444        }
445        Ok(())
446    }
447
448    async fn submit(
449        &self,
450        tx_request: &L1TransactionRequest,
451    ) -> Result<L1TransactionReceipt, L1CommunicationError> {
452        let l1_gas_limit = self.get_l1_tx_gas_limit(tx_request).await?;
453        let l1_tx_request = tx_request.clone().with_gas_limit(l1_gas_limit);
454        let l1_tx_receipt = self
455            .l1_provider
456            .send_transaction(l1_tx_request)
457            .await
458            .map_err(|_| {
459                L1CommunicationError::Custom(
460                    "Error occurred while sending the L1 -> L2 deposit transaction.",
461                )
462            })?
463            .get_receipt()
464            .await
465            .map_err(|_| {
466                L1CommunicationError::Custom(
467                    "Error occurred while sending the L1 -> L2 deposit transaction receipt.",
468                )
469            })?;
470        Ok(L1TransactionReceipt::new(
471            l1_tx_receipt,
472            self.l2_provider.root().clone(),
473        ))
474    }
475
476    /// Executes specified deposit request. This will handle:
477    /// - Approving tokens if necessary.
478    /// - Sending the deposit transaction.
479    /// - Returning the [`L1TransactionReceipt`] of the deposit transaction.
480    ///
481    /// Returned receipt can be converted into a pending L2 transaction and awaited
482    /// using [`PendingTransactionBuilder`](https://docs.rs/alloy/latest/alloy/providers/struct.PendingTransactionBuilder.html)
483    /// interface.
484    ///
485    /// ## Returns
486    ///
487    /// L1TransactionReceipt of the deposit transaction.
488    pub async fn execute(&self) -> Result<L1TransactionReceipt, L1CommunicationError> {
489        let l2_chain_id = U256::from(self.l2_provider.get_chain_id().await.map_err(|_| {
490            L1CommunicationError::Custom("Error occurred while fetching L2 chain id.")
491        })?);
492
493        let bridge_addresses = if self.request.token != ETHER_L1_ADDRESS {
494            let (l1_bridge_address, l2_bridge_address) =
495                self.get_bridge_addresses_for_deposit(l2_chain_id).await?;
496
497            Some(BridgeAddresses {
498                l1_bridge_address,
499                l2_bridge_address,
500            })
501        } else {
502            None
503        };
504
505        let sender = self.l2_provider.wallet().default_signer_address();
506        let receiver = self.request.receiver.unwrap_or(sender);
507
508        let l1_fee_params = self.get_l1_fee_params().await?;
509
510        let l1_deposit_tx = self
511            .get_l1_deposit_tx(
512                sender,
513                receiver,
514                bridge_addresses,
515                l2_chain_id,
516                &l1_fee_params,
517            )
518            .await?;
519
520        self.approve_tokens(sender, bridge_addresses, &l1_fee_params)
521            .await?;
522
523        self.submit(&l1_deposit_tx).await
524    }
525}