diff --git a/package.json b/package.json index 574ef3d45..2d7ce4a6b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@across-protocol/constants": "^3.1.19", "@across-protocol/contracts": "^3.0.16", "@across-protocol/sdk": "^3.2.16", - "@arbitrum/sdk": "^3.1.3", + "@arbitrum/sdk": "^4.0.2", "@consensys/linea-sdk": "^0.2.1", "@defi-wonderland/smock": "^2.3.5", "@eth-optimism/sdk": "^3.3.2", diff --git a/scripts/withdrawFromArbitrumOrbit.ts b/scripts/withdrawFromArbitrumOrbit.ts new file mode 100644 index 000000000..3efe2fdbf --- /dev/null +++ b/scripts/withdrawFromArbitrumOrbit.ts @@ -0,0 +1,117 @@ +// Submits a bridge from Arbitrum Orbit L2 to L1. +// For now, this script only supports WETH withdrawals on AlephZero. + +import { + ethers, + retrieveSignerFromCLIArgs, + getProvider, + ERC20, + TOKEN_SYMBOLS_MAP, + assert, + getL1TokenInfo, + Contract, + fromWei, + blockExplorerLink, + getNativeTokenSymbol, +} from "../src/utils"; +import { CONTRACT_ADDRESSES } from "../src/common"; +import { askYesNoQuestion } from "./utils"; + +import minimist from "minimist"; + +const cliArgs = ["amount", "chainId", "token"]; +const args = minimist(process.argv.slice(2), { + string: cliArgs, +}); + +// Example run: +// ts-node ./scripts/withdrawFromArbitrumOrbit.ts +// \ --amount 3000000000000000000 +// \ --chainId 41455 +// \ --token WETH +// \ --wallet gckms +// \ --keys bot1 + +export async function run(): Promise { + assert( + cliArgs.every((cliArg) => Object.keys(args).includes(cliArg)), + `Missing cliArg, expected: ${cliArgs}` + ); + const baseSigner = await retrieveSignerFromCLIArgs(); + const signerAddr = await baseSigner.getAddress(); + const chainId = parseInt(args.chainId); + const connectedSigner = baseSigner.connect(await getProvider(chainId)); + const l2Token = TOKEN_SYMBOLS_MAP[args.token]?.addresses[chainId]; + assert(l2Token, `${args.token} not found on chain ${chainId} in TOKEN_SYMBOLS_MAP`); + const l1TokenInfo = getL1TokenInfo(l2Token, chainId); + console.log("Fetched L1 token info:", l1TokenInfo); + const amount = args.amount; + const amountFromWei = ethers.utils.formatUnits(amount, l1TokenInfo.decimals); + console.log(`Amount to bridge from chain ${chainId}: ${amountFromWei} ${l2Token}`); + + const erc20 = new Contract(l2Token, ERC20.abi, connectedSigner); + const currentBalance = await erc20.balanceOf(signerAddr); + const nativeTokenSymbol = getNativeTokenSymbol(chainId); + const currentNativeBalance = await connectedSigner.getBalance(); + console.log( + `Current ${l1TokenInfo.symbol} balance for account ${signerAddr}: ${fromWei( + currentBalance, + l1TokenInfo.decimals + )} ${l2Token}` + ); + console.log( + `Current native ${nativeTokenSymbol} token balance for account ${signerAddr}: ${fromWei(currentNativeBalance, 18)}` + ); + + // Now, submit a withdrawal: + let contract: Contract, functionName: string, functionArgs: any[]; + if (l1TokenInfo.symbol !== nativeTokenSymbol) { + const arbErc20GatewayObj = CONTRACT_ADDRESSES[chainId].erc20Gateway; + contract = new Contract(arbErc20GatewayObj.address, arbErc20GatewayObj.abi, connectedSigner); + functionName = "outboundTransfer"; + functionArgs = [ + l1TokenInfo.address, // l1Token + signerAddr, // to + amount, // amount + "0x", // data + ]; + + console.log( + `Submitting ${functionName} on the Arbitrum ERC20 gateway router @ ${contract.address} with the following args: `, + ...functionArgs + ); + } else { + const arbSys = CONTRACT_ADDRESSES[chainId].arbSys; + contract = new Contract(arbSys.address, arbSys.abi, connectedSigner); + functionName = "withdrawEth"; + functionArgs = [ + signerAddr, // to + { value: amount }, + ]; + console.log( + `Submitting ${functionName} on the ArbSys contract @ ${contract.address} with the following args: `, + ...functionArgs + ); + } + + if (!(await askYesNoQuestion("\nDo you want to proceed?"))) { + return; + } + const withdrawal = await contract[functionName](...functionArgs); + console.log(`Submitted withdrawal: ${blockExplorerLink(withdrawal.hash, chainId)}.`); + const receipt = await withdrawal.wait(); + console.log("Receipt", receipt); +} + +if (require.main === module) { + run() + .then(async () => { + // eslint-disable-next-line no-process-exit + process.exit(0); + }) + .catch(async (error) => { + console.error("Process exited with", error); + // eslint-disable-next-line no-process-exit + process.exit(1); + }); +} diff --git a/scripts/withdrawFromOpStack.ts b/scripts/withdrawFromOpStack.ts index 3b7b7121b..e4ba5041d 100644 --- a/scripts/withdrawFromOpStack.ts +++ b/scripts/withdrawFromOpStack.ts @@ -60,7 +60,7 @@ export async function run(): Promise { l1TokenInfo.decimals )} ${l2Token}` ); - console.log(`Current ETH balance for account ${signerAddr}: ${fromWei(currentEthBalance, l1TokenInfo.decimals)}`); + console.log(`Current ETH balance for account ${signerAddr}: ${fromWei(currentEthBalance)}`); // First offer user option to unwrap WETH into ETH if (l1TokenInfo.symbol === "ETH") { diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index cdd9473ee..2c4b78e39 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -22,6 +22,7 @@ import ARBITRUM_ERC20_GATEWAY_ROUTER_L1_ABI from "./abi/ArbitrumErc20GatewayRout import ARBITRUM_ERC20_GATEWAY_L1_ABI from "./abi/ArbitrumErc20GatewayL1.json"; import ARBITRUM_ERC20_GATEWAY_L2_ABI from "./abi/ArbitrumErc20GatewayL2.json"; import ARBITRUM_OUTBOX_ABI from "./abi/ArbitrumOutbox.json"; +import ARBSYS_L2_ABI from "./abi/ArbSysL2.json"; import LINEA_MESSAGE_SERVICE_ABI from "./abi/LineaMessageService.json"; import LINEA_TOKEN_BRIDGE_ABI from "./abi/LineaTokenBridge.json"; import LINEA_USDC_BRIDGE_ABI from "./abi/LineaUsdcBridge.json"; @@ -132,6 +133,10 @@ export const CONTRACT_ADDRESSES: { address: "0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840", abi: ARBITRUM_OUTBOX_ABI, }, + orbitOutbox_41455: { + address: "0x73bb50c32a3BD6A1032aa5cFeA048fBDA3D6aF6e", + abi: ARBITRUM_OUTBOX_ABI, + }, orbitErc20GatewayRouter_42161: { address: "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef", abi: ARBITRUM_ERC20_GATEWAY_ROUTER_L1_ABI, @@ -334,8 +339,13 @@ export const CONTRACT_ADDRESSES: { }, 41455: { erc20Gateway: { + address: "0x2A5a79061b723BBF453ef7E07c583C750AFb9BD6", abi: ARBITRUM_ERC20_GATEWAY_L2_ABI, }, + arbSys: { + address: "0x0000000000000000000000000000000000000064", + abi: ARBSYS_L2_ABI, + }, }, 59144: { l2MessageService: { diff --git a/src/common/abi/ArbSysL2.json b/src/common/abi/ArbSysL2.json new file mode 100644 index 000000000..f3b864195 --- /dev/null +++ b/src/common/abi/ArbSysL2.json @@ -0,0 +1,82 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "destination", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "hash", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "position", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "arbBlockNum", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ethBlockNum", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "callvalue", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "L2ToL1Tx", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "destination", + "type": "address" + } + ], + "name": "withdrawEth", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/common/abi/ArbitrumErc20GatewayL2.json b/src/common/abi/ArbitrumErc20GatewayL2.json index 3ed24368a..48a1ae4af 100644 --- a/src/common/abi/ArbitrumErc20GatewayL2.json +++ b/src/common/abi/ArbitrumErc20GatewayL2.json @@ -9,5 +9,60 @@ ], "name": "DepositFinalized", "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "_l2ToL1Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_exitNum", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "WithdrawalInitiated", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "_token", "type": "address" }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "bytes", "name": "_data", "type": "bytes" } + ], + "name": "outboundTransfer", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "payable", + "type": "function" } ] diff --git a/src/finalizer/utils/arbStack.ts b/src/finalizer/utils/arbStack.ts index 0373c0cf3..b38e34723 100644 --- a/src/finalizer/utils/arbStack.ts +++ b/src/finalizer/utils/arbStack.ts @@ -1,4 +1,10 @@ -import { L2ToL1MessageStatus, L2TransactionReceipt, L2ToL1MessageWriter } from "@arbitrum/sdk"; +import { + ChildToParentMessageStatus, + ChildTransactionReceipt, + ChildToParentMessageWriter, + registerCustomArbitrumNetwork, + ArbitrumNetwork, +} from "@arbitrum/sdk"; import { winston, convertFromWei, @@ -15,41 +21,206 @@ import { compareAddressesSimple, CHAIN_IDs, TOKEN_SYMBOLS_MAP, + getProvider, + averageBlockTime, + paginatedEventQuery, + getNetworkName, + ethers, + getL2TokenAddresses, + getNativeTokenSymbol, + fromWei, } from "../../utils"; import { TokensBridged } from "../../interfaces"; import { HubPoolClient, SpokePoolClient } from "../../clients"; import { CONTRACT_ADDRESSES } from "../../common"; import { FinalizerPromise, CrossChainMessage } from "../types"; +let LATEST_MAINNET_BLOCK: number; +let MAINNET_BLOCK_TIME: number; + +type PartialArbitrumNetwork = Omit & { + challengePeriodSeconds: number; + registered: boolean; +}; +// These network configs are defined in the Arbitrum SDK, and we need to register them in the SDK's memory. +// We should export this out of a common file but we don't use this SDK elsewhere currentlyl. +export const ARB_ORBIT_NETWORK_CONFIGS: PartialArbitrumNetwork[] = [ + { + // Addresses are available here: + // https://raas.gelato.network/rollups/details/public/aleph-zero-evm + chainId: CHAIN_IDs.ALEPH_ZERO, + name: "Aleph Zero", + parentChainId: CHAIN_IDs.MAINNET, + ethBridge: { + bridge: "0x41Ec9456AB918f2aBA81F38c03Eb0B93b78E84d9", + inbox: "0x56D8EC76a421063e1907503aDd3794c395256AEb ", + sequencerInbox: "0xF75206c49c1694594E3e69252E519434f1579876", + outbox: CONTRACT_ADDRESSES[CHAIN_IDs.MAINNET][`orbitOutbox_${CHAIN_IDs.ALEPH_ZERO}`].address, + rollup: "0x1CA12290D954CFe022323b6A6Df92113ed6b1C98", + }, + challengePeriodSeconds: 6 * 60 * 60, // ~ 6 hours + retryableLifetimeSeconds: 7 * 24 * 60 * 60, + nativeToken: TOKEN_SYMBOLS_MAP.AZERO.addresses[CHAIN_IDs.MAINNET], + isTestnet: false, + registered: false, + // Must be set to true for L3's + isCustom: true, + }, +]; + +export function getOrbitNetwork(chainId: number): PartialArbitrumNetwork | undefined { + return ARB_ORBIT_NETWORK_CONFIGS.find((network) => network.chainId === chainId); +} +export function getArbitrumOrbitFinalizationTime(chainId: number): number { + return getOrbitNetwork(chainId)?.challengePeriodSeconds ?? 7 * 60 * 60 * 24; +} + export async function arbStackFinalizer( logger: winston.Logger, signer: Signer, hubPoolClient: HubPoolClient, spokePoolClient: SpokePoolClient ): Promise { + LATEST_MAINNET_BLOCK = hubPoolClient.latestBlockSearched; + const hubPoolProvider = await getProvider(hubPoolClient.chainId, logger); + MAINNET_BLOCK_TIME = (await averageBlockTime(hubPoolProvider)).average; + // Now that we know the L1 block time, we can calculate the confirmPeriodBlocks. + + ARB_ORBIT_NETWORK_CONFIGS.forEach((_networkConfig) => { + if (_networkConfig.registered) { + return; + } + const networkConfig: ArbitrumNetwork = { + ..._networkConfig, + confirmPeriodBlocks: _networkConfig.challengePeriodSeconds / MAINNET_BLOCK_TIME, + }; + // The network config object should be full now. + registerCustomArbitrumNetwork(networkConfig); + _networkConfig.registered = true; + }); + const { chainId } = spokePoolClient; + const networkName = getNetworkName(chainId); - // Arbitrum takes 7 days to finalize withdrawals, so don't look up events younger than that. + // Arbitrum orbit takes 7 days to finalize withdrawals, so don't look up events younger than that. const redis = await getRedisCache(logger); const latestBlockToFinalize = await getBlockForTimestamp( chainId, - getCurrentTime() - 7 * 60 * 60 * 24, + getCurrentTime() - getArbitrumOrbitFinalizationTime(chainId), undefined, redis ); logger.debug({ - at: "Finalizer#ArbitrumFinalizer", - message: "Arbitrum TokensBridged event filter", + at: `Finalizer#${networkName}Finalizer`, + message: `${networkName} TokensBridged event filter`, toBlock: latestBlockToFinalize, }); // Skip events that are likely not past the seven day challenge period. const olderTokensBridgedEvents = spokePoolClient.getTokensBridged().filter( (e) => e.blockNumber <= latestBlockToFinalize && - // USDC withdrawals for Arbitrum should be finalized via the CCTP Finalizer. - !compareAddressesSimple(e.l2TokenAddress, TOKEN_SYMBOLS_MAP["USDC"].addresses[chainId]) + // USDC withdrawals for chains that support CCTP should be finalized via the CCTP Finalizer. + // The way we detect if a chain supports CCTP is by checking if there is a `cctpMessageTransmitter` + // entry in CONTRACT_ADDRESSES + (CONTRACT_ADDRESSES[chainId].cctpMessageTransmitter === undefined || + !compareAddressesSimple(e.l2TokenAddress, TOKEN_SYMBOLS_MAP["USDC"].addresses[chainId])) ); + // Experimental feature: Add in all ETH withdrawals from Arbitrum Orbit chain to the finalizer. This will help us + // in the short term to automate ETH withdrawals from Lite chains, which can build up ETH balances over time + // and because they are lite chains, our only way to withdraw them is to initiate a slow bridge of ETH from the + // the lite chain to Ethereum. + const withdrawalToAddresses: string[] = process.env.FINALIZER_WITHDRAWAL_TO_ADDRESSES + ? JSON.parse(process.env.FINALIZER_WITHDRAWAL_TO_ADDRESSES).map((address) => ethers.utils.getAddress(address)) + : []; + if (getOrbitNetwork(chainId) !== undefined && withdrawalToAddresses.length > 0) { + // ERC20 withdrawals emit events in the erc20Gateway. + // Native token withdrawals emit events in the ArbSys contract. + const l2ArbSys = CONTRACT_ADDRESSES[chainId].arbSys; + const arbSys = new Contract(l2ArbSys.address, l2ArbSys.abi, spokePoolClient.spokePool.provider); + const l2Erc20Gateway = CONTRACT_ADDRESSES[chainId].erc20Gateway; + const arbitrumGateway = new Contract( + l2Erc20Gateway.address, + l2Erc20Gateway.abi, + spokePoolClient.spokePool.provider + ); + // TODO: For this to work for ArbitrumOrbit, we need to first query ERC20GatewayRouter.getGateway(l2Token) to + // get the ERC20 Gateway. Then, on the ERC20 Gateway, query the WithdrawalInitiated event. + // See example txn: https://evm-explorer.alephzero.org/tx/0xb493174af0822c1a5a5983c2cbd4fe74055ee70409c777b9c665f417f89bde92 + // which withdraws WETH to mainnet using dev wallet. + const withdrawalErc20Events = await paginatedEventQuery( + arbitrumGateway, + arbitrumGateway.filters.WithdrawalInitiated( + null, // l1Token, not-indexed so can't filter + null, // from + withdrawalToAddresses // to + ), + { + ...spokePoolClient.eventSearchConfig, + toBlock: spokePoolClient.latestBlockSearched, + } + ); + const withdrawalNativeEvents = await paginatedEventQuery( + arbSys, + arbSys.filters.L2ToL1Tx( + null, // caller, not-indexed so can't filter + withdrawalToAddresses // destination + ), + { + ...spokePoolClient.eventSearchConfig, + toBlock: spokePoolClient.latestBlockSearched, + } + ); + const withdrawalEvents = [ + ...withdrawalErc20Events.map((e) => { + const l2Token = getL2TokenAddresses(e.args.l1Token)[chainId]; + return { + ...e, + amount: e.args._amount, + l2TokenAddress: l2Token, + }; + }), + ...withdrawalNativeEvents.map((e) => { + const nativeTokenSymbol = getNativeTokenSymbol(chainId); + const l2Token = TOKEN_SYMBOLS_MAP[nativeTokenSymbol].addresses[chainId]; + return { + ...e, + amount: e.args.callvalue, + l2TokenAddress: l2Token, + }; + }), + ]; + // If there are any found withdrawal initiated events, then add them to the list of TokenBridged events we'll + // submit proofs and finalizations for. + withdrawalEvents.forEach((event) => { + try { + const tokenBridgedEvent: TokensBridged = { + ...event, + amountToReturn: event.amount, + chainId, + leafId: 0, + l2TokenAddress: event.l2TokenAddress, + }; + if (event.blockNumber <= latestBlockToFinalize) { + olderTokensBridgedEvents.push(tokenBridgedEvent); + } else { + const l1TokenInfo = getL1TokenInfo(tokenBridgedEvent.l2TokenAddress, chainId); + const amountFromWei = fromWei(tokenBridgedEvent.amountToReturn.toString(), l1TokenInfo.decimals); + logger.debug({ + at: `Finalizer#${networkName}Finalizer`, + message: `Withdrawal event for ${amountFromWei} of ${l1TokenInfo.symbol} is too recent to finalize`, + }); + } + } catch (err) { + logger.debug({ + at: `Finalizer#${networkName}Finalizer`, + message: `Skipping ERC20 withdrawal event for unknown token ${event.l2TokenAddress} on chain ${networkName}`, + event: event, + }); + } + }); + } + return await multicallArbitrumFinalizations(olderTokensBridgedEvents, signer, hubPoolClient, logger, chainId); } @@ -81,14 +252,14 @@ async function multicallArbitrumFinalizations( }; } -async function finalizeArbitrum(message: L2ToL1MessageWriter, chainId: number): Promise { +async function finalizeArbitrum(message: ChildToParentMessageWriter, chainId: number): Promise { const l2Provider = getCachedProvider(chainId, true); const proof = await message.getOutboxProof(l2Provider); const { address, abi } = CONTRACT_ADDRESSES[CHAIN_IDs.MAINNET][`orbitOutbox_${chainId}`]; const outbox = new Contract(address, abi); // eslint-disable-next-line @typescript-eslint/no-explicit-any const eventData = (message as any).nitroWriter.event; // nitroWriter is a private property on the - // L2ToL1MessageWriter class, which we need to form the calldata so unfortunately we must cast to `any`. + // ChildToParentMessageWriter class, which we need to form the calldata so unfortunately we must cast to `any`. const callData = await outbox.populateTransaction.executeTransaction( proof, eventData.position, @@ -115,7 +286,7 @@ async function getFinalizableMessages( ): Promise< { info: TokensBridged; - message: L2ToL1MessageWriter; + message: ChildToParentMessageWriter; status: string; }[] > { @@ -124,12 +295,15 @@ async function getFinalizableMessages( allMessagesWithStatuses, (message: { status: string }) => message.status ); + const networkName = getNetworkName(chainId); logger.debug({ - at: "ArbitrumFinalizer", - message: "Arbitrum outbox message statuses", + at: `Finalizer#${networkName}Finalizer`, + message: `${networkName} outbox message statuses`, statusesGrouped, }); - return allMessagesWithStatuses.filter((x) => x.status === L2ToL1MessageStatus[L2ToL1MessageStatus.CONFIRMED]); + return allMessagesWithStatuses.filter( + (x) => x.status === ChildToParentMessageStatus[ChildToParentMessageStatus.CONFIRMED] + ); } async function getAllMessageStatuses( @@ -140,7 +314,7 @@ async function getAllMessageStatuses( ): Promise< { info: TokensBridged; - message: L2ToL1MessageWriter; + message: ChildToParentMessageWriter; status: string; }[] > { @@ -170,22 +344,23 @@ async function getMessageOutboxStatusAndProof( logIndex: number, chainId: number ): Promise<{ - message: L2ToL1MessageWriter; + message: ChildToParentMessageWriter; status: string; }> { + const networkName = getNetworkName(chainId); const l2Provider = getCachedProvider(chainId, true); const receipt = await l2Provider.getTransactionReceipt(event.transactionHash); - const l2Receipt = new L2TransactionReceipt(receipt); + const l2Receipt = new ChildTransactionReceipt(receipt); try { - const l2ToL1Messages = await l2Receipt.getL2ToL1Messages(l1Signer); + const l2ToL1Messages = await l2Receipt.getChildToParentMessages(l1Signer); if (l2ToL1Messages.length === 0 || l2ToL1Messages.length - 1 < logIndex) { const error = new Error( `No outgoing messages found in transaction:${event.transactionHash} for l2 token ${event.l2TokenAddress}` ); logger.warn({ - at: "ArbitrumFinalizer", - message: "Arbitrum transaction that emitted TokensBridged event unexpectedly contains 0 L2-to-L1 messages 🤢!", + at: `Finalizer#${networkName}Finalizer`, + message: "Transaction that emitted TokensBridged event unexpectedly contains 0 L2-to-L1 messages 🤢!", logIndex, l2ToL1Messages: l2ToL1Messages.length, event, @@ -199,30 +374,38 @@ async function getMessageOutboxStatusAndProof( // Check if already executed or unconfirmed (i.e. not yet available to be executed on L1 following dispute // window) const outboxMessageExecutionStatus = await l2Message.status(l2Provider); - if (outboxMessageExecutionStatus === L2ToL1MessageStatus.EXECUTED) { + if (outboxMessageExecutionStatus === ChildToParentMessageStatus.EXECUTED) { return { message: l2Message, - status: L2ToL1MessageStatus[L2ToL1MessageStatus.EXECUTED], + status: ChildToParentMessageStatus[ChildToParentMessageStatus.EXECUTED], }; } - if (outboxMessageExecutionStatus !== L2ToL1MessageStatus.CONFIRMED) { - return { - message: l2Message, - status: L2ToL1MessageStatus[L2ToL1MessageStatus.UNCONFIRMED], - }; + if (outboxMessageExecutionStatus !== ChildToParentMessageStatus.CONFIRMED) { + const estimatedFinalizationBlock = await l2Message.getFirstExecutableBlock(l2Provider); + const estimatedFinalizationBlockDelta = estimatedFinalizationBlock.toNumber() - LATEST_MAINNET_BLOCK; + logger.debug({ + at: `Finalizer#${networkName}Finalizer`, + message: `Unconfirmed withdrawal can be finalized in ${ + (estimatedFinalizationBlockDelta * MAINNET_BLOCK_TIME) / 60 / 60 + } hours`, + chainId, + token: event.l2TokenAddress, + amount: event.amountToReturn, + receipt: l2Receipt.transactionHash, + }); } // Now that its confirmed and not executed, we can execute our // message in its outbox entry. return { message: l2Message, - status: L2ToL1MessageStatus[outboxMessageExecutionStatus], + status: ChildToParentMessageStatus[outboxMessageExecutionStatus], }; } catch (error) { // Likely L1 message hasn't been included in an arbitrum batch yet, so ignore it for now. return { message: undefined, - status: L2ToL1MessageStatus[L2ToL1MessageStatus.UNCONFIRMED], + status: ChildToParentMessageStatus[ChildToParentMessageStatus.UNCONFIRMED], }; } } diff --git a/yarn.lock b/yarn.lock index ebf42b8c6..9ff89fe7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -83,14 +83,15 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== -"@arbitrum/sdk@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.1.3.tgz#75236043717a450b569faaa087687c51d525b0c3" - integrity sha512-Dn1or7/Guc3dItuiiWaoYQ37aCDwiWTZGPIrg4yBJW27BgiDGbo0mjPDAhKTh4p5NDOWyE8bZ0vZai86COZIUA== +"@arbitrum/sdk@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-4.0.2.tgz#23555858f49e2b237b94a65bd486c65edb7b1690" + integrity sha512-KkuXNwbG5c/hCT66EG2tFMHXxIDCvt9dxAIeykZYnW7KyEH5GNlRwaPzwo6MU0shHNc0qg6pZzy2XakJWuSw2Q== dependencies: "@ethersproject/address" "^5.0.8" "@ethersproject/bignumber" "^5.1.1" "@ethersproject/bytes" "^5.0.8" + async-mutex "^0.4.0" ethers "^5.1.0" "@aws-crypto/sha256-js@1.2.2": @@ -4211,6 +4212,13 @@ async-listener@^0.6.0: semver "^5.3.0" shimmer "^1.1.0" +async-mutex@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.1.tgz#bccf55b96f2baf8df90ed798cb5544a1f6ee4c2c" + integrity sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA== + dependencies: + tslib "^2.4.0" + async-retry@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"