UNPKG

@arbitrum/sdk

Version:

Typescript library client-side interactions with Arbitrum

827 lines (826 loc) 42.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EthL1L3Bridger = exports.Erc20L1L3Bridger = exports.TeleportationType = void 0; const ethers_1 = require("ethers"); const IERC20__factory_1 = require("../abi/factories/IERC20__factory"); const L1GatewayRouter__factory_1 = require("../abi/factories/L1GatewayRouter__factory"); const IL2ForwarderFactory__factory_1 = require("../abi/factories/IL2ForwarderFactory__factory"); const L2GatewayToken__factory_1 = require("../abi/factories/L2GatewayToken__factory"); const IL1Teleporter__factory_1 = require("../abi/factories/IL1Teleporter__factory"); const address_1 = require("../dataEntities/address"); const errors_1 = require("../dataEntities/errors"); const networks_1 = require("../dataEntities/networks"); const signerOrProvider_1 = require("../dataEntities/signerOrProvider"); const ParentToChildMessage_1 = require("../message/ParentToChildMessage"); const ParentToChildMessageCreator_1 = require("../message/ParentToChildMessageCreator"); const ParentToChildMessageGasEstimator_1 = require("../message/ParentToChildMessageGasEstimator"); const ParentTransaction_1 = require("../message/ParentTransaction"); const erc20Bridger_1 = require("./erc20Bridger"); const Inbox__factory_1 = require("../abi/factories/Inbox__factory"); const utils_1 = require("ethers/lib/utils"); const ERC20__factory_1 = require("../abi/factories/ERC20__factory"); const IL2ForwarderPredictor__factory_1 = require("../abi/factories/IL2ForwarderPredictor__factory"); const IInbox__factory_1 = require("../abi/factories/IInbox__factory"); var TeleportationType; (function (TeleportationType) { /** * Teleporting to an ETH fee L3 */ TeleportationType[TeleportationType["Standard"] = 0] = "Standard"; /** * Teleporting the fee token to a custom fee L3 */ TeleportationType[TeleportationType["OnlyGasToken"] = 1] = "OnlyGasToken"; /** * Teleporting a non-fee token to a custom fee L3 */ TeleportationType[TeleportationType["NonGasTokenToCustomGas"] = 2] = "NonGasTokenToCustomGas"; })(TeleportationType || (exports.TeleportationType = TeleportationType = {})); /** * Base functionality for L1 to L3 bridging. */ class BaseL1L3Bridger { constructor(l3Network) { this.defaultGasPricePercentIncrease = ethers_1.BigNumber.from(500); this.defaultGasLimitPercentIncrease = ethers_1.BigNumber.from(100); const l2Network = (0, networks_1.getArbitrumNetworks)().find(network => network.chainId === l3Network.parentChainId); if (!l2Network) { throw new errors_1.ArbSdkError(`Unknown arbitrum network chain id: ${l3Network.parentChainId}`); } this.l1Network = { chainId: l2Network.parentChainId }; this.l2Network = l2Network; this.l3Network = l3Network; } /** * Check the signer/provider matches the l1Network, throws if not * @param sop */ async _checkL1Network(sop) { await signerOrProvider_1.SignerProviderUtils.checkNetworkMatches(sop, this.l1Network.chainId); } /** * Check the signer/provider matches the l2Network, throws if not * @param sop */ async _checkL2Network(sop) { await signerOrProvider_1.SignerProviderUtils.checkNetworkMatches(sop, this.l2Network.chainId); } /** * Check the signer/provider matches the l3Network, throws if not * @param sop */ async _checkL3Network(sop) { await signerOrProvider_1.SignerProviderUtils.checkNetworkMatches(sop, this.l3Network.chainId); } _percentIncrease(num, increase) { return num.add(num.mul(increase).div(100)); } _getTxHashFromTxRef(txRef) { if ('txHash' in txRef) { return txRef.txHash; } else if ('tx' in txRef) { return txRef.tx.hash; } else { return txRef.txReceipt.transactionHash; } } async _getTxFromTxRef(txRef, provider) { if ('tx' in txRef) { return txRef.tx; } return ParentTransaction_1.ParentTransactionReceipt.monkeyPatchContractCallWait(await provider.getTransaction(this._getTxHashFromTxRef(txRef))); } async _getTxReceiptFromTxRef(txRef, provider) { if ('txReceipt' in txRef) { return txRef.txReceipt; } return new ParentTransaction_1.ParentContractCallTransactionReceipt(await provider.getTransactionReceipt(this._getTxHashFromTxRef(txRef))); } } /** * Bridger for moving ERC20 tokens from L1 to L3 */ class Erc20L1L3Bridger extends BaseL1L3Bridger { constructor(l3Network) { super(l3Network); /** * Default gas limit for L2ForwarderFactory.callForwarder of 1,000,000 * * Measured Standard: 361746 * * Measured OnlyGasToken: 220416 * * Measured NonGasTokenToCustomGas: 373449 */ this.l2ForwarderFactoryDefaultGasLimit = ethers_1.BigNumber.from(1000000); this.skipL1GasTokenMagic = ethers_1.ethers.utils.getAddress(ethers_1.ethers.utils.hexDataSlice(ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.toUtf8Bytes('SKIP_FEE_TOKEN')), 0, 20)); this.l2Erc20Bridger = new erc20Bridger_1.Erc20Bridger(this.l2Network); this.l3Erc20Bridger = new erc20Bridger_1.Erc20Bridger(this.l3Network); if (!this.l2Network.teleporter) { throw new errors_1.ArbSdkError(`L2 network ${this.l2Network.name} does not have teleporter contracts`); } if (this.l3Network.nativeToken && this.l3Network.nativeToken !== ethers_1.ethers.constants.AddressZero) { this.l2GasTokenAddress = this.l3Network.nativeToken; } this.teleporter = this.l2Network.teleporter; } /** * If the L3 network uses a custom gas token, return the address of that token on L1. * If the fee token is not available on L1, does not use 18 decimals on L1 and L2, or the L3 network uses ETH for fees, throw. */ async getGasTokenOnL1(l1Provider, l2Provider) { // if the L3 network uses ETH for fees, early return zero if (!this.l2GasTokenAddress) throw new errors_1.ArbSdkError('L3 uses ETH for gas'); // if we've already fetched the L1 fee token address, early return it if (this._l1FeeTokenAddress) return this._l1FeeTokenAddress; await this._checkL1Network(l1Provider); await this._checkL2Network(l2Provider); let l1FeeTokenAddress; try { l1FeeTokenAddress = await this.l2Erc20Bridger.getParentErc20Address(this.l2GasTokenAddress, l2Provider); } catch (e) { // if the error is a CALL_EXCEPTION, we can't find the token on L1 // if the error is something else, rethrow if (e.code !== 'CALL_EXCEPTION') { throw e; } } if (!l1FeeTokenAddress || l1FeeTokenAddress === ethers_1.ethers.constants.AddressZero) { throw new errors_1.ArbSdkError('L1 gas token not found. Use skipGasToken when depositing'); } // make sure both the L1 and L2 tokens have 18 decimals if ((await ERC20__factory_1.ERC20__factory.connect(l1FeeTokenAddress, l1Provider).decimals()) !== 18) { throw new errors_1.ArbSdkError('L1 gas token has incorrect decimals. Use skipGasToken when depositing'); } if ((await ERC20__factory_1.ERC20__factory.connect(this.l2GasTokenAddress, l2Provider).decimals()) !== 18) { throw new errors_1.ArbSdkError('L2 gas token has incorrect decimals. Use skipGasToken when depositing'); } if (await this.l1TokenIsDisabled(l1FeeTokenAddress, l1Provider)) { throw new errors_1.ArbSdkError('L1 gas token is disabled on the L1 to L2 token bridge. Use skipGasToken when depositing'); } if (await this.l2TokenIsDisabled(this.l2GasTokenAddress, l2Provider)) { throw new errors_1.ArbSdkError('L2 gas token is disabled on the L2 to L3 token bridge. Use skipGasToken when depositing'); } return (this._l1FeeTokenAddress = l1FeeTokenAddress); } /** * Get the corresponding L2 token address for the provided L1 token */ getL2Erc20Address(erc20L1Address, l1Provider) { return this.l2Erc20Bridger.getChildErc20Address(erc20L1Address, l1Provider); } /** * Get the corresponding L3 token address for the provided L1 token */ async getL3Erc20Address(erc20L1Address, l1Provider, l2Provider) { return this.l3Erc20Bridger.getChildErc20Address(await this.getL2Erc20Address(erc20L1Address, l1Provider), l2Provider); } /** * Given an L1 token's address, get the address of the token's L1 <-> L2 gateway on L1 */ getL1L2GatewayAddress(erc20L1Address, l1Provider) { return this.l2Erc20Bridger.getParentGatewayAddress(erc20L1Address, l1Provider); } /** * Get the address of the L2 <-> L3 gateway on L2 given an L1 token address */ async getL2L3GatewayAddress(erc20L1Address, l1Provider, l2Provider) { const l2Token = await this.getL2Erc20Address(erc20L1Address, l1Provider); return this.l3Erc20Bridger.getParentGatewayAddress(l2Token, l2Provider); } /** * Get the L1 token contract at the provided address * Note: This function just returns a typed ethers object for the provided address, it doesn't * check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity * of any of the underlying functions on that contract. */ getL1TokenContract(l1TokenAddr, l1Provider) { return IERC20__factory_1.IERC20__factory.connect(l1TokenAddr, l1Provider); } /** * Get the L2 token contract at the provided address * Note: This function just returns a typed ethers object for the provided address, it doesn't * check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity * of any of the underlying functions on that contract. */ getL2TokenContract(l2TokenAddr, l2Provider) { return L2GatewayToken__factory_1.L2GatewayToken__factory.connect(l2TokenAddr, l2Provider); } /** * Get the L3 token contract at the provided address * Note: This function just returns a typed ethers object for the provided address, it doesn't * check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity * of any of the underlying functions on that contract. */ getL3TokenContract(l3TokenAddr, l3Provider) { return L2GatewayToken__factory_1.L2GatewayToken__factory.connect(l3TokenAddr, l3Provider); } /** * Whether the L1 token has been disabled on the L1 <-> L2 router given an L1 token address */ async l1TokenIsDisabled(l1TokenAddress, l1Provider) { return this.l2Erc20Bridger.isDepositDisabled(l1TokenAddress, l1Provider); } /** * Whether the L2 token has been disabled on the L2 <-> L3 router given an L2 token address */ async l2TokenIsDisabled(l2TokenAddress, l2Provider) { return this.l3Erc20Bridger.isDepositDisabled(l2TokenAddress, l2Provider); } /** * Given some L2Forwarder parameters, get the address of the L2Forwarder contract */ async l2ForwarderAddress(owner, routerOrInbox, destinationAddress, l1OrL2Provider) { const chainId = (await l1OrL2Provider.getNetwork()).chainId; let predictor; if (chainId === this.l1Network.chainId) { predictor = this.teleporter.l1Teleporter; } else if (chainId === this.l2Network.chainId) { predictor = this.teleporter.l2ForwarderFactory; } else { throw new errors_1.ArbSdkError(`Unknown chain id: ${chainId}`); } return IL2ForwarderPredictor__factory_1.IL2ForwarderPredictor__factory.connect(predictor, l1OrL2Provider).l2ForwarderAddress(owner, routerOrInbox, destinationAddress); } /** * Get a tx request to approve tokens for teleportation. * The tokens will be approved for L1Teleporter. */ async getApproveTokenRequest(params) { const iface = IERC20__factory_1.IERC20__factory.createInterface(); const data = iface.encodeFunctionData('approve', [ this.teleporter.l1Teleporter, params.amount || ethers_1.ethers.constants.MaxUint256, ]); return { to: params.erc20L1Address, data, value: 0, }; } /** * Approve tokens for teleportation. * The tokens will be approved for L1Teleporter. */ async approveToken(params) { await this._checkL1Network(params.l1Signer); const approveRequest = 'txRequest' in params ? params.txRequest : await this.getApproveTokenRequest(params); return params.l1Signer.sendTransaction(Object.assign(Object.assign({}, approveRequest), params.overrides)); } /** * Get a tx request to approve the L3's fee token for teleportation. * The tokens will be approved for L1Teleporter. * Will throw if the L3 network uses ETH for fees or the fee token doesn't exist on L1. */ async getApproveGasTokenRequest(params) { return this.getApproveTokenRequest({ erc20L1Address: await this.getGasTokenOnL1(params.l1Provider, params.l2Provider), amount: params.amount, }); } /** * Approve the L3's fee token for teleportation. * The tokens will be approved for L1Teleporter. * Will throw if the L3 network uses ETH for fees or the fee token doesn't exist on L1. */ async approveGasToken(params) { await this._checkL1Network(params.l1Signer); const approveRequest = 'txRequest' in params ? params.txRequest : await this.getApproveGasTokenRequest({ l1Provider: params.l1Signer.provider, l2Provider: params.l2Provider, amount: params.amount, }); return params.l1Signer.sendTransaction(Object.assign(Object.assign({}, approveRequest), params.overrides)); } /** * Get a tx request for teleporting some tokens from L1 to L3. * Also returns the amount of fee tokens required for teleportation. */ async getDepositRequest(params) { (0, networks_1.assertArbitrumNetworkHasTokenBridge)(this.l2Network); (0, networks_1.assertArbitrumNetworkHasTokenBridge)(this.l3Network); const l1Provider = 'l1Provider' in params ? params.l1Provider : params.l1Signer.provider; await this._checkL1Network(l1Provider); await this._checkL2Network(params.l2Provider); await this._checkL3Network(params.l3Provider); const from = 'from' in params ? params.from : await params.l1Signer.getAddress(); let l1FeeToken; // if the l3 uses eth for fees, set to zero if (!this.l2GasTokenAddress) { l1FeeToken = ethers_1.ethers.constants.AddressZero; } // if the l3 uses custom fee but the user opts to skip payment, set to magic address else if (params.skipGasToken) { l1FeeToken = this.skipL1GasTokenMagic; } // if the l3 uses custom fee and the user opts to not skip, try to get the token // will throw if the token doesn't exist on L1 or is unsupported else { l1FeeToken = await this.getGasTokenOnL1(l1Provider, params.l2Provider); } const partialTeleportParams = { l1Token: params.erc20L1Address, l3FeeTokenL1Addr: l1FeeToken, l1l2Router: this.l2Network.tokenBridge.parentGatewayRouter, l2l3RouterOrInbox: l1FeeToken && (0, utils_1.getAddress)(params.erc20L1Address) === (0, utils_1.getAddress)(l1FeeToken) ? this.l3Network.ethBridge.inbox : this.l3Network.tokenBridge.parentGatewayRouter, to: params.destinationAddress || from, amount: params.amount, }; const { teleportParams, costs } = await this._fillPartialTeleportParams(partialTeleportParams, params.retryableOverrides || {}, l1Provider, params.l2Provider, params.l3Provider); const data = IL1Teleporter__factory_1.IL1Teleporter__factory.createInterface().encodeFunctionData('teleport', [teleportParams]); return { txRequest: { to: this.teleporter.l1Teleporter, data, value: costs.ethAmount, }, gasTokenAmount: costs.feeTokenAmount, }; } /** * Execute a teleportation of some tokens from L1 to L3. */ async deposit(params) { await this._checkL1Network(params.l1Signer); const depositRequest = 'txRequest' in params ? params.txRequest : (await this.getDepositRequest(params)).txRequest; const tx = await params.l1Signer.sendTransaction(Object.assign(Object.assign({}, depositRequest), params.overrides)); return ParentTransaction_1.ParentTransactionReceipt.monkeyPatchContractCallWait(tx); } /** * Given a teleportation tx, get the L1Teleporter parameters, L2Forwarder parameters, and L2Forwarder address */ async getDepositParameters(params) { await this._checkL1Network(params.l1Provider); await this._checkL2Network(params.l2Provider); const tx = await this._getTxFromTxRef(params, params.l1Provider); const txReceipt = await tx.wait(); const l1l2Messages = await this._getL1ToL2Messages(txReceipt, params.l2Provider); const l2ForwarderParams = this._decodeCallForwarderCalldata(l1l2Messages.l2ForwarderFactoryRetryable.messageData.data); const l2ForwarderAddress = this.l2ForwarderAddress(l2ForwarderParams.owner, l2ForwarderParams.routerOrInbox, l2ForwarderParams.to, params.l2Provider); const teleportParams = this._decodeTeleportCalldata(tx.data); return { teleportParams, l2ForwarderParams, l2ForwarderAddress, }; } /** * Fetch the cross chain messages and their status * * Can provide either the txHash, the tx, or the txReceipt */ async getDepositStatus(params) { await this._checkL1Network(params.l1Provider); await this._checkL2Network(params.l2Provider); await this._checkL3Network(params.l3Provider); const l1TxReceipt = await this._getTxReceiptFromTxRef(params, params.l1Provider); const partialResult = await this._getL1ToL2Messages(l1TxReceipt, params.l2Provider); const factoryRedeem = await partialResult.l2ForwarderFactoryRetryable.getSuccessfulRedeem(); const l2l3Message = factoryRedeem.status === ParentToChildMessage_1.ParentToChildMessageStatus.REDEEMED ? (await new ParentTransaction_1.ParentTransactionReceipt(factoryRedeem.childTxReceipt).getParentToChildMessages(params.l3Provider))[0] : undefined; // check if we got a race condition where another teleportation front ran the l2 forwarder factory call // if the balance is 0, l1l2TokenBridgeRetryable is redeemed, and l2ForwarderFactoryRetryable failed, // then another teleportation front ran the l2 forwarder factory call // set a flag to indicate this let l2ForwarderFactoryRetryableFrontRan = false; const l1l2TokenBridgeRetryableStatus = await partialResult.l1l2TokenBridgeRetryable.status(); if (l1l2TokenBridgeRetryableStatus === ParentToChildMessage_1.ParentToChildMessageStatus.REDEEMED && factoryRedeem.status === ParentToChildMessage_1.ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD) { // decoding the factory call is the most reliable way to get the owner and other parameters const decodedFactoryCall = this._decodeCallForwarderCalldata(partialResult.l2ForwarderFactoryRetryable.messageData.data); // get the token balance of the l2 forwarder // we only do this check if the token bridge retryable has been redeemed, otherwise the token might not exist const balance = await IERC20__factory_1.IERC20__factory.connect(decodedFactoryCall.l2Token, params.l2Provider).balanceOf(await this.l2ForwarderAddress(decodedFactoryCall.owner, decodedFactoryCall.routerOrInbox, decodedFactoryCall.to, params.l2Provider)); l2ForwarderFactoryRetryableFrontRan = balance.isZero(); } return Object.assign(Object.assign({}, partialResult), { l2l3TokenBridgeRetryable: l2l3Message, l2ForwarderFactoryRetryableFrontRan, completed: (await (l2l3Message === null || l2l3Message === void 0 ? void 0 : l2l3Message.status())) === ParentToChildMessage_1.ParentToChildMessageStatus.REDEEMED }); } /** * Get the type of teleportation from the l1Token and l3FeeTokenL1Addr teleport parameters */ teleportationType(partialTeleportParams) { if (partialTeleportParams.l3FeeTokenL1Addr === ethers_1.ethers.constants.AddressZero) { return TeleportationType.Standard; } else if ((0, utils_1.getAddress)(partialTeleportParams.l1Token) === (0, utils_1.getAddress)(partialTeleportParams.l3FeeTokenL1Addr)) { return TeleportationType.OnlyGasToken; } else { return TeleportationType.NonGasTokenToCustomGas; } } /** * Estimate the gasLimit and maxSubmissionFee for a token bridge retryable */ async _getTokenBridgeGasEstimates(params) { const parentGateway = L1GatewayRouter__factory_1.L1GatewayRouter__factory.connect(params.parentGatewayAddress, params.parentProvider); const outboundCalldata = await parentGateway.getOutboundCalldata(params.parentErc20Address, params.from, params.to, params.amount, '0x'); const estimates = await new ParentToChildMessageGasEstimator_1.ParentToChildMessageGasEstimator(params.childProvider).estimateAll({ to: await parentGateway.counterpartGateway(), data: outboundCalldata, from: parentGateway.address, l2CallValue: params.isWeth ? params.amount : ethers_1.BigNumber.from(0), excessFeeRefundAddress: params.to, callValueRefundAddress: new address_1.Address(params.from).applyAlias().value, }, params.parentGasPrice, params.parentProvider); return { gasLimit: estimates.gasLimit, maxSubmissionFee: estimates.maxSubmissionCost, }; } /** * Estimate the gasLimit and maxSubmissionFee for the L1 to L2 token bridge leg of a teleportation */ async _getL1L2TokenBridgeGasEstimates(params) { (0, networks_1.assertArbitrumNetworkHasTokenBridge)(this.l2Network); const parentGatewayAddress = await this.getL1L2GatewayAddress(params.l1Token, params.l1Provider); return this._getTokenBridgeGasEstimates({ parentProvider: params.l1Provider, childProvider: params.l2Provider, parentGasPrice: params.l1GasPrice, parentErc20Address: params.l1Token, parentGatewayAddress, from: this.teleporter.l1Teleporter, to: params.l2ForwarderAddress, amount: ethers_1.BigNumber.from(params.amount), isWeth: (0, utils_1.getAddress)(parentGatewayAddress) === (0, utils_1.getAddress)(this.l2Network.tokenBridge.parentWethGateway), }); } /** * Estimate the gasLimit and maxSubmissionFee for the L1 to L2 fee token bridge leg of a teleportation */ async _getL1L2FeeTokenBridgeGasEstimates(params) { (0, networks_1.assertArbitrumNetworkHasTokenBridge)(this.l2Network); if (params.l3FeeTokenL1Addr === this.skipL1GasTokenMagic) { return { gasLimit: ethers_1.BigNumber.from(0), maxSubmissionFee: ethers_1.BigNumber.from(0), }; } const parentGatewayAddress = await this.getL1L2GatewayAddress(params.l3FeeTokenL1Addr, params.l1Provider); return this._getTokenBridgeGasEstimates({ parentProvider: params.l1Provider, childProvider: params.l2Provider, parentGasPrice: params.l1GasPrice, parentErc20Address: params.l3FeeTokenL1Addr, parentGatewayAddress, from: this.teleporter.l1Teleporter, to: params.l2ForwarderAddress, amount: params.feeTokenAmount, isWeth: (0, utils_1.getAddress)(parentGatewayAddress) === (0, utils_1.getAddress)(this.l2Network.tokenBridge.parentWethGateway), }); } /** * Estimate the gasLimit and maxSubmissionFee for L2ForwarderFactory.callForwarder leg of a teleportation. * Gas limit is hardcoded to 1,000,000 */ async _getL2ForwarderFactoryGasEstimates(l1GasPrice, l1Provider) { const inbox = Inbox__factory_1.Inbox__factory.connect(this.l2Network.ethBridge.inbox, l1Provider); const maxSubmissionFee = await inbox.calculateRetryableSubmissionFee(this._l2ForwarderFactoryCalldataSize(), l1GasPrice); return { gasLimit: this.l2ForwarderFactoryDefaultGasLimit, maxSubmissionFee, }; } /** * Estimate the gasLimit and maxSubmissionFee for the L2 -> L3 leg of a teleportation. */ async _getL2L3BridgeGasEstimates(params) { (0, networks_1.assertArbitrumNetworkHasTokenBridge)(this.l3Network); const teleportationType = this.teleportationType(params.partialTeleportParams); if (teleportationType === TeleportationType.NonGasTokenToCustomGas && params.partialTeleportParams.l3FeeTokenL1Addr === this.skipL1GasTokenMagic) { // we aren't paying for the retryable to L3 return { gasLimit: ethers_1.BigNumber.from(0), maxSubmissionFee: ethers_1.BigNumber.from(0), }; } else if (teleportationType === TeleportationType.OnlyGasToken) { // we are bridging the fee token to l3, this will not go through the l2l3 token bridge, instead it's just a regular retryable const estimate = await new ParentToChildMessageGasEstimator_1.ParentToChildMessageGasEstimator(params.l3Provider).estimateAll({ to: params.partialTeleportParams.to, data: '0x', from: params.l2ForwarderAddress, // l2CallValue will be amount less the fees in reality l2CallValue: ethers_1.BigNumber.from(params.partialTeleportParams.amount), excessFeeRefundAddress: params.partialTeleportParams.to, callValueRefundAddress: params.partialTeleportParams.to, }, params.l2GasPrice, params.l2Provider); return { gasLimit: estimate.gasLimit, maxSubmissionFee: estimate.maxSubmissionCost, }; } else { // we are bridging a non fee token to l3, this will go through the token bridge const parentGatewayAddress = await this.getL2L3GatewayAddress(params.partialTeleportParams.l1Token, params.l1Provider, params.l2Provider); return this._getTokenBridgeGasEstimates({ parentProvider: params.l2Provider, childProvider: params.l3Provider, parentGasPrice: params.l2GasPrice, parentErc20Address: await this.getL2Erc20Address(params.partialTeleportParams.l1Token, params.l1Provider), parentGatewayAddress, from: params.l2ForwarderAddress, to: params.partialTeleportParams.to, amount: ethers_1.BigNumber.from(params.partialTeleportParams.amount), isWeth: (0, utils_1.getAddress)(parentGatewayAddress) === (0, utils_1.getAddress)(this.l3Network.tokenBridge.parentWethGateway), }); } } /** * Given TeleportParams without the gas parameters, return TeleportParams with gas parameters populated. * Does not modify the input parameters. */ async _fillPartialTeleportParams(partialTeleportParams, retryableOverrides, l1Provider, l2Provider, l3Provider) { // get gasLimit and submission cost for a retryable while respecting overrides const getRetryableGasValuesWithOverrides = async (overrides, getEstimates) => { var _a, _b, _c, _d, _e, _f, _g; let base; if (((_a = overrides === null || overrides === void 0 ? void 0 : overrides.gasLimit) === null || _a === void 0 ? void 0 : _a.base) && ((_b = overrides === null || overrides === void 0 ? void 0 : overrides.maxSubmissionFee) === null || _b === void 0 ? void 0 : _b.base)) { base = { gasLimit: overrides.gasLimit.base, maxSubmissionFee: overrides.maxSubmissionFee.base, }; } else { const calculated = await getEstimates(); base = { gasLimit: ((_c = overrides === null || overrides === void 0 ? void 0 : overrides.gasLimit) === null || _c === void 0 ? void 0 : _c.base) || calculated.gasLimit, maxSubmissionFee: ((_d = overrides === null || overrides === void 0 ? void 0 : overrides.maxSubmissionFee) === null || _d === void 0 ? void 0 : _d.base) || calculated.maxSubmissionFee, }; } const gasLimit = this._percentIncrease(base.gasLimit, ((_e = overrides === null || overrides === void 0 ? void 0 : overrides.gasLimit) === null || _e === void 0 ? void 0 : _e.percentIncrease) || this.defaultGasLimitPercentIncrease); const submissionFee = this._percentIncrease(base.maxSubmissionFee, ((_f = overrides === null || overrides === void 0 ? void 0 : overrides.maxSubmissionFee) === null || _f === void 0 ? void 0 : _f.percentIncrease) || ethers_1.BigNumber.from(0)); const minGasLimit = ((_g = overrides === null || overrides === void 0 ? void 0 : overrides.gasLimit) === null || _g === void 0 ? void 0 : _g.min) || ethers_1.BigNumber.from(0); return { gasLimit: gasLimit.gt(minGasLimit) ? gasLimit : minGasLimit, maxSubmissionFee: submissionFee, }; }; // get gas price while respecting overrides const applyGasPercentIncrease = async (overrides, getEstimate) => { return this._percentIncrease((overrides === null || overrides === void 0 ? void 0 : overrides.base) || (await getEstimate()), (overrides === null || overrides === void 0 ? void 0 : overrides.percentIncrease) || this.defaultGasPricePercentIncrease); }; const l1GasPrice = await applyGasPercentIncrease(retryableOverrides.l1GasPrice, () => l1Provider.getGasPrice()); const l2GasPrice = await applyGasPercentIncrease(retryableOverrides.l2GasPrice, () => l2Provider.getGasPrice()); const l3GasPrice = partialTeleportParams.l3FeeTokenL1Addr === this.skipL1GasTokenMagic ? ethers_1.BigNumber.from(0) : await applyGasPercentIncrease(retryableOverrides.l3GasPrice, () => l3Provider.getGasPrice()); const fakeRandomL2Forwarder = ethers_1.ethers.utils.hexlify(ethers_1.ethers.utils.randomBytes(20)); const l1l2TokenBridgeGasValues = await getRetryableGasValuesWithOverrides(retryableOverrides.l1l2TokenBridgeRetryableGas, () => this._getL1L2TokenBridgeGasEstimates({ l1Token: partialTeleportParams.l1Token, amount: partialTeleportParams.amount, l1GasPrice, l2ForwarderAddress: fakeRandomL2Forwarder, l1Provider, l2Provider, })); const l2ForwarderFactoryGasValues = await getRetryableGasValuesWithOverrides(retryableOverrides.l2ForwarderFactoryRetryableGas, () => this._getL2ForwarderFactoryGasEstimates(l1GasPrice, l1Provider)); const l2l3TokenBridgeGasValues = await getRetryableGasValuesWithOverrides(retryableOverrides.l2l3TokenBridgeRetryableGas, () => this._getL2L3BridgeGasEstimates({ partialTeleportParams, l2GasPrice, l1Provider, l2Provider, l3Provider, l2ForwarderAddress: fakeRandomL2Forwarder, })); let l1l2FeeTokenBridgeGasValues; if (this.teleportationType(partialTeleportParams) === TeleportationType.NonGasTokenToCustomGas) { l1l2FeeTokenBridgeGasValues = await getRetryableGasValuesWithOverrides(retryableOverrides.l1l2GasTokenBridgeRetryableGas, () => this._getL1L2FeeTokenBridgeGasEstimates({ l1GasPrice, feeTokenAmount: l2l3TokenBridgeGasValues.gasLimit .mul(l3GasPrice) .add(l2l3TokenBridgeGasValues.maxSubmissionFee), l3FeeTokenL1Addr: partialTeleportParams.l3FeeTokenL1Addr, l2ForwarderAddress: fakeRandomL2Forwarder, l1Provider, l2Provider, })); } else { // eth fee l3, or only bridging fee token. this retryable will not be created l1l2FeeTokenBridgeGasValues = { gasLimit: ethers_1.BigNumber.from(0), maxSubmissionFee: ethers_1.BigNumber.from(0), }; } const gasParams = { l2GasPriceBid: l2GasPrice, l3GasPriceBid: l3GasPrice, l1l2TokenBridgeGasLimit: l1l2TokenBridgeGasValues.gasLimit, l1l2FeeTokenBridgeGasLimit: l1l2FeeTokenBridgeGasValues.gasLimit, l2l3TokenBridgeGasLimit: l2l3TokenBridgeGasValues.gasLimit, l2ForwarderFactoryGasLimit: l2ForwarderFactoryGasValues.gasLimit, l2ForwarderFactoryMaxSubmissionCost: l2ForwarderFactoryGasValues.maxSubmissionFee, l1l2TokenBridgeMaxSubmissionCost: l1l2TokenBridgeGasValues.maxSubmissionFee, l1l2FeeTokenBridgeMaxSubmissionCost: l1l2FeeTokenBridgeGasValues.maxSubmissionFee, l2l3TokenBridgeMaxSubmissionCost: l2l3TokenBridgeGasValues.maxSubmissionFee, }; const teleportParams = Object.assign(Object.assign({}, partialTeleportParams), { gasParams }); const costs = await IL1Teleporter__factory_1.IL1Teleporter__factory.connect(this.teleporter.l1Teleporter, l1Provider).determineTypeAndFees(teleportParams); return { teleportParams, costs, }; } /** * @returns The size of the calldata for a call to L2ForwarderFactory.callForwarder */ _l2ForwarderFactoryCalldataSize() { const struct = { owner: ethers_1.ethers.constants.AddressZero, l2Token: ethers_1.ethers.constants.AddressZero, l3FeeTokenL2Addr: ethers_1.ethers.constants.AddressZero, routerOrInbox: ethers_1.ethers.constants.AddressZero, to: ethers_1.ethers.constants.AddressZero, gasLimit: 0, gasPriceBid: 0, maxSubmissionCost: 0, }; const dummyCalldata = IL2ForwarderFactory__factory_1.IL2ForwarderFactory__factory.createInterface().encodeFunctionData('callForwarder', [struct]); return ethers_1.ethers.utils.hexDataLength(dummyCalldata) - 4; } /** * Given raw calldata for a teleport tx, decode the teleport parameters */ _decodeTeleportCalldata(data) { const iface = IL1Teleporter__factory_1.IL1Teleporter__factory.createInterface(); const decoded = iface.parseTransaction({ data }); if (decoded.functionFragment.name !== 'teleport') { throw new errors_1.ArbSdkError(`not a teleport tx`); } return decoded.args[0]; } /** * Given raw calldata for a callForwarder call, decode the parameters */ _decodeCallForwarderCalldata(data) { const iface = IL2ForwarderFactory__factory_1.IL2ForwarderFactory__factory.createInterface(); const decoded = iface.parseTransaction({ data }); if (decoded.functionFragment.name !== 'callForwarder') { throw new errors_1.ArbSdkError(`not callForwarder data`); } return decoded.args[0]; } async _getL1ToL2Messages(l1TxReceipt, l2Provider) { const l1l2Messages = await l1TxReceipt.getParentToChildMessages(l2Provider); let partialResult; if (l1l2Messages.length === 2) { partialResult = { l1l2TokenBridgeRetryable: l1l2Messages[0], l2ForwarderFactoryRetryable: l1l2Messages[1], l1l2GasTokenBridgeRetryable: undefined, }; } else { partialResult = { l1l2GasTokenBridgeRetryable: l1l2Messages[0], l1l2TokenBridgeRetryable: l1l2Messages[1], l2ForwarderFactoryRetryable: l1l2Messages[2], }; } return partialResult; } } exports.Erc20L1L3Bridger = Erc20L1L3Bridger; /** * Bridge ETH from L1 to L3 using a double retryable ticket */ class EthL1L3Bridger extends BaseL1L3Bridger { constructor(l3Network) { super(l3Network); if (l3Network.nativeToken && l3Network.nativeToken !== ethers_1.ethers.constants.AddressZero) { throw new errors_1.ArbSdkError(`L3 network ${l3Network.name} uses a custom fee token`); } } /** * Get a tx request to deposit ETH to L3 via a double retryable ticket */ async getDepositRequest(params) { const l1Provider = 'l1Provider' in params ? params.l1Provider : params.l1Signer.provider; await this._checkL1Network(l1Provider); await this._checkL2Network(params.l2Provider); await this._checkL3Network(params.l3Provider); const from = 'from' in params ? params.from : await params.l1Signer.getAddress(); const l3DestinationAddress = params.destinationAddress || from; const l2RefundAddress = params.l2RefundAddress || from; const l3TicketRequest = await ParentToChildMessageCreator_1.ParentToChildMessageCreator.getTicketCreationRequest({ to: l3DestinationAddress, data: '0x', from: new address_1.Address(from).applyAlias().value, l2CallValue: ethers_1.BigNumber.from(params.amount), excessFeeRefundAddress: l3DestinationAddress, callValueRefundAddress: l3DestinationAddress, }, params.l2Provider, params.l3Provider, params.l3TicketGasOverrides); const l2TicketRequest = await ParentToChildMessageCreator_1.ParentToChildMessageCreator.getTicketCreationRequest({ from, to: l3TicketRequest.txRequest.to, l2CallValue: ethers_1.BigNumber.from(l3TicketRequest.txRequest.value), data: ethers_1.ethers.utils.hexlify(l3TicketRequest.txRequest.data), excessFeeRefundAddress: l2RefundAddress, callValueRefundAddress: l2RefundAddress, }, l1Provider, params.l2Provider, params.l2TicketGasOverrides); return l2TicketRequest; } /** * Deposit ETH to L3 via a double retryable ticket */ async deposit(params) { await this._checkL1Network(params.l1Signer); const depositRequest = 'txRequest' in params ? params.txRequest : (await this.getDepositRequest(params)).txRequest; const tx = await params.l1Signer.sendTransaction(Object.assign(Object.assign({}, depositRequest), params.overrides)); return ParentTransaction_1.ParentTransactionReceipt.monkeyPatchContractCallWait(tx); } /** * Given an L1 transaction, get the retryable parameters for both l2 and l3 tickets */ async getDepositParameters(params) { await this._checkL1Network(params.l1Provider); const tx = await this._getTxFromTxRef(params, params.l1Provider); const l1l2TicketData = Object.assign(Object.assign({}, this._decodeCreateRetryableTicket(tx.data)), { l1Value: tx.value }); const l2l3TicketData = Object.assign(Object.assign({}, this._decodeCreateRetryableTicket(l1l2TicketData.data)), { l1Value: l1l2TicketData.l2CallValue }); return { l1l2TicketData, l2l3TicketData, }; } /** * Get the status of a deposit given an L1 tx receipt. Does not check if the tx is actually a deposit tx. * * @return Information regarding each step of the deposit * and `EthL1L3DepositStatus.completed` which indicates whether the deposit has fully completed. */ async getDepositStatus(params) { await this._checkL1Network(params.l1Provider); await this._checkL2Network(params.l2Provider); await this._checkL3Network(params.l3Provider); const l1TxReceipt = await this._getTxReceiptFromTxRef(params, params.l1Provider); const l1l2Message = (await l1TxReceipt.getParentToChildMessages(params.l2Provider))[0]; const l1l2Redeem = await l1l2Message.getSuccessfulRedeem(); if (l1l2Redeem.status != ParentToChildMessage_1.ParentToChildMessageStatus.REDEEMED) { return { l2Retryable: l1l2Message, l3Retryable: undefined, completed: false, }; } const l2l3Message = (await new ParentTransaction_1.ParentEthDepositTransactionReceipt(l1l2Redeem.childTxReceipt).getParentToChildMessages(params.l3Provider))[0]; if (l2l3Message === undefined) { throw new errors_1.ArbSdkError(`L2 to L3 message not found`); } return { l2Retryable: l1l2Message, l3Retryable: l2l3Message, completed: (await l2l3Message.status()) === ParentToChildMessage_1.ParentToChildMessageStatus.REDEEMED, }; } _decodeCreateRetryableTicket(data) { const iface = IInbox__factory_1.IInbox__factory.createInterface(); const decoded = iface.parseTransaction({ data }); if (decoded.functionFragment.name !== 'createRetryableTicket') { throw new errors_1.ArbSdkError(`not createRetryableTicket data`); } const args = decoded.args; return { destAddress: args[0], l2CallValue: args[1], maxSubmissionFee: args[2], excessFeeRefundAddress: args[3], callValueRefundAddress: args[4], gasLimit: args[5], maxFeePerGas: args[6], data: args[7], }; } } exports.EthL1L3Bridger = EthL1L3Bridger;