diff --git a/package.json b/package.json index 37923b31b..57a700e1b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@across-protocol/contracts-v2": "2.4.3", + "@across-protocol/constants-v2": "1.0.4", "@across-protocol/sdk-v2": "0.16.4", "@arbitrum/sdk": "^3.1.3", "@defi-wonderland/smock": "^2.3.5", diff --git a/src/clients/AcrossAPIClient.ts b/src/clients/AcrossAPIClient.ts index ca9c8e9df..c202d2449 100644 --- a/src/clients/AcrossAPIClient.ts +++ b/src/clients/AcrossAPIClient.ts @@ -1,10 +1,9 @@ import { winston, BigNumber, getL2TokenAddresses } from "../utils"; import axios, { AxiosError } from "axios"; import { HubPoolClient } from "./HubPoolClient"; -import { constants } from "@across-protocol/sdk-v2"; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2"; import { SpokePoolClientsByChain } from "../interfaces"; import _ from "lodash"; -const { TOKEN_SYMBOLS_MAP, CHAIN_IDs } = constants; export interface DepositLimits { maxDeposit: BigNumber; diff --git a/src/clients/ProfitClient.ts b/src/clients/ProfitClient.ts index e5eba34b2..6ba204268 100644 --- a/src/clients/ProfitClient.ts +++ b/src/clients/ProfitClient.ts @@ -4,10 +4,10 @@ import * as constants from "../common/Constants"; import { assert, BigNumber, formatFeePct, max, winston, toBNWei, toBN, assign } from "../utils"; import { HubPoolClient } from "."; import { Deposit, DepositWithBlock, L1Token, SpokePoolClientsByChain } from "../interfaces"; -import { constants as sdkConstants, priceClient, relayFeeCalculator, utils as sdkUtils } from "@across-protocol/sdk-v2"; +import { priceClient, relayFeeCalculator, utils as sdkUtils } from "@across-protocol/sdk-v2"; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2"; const { formatEther } = ethersUtils; -const { TOKEN_SYMBOLS_MAP, CHAIN_IDs } = sdkConstants; const { fixedPointAdjustment: fixedPoint } = sdkUtils; // We use wrapped ERC-20 versions instead of the native tokens such as ETH, MATIC for ease of computing prices. diff --git a/src/clients/bridges/ArbitrumAdapter.ts b/src/clients/bridges/ArbitrumAdapter.ts index 823a701a0..4851e882b 100644 --- a/src/clients/bridges/ArbitrumAdapter.ts +++ b/src/clients/bridges/ArbitrumAdapter.ts @@ -17,9 +17,8 @@ import { import { SpokePoolClient } from "../../clients"; import { BaseAdapter } from "./BaseAdapter"; import { SortableEvent, OutstandingTransfers } from "../../interfaces"; -import { constants } from "@across-protocol/sdk-v2"; import { CONTRACT_ADDRESSES } from "../../common"; -const { TOKEN_SYMBOLS_MAP, CHAIN_IDs } = constants; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2"; // TODO: Move to ../../common/ContractAddresses.ts // These values are obtained from Arbitrum's gateway router contract. diff --git a/src/clients/bridges/PolygonAdapter.ts b/src/clients/bridges/PolygonAdapter.ts index 9f05ef725..f5ba05877 100644 --- a/src/clients/bridges/PolygonAdapter.ts +++ b/src/clients/bridges/PolygonAdapter.ts @@ -16,9 +16,8 @@ import { import { SpokePoolClient } from "../../clients"; import { BaseAdapter } from "./"; import { SortableEvent, OutstandingTransfers } from "../../interfaces"; -import { constants } from "@across-protocol/sdk-v2"; import { CONTRACT_ADDRESSES } from "../../common"; -const { TOKEN_SYMBOLS_MAP, CHAIN_IDs } = constants; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2"; // ether bridge = 0x8484Ef722627bf18ca5Ae6BcF031c23E6e922B30 // erc20 bridge = 0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index 7b931797c..a3ef51e88 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -1,5 +1,6 @@ import assert from "assert"; import { typeguards, utils as sdkUtils } from "@across-protocol/sdk-v2"; +import { providers } from "ethers"; import { groupBy } from "lodash"; import { Wallet, @@ -27,7 +28,11 @@ import { FINALIZER_TOKENBRIDGE_LOOKBACK, Multicall2Call, } from "../common"; -import { ChainFinalizer, Withdrawal } from "./types"; +import { ChainFinalizer, Withdrawal as _Withdrawal } from "./types"; + +type TransactionReceipt = providers.TransactionReceipt; + +type Withdrawal = _Withdrawal & { txns: Multicall2Call[] }; const { isError, isEthersError } = typeguards; @@ -70,10 +75,7 @@ export async function finalize( // Note: Could move this into a client in the future to manage # of calls and chunk calls based on // input byte length. const multicall2 = getMultisender(hubChainId, hubSigner); - const finalizationsToBatch: { - callData: Multicall2Call; - withdrawal: Withdrawal; - }[] = []; + const finalizationsToBatch: Withdrawal[] = []; // For each chain, delegate to a handler to look up any TokensBridged events and attempt finalization. for (const chainId of configuredChainIds) { @@ -100,34 +102,59 @@ export async function finalize( const network = getNetworkName(chainId); logger.debug({ at: "finalize", message: `Spawning ${network} finalizer.`, latestBlockToFinalize }); - const { callData, withdrawals } = await chainFinalizer( + const { callData: txns, withdrawals: _withdrawals } = await chainFinalizer( logger, hubSigner, hubPoolClient, client, latestBlockToFinalize ); - logger.debug({ at: "finalize", message: `Found ${callData.length} ${network} withdrawals for finalization.` }); + logger.debug({ + at: "finalize", + message: `Found ${_withdrawals.length} ${network} withdrawals for finalization.`, + }); - const txns = callData.map((callData, i) => { - return { callData, withdrawal: withdrawals[i] }; + if (_withdrawals.length === 0) { + continue; + } + + if (![1, 2].includes(txns.length / _withdrawals.length)) { + logger.warn({ + at: "finalize", + message: `Unexpected ${network} txn/withdrawal ratio (${txns.length / _withdrawals.length}).`, + txns, + withdrawals: _withdrawals, + }); + continue; + } + + // Normalise withdawals, such that 1 withdrawal has an array of calldata (usually only 1 call), but can be more. + // @todo: Refactor the underlying adapters so they return in this data structure. + const withdrawals: Withdrawal[] = _withdrawals.map((withdrawal) => { + return { ...withdrawal, txns: [] }; }); - finalizationsToBatch.push(...txns); + // Append calldata. If multiple calls are needed per withdrawal (i.e. Polygon), + // require that the 2nd batch is appended to the first. + txns.forEach((txn, i) => withdrawals[i % withdrawals.length].txns.push(txn)); + + finalizationsToBatch.push(...withdrawals); } // Ensure each transaction would succeed in isolation. - const finalizations = await sdkUtils.filterAsync(finalizationsToBatch, async (finalization) => { + const finalizations = await sdkUtils.filterAsync(finalizationsToBatch, async (withdrawal) => { + const { txns } = withdrawal; try { - const { target: to, callData: data } = finalization.callData; - await multicall2.provider.estimateGas({ to, data }); + const txn = await multicall2.populateTransaction.aggregate(txns); + await multicall2.provider.estimateGas(txn); return true; } catch (err) { - const { l2ChainId, type, l1TokenSymbol, amount } = finalization.withdrawal; + const { l2ChainId, type, l1TokenSymbol, amount } = withdrawal; const network = getNetworkName(l2ChainId); logger.info({ at: "finalizer", message: `Failed to estimate gas for ${network} ${amount} ${l1TokenSymbol} ${type}.`, + txns, reason: isEthersError(err) ? err.reason : isError(err) ? err.message : "unknown error", }); return false; @@ -135,31 +162,11 @@ export async function finalize( }); if (finalizations.length > 0) { + let txn: TransactionReceipt; try { // Note: If the sum of finalizations approaches the gas limit, consider slicing them up. - const callData = finalizations.map(({ callData }) => callData); - const txn = await (await multicall2.aggregate(callData)).wait(); - - const { withdrawals = [], proofs = [] } = groupBy( - finalizations.map(({ withdrawal }) => withdrawal), - ({ type }) => (type === "withdrawal" ? "withdrawals" : "proofs") - ); - proofs.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => { - const spokeChain = getNetworkName(l2ChainId); - logger.info({ - at: "Finalizer", - message: `Submitted proof on chain ${hubChain} to initiate ${spokeChain} withdrawal of ${amount} ${symbol} 🔜`, - transactionHash: blockExplorerLink(txn.transactionHash, hubChainId), - }); - }); - withdrawals.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => { - const spokeChain = getNetworkName(l2ChainId); - logger.info({ - at: "Finalizer", - message: `Finalized ${spokeChain} withdrawal for ${amount} ${symbol} 🪃`, - transactionHash: blockExplorerLink(txn.transactionHash, hubChainId), - }); - }); + const txns = finalizations.map(({ txns }) => txns).flat(); + txn = await (await multicall2.aggregate(txns)).wait(); } catch (_error) { const error = _error as Error; logger.warn({ @@ -167,8 +174,31 @@ export async function finalize( message: "Error creating aggregateTx", reason: error.stack || error.message || error.toString(), notificationPath: "across-error", + finalizations, }); + + return; } + + const { withdrawals = [], proofs = [] } = groupBy(finalizations, ({ type }) => + type === "withdrawal" ? "withdrawals" : "proofs" + ); + proofs.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => { + const spokeChain = getNetworkName(l2ChainId); + logger.info({ + at: "Finalizer", + message: `Submitted proof on chain ${hubChain} to initiate ${spokeChain} withdrawal of ${amount} ${symbol} 🔜`, + transactionHash: blockExplorerLink(txn.transactionHash, hubChainId), + }); + }); + withdrawals.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => { + const spokeChain = getNetworkName(l2ChainId); + logger.info({ + at: "Finalizer", + message: `Finalized ${spokeChain} withdrawal for ${amount} ${symbol} 🪃`, + transactionHash: blockExplorerLink(txn.transactionHash, hubChainId), + }); + }); } } diff --git a/src/utils/TokenUtils.ts b/src/utils/TokenUtils.ts index 1d2f8bbab..230827867 100644 --- a/src/utils/TokenUtils.ts +++ b/src/utils/TokenUtils.ts @@ -1,7 +1,8 @@ import { constants, utils } from "@across-protocol/sdk-v2"; import { CONTRACT_ADDRESSES } from "../common"; import { BigNumberish, utils as ethersUtils } from "ethers"; -const { TOKEN_SYMBOLS_MAP, CHAIN_IDs, ZERO_ADDRESS } = constants; +const { ZERO_ADDRESS } = constants; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2"; export const { fetchTokenInfo } = utils; diff --git a/test/AdapterManager.SendTokensCrossChain.ts b/test/AdapterManager.SendTokensCrossChain.ts index 5b27cfe28..153505794 100644 --- a/test/AdapterManager.SendTokensCrossChain.ts +++ b/test/AdapterManager.SendTokensCrossChain.ts @@ -1,4 +1,3 @@ -import { constants } from "@across-protocol/sdk-v2"; import * as zksync from "zksync-web3"; import { SpokePoolClient } from "../src/clients"; import { AdapterManager } from "../src/clients/bridges"; // Tested @@ -18,7 +17,7 @@ import { toBN, winston, } from "./utils"; -const { TOKEN_SYMBOLS_MAP, CHAIN_IDs } = constants; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2"; let hubPoolClient: MockHubPoolClient; const mockSpokePoolClients: { diff --git a/yarn.lock b/yarn.lock index a2568d0f5..5aef267a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,6 +11,11 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" +"@across-protocol/constants-v2@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@across-protocol/constants-v2/-/constants-v2-1.0.4.tgz#df31c81038982a25de2b1b8f7604875f3de1186c" + integrity sha512-Nzl8Z1rZFvcpuKQu7CmBVfvgB13/NoulcsRVYBSkG90imS/e6mugxzqD9UrUb+WOL0ODMCANCAoDw54ZBBzNiQ== + "@across-protocol/contracts-v2@2.4.3", "@across-protocol/contracts-v2@^2.4.3": version "2.4.3" resolved "https://registry.yarnpkg.com/@across-protocol/contracts-v2/-/contracts-v2-2.4.3.tgz#9cc0b1f52b4f819b32ca1524ef84af9dfed8687a"