diff --git a/package.json b/package.json index aa064cf906..08979f9ac2 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "node": ">=20" }, "dependencies": { - "@across-protocol/constants": "^3.1.25", - "@across-protocol/contracts": "^3.0.19", - "@across-protocol/sdk": "^3.3.32", + "@across-protocol/constants": "^3.1.27", + "@across-protocol/contracts": "^3.0.20", + "@across-protocol/sdk": "^3.4.9", "@arbitrum/sdk": "^4.0.2", "@consensys/linea-sdk": "^0.2.1", "@defi-wonderland/smock": "^2.3.5", diff --git a/scripts/zkSyncDemo.ts b/scripts/zkSyncDemo.ts index 34d5aec6c3..5edb3e5c79 100644 --- a/scripts/zkSyncDemo.ts +++ b/scripts/zkSyncDemo.ts @@ -62,7 +62,7 @@ export async function run(): Promise { connectedSigner ); const l2PubdataByteLimit = zksync.utils.REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT; - const l1GasPriceData = await gasPriceOracle.getGasPriceEstimate(l1Provider, l1ChainId); + const l1GasPriceData = await gasPriceOracle.getGasPriceEstimate(l1Provider, { chainId: l1ChainId }); const estimatedL1GasPrice = l1GasPriceData.maxPriorityFeePerGas.add(l1GasPriceData.maxFeePerGas); // The ZkSync Mailbox contract checks that the msg.value of the transaction is enough to cover the transaction base // cost. The transaction base cost can be queried from the Mailbox by passing in an L1 "executed" gas price, diff --git a/src/adapter/AdapterManager.ts b/src/adapter/AdapterManager.ts deleted file mode 100644 index d345c62af2..0000000000 --- a/src/adapter/AdapterManager.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { winston, TOKEN_SYMBOLS_MAP } from "../utils"; -import { SpokePoolClient, HubPoolClient } from "../clients"; -import { BaseChainAdapter } from "./"; -import { SUPPORTED_TOKENS, CUSTOM_BRIDGE, CANONICAL_BRIDGE, DEFAULT_GAS_MULTIPLIER } from "../common"; - -import { AdapterManager } from "../clients/bridges"; - -export class GenericAdapterManager extends AdapterManager { - constructor( - readonly logger: winston.Logger, - readonly spokePoolClients: { [chainId: number]: SpokePoolClient }, - readonly hubPoolClient: HubPoolClient, - readonly monitoredAddresses: string[] - ) { - super(logger, spokePoolClients, hubPoolClient, monitoredAddresses); - if (!spokePoolClients) { - return; - } - const spokePoolAddresses = Object.values(spokePoolClients).map((client) => client.spokePool.address); - const hubChainId = this.hubPoolClient.chainId; - - // The adapters are only set up to monitor EOA's and the HubPool and SpokePool address, so remove - // spoke pool addresses from other chains. - const filterMonitoredAddresses = (chainId: number) => { - return monitoredAddresses.filter( - (address) => - this.hubPoolClient.hubPool.address === address || - this.spokePoolClients[chainId].spokePool.address === address || - !spokePoolAddresses.includes(address) - ); - }; - - const l1Signer = spokePoolClients[hubChainId].spokePool.signer; - - Object.values(this.spokePoolClients) - .filter(({ chainId }) => chainId !== hubChainId) - .map(({ chainId }) => { - // First, fetch all the bridges associated with the chain. - const bridges = {}; - const l2Signer = spokePoolClients[chainId].spokePool.signer; - - SUPPORTED_TOKENS[chainId]?.map((symbol) => { - const l1Token = TOKEN_SYMBOLS_MAP[symbol].addresses[hubChainId]; - const bridgeConstructor = CUSTOM_BRIDGE[chainId]?.[l1Token] ?? CANONICAL_BRIDGE[chainId]; - - bridges[l1Token] = new bridgeConstructor(chainId, hubChainId, l1Signer, l2Signer, l1Token); - }); - - // Then instantiate a generic adapter. - this.adapters[chainId] = new BaseChainAdapter( - spokePoolClients, - chainId, - hubChainId, - filterMonitoredAddresses(chainId), - logger, - SUPPORTED_TOKENS[chainId], - bridges, - DEFAULT_GAS_MULTIPLIER[chainId] ?? 1 - ); - }); - - logger.debug({ - at: "AdapterManager#constructor", - message: "Initialized experimental AdapterManager for generic adapters. Ensure this was intended!", - adapterChains: Object.keys(this.adapters).map((chainId) => chainId), - }); - } -} diff --git a/src/adapter/bridges/ArbitrumOrbitBridge.ts b/src/adapter/bridges/ArbitrumOrbitBridge.ts index 89d045a165..f0a510bbcc 100644 --- a/src/adapter/bridges/ArbitrumOrbitBridge.ts +++ b/src/adapter/bridges/ArbitrumOrbitBridge.ts @@ -16,7 +16,7 @@ import { import { CONTRACT_ADDRESSES, CUSTOM_ARBITRUM_GATEWAYS, DEFAULT_ARBITRUM_GATEWAY } from "../../common"; import { BridgeTransactionDetails, BaseBridgeAdapter, BridgeEvents } from "./BaseBridgeAdapter"; import { processEvent } from "../utils"; -import { PRODUCTION_NETWORKS } from "@across-protocol/constants"; +import { PUBLIC_NETWORKS } from "@across-protocol/constants"; const bridgeSubmitValue: { [chainId: number]: BigNumber } = { [CHAIN_IDs.ARBITRUM]: toWei(0.013), @@ -53,7 +53,7 @@ export class ArbitrumOrbitBridge extends BaseBridgeAdapter { super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [l1Address]); - const nativeToken = PRODUCTION_NETWORKS[l2chainId].nativeToken; + const nativeToken = PUBLIC_NETWORKS[l2chainId].nativeToken; // Only set nonstandard gas tokens. if (nativeToken !== "ETH") { this.gasToken = TOKEN_SYMBOLS_MAP[nativeToken].addresses[hubChainId]; diff --git a/src/clients/MultiCallerClient.ts b/src/clients/MultiCallerClient.ts index 851fbe3cc9..93956232b2 100644 --- a/src/clients/MultiCallerClient.ts +++ b/src/clients/MultiCallerClient.ts @@ -79,7 +79,7 @@ export interface TryMulticallTransaction { export class MultiCallerClient { protected txnClient: TransactionClient; protected txns: { [chainId: number]: AugmentedTransaction[] } = {}; - protected valueTxns: { [chainId: number]: AugmentedTransaction[] } = {}; + protected nonMulticallTxns: { [chainId: number]: AugmentedTransaction[] } = {}; constructor( readonly logger: winston.Logger, readonly chunkSize: { [chainId: number]: number } = {}, @@ -90,8 +90,8 @@ export class MultiCallerClient { getQueuedTransactions(chainId: number): AugmentedTransaction[] { const allTxns = []; - if (this.valueTxns?.[chainId]) { - allTxns.push(...this.valueTxns[chainId]); + if (this.nonMulticallTxns?.[chainId]) { + allTxns.push(...this.nonMulticallTxns[chainId]); } if (this.txns?.[chainId]) { allTxns.push(...this.txns[chainId]); @@ -101,8 +101,8 @@ export class MultiCallerClient { // Adds all information associated with a transaction to the transaction queue. enqueueTransaction(txn: AugmentedTransaction): void { - // Value transactions are sorted immediately because the UMA multicall implementation rejects them. - const txnQueue = txn.value && txn.value.gt(0) ? this.valueTxns : this.txns; + // We do not attempt to batch together transactions that have value or are explicitly nonMulticall. + const txnQueue = (txn.value && txn.value.gt(0)) || txn.nonMulticall ? this.nonMulticallTxns : this.txns; if (txnQueue[txn.chainId] === undefined) { txnQueue[txn.chainId] = []; } @@ -111,17 +111,17 @@ export class MultiCallerClient { transactionCount(): number { return Object.values(this.txns) - .concat(Object.values(this.valueTxns)) + .concat(Object.values(this.nonMulticallTxns)) .reduce((count, txnQueue) => (count += txnQueue.length), 0); } clearTransactionQueue(chainId: number | null = null): void { if (chainId !== null) { this.txns[chainId] = []; - this.valueTxns[chainId] = []; + this.nonMulticallTxns[chainId] = []; } else { this.txns = {}; - this.valueTxns = {}; + this.nonMulticallTxns = {}; } } @@ -129,7 +129,7 @@ export class MultiCallerClient { async executeTxnQueues(simulate = false, chainIds: number[] = []): Promise> { if (chainIds.length === 0) { chainIds = sdkUtils.dedupArray([ - ...Object.keys(this.valueTxns).map(Number), + ...Object.keys(this.nonMulticallTxns).map(Number), ...Object.keys(this.txns).map(Number), ]); } @@ -174,9 +174,9 @@ export class MultiCallerClient { // For a single chain, take any enqueued transactions and attempt to execute them. async executeTxnQueue(chainId: number, simulate = false): Promise { const txns: AugmentedTransaction[] | undefined = this.txns[chainId]; - const valueTxns: AugmentedTransaction[] | undefined = this.valueTxns[chainId]; + const nonMulticallTxns: AugmentedTransaction[] | undefined = this.nonMulticallTxns[chainId]; this.clearTransactionQueue(chainId); - return this._executeTxnQueue(chainId, txns, valueTxns, simulate); + return this._executeTxnQueue(chainId, txns, nonMulticallTxns, simulate); } // For a single chain, simulate all potential multicall txns and group the ones that pass into multicall bundles. @@ -185,10 +185,10 @@ export class MultiCallerClient { protected async _executeTxnQueue( chainId: number, txns: AugmentedTransaction[] = [], - valueTxns: AugmentedTransaction[] = [], + nonMulticallTxns: AugmentedTransaction[] = [], simulate = false ): Promise { - const nTxns = txns.length + valueTxns.length; + const nTxns = txns.length + nonMulticallTxns.length; if (nTxns === 0) { return []; } @@ -204,7 +204,7 @@ export class MultiCallerClient { // First try to simulate the transaction as a batch. If the full batch succeeded, then we don't // need to simulate transactions individually. If the batch failed, then we need to // simulate the transactions individually and pick out the successful ones. - const batchTxns: AugmentedTransaction[] = valueTxns.concat( + const batchTxns: AugmentedTransaction[] = nonMulticallTxns.concat( await this.buildMultiCallBundles(txns, this.chunkSize[chainId]) ); const batchSimResults = await this.txnClient.simulate(batchTxns); @@ -227,7 +227,7 @@ export class MultiCallerClient { } else { const individualTxnSimResults = await Promise.allSettled([ this.simulateTransactionQueue(txns), - this.simulateTransactionQueue(valueTxns), + this.simulateTransactionQueue(nonMulticallTxns), ]); const [_txns, _valueTxns] = individualTxnSimResults.map((result): AugmentedTransaction[] => { return isPromiseFulfilled(result) ? result.value : []; diff --git a/src/clients/ProfitClient.ts b/src/clients/ProfitClient.ts index 33e66197b0..41df0ff4b5 100644 --- a/src/clients/ProfitClient.ts +++ b/src/clients/ProfitClient.ts @@ -29,6 +29,7 @@ import { TOKEN_EQUIVALENCE_REMAPPING, ZERO_ADDRESS, formatGwei, + fixedPointAdjustment, } from "../utils"; import { Deposit, DepositWithBlock, L1Token, SpokePoolClientsByChain } from "../interfaces"; import { getAcrossHost } from "./AcrossAPIClient"; @@ -213,7 +214,7 @@ export class ProfitClient { message: "Failed to simulate fill for deposit.", reason, deposit, - notificationPath: "across-unprofitable-fills", + notificationPath: "across-warn", }); return { nativeGasCost: uint256Max, tokenGasCost: uint256Max, gasPrice: uint256Max }; } @@ -543,7 +544,12 @@ export class ProfitClient { .filter(({ symbol }) => isDefined(TOKEN_SYMBOLS_MAP[symbol])) .map(({ symbol }) => { const { addresses } = TOKEN_SYMBOLS_MAP[symbol]; - const address = addresses[CHAIN_IDs.MAINNET]; + let address = addresses[CHAIN_IDs.MAINNET]; + // For testnet only, if we cannot resolve the token address, revert to ETH. On mainnet, if `address` is undefined, + // we will throw an error instead. + if (this.hubPoolClient.chainId === CHAIN_IDs.SEPOLIA && !isDefined(address)) { + address = TOKEN_SYMBOLS_MAP.ETH.addresses[CHAIN_IDs.MAINNET]; + } return [symbol, address]; }) ); @@ -566,7 +572,12 @@ export class ProfitClient { // Also ensure all gas tokens are included in the lookup. this.enabledChainIds.forEach((chainId) => { const symbol = getNativeTokenSymbol(chainId); - tokens[symbol] ??= TOKEN_SYMBOLS_MAP[symbol].addresses[CHAIN_IDs.MAINNET]; + let nativeTokenAddress = TOKEN_SYMBOLS_MAP[symbol].addresses[CHAIN_IDs.MAINNET]; + // For testnet only, if the custom gas token has no mainnet address, use ETH. + if (this.hubPoolClient.chainId === CHAIN_IDs.SEPOLIA && !isDefined(nativeTokenAddress)) { + nativeTokenAddress = TOKEN_SYMBOLS_MAP["ETH"].addresses[CHAIN_IDs.MAINNET]; + } + tokens[symbol] ??= nativeTokenAddress; }); this.logger.debug({ at: "ProfitClient", message: "Updating Profit client", tokens }); @@ -609,6 +620,7 @@ export class ProfitClient { [CHAIN_IDs.REDSTONE]: "WETH", // Redstone only supports WETH. [CHAIN_IDs.WORLD_CHAIN]: "WETH", // USDC deferred on World Chain. [CHAIN_IDs.INK]: "WETH", // USDC deferred on Ink. + [CHAIN_IDs.LENS_SEPOLIA]: "WETH", // No USD token on Lens Sepolia }; const prodRelayer = process.env.RELAYER_FILL_SIMULATION_ADDRESS ?? PROD_RELAYER; const [defaultTestSymbol, relayer] = @@ -636,24 +648,49 @@ export class ProfitClient { }; // Pre-fetch total gas costs for relays on enabled chains. - await sdkUtils.mapAsync(enabledChainIds, async (destinationChainId) => { - const symbol = testSymbols[destinationChainId] ?? defaultTestSymbol; - const hubToken = TOKEN_SYMBOLS_MAP[symbol].addresses[this.hubPoolClient.chainId]; - const outputToken = - destinationChainId === hubPoolClient.chainId - ? hubToken - : hubPoolClient.getL2TokenForL1TokenAtBlock(hubToken, destinationChainId); - assert(isDefined(outputToken), `Chain ${destinationChainId} SpokePool is not configured for ${symbol}`); - - const deposit = { ...sampleDeposit, destinationChainId, outputToken }; - this.totalGasCosts[destinationChainId] = await this._getTotalGasCost(deposit, relayer); - }); + const totalGasCostsToLog = Object.fromEntries( + await sdkUtils.mapAsync(enabledChainIds, async (destinationChainId) => { + const symbol = testSymbols[destinationChainId] ?? defaultTestSymbol; + const hubToken = TOKEN_SYMBOLS_MAP[symbol].addresses[this.hubPoolClient.chainId]; + const outputToken = + destinationChainId === hubPoolClient.chainId + ? hubToken + : hubPoolClient.getL2TokenForL1TokenAtBlock(hubToken, destinationChainId); + assert(isDefined(outputToken), `Chain ${destinationChainId} SpokePool is not configured for ${symbol}`); + + const deposit = { ...sampleDeposit, destinationChainId, outputToken }; + const gasCosts = await this._getTotalGasCost(deposit, relayer); + // The scaledNativeGasCost is approximately what the relayer will set as the `gasLimit` when submitting + // fills on the destination chain. + const scaledNativeGasCost = gasCosts.nativeGasCost.mul(this.gasPadding).div(fixedPointAdjustment); + // The scaledTokenGasCost is the estimated gas cost of submitting a fill on the destination chain and is used + // in the this.estimateFillCost function to determine whether a deposit is profitable to fill. Therefore, + // the scaledTokenGasCost should be safely lower than the quote API's tokenGasCosts in order for the relayer + // to consider a deposit is profitable. + const scaledTokenGasCost = gasCosts.tokenGasCost + .mul(this.gasPadding) + .div(fixedPointAdjustment) + .mul(this.gasMultiplier) + .div(fixedPointAdjustment); + this.totalGasCosts[destinationChainId] = gasCosts; + return [ + destinationChainId, + { + ...gasCosts, + scaledNativeGasCost, + scaledTokenGasCost, + gasPadding: formatEther(this.gasPadding), + gasMultiplier: formatEther(this.gasMultiplier), + }, + ]; + }) + ); this.logger.debug({ at: "ProfitClient", message: "Updated gas cost", enabledChainIds: this.enabledChainIds, - totalGasCosts: this.totalGasCosts, + totalGasCosts: totalGasCostsToLog, }); } diff --git a/src/clients/TransactionClient.ts b/src/clients/TransactionClient.ts index 3c8aacc866..acfa06bcf8 100644 --- a/src/clients/TransactionClient.ts +++ b/src/clients/TransactionClient.ts @@ -29,6 +29,9 @@ export interface AugmentedTransaction { canFailInSimulation?: boolean; // Optional batch ID to use to group transactions groupId?: string; + // If true, the transaction is being sent to a non Multicall contract so we can't batch it together + // with other transactions. + nonMulticall?: boolean; } const { fixedPointAdjustment: fixedPoint } = sdkUtils; diff --git a/src/clients/bridges/AdapterManager.ts b/src/clients/bridges/AdapterManager.ts index 0e828d8baf..8bc6854ce6 100644 --- a/src/clients/bridges/AdapterManager.ts +++ b/src/clients/bridges/AdapterManager.ts @@ -9,11 +9,8 @@ import { import { InventoryConfig, OutstandingTransfers } from "../../interfaces"; import { BigNumber, isDefined, winston, Signer, getL2TokenAddresses, TransactionResponse, assert } from "../../utils"; import { SpokePoolClient, HubPoolClient } from "../"; -import { ArbitrumAdapter, PolygonAdapter, ZKSyncAdapter, LineaAdapter, ScrollAdapter } from "./"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; - import { BaseChainAdapter } from "../../adapter"; -import { EthereumAdapter } from "./EthereumAdapter"; export class AdapterManager { public adapters: { [chainId: number]: BaseChainAdapter } = {}; @@ -46,172 +43,37 @@ export class AdapterManager { ); }; - const { - OPTIMISM, - ARBITRUM, - POLYGON, - ZK_SYNC, - BASE, - MODE, - LINEA, - LISK, - BLAST, - REDSTONE, - SCROLL, - ZORA, - ALEPH_ZERO, - INK, - } = CHAIN_IDs; const hubChainId = hubPoolClient.chainId; const l1Signer = spokePoolClients[hubChainId].spokePool.signer; const constructBridges = (chainId: number) => { + if (chainId === hubChainId) { + return {}; + } // Special case for the EthereumAdapter return Object.fromEntries( - SUPPORTED_TOKENS[chainId].map((symbol) => { + SUPPORTED_TOKENS[chainId]?.map((symbol) => { const l2Signer = spokePoolClients[chainId].spokePool.signer; const l1Token = TOKEN_SYMBOLS_MAP[symbol].addresses[hubChainId]; const bridgeConstructor = CUSTOM_BRIDGE[chainId]?.[l1Token] ?? CANONICAL_BRIDGE[chainId]; const bridge = new bridgeConstructor(chainId, hubChainId, l1Signer, l2Signer, l1Token); return [l1Token, bridge]; - }) + }) ?? [] ); }; - if (this.spokePoolClients[OPTIMISM] !== undefined) { - this.adapters[OPTIMISM] = new BaseChainAdapter( - spokePoolClients, - OPTIMISM, - hubChainId, - filterMonitoredAddresses(OPTIMISM), - logger, - SUPPORTED_TOKENS[OPTIMISM], - constructBridges(OPTIMISM), - DEFAULT_GAS_MULTIPLIER[OPTIMISM] ?? 1 - ); - } - if (this.spokePoolClients[POLYGON] !== undefined) { - this.adapters[POLYGON] = new PolygonAdapter(logger, spokePoolClients, filterMonitoredAddresses(POLYGON)); - } - if (this.spokePoolClients[ARBITRUM] !== undefined) { - this.adapters[ARBITRUM] = new ArbitrumAdapter(logger, spokePoolClients, filterMonitoredAddresses(ARBITRUM)); - } - if (this.spokePoolClients[ZK_SYNC] !== undefined) { - this.adapters[ZK_SYNC] = new ZKSyncAdapter(logger, spokePoolClients, filterMonitoredAddresses(ZK_SYNC)); - } - if (this.spokePoolClients[BASE] !== undefined) { - this.adapters[BASE] = new BaseChainAdapter( - spokePoolClients, - BASE, - hubChainId, - filterMonitoredAddresses(BASE), - logger, - SUPPORTED_TOKENS[BASE], - constructBridges(BASE), - DEFAULT_GAS_MULTIPLIER[BASE] ?? 1 - ); - } - if (this.spokePoolClients[LINEA] !== undefined) { - this.adapters[LINEA] = new LineaAdapter(logger, spokePoolClients, filterMonitoredAddresses(LINEA)); - } - if (this.spokePoolClients[MODE] !== undefined) { - this.adapters[MODE] = new BaseChainAdapter( - spokePoolClients, - MODE, - hubChainId, - filterMonitoredAddresses(MODE), - logger, - SUPPORTED_TOKENS[MODE], - constructBridges(MODE), - DEFAULT_GAS_MULTIPLIER[MODE] ?? 1 - ); - } - if (this.spokePoolClients[REDSTONE] !== undefined) { - this.adapters[REDSTONE] = new BaseChainAdapter( - spokePoolClients, - REDSTONE, - hubChainId, - filterMonitoredAddresses(REDSTONE), - logger, - SUPPORTED_TOKENS[REDSTONE], - constructBridges(REDSTONE), - DEFAULT_GAS_MULTIPLIER[REDSTONE] ?? 1 - ); - } - if (this.spokePoolClients[LISK] !== undefined) { - this.adapters[LISK] = new BaseChainAdapter( - spokePoolClients, - LISK, - hubChainId, - filterMonitoredAddresses(LISK), - logger, - SUPPORTED_TOKENS[LISK], - constructBridges(LISK), - DEFAULT_GAS_MULTIPLIER[LISK] ?? 1 - ); - } - if (this.spokePoolClients[BLAST] !== undefined) { - this.adapters[BLAST] = new BaseChainAdapter( - spokePoolClients, - BLAST, - hubChainId, - filterMonitoredAddresses(BLAST), - logger, - SUPPORTED_TOKENS[BLAST], - constructBridges(BLAST), - DEFAULT_GAS_MULTIPLIER[BLAST] ?? 1 - ); - } - if (this.spokePoolClients[SCROLL] !== undefined) { - this.adapters[SCROLL] = new ScrollAdapter(logger, spokePoolClients, filterMonitoredAddresses(SCROLL)); - } - if (this.spokePoolClients[CHAIN_IDs.WORLD_CHAIN] !== undefined) { - this.adapters[CHAIN_IDs.WORLD_CHAIN] = new BaseChainAdapter( - spokePoolClients, - CHAIN_IDs.WORLD_CHAIN, - hubChainId, - filterMonitoredAddresses(CHAIN_IDs.WORLD_CHAIN), - logger, - SUPPORTED_TOKENS[CHAIN_IDs.WORLD_CHAIN], - constructBridges(CHAIN_IDs.WORLD_CHAIN), - DEFAULT_GAS_MULTIPLIER[CHAIN_IDs.WORLD_CHAIN] ?? 1 - ); - } - if (this.spokePoolClients[ZORA] !== undefined) { - this.adapters[ZORA] = new BaseChainAdapter( - spokePoolClients, - ZORA, - hubChainId, - filterMonitoredAddresses(ZORA), - logger, - SUPPORTED_TOKENS[ZORA], - constructBridges(ZORA), - DEFAULT_GAS_MULTIPLIER[ZORA] ?? 1 - ); - } - if (this.spokePoolClients[ALEPH_ZERO] !== undefined) { - this.adapters[ALEPH_ZERO] = new BaseChainAdapter( - spokePoolClients, - ALEPH_ZERO, - hubChainId, - filterMonitoredAddresses(ALEPH_ZERO), - logger, - SUPPORTED_TOKENS[ALEPH_ZERO], - constructBridges(ALEPH_ZERO), - DEFAULT_GAS_MULTIPLIER[ALEPH_ZERO] ?? 1 - ); - } - - if (this.spokePoolClients[INK] !== undefined) { - this.adapters[INK] = new BaseChainAdapter( + Object.keys(this.spokePoolClients).map((_chainId) => { + const chainId = Number(_chainId); + assert(chainId.toString() === _chainId); + // Instantiate a generic adapter and supply all network-specific configurations. + this.adapters[chainId] = new BaseChainAdapter( spokePoolClients, - INK, + chainId, hubChainId, - filterMonitoredAddresses(INK), + filterMonitoredAddresses(chainId), logger, - SUPPORTED_TOKENS[INK], - constructBridges(INK), - DEFAULT_GAS_MULTIPLIER[INK] ?? 1 + SUPPORTED_TOKENS[chainId] ?? [], + constructBridges(chainId), + DEFAULT_GAS_MULTIPLIER[chainId] ?? 1 ); - } - + }); logger.debug({ at: "AdapterManager#constructor", message: "Initialized AdapterManager", @@ -273,14 +135,7 @@ export class AdapterManager { wrapThreshold.gte(wrapTarget), `wrapEtherThreshold ${wrapThreshold.toString()} must be >= wrapEtherTarget ${wrapTarget.toString()}` ); - if (chainId === CHAIN_IDs.MAINNET) { - // For mainnet, construct one-off adapter to wrap ETH, because Ethereum is typically not a chain - // that we have an adapter for. - const ethAdapter = new EthereumAdapter(this.logger, this.spokePoolClients); - await ethAdapter.wrapEthIfAboveThreshold(wrapThreshold, wrapTarget, simMode); - } else { - await this.adapters[chainId].wrapEthIfAboveThreshold(wrapThreshold, wrapTarget, simMode); - } + await this.adapters[chainId].wrapEthIfAboveThreshold(wrapThreshold, wrapTarget, simMode); } ); } diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 199d3e214a..ed6f6764c2 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -121,6 +121,7 @@ export const CHAIN_MAX_BLOCK_LOOKBACK = { [CHAIN_IDs.BASE_SEPOLIA]: 10000, [CHAIN_IDs.BLAST_SEPOLIA]: 10000, [CHAIN_IDs.INK_SEPOLIA]: 10000, + [CHAIN_IDs.LENS_SEPOLIA]: 10000, [CHAIN_IDs.LISK_SEPOLIA]: 10000, [CHAIN_IDs.MODE_SEPOLIA]: 10000, [CHAIN_IDs.OPTIMISM_SEPOLIA]: 10000, @@ -155,6 +156,7 @@ export const BUNDLE_END_BLOCK_BUFFERS = { [CHAIN_IDs.BASE_SEPOLIA]: 0, [CHAIN_IDs.BLAST_SEPOLIA]: 0, [CHAIN_IDs.INK_SEPOLIA]: 0, + [CHAIN_IDs.LENS_SEPOLIA]: 0, [CHAIN_IDs.LISK_SEPOLIA]: 0, [CHAIN_IDs.MODE_SEPOLIA]: 0, [CHAIN_IDs.OPTIMISM_SEPOLIA]: 0, @@ -204,6 +206,7 @@ export const CHAIN_CACHE_FOLLOW_DISTANCE: { [chainId: number]: number } = { [CHAIN_IDs.BLAST_SEPOLIA]: 0, [CHAIN_IDs.INK_SEPOLIA]: 0, [CHAIN_IDs.LISK_SEPOLIA]: 0, + [CHAIN_IDs.LENS_SEPOLIA]: 0, [CHAIN_IDs.MODE_SEPOLIA]: 0, [CHAIN_IDs.OPTIMISM_SEPOLIA]: 0, [CHAIN_IDs.POLYGON_AMOY]: 0, @@ -314,6 +317,7 @@ export const SUPPORTED_TOKENS: { [chainId: number]: string[] } = { [CHAIN_IDs.ARBITRUM_SEPOLIA]: ["USDC", "USDT", "WETH", "DAI", "WBTC", "UMA", "ACX"], [CHAIN_IDs.BASE_SEPOLIA]: ["BAL", "DAI", "ETH", "WETH", "USDC"], [CHAIN_IDs.BLAST_SEPOLIA]: ["WETH"], + [CHAIN_IDs.LENS_SEPOLIA]: ["WETH"], [CHAIN_IDs.LISK_SEPOLIA]: ["WETH", "USDT"], [CHAIN_IDs.MODE_SEPOLIA]: ["ETH", "WETH", "USDC", "USDT", "WBTC"], [CHAIN_IDs.OPTIMISM_SEPOLIA]: ["DAI", "SNX", "BAL", "ETH", "WETH", "USDC", "USDT", "WBTC", "UMA", "ACX"], @@ -351,16 +355,25 @@ export const CANONICAL_BRIDGE: { } = { [CHAIN_IDs.ALEPH_ZERO]: ArbitrumOrbitBridge, [CHAIN_IDs.ARBITRUM]: ArbitrumOrbitBridge, + [CHAIN_IDs.ARBITRUM_SEPOLIA]: ArbitrumOrbitBridge, [CHAIN_IDs.BASE]: OpStackDefaultERC20Bridge, + [CHAIN_IDs.BASE_SEPOLIA]: OpStackDefaultERC20Bridge, [CHAIN_IDs.BLAST]: OpStackDefaultERC20Bridge, + [CHAIN_IDs.BLAST_SEPOLIA]: OpStackDefaultERC20Bridge, [CHAIN_IDs.INK]: OpStackDefaultERC20Bridge, + [CHAIN_IDs.LENS_SEPOLIA]: ZKSyncBridge, // TODO [CHAIN_IDs.LINEA]: LineaBridge, [CHAIN_IDs.LISK]: OpStackDefaultERC20Bridge, + [CHAIN_IDs.LISK_SEPOLIA]: OpStackDefaultERC20Bridge, [CHAIN_IDs.MODE]: OpStackDefaultERC20Bridge, + [CHAIN_IDs.MODE_SEPOLIA]: OpStackDefaultERC20Bridge, [CHAIN_IDs.OPTIMISM]: OpStackDefaultERC20Bridge, + [CHAIN_IDs.OPTIMISM_SEPOLIA]: OpStackDefaultERC20Bridge, [CHAIN_IDs.POLYGON]: PolygonERC20Bridge, + [CHAIN_IDs.POLYGON_AMOY]: PolygonERC20Bridge, [CHAIN_IDs.REDSTONE]: OpStackDefaultERC20Bridge, [CHAIN_IDs.SCROLL]: ScrollERC20Bridge, + [CHAIN_IDs.SCROLL_SEPOLIA]: ScrollERC20Bridge, [CHAIN_IDs.WORLD_CHAIN]: OpStackDefaultERC20Bridge, [CHAIN_IDs.ZK_SYNC]: ZKSyncBridge, [CHAIN_IDs.ZORA]: OpStackDefaultERC20Bridge, @@ -476,6 +489,10 @@ export const DEFAULT_ARBITRUM_GATEWAY: { [chainId: number]: { l1: string; l2: st l1: "0xa3A7B6F88361F48403514059F1F16C8E78d60EeC", l2: "0x09e9222E96E7B4AE2a407B98d48e330053351EEe", }, + [CHAIN_IDs.ARBITRUM_SEPOLIA]: { + l1: "0x902b3E5f8F19571859F4AB1003B960a5dF693aFF", + l2: "0x6e244cD02BBB8a6dbd7F626f05B2ef82151Ab502", + }, }; // We currently support WBTC, USDT, USDC, and WETH as routes on scroll. WBTC, USDT, and USDC transfer events can all be queried from the standard ERC20 diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 7f5f38e046..05393e9a55 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -44,7 +44,7 @@ export const CONTRACT_ADDRESSES: { }; }; } = { - 1: { + [CHAIN_IDs.MAINNET]: { lineaMessageService: { address: "0xd19d4B5d358258f05D7B411E21A1460D11B0876F", abi: LINEA_MESSAGE_SERVICE_ABI, @@ -204,7 +204,7 @@ export const CONTRACT_ADDRESSES: { abi: SCROLL_GAS_PRICE_ORACLE_ABI, }, }, - 10: { + [CHAIN_IDs.OPTIMISM]: { daiOptimismBridge: { address: "0x467194771dae2967aef3ecbedd3bf9a310c76c65", abi: DAI_OPTIMISM_BRIDGE_L2_ABI, @@ -229,7 +229,7 @@ export const CONTRACT_ADDRESSES: { abi: CCTP_TOKEN_MESSENGER_ABI, }, }, - 137: { + [CHAIN_IDs.POLYGON]: { withdrawableErc20: { abi: POLYGON_WITHDRAWABLE_ERC20_ABI, }, @@ -248,7 +248,7 @@ export const CONTRACT_ADDRESSES: { address: "0x0000000000000000000000000000000000001010", }, }, - 324: { + [CHAIN_IDs.ZK_SYNC]: { zkSyncDefaultErc20Bridge: { address: "0x11f943b2c77b743AB90f4A0Ae7d5A4e7FCA3E102", abi: ZK_SYNC_DEFAULT_ERC20_BRIDGE_L2_ABI, @@ -258,7 +258,7 @@ export const CONTRACT_ADDRESSES: { abi: WETH_ABI, }, }, - 480: { + [CHAIN_IDs.WORLD_CHAIN]: { opUSDCBridge: { address: "0xbD80b06d3dbD0801132c6689429aC09Ca6D27f82", abi: OP_USDC_BRIDGE_ABI, @@ -271,7 +271,7 @@ export const CONTRACT_ADDRESSES: { address: "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", }, }, - 690: { + [CHAIN_IDs.REDSTONE]: { ovmStandardBridge: { address: "0x4200000000000000000000000000000000000010", abi: OVM_L2_STANDARD_BRIDGE_ABI, @@ -280,7 +280,7 @@ export const CONTRACT_ADDRESSES: { address: "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", }, }, - 1135: { + [CHAIN_IDs.LISK]: { ovmStandardBridge: { address: "0x4200000000000000000000000000000000000010", abi: OVM_L2_STANDARD_BRIDGE_ABI, @@ -289,7 +289,7 @@ export const CONTRACT_ADDRESSES: { address: "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", }, }, - 8453: { + [CHAIN_IDs.BASE]: { ovmStandardBridge: { address: "0x4200000000000000000000000000000000000010", abi: OVM_L2_STANDARD_BRIDGE_ABI, @@ -306,7 +306,7 @@ export const CONTRACT_ADDRESSES: { abi: CCTP_TOKEN_MESSENGER_ABI, }, }, - 34443: { + [CHAIN_IDs.MODE]: { ovmStandardBridge: { address: "0x4200000000000000000000000000000000000010", abi: OVM_L2_STANDARD_BRIDGE_ABI, @@ -315,7 +315,7 @@ export const CONTRACT_ADDRESSES: { address: "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", }, }, - 57073: { + [CHAIN_IDs.INK]: { ovmStandardBridge: { address: "0x4200000000000000000000000000000000000010", abi: OVM_L2_STANDARD_BRIDGE_ABI, @@ -324,7 +324,7 @@ export const CONTRACT_ADDRESSES: { address: "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", }, }, - 81457: { + [CHAIN_IDs.BLAST]: { ovmStandardBridge: { address: "0x4200000000000000000000000000000000000010", abi: OVM_L2_STANDARD_BRIDGE_ABI, @@ -337,7 +337,7 @@ export const CONTRACT_ADDRESSES: { abi: BLAST_BRIDGE_ABI, }, }, - 42161: { + [CHAIN_IDs.ARBITRUM]: { erc20Gateway: { abi: ARBITRUM_ERC20_GATEWAY_L2_ABI, }, @@ -350,7 +350,7 @@ export const CONTRACT_ADDRESSES: { abi: CCTP_TOKEN_MESSENGER_ABI, }, }, - 41455: { + [CHAIN_IDs.ALEPH_ZERO]: { erc20Gateway: { address: "0x2A5a79061b723BBF453ef7E07c583C750AFb9BD6", abi: ARBITRUM_ERC20_GATEWAY_L2_ABI, @@ -360,7 +360,7 @@ export const CONTRACT_ADDRESSES: { abi: ARBSYS_L2_ABI, }, }, - 59144: { + [CHAIN_IDs.LINEA]: { l2MessageService: { address: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec", abi: LINEA_MESSAGE_SERVICE_ABI, @@ -377,7 +377,7 @@ export const CONTRACT_ADDRESSES: { address: "0x0000000000000000000000000000000000000000", }, }, - 534352: { + [CHAIN_IDs.SCROLL]: { scrollGatewayRouter: { address: "0x4C0926FF5252A435FD19e10ED15e5a249Ba19d79", abi: SCROLL_GATEWAY_ROUTER_L2_ABI, @@ -401,7 +401,23 @@ export const CONTRACT_ADDRESSES: { }, }, // Testnets - 11155111: { + [CHAIN_IDs.SEPOLIA]: { + ovmStandardBridge_4202: { + address: "0x1Fb30e446eA791cd1f011675E5F3f5311b70faF5", + abi: OVM_L1_STANDARD_BRIDGE_ABI, + }, + ovmStandardBridge_84532: { + address: "0xfd0Bf71F60660E2f608ed56e1659C450eB113120", + abi: OVM_L1_STANDARD_BRIDGE_ABI, + }, + ovmStandardBridge_11155420: { + address: "0xFBb0621E0B23b5478B630BD55a5f21f67730B0F1", + abi: OVM_L1_STANDARD_BRIDGE_ABI, + }, + ovmStandardBridge_168587773: { + address: "0xDeDa8D3CCf044fE2A16217846B6e1f1cfD8e122f", + abi: OVM_L1_STANDARD_BRIDGE_ABI, + }, cctpMessageTransmitter: { address: "0x7865fAfC2db2093669d92c0F33AeEF291086BEFD", abi: CCTP_MESSAGE_TRANSMITTER_ABI, @@ -410,8 +426,39 @@ export const CONTRACT_ADDRESSES: { address: "0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5", abi: CCTP_TOKEN_MESSENGER_ABI, }, + polygonBridge: { + address: "", // FIXME. Can leave undefined for now due to no IM on Sepolia. + abi: POLYGON_BRIDGE_ABI, + }, + orbitErc20GatewayRouter_421614: { + address: "0xcE18836b233C83325Cc8848CA4487e94C6288264", + abi: ARBITRUM_ERC20_GATEWAY_ROUTER_L1_ABI, + }, + orbitErc20Gateway_421614: { + abi: ARBITRUM_ERC20_GATEWAY_L1_ABI, + }, + zkSyncDefaultErc20Bridge: { + address: "0x7303b5ce64f1adb0558572611a0b90620b6dd5f4", + abi: ZK_SYNC_DEFAULT_ERC20_BRIDGE_L1_ABI, + }, + zkSyncMailbox: { + address: "", // TODO + abi: ZK_SYNC_MAILBOX_ABI, + }, + hubPool: { + address: "0x14224e63716afAcE30C9a417E0542281869f7d9e", + }, + }, + [CHAIN_IDs.ARBITRUM_SEPOLIA]: { + erc20Gateway: { + abi: ARBITRUM_ERC20_GATEWAY_L2_ABI, + }, }, - 84532: { + [CHAIN_IDs.BASE_SEPOLIA]: { + ovmStandardBridge: { + address: "0x4200000000000000000000000000000000000010", + abi: OVM_L2_STANDARD_BRIDGE_ABI, + }, cctpMessageTransmitter: { address: "0x7865fAfC2db2093669d92c0F33AeEF291086BEFD", abi: CCTP_MESSAGE_TRANSMITTER_ABI, @@ -421,10 +468,46 @@ export const CONTRACT_ADDRESSES: { abi: CCTP_TOKEN_MESSENGER_ABI, }, }, - 59140: { - l2MessageService: { - address: "0xC499a572640B64eA1C8c194c43Bc3E19940719dC", - abi: LINEA_MESSAGE_SERVICE_ABI, + [CHAIN_IDs.BLAST_SEPOLIA]: { + ovmStandardBridge: { + address: "0x4200000000000000000000000000000000000010", + abi: OVM_L2_STANDARD_BRIDGE_ABI, + }, + }, + [CHAIN_IDs.LENS_SEPOLIA]: { + // TODO: Rename to zkstack once zkstack adapters are made. + zkSyncDefaultErc20Bridge: { + address: "0x427373Be173120D7A042b44D0804E37F25E7330b", + abi: ZK_SYNC_DEFAULT_ERC20_BRIDGE_L2_ABI, + }, + }, + [CHAIN_IDs.LISK_SEPOLIA]: { + ovmStandardBridge: { + address: "0x4200000000000000000000000000000000000010", + abi: OVM_L2_STANDARD_BRIDGE_ABI, + }, + }, + [CHAIN_IDs.MODE_SEPOLIA]: { + ovmStandardBridge: { + address: "0x4200000000000000000000000000000000000010", + abi: OVM_L2_STANDARD_BRIDGE_ABI, + }, + }, + [CHAIN_IDs.OPTIMISM_SEPOLIA]: { + ovmStandardBridge: { + address: "0x4200000000000000000000000000000000000010", + abi: OVM_L2_STANDARD_BRIDGE_ABI, + }, + }, + [CHAIN_IDs.POLYGON_AMOY]: { + withdrawableErc20: { + abi: POLYGON_WITHDRAWABLE_ERC20_ABI, + }, + }, + [CHAIN_IDs.SCROLL_SEPOLIA]: { + scrollGatewayRouter: { + address: "0x9aD3c5617eCAa556d6E166787A97081907171230", + abi: SCROLL_GATEWAY_ROUTER_L2_ABI, }, }, }; diff --git a/src/dataworker/Dataworker.ts b/src/dataworker/Dataworker.ts index f3db13fdcd..3ba94f529d 100644 --- a/src/dataworker/Dataworker.ts +++ b/src/dataworker/Dataworker.ts @@ -1690,9 +1690,14 @@ export class Dataworker { value: requiredAmount, }); } else { + // We can't call multicall() here because the feeToken is not guaranteed to be a Multicaller + // contract and this is a permissioned function where the msg.sender needs to be the + // feeToken balance owner, so we can't simply set `unpermissioned: true` to send it through the Multisender. + // Instead, we need to set `nonMulticall: true` and avoid batch calling this transaction. this.clients.multiCallerClient.enqueueTransaction({ contract: new Contract(feeToken, ERC20.abi, signer), chainId: hubPoolChainId, + nonMulticall: true, method: "transfer", args: [holder, requiredAmount], message: `Loaded orbit gas token for message to ${getNetworkName(leaf.chainId)} 📨!`, diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index 253c00da0e..7496f8100a 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -330,7 +330,9 @@ export async function finalize( if (finalizations.length > 0) { // @dev use multicaller client to execute batched txn to take advantage of its native txn simulation - // safety features + // safety features. This only works because we assume all finalizer transactions are + // unpermissioned (i.e. msg.sender can be anyone). If this is not true for any chain then we'd need to use + // the TransactionClient. const multicallerClient = new MultiCallerClient(logger); let txnHashLookup: Record = {}; try { diff --git a/src/monitor/MonitorClientHelper.ts b/src/monitor/MonitorClientHelper.ts index 908e5f2aac..66d21c3f25 100644 --- a/src/monitor/MonitorClientHelper.ts +++ b/src/monitor/MonitorClientHelper.ts @@ -9,6 +9,7 @@ import { constructSpokePoolClientsWithLookback, } from "../common"; import { SpokePoolClientsByChain } from "../interfaces"; +import { AdapterManager, CrossChainTransferClient } from "../clients/bridges"; export interface MonitorClients extends Clients { bundleDataClient: BundleDataClient; @@ -18,9 +19,6 @@ export interface MonitorClients extends Clients { tokenTransferClient: TokenTransferClient; } -import { GenericAdapterManager } from "../adapter/AdapterManager"; -import { AdapterManager, CrossChainTransferClient } from "../clients/bridges"; - export async function constructMonitorClients( config: MonitorConfig, logger: winston.Logger, @@ -56,8 +54,7 @@ export async function constructMonitorClients( // Cross-chain transfers will originate from the HubPool's address and target SpokePool addresses, so // track both. - const adapterManagerConstructor = config.useGenericAdapter ? GenericAdapterManager : AdapterManager; - const adapterManager = new adapterManagerConstructor(logger, spokePoolClients, hubPoolClient, [ + const adapterManager = new AdapterManager(logger, spokePoolClients, hubPoolClient, [ signerAddr, hubPoolClient.hubPool.address, ...spokePoolAddresses, diff --git a/src/monitor/MonitorConfig.ts b/src/monitor/MonitorConfig.ts index a161a4852e..5c766512c6 100644 --- a/src/monitor/MonitorConfig.ts +++ b/src/monitor/MonitorConfig.ts @@ -66,7 +66,6 @@ export class MonitorConfig extends CommonConfig { REFILL_BALANCES, REFILL_BALANCES_ENABLED, STUCK_REBALANCES_ENABLED, - MONITOR_USE_GENERIC_ADAPTER, REPORT_SPOKE_POOL_BALANCES, MONITORED_SPOKE_POOL_CHAINS, MONITORED_TOKEN_SYMBOLS, @@ -83,8 +82,6 @@ export class MonitorConfig extends CommonConfig { spokePoolBalanceReportEnabled: REPORT_SPOKE_POOL_BALANCES === "true", }; - this.useGenericAdapter = MONITOR_USE_GENERIC_ADAPTER === "true"; - // Used to monitor activities not from whitelisted data workers or relayers. this.whitelistedDataworkers = parseAddressesOptional(WHITELISTED_DATA_WORKERS); this.whitelistedRelayers = parseAddressesOptional(WHITELISTED_RELAYERS); diff --git a/src/relayer/Relayer.ts b/src/relayer/Relayer.ts index ae2313c694..25f36892c9 100644 --- a/src/relayer/Relayer.ts +++ b/src/relayer/Relayer.ts @@ -1065,13 +1065,12 @@ export class Relayer { // @dev If the origin chain is a lite chain and there are no preferred repayment chains, then we can assume // that the origin chain, the only possible repayment chain, is over-allocated. We should log this case because // it is a special edge case the relayer should be aware of. - this.logger[this.config.sendingRelaysEnabled ? "warn" : "debug"]({ + this.logger.debug({ at: "Relayer::resolveRepaymentChain", message: deposit.fromLiteChain ? `Deposit ${depositId} originated from over-allocated lite chain ${originChain}` : `Unable to identify a preferred repayment chain for ${originChain} deposit ${depositId}.`, txn: blockExplorerLink(transactionHash, originChainId), - notificationPath: "across-unprofitable-fills", }); return { repaymentChainProfitability: { @@ -1324,6 +1323,9 @@ export class Relayer { return { symbol, decimals, formatter: createFormatFunction(2, 4, false, decimals) }; } + // TODO: This should really be renamed to "handleUnfillableDeposit" since it not only logs about unprofitable relayer + // fees but also about fills with messages that fail to simulate and deposits from lite chains that are + // over-allocated. private handleUnprofitableFill() { const { profitClient } = this.clients; const unprofitableDeposits = profitClient.getUnprofitableFills(); @@ -1331,8 +1333,8 @@ export class Relayer { let mrkdwn = ""; Object.keys(unprofitableDeposits).forEach((chainId) => { let depositMrkdwn = ""; - Object.keys(unprofitableDeposits[chainId]).forEach((depositId) => { - const { deposit, lpFeePct, relayerFeePct, gasCost } = unprofitableDeposits[chainId][depositId]; + unprofitableDeposits[Number(chainId)].forEach((unprofitableFill) => { + const { deposit, lpFeePct, relayerFeePct, gasCost } = unprofitableFill; // Skip notifying if the unprofitable fill happened too long ago to avoid spamming. if (deposit.quoteTimestamp + UNPROFITABLE_DEPOSIT_NOTICE_PERIOD < getCurrentTime()) { @@ -1351,13 +1353,26 @@ export class Relayer { const formattedRelayerFeePct = formatFeePct(relayerFeePct); const formattedLpFeePct = formatFeePct(lpFeePct); + // @dev If the origin chain is a lite chain and the LP fee percentage is infinity, then we can assume that the + // deposit originated from an over-allocated lite chain because the originChain, the only possible + // repayment chain, was not selected for repayment. So the "unprofitable" log should be modified to indicate + // this lite chain edge case. + const fromOverallocatedLiteChain = deposit.fromLiteChain && lpFeePct.eq(bnUint256Max); + const depositFailedToSimulateWithMessage = !isMessageEmpty(deposit.message) && gasCost.eq(bnUint256Max); depositMrkdwn += `- DepositId ${deposit.depositId} (tx: ${depositblockExplorerLink})` + ` of input amount ${formattedInputAmount} ${inputSymbol}` + ` and output amount ${formattedOutputAmount} ${outputSymbol}` + ` from ${getNetworkName(originChainId)} to ${getNetworkName(destinationChainId)}` + - ` with relayerFeePct ${formattedRelayerFeePct}%, lpFeePct ${formattedLpFeePct}%,` + - ` and gas cost ${formattedGasCost} ${gasTokenSymbol} is unprofitable!\n`; + `${fromOverallocatedLiteChain ? " is from an over-allocated lite chain" : ""}` + + `${ + depositFailedToSimulateWithMessage + ? ` failed to simulate with message of size ${ethersUtils.hexDataLength(deposit.message)} bytes` + : "" + }` + + `${` with relayerFeePct ${formattedRelayerFeePct}% lpFeePct ${ + lpFeePct.eq(bnUint256Max) ? "∞" : formattedLpFeePct + }% and gas cost ${gasCost.eq(bnUint256Max) ? "∞" : formattedGasCost} ${gasTokenSymbol}\n`}`; }); if (depositMrkdwn) { diff --git a/src/relayer/RelayerClientHelper.ts b/src/relayer/RelayerClientHelper.ts index c598b38d40..8a04fdaab7 100644 --- a/src/relayer/RelayerClientHelper.ts +++ b/src/relayer/RelayerClientHelper.ts @@ -21,8 +21,6 @@ import { import { SpokePoolClientsByChain } from "../interfaces"; import { getBlockForTimestamp, getCurrentTime, getProvider, getRedisCache, Signer, SpokePool } from "../utils"; import { RelayerConfig } from "./RelayerConfig"; - -import { GenericAdapterManager } from "../adapter/AdapterManager"; import { AdapterManager, CrossChainTransferClient } from "../clients/bridges"; export interface RelayerClients extends Clients { @@ -143,8 +141,7 @@ export async function constructRelayerClients( await profitClient.update(); const monitoredAddresses = [signerAddr]; - const adapterManagerConstructor = config.useGenericAdapter ? GenericAdapterManager : AdapterManager; - const adapterManager = new adapterManagerConstructor( + const adapterManager = new AdapterManager( logger, spokePoolClients, hubPoolClient, diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index 03e0a8adde..fb3d626ada 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -89,14 +89,11 @@ export class RelayerConfig extends CommonConfig { RELAYER_IGNORE_LIMITS, RELAYER_EXTERNAL_INDEXER, RELAYER_TRY_MULTICALL_CHAINS, - RELAYER_USE_GENERIC_ADAPTER, RELAYER_LOGGING_INTERVAL = "30", RELAYER_MAINTENANCE_INTERVAL = "60", } = env; super(env); - this.useGenericAdapter = RELAYER_USE_GENERIC_ADAPTER === "true"; - // External indexing is dependent on looping mode being configured. this.externalIndexer = this.pollingDelay > 0 && RELAYER_EXTERNAL_INDEXER === "true"; @@ -258,7 +255,7 @@ export class RelayerConfig extends CommonConfig { // Transform deposit confirmation requirements into an array of ascending // deposit confirmations, sorted by the corresponding threshold in USD. this.minDepositConfirmations = {}; - if (this.hubPoolChainId !== CHAIN_IDs.MAINNET) { + if (this.hubPoolChainId !== CHAIN_IDs.MAINNET && !isDefined(MIN_DEPOSIT_CONFIRMATIONS)) { // Sub in permissive defaults for testnet. const standardConfig = { usdThreshold: toBNWei(Number.MAX_SAFE_INTEGER), minConfirmations: 1 }; Object.values(TESTNET_CHAIN_IDs).forEach((chainId) => (this.minDepositConfirmations[chainId] = [standardConfig])); diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index a8b0fa1ea9..bc6c81043e 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -8,7 +8,6 @@ import { BigNumber, bnZero, Contract, - fixedPointAdjustment as fixedPoint, isDefined, TransactionResponse, ethers, @@ -82,7 +81,12 @@ export async function runTransaction( Number(process.env[`MAX_FEE_PER_GAS_SCALER_${chainId}`] || process.env.MAX_FEE_PER_GAS_SCALER) || DEFAULT_GAS_FEE_SCALERS[chainId]?.maxFeePerGasScaler; - const gas = await getGasPrice(provider, priorityFeeScaler, maxFeePerGasScaler); + const gas = await getGasPrice( + provider, + priorityFeeScaler, + maxFeePerGasScaler, + await contract.populateTransaction[method](...(args as Array), { value }) + ); logger.debug({ at: "TxUtil", @@ -154,30 +158,30 @@ export async function runTransaction( } } -// TODO: add in gasPrice when the SDK has this for the given chainId. TODO: improve how we fetch prices. -// For now this method will extract the provider's Fee data from the associated network and scale it by a priority -// scaler. This works on both mainnet and L2's by the utility switching the response structure accordingly. export async function getGasPrice( provider: ethers.providers.Provider, priorityScaler = 1.2, - maxFeePerGasScaler = 3 + maxFeePerGasScaler = 3, + transactionObject?: ethers.PopulatedTransaction ): Promise> { + // Floor scalers at 1.0 as we'll rarely want to submit too low of a gas price. We mostly + // just want to submit with as close to prevailing fees as possible. + maxFeePerGasScaler = Math.max(1, maxFeePerGasScaler); + priorityScaler = Math.max(1, priorityScaler); const { chainId } = await provider.getNetwork(); - const feeData = await gasPriceOracle.getGasPriceEstimate(provider, chainId); - - if (feeData.maxPriorityFeePerGas.gt(feeData.maxFeePerGas)) { - feeData.maxFeePerGas = scaleByNumber(feeData.maxPriorityFeePerGas, 1.5); - } - - // Handle chains with legacy pricing. - if (feeData.maxPriorityFeePerGas.eq(bnZero)) { - return { gasPrice: scaleByNumber(feeData.maxFeePerGas, priorityScaler) }; - } - - // Default to EIP-1559 (type 2) pricing. + // Pass in unsignedTx here for better Linea gas price estimations via the Linea Viem provider. + const feeData = await gasPriceOracle.getGasPriceEstimate(provider, { + chainId, + baseFeeMultiplier: toBNWei(maxFeePerGasScaler), + priorityFeeMultiplier: toBNWei(priorityScaler), + unsignedTx: transactionObject, + }); + + // Default to EIP-1559 (type 2) pricing. If gasPriceOracle is using a legacy adapter for this chain then + // the priority fee will be 0. return { - maxFeePerGas: scaleByNumber(feeData.maxFeePerGas, Math.max(priorityScaler * maxFeePerGasScaler, 1)), - maxPriorityFeePerGas: scaleByNumber(feeData.maxPriorityFeePerGas, priorityScaler), + maxFeePerGas: feeData.maxFeePerGas, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, }; } @@ -231,7 +235,3 @@ export function getTarget(targetAddress: string): return { targetAddress }; } } - -function scaleByNumber(amount: BigNumber, scaling: number) { - return amount.mul(toBNWei(scaling)).div(fixedPoint); -} diff --git a/test/MultiCallerClient.ts b/test/MultiCallerClient.ts index 1ce18fd1fb..e69c11db12 100644 --- a/test/MultiCallerClient.ts +++ b/test/MultiCallerClient.ts @@ -34,8 +34,8 @@ class DummyMultiCallerClient extends MockedMultiCallerClient { return Object.values(txnQueue).reduce((count, txnQueue) => (count += txnQueue.length), 0); } - valueTxnCount(): number { - return this.txnCount(this.valueTxns); + nonMulticallTxnCount(): number { + return this.txnCount(this.nonMulticallTxns); } multiCallTransactionCount(): number { @@ -68,7 +68,15 @@ describe("MultiCallerClient", async function () { it("Correctly enqueues value transactions", async function () { chainIds.forEach((chainId) => multiCaller.enqueueTransaction({ chainId, value: toBN(1) } as AugmentedTransaction)); - expect(multiCaller.valueTxnCount()).to.equal(chainIds.length); + expect(multiCaller.nonMulticallTxnCount()).to.equal(chainIds.length); + expect(multiCaller.transactionCount()).to.equal(chainIds.length); + }); + + it("Correctly enqueues non-multicall transactions", async function () { + chainIds.forEach((chainId) => + multiCaller.enqueueTransaction({ chainId, nonMulticall: true } as AugmentedTransaction) + ); + expect(multiCaller.nonMulticallTxnCount()).to.equal(chainIds.length); expect(multiCaller.transactionCount()).to.equal(chainIds.length); }); @@ -87,10 +95,11 @@ describe("MultiCallerClient", async function () { chainIds.forEach((chainId) => { multiCaller.enqueueTransaction({ chainId } as AugmentedTransaction); multiCaller.enqueueTransaction({ chainId, value: bnOne } as AugmentedTransaction); + multiCaller.enqueueTransaction({ chainId, nonMulticall: true } as AugmentedTransaction); }); - expect(multiCaller.valueTxnCount()).to.equal(chainIds.length); expect(multiCaller.multiCallTransactionCount()).to.equal(chainIds.length); - expect(multiCaller.transactionCount()).to.equal(2 * chainIds.length); + expect(multiCaller.nonMulticallTxnCount()).to.equal(2 * chainIds.length); + expect(multiCaller.transactionCount()).to.equal(3 * chainIds.length); }); it("Propagates input transaction gasLimits: internal multicall", async function () { @@ -271,6 +280,33 @@ describe("MultiCallerClient", async function () { } }); + it("Submits non-multicall txns", async function () { + const nTxns = 3; + for (let txn = 1; txn <= nTxns; ++txn) { + const chainId = chainIds[0]; + const txnRequest: AugmentedTransaction = { + chainId, + contract: { + address, + interface: { encodeFunctionData }, + multicall: 1, + } as unknown as Contract, + method: "test", + args: [{ result: txnClientPassResult }], + nonMulticall: true, + message: `Test nonMulticall transaction (${txn}/${nTxns}) on chain ${chainId}`, + mrkdwn: `Sample markdown string for chain ${chainId} transaction`, + }; + + multiCaller.enqueueTransaction(txnRequest); + } + expect(multiCaller.transactionCount()).to.equal(nTxns); + + // Should have nTxns since non-multicall txns are not batched. + const results = await multiCaller.executeTxnQueues(); + expect(Object.values(results).flat().length).to.equal(nTxns); + }); + it("Correctly filters loggable vs. ignorable simulation failures", async function () { const txn = { chainId: chainIds[0], diff --git a/test/TryMulticallClient.ts b/test/TryMulticallClient.ts index cb18079b09..e446593642 100644 --- a/test/TryMulticallClient.ts +++ b/test/TryMulticallClient.ts @@ -37,8 +37,8 @@ class DummyTryMulticallClient extends TryMulticallClient { return Object.values(txnQueue).reduce((count, txnQueue) => (count += txnQueue.length), 0); } - valueTxnCount(): number { - return this.txnCount(this.valueTxns); + nonMulticallTxnCount(): number { + return this.txnCount(this.nonMulticallTxns); } multiCallTransactionCount(): number { diff --git a/test/generic-adapters/AdapterManager.SendTokensCrossChain.ts b/test/generic-adapters/AdapterManager.SendTokensCrossChain.ts index 9bf7e797cc..9f3e880a57 100644 --- a/test/generic-adapters/AdapterManager.SendTokensCrossChain.ts +++ b/test/generic-adapters/AdapterManager.SendTokensCrossChain.ts @@ -1,6 +1,6 @@ import * as zksync from "zksync-ethers"; import { SpokePoolClient } from "../../src/clients"; -import { GenericAdapterManager } from "../../src/adapter/AdapterManager"; // Tested +import { AdapterManager } from "../../src/clients/bridges"; import { CONTRACT_ADDRESSES, chainIdsToCctpDomains } from "../../src/common"; import { bnToHex, @@ -30,7 +30,7 @@ const mockSpokePoolClients: { [chainId: number]: SpokePoolClient; } = {}; let relayer: SignerWithAddress, owner: SignerWithAddress, spyLogger: winston.Logger, amountToSend: BigNumber; -let adapterManager: AdapterManager; // tested +let adapterManager: AdapterManager; // Atomic depositor let l1AtomicDepositor: FakeContract; @@ -78,7 +78,7 @@ describe("AdapterManager: Send tokens cross-chain", async function () { const { hubPool } = await hubPoolFixture(); hubPoolClient = new MockHubPoolClient(spyLogger, hubPool, configStoreClient); await seedMocks(); - adapterManager = new GenericAdapterManager(spyLogger, mockSpokePoolClients, hubPoolClient, [relayer.address]); + adapterManager = new AdapterManager(spyLogger, mockSpokePoolClients, hubPoolClient, [relayer.address]); await constructChainSpecificFakes(); diff --git a/yarn.lock b/yarn.lock index c2e2d330e4..4d646c72c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,15 +11,10 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants@^3.1.24": - version "3.1.24" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.24.tgz#01fe49330bb467dd01813387ddbac741bc74a035" - integrity sha512-guKtvIbif//vsmSZbwGubTWVtfkWiyWenr2sVyo63U/68GOW89ceJRLu4efLjeLVGiSrNAJtFUCv9dTwrrosWA== - -"@across-protocol/constants@^3.1.25": - version "3.1.25" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.25.tgz#60d6d9814582ff91faf2b6d9f51d6dccb447b4ce" - integrity sha512-GpZoYn7hETYL2BPMM2GqXAer6+l/xuhder+pvpb00HJcb/sqCjF7vaaeKxjKJ3jKtyeulYmdu0NDkeNm5KbNWA== +"@across-protocol/constants@^3.1.27": + version "3.1.27" + resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.27.tgz#6c7b2b277d83ae1024c1de6faf474192391af0bd" + integrity sha512-3YEK2ERB2FVdDdrkFk5YeuRu/oIAHTIeu6YfT39SfZjIPyltP0ReFlDfZQ8c+rFiHurwyK39kjZF70VxRRREYw== "@across-protocol/contracts@^0.1.4": version "0.1.4" @@ -30,12 +25,12 @@ "@openzeppelin/contracts" "4.1.0" "@uma/core" "^2.18.0" -"@across-protocol/contracts@^3.0.19": - version "3.0.19" - resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.19.tgz#3756504bb3f5f625f9ca403045a79050e675602f" - integrity sha512-9GjKKF8SHGKP9FGhawHzLZ8sfBVFUICd+Bn1pn3SFuh0p+ndQIayG+QEYRKGFUXVPV6+XXLve750PQ1Hu7dIEg== +"@across-protocol/contracts@^3.0.20": + version "3.0.20" + resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.20.tgz#5a70782093d21a96b2e955b7ed725bea7af6e804" + integrity sha512-ufyO+MrbY7+0TDm/1cDl9iAeR4P8jt0AM1F9wiCBHVIYtj1wMD4eNm7G5Am3u8p1ruMjRhi6dJEVQcRF2O+LUg== dependencies: - "@across-protocol/constants" "^3.1.24" + "@across-protocol/constants" "^3.1.27" "@coral-xyz/anchor" "^0.30.1" "@defi-wonderland/smock" "^2.3.4" "@eth-optimism/contracts" "^0.5.40" @@ -58,14 +53,14 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.3.32": - version "3.3.32" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.3.32.tgz#fa2428df5f9b6cb0392c46f742f11265efa4abb3" - integrity sha512-ADyZQeWxjGAreLoeVQYNiJN4zMmmJ7h6ItgbSjP2+JvZENPaH9t23xCegPIyI0oiVqLrOHOGCJ/yEdX6X3HqpQ== +"@across-protocol/sdk@^3.4.9": + version "3.4.9" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.9.tgz#9e23a9d678e09c97f5b3c60b811ef7d5291d82a4" + integrity sha512-o6WRvjwPgdicpCL8v32Ldp6sPBVC2EObhLn8fSTPn7F/3N7bI0A/8IEAHw3HzSCvMSgRXXcBNObJyZRE5PBMVQ== dependencies: "@across-protocol/across-token" "^1.0.0" - "@across-protocol/constants" "^3.1.25" - "@across-protocol/contracts" "^3.0.19" + "@across-protocol/constants" "^3.1.27" + "@across-protocol/contracts" "^3.0.20" "@eth-optimism/sdk" "^3.3.1" "@ethersproject/bignumber" "^5.7.0" "@pinata/sdk" "^2.1.0"