zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
869 lines • 75.7 kB
JavaScript
import { ethers, } from 'ethers';
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, resolveAssetId, encodeNativeTokenVaultTransferData, encodeSecondBridgeDataV1, } from './utils';
import { IBridgehub__factory, IERC20__factory, IL1ERC20Bridge__factory, IL1Bridge__factory, IL1SharedBridge__factory, IL2Bridge__factory, INonceHolder__factory, IZkSyncHyperchain__factory, IL2SharedBridge__factory, IL1AssetRouter__factory, IL1Nullifier__factory, IAssetRouterBase__factory, IL1NativeTokenVault__factory, } from './typechain';
export function AdapterL1(Base) {
return class Adapter extends Base {
/**
* Returns a provider instance for connecting to an L2 network.
*/
_providerL2() {
throw new Error('Must be implemented by the derived class!');
}
/**
* Returns a provider instance for connecting to a L1 network.
*/
_providerL1() {
throw new Error('Must be implemented by the derived class!');
}
/**
* Returns a signer instance used for signing transactions sent to the L1 network.
*/
_signerL1() {
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() {
await this._providerL2().getDefaultBridgeAddresses();
const addresses = await this._providerL2().contractAddresses();
let l1Nullifier;
let l1NativeTokenVault;
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() {
const address = await this._providerL2().getMainContractAddress();
return IZkSyncHyperchain__factory.connect(address, this._signerL1());
}
/**
* Returns `Contract` wrapper of the Bridgehub smart contract.
*/
async getBridgehubContract() {
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() {
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) {
// 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() {
// 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() {
// 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() {
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() {
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, blockTag) {
token ?? (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, bridgeAddress, blockTag) {
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) {
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) {
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, amount, overrides) {
if (isETH(token)) {
throw new Error("ETH token can't be approved! The address of the token does not exist on L1.");
}
overrides ?? (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) {
const bridgehub = await this.getBridgehubContract();
const parameters = { ...layer1TxDefaults(), ...params };
parameters.gasPrice ?? (parameters.gasPrice = (await this._providerL1().getFeeData()).gasPrice);
parameters.gasPerPubdataByte ?? (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, amount, overrides) {
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) {
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) {
// 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 ?? (tx.gasLimit = gasLimit);
return await this._providerL2().getPriorityOpResponse(await this._signerL1().sendTransaction(tx));
}
async _depositBaseTokenToNonETHBasedChain(transaction) {
var _a;
// 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 = {});
(_a = tx.overrides).gasLimit ?? (_a.gasLimit = gasLimit);
return this.requestExecute(tx);
}
async _depositETHToNonETHBasedChain(transaction) {
// 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 ?? (tx.gasLimit = gasLimit);
return await this._providerL2().getPriorityOpResponse(await this._signerL1().sendTransaction(tx));
}
async _depositTokenToETHBasedChain(transaction) {
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 ?? (tx.gasLimit = gasLimit);
return await this._providerL2().getPriorityOpResponse(await this._signerL1().sendTransaction(tx));
}
async _depositETHToETHBasedChain(transaction) {
var _a;
const tx = await this._getDepositETHOnETHBasedChainTx(transaction);
const baseGasLimit = await this.estimateGasRequestExecute(tx);
const gasLimit = scaleGasLimit(baseGasLimit);
tx.overrides ?? (tx.overrides = {});
(_a = tx.overrides).gasLimit ?? (_a.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) {
if (isAddressEq(transaction.token, LEGACY_ETH_ADDRESS)) {
transaction.token = ETH_ADDRESS_IN_CONTRACTS;
}
const tx = await this.getDepositTx(transaction);
let baseGasLimit;
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) {
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) {
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, gasPriceForEstimation, l2GasLimit, gasPerPubdataByte);
const mintValue = baseCost + BigInt(operatorTip);
await checkBaseCost(baseCost, mintValue);
overrides.value ?? (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) {
// 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, gasPriceForEstimation, 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) {
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, gasPriceForEstimation, l2GasLimit, gasPerPubdataByte);
overrides.value ?? (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) {
// 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, gasPriceForEstimation, tx.l2GasLimit, tx.gasPerPubdataByte);
const mintValue = baseCost + BigInt(operatorTip);
overrides.value ?? (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) {
// 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, gasPriceForEstimation, l2GasLimit, gasPerPubdataByte);
overrides.value ?? (overrides.value = baseCost + BigInt(operatorTip) + BigInt(amount));
return {
contractAddress: to,
calldata: '0x',
mintValue: overrides.value,
l2Value: amount,
...tx,
};
}
async _getSecondBridgeCalldata(token, amount, to) {
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) {
const { ...tx } = transaction;
tx.to = tx.to ?? (await this.getAddress());
tx.operatorTip ?? (tx.operatorTip = 0);
tx.overrides ?? (tx.overrides = {});
tx.overrides.from = await this.getAddress();
tx.gasPerPubdataByte ?? (tx.gasPerPubdataByte = REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT);
tx.l2GasLimit ?? (tx.l2GasLimit = await this._getL2GasLimit(tx));
await insertGasPrice(this._providerL1(), tx.overrides);
return tx;
}
// Default behaviour for calculating l2GasLimit of deposit transaction.
async _getL2GasLimit(transaction) {
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) {
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;
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) {
var _a, _b;
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, gasPriceForEstimation, 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 + dummyAmount) {
const recommendedL1GasLimit = isAddressEq(tx.token, ETH_ADDRESS_IN_CONTRACTS)
? L1_RECOMMENDED_MIN_ETH_DEPOSIT_GAS_LIMIT
: L1_RECOMMENDED_MIN_ERC20_DEPOSIT_GAS_LIMIT;
const recommendedETHBalance = BigInt(recommendedL1GasLimit) * BigInt(gasPriceForEstimation) +
baseCost;
const formattedRecommendedBalance = ethers.formatEther(recommendedETHBalance);
throw new Error(`Not enough balance for deposit! Under the provided gas price, the recommended balance to perform a deposit is ${formattedRecommendedBalance} ETH`);
}
// In case of token deposit, a sufficient token allowance is also required.
if (!isAddressEq(tx.token, ETH_ADDRESS_IN_CONTRACTS) &&
(await this.getAllowanceL1(tx.token, tx.bridgeAddress)) < dummyAmount) {
throw new Error('Not enough allowance to cover the deposit!');
}
}
else {
const mintValue = baseCost + BigInt(tx.operatorTip);
if ((await this.getAllowanceL1(baseTokenAddress)) < mintValue) {
throw new Error('Not enough base token allowance to cover the deposit!');
}
if (isAddressEq(tx.token, ETH_ADDRESS_IN_CONTRACTS) ||
isAddressEq(tx.token, baseTokenAddress)) {
(_a = tx.overrides).value ?? (_a.value = tx.amount);
}
else {
(_b = tx.overrides).value ?? (_b.value = 0);
if ((await this.getAllowanceL1(tx.token)) < dummyAmount) {
throw new Error('Not enough token allowance to cover the deposit!');
}
}
}
// Deleting the explicit gas limits in the fee estimation
// in order to prevent the situation where the transaction
// fails because the user does not have enough balance
const estimationOverrides = { ...tx.overrides };
delete estimationOverrides.gasPrice;
delete estimationOverrides.maxFeePerGas;
delete estimationOverrides.maxPriorityFeePerGas;
const l1GasLimit = await this.estimateGasDeposit({
...tx,
amount: dummyAmount,
overrides: estimationOverrides,
l2GasLimit: tx.l2GasLimit,
});
const fullCost = {
baseCost,
l1GasLimit,
l2GasLimit: BigInt(tx.l2GasLimit),
};
if (tx.overrides.gasPrice) {
fullCost.gasPrice = BigInt(tx.overrides.gasPrice);
}
else {
fullCost.maxFeePerGas = BigInt(tx.overrides.maxFeePerGas);
fullCost.maxPriorityFeePerGas = BigInt(tx.overrides.maxPriorityFeePerGas);
}
return fullCost;
}
/**
* Returns the transaction confirmation data that is part of `L2->L1` message.
*
* @param txHash The hash of the L2 transaction where the message was initiated.
* @param [index=0] In case there were multiple transactions in one message, you may pass an index of the
* transaction which confirmation data should be fetched.
* @throws {Error} If log proof can not be found.
*/
async getPriorityOpConfirmation(txHash, index = 0) {
return this._providerL2().getPriorityOpConfirmation(txHash, index);
}
async _getWithdrawalLog(withdrawalHash, index = 0) {
const hash = ethers.hexlify(withdrawalHash);
const receipt = await this._providerL2().getTransactionReceipt(hash);
if (!receipt) {
throw new Error('Transaction is not mined!');
}
const log = receipt.logs.filter(log => isAddressEq(log.address, L1_MESSENGER_ADDRESS) &&
log.topics[0] === ethers.id('L1MessageSent(address,bytes32,bytes)'))[index];
return {
log,
l1BatchTxId: receipt.l1BatchTxIndex,
};
}
async _getWithdrawalL2ToL1Log(withdrawalHash, index = 0) {
const hash = ethers.hexlify(withdrawalHash);
const receipt = await this._providerL2().getTransactionReceipt(hash);
if (!receipt) {
throw new Error('Transaction is not mined!');
}
const messages = Array.from(receipt.l2ToL1Logs.entries()).filter(([, log]) => isAddressEq(log.sender, L1_MESSENGER_ADDRESS));
const [l2ToL1LogIndex, l2ToL1Log] = messages[index];
return {
l2ToL1LogIndex,
l2ToL1Log,
};
}
/**
* @deprecated In favor of {@link getFinalizeWithdrawalParams}.
*
* Returns the {@link FinalizeWithdrawalParams parameters} required for finalizing a withdrawal from the
* withdrawal transaction's log on the L1 network.
*
* @param withdrawalHash Hash of the L2 transaction where the withdrawal was initiated.
* @param [index=0] In case there were multiple withdrawals in one transaction, you may pass an index of the
* withdrawal you want to finalize.
* @throws {Error} If log proof can not be found.
*/
async finalizeWithdrawalParams(withdrawalHash, index = 0) {
const { log, l1BatchTxId } = await this._getWithdrawalLog(withdrawalHash, index);
const { l2ToL1LogIndex } = await this._getWithdrawalL2ToL1Log(withdrawalHash, index);
const sender = ethers.dataSlice(log.topics[1], 12);
const proof = await this._providerL2().getLogProof(withdrawalHash, l2ToL1LogIndex);
if (!proof) {
throw new Error('Log proof not found!');
}
const message = ethers.AbiCoder.defaultAbiCoder().decode(['bytes'], log.data)[0];
return {
l1BatchNumber: log.l1BatchNumber,
l2MessageIndex: proof.id,
l2TxNumberInBlock: l1BatchTxId,
message,
sender,
proof: proof.proof,
};
}
/**
* Returns the {@link FinalizeWithdrawalParams parameters} required for finalizing a withdrawal from the
* withdrawal transaction's log on the L2 network. This struct is @deprecated in favor of {@link getFinalizeDepositParams}.
*
* @param withdrawalHash Hash of the L2 transaction where the withdrawal was initiated.
* @param [index=0] In case there were multiple withdrawals in one transaction, you may pass an index of the
* withdrawal you want to finalize.
* @throws {Error} If log proof can not be found.
*/
async getFinalizeWithdrawalParams(withdrawalHash