From 1a171d393eeb429b3f490b6b57a41e583cfe83d4 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 9 May 2024 21:41:35 +0200 Subject: [PATCH] refactor(CrossChainTransferClient): Support unique L2 tokens (#1469) This change applies a bunch of updates to enable cross chain transfers to distinguish between multiple remote tokens mapping to a single mainnet token. Done in collaboration w/ James. --------- Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> --- package.json | 2 +- src/clients/InventoryClient.ts | 34 +++++--- src/clients/bridges/AdapterManager.ts | 24 +++--- src/clients/bridges/ArbitrumAdapter.ts | 7 +- src/clients/bridges/BaseAdapter.ts | 76 +++++++++-------- src/clients/bridges/CCTPAdapter.ts | 9 -- .../bridges/CrossChainTransferClient.ts | 84 ++++++++++++------- src/clients/bridges/LineaAdapter.ts | 13 +-- src/clients/bridges/PolygonAdapter.ts | 9 +- src/clients/bridges/ZKSyncAdapter.ts | 11 ++- src/clients/bridges/index.ts | 2 + .../bridges/op-stack/DefaultErc20Bridge.ts | 48 ++++++----- .../bridges/op-stack/OpStackAdapter.ts | 17 ++-- .../op-stack/OpStackBridgeInterface.ts | 41 +++++++-- .../bridges/op-stack/UsdcCCTPBridge.ts | 52 ++++++------ .../op-stack/UsdcTokenSplitterBridge.ts | 50 +++++++---- src/clients/bridges/op-stack/WethBridge.ts | 59 +++++++------ .../op-stack/optimism/DaiOptimismBridge.ts | 56 ++++++------- .../op-stack/optimism/SnxOptimismBridge.ts | 54 ++++++------ src/interfaces/index.ts | 12 ++- src/utils/AddressUtils.ts | 30 ++++++- ...utstandingCrossChainTokenTransferAmount.ts | 6 +- test/Monitor.ts | 4 +- test/cross-chain-adapters/Linea.ts | 27 +++--- test/cross-chain-adapters/OpStack.ts | 48 ++++++----- test/mocks/MockAdapterManager.ts | 29 +++++-- yarn.lock | 8 +- 27 files changed, 489 insertions(+), 323 deletions(-) diff --git a/package.json b/package.json index 59dfe4de7..9cffe3d5b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "node": ">=16.18.0" }, "dependencies": { - "@across-protocol/constants-v2": "1.0.22", + "@across-protocol/constants-v2": "1.0.24", "@across-protocol/contracts-v2": "2.5.6", "@across-protocol/sdk-v2": "0.23.10", "@arbitrum/sdk": "^3.1.3", diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 2777a58fb..174bacad4 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -171,9 +171,15 @@ export class InventoryClient { } // Decrement Tokens Balance And Increment Cross Chain Transfer - trackCrossChainTransfer(l1Token: string, rebalance: BigNumber, chainId: number | string): void { + trackCrossChainTransfer(l1Token: string, l2Token: string, rebalance: BigNumber, chainId: number | string): void { this.tokenClient.decrementLocalBalance(this.hubPoolClient.chainId, l1Token, rebalance); - this.crossChainTransferClient.increaseOutstandingTransfer(this.relayer, l1Token, rebalance, Number(chainId)); + this.crossChainTransferClient.increaseOutstandingTransfer( + this.relayer, + l1Token, + l2Token, + rebalance, + Number(chainId) + ); } async getAllBundleRefunds(): Promise { @@ -677,7 +683,7 @@ export class InventoryClient { // Next, evaluate if we have enough tokens on L1 to actually do these rebalances. for (const rebalance of rebalancesRequired) { - const { balance, amount, l1Token, chainId } = rebalance; + const { balance, amount, l1Token, l2Token, chainId } = rebalance; // This is the balance left after any assumed rebalances from earlier loop iterations. const unallocatedBalance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token); @@ -711,7 +717,7 @@ export class InventoryClient { }); possibleRebalances.push(rebalance); // Decrement token balance in client for this chain and increment cross chain counter. - this.trackCrossChainTransfer(l1Token, amount, chainId); + this.trackCrossChainTransfer(l1Token, l2Token, amount, chainId); } } else { // Extract unexecutable rebalances for logging. @@ -729,8 +735,8 @@ export class InventoryClient { // sends each transaction one after the other with incrementing nonce. this will be left for a follow on PR as this // is already complex logic and most of the time we'll not be sending batches of rebalance transactions. for (const rebalance of possibleRebalances) { - const { chainId, l1Token, amount } = rebalance; - const { hash } = await this.sendTokenCrossChain(chainId, l1Token, amount, this.simMode); + const { chainId, l1Token, l2Token, amount } = rebalance; + const { hash } = await this.sendTokenCrossChain(chainId, l1Token, amount, this.simMode, l2Token); executedTransactions.push({ ...rebalance, hash }); } @@ -783,7 +789,7 @@ export class InventoryClient { " This chain's pending L1->L2 transfer amount is " + `${formatter( this.crossChainTransferClient - .getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token) + .getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token, l2Token) .toString() )}.\n`; } @@ -957,7 +963,8 @@ export class InventoryClient { const transfers = this.crossChainTransferClient.getOutstandingCrossChainTransferAmount( this.relayer, chainId, - l1Token + l1Token, + l2Token ); const actualBalanceOnChain = this.tokenClient.getBalance(chainId, l2Token); @@ -979,20 +986,21 @@ export class InventoryClient { }); } - async sendTokenCrossChain( + sendTokenCrossChain( chainId: number | string, l1Token: string, amount: BigNumber, - simMode = false + simMode = false, + l2Token?: string ): Promise { - return await this.adapterManager.sendTokenCrossChain(this.relayer, Number(chainId), l1Token, amount, simMode); + return this.adapterManager.sendTokenCrossChain(this.relayer, Number(chainId), l1Token, amount, simMode, l2Token); } - async _unwrapWeth(chainId: number, _l2Weth: string, amount: BigNumber): Promise { + _unwrapWeth(chainId: number, _l2Weth: string, amount: BigNumber): Promise { const l2Signer = this.tokenClient.spokePoolClients[chainId].spokePool.signer; const l2Weth = new Contract(_l2Weth, CONTRACT_ADDRESSES[1].weth.abi, l2Signer); this.log("Unwrapping WETH", { amount: amount.toString() }); - return await runTransaction(this.logger, l2Weth, "withdraw", [amount]); + return runTransaction(this.logger, l2Weth, "withdraw", [amount]); } async setL1TokenApprovals(): Promise { diff --git a/src/clients/bridges/AdapterManager.ts b/src/clients/bridges/AdapterManager.ts index c1d3029f8..cfd74a4bf 100644 --- a/src/clients/bridges/AdapterManager.ts +++ b/src/clients/bridges/AdapterManager.ts @@ -1,12 +1,19 @@ import { BigNumber, isDefined, winston, Signer, getL2TokenAddresses, TransactionResponse, assert } from "../../utils"; import { SpokePoolClient, HubPoolClient } from "../"; -import { OptimismAdapter, ArbitrumAdapter, PolygonAdapter, BaseAdapter, ZKSyncAdapter } from "./"; +import { + OptimismAdapter, + ArbitrumAdapter, + PolygonAdapter, + BaseAdapter, + ZKSyncAdapter, + BaseChainAdapter, + LineaAdapter, +} from "./"; import { InventoryConfig, OutstandingTransfers } from "../../interfaces"; import { utils } from "@across-protocol/sdk-v2"; import { CHAIN_IDs } from "@across-protocol/constants-v2"; -import { BaseChainAdapter } from "./op-stack/base/BaseChainAdapter"; import { spokesThatHoldEthAndWeth } from "../../common/Constants"; -import { LineaAdapter } from "./LineaAdapter"; + export class AdapterManager { public adapters: { [chainId: number]: BaseAdapter } = {}; @@ -71,10 +78,7 @@ export class AdapterManager { return Object.keys(this.adapters).map((chainId) => Number(chainId)); } - async getOutstandingCrossChainTokenTransferAmount( - chainId: number, - l1Tokens: string[] - ): Promise { + getOutstandingCrossChainTokenTransferAmount(chainId: number, l1Tokens: string[]): Promise { const adapter = this.adapters[chainId]; // @dev The adapter should filter out tokens that are not supported by the adapter, but we do it here as well. const adapterSupportedL1Tokens = l1Tokens.filter((token) => @@ -86,10 +90,10 @@ export class AdapterManager { adapterSupportedL1Tokens, searchConfigs: adapter.getUpdatedSearchConfigs(), }); - return await this.adapters[chainId].getOutstandingCrossChainTransfers(adapterSupportedL1Tokens); + return this.adapters[chainId].getOutstandingCrossChainTransfers(adapterSupportedL1Tokens); } - async sendTokenCrossChain( + sendTokenCrossChain( address: string, chainId: number | string, l1Token: string, @@ -100,7 +104,7 @@ export class AdapterManager { chainId = Number(chainId); // Ensure chainId is a number before using. this.logger.debug({ at: "AdapterManager", message: "Sending token cross-chain", chainId, l1Token, amount }); l2Token ??= this.l2TokenForL1Token(l1Token, Number(chainId)); - return await this.adapters[chainId].sendTokenToTargetChain(address, l1Token, l2Token, amount, simMode); + return this.adapters[chainId].sendTokenToTargetChain(address, l1Token, l2Token, amount, simMode); } // Check how much ETH is on the target chain and if it is above the threshold the wrap it to WETH. Note that this only diff --git a/src/clients/bridges/ArbitrumAdapter.ts b/src/clients/bridges/ArbitrumAdapter.ts index 887a1c774..d5fdac167 100644 --- a/src/clients/bridges/ArbitrumAdapter.ts +++ b/src/clients/bridges/ArbitrumAdapter.ts @@ -161,12 +161,15 @@ export class ArbitrumAdapter extends CCTPAdapter { }; }); const eventsStorage = index % 2 === 0 ? this.l1DepositInitiatedEvents : this.l2DepositFinalizedEvents; - assign(eventsStorage, [monitoredAddress, l1Token], events); + const l2Token = this.resolveL2TokenAddress(l1Token, false); // This codepath will never have native USDC - therefore we should pass `false`. + assign(eventsStorage, [monitoredAddress, l1Token, l2Token], events); }); if (isDefined(resultingCCTPEvents[monitoredAddress])) { + const usdcL1Token = TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]; + const usdcL2Token = this.resolveL2TokenAddress(usdcL1Token, true); // Must specifically be native USDC assign( this.l1DepositInitiatedEvents, - [monitoredAddress, TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]], + [monitoredAddress, usdcL1Token, usdcL2Token], resultingCCTPEvents[monitoredAddress] ); } diff --git a/src/clients/bridges/BaseAdapter.ts b/src/clients/bridges/BaseAdapter.ts index 3f8d91281..05ce6ed53 100644 --- a/src/clients/bridges/BaseAdapter.ts +++ b/src/clients/bridges/BaseAdapter.ts @@ -28,6 +28,7 @@ import { BigNumberish, TOKEN_SYMBOLS_MAP, getRedisCache, + getTokenAddressWithCCTP, } from "../../utils"; import { utils } from "@across-protocol/sdk-v2"; @@ -40,7 +41,7 @@ export interface DepositEvent extends SortableEvent { interface Events { [address: string]: { - [l1Token: string]: DepositEvent[]; + [l1Token: string]: { [l2Token: string]: DepositEvent[] }; }; } @@ -117,6 +118,10 @@ export abstract class BaseAdapter { ).getAddress()}_targetContract:${targetContract}`; } + resolveL2TokenAddress(l1Token: string, isNativeUsdc = false): string { + return getTokenAddressWithCCTP(l1Token, this.hubChainId, this.chainId, isNativeUsdc); + } + async checkAndSendTokenApprovals(address: string, l1Tokens: string[], associatedL1Bridges: string[]): Promise { this.log("Checking and sending token approvals", { l1Tokens, associatedL1Bridges }); @@ -202,12 +207,9 @@ export abstract class BaseAdapter { continue; } - if (outstandingTransfers[monitoredAddress] === undefined) { - outstandingTransfers[monitoredAddress] = {}; - } - if (this.l2DepositFinalizedEvents[monitoredAddress] === undefined) { - this.l2DepositFinalizedEvents[monitoredAddress] = {}; - } + outstandingTransfers[monitoredAddress] ??= {}; + + this.l2DepositFinalizedEvents[monitoredAddress] ??= {}; for (const l1Token of l1Tokens) { // Skip if there has been no deposits for this token. @@ -216,35 +218,43 @@ export abstract class BaseAdapter { } // It's okay to not have any finalization events. In that case, all deposits are outstanding. - if (this.l2DepositFinalizedEvents[monitoredAddress][l1Token] === undefined) { - this.l2DepositFinalizedEvents[monitoredAddress][l1Token] = []; - } - const l2FinalizationSet = this.l2DepositFinalizedEvents[monitoredAddress][l1Token]; - - // Match deposits and finalizations by amount. We're only doing a limited lookback of events so collisions - // should be unlikely. - const finalizedAmounts = l2FinalizationSet.map((finalization) => finalization.amount.toString()); - const pendingDeposits = this.l1DepositInitiatedEvents[monitoredAddress][l1Token].filter((deposit) => { - // Remove the first match. This handles scenarios where are collisions by amount. - const index = finalizedAmounts.indexOf(deposit.amount.toString()); - if (index > -1) { - finalizedAmounts.splice(index, 1); - return false; + this.l2DepositFinalizedEvents[monitoredAddress][l1Token] ??= {}; + + // We want to iterate over the deposit events that have been initiated. We'll then match them with the + // finalization events to determine which deposits are still outstanding. + for (const l2Token of Object.keys(this.l1DepositInitiatedEvents[monitoredAddress][l1Token])) { + this.l2DepositFinalizedEvents[monitoredAddress][l1Token][l2Token] ??= []; + const l2FinalizationSet = this.l2DepositFinalizedEvents[monitoredAddress][l1Token][l2Token]; + + // Match deposits and finalizations by amount. We're only doing a limited lookback of events so collisions + // should be unlikely. + const finalizedAmounts = l2FinalizationSet.map((finalization) => finalization.amount.toString()); + const pendingDeposits = this.l1DepositInitiatedEvents[monitoredAddress][l1Token][l2Token].filter( + (deposit) => { + // Remove the first match. This handles scenarios where are collisions by amount. + const index = finalizedAmounts.indexOf(deposit.amount.toString()); + if (index > -1) { + finalizedAmounts.splice(index, 1); + return false; + } + return true; + } + ); + + // Short circuit early if there are no pending deposits. + if (pendingDeposits.length === 0) { + continue; } - return true; - }); - // Short circuit early if there are no pending deposits. - if (pendingDeposits.length === 0) { - continue; - } + outstandingTransfers[monitoredAddress][l1Token] ??= {}; - const totalAmount = pendingDeposits.reduce((acc, curr) => acc.add(curr.amount), bnZero); - const depositTxHashes = pendingDeposits.map((deposit) => deposit.transactionHash); - outstandingTransfers[monitoredAddress][l1Token] = { - totalAmount, - depositTxHashes, - }; + const totalAmount = pendingDeposits.reduce((acc, curr) => acc.add(curr.amount), bnZero); + const depositTxHashes = pendingDeposits.map((deposit) => deposit.transactionHash); + outstandingTransfers[monitoredAddress][l1Token][l2Token] = { + totalAmount, + depositTxHashes, + }; + } } } diff --git a/src/clients/bridges/CCTPAdapter.ts b/src/clients/bridges/CCTPAdapter.ts index fd98a87bf..556b56f6b 100644 --- a/src/clients/bridges/CCTPAdapter.ts +++ b/src/clients/bridges/CCTPAdapter.ts @@ -23,15 +23,6 @@ import { BaseAdapter } from "./BaseAdapter"; * to be used to bridge USDC via CCTP. */ export abstract class CCTPAdapter extends BaseAdapter { - /** - * Get the CCTP domain of the hub chain. This is used to determine the source - * domain of a CCTP message. - * @returns The CCTP domain of the hub chain - */ - private get l1SourceDomain(): number { - return chainIdsToCctpDomains[this.hubChainId]; - } - /** * Get the CCTP domain of the target chain. This is used to determine the destination * domain of a CCTP message. diff --git a/src/clients/bridges/CrossChainTransferClient.ts b/src/clients/bridges/CrossChainTransferClient.ts index d69f0600f..e6c67839d 100644 --- a/src/clients/bridges/CrossChainTransferClient.ts +++ b/src/clients/bridges/CrossChainTransferClient.ts @@ -1,4 +1,4 @@ -import { BigNumber, bnZero, winston, assign, toBN, DefaultLogLevels, AnyObject } from "../../utils"; +import { BigNumber, bnZero, winston, DefaultLogLevels, AnyObject } from "../../utils"; import { AdapterManager } from "./AdapterManager"; import { OutstandingTransfers } from "../../interfaces"; @@ -12,14 +12,42 @@ export class CrossChainTransferClient { ) {} // Get any funds currently in the canonical bridge. - getOutstandingCrossChainTransferAmount(address: string, chainId: number | string, l1Token: string): BigNumber { - const amount = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]?.totalAmount; - return amount ? toBN(amount) : bnZero; + getOutstandingCrossChainTransferAmount( + address: string, + chainId: number | string, + l1Token: string, + l2Token?: string + ): BigNumber { + const transfers = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]; + if (!transfers) { + return bnZero; + } + + if (l2Token) { + return transfers[l2Token]?.totalAmount ?? bnZero; + } + + // No specific l2Token specified; return the sum of all l1Token transfers to chainId. + return Object.values(transfers).reduce((acc, { totalAmount }) => acc.add(totalAmount), bnZero); } - getOutstandingCrossChainTransferTxs(address: string, chainId: number | string, l1Token: string): string[] { - const txHashes = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]?.depositTxHashes; - return txHashes ? txHashes : []; + getOutstandingCrossChainTransferTxs( + address: string, + chainId: number | string, + l1Token: string, + l2Token?: string + ): string[] { + const transfers = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]; + if (!transfers) { + return []; + } + + if (l2Token) { + return transfers[l2Token]?.depositTxHashes ?? []; + } + + // No specific l2Token specified; return the set of all l1Token transfers to chainId. + return Object.values(transfers).flatMap(({ depositTxHashes }) => depositTxHashes); } getEnabledChains(): number[] { @@ -30,26 +58,24 @@ export class CrossChainTransferClient { return this.getEnabledChains().filter((chainId) => chainId !== 1); } - increaseOutstandingTransfer(address: string, l1Token: string, rebalance: BigNumber, chainId: number): void { - if (!this.outstandingCrossChainTransfers[chainId]) { - this.outstandingCrossChainTransfers[chainId] = {}; - } - const transfers = this.outstandingCrossChainTransfers[chainId]; - if (transfers[address] === undefined) { - transfers[address] = {}; - } - if (transfers[address][l1Token] === undefined) { - transfers[address][l1Token] = { - totalAmount: bnZero, - depositTxHashes: [], - }; - } + increaseOutstandingTransfer( + address: string, + l1Token: string, + l2Token: string, + rebalance: BigNumber, + chainId: number + ): void { + const transfers = (this.outstandingCrossChainTransfers[chainId] ??= {}); + transfers[address] ??= {}; + transfers[address][l1Token] ??= {}; + transfers[address][l1Token][l2Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; // TODO: Require a tx hash here so we can track it as well. - transfers[address][l1Token].totalAmount = this.getOutstandingCrossChainTransferAmount( + transfers[address][l1Token][l2Token].totalAmount = this.getOutstandingCrossChainTransferAmount( address, chainId, - l1Token + l1Token, + l2Token ).add(rebalance); } @@ -58,14 +84,12 @@ export class CrossChainTransferClient { this.log("Updating cross chain transfers", { monitoredChains }); const outstandingTransfersPerChain = await Promise.all( - monitoredChains.map((chainId) => - this.adapterManager.getOutstandingCrossChainTokenTransferAmount(chainId, l1Tokens) - ) + monitoredChains.map(async (chainId) => [ + chainId, + await this.adapterManager.getOutstandingCrossChainTokenTransferAmount(chainId, l1Tokens), + ]) ); - outstandingTransfersPerChain.forEach((outstandingTransfers, index) => { - assign(this.outstandingCrossChainTransfers, [monitoredChains[index]], outstandingTransfers); - }); - + this.outstandingCrossChainTransfers = Object.fromEntries(outstandingTransfersPerChain); this.log("Updated cross chain transfers", { outstandingCrossChainTransfers: this.outstandingCrossChainTransfers }); } diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index 86e5aad47..df435ff17 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -139,7 +139,7 @@ export class LineaAdapter extends BaseAdapter { : this.getL1TokenBridge(); } - async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { + async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { const outstandingTransfers: OutstandingTransfers = {}; const { l1SearchConfig, l2SearchConfig } = this.getUpdatedSearchConfigs(); const supportedL1Tokens = this.filterSupportedTokens(l1Tokens); @@ -266,15 +266,18 @@ export class LineaAdapter extends BaseAdapter { l1Token: string, transferEvents: Event[] ): void { + const l2Token = this.resolveL2TokenAddress(l1Token, false); // There's no native USDC on Linea + assert(!isDefined(TOKEN_SYMBOLS_MAP._USDC.addresses[this.chainId])); // We can blow up if this eventually stops being true transferEvents.forEach((event) => { const txHash = event.transactionHash; // @dev WETH events have a _value field, while ERC20 events have an amount field. const amount = event.args._value ?? event.args.amount; outstandingTransfers[monitoredAddress] ??= {}; - outstandingTransfers[monitoredAddress][l1Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; - outstandingTransfers[monitoredAddress][l1Token] = { - totalAmount: outstandingTransfers[monitoredAddress][l1Token].totalAmount.add(amount), - depositTxHashes: [...outstandingTransfers[monitoredAddress][l1Token].depositTxHashes, txHash], + outstandingTransfers[monitoredAddress][l1Token] ??= {}; + outstandingTransfers[monitoredAddress][l1Token][l2Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; + outstandingTransfers[monitoredAddress][l1Token][l2Token] = { + totalAmount: outstandingTransfers[monitoredAddress][l1Token][l2Token].totalAmount.add(amount), + depositTxHashes: [...outstandingTransfers[monitoredAddress][l1Token][l2Token].depositTxHashes, txHash], }; }); } diff --git a/src/clients/bridges/PolygonAdapter.ts b/src/clients/bridges/PolygonAdapter.ts index ed3285509..4712ba8ac 100644 --- a/src/clients/bridges/PolygonAdapter.ts +++ b/src/clients/bridges/PolygonAdapter.ts @@ -232,12 +232,17 @@ export class PolygonAdapter extends CCTPAdapter { }; }); const eventsStorage = index % 2 === 0 ? this.l1DepositInitiatedEvents : this.l2DepositFinalizedEvents; - assign(eventsStorage, [monitoredAddress, l1Token], events); + const l2Token = this.resolveL2TokenAddress(l1Token, false); // these are all either normal L2 tokens or bridged USDC + assign(eventsStorage, [monitoredAddress, l1Token, l2Token], events); }); if (isDefined(resultingCCTPEvents[monitoredAddress])) { assign( this.l1DepositInitiatedEvents, - [monitoredAddress, TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]], + [ + monitoredAddress, + TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId], + TOKEN_SYMBOLS_MAP._USDC.addresses[this.chainId], // Must map to the USDC Native L2 token address + ], resultingCCTPEvents[monitoredAddress] ); } diff --git a/src/clients/bridges/ZKSyncAdapter.ts b/src/clients/bridges/ZKSyncAdapter.ts index 0e845c048..705f7601e 100644 --- a/src/clients/bridges/ZKSyncAdapter.ts +++ b/src/clients/bridges/ZKSyncAdapter.ts @@ -9,7 +9,6 @@ import { assign, Event, ZERO_ADDRESS, - getTokenAddress, TOKEN_SYMBOLS_MAP, bnZero, } from "../../utils"; @@ -78,6 +77,7 @@ export class ZKSyncAdapter extends BaseAdapter { // Resolve whether the token is WETH or not. const isWeth = this.isWeth(l1TokenAddress); + const l2Token = this.resolveL2TokenAddress(l1TokenAddress, false); // CCTP doesn't exist on ZkSync. if (isWeth) { [initiatedQueryResult, finalizedQueryResult, wrapQueryResult] = await Promise.all([ // If sending WETH from EOA, we can assume the EOA is unwrapping ETH and sending it through the @@ -121,7 +121,6 @@ export class ZKSyncAdapter extends BaseAdapter { finalizedQueryResult = matchL2EthDepositAndWrapEvents(finalizedQueryResult, wrapQueryResult); } } else { - const l2Token = getTokenAddress(l1TokenAddress, this.hubChainId, this.chainId); [initiatedQueryResult, finalizedQueryResult] = await Promise.all([ // Filter on 'from' and 'to' address paginatedEventQuery( @@ -141,13 +140,17 @@ export class ZKSyncAdapter extends BaseAdapter { assign( this.l1DepositInitiatedEvents, - [address, l1TokenAddress], + [address, l1TokenAddress, l2Token], // An initiatedQueryResult could be a zkSync DepositInitiated or an AtomicDepositor // ZkSyncEthDepositInitiated event, subject to whether the deposit token was WETH or not. // A ZkSyncEthDepositInitiated event doesn't have a token or l1Token param. initiatedQueryResult.map(processEvent).filter((e) => isWeth || e.l1Token === l1TokenAddress) ); - assign(this.l2DepositFinalizedEvents, [address, l1TokenAddress], finalizedQueryResult.map(processEvent)); + assign( + this.l2DepositFinalizedEvents, + [address, l1TokenAddress, l2Token], + finalizedQueryResult.map(processEvent) + ); }); }); diff --git a/src/clients/bridges/index.ts b/src/clients/bridges/index.ts index d2bb55e17..c8ce57d9a 100644 --- a/src/clients/bridges/index.ts +++ b/src/clients/bridges/index.ts @@ -1,7 +1,9 @@ export * from "./BaseAdapter"; export * from "./AdapterManager"; export * from "./op-stack/optimism/OptimismAdapter"; +export * from "./op-stack/base/BaseChainAdapter"; export * from "./ArbitrumAdapter"; export * from "./PolygonAdapter"; export * from "./CrossChainTransferClient"; export * from "./ZKSyncAdapter"; +export * from "./LineaAdapter"; diff --git a/src/clients/bridges/op-stack/DefaultErc20Bridge.ts b/src/clients/bridges/op-stack/DefaultErc20Bridge.ts index fed56c2c5..a80bb85a3 100644 --- a/src/clients/bridges/op-stack/DefaultErc20Bridge.ts +++ b/src/clients/bridges/op-stack/DefaultErc20Bridge.ts @@ -1,12 +1,16 @@ -import { Contract, BigNumber, paginatedEventQuery, Event, Signer, EventSearchConfig, Provider } from "../../../utils"; +import { Contract, BigNumber, paginatedEventQuery, Signer, EventSearchConfig, Provider } from "../../../utils"; import { CONTRACT_ADDRESSES } from "../../../common"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; -export class DefaultERC20Bridge implements OpStackBridge { +export class DefaultERC20Bridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; - constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`].address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`]; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -14,10 +18,6 @@ export class DefaultERC20Bridge implements OpStackBridge { this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); } - get l1Gateways(): string[] { - return [this.l1Bridge.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -32,27 +32,31 @@ export class DefaultERC20Bridge implements OpStackBridge { }; } - queryL1BridgeInitiationEvents( + async queryL1BridgeInitiationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l1Bridge, - this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), + eventConfig + ), + }; } - queryL2BridgeFinalizationEvents( + async queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l2Bridge, - this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), + eventConfig + ), + }; } } diff --git a/src/clients/bridges/op-stack/OpStackAdapter.ts b/src/clients/bridges/op-stack/OpStackAdapter.ts index 9530d3e4d..da61d233d 100644 --- a/src/clients/bridges/op-stack/OpStackAdapter.ts +++ b/src/clients/bridges/op-stack/OpStackAdapter.ts @@ -99,16 +99,13 @@ export class OpStackAdapter extends BaseAdapter { bridge.queryL2BridgeFinalizationEvents(l1Token, monitoredAddress, l2SearchConfig), ]); - assign( - this.l1DepositInitiatedEvents, - [monitoredAddress, l1Token], - depositInitiatedResults.map(processEvent) - ); - assign( - this.l2DepositFinalizedEvents, - [monitoredAddress, l1Token], - depositFinalizedResults.map(processEvent) - ); + Object.entries(depositInitiatedResults).forEach(([l2Token, events]) => { + assign(this.l1DepositInitiatedEvents, [monitoredAddress, l1Token, l2Token], events.map(processEvent)); + }); + + Object.entries(depositFinalizedResults).forEach(([l2Token, events]) => { + assign(this.l2DepositFinalizedEvents, [monitoredAddress, l1Token, l2Token], events.map(processEvent)); + }); }) ) ) diff --git a/src/clients/bridges/op-stack/OpStackBridgeInterface.ts b/src/clients/bridges/op-stack/OpStackBridgeInterface.ts index 508d97c10..407fe1e27 100644 --- a/src/clients/bridges/op-stack/OpStackBridgeInterface.ts +++ b/src/clients/bridges/op-stack/OpStackBridgeInterface.ts @@ -1,4 +1,12 @@ -import { Contract, BigNumber, Event, EventSearchConfig } from "../../../utils"; +import { + Contract, + BigNumber, + Event, + EventSearchConfig, + Signer, + Provider, + getTokenAddressWithCCTP, +} from "../../../utils"; export interface BridgeTransactionDetails { readonly contract: Contract; @@ -6,19 +14,38 @@ export interface BridgeTransactionDetails { readonly args: unknown[]; } -export interface OpStackBridge { - readonly l1Gateways: string[]; - constructL1ToL2Txn( +export type OpStackEvents = { [l2Token: string]: Event[] }; + +export abstract class OpStackBridge { + constructor( + protected l2chainId: number, + protected hubChainId: number, + protected l1Signer: Signer, + protected l2SignerOrProvider: Signer | Provider, + readonly l1Gateways: string[] + ) {} + + abstract constructL1ToL2Txn( toAddress: string, l1Token: string, l2Token: string, amount: BigNumber, l2Gas: number ): BridgeTransactionDetails; - queryL1BridgeInitiationEvents(l1Token: string, fromAddress: string, eventConfig: EventSearchConfig): Promise; - queryL2BridgeFinalizationEvents( + + abstract queryL1BridgeInitiationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise; + + abstract queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise; + ): Promise; + + protected resolveL2TokenAddress(l1Token: string): string { + return getTokenAddressWithCCTP(l1Token, this.hubChainId, this.l2chainId, false); + } } diff --git a/src/clients/bridges/op-stack/UsdcCCTPBridge.ts b/src/clients/bridges/op-stack/UsdcCCTPBridge.ts index a5ce7cd37..6aeacec14 100644 --- a/src/clients/bridges/op-stack/UsdcCCTPBridge.ts +++ b/src/clients/bridges/op-stack/UsdcCCTPBridge.ts @@ -1,19 +1,18 @@ -import { BigNumber, Contract, Event, Signer } from "ethers"; +import { BigNumber, Contract, Signer } from "ethers"; import { CONTRACT_ADDRESSES, chainIdsToCctpDomains } from "../../../common"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; import { EventSearchConfig, Provider, TOKEN_SYMBOLS_MAP } from "../../../utils"; import { cctpAddressToBytes32, retrieveOutstandingCCTPBridgeUSDCTransfers } from "../../../utils/CCTPUtils"; -export class UsdcCCTPBridge implements OpStackBridge { +export class UsdcCCTPBridge extends OpStackBridge { private readonly l1CctpTokenBridge: Contract; private readonly l2CctpMessageTransmitter: Contract; - constructor( - private l2chainId: number, - private hubChainId: number, - l1Signer: Signer, - l2SignerOrProvider: Signer | Provider - ) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].cctpTokenMessenger.address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].cctpTokenMessenger; this.l1CctpTokenBridge = new Contract(l1Address, l1Abi, l1Signer); @@ -29,14 +28,11 @@ export class UsdcCCTPBridge implements OpStackBridge { return TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId]; } - private get l2UsdcTokenAddress(): string { + protected resolveL2TokenAddress(l1Token: string): string { + l1Token; return TOKEN_SYMBOLS_MAP._USDC.addresses[this.l2chainId]; } - get l1Gateways(): string[] { - return [this.l1CctpTokenBridge.address]; - } - constructL1ToL2Txn( toAddress: string, _l1Token: string, @@ -53,25 +49,27 @@ export class UsdcCCTPBridge implements OpStackBridge { } async queryL1BridgeInitiationEvents( - _l1Token: string, + l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return retrieveOutstandingCCTPBridgeUSDCTransfers( - this.l1CctpTokenBridge, - this.l2CctpMessageTransmitter, - eventConfig, - this.l1UsdcTokenAddress, - this.hubChainId, - this.l2chainId, - fromAddress - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await retrieveOutstandingCCTPBridgeUSDCTransfers( + this.l1CctpTokenBridge, + this.l2CctpMessageTransmitter, + eventConfig, + this.l1UsdcTokenAddress, + this.hubChainId, + this.l2chainId, + fromAddress + ), + }; } queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { + ): Promise { // Lint Appeasement l1Token; fromAddress; @@ -80,6 +78,6 @@ export class UsdcCCTPBridge implements OpStackBridge { // Per the documentation of the BaseAdapter's computeOutstandingCrossChainTransfers method, we can return an empty array here // and only return the relevant outstanding events from queryL1BridgeInitiationEvents. // Relevant link: https://github.com/across-protocol/relayer-v2/blob/master/src/clients/bridges/BaseAdapter.ts#L189 - return Promise.resolve([]); + return Promise.resolve({}); } } diff --git a/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts b/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts index ed4cbdcf7..79395853f 100644 --- a/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts +++ b/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts @@ -1,19 +1,19 @@ -import { BigNumber, Event, Signer } from "ethers"; +import { BigNumber, Signer } from "ethers"; import { DefaultERC20Bridge } from "./DefaultErc20Bridge"; import { UsdcCCTPBridge } from "./UsdcCCTPBridge"; import { EventSearchConfig, Provider, TOKEN_SYMBOLS_MAP, assert, compareAddressesSimple } from "../../../utils"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; +import { CONTRACT_ADDRESSES } from "../../../common"; -export class UsdcTokenSplitterBridge implements OpStackBridge { +export class UsdcTokenSplitterBridge extends OpStackBridge { private readonly cctpBridge: UsdcCCTPBridge; private readonly canonicalBridge: DefaultERC20Bridge; - constructor( - private l2chainId: number, - private hubChainId: number, - l1Signer: Signer, - l2SignerOrProvider: Signer | Provider - ) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].cctpTokenMessenger.address, + CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`].address, + ]); this.cctpBridge = new UsdcCCTPBridge(l2chainId, hubChainId, l1Signer, l2SignerOrProvider); this.canonicalBridge = new DefaultERC20Bridge(l2chainId, hubChainId, l1Signer, l2SignerOrProvider); } @@ -45,31 +45,47 @@ export class UsdcTokenSplitterBridge implements OpStackBridge { l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { + ): Promise { // We should *only* be calling this class for USDC tokens assert(compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId])); const events = await Promise.all([ this.cctpBridge.queryL1BridgeInitiationEvents(l1Token, fromAddress, eventConfig), this.canonicalBridge.queryL1BridgeInitiationEvents(l1Token, fromAddress, eventConfig), ]); - return events.flat(); + // Reduce the events to a single Object. If there are any duplicate keys, merge the events. + return events.reduce((acc, event) => { + Object.entries(event).forEach(([l2Token, events]) => { + if (l2Token in acc) { + acc[l2Token] = acc[l2Token].concat(events); + } else { + acc[l2Token] = events; + } + }); + return acc; + }, {}); } async queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { + ): Promise { // We should *only* be calling this class for USDC tokens assert(compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId])); const events = await Promise.all([ this.cctpBridge.queryL2BridgeFinalizationEvents(l1Token, fromAddress, eventConfig), this.canonicalBridge.queryL2BridgeFinalizationEvents(l1Token, fromAddress, eventConfig), ]); - return events.flat(); - } - - get l1Gateways(): string[] { - return [...this.cctpBridge.l1Gateways, ...this.canonicalBridge.l1Gateways]; + // Reduce the events to a single object. If there are any duplicate keys, merge the events. + return events.reduce((acc, event) => { + Object.entries(event).forEach(([l2Token, events]) => { + if (l2Token in acc) { + acc[l2Token] = acc[l2Token].concat(events); + } else { + acc[l2Token] = events; + } + }); + return acc; + }, {}); } } diff --git a/src/clients/bridges/op-stack/WethBridge.ts b/src/clients/bridges/op-stack/WethBridge.ts index 6d7970d0f..aea3c8e25 100644 --- a/src/clients/bridges/op-stack/WethBridge.ts +++ b/src/clients/bridges/op-stack/WethBridge.ts @@ -1,31 +1,36 @@ import { Contract, BigNumber, - Event, EventSearchConfig, paginatedEventQuery, Signer, Provider, ZERO_ADDRESS, + Event, + TOKEN_SYMBOLS_MAP, } from "../../../utils"; import { CONTRACT_ADDRESSES } from "../../../common"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; import { matchL2EthDepositAndWrapEvents } from "../utils"; import { utils } from "@across-protocol/sdk-v2"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; -export class WethBridge implements OpStackBridge { +export class WethBridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; private readonly atomicDepositor: Contract; private readonly l2Weth: Contract; private readonly hubPoolAddress: string; - constructor( - private l2chainId: number, - readonly hubChainId: number, - l1Signer: Signer, - l2SignerOrProvider: Signer | Provider - ) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super( + l2chainId, + hubChainId, + l1Signer, + l2SignerOrProvider, + // To keep existing logic, we should use ataomic depositor as the l1 bridge + [CONTRACT_ADDRESSES[hubChainId].atomicDepositor.address] + ); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`]; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -41,10 +46,6 @@ export class WethBridge implements OpStackBridge { this.hubPoolAddress = CONTRACT_ADDRESSES[this.hubChainId]?.hubPool?.address; } - get l1Gateways(): string[] { - return [this.atomicDepositor.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -59,12 +60,18 @@ export class WethBridge implements OpStackBridge { }; } + private convertEventListToOpStackEvents(events: Event[]): OpStackEvents { + return { + [this.resolveL2TokenAddress(TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubChainId])]: events, + }; + } + async queryL1BridgeInitiationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig, l1Bridge = this.l1Bridge - ): Promise { + ): Promise { // We need to be smart about the filtering here because the ETHDepositInitiated event does not // index on the `toAddress` which is the `fromAddress` that we pass in here and the address we want // to actually filter on. So we make some simplifying assumptions: @@ -76,7 +83,7 @@ export class WethBridge implements OpStackBridge { // Since we can only index on the `fromAddress` for the ETHDepositInitiated event, we can't support // monitoring the spoke pool address if (isL2ChainContract || (isContract && fromAddress !== this.hubPoolAddress)) { - return []; + return this.convertEventListToOpStackEvents([]); } const events = await paginatedEventQuery( @@ -87,9 +94,9 @@ export class WethBridge implements OpStackBridge { // If EOA sent the ETH via the AtomicDepositor, then remove any events where the // toAddress is not the EOA so we don't get confused with other users using the AtomicDepositor if (!isContract) { - return events.filter((event) => event.args._to === fromAddress); + return this.convertEventListToOpStackEvents(events.filter((event) => event.args._to === fromAddress)); } - return events; + return this.convertEventListToOpStackEvents(events); } async queryL2BridgeFinalizationEvents( @@ -98,14 +105,14 @@ export class WethBridge implements OpStackBridge { eventConfig: EventSearchConfig, l2Bridge = this.l2Bridge, l2Weth = this.l2Weth - ): Promise { + ): Promise { // Check if the sender is a contract on the L1 network. const isContract = await this.isHubChainContract(fromAddress); // See above for why we don't want to monitor the spoke pool contract. const isL2ChainContract = await this.isL2ChainContract(fromAddress); if (isL2ChainContract || (isContract && fromAddress !== this.hubPoolAddress)) { - return []; + return this.convertEventListToOpStackEvents([]); } if (!isContract) { @@ -132,18 +139,20 @@ export class WethBridge implements OpStackBridge { // on L1 and received as ETH on L2 by the recipient, which is finally wrapped into WETH on the L2 by the // recipient--the L2 signer in this class. const l2EthWrapEvents = await this.queryL2WrapEthEvents(fromAddress, eventConfig, l2Weth); - return matchL2EthDepositAndWrapEvents(l2EthDepositEvents, l2EthWrapEvents); + return this.convertEventListToOpStackEvents(matchL2EthDepositAndWrapEvents(l2EthDepositEvents, l2EthWrapEvents)); } else { // Since we can only index on the `fromAddress` for the DepositFinalized event, we can't support // monitoring the spoke pool address if (fromAddress !== this.hubPoolAddress) { - return []; + return this.convertEventListToOpStackEvents([]); } - return await paginatedEventQuery( - l2Bridge, - l2Bridge.filters.DepositFinalized(ZERO_ADDRESS, undefined, fromAddress), - eventConfig + return this.convertEventListToOpStackEvents( + await paginatedEventQuery( + l2Bridge, + l2Bridge.filters.DepositFinalized(ZERO_ADDRESS, undefined, fromAddress), + eventConfig + ) ); } } diff --git a/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts b/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts index eedc9950b..6d84a374d 100644 --- a/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts +++ b/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts @@ -1,20 +1,16 @@ -import { - Contract, - BigNumber, - paginatedEventQuery, - Event, - EventSearchConfig, - Signer, - Provider, -} from "../../../../utils"; +import { Contract, BigNumber, paginatedEventQuery, EventSearchConfig, Signer, Provider } from "../../../../utils"; import { CONTRACT_ADDRESSES } from "../../../../common"; -import { OpStackBridge, BridgeTransactionDetails } from "../OpStackBridgeInterface"; +import { OpStackBridge, BridgeTransactionDetails, OpStackEvents } from "../OpStackBridgeInterface"; -export class DaiOptimismBridge implements OpStackBridge { +export class DaiOptimismBridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; - constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].daiOptimismBridge.address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].daiOptimismBridge; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -22,10 +18,6 @@ export class DaiOptimismBridge implements OpStackBridge { this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); } - get l1Gateways(): string[] { - return [this.l1Bridge.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -40,27 +32,31 @@ export class DaiOptimismBridge implements OpStackBridge { }; } - queryL1BridgeInitiationEvents( + async queryL1BridgeInitiationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l1Bridge, - this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), + eventConfig + ), + }; } - queryL2BridgeFinalizationEvents( + async queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l2Bridge, - this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), + eventConfig + ), + }; } } diff --git a/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts b/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts index e903cc44b..fdfc542c8 100644 --- a/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts +++ b/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts @@ -1,20 +1,16 @@ -import { - Contract, - BigNumber, - paginatedEventQuery, - Event, - EventSearchConfig, - Signer, - Provider, -} from "../../../../utils"; +import { Contract, BigNumber, paginatedEventQuery, EventSearchConfig, Signer, Provider } from "../../../../utils"; import { CONTRACT_ADDRESSES } from "../../../../common"; -import { OpStackBridge, BridgeTransactionDetails } from "../OpStackBridgeInterface"; +import { OpStackBridge, BridgeTransactionDetails, OpStackEvents } from "../OpStackBridgeInterface"; -export class SnxOptimismBridge implements OpStackBridge { +export class SnxOptimismBridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; - constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].snxOptimismBridge.address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].snxOptimismBridge; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -22,10 +18,6 @@ export class SnxOptimismBridge implements OpStackBridge { this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); } - get l1Gateways(): string[] { - return [this.l1Bridge.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -41,21 +33,33 @@ export class SnxOptimismBridge implements OpStackBridge { }; } - queryL1BridgeInitiationEvents(l1Token: string, toAddress: string, eventConfig: EventSearchConfig): Promise { + async queryL1BridgeInitiationEvents( + l1Token: string, + toAddress: string, + eventConfig: EventSearchConfig + ): Promise { // @dev For the SnxBridge, only the `toAddress` is indexed on the L2 event so we treat the `fromAddress` as the // toAddress when fetching the L1 event. - return paginatedEventQuery( - this.l1Bridge, - this.l1Bridge.filters.DepositInitiated(undefined, toAddress), - eventConfig - ); + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.DepositInitiated(undefined, toAddress), + eventConfig + ), + }; } - queryL2BridgeFinalizationEvents( + async queryL2BridgeFinalizationEvents( l1Token: string, toAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery(this.l2Bridge, this.l2Bridge.filters.DepositFinalized(toAddress), eventConfig); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(toAddress), + eventConfig + ), + }; } } diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 243985ddc..c081f5763 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -1,3 +1,4 @@ +import { BigNumber } from "ethers"; import { interfaces } from "@across-protocol/sdk-v2"; export * from "./InventoryManagement"; @@ -8,7 +9,16 @@ export * from "./Report"; export * from "./Arweave"; // Bridge interfaces -export type OutstandingTransfers = interfaces.OutstandingTransfers; +export interface OutstandingTransfers { + [address: string]: { + [l1Token: string]: { + [l2Token: string]: { + totalAmount: BigNumber; + depositTxHashes: string[]; + }; + }; + }; +} // Common interfaces export type SortableEvent = interfaces.SortableEvent; diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 589e9e2c8..7d0b429c0 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -1,5 +1,5 @@ -import { TOKEN_SYMBOLS_MAP } from "@across-protocol/constants-v2"; -import { BigNumber, ethers } from "."; +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants-v2"; +import { BigNumber, ethers, isDefined } from "."; export function compareAddresses(addressA: string, addressB: string): 1 | -1 | 0 { // Convert address strings to BigNumbers and then sort numerical value of the BigNumber, which sorts the addresses @@ -15,13 +15,20 @@ export function compareAddresses(addressA: string, addressB: string): 1 | -1 | 0 } } -export function compareAddressesSimple(addressA: string, addressB: string): boolean { +export function compareAddressesSimple(addressA?: string, addressB?: string): boolean { if (addressA === undefined || addressB === undefined) { return false; } return addressA.toLowerCase() === addressB.toLowerCase(); } +export function includesAddressSimple(address: string | undefined, list: string[]): boolean { + if (!isDefined(address)) { + return false; + } + return list.filter((listAddress) => compareAddressesSimple(address, listAddress)).length > 0; +} + /** * Match the token symbol for the given token address and chain ID. * @param tokenAddress The token address to resolve the symbol for. @@ -76,6 +83,23 @@ export function getTokenAddress(tokenAddress: string, chainId: number, targetCha return targetAddress; } +export function getTokenAddressWithCCTP( + l1Token: string, + hubChainId: number, + l2ChainId: number, + isNativeUsdc = false +): string { + // Base Case + if (hubChainId === l2ChainId) { + return l1Token; + } + if (compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP._USDC.addresses[hubChainId])) { + const onBase = l2ChainId === CHAIN_IDs.BASE || l2ChainId === CHAIN_IDs.BASE_SEPOLIA; + return TOKEN_SYMBOLS_MAP[isNativeUsdc ? "_USDC" : onBase ? "USDbC" : "USDC.e"].addresses[l2ChainId]; + } + return getTokenAddress(l1Token, hubChainId, l2ChainId); +} + export function checkAddressChecksum(tokenAddress: string): boolean { return ethers.utils.getAddress(tokenAddress) === tokenAddress; } diff --git a/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts b/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts index 29f7fe49f..17593930a 100644 --- a/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts +++ b/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts @@ -21,14 +21,14 @@ class TestAdapter extends BaseAdapter { const deposits = amounts.map((amount) => { return { amount: toBN(amount) }; }); - this.l1DepositInitiatedEvents = { "0xmonitored": { token: deposits as unknown as DepositEvent[] } }; + this.l1DepositInitiatedEvents = { "0xmonitored": { token: { token: deposits as unknown as DepositEvent[] } } }; } public setFinalizationEvents(amounts: number[]) { const deposits = amounts.map((amount) => { return { amount: toBN(amount) }; }); - this.l2DepositFinalizedEvents = { "0xmonitored": { token: deposits as unknown as DepositEvent[] } }; + this.l2DepositFinalizedEvents = { "0xmonitored": { token: { token: deposits as unknown as DepositEvent[] } } }; } getOutstandingCrossChainTransfers(): Promise { @@ -80,6 +80,6 @@ describe("AdapterManager: Get outstanding cross chain token transfer amounts", a }); const expectOutstandingTransfersAmount = (transfers: OutstandingTransfers, amount: number) => { - const actualAmount = transfers["0xmonitored"]?.["token"]?.totalAmount || toBN(0); + const actualAmount = transfers["0xmonitored"]?.["token"]?.["token"]?.totalAmount || toBN(0); expect(actualAmount).to.eq(toBN(amount)); }; diff --git a/test/Monitor.ts b/test/Monitor.ts index 65344af15..6dd54a5da 100644 --- a/test/Monitor.ts +++ b/test/Monitor.ts @@ -292,6 +292,7 @@ describe("Monitor", async function () { crossChainTransferClient.increaseOutstandingTransfer( depositor.address, l1Token.address, + l2Token.address, toBN(5), destinationChainId ); @@ -333,7 +334,8 @@ describe("Monitor", async function () { originChainId, spokePool_1.address, l1Token.address, - toBN(5) + toBN(5), + l2Token.address ); await updateAllClients(); await monitorInstance.update(); diff --git a/test/cross-chain-adapters/Linea.ts b/test/cross-chain-adapters/Linea.ts index 94f5d7ca5..971393641 100644 --- a/test/cross-chain-adapters/Linea.ts +++ b/test/cross-chain-adapters/Linea.ts @@ -9,7 +9,7 @@ import { CONTRACT_ADDRESSES } from "../../src/common"; describe("Cross Chain Adapter: Linea", async function () { let adapter: LineaAdapter; let monitoredEoa: string; - let l1Token: string; + let l1Token, l1USDCToken, l1WETHToken: string; let wethBridgeContract: Contract; let usdcBridgeContract: Contract; @@ -24,7 +24,9 @@ describe("Cross Chain Adapter: Linea", async function () { const [deployer] = await ethers.getSigners(); monitoredEoa = randomAddress(); - l1Token = randomAddress(); + l1Token = TOKEN_SYMBOLS_MAP.WBTC.addresses[CHAIN_IDs.MAINNET]; + l1USDCToken = TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]; + l1WETHToken = TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]; const spokePool = await (await getContractFactory("MockSpokePool", deployer)).deploy(ZERO_ADDRESS); @@ -85,13 +87,15 @@ describe("Cross Chain Adapter: Linea", async function () { let outstandingTransfers = {}; // 1. If l1 and l2 events pair off, outstanding transfers will be empty - adapter.matchWethDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1Token); + adapter.matchWethDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1WETHToken); expect(outstandingTransfers).to.deep.equal({}); // 2. If finalized event is missing, there will be an outstanding transfer. outstandingTransfers = {}; - adapter.matchWethDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1Token); - expect(outstandingTransfers[monitoredEoa][l1Token]).to.deep.equal({ + adapter.matchWethDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1WETHToken); + expect( + outstandingTransfers[monitoredEoa][l1WETHToken][TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.LINEA]] + ).to.deep.equal({ totalAmount: toBN(1), depositTxHashes: l1Events.map((e) => e.transactionHash), }); @@ -121,13 +125,15 @@ describe("Cross Chain Adapter: Linea", async function () { let outstandingTransfers = {}; // 1. If l1 and l2 events pair off, outstanding transfers will be empty - adapter.matchUsdcDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1Token); + adapter.matchUsdcDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1USDCToken); expect(outstandingTransfers).to.deep.equal({}); // 2. If finalized event is missing, there will be an outstanding transfer. outstandingTransfers = {}; - adapter.matchUsdcDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1Token); - expect(outstandingTransfers[monitoredEoa][l1Token]).to.deep.equal({ + adapter.matchUsdcDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1USDCToken); + expect( + outstandingTransfers[monitoredEoa][l1USDCToken][TOKEN_SYMBOLS_MAP["USDC.e"].addresses[CHAIN_IDs.LINEA]] + ).to.deep.equal({ totalAmount: toBN(0), depositTxHashes: l1Events.map((e) => e.transactionHash), }); @@ -164,7 +170,6 @@ describe("Cross Chain Adapter: Linea", async function () { expect(result[0].args.nativeToken).to.equal(l1Token); }); it("Matches L1 and L2 events", async function () { - const l1Token = randomAddress(); await erc20BridgeContract.emitBridgingInitiated(randomAddress(), monitoredEoa, l1Token); await erc20BridgeContract.emitBridgingFinalized(l1Token, monitoredEoa); const l1Events = await adapter.getErc20DepositInitiatedEvents( @@ -189,7 +194,9 @@ describe("Cross Chain Adapter: Linea", async function () { // 2. If finalized event is missing, there will be an outstanding transfer. outstandingTransfers = {}; adapter.matchErc20DepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1Token); - expect(outstandingTransfers[monitoredEoa][l1Token]).to.deep.equal({ + expect( + outstandingTransfers[monitoredEoa][l1Token][TOKEN_SYMBOLS_MAP.WBTC.addresses[CHAIN_IDs.LINEA]] + ).to.deep.equal({ totalAmount: toBN(0), depositTxHashes: l1Events.map((e) => e.transactionHash), }); diff --git a/test/cross-chain-adapters/OpStack.ts b/test/cross-chain-adapters/OpStack.ts index 8f6b158af..7cb83c9e5 100644 --- a/test/cross-chain-adapters/OpStack.ts +++ b/test/cross-chain-adapters/OpStack.ts @@ -1,9 +1,9 @@ -import { CHAIN_IDs } from "@across-protocol/constants-v2"; +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants-v2"; import { ethers, getContractFactory, Contract, randomAddress, expect } from "../utils"; import { utils } from "@across-protocol/sdk-v2"; import { CONTRACT_ADDRESSES } from "../../src/common"; import { WethBridge } from "../../src/clients/bridges/op-stack/WethBridge"; -import { Signer } from "ethers"; +import { Event, Signer } from "ethers"; describe("Cross Chain Adapter: OP Stack", async function () { let monitoredEoa: string; @@ -39,12 +39,14 @@ describe("Cross Chain Adapter: OP Stack", async function () { await wethBridgeContract.emitDepositInitiated(randomAddress(), monitoredEoa, 1); await wethBridgeContract.emitDepositInitiated(atomicDepositorAddress, randomAddress(), 1); await wethBridgeContract.emitDepositInitiated(atomicDepositorAddress, monitoredEoa, 1); - const result = await wethBridge.queryL1BridgeInitiationEvents( - wethContract.address, - monitoredEoa, - searchConfig, - wethBridgeContract - ); + const result = ( + await wethBridge.queryL1BridgeInitiationEvents( + wethContract.address, + monitoredEoa, + searchConfig, + wethBridgeContract + ) + )[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.OPTIMISM]]; expect(result.length).to.equal(1); expect(result[0].args._from).to.equal(atomicDepositorAddress); expect(result[0].args._to).to.equal(monitoredEoa); @@ -56,23 +58,29 @@ describe("Cross Chain Adapter: OP Stack", async function () { // Counts only finalized events preceding a WETH wrap event. // For EOA's, weth transfer from address should be atomic depositor address await wethBridgeContract.emitDepositFinalized(atomicDepositorAddress, monitoredEoa, 1); - const emptyResult = await wethBridge.queryL2BridgeFinalizationEvents( - wethContract.address, - monitoredEoa, - searchConfig, - wethBridgeContract, - wethContract + const convertResults = (result: Record) => + result[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.OPTIMISM]]; + const emptyResult = convertResults( + await wethBridge.queryL2BridgeFinalizationEvents( + wethContract.address, + monitoredEoa, + searchConfig, + wethBridgeContract, + wethContract + ) ); expect(emptyResult.length).to.equal(0); // Mine Deposit event now. await wethContract.connect(monitoredEoaAccount).deposit({ value: 0 }); - const result = await wethBridge.queryL2BridgeFinalizationEvents( - wethContract.address, - monitoredEoa, - searchConfig, - wethBridgeContract, - wethContract + const result = convertResults( + await wethBridge.queryL2BridgeFinalizationEvents( + wethContract.address, + monitoredEoa, + searchConfig, + wethBridgeContract, + wethContract + ) ); expect(result.length).to.equal(1); expect(result[0].args._from).to.equal(atomicDepositorAddress); diff --git a/test/mocks/MockAdapterManager.ts b/test/mocks/MockAdapterManager.ts index ef24f28e2..c8edf0878 100644 --- a/test/mocks/MockAdapterManager.ts +++ b/test/mocks/MockAdapterManager.ts @@ -1,5 +1,5 @@ import { AdapterManager } from "../../src/clients/bridges"; -import { BigNumber, TransactionResponse } from "../../src/utils"; +import { BigNumber, TransactionResponse, getTokenAddressWithCCTP } from "../../src/utils"; import { createRandomBytes32 } from "../utils"; import { OutstandingTransfers } from "../../src/interfaces"; @@ -40,14 +40,25 @@ export class MockAdapterManager extends AdapterManager { return this.mockedOutstandingCrossChainTransfers[chainId]; } - setMockedOutstandingCrossChainTransfers(chainId: number, address: string, l1Token: string, amount: BigNumber): void { - if (!this.mockedOutstandingCrossChainTransfers[chainId]) { - this.mockedOutstandingCrossChainTransfers[chainId] = {}; - } + setMockedOutstandingCrossChainTransfers( + chainId: number, + address: string, + l1Token: string, + amount: BigNumber, + l2Token?: string + ): void { + this.mockedOutstandingCrossChainTransfers[chainId] ??= {}; + const transfers = this.mockedOutstandingCrossChainTransfers[chainId]; - if (!transfers[address]) { - transfers[address] = {}; - } - transfers[address][l1Token] = { totalAmount: amount, depositTxHashes: [] }; + + transfers[address] ??= {}; + transfers[address][l1Token] ??= {}; + + l2Token ??= getTokenAddressWithCCTP(l1Token, 1, chainId, false); + + transfers[address][l1Token][l2Token] = { + totalAmount: amount, + depositTxHashes: [], + }; } } diff --git a/yarn.lock b/yarn.lock index 6757c2076..b7167cd57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,10 +11,10 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants-v2@1.0.22": - version "1.0.22" - resolved "https://registry.yarnpkg.com/@across-protocol/constants-v2/-/constants-v2-1.0.22.tgz#b3ca8c6a6276fa36c9ac5fbf17b9f57e4730fa63" - integrity sha512-j559oRNY5xNOo2YYb94cPS1sR6dSxKa2YE2rY8SAdagW11tc7aDflV3AAxmCRPyuI7WpB6w5IklBx0JvwNAO0w== +"@across-protocol/constants-v2@1.0.24": + version "1.0.24" + resolved "https://registry.yarnpkg.com/@across-protocol/constants-v2/-/constants-v2-1.0.24.tgz#30b7dd898ff6d4848717fd6cf393a10f1f2f40b0" + integrity sha512-lDu6rMoTMUolGHwUnX/YkeMcIKB1avd6MnBcyfa4z5DkuF0HCo+7Mbviri2LXW0SOTg/pE26Dvfr1P+HmBUD9A== "@across-protocol/constants-v2@^1.0.19": version "1.0.20"