diff --git a/src/adapter/BaseChainAdapter.ts b/src/adapter/BaseChainAdapter.ts index e18a297d0a..4e1eff5402 100644 --- a/src/adapter/BaseChainAdapter.ts +++ b/src/adapter/BaseChainAdapter.ts @@ -38,6 +38,9 @@ import { sendAndConfirmSolanaTransaction, getSvmProvider, submitTransaction, + getTokenInfo, + ConvertDecimals, + toAddressType, } from "../utils"; import { AugmentedTransaction, TransactionClient } from "../clients/TransactionClient"; import { @@ -69,7 +72,7 @@ export class BaseChainAdapter { spokePoolClients: { [chainId: number]: SpokePoolClient }, protected readonly chainId: number, protected readonly hubChainId: number, - protected readonly monitoredAddresses: Address[], + protected readonly monitoredAddresses: { [l1Token: string]: Address[] }, protected readonly logger: winston.Logger, public readonly supportedTokens: SupportedTokenSymbol[], protected readonly bridges: { [l1Token: string]: BaseBridgeAdapter }, @@ -532,8 +535,17 @@ export class BaseChainAdapter { const outstandingTransfers: OutstandingTransfers = {}; - await forEachAsync(this.monitoredAddresses, async (monitoredAddress) => { - await forEachAsync(availableL1Tokens, async (l1Token) => { + this.logger.debug({ + at: `${this.adapterName}#getOutstandingCrossChainTransfers`, + message: "Getting outstanding cross chain transfers", + monitoredAddresses: Object.fromEntries( + Object.entries(this.monitoredAddresses).map(([l1Token, addresses]) => [l1Token, addresses]) + ), + }); + + await forEachAsync(availableL1Tokens, async (l1Token) => { + const monitoredAddresses = this.monitoredAddresses[l1Token.toNative()]; + await forEachAsync(monitoredAddresses, async (monitoredAddress) => { const bridge = this.bridges[l1Token.toNative()]; const [depositInitiatedResults, depositFinalizedResults] = await Promise.all([ bridge.queryL1BridgeInitiationEvents(l1Token, monitoredAddress, monitoredAddress, l1SearchConfig), @@ -544,27 +556,37 @@ export class BaseChainAdapter { this.chainId, monitoredAddress ); - Object.entries(depositInitiatedResults).forEach(([l2Token, depositInitiatedEvents]) => { - const trackedInitiatedEvents = depositInitiatedEvents.filter( + const filteredDepositEvents = (depositInitiatedEvents ?? []).filter( (event) => !ignoredPendingBridgeTxnRefs.has(event.txnRef) ); - const finalizedAmounts = depositFinalizedResults?.[l2Token]?.map((event) => event.amount.toString()) ?? []; - const outstandingInitiatedEvents: typeof trackedInitiatedEvents = []; - const totalAmount = trackedInitiatedEvents.reduce((acc, event) => { - // Remove the first match. This handles scenarios where are collisions by amount. - const index = finalizedAmounts.indexOf(event.amount.toString()); - if (index > -1) { - finalizedAmounts.splice(index, 1); - return acc; - } - outstandingInitiatedEvents.push(event); + const totalDepositedAmount = filteredDepositEvents.reduce((acc, event) => { return acc.add(event.amount); }, bnZero); + const l2TokenDecimals = getTokenInfo(toAddressType(l2Token, this.chainId), this.chainId).decimals; + const l1TokenDecimals = getTokenInfo(l1Token, this.hubChainId).decimals; + const totalFinalizedAmount = (depositFinalizedResults?.[l2Token] ?? []).reduce((acc, event) => { + return acc.add(ConvertDecimals(l2TokenDecimals, l1TokenDecimals)(event.amount)); + }, bnZero); + + // If there is a net unfinalized amount, go through deposit initiated events in newest to oldest order + // and assume that the newest bridges are the ones that are not yet finalized. + const outstandingInitiatedEvents: string[] = []; + const totalAmount = totalDepositedAmount.sub(totalFinalizedAmount); + let remainingUnfinalizedAmount = totalAmount; + if (remainingUnfinalizedAmount.gt(0)) { + for (const depositEvent of filteredDepositEvents) { + if (remainingUnfinalizedAmount.lte(0)) { + break; + } + outstandingInitiatedEvents.push(depositEvent.txnRef); + remainingUnfinalizedAmount = remainingUnfinalizedAmount.sub(depositEvent.amount); + } + } if (totalAmount.gt(0) && outstandingInitiatedEvents.length > 0) { assign(outstandingTransfers, [monitoredAddress.toNative(), l1Token.toNative(), l2Token], { totalAmount, - depositTxHashes: outstandingInitiatedEvents.map((event) => event.txnRef), + depositTxHashes: outstandingInitiatedEvents, }); } }); diff --git a/src/adapter/bridges/BinanceCEXBridge.ts b/src/adapter/bridges/BinanceCEXBridge.ts index 5151bd5ed0..941e33e8a5 100644 --- a/src/adapter/bridges/BinanceCEXBridge.ts +++ b/src/adapter/bridges/BinanceCEXBridge.ts @@ -24,6 +24,7 @@ import { getBinanceDepositType, BinanceTransactionType, getBinanceWithdrawalType, + toAddressType, } from "../../utils"; import { BaseBridgeAdapter, BridgeTransactionDetails, BridgeEvents, BridgeEvent } from "./BaseBridgeAdapter"; import ERC20_ABI from "../../common/abi/MinimalERC20.json"; @@ -159,12 +160,13 @@ export class BinanceCEXBridge extends BaseBridgeAdapter { withdrawalType !== BinanceTransactionType.SWAP ); }); - const { decimals: l1Decimals } = getTokenInfo(l1Token, this.hubChainId); + const l2TokenAddress = this.resolveL2TokenAddress(l1Token); + const { decimals: l2Decimals } = getTokenInfo(toAddressType(l2TokenAddress, this.l2chainId), this.l2chainId); return { - [this.resolveL2TokenAddress(l1Token)]: await mapAsync(withdrawalHistory, async (withdrawal) => { + [l2TokenAddress]: await mapAsync(withdrawalHistory, async (withdrawal) => { const txnReceipt = await this.l2Provider.getTransactionReceipt(withdrawal.txId); - return this.toBridgeEvent(floatToBN(withdrawal.amount, l1Decimals), txnReceipt); + return this.toBridgeEvent(floatToBN(withdrawal.amount, l2Decimals), txnReceipt); }), }; } diff --git a/src/adapter/bridges/BridgeApi.ts b/src/adapter/bridges/BridgeApi.ts index d1bca5afbc..6268d1386e 100644 --- a/src/adapter/bridges/BridgeApi.ts +++ b/src/adapter/bridges/BridgeApi.ts @@ -106,7 +106,7 @@ export class BridgeApi extends BaseBridgeAdapter { const statusesGrouped = groupObjectCountsByProp(pendingTransfers, (pendingTransfer) => pendingTransfer.state); this.logger.debug({ at: "BridgeApi#queryL1BridgeInitiationEvents", - message: "Pending transfer statuses", + message: `Pending transfer statuses for ${this.l1TokenInfo.symbol} and ${toAddress}`, statusesGrouped, }); diff --git a/src/clients/TokenTransferClient.ts b/src/clients/TokenTransferClient.ts deleted file mode 100644 index 7689a40d38..0000000000 --- a/src/clients/TokenTransferClient.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - EventSearchConfig, - winston, - assign, - ERC20, - Contract, - paginatedEventQuery, - spreadEventWithBlockNumber, - Address, -} from "../utils"; -import { Log, TokenTransfer, TransfersByChain } from "../interfaces"; -import { Provider } from "@ethersproject/abstract-provider"; - -export class TokenTransferClient { - private tokenTransfersByAddress: { [address: string]: TransfersByChain } = {}; - - constructor( - readonly logger: winston.Logger, - // We can accept spokePoolClients here instead, but just accepting providers makes it very clear that we don't - // rely on SpokePoolClient and its cached state. - readonly providerByChainIds: { [chainId: number]: Provider }, - readonly monitoredAddresses: Address[] - ) {} - - getTokenTransfers(address: Address): TransfersByChain { - return this.tokenTransfersByAddress[address.toBytes32()]; - } - - async update( - searchConfigByChainIds: { [chainId: number]: EventSearchConfig }, - tokenByChainIds: { [chainId: number]: Address[] } - ): Promise { - this.logger.debug({ - at: "TokenTransferClient", - message: "Updating TokenTransferClient client", - searchConfigByChainIds, - tokenByChainIds, - }); - const tokenContractsByChainId = Object.fromEntries( - Object.entries(tokenByChainIds).map(([chainId, tokens]) => [ - Number(chainId), - tokens.map( - (token: Address) => new Contract(token.toEvmAddress(), ERC20.abi, this.providerByChainIds[Number(chainId)]) - ), - ]) - ); - - const chainIds = Object.keys(this.providerByChainIds).map(Number); - for (const chainId of chainIds) { - const tokenContracts = tokenContractsByChainId[chainId]; - const evmMonitoredAddresses = this.monitoredAddresses.filter((address) => address.isEVM()); - for (const monitoredAddress of evmMonitoredAddresses) { - const transferEventsList = await Promise.all( - tokenContracts.map((tokenContract) => - this.querySendAndReceiveEvents(tokenContract, monitoredAddress, searchConfigByChainIds[chainId]) - ) - ); - const transferEventsPerToken: { [tokenAddress: string]: Log[][] } = Object.fromEntries( - transferEventsList.map((transferEvents, i) => [tokenContracts[i].address, transferEvents]) - ); - - // Create an entry in the cache if not initialized. - const tokenTransfers = this.tokenTransfersByAddress[monitoredAddress.toBytes32()]; - if (tokenTransfers === undefined || tokenTransfers[chainId] === undefined) { - assign(this.tokenTransfersByAddress, [monitoredAddress.toBytes32(), chainId], {}); - } - - // Update outgoing and incoming transfers for current relayer in the cache. - const transferCache = this.tokenTransfersByAddress[monitoredAddress.toBytes32()][chainId]; - for (const [tokenAddress, events] of Object.entries(transferEventsPerToken)) { - if (transferCache[tokenAddress] === undefined) { - transferCache[tokenAddress] = { - incoming: [], - outgoing: [], - }; - } - - for (const event of events[0]) { - const outgoingTransfer = spreadEventWithBlockNumber(event) as TokenTransfer; - transferCache[tokenAddress].outgoing.push(outgoingTransfer); - } - - for (const event of events[1]) { - const incomingTransfer = spreadEventWithBlockNumber(event) as TokenTransfer; - transferCache[tokenAddress].incoming.push(incomingTransfer); - } - } - } - } - - this.logger.debug({ at: "TokenTransferClient", message: "TokenTransfer client updated!" }); - } - - // Returns outgoing and incoming transfers for the specified tokenContract and address. - querySendAndReceiveEvents(tokenContract: Contract, address: Address, config: EventSearchConfig): Promise { - const eventFilters = [[address.toEvmAddress()], [undefined, address.toEvmAddress()]]; - return Promise.all( - eventFilters.map((eventFilter) => - paginatedEventQuery(tokenContract, tokenContract.filters.Transfer(...eventFilter), config) - ) - ); - } -} diff --git a/src/clients/bridges/AdapterManager.ts b/src/clients/bridges/AdapterManager.ts index dd17c395e9..91c2240d0b 100644 --- a/src/clients/bridges/AdapterManager.ts +++ b/src/clients/bridges/AdapterManager.ts @@ -56,13 +56,19 @@ export class AdapterManager { (client) => client.spokePoolAddress ); + const isHubPoolOrSpokePoolAddress = (chainId: number, address: Address) => { + return ( + EvmAddress.from(this.hubPoolClient.hubPool.address).eq(address) || + this.spokePoolManager.getClient(chainId)?.spokePoolAddress.eq(address) + ); + }; + // The adapters are only set up to monitor EOA's and the HubPool and SpokePool address, so remove // spoke pool addresses from other chains. const filterMonitoredAddresses = (chainId: number) => { return monitoredAddresses.filter( (address) => - EvmAddress.from(this.hubPoolClient.hubPool.address).eq(address) || - this.spokePoolManager.getClient(chainId)?.spokePoolAddress.eq(address) || + isHubPoolOrSpokePoolAddress(chainId, address) || !spokePoolAddresses.some((spokePoolAddress) => spokePoolAddress.eq(address)) ); }; @@ -129,12 +135,34 @@ export class AdapterManager { ); }; Object.values(this.spokePoolManager.getSpokePoolClients()).map(({ chainId }) => { + // Filter hub/spoke pool addresses from the monitored addresses for chains that don't have a pool rebalance + // route for the l1 token. + const monitoredAddresses = Object.fromEntries( + (SUPPORTED_TOKENS[chainId] ?? []).map((symbol) => { + const l1Token = TOKEN_SYMBOLS_MAP[symbol].addresses[hubChainId]; + return [ + l1Token, + filterMonitoredAddresses(chainId).filter((address) => { + if (!l1Token) { + return false; + } + const hasPoolRebalanceRoute = hubPoolClient.l2TokenEnabledForL1Token(EvmAddress.from(l1Token), chainId); + if (!hasPoolRebalanceRoute) { + // Chain does not have a pool rebalance route, only allow EOA's. + return !isHubPoolOrSpokePoolAddress(chainId, address); + } + // Chain has pool rebalance route, all addresses from filterMonitoredAddresses are valid. + return true; + }), + ]; + }) + ); // Instantiate a generic adapter and supply all network-specific configurations. this.adapters[chainId] = new BaseChainAdapter( this.spokePoolManager.getSpokePoolClients(), chainId, hubChainId, - filterMonitoredAddresses(chainId), + monitoredAddresses, logger, SUPPORTED_TOKENS[chainId] ?? [], constructBridges(chainId), diff --git a/src/clients/bridges/CrossChainTransferClient.ts b/src/clients/bridges/CrossChainTransferClient.ts index 13ad75f009..3a0157f351 100644 --- a/src/clients/bridges/CrossChainTransferClient.ts +++ b/src/clients/bridges/CrossChainTransferClient.ts @@ -85,8 +85,6 @@ export class CrossChainTransferClient { return; } - this.log("Updating cross chain transfers", { chainIds }); - const outstandingTransfersPerChain = await Promise.all( chainIds.map(async (chainId) => [ chainId, diff --git a/src/clients/index.ts b/src/clients/index.ts index fb97a84901..643e3d3d88 100644 --- a/src/clients/index.ts +++ b/src/clients/index.ts @@ -17,7 +17,6 @@ export * from "./ConfigStoreClient"; export * from "./MultiCallerClient"; export * from "./ProfitClient"; export * from "./TokenClient"; -export * from "./TokenTransferClient"; export * from "./TransactionClient"; export * from "./InventoryClient"; export * from "./AcrossAPIClient"; diff --git a/src/interfaces/Report.ts b/src/interfaces/Report.ts index 61146b3324..f74391aa4b 100644 --- a/src/interfaces/Report.ts +++ b/src/interfaces/Report.ts @@ -1,44 +1,5 @@ -import { BigNumber } from "../utils"; - export enum BundleAction { PROPOSED = "proposed", DISPUTED = "disputed", CANCELED = "canceled", } - -export enum BalanceType { - // Current balance. - CURRENT = "current", - // Balance from any pending bundle's refunds. - PENDING = "pending", - // Balance from next bundle's refunds. - NEXT = "next", - // Balance from pending cross chain transfers. - PENDING_TRANSFERS = "pending transfers", - // Total balance across current, pending, next. - TOTAL = "total", -} - -export interface RelayerBalanceCell { - // This should match BalanceType values. We can't use BalanceType directly because interface only accepts static keys, - // not those, such as enums, that are determined at runtime. - [balanceType: string]: BigNumber; -} - -export interface RelayerBalanceColumns { - // Including a column for "Total". - [chainName: string]: RelayerBalanceCell; -} - -// The relayer balance table is organized as below: -// 1. Rows are token symbols, e.g. WETH, USDC -// 2. Columns are chains, e.g. Optimism. -// 3. Each column is further broken into subcolumns: current (live balance), pending (balance from pending bundle's refunds) -// and next bundle (balance from next bundle's refunds) -export interface RelayerBalanceTable { - [tokenSymbol: string]: RelayerBalanceColumns; -} - -export interface RelayerBalanceReport { - [relayer: string]: RelayerBalanceTable; -} diff --git a/src/interfaces/Token.ts b/src/interfaces/Token.ts index c8acb7d53a..e69de29bb2 100644 --- a/src/interfaces/Token.ts +++ b/src/interfaces/Token.ts @@ -1,19 +0,0 @@ -import { BigNumber } from "../utils"; -import { SortableEvent } from "."; - -export interface TokenTransfer extends SortableEvent { - value: BigNumber; - from: string; - to: string; -} - -export interface TransfersByTokens { - [token: string]: { - incoming: TokenTransfer[]; - outgoing: TokenTransfer[]; - }; -} - -export interface TransfersByChain { - [chainId: number]: TransfersByTokens; -} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index a79ebcf4eb..4b0eda7820 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -3,7 +3,6 @@ import { BigNumber } from "../utils"; export * from "./InventoryManagement"; export * from "./SpokePool"; -export * from "./Token"; export * from "./Error"; export * from "./Report"; export * from "./Arweave"; diff --git a/src/monitor/Monitor.ts b/src/monitor/Monitor.ts index 3ed4fd4d7d..a453e6449b 100644 --- a/src/monitor/Monitor.ts +++ b/src/monitor/Monitor.ts @@ -1,15 +1,11 @@ import { BundleDataApproxClient } from "../clients"; import { EXPECTED_L1_TO_L2_MESSAGE_TIME } from "../common"; import { - BalanceType, BundleAction, DepositWithBlock, FillStatus, FillWithBlock, L1Token, - RelayerBalanceReport, - RelayerBalanceTable, - TokenTransfer, TokenInfo, SwapFlowInitialized, } from "../interfaces"; @@ -29,24 +25,17 @@ import { getNetworkName, getUnfilledDeposits, mapAsync, - getEndBlockBuffers, parseUnits, providers, toBN, toBNWei, winston, - TOKEN_SYMBOLS_MAP, CHAIN_IDs, isDefined, - resolveTokenDecimals, - sortEventsDescending, - getWidestPossibleExpectedBlockRange, - utils, - _buildPoolRebalanceRoot, getRemoteTokenForL1Token, getTokenInfo, ConvertDecimals, - getL1TokenAddress, + getInventoryBalanceContributorTokens, isEVMSpokePoolClient, isSVMSpokePoolClient, toAddressType, @@ -63,14 +52,13 @@ import { getRelayDataFromFill, sortEventsAscending, chainHasNativeToken, + getLatestRunningBalances, ALT_DEACTIVATION_COOLDOWN, simulateSolanaTransaction, sendAndConfirmSolanaTransaction, } from "../utils"; import { MonitorClients, updateMonitorClients } from "./MonitorClientHelper"; -import { MonitorConfig, L2Token } from "./MonitorConfig"; -import { getImpliedBundleBlockRanges } from "../dataworker/DataworkerUtils"; -import { PUBLIC_NETWORKS, TOKEN_EQUIVALENCE_REMAPPING } from "@across-protocol/constants"; +import { MonitorConfig } from "./MonitorConfig"; import { utils as sdkUtils, arch } from "@across-protocol/sdk"; import { address, @@ -95,10 +83,6 @@ import { HyperliquidExecutorConfig } from "../hyperliquid/HyperliquidExecutorCon // then its finalizing after the subsequent challenge period has started, which is sub-optimal. export const REBALANCE_FINALIZE_GRACE_PERIOD = Number(process.env.REBALANCE_FINALIZE_GRACE_PERIOD ?? 60 * 60); -// bundle frequency. -export const ALL_CHAINS_NAME = "All chains"; -const ALL_BALANCE_TYPES = [BalanceType.CURRENT, BalanceType.PENDING, BalanceType.PENDING_TRANSFERS, BalanceType.TOTAL]; - type BalanceRequest = { chainId: number; token: Address; account: Address }; export class Monitor { @@ -109,7 +93,6 @@ export class Monitor { private balanceCache: { [chainId: number]: { [token: string]: { [account: string]: BigNumber } } } = {}; private decimals: { [chainId: number]: { [token: string]: number } } = {}; private additionalL1Tokens: L1Token[] = []; - private l2OnlyTokens: L2Token[] = []; // Chains for each spoke pool client. public monitorChains: number[]; // Chains that we care about inventory manager activity on, so doesn't include Ethereum which doesn't @@ -135,14 +118,13 @@ export class Monitor { crossChainAdapterSupportedChains: this.crossChainAdapterSupportedChains, }); this.additionalL1Tokens = monitorConfig.additionalL1NonLpTokens.map((l1Token) => { - const l1TokenInfo = getTokenInfo(EvmAddress.from(l1Token), this.clients.hubPoolClient.chainId); + const l1TokenInfo = this.getTokenInfo(EvmAddress.from(l1Token), this.clients.hubPoolClient.chainId); assert(l1TokenInfo.address.isEVM()); return { ...l1TokenInfo, address: l1TokenInfo.address, }; }); - this.l2OnlyTokens = monitorConfig.l2OnlyTokens; this.l1Tokens = this.clients.hubPoolClient.getL1Tokens(); this.bundleDataApproxClient = new BundleDataApproxClient( this.clients.spokePoolClients, @@ -153,44 +135,8 @@ export class Monitor { ); } - /** - * Returns L2-only tokens for a specific chain. - */ - private getL2OnlyTokensForChain(chainId: number): L2Token[] { - return this.l2OnlyTokens.filter((token) => token.chainId === chainId); - } - - /** - * Generates markdown report for a token's balances across the specified chains. - * Returns the token markdown section and summary entry. - */ - private generateTokenBalanceMarkdown( - report: RelayerBalanceTable, - token: { symbol: string; decimals: number }, - chainNames: string[], - labelSuffix = "" - ): { mrkdwn: string; summaryEntry: string } { - let tokenMrkdwn = ""; - for (const chainName of chainNames) { - const balancesBN = Object.values(report[token.symbol]?.[chainName] ?? {}); - if (balancesBN.find((b) => b.gt(bnZero))) { - const balances = balancesBN.map((balance) => - balance.gt(bnZero) ? convertFromWei(balance.toString(), token.decimals) : "0" - ); - tokenMrkdwn += `${chainName}: ${balances.join(", ")}\n`; - } else { - tokenMrkdwn += `${chainName}: 0\n`; - } - } - - const totalBalance = report[token.symbol]?.[ALL_CHAINS_NAME]?.[BalanceType.TOTAL] ?? bnZero; - if (totalBalance.gt(bnZero)) { - return { - mrkdwn: `*[${token.symbol}${labelSuffix}]*\n` + tokenMrkdwn, - summaryEntry: `${token.symbol}: ${convertFromWei(totalBalance.toString(), token.decimals)}\n`, - }; - } - return { mrkdwn: "", summaryEntry: `${token.symbol}: 0\n` }; + protected getTokenInfo(token: Address, chainId: number): TokenInfo { + return getTokenInfo(token, chainId); } public async update(): Promise { @@ -203,26 +149,6 @@ export class Monitor { // We should initialize the bundle data approx client here because it depends on the spoke pool clients, and we // should do it every time the spoke pool clients are updated. this.bundleDataApproxClient.initialize(); - - const searchConfigs = Object.fromEntries( - Object.entries(this.spokePoolsBlocks).map(([chainId, config]) => [ - chainId, - { - from: config.startingBlock, - to: config.endingBlock, - maxLookBack: 0, - }, - ]) - ); - const tokensPerChain = Object.fromEntries( - this.monitorChains.filter(chainIsEvm).map((chainId) => { - const l2Tokens = this.l1Tokens - .map((l1Token) => this.getRemoteTokenForL1Token(l1Token.address, chainId)) - .filter(isDefined); - return [chainId, l2Tokens]; - }) - ); - await this.clients.tokenTransferClient.update(searchConfigs, tokensPerChain); } async checkUtilization(): Promise { @@ -283,7 +209,7 @@ export class Monitor { let tokenInfo: TokenInfo; try { - tokenInfo = this.clients.hubPoolClient.getTokenInfoForAddress(outputToken, destinationChainId); + tokenInfo = this.getTokenInfo(outputToken, destinationChainId); } catch { tokenInfo = { symbol: "UNKNOWN TOKEN", decimals: 18, address: outputToken }; } @@ -386,10 +312,7 @@ export class Monitor { let unfilledAmount: string; try { let decimals: number; - ({ symbol, decimals } = this.clients.hubPoolClient.getTokenInfoForAddress( - toAddressType(tokenAddress, chainId), - chainId - )); + ({ symbol, decimals } = this.getTokenInfo(toAddressType(tokenAddress, chainId), chainId)); unfilledAmount = convertFromWei(amountByToken[tokenAddress].toString(), decimals); } catch { symbol = tokenAddress; // Using the address helps investigation. @@ -479,213 +402,205 @@ export class Monitor { } } - l2TokenAmountToL1TokenAmountConverter(l2Token: Address, chainId: number): (BigNumber) => BigNumber { - // Step 1: Get l1 token address equivalent of L2 token - const l1Token = getL1TokenAddress(l2Token, chainId); - const l1TokenDecimals = getTokenInfo(l1Token, this.clients.hubPoolClient.chainId).decimals; - const l2TokenDecimals = getTokenInfo(l2Token, chainId).decimals; - return ConvertDecimals(l2TokenDecimals, l1TokenDecimals); - } - - getL1TokensForRelayerBalancesReport(): L1Token[] { - const allL1Tokens = [...this.l1Tokens, ...this.additionalL1Tokens].map(({ symbol, ...tokenInfo }) => { - return { - ...tokenInfo, - // Remap symbols so that we're using a symbol available to us in TOKEN_SYMBOLS_MAP. - symbol: TOKEN_EQUIVALENCE_REMAPPING[symbol] ?? symbol, - }; - }); - // @dev Handle special case for L1 USDC which is mapped to two L2 tokens on some chains, so we can more easily - // see L2 Bridged USDC balance versus Native USDC. Add USDC.e right after the USDC element. - const indexOfUsdc = allL1Tokens.findIndex(({ symbol }) => symbol === "USDC"); - if (indexOfUsdc > -1 && TOKEN_SYMBOLS_MAP["USDC.e"].addresses[this.clients.hubPoolClient.chainId]) { - allL1Tokens.splice(indexOfUsdc, 0, { - symbol: "USDC.e", - address: EvmAddress.from(TOKEN_SYMBOLS_MAP["USDC.e"].addresses[this.clients.hubPoolClient.chainId]), - decimals: 6, - }); - } - return allL1Tokens; - } - async reportRelayerBalances(): Promise { + const hubChainId = this.clients.hubPoolClient.chainId; const relayers = this.monitorConfig.monitoredRelayers; - const allL1Tokens = this.getL1TokensForRelayerBalancesReport(); - const l2OnlyTokens = this.l2OnlyTokens; + const allL1Tokens = [...this.l1Tokens, ...this.additionalL1Tokens]; - const chainIds = this.monitorChains; - const allChainNames = chainIds.map(getNetworkName).concat([ALL_CHAINS_NAME]); - const reports = this.initializeBalanceReports(relayers, allL1Tokens, l2OnlyTokens, allChainNames); - - await this.updateCurrentRelayerBalances(reports); - await this.updateLatestAndFutureRelayerRefunds(reports); + // Fetch pending rebalances once for all relayers. + let pendingRebalances: { [chainId: number]: { [token: string]: BigNumber } } = {}; + if (isDefined(this.clients.rebalancerClient)) { + pendingRebalances = await this.clients.rebalancerClient.getPendingRebalances(); + } for (const relayer of relayers) { - const report = reports[relayer.toNative()]; - let summaryMrkdwn = "*[Summary]*\n"; - let mrkdwn = "Token amounts: current, pending execution, cross-chain transfers, total\n"; - - // Report L1 tokens (all chains) - for (const token of allL1Tokens) { - const { mrkdwn: tokenMrkdwn, summaryEntry } = this.generateTokenBalanceMarkdown(report, token, allChainNames); - mrkdwn += tokenMrkdwn; - summaryMrkdwn += summaryEntry; + // Pre-compute L2 tokens per (l1Token, chainId) and build a single batch of all balance requests + // so we can fetch all balances in one parallel call instead of sequentially per chain. + type L2TokenEntry = { l1Token: L1Token; chainId: number; l2Tokens: Address[] }; + const l2TokenEntries: L2TokenEntry[] = []; + const allBalanceRequests: BalanceRequest[] = []; + + for (const l1Token of allL1Tokens) { + for (const chainId of this.monitorChains) { + if (!relayer.isValidOn(chainId)) { + continue; + } + const l2Tokens = getInventoryBalanceContributorTokens(l1Token.address, chainId, hubChainId); + if (l2Tokens.length === 0) { + continue; + } + l2TokenEntries.push({ l1Token, chainId, l2Tokens }); + for (const l2Token of l2Tokens) { + allBalanceRequests.push({ chainId, token: l2Token, account: relayer }); + } + } } - // Report L2-only tokens (only their specific chain) - for (const token of l2OnlyTokens) { - const { mrkdwn: tokenMrkdwn, summaryEntry } = this.generateTokenBalanceMarkdown( - report, - token, - [getNetworkName(token.chainId)], - " (L2-only)" - ); - mrkdwn += tokenMrkdwn; - summaryMrkdwn += summaryEntry; + // Fetch all balances and pending L2 withdrawals in parallel. + const [allRawBalances, pendingL2Withdrawals] = await Promise.all([ + this._getBalances(allBalanceRequests), + (async () => { + const withdrawals: { [l1Token: string]: { [chainId: number]: BigNumber } } = {}; + await Promise.all( + allL1Tokens.map(async (l1Token) => { + withdrawals[l1Token.address.toNative()] = + await this.clients.crossChainTransferClient.adapterManager.getTotalPendingWithdrawalAmount( + this.crossChainAdapterSupportedChains.filter((chainId) => chainId !== hubChainId), + relayer, + l1Token.address + ); + }) + ); + this.logger.debug({ + at: "Monitor#reportRelayerBalances", + message: "Pending L2->L1 withdrawals", + withdrawals, + }); + return withdrawals; + })(), + ]); + + // Index raw balances by (chainId, l2Token) for O(1) lookup. + const balanceIndex: { [chainId: number]: { [l2Token: string]: BigNumber } } = {}; + let balIdx = 0; + for (const { chainId, l2Tokens } of l2TokenEntries) { + balanceIndex[chainId] ??= {}; + for (const l2Token of l2Tokens) { + balanceIndex[chainId][l2Token.toNative()] = allRawBalances[balIdx++]; + } } - mrkdwn += summaryMrkdwn; - this.logger.info({ - at: "Monitor#reportRelayerBalances", - message: `Balance report for ${relayer} 📖`, - mrkdwn, - }); - } + for (const l1Token of allL1Tokens) { + const l1TokenDecimals = l1Token.decimals; + const formatWei = createFormatFunction(2, 4, false, l1TokenDecimals); - // Build a combined token list for decimal lookups in the debug logging - const allTokensWithDecimals = new Map(); - allL1Tokens.forEach((token) => allTokensWithDecimals.set(token.symbol, token.decimals)); - l2OnlyTokens.forEach((token) => allTokensWithDecimals.set(token.symbol, token.decimals)); + // Collect all rows first so we can compute column widths for alignment. + type Row = { chain: string; token: string; current: string; pending: string; total: string }; + const rows: Row[] = []; + let tokenTotal = bnZero; - Object.entries(reports).forEach(([relayer, balanceTable]) => { - Object.entries(balanceTable).forEach(([tokenSymbol, columns]) => { - const decimals = allTokensWithDecimals.get(tokenSymbol); - if (!decimals) { - throw new Error(`No decimals found for ${tokenSymbol}`); - } - Object.entries(columns).forEach(([chainName, cell]) => { - if (this._tokenEnabledForNetwork(tokenSymbol, chainName) || chainName === ALL_CHAINS_NAME) { - Object.entries(cell).forEach(([balanceType, balance]) => { - // Don't log zero balances. - if (balance.isZero()) { - return; + for (const chainId of this.monitorChains) { + if (!relayer.isValidOn(chainId)) { + continue; + } + + const l2Tokens = getInventoryBalanceContributorTokens(l1Token.address, chainId, hubChainId); + if (l2Tokens.length === 0) { + continue; + } + + for (const l2Token of l2Tokens) { + const { symbol: l2Symbol, decimals: l2Decimals } = this.getTokenInfo(l2Token, chainId); + const toL1Decimals = ConvertDecimals(l2Decimals, l1TokenDecimals); + + // Current balance (converted to L1 decimals). + const rawBalance = balanceIndex[chainId]?.[l2Token.toNative()] ?? bnZero; + const currentBalance = toL1Decimals(rawBalance); + + // Pending: cross-chain transfers + pending L2 withdrawals (hub chain only) + pending swap rebalances. + let pending = this.clients.crossChainTransferClient.getOutstandingCrossChainTransferAmount( + relayer, + chainId, + l1Token.address, + l2Token + ); + + // If chain is hub chain, there should only be one l2 token,so its safe to add the pendingL2Withdrawals + // amount here and assume it won't get re-added on the next l2 token iteration. + if (chainId === hubChainId) { + assert(l2Tokens.length === 1, "Hub chain should only have one l2 token"); + const withdrawals = pendingL2Withdrawals[l1Token.address.toNative()] ?? {}; + const totalWithdrawals = Object.values(withdrawals).reduce((acc, amt) => acc.add(amt), bnZero); + pending = pending.add(totalWithdrawals); + } + + // Only add pending rebalance amount for the canonical L2 token to avoid double-counting + // when multiple contributor tokens exist on the same chain. + const canonicalL2Token = getRemoteTokenForL1Token(l1Token.address, chainId, hubChainId); + if (isDefined(canonicalL2Token) && l2Token.eq(canonicalL2Token)) { + const pendingRebalanceAmount = pendingRebalances[chainId]?.[l1Token.symbol]; + if (isDefined(pendingRebalanceAmount) && !pendingRebalanceAmount.isZero()) { + pending = pending.add(toL1Decimals(pendingRebalanceAmount)); } + } + + const totalBalance = currentBalance.add(pending); + tokenTotal = tokenTotal.add(totalBalance); + + // Skip rows where all value columns are zero. + if (!currentBalance.isZero() || !pending.isZero()) { + rows.push({ + chain: getNetworkName(chainId), + token: l2Symbol, + current: formatWei(currentBalance.toString()), + pending: formatWei(pending.toString()), + total: formatWei(totalBalance.toString()), + }); + } + + // Machine-readable debug log — skip zero-balance entries. + if (!totalBalance.isZero()) { this.logger.debug({ at: "Monitor#reportRelayerBalances", message: "Machine-readable single balance report", relayer, - tokenSymbol, - decimals, - chainName, - balanceType, - balanceInWei: balance.toString(), - balance: Number(utils.formatUnits(balance, decimals)), + tokenSymbol: l1Token.symbol, + l2TokenSymbol: l2Symbol, + chainName: getNetworkName(chainId), + decimals: l1TokenDecimals, + balanceInWei: totalBalance.toString(), + balance: Number(formatUnits(totalBalance, l1TokenDecimals)), datadog: true, }); - }); + } } - }); - }); - }); - } - - // Update current balances of all tokens on each supported chain for each relayer. - async updateCurrentRelayerBalances(relayerBalanceReport: RelayerBalanceReport): Promise { - const l1Tokens = this.getL1TokensForRelayerBalancesReport(); - for (const relayer of this.monitorConfig.monitoredRelayers) { - for (const chainId of this.monitorChains) { - // If the monitored relayer address is invalid on the monitored chain (e.g. the monitored relayer is a base58 address while the chain ID is mainnet), - // then there is no balance to update in this loop. - if (!relayer.isValidOn(chainId)) { - continue; + // Upcoming refund row per chain (one per chain, not per L2 token). + const upcomingRefunds = this.bundleDataApproxClient.getUpcomingRefunds(chainId, l1Token.address, relayer); + if (upcomingRefunds.gt(0)) { + tokenTotal = tokenTotal.add(upcomingRefunds); + rows.push({ + chain: getNetworkName(chainId), + token: "refunds", + current: "-", + pending: "-", + total: formatWei(upcomingRefunds.toString()), + }); + } } - const l2ToL1Tokens = this.getL2ToL1TokenMap(l1Tokens, chainId); - const l2TokenAddresses = Object.keys(l2ToL1Tokens); - const tokenBalances = await this._getBalances( - l2TokenAddresses.map((address) => ({ - token: toAddressType(address, chainId), - chainId: chainId, - account: relayer, - })) - ); - for (let i = 0; i < l2TokenAddresses.length; i++) { - const decimalConverter = this.l2TokenAmountToL1TokenAmountConverter( - toAddressType(l2TokenAddresses[i], chainId), - chainId - ); - const { symbol } = l2ToL1Tokens[l2TokenAddresses[i]]; - this.updateRelayerBalanceTable( - relayerBalanceReport[relayer.toNative()], - symbol, - getNetworkName(chainId), - BalanceType.CURRENT, - decimalConverter(tokenBalances[i]) - ); + // Skip entire token table if total balance is zero. + if (tokenTotal.lte(0)) { + continue; } - // Handle L2-only tokens for this chain - const l2OnlyTokensForChain = this.getL2OnlyTokensForChain(chainId); - if (l2OnlyTokensForChain.length > 0) { - const l2OnlyBalances = await this._getBalances( - l2OnlyTokensForChain.map((token) => ({ - token: token.address, - chainId: chainId, - account: relayer, - })) - ); - - for (let i = 0; i < l2OnlyTokensForChain.length; i++) { - const token = l2OnlyTokensForChain[i]; - // L2-only tokens don't need decimal conversion since they don't map to L1 - this.updateRelayerBalanceTable( - relayerBalanceReport[relayer.toNative()], - token.symbol, - getNetworkName(chainId), - BalanceType.CURRENT, - l2OnlyBalances[i] - ); + // Build stacked key-value format for mobile readability. + const totalFormatted = formatWei(tokenTotal.toString()); + const valueWidth = Math.max( + ...rows.flatMap((r) => [r.current, r.pending, r.total].map((v) => v.length)), + totalFormatted.length + ); + let tokenMrkdwn = "```\n"; + for (const row of rows) { + tokenMrkdwn += `${row.chain} — ${row.token}\n`; + if (row.current !== "-") { + tokenMrkdwn += ` Current: ${row.current.padStart(valueWidth)}\n`; + tokenMrkdwn += ` Pending: ${row.pending.padStart(valueWidth)}\n`; } + tokenMrkdwn += ` Total: ${row.total.padStart(valueWidth)}\n\n`; } + tokenMrkdwn += ` TOTAL: ${totalFormatted.padStart(valueWidth)}\n`; + tokenMrkdwn += "```"; + + this.logger.info({ + at: "Monitor#reportRelayerBalances", + message: `Balance report for ${relayer} [${l1Token.symbol}]`, + mrkdwn: tokenMrkdwn, + }); } } } - // Returns a dictionary of L2 token addresses on this chain to their mapped L1 token info. For example, this - // will return a dictionary for Optimism including WETH, WBTC, USDC, USDC.e, USDT entries where the key is - // the token's Optimism address and the value is the equivalent L1 token info. - protected getL2ToL1TokenMap(l1Tokens: L1Token[], chainId: number): { [l2TokenAddress: string]: L1Token } { - return Object.fromEntries( - l1Tokens - .map((l1Token) => { - // @dev l2TokenSymbols is a list of all keys in TOKEN_SYMBOLS_MAP where the hub chain address is equal to the - // l1 token address. - const l2TokenSymbols = Object.entries(TOKEN_SYMBOLS_MAP) - .filter( - ([, { addresses }]) => - addresses[this.clients.hubPoolClient.chainId]?.toLowerCase() === - l1Token.address.toEvmAddress().toLowerCase() - ) - .map(([symbol]) => symbol); - - // Create an entry for all L2 tokens that share a symbol with the L1 token. This includes tokens - // like USDC which has multiple L2 tokens mapped to the same L1 token for a given chain ID. - return l2TokenSymbols - .filter((symbol) => TOKEN_SYMBOLS_MAP[symbol].addresses[chainId] !== undefined) - .map((symbol) => { - if (chainId !== this.clients.hubPoolClient.chainId && sdkUtils.isBridgedUsdc(symbol)) { - return [TOKEN_SYMBOLS_MAP[symbol].addresses[chainId], { ...l1Token, symbol: "USDC.e" }]; - } else { - const remappedSymbol = TOKEN_EQUIVALENCE_REMAPPING[symbol] ?? symbol; - return [TOKEN_SYMBOLS_MAP[symbol].addresses[chainId], { ...l1Token, symbol: remappedSymbol }]; - } - }); - }) - .flat() - ); - } - async checkBalances(): Promise { const { monitoredBalances } = this.monitorConfig; const balances = await this._getBalances(monitoredBalances); @@ -736,7 +651,7 @@ export class Monitor { spokePoolClient.spokePool.provider ).symbol(); } else { - symbol = getTokenInfo(token, chainId).symbol; + symbol = this.getTokenInfo(token, chainId).symbol; } } return { @@ -779,242 +694,59 @@ export class Monitor { }); } - async checkSpokePoolRunningBalances(): Promise { - // We define a custom format function since we do not want the same precision that `convertFromWei` gives us. - const formatWei = (weiVal: string, decimals: number) => - weiVal === "0" ? "0" : createFormatFunction(1, 4, false, decimals)(weiVal); - - const hubPoolClient = this.clients.hubPoolClient; - const monitoredTokenSymbols = this.monitorConfig.monitoredTokenSymbols; - - // Define the chain IDs in the same order as `enabledChainIds` so that block range ordering is preserved. + async reportSpokePoolRunningBalances(): Promise { const chainIds = this.monitorConfig.monitoredSpokePoolChains.length !== 0 ? this.monitorChains.filter((chain) => this.monitorConfig.monitoredSpokePoolChains.includes(chain)) : this.monitorChains; - const l2TokenForChain = (chainId: number, symbol: string) => { - const _l2Token = TOKEN_SYMBOLS_MAP[symbol]?.addresses[chainId]; - return isDefined(_l2Token) ? toAddressType(_l2Token, chainId) : undefined; - }; - const pendingRelayerRefunds = {}; - const pendingRebalanceRoots = {}; - - // Take the validated bundles from the hub pool client. - const validatedBundles = sortEventsDescending(hubPoolClient.getValidatedRootBundles()).slice( - 0, - this.monitorConfig.bundlesCount - ); - - // Fetch the data from the latest root bundle. - const bundle = hubPoolClient.getLatestProposedRootBundle(); - const nextBundleMainnetStartBlock = hubPoolClient.getNextBundleStartBlockNumber( - this.clients.bundleDataClient.chainIdListForBundleEvaluationBlockNumbers, - hubPoolClient.latestHeightSearched, - hubPoolClient.chainId - ); - const enabledChainIds = this.clients.configStoreClient.getChainIdIndicesForBlock(nextBundleMainnetStartBlock); - - this.logger.debug({ - at: "Monitor#checkSpokePoolRunningBalances", - message: "Mainnet root bundles in scope", - validatedBundles, - outstandingBundle: bundle, - }); - - const slowFillBlockRange = await getWidestPossibleExpectedBlockRange( - enabledChainIds, - this.clients.spokePoolClients, - getEndBlockBuffers(enabledChainIds, this.clients.bundleDataClient.blockRangeEndBlockBuffer), - this.clients, - hubPoolClient.latestHeightSearched, - this.clients.configStoreClient.getEnabledChains(hubPoolClient.latestHeightSearched) - ); - const blockRangeTail = bundle.bundleEvaluationBlockNumbers.map((endBlockForChain, idx) => { - const endBlockNumber = Number(endBlockForChain); - const spokeLatestBlockSearched = this.clients.spokePoolClients[enabledChainIds[idx]]?.latestHeightSearched ?? 0; - return spokeLatestBlockSearched === 0 - ? [endBlockNumber, endBlockNumber] - : [endBlockNumber + 1, spokeLatestBlockSearched > endBlockNumber ? spokeLatestBlockSearched : endBlockNumber]; - }); - - this.logger.debug({ - at: "Monitor#checkSpokePoolRunningBalances", - message: "Block ranges to search", - slowFillBlockRange, - blockRangeTail, - }); - - const lastProposedBundleBlockRanges = getImpliedBundleBlockRanges( - hubPoolClient, - this.clients.configStoreClient, - hubPoolClient.hasPendingProposal() - ? hubPoolClient.getLatestProposedRootBundle() - : hubPoolClient.getNthFullyExecutedRootBundle(-1) - ); - // 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. - const [poolRebalanceRoot, currentBundleData, currentSpokeBalances] = await Promise.all([ - this.clients.bundleDataClient.loadData(lastProposedBundleBlockRanges, this.clients.spokePoolClients, true), - this.clients.bundleDataClient.loadData(slowFillBlockRange, this.clients.spokePoolClients, true), - Object.fromEntries( - await mapAsync(chainIds, async (chainId) => { - const spokePool = this.clients.spokePoolClients[chainId].spokePoolAddress; - const l2TokenAddresses = monitoredTokenSymbols - .map((symbol) => l2TokenForChain(chainId, symbol)) - .filter(isDefined); - const balances = Object.fromEntries( - await mapAsync(l2TokenAddresses, async (l2Token) => [ - l2Token, - ( - await this._getBalances([ - { - token: l2Token, - chainId: chainId, - account: spokePool, - }, - ]) - )[0], - ]) - ); - return [chainId, balances]; - }) - ), - ]); - - const poolRebalanceLeaves = ( - await _buildPoolRebalanceRoot( - lastProposedBundleBlockRanges[0][1], - lastProposedBundleBlockRanges[0][1], - poolRebalanceRoot.bundleDepositsV3, - poolRebalanceRoot.bundleFillsV3, - poolRebalanceRoot.bundleSlowFillsV3, - poolRebalanceRoot.unexecutableSlowFills, - poolRebalanceRoot.expiredDepositsToRefundV3, - this.clients - ) - ).leaves; - - // Get the pool rebalance leaf amounts. - const enabledTokens = [...this.l1Tokens]; - for (const leaf of poolRebalanceLeaves) { - if (!chainIds.includes(leaf.chainId)) { - continue; - } - const l2TokenMap = this.getL2ToL1TokenMap(enabledTokens, leaf.chainId); - pendingRebalanceRoots[leaf.chainId] = {}; - Object.entries(l2TokenMap).forEach(([l2Token, l1Token]) => { - const rebalanceAmount = - leaf.netSendAmounts[ - leaf.l1Tokens - .map((l1Token) => l1Token.toEvmAddress()) - .findIndex((token) => token === l1Token.address.toEvmAddress()) - ]; - pendingRebalanceRoots[leaf.chainId][l2Token] = rebalanceAmount ?? bnZero; - }); - } - - this.logger.debug({ - at: "Monitor#checkSpokePoolRunningBalances", - message: "Print pool rebalance leaves", - poolRebalanceRootLeaves: poolRebalanceLeaves, - }); - - // Calculate the pending refunds. - for (const chainId of chainIds) { - const l2TokenMap = this.getL2ToL1TokenMap(enabledTokens, chainId); - const l2TokenAddresses = monitoredTokenSymbols - .map((symbol) => l2TokenForChain(chainId, symbol)) - .filter(isDefined); - pendingRelayerRefunds[chainId] = {}; - l2TokenAddresses.forEach((l2Token) => { - const l1Token = l2TokenMap[l2Token.toNative()]; - const upcomingBundleRefunds = this.getUpcomingRefunds(chainId, l1Token.address); - pendingRelayerRefunds[chainId][l2Token.toNative()] = upcomingBundleRefunds; - }); - - 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. - Object.entries(currentBundleData.bundleSlowFillsV3) - .filter(([chainId]) => chainIds.includes(+chainId)) - .map(([chainId, bundleSlowFills]) => { - const l2TokenAddresses = monitoredTokenSymbols - .map((symbol) => l2TokenForChain(+chainId, symbol)) - .filter(isDefined); - Object.entries(bundleSlowFills) - .filter(([l2Token]) => l2TokenAddresses.map((_l2Token) => _l2Token.toBytes32()).includes(l2Token)) - .map(([l2Token, fills]) => { - const _l2Token = toAddressType(l2Token, +chainId); - const pendingSlowFillAmounts = fills - .map((fill) => fill.outputAmount) - .filter(isDefined) - .reduce((totalAmounts, outputAmount) => totalAmounts.add(outputAmount), bnZero); - pendingRelayerRefunds[chainId][_l2Token.toNative()] = - pendingRelayerRefunds[chainId][_l2Token.toNative()].add(pendingSlowFillAmounts); - }); - }); + for (const l1Token of this.l1Tokens.filter((l1Token) => + this.monitorConfig.monitoredTokenSymbols.includes(l1Token.symbol) + )) { + const formatWei = createFormatFunction(1, 4, false, l1Token.decimals); + const results = await getLatestRunningBalances( + l1Token.address, + chainIds, + this.clients.hubPoolClient, + this.bundleDataApproxClient + ); - // Print the output: The current spoke pool balance, the amount of refunds to payout, the pending pool rebalances, and then the sum of the three. - let tokenMarkdown = - "Token amounts: current, pending relayer refunds, pool rebalances, adjusted spoke pool balance\n"; - for (const tokenSymbol of monitoredTokenSymbols) { - tokenMarkdown += `*[${tokenSymbol}]*\n`; + type Row = { chain: string; validated: string; deposits: string; refunds: string; total: string }; + const rows: Row[] = []; for (const chainId of chainIds) { - const tokenAddress = l2TokenForChain(chainId, tokenSymbol); - - // If the token does not exist on the chain, then ignore this report. - if (!isDefined(tokenAddress)) { + const r = results[chainId]; + if (!r) { continue; } + rows.push({ + chain: getNetworkName(chainId), + validated: formatWei(r.lastValidatedRunningBalance.toString()), + deposits: `-${formatWei(r.upcomingDeposits.toString())}`, + refunds: `+${formatWei(r.upcomingRefunds.toString())}`, + total: formatWei(r.absLatestRunningBalance.toString()), + }); + } - const tokenDecimals = resolveTokenDecimals(tokenSymbol); - const currentSpokeBalance = formatWei( - currentSpokeBalances[chainId][tokenAddress.toNative()].toString(), - tokenDecimals - ); - - // Relayer refunds may be undefined when there were no refunds included in the last bundle. - const currentRelayerRefunds = formatWei( - (pendingRelayerRefunds[chainId]?.[tokenAddress.toNative()] ?? bnZero).toString(), - tokenDecimals - ); - // Rebalance roots will be undefined when there was no root in the last bundle for the chain. - const currentRebalanceRoots = formatWei( - (pendingRebalanceRoots[chainId]?.[tokenAddress.toNative()] ?? bnZero).toString(), - tokenDecimals - ); - const virtualSpokeBalance = formatWei( - currentSpokeBalances[chainId][tokenAddress.toNative()] - .add(pendingRebalanceRoots[chainId]?.[tokenAddress.toNative()] ?? bnZero) - .sub(pendingRelayerRefunds[chainId]?.[tokenAddress.toNative()] ?? bnZero) - .toString(), - tokenDecimals - ); - tokenMarkdown += `${getNetworkName(chainId)}: `; - tokenMarkdown += - currentSpokeBalance + - `, ${currentRelayerRefunds !== "0" ? "-" : ""}` + - currentRelayerRefunds + - ", " + - currentRebalanceRoots + - ", " + - virtualSpokeBalance + - "\n"; + // Build stacked key-value format for mobile readability. + const valueWidth = Math.max( + ...rows.flatMap((r) => [r.validated, r.deposits, r.refunds, r.total].map((v) => v.length)) + ); + let tokenMrkdwn = "```\n"; + for (const row of rows) { + tokenMrkdwn += `${row.chain} — ${l1Token.symbol}\n`; + tokenMrkdwn += ` Last Validated: ${row.validated.padStart(valueWidth)}\n`; + tokenMrkdwn += ` Deposits: ${row.deposits.padStart(valueWidth)}\n`; + tokenMrkdwn += ` Refunds: ${row.refunds.padStart(valueWidth)}\n`; + tokenMrkdwn += ` Total: ${row.total.padStart(valueWidth)}\n\n`; } + tokenMrkdwn += "```"; + + this.logger.info({ + at: "Monitor#reportSpokePoolRunningBalances", + message: `Spoke pool running balances [${l1Token.symbol}]`, + mrkdwn: tokenMrkdwn, + }); } - this.logger.info({ - at: "Monitor#checkSpokePoolRunningBalances", - message: "Spoke pool balance report", - mrkdwn: tokenMarkdown, - }); } // We approximate stuck rebalances by checking if there are still any pending cross chain transfers to any SpokePools @@ -1325,264 +1057,6 @@ export class Monitor { }); } - async updateLatestAndFutureRelayerRefunds(relayerBalanceReport: RelayerBalanceReport): Promise { - // Calculate which fills have not yet been refunded for each monitored relayer. - const allL1Tokens = this.getL1TokensForRelayerBalancesReport(); - for (const relayer of this.monitorConfig.monitoredRelayers) { - for (const l1Token of allL1Tokens) { - for (const chainId of this.monitorChains) { - if (l1Token.symbol === "USDC.e") { - // We don't want to double count USDC/USDC.e repayments. When this.getUpcomingRefunds() is queried for a - // specific L1 token address, it will return either USDC.e and USDC repayments--depending on the 'native' USDC - // for the L2 chain in question. USDC.e is a special token injected into - // this.getL1TokensForRelayerBalancesReport() so we can skip it here. - continue; - } - const upcomingRefunds = this.getUpcomingRefunds(chainId, l1Token.address, relayer); - if (upcomingRefunds.gt(0)) { - const l2TokenAddress = getRemoteTokenForL1Token( - l1Token.address, - chainId, - this.clients.hubPoolClient.chainId - ); - const decimalConverter = this.l2TokenAmountToL1TokenAmountConverter(l2TokenAddress, chainId); - this.updateRelayerBalanceTable( - relayerBalanceReport[relayer.toNative()], - l1Token.symbol, - getNetworkName(chainId), - BalanceType.PENDING, - decimalConverter(upcomingRefunds) - ); - } - } - } - } - for (const relayer of this.monitorConfig.monitoredRelayers) { - this.updateCrossChainTransfers(relayer, relayerBalanceReport[relayer.toNative()]); - } - await Promise.all( - this.monitorConfig.monitoredRelayers.map(async (relayer) => { - await this.updatePendingL2Withdrawals(relayer, relayerBalanceReport[relayer.toNative()]); - await this.updatePendingRebalances(relayer, relayerBalanceReport[relayer.toNative()]); - }) - ); - } - - getUpcomingRefunds(chainId: number, l1Token: Address, relayer?: Address): BigNumber { - return this.bundleDataApproxClient.getUpcomingRefunds(chainId, l1Token, relayer); - } - - updateCrossChainTransfers(relayer: Address, relayerBalanceTable: RelayerBalanceTable): void { - const allL1Tokens = this.getL1TokensForRelayerBalancesReport(); - const supportedChains = this.crossChainAdapterSupportedChains.filter((chainId) => - this.monitorChains.includes(chainId) - ); - for (const chainId of supportedChains) { - const l2ToL1Tokens = this.getL2ToL1TokenMap(allL1Tokens, chainId); - const l2TokenAddresses = Object.keys(l2ToL1Tokens); - - for (const l2Token of l2TokenAddresses) { - const tokenInfo = l2ToL1Tokens[l2Token]; - const bridgedTransferBalance = this.clients.crossChainTransferClient.getOutstandingCrossChainTransferAmount( - relayer, - chainId, - tokenInfo.address, - toAddressType(l2Token, chainId) - ); - this.updateRelayerBalanceTable( - relayerBalanceTable, - tokenInfo.symbol, - getNetworkName(chainId), - BalanceType.PENDING_TRANSFERS, - bridgedTransferBalance - ); - } - } - } - - async updatePendingL2Withdrawals(relayer: Address, relayerBalanceTable: RelayerBalanceTable): Promise { - const allL1Tokens = this.getL1TokensForRelayerBalancesReport(); - const supportedChains = this.crossChainAdapterSupportedChains.filter( - (chainId) => this.monitorChains.includes(chainId) && chainId !== CHAIN_IDs.BSC // @todo temporarily skip BSC as the following - // getTotalPendingWithdrawalAmount() async call is getting rate limited by the Binance API. - // We should add more rate limiting or retry logic to this call. - ); - const allPendingWithdrawalBalances: { [l1Token: string]: { [chainId: number]: BigNumber } } = {}; - await Promise.all( - allL1Tokens.map(async (l1Token) => { - const pendingWithdrawalBalances = - await this.clients.crossChainTransferClient.adapterManager.getTotalPendingWithdrawalAmount( - supportedChains, - relayer, - l1Token.address - ); - allPendingWithdrawalBalances[l1Token.symbol] = pendingWithdrawalBalances; - for (const _chainId of Object.keys(pendingWithdrawalBalances)) { - const chainId = Number(_chainId); - if (pendingWithdrawalBalances[chainId].eq(bnZero)) { - continue; - } - if (!this.clients.crossChainTransferClient.adapterManager.l2TokenExistForL1Token(l1Token.address, chainId)) { - continue; - } - - const l2Token = this.clients.crossChainTransferClient.adapterManager.l2TokenForL1Token( - l1Token.address, - chainId - ); - const l2TokenInfo = getTokenInfo(l2Token, chainId); - const l2ToL1DecimalConverter = sdkUtils.ConvertDecimals(l2TokenInfo.decimals, l1Token.decimals); - // Add pending withdrawals as a "cross chain transfer" to the hub balance - this.updateRelayerBalanceTable( - relayerBalanceTable, - l1Token.symbol, - getNetworkName(this.clients.hubPoolClient.chainId), - BalanceType.PENDING_TRANSFERS, - l2ToL1DecimalConverter(pendingWithdrawalBalances[Number(chainId)]) - ); - } - }) - ); - this.logger.debug({ - at: "Monitor#updatePendingL2Withdrawals", - message: "Updated pending L2->L1 withdrawals", - allPendingWithdrawalBalances, - }); - } - - async updatePendingRebalances(relayer: Address, relayerBalanceTable: RelayerBalanceTable): Promise { - // Rebalancer integration is optional in monitor; if absent, treat as no pending rebalances. - if (!isDefined(this.clients.rebalancerClient)) { - return; - } - - let pendingRebalances: { [chainId: number]: { [token: string]: BigNumber } }; - try { - pendingRebalances = await this.clients.rebalancerClient.getPendingRebalances(); - } catch (error) { - this.logger.warn({ - at: "Monitor#updatePendingRebalances", - message: "Unable to fetch pending rebalances; defaulting to zero", - error, - }); - return; - } - - const l1TokenBySymbol = Object.fromEntries( - this.getL1TokensForRelayerBalancesReport().map((token) => [token.symbol, token]) - ); - for (const [_chainId, tokenBalances] of Object.entries(pendingRebalances)) { - const chainId = Number(_chainId); - if (!this.monitorChains.includes(chainId)) { - continue; - } - for (const [tokenSymbol, amount] of Object.entries(tokenBalances)) { - if (amount.eq(bnZero)) { - continue; - } - const l1Token = l1TokenBySymbol[tokenSymbol]; - if (!isDefined(l1Token)) { - continue; - } - const l2TokenAddress = this.getRemoteTokenForL1Token(l1Token.address, chainId); - if (!isDefined(l2TokenAddress)) { - continue; - } - const l2ToL1DecimalConverter = this.l2TokenAmountToL1TokenAmountConverter(l2TokenAddress, chainId); - this.updateRelayerBalanceTable( - relayerBalanceTable, - l1Token.symbol, - getNetworkName(chainId), - BalanceType.PENDING_TRANSFERS, - l2ToL1DecimalConverter(amount) - ); - } - } - this.logger.debug({ - at: "Monitor#updatePendingRebalances", - message: "Updated pending rebalance credits", - relayer, - pendingRebalances, - }); - } - - getTotalTransferAmount(transfers: TokenTransfer[]): BigNumber { - return transfers.map((transfer) => transfer.value).reduce((a, b) => a.add(b)); - } - - initializeBalanceReports( - relayers: Address[], - allL1Tokens: L1Token[], - l2OnlyTokens: L2Token[], - allChainNames: string[] - ): RelayerBalanceReport { - const reports: RelayerBalanceReport = {}; - for (const relayer of relayers) { - reports[relayer.toNative()] = {}; - - // Initialize L1 tokens for all chains - for (const token of allL1Tokens) { - reports[relayer.toNative()][token.symbol] = {}; - for (const chainName of allChainNames) { - reports[relayer.toNative()][token.symbol][chainName] = {}; - for (const balanceType of ALL_BALANCE_TYPES) { - reports[relayer.toNative()][token.symbol][chainName][balanceType] = bnZero; - } - } - } - - // Initialize L2-only tokens for their specific chain and the "All chains" summary - for (const token of l2OnlyTokens) { - const tokenChainName = getNetworkName(token.chainId); - reports[relayer.toNative()][token.symbol] = {}; - // Initialize for the specific chain the token exists on - reports[relayer.toNative()][token.symbol][tokenChainName] = {}; - for (const balanceType of ALL_BALANCE_TYPES) { - reports[relayer.toNative()][token.symbol][tokenChainName][balanceType] = bnZero; - } - // Initialize for "All chains" summary - reports[relayer.toNative()][token.symbol][ALL_CHAINS_NAME] = {}; - for (const balanceType of ALL_BALANCE_TYPES) { - reports[relayer.toNative()][token.symbol][ALL_CHAINS_NAME][balanceType] = bnZero; - } - } - } - return reports; - } - protected getRemoteTokenForL1Token(l1Token: EvmAddress, chainId: number | string): Address | undefined { - return chainId === this.clients.hubPoolClient.chainId - ? l1Token - : getRemoteTokenForL1Token(l1Token, chainId, this.clients.hubPoolClient.chainId); - } - - private updateRelayerBalanceTable( - relayerBalanceTable: RelayerBalanceTable, - tokenSymbol: string, - chainName: string, - balanceType: BalanceType, - amount: BigNumber - ) { - this.incrementBalance(relayerBalanceTable, tokenSymbol, chainName, balanceType, amount); - - // We want to update the total balance when there are changes to each individual balance. - this.incrementBalance(relayerBalanceTable, tokenSymbol, chainName, BalanceType.TOTAL, amount); - - // We want to update the all chains column for any changes to each chain's column. - this.incrementBalance(relayerBalanceTable, tokenSymbol, ALL_CHAINS_NAME, balanceType, amount); - this.incrementBalance(relayerBalanceTable, tokenSymbol, ALL_CHAINS_NAME, BalanceType.TOTAL, amount); - } - - private incrementBalance( - relayerBalanceTable: RelayerBalanceTable, - tokenSymbol: string, - chainName: string, - balanceType: BalanceType, - amount: BigNumber - ) { - relayerBalanceTable[tokenSymbol][chainName][balanceType] = - relayerBalanceTable[tokenSymbol][chainName][balanceType].add(amount); - } - private notifyIfUnknownCaller(caller: string, action: BundleAction, txnRef: string) { if ( this.monitorConfig.whitelistedDataworkers.some((dataworker) => @@ -1751,7 +1225,7 @@ export class Monitor { if (isEVMSpokePoolClient(spokePoolClient)) { decimals = await new Contract(token.toEvmAddress(), ERC20.abi, spokePoolClient.spokePool.provider).decimals(); } else { - decimals = getTokenInfo(token, chainId).decimals; + decimals = this.getTokenInfo(token, chainId).decimals; } if (!this.decimals[chainId]) { this.decimals[chainId] = {}; @@ -1764,15 +1238,6 @@ export class Monitor { ); } - private _tokenEnabledForNetwork(tokenSymbol: string, networkName: string): boolean { - for (const [chainId, network] of Object.entries(PUBLIC_NETWORKS)) { - if (network.name === networkName) { - return isDefined(TOKEN_SYMBOLS_MAP[tokenSymbol]?.addresses[chainId]); - } - } - return false; - } - private _shouldCloseFillPDA(fillStatus: FillStatus, fillDeadline: number, currentTime: number): boolean { return fillStatus === FillStatus.Filled && currentTime > fillDeadline; } diff --git a/src/monitor/MonitorClientHelper.ts b/src/monitor/MonitorClientHelper.ts index 32bc457621..7e91c66fbc 100644 --- a/src/monitor/MonitorClientHelper.ts +++ b/src/monitor/MonitorClientHelper.ts @@ -1,6 +1,6 @@ import { MonitorConfig } from "./MonitorConfig"; -import { Signer, winston, assert, isEVMSpokePoolClient, toAddressType } from "../utils"; -import { BundleDataClient, HubPoolClient, TokenTransferClient } from "../clients"; +import { Signer, winston, toAddressType } from "../utils"; +import { BundleDataClient, HubPoolClient } from "../clients"; import { Clients, updateClients, @@ -19,7 +19,6 @@ export interface MonitorClients extends Clients { hubPoolClient: HubPoolClient; rebalancerClient?: RebalancerClient; spokePoolClients: SpokePoolClientsByChain; - tokenTransferClient: TokenTransferClient; } export async function constructMonitorClients( @@ -36,6 +35,7 @@ export async function constructMonitorClients( const { hubPoolClient, configStoreClient } = commonClients; await updateClients(commonClients, config, logger); + // Need to update HubPoolClient to get latest tokens via hubPoolClient.getL1Tokens(). await hubPoolClient.update(); // Construct spoke pool clients for all chains that are not *currently* disabled. Caller can override @@ -56,31 +56,30 @@ export async function constructMonitorClients( config.blockRangeEndBlockBuffer ); - // Need to update HubPoolClient to get latest tokens. - const spokePoolAddresses = Object.values(spokePoolClients).map((client) => client.spokePoolAddress); + // Spoke pool addresses can be reused on different chains so we need to deduplicate them. + const uniqueSpokePoolAddresses: { [chainId: number]: string } = {}; + Object.entries(spokePoolClients).forEach(([chainId, client]) => { + if (!Object.values(uniqueSpokePoolAddresses).includes(client.spokePoolAddress.toNative())) { + uniqueSpokePoolAddresses[chainId] = client.spokePoolAddress.toNative(); + } + }); + const spokePoolAddresses = Object.entries(uniqueSpokePoolAddresses).map(([chainId, address]) => + toAddressType(address, Number(chainId)) + ); // Cross-chain transfers will originate from the HubPool's address and target SpokePool addresses, so // track both. const adapterManager = new AdapterManager(logger, spokePoolClients, hubPoolClient, [ toAddressType(signerAddr, hubPoolClient.chainId), toAddressType(hubPoolClient.hubPool.address, hubPoolClient.chainId), + ...config.monitoredRelayers, ...spokePoolAddresses, ]); const spokePoolChains = Object.keys(spokePoolClients).map((chainId) => Number(chainId)); - const providerPerChain = Object.fromEntries( - spokePoolChains - .filter((chainId) => isEVMSpokePoolClient(spokePoolClients[chainId])) - .map((chainId) => { - const spokePoolClient = spokePoolClients[chainId]; - assert(isEVMSpokePoolClient(spokePoolClient)); - return [chainId, spokePoolClient.spokePool.provider]; - }) - ); - const tokenTransferClient = new TokenTransferClient(logger, providerPerChain, config.monitoredRelayers); // The CrossChainTransferClient is dependent on having adapters for all passed in chains - // so we need to filter out any chains that don't have adapters. This means limiting the chains we keep in - // `providerPerChain` when constructing the TokenTransferClient and limiting `spokePoolChains` when constructing + // so we need to filter out any chains that don't have adapters. This means + // limiting `spokePoolChains` when constructing // the CrossChainTransferClient. const crossChainAdapterSupportedChains = adapterManager.supportedChains(); const crossChainTransferClient = new CrossChainTransferClient( @@ -97,7 +96,6 @@ export async function constructMonitorClients( crossChainTransferClient, rebalancerClient, spokePoolClients, - tokenTransferClient, }; } diff --git a/src/monitor/MonitorConfig.ts b/src/monitor/MonitorConfig.ts index d63b574e83..bc54c01c5c 100644 --- a/src/monitor/MonitorConfig.ts +++ b/src/monitor/MonitorConfig.ts @@ -1,5 +1,4 @@ import winston from "winston"; -import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; import { CommonConfig, ProcessEnv } from "../common"; import { CHAIN_IDs, @@ -8,18 +7,8 @@ import { TOKEN_SYMBOLS_MAP, Address, toAddressType, - EvmAddress, } from "../utils"; -// Interface for tokens that exist only on L2 (no L1 equivalent) -// @TODO: Move this to SDK -export interface L2Token { - symbol: string; - chainId: number; - address: EvmAddress; - decimals: number; -} - // Set modes to true that you want to enable in the AcrossMonitor bot. export interface BotModes { balancesEnabled: boolean; @@ -57,7 +46,6 @@ export class MonitorConfig extends CommonConfig { token: Address; }[] = []; readonly additionalL1NonLpTokens: string[] = []; - readonly l2OnlyTokens: L2Token[] = []; readonly binanceWithdrawWarnThreshold: number; readonly binanceWithdrawAlertThreshold: number; readonly hyperliquidOrderMaximumLifetime: number; @@ -91,7 +79,6 @@ export class MonitorConfig extends CommonConfig { CLOSE_ALTS_ENABLED, HYPERLIQUID_ORDER_MAXIMUM_LIFETIME, HYPERLIQUID_SUPPORTED_TOKENS, - L2_ONLY_TOKENS, } = env; this.botModes = { @@ -126,28 +113,6 @@ export class MonitorConfig extends CommonConfig { } }); - // Parse L2-only tokens: tokens that exist only on L2 chains (no L1 equivalent). - // Format: ["USDH", "OTHER_TOKEN"] - array of token symbols - // - will look up token info from TOKEN_SYMBOLS_MAP - // - will create entries for all chains in MAINNET_CHAIN_IDs where the token has an address - // - all monitored relayers (MONITORED_RELAYERS) will be tracked for these tokens - const l2OnlySymbols: string[] = JSON.parse(L2_ONLY_TOKENS ?? "[]"); - const mainnetChainIds = Object.values(MAINNET_CHAIN_IDs) as number[]; - this.l2OnlyTokens = l2OnlySymbols.flatMap((symbol) => { - const tokenInfo = TOKEN_SYMBOLS_MAP[symbol]; - if (!tokenInfo?.addresses) { - return []; - } - return mainnetChainIds - .filter((chainId) => isDefined(tokenInfo.addresses[chainId])) - .map((chainId) => ({ - symbol, - chainId, - address: EvmAddress.from(tokenInfo.addresses[chainId]), - decimals: tokenInfo.decimals, - })); - }); - this.binanceWithdrawWarnThreshold = Number(BINANCE_WITHDRAW_WARN_THRESHOLD ?? 1); this.binanceWithdrawAlertThreshold = Number(BINANCE_WITHDRAW_ALERT_THRESHOLD ?? 1); diff --git a/src/monitor/index.ts b/src/monitor/index.ts index f527da6784..fe53d900e2 100644 --- a/src/monitor/index.ts +++ b/src/monitor/index.ts @@ -51,7 +51,7 @@ export async function runMonitor(_logger: winston.Logger, baseSigner: Signer): P } if (config.botModes.spokePoolBalanceReportEnabled) { - await acrossMonitor.checkSpokePoolRunningBalances(); + await acrossMonitor.reportSpokePoolRunningBalances(); } else { logger.debug({ at: "Monitor#index", message: "Check spoke pool balances monitor disabled" }); } diff --git a/test/Monitor.ts b/test/Monitor.ts index 318d1c1ac1..ab64222e20 100644 --- a/test/Monitor.ts +++ b/test/Monitor.ts @@ -1,23 +1,27 @@ -import { - BundleDataClient, - ConfigStoreClient, - HubPoolClient, - MultiCallerClient, - SpokePoolClient, - TokenTransferClient, -} from "../src/clients"; +import { BundleDataClient, ConfigStoreClient, HubPoolClient, MultiCallerClient, SpokePoolClient } from "../src/clients"; import { CrossChainTransferClient } from "../src/clients/bridges"; import { Dataworker } from "../src/dataworker/Dataworker"; -import { BalanceType, L1Token } from "../src/interfaces"; -import { ALL_CHAINS_NAME, Monitor, REBALANCE_FINALIZE_GRACE_PERIOD } from "../src/monitor/Monitor"; +import { Monitor, REBALANCE_FINALIZE_GRACE_PERIOD } from "../src/monitor/Monitor"; import { MonitorConfig } from "../src/monitor/MonitorConfig"; -import { MAX_UINT_VAL, getNetworkName, toBN, Address, toAddressType, bnZero, EvmAddress } from "../src/utils"; +import { TokenInfo } from "../src/interfaces"; +import { MAX_UINT_VAL, toBN, toAddressType, EvmAddress, Address, getTokenInfo } from "../src/utils"; + +// Mock Monitor that falls back to hubPoolClient.getTokenInfoForAddress for test tokens +// not found in TOKEN_SYMBOLS_MAP. +class MockMonitor extends Monitor { + protected getTokenInfo(token: Address, chainId: number): TokenInfo { + try { + return getTokenInfo(token, chainId); + } catch { + return this.clients.hubPoolClient.getTokenInfoForAddress(token, chainId); + } + } +} import * as constants from "./constants"; import { amountToDeposit, destinationChainId, mockTreeRoot, originChainId, repaymentChainId } from "./constants"; import { setupDataworker } from "./fixtures/Dataworker.Fixture"; import { MockAdapterManager, SimpleMockHubPoolClient } from "./mocks"; import { - BigNumber, Contract, SignerWithAddress, createSpyLogger, @@ -29,57 +33,7 @@ import { deployMulticall3, } from "./utils"; -type TokenMap = { [l2TokenAddress: string]: L1Token }; - -class TestMonitor extends Monitor { - private overriddenTokenMap: { [chainId: number]: TokenMap } = {}; - private upcomingRefunds: { [l1Token: string]: { [chainId: number]: BigNumber } } = {}; - - setL2ToL1TokenMap(chainId: number, map: TokenMap): void { - this.overriddenTokenMap[chainId] = map; - } - // Override internal function that calls into externally defined and hard-coded TOKEN_SYMBOLS_MAP. - protected getL2ToL1TokenMap(l1Tokens: L1Token[], chainId): TokenMap { - return this.overriddenTokenMap[chainId] ?? super.getL2ToL1TokenMap(l1Tokens, chainId); - } - - getRemoteTokenForL1Token(l1Token: Address, chainId: number | string): Address | undefined { - const targetChain = Number(chainId); - const tokenMapForChain = this.overriddenTokenMap[targetChain]; - if (tokenMapForChain) { - const matchedToken = Object.entries(tokenMapForChain).find(([, l1TokenObject]) => - l1Token.eq(l1TokenObject.address) - ); - if (matchedToken) { - return toAddressType(matchedToken[0], targetChain); - } - } - - for (const [_chainId, tokenMap] of Object.entries(this.overriddenTokenMap)) { - const matchedToken = Object.entries(tokenMap).find(([, l1TokenObject]) => l1Token.eq(l1TokenObject.address)); - if (matchedToken) { - return toAddressType(matchedToken[0], Number(_chainId)); - } - } - - return super.getRemoteTokenForL1Token(l1Token, chainId); - } - - l2TokenAmountToL1TokenAmountConverter(): (BigNumber) => BigNumber { - return (amount: BigNumber) => amount; - } - - setUpcomingRefunds(upcomingRefunds: { [l1Token: string]: { [chainId: number]: BigNumber } }): void { - this.upcomingRefunds = upcomingRefunds; - } - - override getUpcomingRefunds(chainId: number, l1Token: Address): BigNumber { - return this.upcomingRefunds[l1Token.toNative()]?.[chainId] ?? bnZero; - } -} - describe("Monitor", async function () { - const TEST_NETWORK_NAMES = ["Hardhat1", "Hardhat2", "unknown", "HardhatNetwork", ALL_CHAINS_NAME]; let l1Token: Contract, l2Token: Contract, erc20_2: Contract; let hubPool: Contract, spokePool_1: Contract, spokePool_2: Contract; let dataworker: SignerWithAddress, depositor: SignerWithAddress, relayer: SignerWithAddress; @@ -87,14 +41,12 @@ describe("Monitor", async function () { let bundleDataClient: BundleDataClient; let configStoreClient: ConfigStoreClient; let hubPoolClient: HubPoolClient, multiCallerClient: MultiCallerClient; - let tokenTransferClient: TokenTransferClient; let monitorInstance: Monitor; let spokePoolClients: { [chainId: number]: SpokePoolClient }; let crossChainTransferClient: CrossChainTransferClient; let adapterManager: MockAdapterManager; let defaultMonitorEnvVars: Record; let updateAllClients: () => Promise; - let relayerAddress; const { spy, spyLogger } = createSpyLogger(); const executeBundle = async (hubPool: Contract) => { @@ -186,7 +138,6 @@ describe("Monitor", async function () { // Set the config store version to 0 to match the default version in the ConfigStoreClient. process.env.CONFIG_STORE_VERSION = "0"; - relayerAddress = toAddressType(relayer.address, hubPoolClient.chainId); const chainIds = [hubPoolClient.chainId, repaymentChainId, originChainId, destinationChainId]; bundleDataClient = new BundleDataClient( @@ -196,44 +147,17 @@ describe("Monitor", async function () { chainIds ); - const providers = Object.fromEntries( - Object.entries(spokePoolClients).map(([chainId, client]) => [chainId, client.spokePool.provider]) - ); - tokenTransferClient = new TokenTransferClient(spyLogger, providers, [relayerAddress]); - adapterManager = new MockAdapterManager(null, null, null, null); adapterManager.setSupportedChains(chainIds); crossChainTransferClient = new CrossChainTransferClient(spyLogger, chainIds, adapterManager); - monitorInstance = new TestMonitor(spyLogger, monitorConfig, { + monitorInstance = new MockMonitor(spyLogger, monitorConfig, { bundleDataClient, configStoreClient, multiCallerClient, hubPoolClient, spokePoolClients, - tokenTransferClient, crossChainTransferClient, }); - (monitorInstance as TestMonitor).setL2ToL1TokenMap(originChainId, { - [l2Token.address]: { - symbol: "L1Token1", - address: toAddressType(l1Token.address, hubPoolClient.chainId), - decimals: 18, - }, - }); - (monitorInstance as TestMonitor).setL2ToL1TokenMap(destinationChainId, { - [erc20_2.address]: { - symbol: "L1Token1", - address: toAddressType(l1Token.address, hubPoolClient.chainId), - decimals: 18, - }, - }); - (monitorInstance as TestMonitor).setL2ToL1TokenMap(hubPoolClient.chainId, { - [l1Token.address]: { - symbol: "L1Token1", - address: toAddressType(l1Token.address, hubPoolClient.chainId), - decimals: 18, - }, - }); await updateAllClients(); }); @@ -263,95 +187,6 @@ describe("Monitor", async function () { expect(lastSpyLogIncludes(spy, unknownDisputerMessage)).to.be.true; }); - it("Monitor should report balances", async function () { - await monitorInstance.update(); - const reports = monitorInstance.initializeBalanceReports( - monitorInstance.monitorConfig.monitoredRelayers, - monitorInstance.clients.hubPoolClient.getL1Tokens(), - [], // No L2-only tokens in test - TEST_NETWORK_NAMES - ); - await monitorInstance.updateCurrentRelayerBalances(reports); - - // setupDataworker seeds relayer with 10 * 1500 erc20_2, erc20_1, and l1Token_1 tokens on two different - // spoke pools, adding to a total of 6 * 10 * 1500 = 90,000 tokens. - expect(reports[relayerAddress.toNative()]["L1Token1"][ALL_CHAINS_NAME][BalanceType.CURRENT].toString()).to.be.equal( - "90000000000000000000000" - ); - }); - - it("Monitor should get relayer refunds", async function () { - await updateAllClients(); - await monitorInstance.update(); - - // Pending refunds should include the amount to deposit on the destination chain. - (monitorInstance as TestMonitor).setUpcomingRefunds({ - [l1Token.address]: { - [destinationChainId]: amountToDeposit, - [originChainId]: bnZero, - }, - }); - await monitorInstance.update(); - - await monitorInstance.update(); - const reports = monitorInstance.initializeBalanceReports( - monitorInstance.monitorConfig.monitoredRelayers, - monitorInstance.clients.hubPoolClient.getL1Tokens(), - [], // No L2-only tokens in test - TEST_NETWORK_NAMES - ); - await monitorInstance.updateLatestAndFutureRelayerRefunds(reports); - expect(reports[relayerAddress.toNative()]["L1Token1"][ALL_CHAINS_NAME][BalanceType.PENDING]).to.be.equal( - amountToDeposit - ); - - // Simulate some pending cross chain transfers. - crossChainTransferClient.increaseOutstandingTransfer( - relayerAddress, - toAddressType(l1Token.address, hubPoolClient.chainId), - toAddressType(erc20_2.address, destinationChainId), - toBN(5), - destinationChainId - ); - - // Pending rebalance credits default to zero if no rebalancer client is configured. - monitorInstance.clients.rebalancerClient = undefined; - const reportsWithoutRebalanceCredits = monitorInstance.initializeBalanceReports( - monitorInstance.monitorConfig.monitoredRelayers, - monitorInstance.clients.hubPoolClient.getL1Tokens(), - [], // No L2-only tokens in test - TEST_NETWORK_NAMES - ); - await monitorInstance.updateLatestAndFutureRelayerRefunds(reportsWithoutRebalanceCredits); - expect( - reportsWithoutRebalanceCredits[relayer.address]["L1Token1"][getNetworkName(destinationChainId)][ - BalanceType.PENDING_TRANSFERS - ] - ).to.be.equal(toBN(5)); - - // Pending rebalance credits should be merged into "pending transfers". - monitorInstance.clients.rebalancerClient = { - getPendingRebalances: async () => ({ - [destinationChainId]: { - L1Token1: toBN(7), - }, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - const reportsWithRebalanceCredits = monitorInstance.initializeBalanceReports( - monitorInstance.monitorConfig.monitoredRelayers, - monitorInstance.clients.hubPoolClient.getL1Tokens(), - [], // No L2-only tokens in test - TEST_NETWORK_NAMES - ); - await monitorInstance.updateLatestAndFutureRelayerRefunds(reportsWithRebalanceCredits); - expect( - reportsWithRebalanceCredits[relayer.address]["L1Token1"][getNetworkName(destinationChainId)][ - BalanceType.PENDING_TRANSFERS - ] - ).to.be.equal(toBN(12)); - }); - it("Monitor should report stuck rebalances", async function () { await updateAllClients(); await monitorInstance.update(); diff --git a/test/generic-adapters/Arbitrum.ts b/test/generic-adapters/Arbitrum.ts index afaaa07009..8e708cd7e0 100644 --- a/test/generic-adapters/Arbitrum.ts +++ b/test/generic-adapters/Arbitrum.ts @@ -80,7 +80,7 @@ describe("Cross Chain Adapter: Arbitrum", async function () { }, CHAIN_IDs.ARBITRUM, CHAIN_IDs.MAINNET, - [toAddress(monitoredEoa)], + { [l1Token]: [toAddress(monitoredEoa)], [l1UsdcAddress]: [toAddress(monitoredEoa)] }, logger, SUPPORTED_TOKENS[CHAIN_IDs.ARBITRUM], // Supported Tokens. bridges, @@ -130,24 +130,25 @@ describe("Cross Chain Adapter: Arbitrum", async function () { it("get outstanding cross-chain transfers", async () => { // Deposits that do not originate from monitoredEoa should be ignored - await erc20BridgeContract.emitDepositInitiated(l1Token, monitoredEoa, monitoredEoa, 0, 1); - await erc20BridgeContract.emitDepositFinalized(l1Token, monitoredEoa, monitoredEoa, 1); - // Finalized deposits that should not be considered as outstanding - await erc20BridgeContract.emitDepositInitiated(l1Token, monitoredEoa, monitoredEoa, 1, 1); - await erc20BridgeContract.emitDepositFinalized(l1Token, monitoredEoa, monitoredEoa, 1); - // Outstanding deposits - const outstandingDepositEvent = await erc20BridgeContract.emitDepositInitiated( + const firstDepositEvent = await erc20BridgeContract.emitDepositInitiated( l1Token, monitoredEoa, monitoredEoa, - 2, + 0, 1 ); + await erc20BridgeContract.emitDepositFinalized(l1Token, monitoredEoa, monitoredEoa, 1); + // Finalized deposits that should not be considered as outstanding + await erc20BridgeContract.emitDepositInitiated(l1Token, monitoredEoa, monitoredEoa, 1, 1); + await erc20BridgeContract.emitDepositFinalized(l1Token, monitoredEoa, monitoredEoa, 1); + // Outstanding deposits + await erc20BridgeContract.emitDepositInitiated(l1Token, monitoredEoa, monitoredEoa, 2, 1); const outstandingTransfers = await adapter.getOutstandingCrossChainTransfers([toAddress(l1Token)]); const transferObject = outstandingTransfers[monitoredEoa][l1Token][l2Token]; expect(transferObject.totalAmount).to.equal(toBN(1)); - expect(transferObject.depositTxHashes).to.deep.equal([outstandingDepositEvent.hash]); + // Net-amount matching: totalDeposited(3) - totalFinalized(2) = 1, picks first event's hash + expect(transferObject.depositTxHashes).to.deep.equal([firstDepositEvent.hash]); }); }); diff --git a/test/generic-adapters/Linea.ts b/test/generic-adapters/Linea.ts index c900bb556b..7914828bdd 100644 --- a/test/generic-adapters/Linea.ts +++ b/test/generic-adapters/Linea.ts @@ -2,12 +2,22 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; import { EVMSpokePoolClient } from "../../src/clients"; import { LineaBridge, LineaWethBridge, UsdcCCTPBridge } from "../../src/adapter/bridges"; import { BaseChainAdapter } from "../../src/adapter"; -import { ethers, getContractFactory, Contract, randomAddress, expect, createRandomBytes32 } from "../utils"; +import { + ethers, + getContractFactory, + Contract, + randomAddress, + expect, + createRandomBytes32, + createSpyLogger, +} from "../utils"; import { utils } from "@across-protocol/sdk"; import { CONTRACT_ADDRESSES, SUPPORTED_TOKENS } from "../../src/common"; import { EVMBlockFinder, toBN, EvmAddress } from "../../src/utils/SDKUtils"; import { getCctpDomainForChainId, ZERO_ADDRESS } from "../../src/utils"; +const logger = createSpyLogger().spyLogger; + describe("Cross Chain Adapter: Linea", async function () { let adapter: BaseChainAdapter; let monitoredEoa: string; @@ -84,8 +94,12 @@ describe("Cross Chain Adapter: Linea", async function () { }, // Don't need spoke pool clients for this test l2ChainId, hubChainId, - [toAddress(monitoredEoa)], - null, + { + [l1WETHToken]: [toAddress(monitoredEoa)], + [l1USDCToken]: [toAddress(monitoredEoa)], + [l1Token]: [toAddress(monitoredEoa)], + }, + logger, SUPPORTED_TOKENS[l2ChainId], bridges, 1.5 @@ -159,24 +173,24 @@ describe("Cross Chain Adapter: Linea", async function () { it("Matches L1 and L2 events", async function () { const messageHash = createRandomBytes32(); const otherMessageHash = createRandomBytes32(); - await wethBridgeContract.emitMessageSentWithMessageHash(randomAddress(), monitoredEoa, 1, messageHash); - const unfinalizedTx = await wethBridgeContract.emitMessageSentWithMessageHash( + const firstTx = await wethBridgeContract.emitMessageSentWithMessageHash( randomAddress(), monitoredEoa, 1, - otherMessageHash + messageHash ); + await wethBridgeContract.emitMessageSentWithMessageHash(randomAddress(), monitoredEoa, 1, otherMessageHash); await wethBridgeContract.emitMessageClaimed(messageHash); await adapter.updateSpokePoolClients(); adapter.bridges[l1WETHToken].blockFinder = new EVMBlockFinder(wethBridgeContract.provider); const result = await adapter.getOutstandingCrossChainTransfers([toAddress(l1WETHToken)]); // There should be one outstanding transfer, since there are two deposit events and one - // finalization event + // finalization event. Net-amount matching picks the first deposit event's hash. expect(Object.keys(result).length).to.equal(1); expect(Object.keys(result[monitoredEoa]).length).to.equal(1); expect(Object.keys(result[monitoredEoa][l1WETHToken])[0]).to.equal(l2WETHToken); - expect(result[monitoredEoa][l1WETHToken][l2WETHToken].depositTxHashes[0]).to.equal(unfinalizedTx.hash); + expect(result[monitoredEoa][l1WETHToken][l2WETHToken].depositTxHashes[0]).to.equal(firstTx.hash); }); }); describe("CCTP", () => { @@ -237,19 +251,19 @@ describe("Cross Chain Adapter: Linea", async function () { expect(Object.keys(result).length).to.equal(1); }); it("Matches L1 and L2 events", async function () { + const firstTx = await erc20BridgeContract.emitBridgingInitiated(randomAddress(), monitoredEoa, l1Token, 1); await erc20BridgeContract.emitBridgingInitiated(randomAddress(), monitoredEoa, l1Token, 1); - const unfinalizedTx = await erc20BridgeContract.emitBridgingInitiated(randomAddress(), monitoredEoa, l1Token, 1); await erc20BridgeContract.emitBridgingFinalized(l1Token, monitoredEoa, 1); await adapter.updateSpokePoolClients(); const result = await adapter.getOutstandingCrossChainTransfers([toAddress(l1Token)]); // There should be one outstanding transfer, since there are two deposit events and one - // finalization event + // finalization event. Net-amount matching picks the first deposit event's hash. expect(Object.keys(result).length).to.equal(1); expect(Object.keys(result[monitoredEoa]).length).to.equal(1); expect(Object.keys(result[monitoredEoa][l1Token])[0]).to.equal(l2Token); - expect(result[monitoredEoa][l1Token][l2Token].depositTxHashes[0]).to.equal(unfinalizedTx.hash); + expect(result[monitoredEoa][l1Token][l2Token].depositTxHashes[0]).to.equal(firstTx.hash); }); }); diff --git a/test/generic-adapters/OpStack.ts b/test/generic-adapters/OpStack.ts index ac4b36b9f0..1a859d31d4 100644 --- a/test/generic-adapters/OpStack.ts +++ b/test/generic-adapters/OpStack.ts @@ -122,7 +122,13 @@ describe("Cross Chain Adapter: OP Stack", async function () { }, CHAIN_IDs.OPTIMISM, CHAIN_IDs.MAINNET, - [toAddress(monitoredEoa)], + { + [l1WethAddress]: [toAddress(monitoredEoa)], + [l1SnxAddress]: [toAddress(monitoredEoa)], + [l1DaiAddress]: [toAddress(monitoredEoa)], + [l1Erc20Address]: [toAddress(monitoredEoa)], + [l1UsdcAddress]: [toAddress(monitoredEoa)], + }, logger, ["WETH", "SNX", "DAI", "WBTC", "USDC"], bridges, diff --git a/test/generic-adapters/Polygon.ts b/test/generic-adapters/Polygon.ts index c29fc67b4d..823f092690 100644 --- a/test/generic-adapters/Polygon.ts +++ b/test/generic-adapters/Polygon.ts @@ -102,7 +102,11 @@ describe("Cross Chain Adapter: Polygon", async function () { }, POLYGON, MAINNET, - [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + { + [l1Weth]: [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + [l1Usdc]: [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + [l1Token]: [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + }, logger, ["WETH", "USDC", "WBTC"], bridges, diff --git a/test/generic-adapters/Scroll.ts b/test/generic-adapters/Scroll.ts index 4dfe63c2e1..f941e89d4e 100644 --- a/test/generic-adapters/Scroll.ts +++ b/test/generic-adapters/Scroll.ts @@ -3,10 +3,12 @@ import { EVMSpokePoolClient } from "../../src/clients"; import { ZERO_ADDRESS } from "../../src/utils"; import { ScrollERC20Bridge } from "../../src/adapter/bridges"; import { BaseChainAdapter } from "../../src/adapter"; -import { ethers, getContractFactory, Contract, randomAddress, expect } from "../utils"; +import { ethers, getContractFactory, Contract, randomAddress, expect, createSpyLogger } from "../utils"; import { utils } from "@across-protocol/sdk"; import { CONTRACT_ADDRESSES } from "../../src/common"; +const logger = createSpyLogger().spyLogger; + describe("Cross Chain Adapter: Scroll", async function () { let adapter: BaseChainAdapter; let monitoredEoa: string; @@ -77,8 +79,11 @@ describe("Cross Chain Adapter: Scroll", async function () { }, // Don't need spoke pool clients for this test l2ChainId, hubChainId, - [toAddress(monitoredEoa), toAddress(l2SpokePoolClient.spokePool.address)], - null, + { + [l1Weth]: [toAddress(monitoredEoa), toAddress(l2SpokePoolClient.spokePool.address)], + [l1Usdc]: [toAddress(monitoredEoa), toAddress(l2SpokePoolClient.spokePool.address)], + }, + logger, ["WETH", "USDC"], bridges, 1.5 @@ -135,38 +140,36 @@ describe("Cross Chain Adapter: Scroll", async function () { expect(result[l2Weth][0].amount).to.equal(1); }); it("Matches L1 and L2 events", async function () { + const firstTx = await scrollBridgeContract.deposit(l1Weth, l2Weth, monitoredEoa, monitoredEoa, 1); await scrollBridgeContract.deposit(l1Weth, l2Weth, monitoredEoa, monitoredEoa, 1); - const pendingTx = scrollBridgeContract.deposit(l1Weth, l2Weth, monitoredEoa, monitoredEoa, 1); // Only one of the deposits were finalized. await scrollBridgeContract.finalize(l1Weth, l2Weth, monitoredEoa, monitoredEoa, 1); await adapter.updateSpokePoolClients(); - const unfinalizedTx = await pendingTx; const result = await adapter.getOutstandingCrossChainTransfers([toAddress(l1Weth)]); // There should be one outstanding transfer, since there are two deposit events and one - // finalization event. + // finalization event. Net-amount matching picks the first deposit event's hash. // Only one key since the other monitored address has no outstanding transfers. expect(Object.keys(result).length).to.equal(1); expect(Object.keys(result[monitoredEoa]).length).to.equal(1); expect(Object.keys(result[monitoredEoa][l1Weth])[0]).to.equal(l2Weth); - expect(result[monitoredEoa][l1Weth][l2Weth].depositTxHashes[0]).to.equal(unfinalizedTx.hash); + expect(result[monitoredEoa][l1Weth][l2Weth].depositTxHashes[0]).to.equal(firstTx.hash); }); it("Matches L1 and L2 events, Hub -> Spoke", async function () { + const firstTx = await scrollBridgeContract.deposit(l1Weth, l2Weth, hubPoolAddress, spokeAddress, 1); await scrollBridgeContract.deposit(l1Weth, l2Weth, hubPoolAddress, spokeAddress, 1); - const pendingTx = scrollBridgeContract.deposit(l1Weth, l2Weth, hubPoolAddress, spokeAddress, 1); // Only one of the deposits were finalized. await scrollBridgeContract.finalize(l1Weth, l2Weth, hubPoolAddress, spokeAddress, 1); await adapter.updateSpokePoolClients(); - const unfinalizedTx = await pendingTx; const result = await adapter.getOutstandingCrossChainTransfers([toAddress(l1Weth)]); // There should be one outstanding transfer, since there are two deposit events and one - // finalization event + // finalization event. Net-amount matching picks the first deposit event's hash. // Only one key since the other monitored address has no outstanding transfers. expect(Object.keys(result).length).to.equal(1); expect(Object.keys(result[spokeAddress]).length).to.equal(1); expect(Object.keys(result[spokeAddress][l1Weth])[0]).to.equal(l2Weth); - expect(result[spokeAddress][l1Weth][l2Weth].depositTxHashes[0]).to.equal(unfinalizedTx.hash); + expect(result[spokeAddress][l1Weth][l2Weth].depositTxHashes[0]).to.equal(firstTx.hash); }); }); describe("USDC", function () { @@ -201,38 +204,36 @@ describe("Cross Chain Adapter: Scroll", async function () { expect(result[l2Usdc][0].amount).to.equal(bnOne); }); it("Matches L1 and L2 events", async function () { + const firstTx = await scrollBridgeContract.deposit(l1Usdc, l2Usdc, monitoredEoa, monitoredEoa, 1); await scrollBridgeContract.deposit(l1Usdc, l2Usdc, monitoredEoa, monitoredEoa, 1); - const pendingTx = scrollBridgeContract.deposit(l1Usdc, l2Usdc, monitoredEoa, monitoredEoa, 1); await scrollBridgeContract.finalize(l1Usdc, l2Usdc, monitoredEoa, monitoredEoa, 1); await adapter.updateSpokePoolClients(); - const unfinalizedTx = await pendingTx; const result = await adapter.getOutstandingCrossChainTransfers([toAddress(l1Usdc)]); // There should be one outstanding transfer, since there are two deposit events and one - // finalization event + // finalization event. Net-amount matching picks the first deposit event's hash. // Only one key since the other monitored address has no outstanding transfers. expect(Object.keys(result).length).to.equal(1); expect(Object.keys(result[monitoredEoa]).length).to.equal(1); expect(Object.keys(result[monitoredEoa][l1Usdc])[0]).to.equal(l2Usdc); - expect(result[monitoredEoa][l1Usdc][l2Usdc].depositTxHashes[0]).to.equal(unfinalizedTx.hash); + expect(result[monitoredEoa][l1Usdc][l2Usdc].depositTxHashes[0]).to.equal(firstTx.hash); }); it("Matches L1 and L2 events, Hub -> Spoke", async function () { + const firstTx = await scrollBridgeContract.deposit(l1Usdc, l2Usdc, hubPoolAddress, spokeAddress, 1); await scrollBridgeContract.deposit(l1Usdc, l2Usdc, hubPoolAddress, spokeAddress, 1); - const pendingTx = scrollBridgeContract.deposit(l1Usdc, l2Usdc, hubPoolAddress, spokeAddress, 1); // Only one of the deposits were finalized. await scrollBridgeContract.finalize(l1Usdc, l2Usdc, hubPoolAddress, spokeAddress, 1); await adapter.updateSpokePoolClients(); - const unfinalizedTx = await pendingTx; const result = await adapter.getOutstandingCrossChainTransfers([toAddress(l1Usdc)]); // There should be one outstanding transfer, since there are two deposit events and one - // finalization event + // finalization event. Net-amount matching picks the first deposit event's hash. // Only one key since the other monitored address has no outstanding transfers. expect(Object.keys(result).length).to.equal(1); expect(Object.keys(result[spokeAddress]).length).to.equal(1); expect(Object.keys(result[spokeAddress][l1Usdc])[0]).to.equal(l2Usdc); - expect(result[spokeAddress][l1Usdc][l2Usdc].depositTxHashes[0]).to.equal(unfinalizedTx.hash); + expect(result[spokeAddress][l1Usdc][l2Usdc].depositTxHashes[0]).to.equal(firstTx.hash); }); }); }); diff --git a/test/generic-adapters/SplitBridgeTracking.ts b/test/generic-adapters/SplitBridgeTracking.ts index 06b0108b9c..54afa89aba 100644 --- a/test/generic-adapters/SplitBridgeTracking.ts +++ b/test/generic-adapters/SplitBridgeTracking.ts @@ -48,17 +48,20 @@ describe("BaseChainAdapter split bridge tracking", function () { expect(outstandingTransfers[MONITORED_ADDRESS][l1Token][l2Token].depositTxHashes).to.deep.equal(["tracked"]); }); - it("does not let finalized events for ignored initiations reduce tracked outstanding amounts", async function () { + it("finalized events reduce tracked outstanding amounts via net-amount matching", async function () { const trackedAmount = toBNWei("2", 6); + const finalizedAmount = toBNWei("1", 6); const outstandingTransfers = await getOutstandingTransfersForTrackedBridge( "oft", "USDT", - [makeBridgeEvent(toBNWei("1", 6), "ignored"), makeBridgeEvent(trackedAmount, "tracked")], - [makeBridgeEvent(toBNWei("1", 6), "ignored-finalized")] + [makeBridgeEvent(finalizedAmount, "ignored"), makeBridgeEvent(trackedAmount, "tracked")], + [makeBridgeEvent(finalizedAmount, "ignored-finalized")] ); const l1Token = TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.MAINNET]; const l2Token = TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.ARBITRUM]; - expect(outstandingTransfers[MONITORED_ADDRESS][l1Token][l2Token].totalAmount).to.equal(trackedAmount); + // Net-amount matching: totalDeposited (only "tracked" = 2000000) - totalFinalized (1000000) = 1000000 + const expectedOutstanding = trackedAmount.sub(finalizedAmount); + expect(outstandingTransfers[MONITORED_ADDRESS][l1Token][l2Token].totalAmount).to.equal(expectedOutstanding); expect(outstandingTransfers[MONITORED_ADDRESS][l1Token][l2Token].depositTxHashes).to.deep.equal(["tracked"]); }); }); @@ -94,7 +97,7 @@ async function getOutstandingTransfersForTrackedBridge( } as unknown as { [chainId: number]: SpokePoolClient }, l2ChainId, CHAIN_IDs.MAINNET, - [EvmAddress.from(MONITORED_ADDRESS)], + { [l1Token.toNative()]: [EvmAddress.from(MONITORED_ADDRESS)] }, TEST_LOGGER, [tokenSymbol], { [l1Token.toNative()]: bridge }, diff --git a/test/generic-adapters/zkSync.ts b/test/generic-adapters/zkSync.ts index 795ba08106..1fda50f4e6 100644 --- a/test/generic-adapters/zkSync.ts +++ b/test/generic-adapters/zkSync.ts @@ -20,6 +20,7 @@ import * as zksync from "zksync-ethers"; const { LENS, MAINNET, ZK_SYNC } = CHAIN_IDs; const { DAI, USDC, WETH } = TOKEN_SYMBOLS_MAP; const l1Weth = WETH.addresses[MAINNET]; +const l2WethAddress = WETH.addresses[ZK_SYNC]; let l1Bridge: Contract, l2Bridge: Contract; let l2Eth: Contract, l2Weth: Contract; @@ -71,7 +72,7 @@ class TestZkSyncWethBridge extends ZKStackWethBridge { } override resolveL2TokenAddress(l1Token: EvmAddress) { - return l1Token.toNative() === l1Weth ? l2Weth.address : super.resolveL2TokenAddress(l1Token); + return l1Token.toNative() === l1Weth ? WETH.addresses[ZK_SYNC] : super.resolveL2TokenAddress(l1Token); } public setHubPool(hubPool: Contract) { @@ -158,7 +159,10 @@ describe("Cross Chain Adapter: zkSync", async function () { }, ZK_SYNC, MAINNET, - [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + { + [WETH.addresses[MAINNET]]: [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + [DAI.addresses[MAINNET]]: [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + }, logger, ["DAI", "WETH"], bridges, @@ -196,7 +200,7 @@ describe("Cross Chain Adapter: zkSync", async function () { describe("WETH bridge", function () { it("Get L1 deposits: EOA", async function () { - // await adapter.sendTokenToTargetChain(monitoredEoa, WETH.addresses[MAINNET], l2Weth.address, depositAmount, false); + // await adapter.sendTokenToTargetChain(monitoredEoa, WETH.addresses[MAINNET], l2WethAddress, depositAmount, false); await atomicDepositor.bridgeWeth(ZK_SYNC, depositAmount, depositAmount, bnZero, "0x"); const result = await adapter.bridges[l1Weth].queryL1BridgeInitiationEvents( @@ -208,7 +212,7 @@ describe("Cross Chain Adapter: zkSync", async function () { expect(result).to.exist; expect(Object.keys(result).length).to.equal(1); - const deposit = result[l2Weth.address]; + const deposit = result[l2WethAddress]; expect(deposit).to.exist; const { amount } = deposit[0]; expect(amount).to.equal(amount); @@ -227,7 +231,7 @@ describe("Cross Chain Adapter: zkSync", async function () { ); expect(Object.keys(result).length).to.equal(1); - const receipt = result[l2Weth.address]; + const receipt = result[l2WethAddress]; expect(receipt).to.exist; const { amount } = receipt[0]; expect(amount).to.equal(amount); @@ -252,7 +256,7 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(deposits).to.exist; - expect(deposits[l2Weth.address].length).to.equal(1); + expect(deposits[l2WethAddress].length).to.equal(1); let receipts = await adapter.bridges[l1Weth].queryL2BridgeFinalizationEvents( toAddress(l1Weth), @@ -261,7 +265,7 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(receipts).to.exist; - expect(receipts[l2Weth.address].length).to.equal(0); + expect(receipts[l2WethAddress].length).to.equal(0); // There should be 1 outstanding transfer. await Promise.all( @@ -271,9 +275,9 @@ describe("Cross Chain Adapter: zkSync", async function () { expect(transfers).to.deep.equal({ [monitoredEoa]: { [l1Weth]: { - [l2Weth.address]: { - depositTxHashes: [deposits[l2Weth.address][0].txnRef], - totalAmount: deposits[l2Weth.address][0].amount, + [l2WethAddress]: { + depositTxHashes: [deposits[l2WethAddress][0].txnRef], + totalAmount: deposits[l2WethAddress][0].amount, }, }, }, @@ -289,7 +293,7 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(receipts).to.exist; - expect(receipts[l2Weth.address].length).to.equal(1); + expect(receipts[l2WethAddress].length).to.equal(1); // There should be no outstanding transfers. await Promise.all( @@ -300,7 +304,7 @@ describe("Cross Chain Adapter: zkSync", async function () { }); it("Get L1 deposits: HubPool", async function () { - await hubPool.relayTokens(l1Weth, l2Weth.address, depositAmount, spokePool.address); + await hubPool.relayTokens(l1Weth, l2WethAddress, depositAmount, spokePool.address); const result = await adapter.bridges[l1Weth].queryL1BridgeInitiationEvents( toAddress(l1Weth), @@ -309,9 +313,9 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(result).to.exist; - expect(result[l2Weth.address].length).to.equal(1); + expect(result[l2WethAddress].length).to.equal(1); - const deposit = result[l2Weth.address]; + const deposit = result[l2WethAddress]; expect(deposit[0]).to.exist; const { amount } = deposit[0]; expect(amount).to.equal(depositAmount); @@ -327,9 +331,9 @@ describe("Cross Chain Adapter: zkSync", async function () { toAddress(spokePool.address), searchConfig ); - expect(result[l2Weth.address].length).to.equal(1); + expect(result[l2WethAddress].length).to.equal(1); - const receipt = result[l2Weth.address]; + const receipt = result[l2WethAddress]; expect(receipt).to.exist; const { amount } = receipt[0]; expect(amount).to.equal(depositAmount); @@ -344,7 +348,7 @@ describe("Cross Chain Adapter: zkSync", async function () { expect(transfers).to.deep.equal({}); // Make a single l1 -> l2 deposit. - await hubPool.relayTokens(l1Weth, l2Weth.address, depositAmount, spokePool.address); + await hubPool.relayTokens(l1Weth, l2WethAddress, depositAmount, spokePool.address); const deposits = await adapter.bridges[l1Weth].queryL1BridgeInitiationEvents( toAddress(l1Weth), toAddress(spokePool.address), @@ -352,7 +356,7 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(deposits).to.exist; - expect(deposits[l2Weth.address].length).to.equal(1); + expect(deposits[l2WethAddress].length).to.equal(1); let receipts = await adapter.bridges[l1Weth].queryL2BridgeFinalizationEvents( toAddress(l1Weth), @@ -361,7 +365,7 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(receipts).to.exist; - expect(receipts[l2Weth.address].length).to.equal(0); + expect(receipts[l2WethAddress].length).to.equal(0); // There should be 1 outstanding transfer. await Promise.all( @@ -371,9 +375,9 @@ describe("Cross Chain Adapter: zkSync", async function () { expect(transfers).to.deep.equal({ [spokePool.address]: { [l1Weth]: { - [l2Weth.address]: { - depositTxHashes: [deposits[l2Weth.address][0].txnRef], - totalAmount: deposits[l2Weth.address][0].amount, + [l2WethAddress]: { + depositTxHashes: [deposits[l2WethAddress][0].txnRef], + totalAmount: deposits[l2WethAddress][0].amount, }, }, }, @@ -388,7 +392,7 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(receipts).to.exist; - expect(receipts[l2Weth.address].length).to.equal(1); + expect(receipts[l2WethAddress].length).to.equal(1); // There should be no outstanding transfers. await Promise.all( @@ -420,7 +424,7 @@ describe("Cross Chain Adapter: zkSync", async function () { searchConfig ); expect(deposits).to.exist; - expect(deposits[l2Weth.address].length).to.equal(1); + expect(deposits[l2WethAddress].length).to.equal(1); // There should be 1 outstanding transfer. await Promise.all( @@ -430,9 +434,9 @@ describe("Cross Chain Adapter: zkSync", async function () { expect(transfers).to.deep.equal({ [monitoredEoa]: { [l1Weth]: { - [l2Weth.address]: { - depositTxHashes: [deposits[l2Weth.address][0].txnRef], - totalAmount: deposits[l2Weth.address][0].amount, + [l2WethAddress]: { + depositTxHashes: [deposits[l2WethAddress][0].txnRef], + totalAmount: deposits[l2WethAddress][0].amount, }, }, }, @@ -810,7 +814,13 @@ describe("Cross Chain Adapter: zkSync", async function () { }, LENS, MAINNET, - [toAddress(monitoredEoa), toAddress(hubPool.address), toAddress(spokePool.address)], + { + [USDC.addresses[MAINNET]]: [ + toAddress(monitoredEoa), + toAddress(hubPool.address), + toAddress(spokePool.address), + ], + }, logger, ["USDC"], { [USDC.addresses[MAINNET]]: new TestZkSyncUSDCBridge(LENS, MAINNET, l1Signer, l2Signer, undefined) },