zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
1,405 lines (1,302 loc) • 83.6 kB
text/typescript
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 +