diff --git a/package.json b/package.json index 51e7c668c..d8fb23d01 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "dependencies": { "@across-protocol/constants": "^3.1.19", "@across-protocol/contracts": "^3.0.16", - "@across-protocol/sdk": "^3.2.15", - "@arbitrum/sdk": "^3.1.3", + "@across-protocol/sdk": "^3.3.18", + "@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/Constants.ts b/src/common/Constants.ts index 90c0225a4..73e67b531 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -442,6 +442,8 @@ export const RELAYER_DEFAULT_SPOKEPOOL_INDEXER = "./dist/src/libexec/RelayerSpok export const DEFAULT_ARWEAVE_GATEWAY = { url: "arweave.net", port: 443, protocol: "https" }; +export const ARWEAVE_TAG_BYTE_LIMIT = 2048; + // Chains with slow (> 2 day liveness) canonical L2-->L1 bridges that we prioritize taking repayment on. // This does not include all 7-day withdrawal chains because we don't necessarily prefer being repaid on some of these 7-day chains, like Mode. // This list should generally exclude Lite chains because the relayer ignores HubPool liquidity in that case which could cause the 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/dataworker/Dataworker.ts b/src/dataworker/Dataworker.ts index 215a7d224..0aca04370 100644 --- a/src/dataworker/Dataworker.ts +++ b/src/dataworker/Dataworker.ts @@ -31,7 +31,7 @@ import { FillStatus, } from "../interfaces"; import { DataworkerClients } from "./DataworkerClientHelper"; -import { SpokePoolClient, BalanceAllocator } from "../clients"; +import { SpokePoolClient, BalanceAllocator, BundleDataClient } from "../clients"; import * as PoolRebalanceUtils from "./PoolRebalanceUtils"; import { blockRangesAreInvalidForSpokeClients, @@ -619,16 +619,33 @@ export class Dataworker { // Root bundle is valid, attempt to persist the raw bundle data and the merkle leaf data to DA layer // if not already there. if (persistBundleData && isDefined(bundleData)) { + const chainIds = this.clients.configStoreClient.getChainIdIndicesForBlock(nextBundleMainnetStartBlock); + // Store the bundle block ranges on Arweave as a map of chainId to block range to aid users in querying. + const bundleBlockRangeMap = Object.fromEntries( + bundleData.bundleBlockRanges.map((range, i) => { + const chainIdForRange = chainIds[i]; + return [chainIdForRange, range]; + }) + ); + // As a unique key for this bundle, use the next bundle mainnet start block, which should + // never be duplicated between bundles as long as the mainnet end block in the bundle block range + // always progresses forwards, which I think is a safe assumption. Other chains might pause + // but mainnet should never pause. + const partialArweaveDataKey = BundleDataClient.getArweaveClientKey(bundleData.bundleBlockRanges); await Promise.all([ persistDataToArweave( this.clients.arweaveClient, - bundleData, + { + ...bundleData, + bundleBlockRanges: bundleBlockRangeMap, + }, this.logger, - `bundles-${bundleData.bundleBlockRanges}` + `bundles-${partialArweaveDataKey}` ), persistDataToArweave( this.clients.arweaveClient, { + bundleBlockRanges: bundleBlockRangeMap, poolRebalanceLeaves: expectedTrees.poolRebalanceTree.leaves.map((leaf) => { return { ...leaf, @@ -652,7 +669,7 @@ export class Dataworker { slowRelayRoot: expectedTrees.slowRelayTree.tree.getHexRoot(), }, this.logger, - `merkletree-${bundleData.bundleBlockRanges}` + `merkletree-${partialArweaveDataKey}` ), ]); } @@ -2297,7 +2314,10 @@ export class Dataworker { at: "Dataworker#_getPoolRebalanceRoot", message: "Constructed new pool rebalance root", key, - root: this.rootCache[key], + root: { + ...this.rootCache[key], + tree: this.rootCache[key].tree.getHexRoot(), + }, }); return _.cloneDeep(this.rootCache[key]); diff --git a/src/dataworker/DataworkerUtils.ts b/src/dataworker/DataworkerUtils.ts index d5ae70a46..3352c5a30 100644 --- a/src/dataworker/DataworkerUtils.ts +++ b/src/dataworker/DataworkerUtils.ts @@ -1,7 +1,11 @@ import assert from "assert"; import { utils, interfaces, caching } from "@across-protocol/sdk"; import { SpokePoolClient } from "../clients"; -import { CONSERVATIVE_BUNDLE_FREQUENCY_SECONDS, spokesThatHoldEthAndWeth } from "../common/Constants"; +import { + ARWEAVE_TAG_BYTE_LIMIT, + CONSERVATIVE_BUNDLE_FREQUENCY_SECONDS, + spokesThatHoldEthAndWeth, +} from "../common/Constants"; import { CONTRACT_ADDRESSES } from "../common/ContractAddresses"; import { PoolRebalanceLeaf, @@ -343,11 +347,17 @@ export async function persistDataToArweave( logger: winston.Logger, tag?: string ): Promise { + assert( + Buffer.from(tag).length <= ARWEAVE_TAG_BYTE_LIMIT, + `Arweave tag cannot exceed ${ARWEAVE_TAG_BYTE_LIMIT} bytes` + ); + const profiler = new Profiler({ logger, at: "DataworkerUtils#persistDataToArweave", }); const mark = profiler.start("persistDataToArweave"); + // Check if data already exists on Arweave with the given tag. // If so, we don't need to persist it again. const [matchingTxns, address, balance] = await Promise.all([ 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/src/monitor/Monitor.ts b/src/monitor/Monitor.ts index 34ab55801..9bee157a5 100644 --- a/src/monitor/Monitor.ts +++ b/src/monitor/Monitor.ts @@ -666,6 +666,13 @@ export class Monitor { ); const enabledChainIds = this.clients.configStoreClient.getChainIdIndicesForBlock(nextBundleMainnetStartBlock); + this.logger.debug({ + at: "Monitor#checkSpokePoolRunningBalances", + message: "Mainnet root bundles in scope", + validatedBundles, + outstandingBundle: bundle, + }); + const slowFillBlockRange = getWidestPossibleExpectedBlockRange( enabledChainIds, this.clients.spokePoolClients, @@ -682,6 +689,13 @@ export class Monitor { : [endBlockNumber + 1, spokeLatestBlockSearched > endBlockNumber ? spokeLatestBlockSearched : endBlockNumber]; }); + this.logger.debug({ + at: "Monitor#checkSpokePoolRunningBalances", + message: "Block ranges to search", + slowFillBlockRange, + blockRangeTail, + }); + // Do all async tasks in parallel. We want to know about the pool rebalances, slow fills in the most recent proposed bundle, refunds // from the last `n` bundles, pending refunds which have not been made official via a root bundle proposal, and the current balances of // all the spoke pools. @@ -739,6 +753,12 @@ export class Monitor { }); } + this.logger.debug({ + at: "Monitor#checkSpokePoolRunningBalances", + message: "Print pool rebalance leaves", + poolRebalanceRootLeaves: poolRebalanceLeaves, + }); + // Calculate the pending refunds. for (const chainId of chainIds) { const l2TokenAddresses = monitoredTokenSymbols @@ -770,6 +790,13 @@ export class Monitor { ); pendingRelayerRefunds[chainId][l2Token] = pendingValidatedDeductions.add(nextBundleDeductions); }); + + this.logger.debug({ + at: "Monitor#checkSpokePoolRunningBalances", + message: "Print refund amounts for chainId", + chainId, + pendingDeductions: pendingRelayerRefunds[chainId], + }); } // Get the slow fill amounts. Only do this step if there were slow fills in the most recent root bundle. diff --git a/src/utils/ProviderUtils.ts b/src/utils/ProviderUtils.ts index 108f37e7a..9f6e8e24a 100644 --- a/src/utils/ProviderUtils.ts +++ b/src/utils/ProviderUtils.ts @@ -139,6 +139,7 @@ export async function getProvider( rpc: getOriginFromURL(url), retryAfter: `${delayMs} ms`, workers: nodeMaxConcurrency, + datadog: true, }); } await delay(delayMs); diff --git a/yarn.lock b/yarn.lock index 07c1bf6b1..03fa787b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,10 +53,10 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.2.15": - version "3.2.15" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.2.15.tgz#148c11f759e1c89dd2efb3806ddbafbc192c251e" - integrity sha512-bvRjU6duZmx2HpZ9ifb6adZC6xRdwXX9B2wJs4bYPr/rf69ibKtv7RreKDILKCQM7cbbqjDM5bqbVkQakA1rvA== +"@across-protocol/sdk@^3.3.18": + version "3.3.18" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.3.18.tgz#d39ef359f9f639921fb412a1355167354014a80f" + integrity sha512-Ea40yDPL94T3uc6HhqDj8X7vovPSyOVSmA6Z3C1uZmdwRdDKt8hlg8k7yxIg+8aR5aEJJ7hCZy6bHdI5XHpbFQ== dependencies: "@across-protocol/across-token" "^1.0.0" "@across-protocol/constants" "^3.1.19" @@ -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": @@ -4421,6 +4422,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-mutex@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.5.0.tgz#353c69a0b9e75250971a64ac203b0ebfddd75482" @@ -5942,9 +5950,9 @@ cross-fetch@^4.0.0: node-fetch "^2.6.12" cross-spawn@^7.0.0, cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -14588,7 +14596,7 @@ string-format@^2.0.0: resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14614,6 +14622,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -14684,7 +14701,7 @@ stringify-package@^1.0.1: resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14712,6 +14729,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -16912,7 +16936,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -16938,6 +16962,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"