UNPKG

zksync-ethers

Version:

A Web3 library for interacting with the ZkSync Layer 2 scaling solution.

1,405 lines (1,302 loc) 83.6 kB
import { BigNumberish, BlockTag, BytesLike, ContractTransactionResponse, ethers, FetchUrlFeeDataNetworkPlugin, TransactionRequest as EthersTransactionRequest, } from 'ethers'; import {Provider} from './provider'; import { BOOTLOADER_FORMAL_ADDRESS, checkBaseCost, DEFAULT_GAS_PER_PUBDATA_LIMIT, estimateCustomBridgeDepositL2Gas, estimateDefaultBridgeDepositL2Gas, getERC20DefaultBridgeData, isETH, L1_MESSENGER_ADDRESS, L1_RECOMMENDED_MIN_ERC20_DEPOSIT_GAS_LIMIT, L1_RECOMMENDED_MIN_ETH_DEPOSIT_GAS_LIMIT, layer1TxDefaults, REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT, scaleGasLimit, undoL1ToL2Alias, NONCE_HOLDER_ADDRESS, ETH_ADDRESS_IN_CONTRACTS, LEGACY_ETH_ADDRESS, isAddressEq, L2_BASE_TOKEN_ADDRESS, resolveAssetId, encodeNativeTokenVaultTransferData, encodeSecondBridgeDataV1, } from './utils'; import { IBridgehub, IBridgehub__factory, IERC20__factory, IL1ERC20Bridge, IL1ERC20Bridge__factory, IL1Bridge__factory, IL1SharedBridge, IL1SharedBridge__factory, IL2Bridge, IL2Bridge__factory, INonceHolder__factory, IZkSyncHyperchain, IZkSyncHyperchain__factory, IL2SharedBridge__factory, IL2SharedBridge, IL1Bridge, IL1Nullifier, IL1AssetRouter, IL1AssetRouter__factory, IL1Nullifier__factory, IAssetRouterBase__factory, IL1NativeTokenVault__factory, IL1NativeTokenVault, } from './typechain'; import { Address, FinalizeL1DepositParams, BalancesMap, Eip712Meta, FinalizeWithdrawalParams, FullDepositFee, PaymasterParams, PriorityOpResponse, TransactionResponse, LogProof, } from './types'; type Constructor<T = {}> = new (...args: any[]) => T; interface TxSender { sendTransaction( tx: EthersTransactionRequest ): Promise<ethers.TransactionResponse>; getAddress(): Promise<Address>; } export function AdapterL1<TBase extends Constructor<TxSender>>(Base: TBase) { return class Adapter extends Base { /** * Returns a provider instance for connecting to an L2 network. */ _providerL2(): Provider { throw new Error('Must be implemented by the derived class!'); } /** * Returns a provider instance for connecting to a L1 network. */ _providerL1(): ethers.Provider { throw new Error('Must be implemented by the derived class!'); } /** * Returns a signer instance used for signing transactions sent to the L1 network. */ _signerL1(): ethers.Signer { throw new Error('Must be implemented by the derived class!'); } /** * Returns the addresses of the default ZKsync Era bridge contracts on both L1 and L2, and some L1 specific contracts. */ async getDefaultBridgeAddresses(): Promise<{ erc20L1: string; erc20L2: string; wethL1: string; wethL2: string; sharedL1: string; sharedL2: string; l1Nullifier: string; l1NativeTokenVault: string; }> { await this._providerL2().getDefaultBridgeAddresses(); const addresses = await this._providerL2().contractAddresses(); let l1Nullifier: Address; let l1NativeTokenVault: Address; if (!addresses.l1Nullifier) { // todo return these values from server instead const l1AssetRouter = await this.getL1AssetRouter( (await this._providerL2().getDefaultBridgeAddresses()).sharedL1! ); l1Nullifier = await l1AssetRouter.L1_NULLIFIER(); l1NativeTokenVault = await l1AssetRouter.nativeTokenVault(); await this._providerL2()._setL1NullifierAndNativeTokenVault( l1Nullifier, l1NativeTokenVault ); } else { l1Nullifier = addresses.l1Nullifier; l1NativeTokenVault = addresses.l1NativeTokenVault!; } return { erc20L1: addresses.erc20BridgeL1!, erc20L2: addresses.erc20BridgeL2!, wethL1: addresses.wethBridgeL1!, wethL2: addresses.wethBridgeL2!, sharedL1: addresses.sharedBridgeL1!, sharedL2: addresses.sharedBridgeL2!, l1Nullifier: l1Nullifier!, l1NativeTokenVault: l1NativeTokenVault!, }; } /** * Returns `Contract` wrapper of the ZKsync Era smart contract. */ async getMainContract(): Promise<IZkSyncHyperchain> { const address = await this._providerL2().getMainContractAddress(); return IZkSyncHyperchain__factory.connect(address, this._signerL1()); } /** * Returns `Contract` wrapper of the Bridgehub smart contract. */ async getBridgehubContract(): Promise<IBridgehub> { const address = await this._providerL2().getBridgehubContractAddress(); return IBridgehub__factory.connect(address, this._signerL1()); } /** * Returns L1 bridge contracts. * * @remarks There is no separate Ether bridge contract, {@link getBridgehubContract Bridgehub} is used instead. */ async getL1BridgeContracts(): Promise<{ erc20: IL1ERC20Bridge; weth: IL1ERC20Bridge; shared: IL1SharedBridge; }> { const addresses = await this._providerL2().getDefaultBridgeAddresses(); return { erc20: IL1ERC20Bridge__factory.connect( addresses.erc20L1, this._signerL1() ), weth: IL1ERC20Bridge__factory.connect( addresses.wethL1 || addresses.erc20L1, this._signerL1() ), shared: IL1SharedBridge__factory.connect( addresses.sharedL1, this._signerL1() ), }; } /** * Returns the L1 asset router contract, used for handling cross chain calls. */ async getL1AssetRouter(address?: string): Promise<IL1AssetRouter> { // FIXME: maybe makes sense to provide an API to do it in one call const _address = address ? address : (await this.getDefaultBridgeAddresses()).sharedL1!; return IL1AssetRouter__factory.connect(_address, this._signerL1()); } /** * Returns the L1 native token vault contract, used for interacting with tokens. */ async getL1NativeTokenVault(): Promise<IL1NativeTokenVault> { // FIXME: maybe makes sense to provide an API to do it in one call const bridgeContracts = await this.getDefaultBridgeAddresses(); return IL1NativeTokenVault__factory.connect( bridgeContracts.l1NativeTokenVault!, this._signerL1() ); } /** * Returns the L1 Nullifier contract, used for replay protection for failed deposits and withdrawals. */ async getL1Nullifier(): Promise<IL1Nullifier> { // FIXME: maybe makes sense to provide an API to do it in one call const bridgeContracts = await this.getDefaultBridgeAddresses(); return IL1Nullifier__factory.connect( bridgeContracts.l1Nullifier!, this._signerL1() ); } /** * Returns the address of the base token on L1. */ async getBaseToken(): Promise<string> { const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; return await bridgehub.baseToken(chainId); } /** * Returns whether the chain is ETH-based. */ async isETHBasedChain(): Promise<boolean> { return this._providerL2().isEthBasedChain(); } /** * Returns the amount of the token held by the account on the L1 network. * * @param [token] The address of the token. Defaults to ETH if not provided. * @param [blockTag] The block in which the balance should be checked. * Defaults to 'committed', i.e., the latest processed block. */ async getBalanceL1(token?: Address, blockTag?: BlockTag): Promise<bigint> { token ??= LEGACY_ETH_ADDRESS; if (isETH(token)) { return await this._providerL1().getBalance( await this.getAddress(), blockTag ); } else { const erc20contract = IERC20__factory.connect( token, this._providerL1() ); return await erc20contract.balanceOf(await this.getAddress()); } } /** * Returns the amount of approved tokens for a specific L1 bridge. * * @param token The Ethereum address of the token. * @param [bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge, either `L1EthBridge` or `L1Erc20Bridge`. * @param [blockTag] The block in which an allowance should be checked. * Defaults to 'committed', i.e., the latest processed block. */ async getAllowanceL1( token: Address, bridgeAddress?: Address, blockTag?: ethers.BlockTag ): Promise<bigint> { if (!bridgeAddress) { const bridgeContracts = await this.getL1BridgeContracts(); bridgeAddress = await bridgeContracts.shared.getAddress(); } const erc20contract = IERC20__factory.connect(token, this._providerL1()); return await erc20contract.allowance( await this.getAddress(), bridgeAddress, { blockTag, } ); } /** * Returns the L2 token address equivalent for a L1 token address as they are not necessarily equal. * The ETH address is set to the zero address. * * @remarks Only works for tokens bridged on default ZKsync Era bridges. * * @param token The address of the token on L1. */ async l2TokenAddress(token: Address): Promise<string> { return this._providerL2().l2TokenAddress(token); } /** * Returns the L1 token address equivalent for a L2 token address as they are not equal. * ETH address is set to zero address. * * @remarks Only works for tokens bridged on default ZKsync Era bridges. * * @param token The address of the token on L2. */ async l1TokenAddress(token: Address): Promise<string> { return this._providerL2().l1TokenAddress(token); } /** * Bridging ERC20 tokens from L1 requires approving the tokens to the ZKsync Era smart contract. * * @param token The L1 address of the token. * @param amount The amount of the token to be approved. * @param [overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @returns A promise that resolves to the response of the approval transaction. * @throws {Error} If attempting to approve an ETH token. */ async approveERC20( token: Address, amount: BigNumberish, overrides?: ethers.Overrides & {bridgeAddress?: Address} ): Promise<ethers.TransactionResponse> { if (isETH(token)) { throw new Error( "ETH token can't be approved! The address of the token does not exist on L1." ); } overrides ??= {}; let bridgeAddress = overrides.bridgeAddress; const erc20contract = IERC20__factory.connect(token, this._signerL1()); if (!bridgeAddress) { bridgeAddress = await ( await this.getL1BridgeContracts() ).shared.getAddress(); } else { delete overrides.bridgeAddress; } return await erc20contract.approve(bridgeAddress, amount, overrides); } /** * Returns the base cost for an L2 transaction. * * @param params The parameters for calculating the base cost. * @param params.gasLimit The gasLimit for the L2 contract call. * @param [params.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [params.gasPrice] The L1 gas price of the L1 transaction that will send the request for an execute call. */ async getBaseCost(params: { gasLimit: BigNumberish; gasPerPubdataByte?: BigNumberish; gasPrice?: BigNumberish; }): Promise<bigint> { const bridgehub = await this.getBridgehubContract(); const parameters = {...layer1TxDefaults(), ...params}; parameters.gasPrice ??= (await this._providerL1().getFeeData()).gasPrice!; parameters.gasPerPubdataByte ??= REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT; return await bridgehub.l2TransactionBaseCost( (await this._providerL2().getNetwork()).chainId, parameters.gasPrice, parameters.gasLimit, parameters.gasPerPubdataByte ); } /** * Returns the parameters for the approval token transaction based on the deposit token and amount. * Some deposit transactions require multiple approvals. Existing allowance for the bridge is not checked; * allowance is calculated solely based on the specified amount. * * @param token The address of the token to deposit. * @param amount The amount of the token to deposit. * @param overrides Transaction's overrides for deposit which may be used to pass * L1 `gasLimit`, `gasPrice`, `value`, etc. */ async getDepositAllowanceParams( token: Address, amount: BigNumberish, overrides?: ethers.Overrides ): Promise<{token: Address; allowance: BigNumberish}[]> { if (isAddressEq(token, LEGACY_ETH_ADDRESS)) { token = ETH_ADDRESS_IN_CONTRACTS; } const baseTokenAddress = await this.getBaseToken(); const isETHBasedChain = await this.isETHBasedChain(); if (isETHBasedChain && isAddressEq(token, ETH_ADDRESS_IN_CONTRACTS)) { throw new Error( "ETH token can't be approved! The address of the token does not exist on L1." ); } else if (isAddressEq(baseTokenAddress, ETH_ADDRESS_IN_CONTRACTS)) { return [{token, allowance: amount}]; } else if (isAddressEq(token, ETH_ADDRESS_IN_CONTRACTS)) { return [ { token: baseTokenAddress, allowance: ( await this._getDepositETHOnNonETHBasedChainTx({ token, amount, overrides, }) ).mintValue, }, ]; } else if (isAddressEq(token, baseTokenAddress)) { return [ { token: baseTokenAddress, allowance: ( await this._getDepositBaseTokenOnNonETHBasedChainTx({ token, amount, overrides, }) ).mintValue, }, ]; } else { // A deposit of a non-base token to a non-ETH-based chain requires two approvals. return [ { token: baseTokenAddress, allowance: ( await this._getDepositNonBaseTokenToNonETHBasedChainTx({ token, amount, overrides, }) ).mintValue, }, { token: token, allowance: amount, }, ]; } } /** * Transfers the specified token from the associated account on the L1 network to the target account on the L2 network. * The token can be either ETH or any ERC20 token. For ERC20 tokens, enough approved tokens must be associated with * the specified L1 bridge (default one or the one defined in `transaction.bridgeAddress`). * In this case, depending on is the chain ETH-based or not `transaction.approveERC20` or `transaction.approveBaseERC20` * can be enabled to perform token approval. If there are already enough approved tokens for the L1 bridge, * token approval will be skipped. To check the amount of approved tokens for a specific bridge, * use the {@link getAllowanceL1} method. * * @param transaction The transaction object containing deposit details. * @param transaction.token The address of the token to deposit. * @param transaction.amount The amount of the token to deposit. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.operatorTip] (currently not used) If the ETH value passed with the transaction is not * explicitly stated in the overrides, this field will be equal to the tip the operator will receive on top of * the base cost of the transaction. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge (either `L1EthBridge` or `L1Erc20Bridge`). * @param [transaction.approveERC20] Whether or not token approval should be performed under the hood. * Set this flag to true if you bridge an ERC20 token and didn't call the {@link approveERC20} function beforehand. * @param [transaction.approveBaseERC20] Whether or not base token approval should be performed under the hood. * Set this flag to true if you bridge a base token and didn't call the {@link approveERC20} function beforehand. * @param [transaction.l2GasLimit] Maximum amount of L2 gas that the transaction can consume during execution on L2. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.refundRecipient] The address on L2 that will receive the refund for the transaction. * If the transaction fails, it will also be the address to receive `l2Value`. * @param [transaction.overrides] Transaction's overrides for deposit which may be used to pass * L1 `gasLimit`, `gasPrice`, `value`, etc. * @param [transaction.approveOverrides] Transaction's overrides for approval of an ERC20 token which may be used * to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @param [transaction.approveBaseOverrides] Transaction's overrides for approval of a base token which may be used * to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. */ async deposit(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; approveERC20?: boolean; approveBaseERC20?: boolean; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; refundRecipient?: Address; overrides?: ethers.Overrides; approveOverrides?: ethers.Overrides; approveBaseOverrides?: ethers.Overrides; customBridgeData?: BytesLike; }): Promise<PriorityOpResponse> { if (isAddressEq(transaction.token, LEGACY_ETH_ADDRESS)) { transaction.token = ETH_ADDRESS_IN_CONTRACTS; } const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const isETHBasedChain = isAddressEq( baseTokenAddress, ETH_ADDRESS_IN_CONTRACTS ); if ( isETHBasedChain && isAddressEq(transaction.token, ETH_ADDRESS_IN_CONTRACTS) ) { return await this._depositETHToETHBasedChain(transaction); } else if (isAddressEq(baseTokenAddress, ETH_ADDRESS_IN_CONTRACTS)) { return await this._depositTokenToETHBasedChain(transaction); } else if (isAddressEq(transaction.token, ETH_ADDRESS_IN_CONTRACTS)) { return await this._depositETHToNonETHBasedChain(transaction); } else if (isAddressEq(transaction.token, baseTokenAddress)) { return await this._depositBaseTokenToNonETHBasedChain(transaction); } else { return await this._depositNonBaseTokenToNonETHBasedChain(transaction); } } async _depositNonBaseTokenToNonETHBasedChain(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; approveERC20?: boolean; approveBaseERC20?: boolean; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; refundRecipient?: Address; overrides?: ethers.Overrides; approveOverrides?: ethers.Overrides; approveBaseOverrides?: ethers.Overrides; customBridgeData?: BytesLike; }): Promise<PriorityOpResponse> { // Deposit a non-ETH and non-base token to a non-ETH-based chain. // Go through the BridgeHub and obtain approval for both tokens. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const bridgeContracts = await this.getL1BridgeContracts(); const {tx, mintValue} = await this._getDepositNonBaseTokenToNonETHBasedChainTx(transaction); if (transaction.approveBaseERC20) { // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1( baseTokenAddress, await bridgeContracts.shared.getAddress() ); if (allowance < mintValue) { const approveTx = await this.approveERC20( baseTokenAddress, mintValue, { bridgeAddress: await bridgeContracts.shared.getAddress(), ...transaction.approveBaseOverrides, } ); await approveTx.wait(); } } if (transaction.approveERC20) { const bridgeAddress = transaction.bridgeAddress ? transaction.bridgeAddress : await bridgeContracts.shared.getAddress(); // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1( transaction.token, bridgeAddress ); if (allowance < BigInt(transaction.amount)) { const approveTx = await this.approveERC20( transaction.token, transaction.amount, { bridgeAddress, ...transaction.approveOverrides, } ); await approveTx.wait(); } } const baseGasLimit = await this._providerL1().estimateGas(tx); const gasLimit = scaleGasLimit(baseGasLimit); tx.gasLimit ??= gasLimit; return await this._providerL2().getPriorityOpResponse( await this._signerL1().sendTransaction(tx) ); } async _depositBaseTokenToNonETHBasedChain(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; approveERC20?: boolean; approveBaseERC20?: boolean; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; refundRecipient?: Address; overrides?: ethers.Overrides; approveOverrides?: ethers.Overrides; approveBaseOverrides?: ethers.Overrides; customBridgeData?: BytesLike; }): Promise<PriorityOpResponse> { // Bridging the base token to a non-ETH-based chain. // Go through the BridgeHub, and give approval. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const sharedBridge = await ( await this.getL1BridgeContracts() ).shared.getAddress(); const {tx, mintValue} = await this._getDepositBaseTokenOnNonETHBasedChainTx(transaction); if (transaction.approveERC20 || transaction.approveBaseERC20) { const approveOverrides = transaction.approveBaseOverrides ?? transaction.approveOverrides!; // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1( baseTokenAddress, sharedBridge ); if (allowance < mintValue) { const approveTx = await this.approveERC20( baseTokenAddress, mintValue, { bridgeAddress: sharedBridge, ...approveOverrides, } ); await approveTx.wait(); } } const baseGasLimit = await this.estimateGasRequestExecute(tx); const gasLimit = scaleGasLimit(baseGasLimit); tx.overrides ??= {}; tx.overrides.gasLimit ??= gasLimit; return this.requestExecute(tx); } async _depositETHToNonETHBasedChain(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; approveERC20?: boolean; approveBaseERC20?: boolean; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; refundRecipient?: Address; overrides?: ethers.Overrides; approveOverrides?: ethers.Overrides; approveBaseOverrides?: ethers.Overrides; customBridgeData?: BytesLike; }): Promise<PriorityOpResponse> { // Depositing ETH into a non-ETH-based chain. // Use requestL2TransactionTwoBridges, secondBridge is the wETH bridge. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const sharedBridge = await ( await this.getL1BridgeContracts() ).shared.getAddress(); const {tx, mintValue} = await this._getDepositETHOnNonETHBasedChainTx(transaction); if (transaction.approveBaseERC20) { // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1( baseTokenAddress, sharedBridge ); if (allowance < mintValue) { const approveTx = await this.approveERC20( baseTokenAddress, mintValue, { bridgeAddress: sharedBridge, ...transaction.approveBaseOverrides, } ); await approveTx.wait(); } } const baseGasLimit = await this._providerL1().estimateGas(tx); const gasLimit = scaleGasLimit(baseGasLimit); tx.gasLimit ??= gasLimit; return await this._providerL2().getPriorityOpResponse( await this._signerL1().sendTransaction(tx) ); } async _depositTokenToETHBasedChain(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; approveERC20?: boolean; approveBaseERC20?: boolean; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; refundRecipient?: Address; overrides?: ethers.Overrides; approveOverrides?: ethers.Overrides; approveBaseOverrides?: ethers.Overrides; customBridgeData?: BytesLike; }): Promise<PriorityOpResponse> { const bridgeContracts = await this.getL1BridgeContracts(); const tx = await this._getDepositTokenOnETHBasedChainTx(transaction); if (transaction.approveERC20) { const proposedBridge = await bridgeContracts.shared.getAddress(); const bridgeAddress = transaction.bridgeAddress ? transaction.bridgeAddress : proposedBridge; // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1( transaction.token, bridgeAddress ); if (allowance < BigInt(transaction.amount)) { const approveTx = await this.approveERC20( transaction.token, transaction.amount, { bridgeAddress, ...transaction.approveOverrides, } ); await approveTx.wait(); } } const baseGasLimit = await this._providerL1().estimateGas(tx); const gasLimit = scaleGasLimit(baseGasLimit); tx.gasLimit ??= gasLimit; return await this._providerL2().getPriorityOpResponse( await this._signerL1().sendTransaction(tx) ); } async _depositETHToETHBasedChain(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; approveERC20?: boolean; approveBaseERC20?: boolean; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; refundRecipient?: Address; overrides?: ethers.Overrides; approveOverrides?: ethers.Overrides; approveBaseOverrides?: ethers.Overrides; customBridgeData?: BytesLike; }): Promise<PriorityOpResponse> { const tx = await this._getDepositETHOnETHBasedChainTx(transaction); const baseGasLimit = await this.estimateGasRequestExecute(tx); const gasLimit = scaleGasLimit(baseGasLimit); tx.overrides ??= {}; tx.overrides.gasLimit ??= gasLimit; return this.requestExecute(tx); } /** * Estimates the amount of gas required for a deposit transaction on the L1 network. * Gas for approving ERC20 tokens is not included in the estimation. * * In order for estimation to work, enough token allowance is required in the following cases: * - Depositing ERC20 tokens on an ETH-based chain. * - Depositing any token (including ETH) on a non-ETH-based chain. * * @param transaction The transaction details. * @param transaction.token The address of the token to deposit. * @param transaction.amount The amount of the token to deposit. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.operatorTip] (currently not used) If the ETH value passed with the transaction is not * explicitly stated in the overrides, this field will be equal to the tip the operator will receive on top of the * base cost of the transaction. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge (`L1SharedBridge`). * @param [transaction.l2GasLimit] Maximum amount of L2 gas that the transaction can consume during execution on L2. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. * @param [transaction.refundRecipient] The address on L2 that will receive the refund for the transaction. * If the transaction fails, it will also be the address to receive `l2Value`. * @param [transaction.overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. */ async estimateGasDeposit(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; customBridgeData?: BytesLike; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; refundRecipient?: Address; overrides?: ethers.Overrides; }): Promise<bigint> { if (isAddressEq(transaction.token, LEGACY_ETH_ADDRESS)) { transaction.token = ETH_ADDRESS_IN_CONTRACTS; } const tx = await this.getDepositTx(transaction); let baseGasLimit: bigint; if (tx.token && isAddressEq(tx.token, await this.getBaseToken())) { baseGasLimit = await this.estimateGasRequestExecute(tx); } else { baseGasLimit = await this._providerL1().estimateGas(tx); } return scaleGasLimit(baseGasLimit); } /** * Returns a populated deposit transaction. * * @param transaction The transaction details. * @param transaction.token The address of the token to deposit. * @param transaction.amount The amount of the token to deposit. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.operatorTip] (currently not used) If the ETH value passed with the transaction is not * explicitly stated in the overrides, this field will be equal to the tip the operator will receive on top of the * base cost of the transaction. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. Defaults to the default ZKsync * Era bridge (`L1SharedBridge`). * @param [transaction.l2GasLimit] Maximum amount of L2 gas that the transaction can consume during execution on L2. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. * @param [transaction.refundRecipient] The address on L2 that will receive the refund for the transaction. * If the transaction fails, it will also be the address to receive `l2Value`. * @param [transaction.overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. */ async getDepositTx(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }): Promise<any> { if (isAddressEq(transaction.token, LEGACY_ETH_ADDRESS)) { transaction.token = ETH_ADDRESS_IN_CONTRACTS; } const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const isETHBasedChain = isAddressEq( baseTokenAddress, ETH_ADDRESS_IN_CONTRACTS ); if ( isETHBasedChain && isAddressEq(transaction.token, ETH_ADDRESS_IN_CONTRACTS) ) { return await this._getDepositETHOnETHBasedChainTx(transaction); } else if (isETHBasedChain) { return await this._getDepositTokenOnETHBasedChainTx(transaction); } else if (isAddressEq(transaction.token, ETH_ADDRESS_IN_CONTRACTS)) { return (await this._getDepositETHOnNonETHBasedChainTx(transaction)).tx; } else if (isAddressEq(transaction.token, baseTokenAddress)) { return ( await this._getDepositBaseTokenOnNonETHBasedChainTx(transaction) ).tx; } else { return ( await this._getDepositNonBaseTokenToNonETHBasedChainTx(transaction) ).tx; } } async _getDepositNonBaseTokenToNonETHBasedChainTx(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }) { const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const bridgeContracts = await this.getL1BridgeContracts(); const tx = await this._getDepositTxWithDefaults(transaction); const { token, operatorTip, amount, overrides, l2GasLimit, to, refundRecipient, gasPerPubdataByte, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost( chainId as BigNumberish, gasPriceForEstimation as BigNumberish, l2GasLimit, gasPerPubdataByte ); const mintValue = baseCost + BigInt(operatorTip); await checkBaseCost(baseCost, mintValue); overrides.value ??= 0; const secondBridgeCalldata = await this._getSecondBridgeCalldata( token, amount, to ); return { tx: await bridgehub.requestL2TransactionTwoBridges.populateTransaction( { chainId: chainId, mintValue, l2Value: 0, l2GasLimit: l2GasLimit, l2GasPerPubdataByteLimit: gasPerPubdataByte, refundRecipient: refundRecipient ?? ethers.ZeroAddress, secondBridgeAddress: tx.bridgeAddress ?? (await bridgeContracts.shared.getAddress()), secondBridgeValue: 0, secondBridgeCalldata: secondBridgeCalldata, }, overrides ), mintValue: mintValue, }; } async _getDepositBaseTokenOnNonETHBasedChainTx(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }) { // Depositing the base token to a non-ETH-based chain. // Goes through the BridgeHub. // Have to give approvals for the sharedBridge. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, to, overrides, l2GasLimit, gasPerPubdataByte, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost( chainId as BigNumberish, gasPriceForEstimation as BigNumberish, l2GasLimit, gasPerPubdataByte ); tx.overrides.value = 0; return { tx: { contractAddress: to, calldata: '0x', mintValue: baseCost + BigInt(operatorTip) + BigInt(amount), l2Value: amount, ...tx, }, mintValue: baseCost + BigInt(operatorTip) + BigInt(amount), }; } async _getDepositETHOnNonETHBasedChainTx(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }) { const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const sharedBridge = await ( await this.getL1BridgeContracts() ).shared.getAddress(); const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, overrides, l2GasLimit, to, refundRecipient, gasPerPubdataByte, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost( chainId as BigNumberish, gasPriceForEstimation as BigNumberish, l2GasLimit, gasPerPubdataByte ); overrides.value ??= amount; const mintValue = baseCost + BigInt(operatorTip); await checkBaseCost(baseCost, mintValue); const secondBridgeCalldata = await this._getSecondBridgeCalldata( ETH_ADDRESS_IN_CONTRACTS, amount, to ); return { tx: await bridgehub.requestL2TransactionTwoBridges.populateTransaction( { chainId, mintValue, l2Value: 0, l2GasLimit: l2GasLimit, l2GasPerPubdataByteLimit: gasPerPubdataByte, refundRecipient: refundRecipient ?? ethers.ZeroAddress, secondBridgeAddress: tx.bridgeAddress ?? sharedBridge, secondBridgeValue: amount, secondBridgeCalldata: secondBridgeCalldata, }, overrides ), mintValue: mintValue, }; } async _getDepositTokenOnETHBasedChainTx(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }): Promise<ethers.ContractTransaction> { // Depositing token to an ETH-based chain. Use the ERC20 bridge as done before. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const tx = await this._getDepositTxWithDefaults(transaction); const { token, operatorTip, amount, overrides, l2GasLimit, to, refundRecipient, gasPerPubdataByte, } = tx; const secondBridgeCalldata = await this._getSecondBridgeCalldata( token, amount, to ); const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost( chainId as BigNumberish, gasPriceForEstimation as BigNumberish, tx.l2GasLimit, tx.gasPerPubdataByte ); const mintValue = baseCost + BigInt(operatorTip); overrides.value ??= mintValue; await checkBaseCost(baseCost, mintValue); const secondBridgeAddress = tx.bridgeAddress ?? (await (await this.getL1BridgeContracts()).shared.getAddress()); return await bridgehub.requestL2TransactionTwoBridges.populateTransaction( { chainId, mintValue, l2Value: 0, l2GasLimit, l2GasPerPubdataByteLimit: gasPerPubdataByte, refundRecipient: refundRecipient ?? ethers.ZeroAddress, secondBridgeAddress, secondBridgeValue: 0, secondBridgeCalldata, }, overrides ); } async _getDepositETHOnETHBasedChainTx(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }) { // Call the BridgeHub directly, like it's done with the DiamondProxy. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, overrides, l2GasLimit, gasPerPubdataByte, to, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost( chainId as BigNumberish, gasPriceForEstimation as BigNumberish, l2GasLimit, gasPerPubdataByte ); overrides.value ??= baseCost + BigInt(operatorTip) + BigInt(amount); return { contractAddress: to, calldata: '0x', mintValue: overrides.value, l2Value: amount, ...tx, }; } async _getSecondBridgeCalldata( token: Address, amount: BigNumberish, to: Address ): Promise<string> { const assetId = await resolveAssetId( token, await this.getL1NativeTokenVault() ); const ntvData = encodeNativeTokenVaultTransferData( BigInt(amount), to, token ); const secondBridgeCalldata = encodeSecondBridgeDataV1( ethers.hexlify(assetId), ntvData ); return secondBridgeCalldata; } // Creates a shallow copy of a transaction and populates missing fields with defaults. async _getDepositTxWithDefaults(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }): Promise<{ token: Address; amount: BigNumberish; to: Address; operatorTip: BigNumberish; bridgeAddress?: Address; l2GasLimit: BigNumberish; gasPerPubdataByte: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides: ethers.Overrides; }> { const {...tx} = transaction; tx.to = tx.to ?? (await this.getAddress()); tx.operatorTip ??= 0; tx.overrides ??= {}; tx.overrides.from = await this.getAddress(); tx.gasPerPubdataByte ??= REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT; tx.l2GasLimit ??= await this._getL2GasLimit(tx); await insertGasPrice(this._providerL1(), tx.overrides); return tx as { token: Address; amount: BigNumberish; to: Address; operatorTip: BigNumberish; bridgeAddress?: Address; l2GasLimit: BigNumberish; gasPerPubdataByte: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides: ethers.Overrides; }; } // Default behaviour for calculating l2GasLimit of deposit transaction. async _getL2GasLimit(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }): Promise<BigNumberish> { if (transaction.bridgeAddress) { return await this._getL2GasLimitFromCustomBridge(transaction); } else { return await estimateDefaultBridgeDepositL2Gas( this._providerL1(), this._providerL2(), transaction.token, transaction.amount, transaction.to!, await this.getAddress(), transaction.gasPerPubdataByte ); } } // Calculates the l2GasLimit of deposit transaction using custom bridge. async _getL2GasLimitFromCustomBridge(transaction: { token: Address; amount: BigNumberish; to?: Address; operatorTip?: BigNumberish; bridgeAddress?: Address; l2GasLimit?: BigNumberish; gasPerPubdataByte?: BigNumberish; customBridgeData?: BytesLike; refundRecipient?: Address; overrides?: ethers.Overrides; }): Promise<BigNumberish> { const customBridgeData = transaction.customBridgeData ?? (await getERC20DefaultBridgeData( transaction.token, this._providerL1() )); const bridge = IL1Bridge__factory.connect( transaction.bridgeAddress!, this._signerL1() ); const chainId = (await this._providerL2().getNetwork()) .chainId as BigNumberish; const l2Address = await bridge.l2BridgeAddress(chainId); return await estimateCustomBridgeDepositL2Gas( this._providerL2(), transaction.bridgeAddress!, l2Address, transaction.token, transaction.amount, transaction.to!, customBridgeData, await this.getAddress(), transaction.gasPerPubdataByte ); } /** * Retrieves the full needed ETH fee for the deposit. Returns the L1 fee and the L2 fee {@link FullDepositFee}. * * @param transaction The transaction details. * @param transaction.token The address of the token to deposit. ETH by default. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge (either `L1EthBridge` or `L1Erc20Bridge`). * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @throws {Error} If: * - There's not enough balance for the deposit under the provided gas price. * - There's not enough allowance to cover the deposit. */ async getFullRequiredDepositFee(transaction: { token: Address; to?: Address; bridgeAddress?: Address; customBridgeData?: BytesLike; gasPerPubdataByte?: BigNumberish; overrides?: ethers.Overrides; }): Promise<FullDepositFee> { if (isAddressEq(transaction.token, LEGACY_ETH_ADDRESS)) { transaction.token = ETH_ADDRESS_IN_CONTRACTS; } // It is assumed that the L2 fee for the transaction does not depend on its value. const dummyAmount = 1n; const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const isETHBasedChain = isAddressEq( baseTokenAddress, ETH_ADDRESS_IN_CONTRACTS ); const tx = await this._getDepositTxWithDefaults({ ...transaction, amount: dummyAmount, }); const gasPriceForEstimation = tx.overrides.maxFeePerGas || tx.overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost( chainId as BigNumberish, gasPriceForEstimation as BigNumberish, tx.l2GasLimit, tx.gasPerPubdataByte ); if (isETHBasedChain) { // To ensure that L1 gas estimation succeeds when using estimateGasDeposit, // the account needs to have a sufficient ETH balance. const selfBalanceETH = await this.getBalanceL1(); if (baseCost >= selfBalanceETH +