From f8e3c6a934c4d15292f1474cf821ef9380c3ebf9 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Wed, 20 Mar 2024 23:38:28 -0400 Subject: [PATCH 01/15] improve(linea): add outstanding contracts --- src/clients/bridges/LineaAdapter.ts | 82 +++++++++++++- src/common/ContractAddresses.ts | 168 +++++++++++++++++++++++----- 2 files changed, 219 insertions(+), 31 deletions(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index a3c1355c6..2dcdc22fe 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -1,3 +1,6 @@ +import * as sdk from "@across-protocol/sdk-v2"; +import { CONTRACT_ADDRESSES } from "../../common"; +import { OutstandingTransfers } from "../../interfaces"; import { BigNumber, CHAIN_IDs, @@ -7,13 +10,13 @@ import { assert, bnZero, compareAddressesSimple, + getTokenAddress, isDefined, + paginatedEventQuery, winston, } from "../../utils"; import { SpokePoolClient } from "../SpokePoolClient"; import { BaseAdapter } from "./BaseAdapter"; -import { CONTRACT_ADDRESSES } from "../../common"; -import * as sdk from "@across-protocol/sdk-v2"; export class LineaAdapter extends BaseAdapter { readonly l1TokenBridge = CONTRACT_ADDRESSES[this.hubChainId].lineaL1TokenBridge.address; @@ -83,6 +86,28 @@ export class LineaAdapter extends BaseAdapter { ); } + getL2TokenBridge(): Contract { + const chainId = this.chainId; + return new Contract( + CONTRACT_ADDRESSES[chainId].lineaL2TokenBridge.address, + CONTRACT_ADDRESSES[chainId].lineaL2TokenBridge.abi, + this.getSigner(chainId) + ); + } + + getL2UsdcBridge(): Contract { + const chainId = this.chainId; + return new Contract( + CONTRACT_ADDRESSES[chainId].lineaL2UsdcBridge.address, + CONTRACT_ADDRESSES[chainId].lineaL2UsdcBridge.abi, + this.getSigner(chainId) + ); + } + + getL2Bridge(l1Token: string): Contract { + return this.isUsdc(l1Token) ? this.getL2UsdcBridge() : this.getL2TokenBridge(); + } + /** * Get L1 Atomic WETH depositor contract * @returns L1 Atomic WETH depositor contract @@ -108,10 +133,55 @@ export class LineaAdapter extends BaseAdapter { : this.getL1TokenBridge(); } - // FIXME: NO-OP - getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { - l1Tokens; - return Promise.resolve({}); + async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { + const outstandingTransfers: OutstandingTransfers = {}; + const { l1SearchConfig, l2SearchConfig } = this.getUpdatedSearchConfigs(); + await sdk.utils.mapAsync(this.monitoredAddresses, async (address) => { + await sdk.utils.mapAsync(l1Tokens, async (l1Token) => { + if (this.isWeth(l1Token)) { + // TODO: Implement this + } else { + const isUsdc = this.isUsdc(l1Token); + const l2Token = getTokenAddress(l1Token, this.hubChainId, this.chainId); + const l1Bridge = this.getL1Bridge(l1Token); + const l2Bridge = this.getL2Bridge(l1Token); + // Initiated event filter + const filterL1 = isUsdc + ? l1Bridge.filters.Deposited(address, null, address) + : l1Bridge.filters.DepositInitiated(address, null, l2Token); + // Finalized event filter + const filterL2 = isUsdc + ? l2Bridge.filters.ReceivedFromOtherLayer(address) + : l2Bridge.filters.BridgingFinalized(l1Token); + const [initiatedQueryResult, finalizedQueryResult] = await Promise.all([ + paginatedEventQuery(l1Bridge, filterL1, l1SearchConfig), + paginatedEventQuery(l2Bridge, filterL2, l2SearchConfig), + ]); + initiatedQueryResult.forEach((initialEvent) => { + const txHash = initialEvent.transactionHash; + const amount = initialEvent.args.amount; + const finalizedEvent = finalizedQueryResult.find((finalEvent) => + isUsdc + ? finalEvent.args.amount.eq(initialEvent.args.amount) && + compareAddressesSimple(initialEvent.args.to, finalEvent.args.recipient) + : finalEvent.args.amount.eq(initialEvent.args.amount) && + compareAddressesSimple(initialEvent.args.recipient, finalEvent.args.recipient) && + compareAddressesSimple(finalEvent.args.nativeToken, initialEvent.args.token) + ); + if (!isDefined(finalizedEvent)) { + outstandingTransfers[address] = outstandingTransfers[address] || { + [l1Token]: { totalAmount: bnZero, depositTxHashes: [] }, + }; + outstandingTransfers[address][l1Token] = { + totalAmount: outstandingTransfers[address][l1Token].totalAmount.add(amount), + depositTxHashes: [...outstandingTransfers[address][l1Token].depositTxHashes, txHash], + }; + } + }); + } + }); + }); + return outstandingTransfers; } sendTokenToTargetChain( diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 84375d785..f12829a26 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -55,6 +55,139 @@ export const LINEA_L2_MESSAGE_SERVICE_CONTRACT_ABI = [ }, ]; +export const LINEA_TOKEN_BRIDGE_CONTRACT_ABI = [ + { + inputs: [ + { internalType: "address", name: "_token", type: "address" }, + { internalType: "uint256", name: "_amount", type: "uint256" }, + { internalType: "address", name: "_recipient", type: "address" }, + ], + name: "bridgeToken", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "BridgingInitiated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "nativeToken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "bridgedToken", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + ], + name: "BridgingFinalized", + type: "event", + }, +]; + +export const LINEA_USDC_BRIDGE_CONTRACT_ABI = [ + { + inputs: [ + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "to", type: "address" }, + ], + name: "depositTo", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "depositor", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + ], + name: "Deposited", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "ReceivedFromOtherLayer", + type: "event", + }, +]; + // Constants file exporting hardcoded contract addresses per chain. export const CONTRACT_ADDRESSES: { [chainId: number]: { @@ -70,34 +203,11 @@ export const CONTRACT_ADDRESSES: { }, lineaL1TokenBridge: { address: "0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319", - abi: [ - { - inputs: [ - { internalType: "address", name: "_token", type: "address" }, - { internalType: "uint256", name: "_amount", type: "uint256" }, - { internalType: "address", name: "_recipient", type: "address" }, - ], - name: "bridgeToken", - outputs: [], - stateMutability: "payable", - type: "function", - }, - ], + abi: LINEA_TOKEN_BRIDGE_CONTRACT_ABI, }, lineaL1UsdcBridge: { address: "0x504A330327A089d8364C4ab3811Ee26976d388ce", - abi: [ - { - inputs: [ - { internalType: "uint256", name: "amount", type: "uint256" }, - { internalType: "address", name: "to", type: "address" }, - ], - name: "depositTo", - outputs: [], - stateMutability: "payable", - type: "function", - }, - ], + abi: LINEA_USDC_BRIDGE_CONTRACT_ABI, }, zkSyncMailbox: { address: "0x32400084C286CF3E17e7B677ea9583e60a000324", @@ -903,6 +1013,14 @@ export const CONTRACT_ADDRESSES: { address: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec", abi: LINEA_L2_MESSAGE_SERVICE_CONTRACT_ABI, }, + l2LineaUsdcBridge: { + address: "0xA2Ee6Fce4ACB62D95448729cDb781e3BEb62504A", + abi: LINEA_USDC_BRIDGE_CONTRACT_ABI, + }, + l2LineaTokenBridge: { + address: "0x353012dc4a9A6cF55c941bADC267f82004A8ceB9", + abi: LINEA_TOKEN_BRIDGE_CONTRACT_ABI, + }, weth: { address: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f", abi: [ From 4a28817f9957a655568bf8cddfefb14a6dc23d1a Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 21 Mar 2024 00:00:33 -0400 Subject: [PATCH 02/15] improve: finish WETH contracts --- src/clients/bridges/LineaAdapter.ts | 59 ++++++++++++++++++++++++++++- src/common/ContractAddresses.ts | 55 +++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index 2dcdc22fe..ffc9948af 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -68,6 +68,24 @@ export class LineaAdapter extends BaseAdapter { return null; } + getL2MessageService(): Contract { + const chainId = this.chainId; + return new Contract( + CONTRACT_ADDRESSES[chainId].l2MessageService.address, + CONTRACT_ADDRESSES[chainId].l2MessageService.abi, + this.getSigner(chainId) + ); + } + + getL1MessageService(): Contract { + const { hubChainId } = this; + return new Contract( + CONTRACT_ADDRESSES[hubChainId].lineaMessageService.address, + CONTRACT_ADDRESSES[hubChainId].lineaMessageService.abi, + this.getSigner(hubChainId) + ); + } + getL1TokenBridge(): Contract { const { hubChainId } = this; return new Contract( @@ -139,7 +157,46 @@ export class LineaAdapter extends BaseAdapter { await sdk.utils.mapAsync(this.monitoredAddresses, async (address) => { await sdk.utils.mapAsync(l1Tokens, async (l1Token) => { if (this.isWeth(l1Token)) { - // TODO: Implement this + const atomicDepositor = this.getAtomicDepositor(); + const l1MessageService = this.getL1MessageService(); + const l2MessageService = this.getL2MessageService(); + + // We need to do the following sequential steps. + // 1. Get all initiated MessageSent events from the L1MessageService where the 'from' address is + // the AtomicDepositor and the 'to' address is the user's address. + // 2. Pipe the resulting _messageHash argument from step 1 into the MessageClaimed event filter + // 3. For each MessageSent, match the _messageHash to the _messageHash in the MessageClaimed event + // any unmatched MessageSent events are considered outstanding transfers. + const initiatedQueryResult = await paginatedEventQuery( + l1MessageService, + l1MessageService.filters.MessageSent(atomicDepositor.address, address), + l1SearchConfig + ); + const internalMessageHashes = initiatedQueryResult.map(({ args }) => args._messageHash); + const finalizedQueryResult = await paginatedEventQuery( + l2MessageService, + // Passing in an array of message hashes results in an OR filter + l2MessageService.filters.MessageClaimed(internalMessageHashes), + l2SearchConfig + ); + initiatedQueryResult + .filter( + ({ args }) => + !finalizedQueryResult.some( + (finalizedEvent) => args._messageHash.toLowerCase() === finalizedEvent.args._messageHash.toLowerCase() + ) + ) + .forEach((event) => { + const txHash = event.transactionHash; + const amount = event.args._value; + outstandingTransfers[address] = outstandingTransfers[address] || { + [l1Token]: { totalAmount: bnZero, depositTxHashes: [] }, + }; + outstandingTransfers[address][l1Token] = { + totalAmount: outstandingTransfers[address][l1Token].totalAmount.add(amount), + depositTxHashes: [...outstandingTransfers[address][l1Token].depositTxHashes, txHash], + }; + }); } else { const isUsdc = this.isUsdc(l1Token); const l2Token = getTokenAddress(l1Token, this.hubChainId, this.chainId); diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index f12829a26..493cda256 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -39,7 +39,7 @@ const CCTP_MESSAGE_TRANSMITTER_CONTRACT_ABI = [ }, ]; -export const LINEA_L2_MESSAGE_SERVICE_CONTRACT_ABI = [ +export const LINEA_MESSAGE_SERVICE_CONTRACT_ABI = [ { inputs: [], name: "minimumFeeInWei", @@ -53,6 +53,54 @@ export const LINEA_L2_MESSAGE_SERVICE_CONTRACT_ABI = [ stateMutability: "view", type: "function", }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "bytes32", name: "_messageHash", type: "bytes32" }], + name: "MessageClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: "_from", + type: "address", + }, + { + indexed: true, + name: "_to", + type: "address", + }, + { + indexed: false, + name: "_fee", + type: "uint256", + }, + { + indexed: false, + name: "_value", + type: "uint256", + }, + { + indexed: false, + name: "_nonce", + type: "uint256", + }, + { + indexed: false, + name: "_calldata", + type: "bytes", + }, + { + indexed: true, + name: "_messageHash", + type: "bytes32", + }, + ], + name: "MessageSent", + type: "event", + }, ]; export const LINEA_TOKEN_BRIDGE_CONTRACT_ABI = [ @@ -200,6 +248,7 @@ export const CONTRACT_ADDRESSES: { 1: { lineaMessageService: { address: "0xd19d4B5d358258f05D7B411E21A1460D11B0876F", + abi: LINEA_MESSAGE_SERVICE_CONTRACT_ABI, }, lineaL1TokenBridge: { address: "0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319", @@ -1011,7 +1060,7 @@ export const CONTRACT_ADDRESSES: { 59144: { l2MessageService: { address: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec", - abi: LINEA_L2_MESSAGE_SERVICE_CONTRACT_ABI, + abi: LINEA_MESSAGE_SERVICE_CONTRACT_ABI, }, l2LineaUsdcBridge: { address: "0xA2Ee6Fce4ACB62D95448729cDb781e3BEb62504A", @@ -1085,7 +1134,7 @@ export const CONTRACT_ADDRESSES: { 59140: { l2MessageService: { address: "0xC499a572640B64eA1C8c194c43Bc3E19940719dC", - abi: LINEA_L2_MESSAGE_SERVICE_CONTRACT_ABI, + abi: LINEA_MESSAGE_SERVICE_CONTRACT_ABI, }, }, }; From f79ff8a489b7774c6f3aff7393eeae12bc4d7591 Mon Sep 17 00:00:00 2001 From: "James Morris, MS" <96435344+james-a-morris@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:39:37 -0400 Subject: [PATCH 03/15] nit: use null coalesce operator Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> --- src/clients/bridges/LineaAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index ffc9948af..b7802d0ff 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -189,7 +189,7 @@ export class LineaAdapter extends BaseAdapter { .forEach((event) => { const txHash = event.transactionHash; const amount = event.args._value; - outstandingTransfers[address] = outstandingTransfers[address] || { + outstandingTransfers[address] ??= { [l1Token]: { totalAmount: bnZero, depositTxHashes: [] }, }; outstandingTransfers[address][l1Token] = { From e71022e7bbbed431d3fbb668bf003f7c85fa5b29 Mon Sep 17 00:00:00 2001 From: "James Morris, MS" <96435344+james-a-morris@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:40:44 -0400 Subject: [PATCH 04/15] nit: use null coalesce operator Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> --- src/clients/bridges/LineaAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index b7802d0ff..dee1eafc5 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -226,7 +226,7 @@ export class LineaAdapter extends BaseAdapter { compareAddressesSimple(finalEvent.args.nativeToken, initialEvent.args.token) ); if (!isDefined(finalizedEvent)) { - outstandingTransfers[address] = outstandingTransfers[address] || { + outstandingTransfers[address] ??= { [l1Token]: { totalAmount: bnZero, depositTxHashes: [] }, }; outstandingTransfers[address][l1Token] = { From cfd464e8bbbfb6371b986d347fb89a3b11756972 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:40:08 +0100 Subject: [PATCH 05/15] fixes --- src/clients/bridges/LineaAdapter.ts | 2 +- src/common/ContractAddresses.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index dee1eafc5..182f54f51 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -205,7 +205,7 @@ export class LineaAdapter extends BaseAdapter { // Initiated event filter const filterL1 = isUsdc ? l1Bridge.filters.Deposited(address, null, address) - : l1Bridge.filters.DepositInitiated(address, null, l2Token); + : l1Bridge.filters.BridgingInitiated(address, null, l2Token); // Finalized event filter const filterL2 = isUsdc ? l2Bridge.filters.ReceivedFromOtherLayer(address) diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 55bb9f817..d6b895e8b 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -1082,11 +1082,11 @@ export const CONTRACT_ADDRESSES: { address: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec", abi: LINEA_MESSAGE_SERVICE_CONTRACT_ABI, }, - l2LineaUsdcBridge: { + lineaL2UsdcBridge: { address: "0xA2Ee6Fce4ACB62D95448729cDb781e3BEb62504A", abi: LINEA_USDC_BRIDGE_CONTRACT_ABI, }, - l2LineaTokenBridge: { + lineaL2TokenBridge: { address: "0x353012dc4a9A6cF55c941bADC267f82004A8ceB9", abi: LINEA_TOKEN_BRIDGE_CONTRACT_ABI, }, From 81ca71ec66b1b7ed790bc053aacb617ccb64dc4d Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 21 Mar 2024 09:01:19 -0400 Subject: [PATCH 06/15] nit: clean adapter --- src/clients/bridges/LineaAdapter.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index 182f54f51..e6408a3c4 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -202,14 +202,12 @@ export class LineaAdapter extends BaseAdapter { const l2Token = getTokenAddress(l1Token, this.hubChainId, this.chainId); const l1Bridge = this.getL1Bridge(l1Token); const l2Bridge = this.getL2Bridge(l1Token); - // Initiated event filter - const filterL1 = isUsdc - ? l1Bridge.filters.Deposited(address, null, address) - : l1Bridge.filters.BridgingInitiated(address, null, l2Token); - // Finalized event filter - const filterL2 = isUsdc - ? l2Bridge.filters.ReceivedFromOtherLayer(address) - : l2Bridge.filters.BridgingFinalized(l1Token); + + // Define the initialized and finalized event filters for the L1 and L2 bridges + const [filterL1, filterL2] = isUsdc + ? [l1Bridge.filters.Deposited(address, null, address), l2Bridge.filters.ReceivedFromOtherLayer(address)] + : [l1Bridge.filters.BridgingInitiated(address, null, l2Token), l2Bridge.filters.BridgingFinalized(l1Token)]; + const [initiatedQueryResult, finalizedQueryResult] = await Promise.all([ paginatedEventQuery(l1Bridge, filterL1, l1SearchConfig), paginatedEventQuery(l2Bridge, filterL2, l2SearchConfig), From 98b7e907f3e491286dbb373936c6fdd206cd3cc5 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 21 Mar 2024 09:12:19 -0400 Subject: [PATCH 07/15] improve: code style --- src/clients/bridges/LineaAdapter.ts | 32 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index e6408a3c4..faec0c523 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -212,18 +212,23 @@ export class LineaAdapter extends BaseAdapter { paginatedEventQuery(l1Bridge, filterL1, l1SearchConfig), paginatedEventQuery(l2Bridge, filterL2, l2SearchConfig), ]); - initiatedQueryResult.forEach((initialEvent) => { - const txHash = initialEvent.transactionHash; - const amount = initialEvent.args.amount; - const finalizedEvent = finalizedQueryResult.find((finalEvent) => - isUsdc - ? finalEvent.args.amount.eq(initialEvent.args.amount) && - compareAddressesSimple(initialEvent.args.to, finalEvent.args.recipient) - : finalEvent.args.amount.eq(initialEvent.args.amount) && - compareAddressesSimple(initialEvent.args.recipient, finalEvent.args.recipient) && - compareAddressesSimple(finalEvent.args.nativeToken, initialEvent.args.token) - ); - if (!isDefined(finalizedEvent)) { + initiatedQueryResult + .filter( + (initialEvent) => + !isDefined( + finalizedQueryResult.find((finalEvent) => + isUsdc + ? finalEvent.args.amount.eq(initialEvent.args.amount) && + compareAddressesSimple(initialEvent.args.to, finalEvent.args.recipient) + : finalEvent.args.amount.eq(initialEvent.args.amount) && + compareAddressesSimple(initialEvent.args.recipient, finalEvent.args.recipient) && + compareAddressesSimple(finalEvent.args.nativeToken, initialEvent.args.token) + ) + ) + ) + .forEach((initialEvent) => { + const txHash = initialEvent.transactionHash; + const amount = initialEvent.args.amount; outstandingTransfers[address] ??= { [l1Token]: { totalAmount: bnZero, depositTxHashes: [] }, }; @@ -231,8 +236,7 @@ export class LineaAdapter extends BaseAdapter { totalAmount: outstandingTransfers[address][l1Token].totalAmount.add(amount), depositTxHashes: [...outstandingTransfers[address][l1Token].depositTxHashes, txHash], }; - } - }); + }); } }); }); From f974dec25fb6b84cfccdd31e3beacd72641b4452 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 21 Mar 2024 10:29:09 -0400 Subject: [PATCH 08/15] fix: use l1Token --- src/clients/bridges/LineaAdapter.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index faec0c523..318fb7aea 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -10,7 +10,6 @@ import { assert, bnZero, compareAddressesSimple, - getTokenAddress, isDefined, paginatedEventQuery, winston, @@ -199,14 +198,13 @@ export class LineaAdapter extends BaseAdapter { }); } else { const isUsdc = this.isUsdc(l1Token); - const l2Token = getTokenAddress(l1Token, this.hubChainId, this.chainId); const l1Bridge = this.getL1Bridge(l1Token); const l2Bridge = this.getL2Bridge(l1Token); // Define the initialized and finalized event filters for the L1 and L2 bridges const [filterL1, filterL2] = isUsdc ? [l1Bridge.filters.Deposited(address, null, address), l2Bridge.filters.ReceivedFromOtherLayer(address)] - : [l1Bridge.filters.BridgingInitiated(address, null, l2Token), l2Bridge.filters.BridgingFinalized(l1Token)]; + : [l1Bridge.filters.BridgingInitiated(address, null, l1Token), l2Bridge.filters.BridgingFinalized(l1Token)]; const [initiatedQueryResult, finalizedQueryResult] = await Promise.all([ paginatedEventQuery(l1Bridge, filterL1, l1SearchConfig), From c70fe2b12db58770a19ca60c394aa4864361f17d Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:29:58 +0100 Subject: [PATCH 09/15] fix --- src/clients/bridges/LineaAdapter.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index 318fb7aea..038b973dd 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -188,9 +188,8 @@ export class LineaAdapter extends BaseAdapter { .forEach((event) => { const txHash = event.transactionHash; const amount = event.args._value; - outstandingTransfers[address] ??= { - [l1Token]: { totalAmount: bnZero, depositTxHashes: [] }, - }; + outstandingTransfers[address] ??= {}; + outstandingTransfers[address][l1Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; outstandingTransfers[address][l1Token] = { totalAmount: outstandingTransfers[address][l1Token].totalAmount.add(amount), depositTxHashes: [...outstandingTransfers[address][l1Token].depositTxHashes, txHash], @@ -227,9 +226,8 @@ export class LineaAdapter extends BaseAdapter { .forEach((initialEvent) => { const txHash = initialEvent.transactionHash; const amount = initialEvent.args.amount; - outstandingTransfers[address] ??= { - [l1Token]: { totalAmount: bnZero, depositTxHashes: [] }, - }; + outstandingTransfers[address] ??= {}; + outstandingTransfers[address][l1Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; outstandingTransfers[address][l1Token] = { totalAmount: outstandingTransfers[address][l1Token].totalAmount.add(amount), depositTxHashes: [...outstandingTransfers[address][l1Token].depositTxHashes, txHash], From 53b23cee590b41c92f820944d3d39c00520f42c5 Mon Sep 17 00:00:00 2001 From: "James Morris, MS" <96435344+james-a-morris@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:28:38 -0400 Subject: [PATCH 10/15] feat(finalizer): make l1l2 routes generic (#1328) * feat(finalizer): make l1l2 routes generic * chore: revert index * nit: include linea in non-overrides * wip * improve: overhaul generic finalizer * nit * Exclude missing SpokePoolClient instances * nit: also compare positive `_value` * nit: remove unneeded type * feat: Activate linea adapter manager (#1325) Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> * Update LineaAdapter.ts * Update LineaAdapter.ts * tweak * Update Constants.ts * Fix * Update LineaAdapter.ts * Update LineaAdapter.ts * Revert "Update LineaAdapter.ts" This reverts commit a1a4943275a6eaff0a8cc8f2ff72a87469f881f7. --------- Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> Co-authored-by: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Co-authored-by: nicholaspai --- src/clients/bridges/AdapterManager.ts | 4 + src/clients/bridges/LineaAdapter.ts | 11 +- src/common/Constants.ts | 2 +- src/common/ContractAddresses.ts | 42 +++++ src/finalizer/index.ts | 35 +++-- src/finalizer/types.ts | 3 +- src/finalizer/utils/linea/common.ts | 163 +++++++++++++++++++- src/finalizer/utils/linea/l1ToL2.ts | 213 ++++++++++---------------- 8 files changed, 324 insertions(+), 149 deletions(-) diff --git a/src/clients/bridges/AdapterManager.ts b/src/clients/bridges/AdapterManager.ts index 8e1ce7bd8..18708359b 100644 --- a/src/clients/bridges/AdapterManager.ts +++ b/src/clients/bridges/AdapterManager.ts @@ -6,6 +6,7 @@ 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 } = {}; @@ -39,6 +40,9 @@ export class AdapterManager { if (this.spokePoolClients[8453] !== undefined) { this.adapters[8453] = new BaseChainAdapter(logger, spokePoolClients, monitoredAddresses); } + if (this.spokePoolClients[59144] !== undefined) { + this.adapters[59144] = new LineaAdapter(logger, spokePoolClients, monitoredAddresses); + } logger.debug({ at: "AdapterManager#constructor", diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index 038b973dd..d5b541851 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -153,8 +153,9 @@ export class LineaAdapter extends BaseAdapter { async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { const outstandingTransfers: OutstandingTransfers = {}; const { l1SearchConfig, l2SearchConfig } = this.getUpdatedSearchConfigs(); + const supportedL1Tokens = l1Tokens.filter(this.isSupportedToken.bind(this)); await sdk.utils.mapAsync(this.monitoredAddresses, async (address) => { - await sdk.utils.mapAsync(l1Tokens, async (l1Token) => { + await sdk.utils.mapAsync(supportedL1Tokens, async (l1Token) => { if (this.isWeth(l1Token)) { const atomicDepositor = this.getAtomicDepositor(); const l1MessageService = this.getL1MessageService(); @@ -250,7 +251,13 @@ export class LineaAdapter extends BaseAdapter { const isUsdc = this.isUsdc(l1Token); const l1Bridge = this.getL1Bridge(l1Token); const l1BridgeMethod = isWeth ? "bridgeWethToLinea" : isUsdc ? "depositTo" : "bridgeToken"; - const l1BridgeArgs = isUsdc || isWeth ? [amount, address] : [l1Token, amount, address]; + // prettier-ignore + const l1BridgeArgs = isUsdc + ? [amount, address] + : isWeth + ? [address, amount] + : [l1Token, amount, address]; + return this._sendTokenToTargetChain( l1Token, l2Token, diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 59224691a..ad2d94f2a 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -273,7 +273,7 @@ export type Multicall2Call = { // These are the spokes that can hold both ETH and WETH, so they should be added together when caclulating whether // a bundle execution is possible with the funds in the pool. -export const spokesThatHoldEthAndWeth = [10, 324, 8453]; +export const spokesThatHoldEthAndWeth = [10, 324, 8453, 59144]; /** * An official mapping of chain IDs to CCTP domains. This mapping is separate from chain identifiers diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index d6b895e8b..fffee9f33 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -177,6 +177,38 @@ export const LINEA_TOKEN_BRIDGE_CONTRACT_ABI = [ name: "BridgingFinalized", type: "event", }, + { + inputs: [ + { + internalType: "address", + name: "_nativeToken", + type: "address", + }, + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + { + internalType: "address", + name: "_recipient", + type: "address", + }, + { + internalType: "uint256", + name: "_chainId", + type: "uint256", + }, + { + internalType: "bytes", + name: "_tokenMetadata", + type: "bytes", + }, + ], + stateMutability: "nonpayable", + type: "function", + name: "completeBridging", + }, ]; export const LINEA_USDC_BRIDGE_CONTRACT_ABI = [ @@ -234,6 +266,16 @@ export const LINEA_USDC_BRIDGE_CONTRACT_ABI = [ name: "ReceivedFromOtherLayer", type: "event", }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "receiveFromOtherLayer", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, ]; // Constants file exporting hardcoded contract addresses per chain. diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index 722e60b2a..c8c1fca53 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -1,6 +1,7 @@ import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import assert from "assert"; import { BigNumber, Contract, constants } from "ethers"; +import { getAddress } from "ethers/lib/utils"; import { groupBy, uniq } from "lodash"; import { AugmentedTransaction, HubPoolClient, MultiCallerClient, TransactionClient } from "../clients"; import { @@ -30,12 +31,12 @@ import { arbitrumOneFinalizer, cctpL1toL2Finalizer, cctpL2toL1Finalizer, + lineaL1ToL2Finalizer, + lineaL2ToL1Finalizer, opStackFinalizer, polygonFinalizer, scrollFinalizer, zkSyncFinalizer, - lineaL2ToL1Finalizer, - lineaL1ToL2Finalizer, } from "./utils"; const { isDefined } = sdkUtils; @@ -49,6 +50,7 @@ const chainFinalizers: { [chainId: number]: ChainFinalizer } = { 324: zkSyncFinalizer, 8453: opStackFinalizer, 42161: arbitrumOneFinalizer, + 59144: lineaL2ToL1Finalizer, 534352: scrollFinalizer, }; @@ -64,7 +66,6 @@ const chainFinalizerOverrides: { [chainId: number]: ChainFinalizer[] } = { 137: [polygonFinalizer, cctpL1toL2Finalizer, cctpL2toL1Finalizer], 8453: [opStackFinalizer, cctpL1toL2Finalizer, cctpL2toL1Finalizer], 42161: [arbitrumOneFinalizer, cctpL1toL2Finalizer, cctpL2toL1Finalizer], - 59144: [lineaL2ToL1Finalizer], // Testnets 84532: [cctpL1toL2Finalizer, cctpL2toL1Finalizer], 5: [lineaL1ToL2Finalizer], @@ -77,6 +78,7 @@ export async function finalize( hubPoolClient: HubPoolClient, spokePoolClients: SpokePoolClientsByChain, configuredChainIds: number[], + l1ToL2AddressesToFinalize: string[], submitFinalizationTransactions: boolean ): Promise { const hubChainId = hubPoolClient.chainId; @@ -116,7 +118,13 @@ export async function finalize( let totalDepositsForChain = 0; let totalMiscTxnsForChain = 0; for (const finalizer of chainSpecificFinalizers) { - const { callData, crossChainMessages } = await finalizer(logger, hubSigner, hubPoolClient, client); + const { callData, crossChainMessages } = await finalizer( + logger, + hubSigner, + hubPoolClient, + client, + l1ToL2AddressesToFinalize + ); callData.forEach((txn, idx) => { finalizationsToBatch.push({ txn, crossChainMessage: crossChainMessages[idx] }); @@ -126,9 +134,10 @@ export async function finalize( totalDepositsForChain += crossChainMessages.filter(({ type }) => type === "deposit").length; totalMiscTxnsForChain += crossChainMessages.filter(({ type }) => type === "misc").length; } + const totalTransfers = totalWithdrawalsForChain + totalDepositsForChain + totalMiscTxnsForChain; logger.debug({ at: "finalize", - message: `Found ${totalWithdrawalsForChain} ${network} transfers (${totalWithdrawalsForChain} withdrawals | ${totalDepositsForChain} deposits | ${totalMiscTxnsForChain} misc txns) for finalization.`, + message: `Found ${totalTransfers} ${network} messages (${totalWithdrawalsForChain} withdrawals | ${totalDepositsForChain} deposits | ${totalMiscTxnsForChain} misc txns) for finalization.`, }); } const multicall2Lookup = Object.fromEntries( @@ -320,11 +329,16 @@ async function updateFinalizerClients(clients: Clients) { export class FinalizerConfig extends DataworkerConfig { readonly maxFinalizerLookback: number; + readonly chainsToFinalize: number[]; + readonly addressesToMonitorForL1L2Finalizer: string[]; constructor(env: ProcessEnv) { - const { FINALIZER_MAX_TOKENBRIDGE_LOOKBACK } = env; + const { FINALIZER_MAX_TOKENBRIDGE_LOOKBACK, FINALIZER_CHAINS, L1_L2_FINALIZER_MONITOR_ADDRESS } = env; super(env); + this.chainsToFinalize = JSON.parse(FINALIZER_CHAINS ?? "[]"); + this.addressesToMonitorForL1L2Finalizer = JSON.parse(L1_L2_FINALIZER_MONITOR_ADDRESS ?? "[]").map(getAddress); + // `maxFinalizerLookback` is how far we fetch events from, modifying the search config's 'fromBlock' this.maxFinalizerLookback = Number(FINALIZER_MAX_TOKENBRIDGE_LOOKBACK ?? FINALIZER_TOKENBRIDGE_LOOKBACK); assert( @@ -348,14 +362,17 @@ export async function runFinalizer(_logger: winston.Logger, baseSigner: Signer): await updateSpokePoolClients(spokePoolClients, ["TokensBridged", "EnabledDepositRoute"]); if (config.finalizerEnabled) { + const availableChains = commonClients.configStoreClient + .getChainIdIndicesForBlock() + .filter((chainId) => isDefined(spokePoolClients[chainId])); + await finalize( logger, commonClients.hubSigner, commonClients.hubPoolClient, spokePoolClients, - process.env.FINALIZER_CHAINS - ? JSON.parse(process.env.FINALIZER_CHAINS) - : commonClients.configStoreClient.getChainIdIndicesForBlock(), + config.chainsToFinalize.length === 0 ? availableChains : config.chainsToFinalize, + config.addressesToMonitorForL1L2Finalizer, config.sendingFinalizationsEnabled ); } else { diff --git a/src/finalizer/types.ts b/src/finalizer/types.ts index 130e21b73..b4138264e 100644 --- a/src/finalizer/types.ts +++ b/src/finalizer/types.ts @@ -36,6 +36,7 @@ export interface ChainFinalizer { logger: winston.Logger, signer: Signer, hubPoolClient: HubPoolClient, - spokePoolClient: SpokePoolClient + spokePoolClient: SpokePoolClient, + l1ToL2AddressesToFinalize: string[] ): Promise; } diff --git a/src/finalizer/utils/linea/common.ts b/src/finalizer/utils/linea/common.ts index 0754f0385..941852891 100644 --- a/src/finalizer/utils/linea/common.ts +++ b/src/finalizer/utils/linea/common.ts @@ -3,14 +3,22 @@ import { L1MessageServiceContract, L2MessageServiceContract } from "@consensys/l import { L1ClaimingService } from "@consensys/linea-sdk/dist/lib/sdk/claiming/L1ClaimingService"; import { MessageSentEvent } from "@consensys/linea-sdk/dist/typechain/L2MessageService"; import { Linea_Adapter__factory } from "@across-protocol/contracts-v2"; - import { + BigNumber, + Contract, + EventSearchConfig, + TOKEN_SYMBOLS_MAP, TransactionReceipt, + compareAddressesSimple, + ethers, getBlockForTimestamp, getCurrentTime, getNodeUrlList, getRedisCache, + paginatedEventQuery, } from "../../../utils"; +import { HubPoolClient } from "../../../clients"; +import { CONTRACT_ADDRESSES } from "../../../common"; export type MessageWithStatus = Message & { logIndex: number; @@ -107,3 +115,156 @@ export async function getBlockRangeByHoursOffsets( return { fromBlock, toBlock }; } + +export function determineMessageType( + event: MessageSentEvent, + hubPoolClient: HubPoolClient +): + | { + type: "bridge"; + l1TokenSymbol: string; + l1TokenAddress: string; + amount: BigNumber; + } + | { + type: "misc"; + } { + const { _calldata, _value } = event.args; + // First check a WETH deposit. A WETH deposit is a message with a positive + // value and an empty calldata. + if (_calldata === "0x" && _value.gt(0)) { + return { + type: "bridge", + l1TokenSymbol: "WETH", + l1TokenAddress: TOKEN_SYMBOLS_MAP.WETH.addresses[hubPoolClient.chainId], + amount: _value, + }; + } + // Next check if the calldata is a valid Linea bridge. This can either be in the form of a + // UsdcTokenBridge or a TokenBridge. Both have a different calldata format. + + // Start with the TokenBridge calldata format. + try { + const contractInterface = new ethers.utils.Interface( + CONTRACT_ADDRESSES[hubPoolClient.chainId].lineaL1TokenBridge.abi + ); + const decoded = contractInterface.decodeFunctionData("completeBridging", _calldata); + // If we've made it this far, then the calldata is a valid TokenBridge calldata. + const token = hubPoolClient.getTokenInfo(hubPoolClient.chainId, decoded._nativeToken); + return { + type: "bridge", + l1TokenSymbol: token.symbol, + l1TokenAddress: decoded._nativeToken, + amount: decoded._amount, + }; + } catch (_e) { + // We don't care about this because we have more to check + } + // Next check the UsdcTokenBridge calldata format. + try { + const contractInterface = new ethers.utils.Interface( + CONTRACT_ADDRESSES[hubPoolClient.chainId].lineaL1UsdcBridge.abi + ); + const decoded = contractInterface.decodeFunctionData("receiveFromOtherLayer", _calldata); + // If we've made it this far, then the calldata is a valid UsdcTokenBridge calldata. + return { + type: "bridge", + l1TokenSymbol: "USDC", + l1TokenAddress: TOKEN_SYMBOLS_MAP.USDC.addresses[hubPoolClient.chainId], + amount: decoded.amount, + }; + } catch (_e) { + // We don't care about this because we have more to check + } + // If we've made it to this point, we've neither found a valid bridge calldata nor a WETH deposit. + // I.e. This is a relayed message of some kind. + return { + type: "misc", + }; +} + +export async function findMessageSentEvents( + contract: L1MessageServiceContract | L2MessageServiceContract, + l1ToL2AddressesToFinalize: string[], + searchConfig: EventSearchConfig +): Promise { + return paginatedEventQuery( + contract.contract, + (contract.contract as Contract).filters.MessageSent(l1ToL2AddressesToFinalize), + searchConfig + ) as Promise; +} + +export async function findMessageFromTokenBridge( + bridgeContract: Contract, + messageServiceContract: L1MessageServiceContract | L2MessageServiceContract, + l1ToL2AddressesToFinalize: string[], + searchConfig: EventSearchConfig +): Promise { + const bridgeEvents = await paginatedEventQuery( + bridgeContract, + bridgeContract.filters.BridgingInitiated(l1ToL2AddressesToFinalize), + searchConfig + ); + const messageSent = messageServiceContract.contract.interface.getEventTopic("MessageSent"); + const associatedMessages = await Promise.all( + bridgeEvents.map(async (event) => { + const { logs } = await bridgeContract.provider.getTransactionReceipt(event.transactionHash); + return logs + .filter((log) => log.topics[0] === messageSent) + .map((log) => ({ + ...log, + args: messageServiceContract.contract.interface.decodeEventLog("MessageSent", log.data, log.topics), + })) + .filter((log) => { + // Start with the TokenBridge calldata format. + try { + const decoded = bridgeContract.interface.decodeFunctionData("completeBridging", log.args._calldata); + return ( + compareAddressesSimple(decoded._recipient, event.args.recipient) && decoded._amount.eq(event.args.amount) + ); + } catch (_e) { + // We don't care about this because we have more to check + return false; + } + }); + }) + ); + return associatedMessages.flat() as unknown as MessageSentEvent[]; +} + +export async function findMessageFromUsdcBridge( + bridgeContract: Contract, + messageServiceContract: L1MessageServiceContract | L2MessageServiceContract, + l1ToL2AddressesToFinalize: string[], + searchConfig: EventSearchConfig +): Promise { + const bridgeEvents = await paginatedEventQuery( + bridgeContract, + bridgeContract.filters.Deposited(l1ToL2AddressesToFinalize), + searchConfig + ); + const messageSent = messageServiceContract.contract.interface.getEventTopic("MessageSent"); + const associatedMessages = await Promise.all( + bridgeEvents.map(async (event) => { + const { logs } = await bridgeContract.provider.getTransactionReceipt(event.transactionHash); + return logs + .filter((log) => log.topics[0] === messageSent) + .map((log) => ({ + ...log, + args: messageServiceContract.contract.interface.decodeEventLog("MessageSent", log.data, log.topics), + })) + .filter((log) => { + // Next check the UsdcTokenBridge calldata format. + try { + const decoded = bridgeContract.interface.decodeFunctionData("receiveFromOtherLayer", log.args._calldata); + return compareAddressesSimple(decoded.recipient, event.args.to) && decoded.amount.eq(event.args.amount); + } catch (_e) { + // We don't care about this because we have more to check + return false; + } + }); + }) + ); + return associatedMessages.flat() as unknown as MessageSentEvent[]; +} diff --git a/src/finalizer/utils/linea/l1ToL2.ts b/src/finalizer/utils/linea/l1ToL2.ts index e193e7cae..a14658e2d 100644 --- a/src/finalizer/utils/linea/l1ToL2.ts +++ b/src/finalizer/utils/linea/l1ToL2.ts @@ -1,44 +1,50 @@ +import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import { OnChainMessageStatus } from "@consensys/linea-sdk"; -import { L1MessageServiceContract } from "@consensys/linea-sdk/dist/lib/contracts"; -import { TokensRelayedEvent } from "@across-protocol/contracts-v2/dist/typechain/contracts/chain-adapters/Linea_Adapter"; -import { utils, providers } from "ethers"; +import { Contract } from "ethers"; +import { getAddress } from "ethers/lib/utils"; import { groupBy } from "lodash"; - -import { HubPoolClient } from "../../../clients"; -import { CHAIN_MAX_BLOCK_LOOKBACK } from "../../../common"; -import { - Signer, - winston, - convertFromWei, - TransactionReceipt, - paginatedEventQuery, - getDeployedAddress, -} from "../../../utils"; -import { FinalizerPromise, CrossChainMessage } from "../../types"; +import { HubPoolClient, SpokePoolClient } from "../../../clients"; +import { CHAIN_MAX_BLOCK_LOOKBACK, CONTRACT_ADDRESSES } from "../../../common"; +import { EventSearchConfig, Signer, convertFromWei, winston } from "../../../utils"; +import { CrossChainMessage, FinalizerPromise } from "../../types"; import { - initLineaSdk, - makeGetMessagesWithStatusByTxHash, - MessageWithStatus, - lineaAdapterIface, + determineMessageType, + findMessageFromTokenBridge, + findMessageFromUsdcBridge, + findMessageSentEvents, getBlockRangeByHoursOffsets, + initLineaSdk, } from "./common"; -type ParsedAdapterEvent = { - parsedLog: utils.LogDescription; - log: providers.Log; -}; - export async function lineaL1ToL2Finalizer( logger: winston.Logger, signer: Signer, - hubPoolClient: HubPoolClient + hubPoolClient: HubPoolClient, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _spokePoolClient: SpokePoolClient, + l1ToL2AddressesToFinalize: string[] ): Promise { const [l1ChainId, hubPoolAddress] = [hubPoolClient.chainId, hubPoolClient.hubPool.address]; const l2ChainId = l1ChainId === 1 ? 59144 : 59140; const lineaSdk = initLineaSdk(l1ChainId, l2ChainId); - const l2Contract = lineaSdk.getL2Contract(); - const l1Contract = lineaSdk.getL1Contract(); - const getMessagesWithStatusByTxHash = makeGetMessagesWithStatusByTxHash(l1Contract, l2Contract); + const l2MessageServiceContract = lineaSdk.getL2Contract(); + const l1MessageServiceContract = lineaSdk.getL1Contract(); + const l1TokenBridge = new Contract( + CONTRACT_ADDRESSES[l1ChainId].lineaL1TokenBridge.address, + CONTRACT_ADDRESSES[l1ChainId].lineaL1TokenBridge.abi, + hubPoolClient.hubPool.provider + ); + const l1UsdcBridge = new Contract( + CONTRACT_ADDRESSES[l1ChainId].lineaL1UsdcBridge.address, + CONTRACT_ADDRESSES[l1ChainId].lineaL1UsdcBridge.abi, + hubPoolClient.hubPool.provider + ); + + // We always want to make sure that the l1ToL2AddressesToFinalize array contains + // the HubPool address, so we can finalize any pending messages sent from the HubPool. + if (!l1ToL2AddressesToFinalize.includes(getAddress(hubPoolAddress))) { + l1ToL2AddressesToFinalize.push(hubPoolAddress); + } // Optimize block range for querying Linea's MessageSent events on L1. // We want to conservatively query for events that are between 0 and 24 hours old @@ -51,42 +57,48 @@ export async function lineaL1ToL2Finalizer( toBlock, }); - // Get Linea's `MessageSent` events originating from HubPool - const messageSentEvents = await paginatedEventQuery( - l1Contract.contract, - l1Contract.contract.filters.MessageSent(hubPoolAddress), - { - fromBlock, - toBlock, - maxBlockLookBack: CHAIN_MAX_BLOCK_LOOKBACK[l1ChainId] || 10_000, - } - ); - - // Get relevant tx receipts - const txnReceipts = await Promise.all( - messageSentEvents.map(({ transactionHash }) => - hubPoolClient.hubPool.provider.getTransactionReceipt(transactionHash) - ) - ); - const relevantTxReceipts = filterLineaTxReceipts(txnReceipts, l1Contract); - - // Get relevant Linea_Adapter events, i.e. TokensRelayed, RelayedMessage - const l1SrcEvents = parseAdapterEventsFromTxReceipts(relevantTxReceipts, l2ChainId); - - // Get Linea's MessageSent events with status - const relevantMessages = ( - await Promise.all(relevantTxReceipts.map(({ transactionHash }) => getMessagesWithStatusByTxHash(transactionHash))) - ).flat(); - - // Merge messages with TokensRelayed/RelayedMessage events - const mergedMessages = mergeMessagesWithAdapterEvents(relevantMessages, l1SrcEvents); - + const searchConfig: EventSearchConfig = { + fromBlock, + toBlock, + maxBlockLookBack: CHAIN_MAX_BLOCK_LOOKBACK[l1ChainId] || 10_000, + }; + + const [wethAndRelayEvents, tokenBridgeEvents, usdcBridgeEvents] = await Promise.all([ + findMessageSentEvents(l1MessageServiceContract, l1ToL2AddressesToFinalize, searchConfig), + findMessageFromTokenBridge(l1TokenBridge, l1MessageServiceContract, l1ToL2AddressesToFinalize, searchConfig), + findMessageFromUsdcBridge(l1UsdcBridge, l1MessageServiceContract, l1ToL2AddressesToFinalize, searchConfig), + ]); + + const messageSentEvents = [...wethAndRelayEvents, ...tokenBridgeEvents, ...usdcBridgeEvents]; + const enrichedMessageSentEvents = await sdkUtils.mapAsync(messageSentEvents, async (event) => { + const { + transactionHash: txHash, + logIndex, + args: { _from, _to, _fee, _value, _nonce, _calldata, _messageHash }, + } = event; + // It's unlikely that our multicall will have multiple transactions to bridge to Linea + // so we can grab the statuses individually. + const messageStatus = await l2MessageServiceContract.getMessageStatus(_messageHash); + return { + messageSender: _from, + destination: _to, + fee: _fee, + value: _value, + messageNonce: _nonce, + calldata: _calldata, + messageHash: _messageHash, + txHash, + logIndex, + status: messageStatus, + messageType: determineMessageType(event, hubPoolClient), + }; + }); // Group messages by status const { claimed = [], claimable = [], unknown = [], - } = groupBy(mergedMessages, ({ message }) => { + } = groupBy(enrichedMessageSentEvents, (message) => { switch (message.status) { case OnChainMessageStatus.CLAIMED: return "claimed"; @@ -99,8 +111,8 @@ export async function lineaL1ToL2Finalizer( // Populate txns for claimable messages const populatedTxns = await Promise.all( - claimable.map(async ({ message }) => { - return l2Contract.contract.populateTransaction.claimMessage( + claimable.map(async (message) => { + return l2MessageServiceContract.contract.populateTransaction.claimMessage( message.messageSender, message.destination, message.fee, @@ -112,21 +124,14 @@ export async function lineaL1ToL2Finalizer( }) ); const multicall3Call = populatedTxns.map((txn) => ({ - target: l2Contract.contractAddress, + target: l2MessageServiceContract.contractAddress, callData: txn.data, })); // Populate cross chain calls for claimable messages - const messages = claimable.flatMap(({ adapterEvent }) => { - const { name, args } = adapterEvent.parsedLog; - - if (!["TokensRelayed", "MessageRelayed"].includes(name)) { - return []; - } - + const messages = claimable.flatMap(({ messageType }) => { let crossChainCall: CrossChainMessage; - - if (name === "MessageRelayed") { + if (messageType.type === "misc") { crossChainCall = { originationChainId: l1ChainId, destinationChainId: l2ChainId, @@ -134,9 +139,8 @@ export async function lineaL1ToL2Finalizer( miscReason: "lineaClaim:relayMessage", }; } else { - const [l1Token, , amount] = args as TokensRelayedEvent["args"]; - const { decimals, symbol: l1TokenSymbol } = hubPoolClient.getTokenInfo(l1ChainId, l1Token); - const amountFromWei = convertFromWei(amount.toString(), decimals); + const { decimals, symbol: l1TokenSymbol } = hubPoolClient.getTokenInfo(l1ChainId, messageType.l1TokenAddress); + const amountFromWei = convertFromWei(messageType.amount.toString(), decimals); crossChainCall = { originationChainId: l1ChainId, destinationChainId: l2ChainId, @@ -145,7 +149,6 @@ export async function lineaL1ToL2Finalizer( type: "deposit", }; } - return crossChainCall; }); @@ -161,63 +164,3 @@ export async function lineaL1ToL2Finalizer( return { callData: multicall3Call, crossChainMessages: messages }; } - -function filterLineaTxReceipts(receipts: TransactionReceipt[], l1MessageService: L1MessageServiceContract) { - const lineaMessageSentEventTopic = l1MessageService.contract.interface.getEventTopic("MessageSent"); - const lineaTxHashes = receipts - .filter((receipt) => receipt.logs.some((log) => log.topics[0] === lineaMessageSentEventTopic)) - .map((receipt) => receipt.transactionHash); - const uniqueTxHashes = Array.from(new Set(lineaTxHashes)); - return uniqueTxHashes.map((txHash) => receipts.find((receipt) => receipt.transactionHash === txHash)); -} - -function parseAdapterEventsFromTxReceipts(receipts: TransactionReceipt[], l2ChainId: number) { - const allLogs = receipts.flatMap((receipt) => receipt.logs); - return allLogs.flatMap((log) => { - try { - const parsedLog = lineaAdapterIface.parseLog(log); - if (!parsedLog || !["TokensRelayed", "MessageRelayed"].includes(parsedLog.name)) { - return []; - } - if (parsedLog.name === "MessageRelayed" && parsedLog.args.target !== getDeployedAddress("SpokePool", l2ChainId)) { - return []; - } - if (parsedLog.name === "TokensRelayed" && parsedLog.args.to !== getDeployedAddress("SpokePool", l2ChainId)) { - return []; - } - return { parsedLog, log }; - } catch (e) { - return []; - } - }) as ParsedAdapterEvent[]; -} - -function mergeMessagesWithAdapterEvents(messages: MessageWithStatus[], adapterEvents: ParsedAdapterEvent[]) { - const messagesByTxHash = groupBy(messages, ({ txHash }) => txHash); - const adapterEventsByTxHash = groupBy(adapterEvents, ({ log }) => log.transactionHash); - - const merged: { - message: MessageWithStatus; - adapterEvent: ParsedAdapterEvent; - }[] = []; - for (const txHash of Object.keys(messagesByTxHash)) { - const messages = messagesByTxHash[txHash].sort((a, b) => a.logIndex - b.logIndex); - const adapterEvents = adapterEventsByTxHash[txHash].sort((a, b) => a.log.logIndex - b.log.logIndex); - - if (messages.length !== adapterEvents.length) { - throw new Error( - `Mismatched number of MessageSent and TokensRelayed/MessageRelayed events for transaction hash ${txHash}. ` + - `Found ${messages.length} MessageSent events and ${adapterEvents.length} TokensRelayed/MessageRelayed events.` - ); - } - - for (const [i, message] of messages.entries()) { - merged.push({ - message, - adapterEvent: adapterEvents[i], - }); - } - } - - return merged; -} From e546bb5d2d251ed279acde34b5533015239068e3 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 21 Mar 2024 11:29:46 -0400 Subject: [PATCH 11/15] Update common.ts --- src/finalizer/utils/linea/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finalizer/utils/linea/common.ts b/src/finalizer/utils/linea/common.ts index 941852891..4e3bde269 100644 --- a/src/finalizer/utils/linea/common.ts +++ b/src/finalizer/utils/linea/common.ts @@ -190,7 +190,7 @@ export async function findMessageSentEvents( ): Promise { return paginatedEventQuery( contract.contract, - (contract.contract as Contract).filters.MessageSent(l1ToL2AddressesToFinalize), + (contract.contract as Contract).filters.MessageSent(l1ToL2AddressesToFinalize, l1ToL2AddressesToFinalize), searchConfig ) as Promise; } From 33cae8cea1dd94eefd6f79c85088402d1d47ae6b Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 21 Mar 2024 11:34:45 -0400 Subject: [PATCH 12/15] improve(lineaFinalizer): Force atomic depositor and spoke pool to be included Force these contracts to be in list of finalizable origins --- src/finalizer/utils/linea/l1ToL2.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/finalizer/utils/linea/l1ToL2.ts b/src/finalizer/utils/linea/l1ToL2.ts index a14658e2d..10aa51068 100644 --- a/src/finalizer/utils/linea/l1ToL2.ts +++ b/src/finalizer/utils/linea/l1ToL2.ts @@ -20,8 +20,7 @@ export async function lineaL1ToL2Finalizer( logger: winston.Logger, signer: Signer, hubPoolClient: HubPoolClient, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _spokePoolClient: SpokePoolClient, + spokePoolClient: SpokePoolClient, l1ToL2AddressesToFinalize: string[] ): Promise { const [l1ChainId, hubPoolAddress] = [hubPoolClient.chainId, hubPoolClient.hubPool.address]; @@ -45,6 +44,17 @@ export async function lineaL1ToL2Finalizer( if (!l1ToL2AddressesToFinalize.includes(getAddress(hubPoolAddress))) { l1ToL2AddressesToFinalize.push(hubPoolAddress); } + // We always want to make sure this array contains the SpokePool address so that it finalizes HubPool messages + // with the SpokePool address as the target. + if (!l1ToL2AddressesToFinalize.includes(getAddress(spokePoolClient.spokePool.address))) { + l1ToL2AddressesToFinalize.push(spokePoolClient.spokePool.address); + } + // Always check for messages originated from atomic depositor so that this finalizer can handle l1 to l2 rebalances. + if ( + !l1ToL2AddressesToFinalize.includes(getAddress(CONTRACT_ADDRESSES[hubPoolClient.chainid].atomicDepositor.address)) + ) { + l1ToL2AddressesToFinalize.push(CONTRACT_ADDRESSES[hubPoolClient.chainid].atomicDepositor.address); + } // Optimize block range for querying Linea's MessageSent events on L1. // We want to conservatively query for events that are between 0 and 24 hours old From 42ed27462d410d6dd8ff0415fc22a8dfb3543a8c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 21 Mar 2024 11:47:41 -0400 Subject: [PATCH 13/15] fix --- src/finalizer/index.ts | 24 ++++++++++++++++++++++++ src/finalizer/utils/linea/l1ToL2.ts | 23 +++-------------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index c8c1fca53..70b2d91a4 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -5,6 +5,7 @@ import { getAddress } from "ethers/lib/utils"; import { groupBy, uniq } from "lodash"; import { AugmentedTransaction, HubPoolClient, MultiCallerClient, TransactionClient } from "../clients"; import { + CONTRACT_ADDRESSES, Clients, FINALIZER_TOKENBRIDGE_LOOKBACK, Multicall2Call, @@ -16,6 +17,7 @@ import { import { DataworkerConfig } from "../dataworker/DataworkerConfig"; import { SpokePoolClientsByChain } from "../interfaces"; import { + CHAIN_IDs, Signer, blockExplorerLink, config, @@ -72,6 +74,17 @@ const chainFinalizerOverrides: { [chainId: number]: ChainFinalizer[] } = { 59140: [lineaL2ToL1Finalizer], }; +function enrichL1ToL2AddressesToFinalize(l1ToL2AddressesToFinalize: string[], addressesToEnsure: string[]): string[] { + const resultingAddresses = l1ToL2AddressesToFinalize.slice().map(getAddress); + for (const address of addressesToEnsure) { + const checksummedAddress = getAddress(address); + if (!resultingAddresses.includes(checksummedAddress)) { + resultingAddresses.push(checksummedAddress); + } + } + return resultingAddresses; +} + export async function finalize( logger: winston.Logger, hubSigner: Signer, @@ -109,6 +122,17 @@ export async function finalize( const network = getNetworkName(chainId); + // For certain chains we always want to track certain addresses for finalization: + // LineaL1ToL2: Always track HubPool, AtomicDepositor, LineaSpokePool. HubPool sends messages and tokens to the + // SpokePool, while the relayer rebalances ETH via the AtomicDepositor + if (chainId === hubChainId) { + l1ToL2AddressesToFinalize = enrichL1ToL2AddressesToFinalize(l1ToL2AddressesToFinalize, [ + hubPoolClient.hubPool.address, + spokePoolClients[CHAIN_IDs.LINEA].spokePool.address, + CONTRACT_ADDRESSES[hubChainId].atomicDepositor.address, + ]); + } + // We can subloop through the finalizers for each chain, and then execute the finalizer. For now, the // main reason for this is related to CCTP finalizations. We want to run the CCTP finalizer AND the // normal finalizer for each chain. This is going to cause an overlap of finalization attempts on USDC. diff --git a/src/finalizer/utils/linea/l1ToL2.ts b/src/finalizer/utils/linea/l1ToL2.ts index 10aa51068..c2575ccc7 100644 --- a/src/finalizer/utils/linea/l1ToL2.ts +++ b/src/finalizer/utils/linea/l1ToL2.ts @@ -1,7 +1,6 @@ import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import { OnChainMessageStatus } from "@consensys/linea-sdk"; import { Contract } from "ethers"; -import { getAddress } from "ethers/lib/utils"; import { groupBy } from "lodash"; import { HubPoolClient, SpokePoolClient } from "../../../clients"; import { CHAIN_MAX_BLOCK_LOOKBACK, CONTRACT_ADDRESSES } from "../../../common"; @@ -20,10 +19,11 @@ export async function lineaL1ToL2Finalizer( logger: winston.Logger, signer: Signer, hubPoolClient: HubPoolClient, - spokePoolClient: SpokePoolClient, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _spokePoolClient: SpokePoolClient, l1ToL2AddressesToFinalize: string[] ): Promise { - const [l1ChainId, hubPoolAddress] = [hubPoolClient.chainId, hubPoolClient.hubPool.address]; + const [l1ChainId] = [hubPoolClient.chainId, hubPoolClient.hubPool.address]; const l2ChainId = l1ChainId === 1 ? 59144 : 59140; const lineaSdk = initLineaSdk(l1ChainId, l2ChainId); const l2MessageServiceContract = lineaSdk.getL2Contract(); @@ -39,23 +39,6 @@ export async function lineaL1ToL2Finalizer( hubPoolClient.hubPool.provider ); - // We always want to make sure that the l1ToL2AddressesToFinalize array contains - // the HubPool address, so we can finalize any pending messages sent from the HubPool. - if (!l1ToL2AddressesToFinalize.includes(getAddress(hubPoolAddress))) { - l1ToL2AddressesToFinalize.push(hubPoolAddress); - } - // We always want to make sure this array contains the SpokePool address so that it finalizes HubPool messages - // with the SpokePool address as the target. - if (!l1ToL2AddressesToFinalize.includes(getAddress(spokePoolClient.spokePool.address))) { - l1ToL2AddressesToFinalize.push(spokePoolClient.spokePool.address); - } - // Always check for messages originated from atomic depositor so that this finalizer can handle l1 to l2 rebalances. - if ( - !l1ToL2AddressesToFinalize.includes(getAddress(CONTRACT_ADDRESSES[hubPoolClient.chainid].atomicDepositor.address)) - ) { - l1ToL2AddressesToFinalize.push(CONTRACT_ADDRESSES[hubPoolClient.chainid].atomicDepositor.address); - } - // Optimize block range for querying Linea's MessageSent events on L1. // We want to conservatively query for events that are between 0 and 24 hours old // because Linea L1->L2 messages are claimable after ~20 mins. From b57b906f994e60f2dbf6d436bab28f63e38daf57 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 21 Mar 2024 11:56:33 -0400 Subject: [PATCH 14/15] Update index.ts --- src/finalizer/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index 70b2d91a4..a3f31a933 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -126,10 +126,16 @@ export async function finalize( // LineaL1ToL2: Always track HubPool, AtomicDepositor, LineaSpokePool. HubPool sends messages and tokens to the // SpokePool, while the relayer rebalances ETH via the AtomicDepositor if (chainId === hubChainId) { + if (chainId !== CHAIN_IDs.MAINNET) { + logger.warn({ + at: "Finalizer", + message: "Testnet Finalizer: skipping finalizations where from or to address is set to AtomicDepositor", + }); + } l1ToL2AddressesToFinalize = enrichL1ToL2AddressesToFinalize(l1ToL2AddressesToFinalize, [ hubPoolClient.hubPool.address, - spokePoolClients[CHAIN_IDs.LINEA].spokePool.address, - CONTRACT_ADDRESSES[hubChainId].atomicDepositor.address, + spokePoolClients[hubChainId === CHAIN_IDs.MAINNET ? CHAIN_IDs.LINEA : CHAIN_IDs.LINEA_GOERLI].spokePool.address, + hubChainId === CHAIN_IDs.MAINNET ? CONTRACT_ADDRESSES[hubChainId].atomicDepositor.address : undefined, ]); } From 638616cda789f2fee3269f25d8dff3056f218811 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Thu, 21 Mar 2024 12:18:51 -0400 Subject: [PATCH 15/15] Update src/finalizer/index.ts Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> --- src/finalizer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index a3f31a933..3dcacbae8 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -135,7 +135,7 @@ export async function finalize( l1ToL2AddressesToFinalize = enrichL1ToL2AddressesToFinalize(l1ToL2AddressesToFinalize, [ hubPoolClient.hubPool.address, spokePoolClients[hubChainId === CHAIN_IDs.MAINNET ? CHAIN_IDs.LINEA : CHAIN_IDs.LINEA_GOERLI].spokePool.address, - hubChainId === CHAIN_IDs.MAINNET ? CONTRACT_ADDRESSES[hubChainId].atomicDepositor.address : undefined, + CONTRACT_ADDRESSES[hubChainId]?.atomicDepositor?.address, ]); }