From 536b39458e60bdf22cdc64e1d657b092a7136578 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:19:57 -0400 Subject: [PATCH 1/5] feat(inventory-manager): Wrap ETH on Arbitrum (#969) * feat(inventory-manager): Wrap ETH on Arbitrum We expect ETH to be sent to the relayer address on Arbitrum because its [hardcoded](https://github.com/across-protocol/contracts-v2/blob/3634df5ec327326ce030629aa0a089f651f20bc0/contracts/chain-adapters/Arbitrum_Adapter.sol#L158) as a refund address for all HubPool messages that get sent to the ArbitrumSpoke. * fix: Restore privateKey wallet validation (#968) privateKey is case sensitive. * fix asserts * WIP * Update src/clients/bridges/BaseAdapter.ts Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> * Update src/clients/bridges/BaseAdapter.ts Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> * Update BaseAdapter.ts * Add configurable target/threshold * lint --------- Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> --- src/clients/InventoryClient.ts | 2 +- src/clients/bridges/AdapterManager.ts | 24 +++++++++----- src/clients/bridges/ArbitrumAdapter.ts | 24 ++++++++++++-- src/clients/bridges/BaseAdapter.ts | 15 ++++++--- src/clients/bridges/ZKSyncAdapter.ts | 10 ++++-- .../bridges/op-stack/OpStackAdapter.ts | 12 ++++--- src/common/ContractAddresses.ts | 14 ++++++++ src/interfaces/InventoryManagement.ts | 10 +++++- src/relayer/RelayerConfig.ts | 32 +++++++++++++++++++ 9 files changed, 120 insertions(+), 23 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 89d49e719..4eebb0ebf 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -612,7 +612,7 @@ export class InventoryClient { return; } this.log("Checking ETH->WETH Wrap status"); - await this.adapterManager.wrapEthIfAboveThreshold(this.inventoryConfig.wrapEtherThreshold, this.simMode); + await this.adapterManager.wrapEthIfAboveThreshold(this.inventoryConfig, this.simMode); } async update(): Promise { diff --git a/src/clients/bridges/AdapterManager.ts b/src/clients/bridges/AdapterManager.ts index 4c7ad5ca6..d3329d518 100644 --- a/src/clients/bridges/AdapterManager.ts +++ b/src/clients/bridges/AdapterManager.ts @@ -1,8 +1,9 @@ -import { BigNumber, isDefined, winston, Signer, getL2TokenAddresses, TransactionResponse } from "../../utils"; +import { BigNumber, isDefined, winston, Signer, getL2TokenAddresses, TransactionResponse, assert } from "../../utils"; import { SpokePoolClient, HubPoolClient } from "../"; import { OptimismAdapter, ArbitrumAdapter, PolygonAdapter, BaseAdapter, ZKSyncAdapter } from "./"; -import { OutstandingTransfers } from "../../interfaces"; +import { InventoryConfig, OutstandingTransfers } from "../../interfaces"; import { utils } from "@across-protocol/sdk-v2"; +import { CHAIN_IDs } from "@across-protocol/constants-v2"; import { BaseChainAdapter } from "./op-stack/base/BaseChainAdapter"; import { spokesThatHoldEthAndWeth } from "../../common/Constants"; export class AdapterManager { @@ -10,9 +11,9 @@ export class AdapterManager { // Some L2's canonical bridges send ETH, not WETH, over the canonical bridges, resulting in recipient addresses // receiving ETH that needs to be wrapped on the L2. This array contains the chainIds of the chains that this - // manager will attempt to wrap ETH on into WETH. This is not necessary for chains that receive WETH, the ERC20, - // over the bridge. - public chainsToWrapEtherOn = spokesThatHoldEthAndWeth; + // manager will attempt to wrap ETH on into WETH. This list also includes chains like Arbitrum where the relayer is + // expected to receive ETH as a gas refund from an L1 to L2 deposit that was intended to rebalance inventory. + public chainsToWrapEtherOn = [...spokesThatHoldEthAndWeth, CHAIN_IDs.ARBITRUM]; constructor( readonly logger: winston.Logger, @@ -77,12 +78,19 @@ export class AdapterManager { // Check how much ETH is on the target chain and if it is above the threshold the wrap it to WETH. Note that this only // needs to be done on chains where rebalancing WETH from L1 to L2 results in the relayer receiving ETH - // (not the ERC20). - async wrapEthIfAboveThreshold(wrapThreshold: BigNumber, simMode = false): Promise { + // (not the ERC20), or if the relayer expects to be sent ETH perhaps as a gas refund from an original L1 to L2 + // deposit. + async wrapEthIfAboveThreshold(inventoryConfig: InventoryConfig, simMode = false): Promise { await utils.mapAsync( this.chainsToWrapEtherOn.filter((chainId) => isDefined(this.spokePoolClients[chainId])), async (chainId) => { - await this.adapters[chainId].wrapEthIfAboveThreshold(wrapThreshold, simMode); + const wrapThreshold = inventoryConfig.wrapEtherThresholdPerChain[chainId] ?? inventoryConfig.wrapEtherThreshold; + const wrapTarget = inventoryConfig.wrapEtherTargetPerChain[chainId] ?? inventoryConfig.wrapEtherTarget; + assert( + wrapThreshold.gte(wrapTarget), + `wrapEtherThreshold ${wrapThreshold.toString()} must be >= wrapEtherTarget ${wrapTarget.toString()}` + ); + await this.adapters[chainId].wrapEthIfAboveThreshold(wrapThreshold, wrapTarget, simMode); } ); } diff --git a/src/clients/bridges/ArbitrumAdapter.ts b/src/clients/bridges/ArbitrumAdapter.ts index 4851e882b..68598f5aa 100644 --- a/src/clients/bridges/ArbitrumAdapter.ts +++ b/src/clients/bridges/ArbitrumAdapter.ts @@ -13,6 +13,7 @@ import { toWei, paginatedEventQuery, Event, + assert, } from "../../utils"; import { SpokePoolClient } from "../../clients"; import { BaseAdapter } from "./BaseAdapter"; @@ -194,8 +195,27 @@ export class ArbitrumAdapter extends BaseAdapter { ); } - async wrapEthIfAboveThreshold(): Promise { - throw new Error("Unnecessary to wrap ETH on Arbitrum"); + // The arbitrum relayer expects to receive ETH steadily per HubPool bundle processed, since it is the L2 refund + // address hardcoded in the Arbitrum Adapter. + async wrapEthIfAboveThreshold( + threshold: BigNumber, + target: BigNumber, + simMode = false + ): Promise { + const { chainId } = this; + assert(42161 === chainId, `chainId ${chainId} is not supported`); + + const weth = CONTRACT_ADDRESSES[this.chainId].weth; + const ethBalance = await this.getSigner(chainId).getBalance(); + + if (ethBalance.gt(threshold)) { + const l2Signer = this.getSigner(chainId); + const contract = new Contract(weth.address, weth.abi, l2Signer); + const value = ethBalance.sub(target); + this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, target, value, ethBalance }); + return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode); + } + return null; } getL1Bridge(l1Token: SupportedL1Token): Contract { diff --git a/src/clients/bridges/BaseAdapter.ts b/src/clients/bridges/BaseAdapter.ts index 4a2f8867c..0added6dc 100644 --- a/src/clients/bridges/BaseAdapter.ts +++ b/src/clients/bridges/BaseAdapter.ts @@ -324,10 +324,13 @@ export abstract class BaseAdapter { ): Promise { const { chainId, txnClient } = this; const method = "deposit"; + const formatFunc = createFormatFunction(2, 4, false, 18); const mrkdwn = - `Ether on chain ${this.chainId} was wrapped due to being over the threshold of ` + - `${createFormatFunction(2, 4, false, 18)(toBN(wrapThreshold).toString())} ETH.`; - const message = `Eth wrapped on target chain ${this.chainId}🎁`; + `${formatFunc( + toBN(value).toString() + )} Ether on chain ${chainId} was wrapped due to being over the threshold of ` + + `${formatFunc(toBN(wrapThreshold).toString())} ETH.`; + const message = `${formatFunc(toBN(value).toString())} Eth wrapped on target chain ${chainId}🎁`; if (simMode) { const { succeed, reason } = ( await txnClient.simulate([{ contract: l2WEthContract, chainId, method, args: [], value, mrkdwn, message }]) @@ -362,5 +365,9 @@ export abstract class BaseAdapter { abstract checkTokenApprovals(address: string, l1Tokens: string[]): Promise; - abstract wrapEthIfAboveThreshold(threshold: BigNumber, simMode: boolean): Promise; + abstract wrapEthIfAboveThreshold( + threshold: BigNumber, + target: BigNumber, + simMode: boolean + ): Promise; } diff --git a/src/clients/bridges/ZKSyncAdapter.ts b/src/clients/bridges/ZKSyncAdapter.ts index e4c3a8723..9a6214005 100644 --- a/src/clients/bridges/ZKSyncAdapter.ts +++ b/src/clients/bridges/ZKSyncAdapter.ts @@ -229,7 +229,11 @@ export class ZKSyncAdapter extends BaseAdapter { * @param threshold * @returns */ - async wrapEthIfAboveThreshold(threshold: BigNumber, simMode = false): Promise { + async wrapEthIfAboveThreshold( + threshold: BigNumber, + target: BigNumber, + simMode = false + ): Promise { const { chainId } = this; assert(chainId === 324, `chainId ${chainId} is not supported`); @@ -239,8 +243,8 @@ export class ZKSyncAdapter extends BaseAdapter { const l2Signer = this.getSigner(chainId); // @dev Can re-use ABI from L1 weth as its the same for the purposes of this function. const contract = new Contract(l2WethAddress, CONTRACT_ADDRESSES[this.hubChainId].weth.abi, l2Signer); - const value = ethBalance.sub(threshold); - this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, value, ethBalance }); + const value = ethBalance.sub(target); + this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, target, value, ethBalance }); return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode); } return null; diff --git a/src/clients/bridges/op-stack/OpStackAdapter.ts b/src/clients/bridges/op-stack/OpStackAdapter.ts index cfff52c8b..c5f2fe268 100644 --- a/src/clients/bridges/op-stack/OpStackAdapter.ts +++ b/src/clients/bridges/op-stack/OpStackAdapter.ts @@ -138,17 +138,21 @@ export class OpStackAdapter extends BaseAdapter { ); } - async wrapEthIfAboveThreshold(threshold: BigNumber, simMode = false): Promise { + async wrapEthIfAboveThreshold( + threshold: BigNumber, + target: BigNumber, + simMode = false + ): Promise { const { chainId } = this; - assert(chainId === this.chainId, `chainId ${chainId} is not supported`); + assert([10, 8453].includes(chainId), `chainId ${chainId} is not supported`); const ovmWeth = CONTRACT_ADDRESSES[this.chainId].weth; const ethBalance = await this.getSigner(chainId).getBalance(); if (ethBalance.gt(threshold)) { const l2Signer = this.getSigner(chainId); const contract = new Contract(ovmWeth.address, ovmWeth.abi, l2Signer); - const value = ethBalance.sub(threshold); - this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, value, ethBalance }); + const value = ethBalance.sub(target); + this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, target, value, ethBalance }); return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode); } return null; diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 1a6b5e9d4..b7b5d1a54 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -695,6 +695,20 @@ export const CONTRACT_ADDRESSES: { }, ], }, + weth: { + address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + abi: [ + { + constant: false, + inputs: [], + name: "deposit", + outputs: [], + payable: true, + stateMutability: "payable", + type: "function", + }, + ], + }, outbox: { address: "0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840", abi: [ diff --git a/src/interfaces/InventoryManagement.ts b/src/interfaces/InventoryManagement.ts index 1874f6da2..89dc3de7e 100644 --- a/src/interfaces/InventoryManagement.ts +++ b/src/interfaces/InventoryManagement.ts @@ -12,5 +12,13 @@ export interface InventoryConfig { }; }; }; - wrapEtherThreshold: BigNumber; // Number of Ether, that if the balance is above, wrap it to WETH on the L2. in wei + // If ETH balance on chain is above threshold, wrap the excess over the target to WETH. + wrapEtherTargetPerChain: { + [chainId: number]: BigNumber; + }; + wrapEtherTarget: BigNumber; + wrapEtherThresholdPerChain: { + [chainId: number]: BigNumber; + }; + wrapEtherThreshold: BigNumber; } diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index 19801228f..c6dabe3ef 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -73,7 +73,39 @@ export class RelayerConfig extends CommonConfig { this.inventoryConfig.wrapEtherThreshold = this.inventoryConfig.wrapEtherThreshold ? toBNWei(this.inventoryConfig.wrapEtherThreshold) : toBNWei(1); // default to keeping 2 Eth on the target chains and wrapping the rest to WETH. + this.inventoryConfig.wrapEtherThresholdPerChain ??= {}; + this.inventoryConfig.wrapEtherTarget = this.inventoryConfig.wrapEtherTarget + ? toBNWei(this.inventoryConfig.wrapEtherTarget) + : this.inventoryConfig.wrapEtherThreshold; // default to wrapping ETH to threshold, same as target. + this.inventoryConfig.wrapEtherTargetPerChain ??= {}; + assert( + this.inventoryConfig.wrapEtherThreshold.gte(this.inventoryConfig.wrapEtherTarget), + `default wrapEtherThreshold ${this.inventoryConfig.wrapEtherThreshold} must be >= default wrapEtherTarget ${this.inventoryConfig.wrapEtherTarget}}` + ); + // Validate the per chain target and thresholds for wrapping ETH: + Object.keys(this.inventoryConfig.wrapEtherThresholdPerChain).forEach((chainId) => { + if (this.inventoryConfig.wrapEtherThresholdPerChain[chainId] !== undefined) { + this.inventoryConfig.wrapEtherThresholdPerChain[chainId] = toBNWei( + this.inventoryConfig.wrapEtherThresholdPerChain[chainId] + ); + } + }); + Object.keys(this.inventoryConfig.wrapEtherTargetPerChain).forEach((chainId) => { + if (this.inventoryConfig.wrapEtherTargetPerChain[chainId] !== undefined) { + this.inventoryConfig.wrapEtherTargetPerChain[chainId] = toBNWei( + this.inventoryConfig.wrapEtherTargetPerChain[chainId] + ); + // Check newly set target against threshold + const threshold = + this.inventoryConfig.wrapEtherThresholdPerChain[chainId] ?? this.inventoryConfig.wrapEtherThreshold; + const target = this.inventoryConfig.wrapEtherTargetPerChain[chainId]; + assert( + threshold.gte(target), + `wrapEtherThresholdPerChain ${threshold.toString()} must be >= wrapEtherTargetPerChain ${target}` + ); + } + }); Object.keys(this.inventoryConfig.tokenConfig).forEach((l1Token) => { Object.keys(this.inventoryConfig.tokenConfig[l1Token]).forEach((chainId) => { const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget } = From 3d0c8770f16659fa38a9edd8ea0e22d751578a61 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:32:43 +0200 Subject: [PATCH 2/5] improve: Retry Across API /limits on failure (#971) AcrossAPIClient.update() currently only tries to query the first available route for a token. In instances where the RPC provider for this chain is unavailable, this results in the client resolving a maxDeposit of 0. Instead, iterate over the set of all available chains for this token until one route results in a usable response. This should boost robustness in the bots. While here, tweak the instantiation of the AcrossAPIClient. The existing implementation requires RELAYER_DESTINATION_CHAINS to be defined. Per this PR, if RELAYER_DESTINATION_CHAINS is not defined, just use the full set of available SpokePoolClient instances. --- src/clients/AcrossAPIClient.ts | 78 +++++++++++++++++------------- src/relayer/RelayerClientHelper.ts | 13 +++-- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/clients/AcrossAPIClient.ts b/src/clients/AcrossAPIClient.ts index c202d2449..992322105 100644 --- a/src/clients/AcrossAPIClient.ts +++ b/src/clients/AcrossAPIClient.ts @@ -1,10 +1,13 @@ -import { winston, BigNumber, getL2TokenAddresses } from "../utils"; +import { isDefined, winston, BigNumber, getL2TokenAddresses } from "../utils"; import axios, { AxiosError } from "axios"; import { HubPoolClient } from "./HubPoolClient"; +import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2"; import { SpokePoolClientsByChain } from "../interfaces"; import _ from "lodash"; +const { bnZero } = sdkUtils; + export interface DepositLimits { maxDeposit: BigNumber; } @@ -58,36 +61,42 @@ export class AcrossApiClient { if (!mainnetSpokePoolClient.isUpdated) { throw new Error("Mainnet SpokePoolClient for chainId must be updated before AcrossAPIClient"); } + const data = await Promise.all( tokensQuery.map((l1Token) => { const l2TokenAddresses = getL2TokenAddresses(l1Token); - const validDestinationChainForL1Token = Object.keys(l2TokenAddresses).find((chainId) => { - return ( - mainnetSpokePoolClient.isDepositRouteEnabled(l1Token, Number(chainId)) && - Number(chainId) !== CHAIN_IDs.MAINNET && - Object.keys(this.spokePoolClients).includes(chainId) - ); - }); + const destinationChains = Object.keys(l2TokenAddresses) + .map((chainId) => Number(chainId)) + .filter((chainId) => { + return ( + chainId !== CHAIN_IDs.MAINNET && + mainnetSpokePoolClient.isDepositRouteEnabled(l1Token, chainId) && + Object.keys(this.spokePoolClients).includes(chainId.toString()) + ); + }); + // No valid deposit routes from mainnet for this token. We won't record a limit for it. - if (validDestinationChainForL1Token === undefined) { + if (destinationChains.length === 0) { return undefined; } - return this.callLimits(l1Token, Number(validDestinationChainForL1Token)); + + return this.callLimits(l1Token, destinationChains); }) ); - for (let i = 0; i < tokensQuery.length; i++) { + + tokensQuery.forEach((token, i) => { const resolvedData = data[i]; - if (resolvedData === undefined) { + if (isDefined(resolvedData)) { + this.limits[token] = data[i].maxDeposit; + } else { this.logger.debug({ at: "AcrossAPIClient", message: "No valid deposit routes for enabled LP token, skipping", - token: tokensQuery[i], + token, }); - continue; } - const l1Token = tokensQuery[i]; - this.limits[l1Token] = resolvedData.maxDeposit; - } + }); + this.logger.debug({ at: "AcrossAPIClient", message: "🏁 Fetched max deposit limits", @@ -105,26 +114,29 @@ export class AcrossApiClient { private async callLimits( l1Token: string, - destinationChainId: number, + destinationChainIds: number[], timeout = this.timeout ): Promise { const path = "limits"; const url = `${this.endpoint}/${path}`; - const params = { token: l1Token, destinationChainId, originChainId: 1 }; - - try { - const result = await axios(url, { timeout, params }); - return result.data; - } catch (err) { - const msg = _.get(err, "response.data", _.get(err, "response.statusText", (err as AxiosError).message)); - this.logger.warn({ - at: "AcrossAPIClient", - message: "Failed to get /limits, setting limit to 0", - url, - params, - msg, - }); - return { maxDeposit: BigNumber.from(0) }; + + for (const destinationChainId of destinationChainIds) { + const params = { token: l1Token, destinationChainId, originChainId: 1 }; + try { + const result = await axios(url, { timeout, params }); + return result.data; + } catch (err) { + const msg = _.get(err, "response.data", _.get(err, "response.statusText", (err as AxiosError).message)); + this.logger.warn({ + at: "AcrossAPIClient", + message: "Failed to get /limits", + url, + params, + msg, + }); + } } + + return { maxDeposit: bnZero }; } } diff --git a/src/relayer/RelayerClientHelper.ts b/src/relayer/RelayerClientHelper.ts index a407a1260..ca98eaaf5 100644 --- a/src/relayer/RelayerClientHelper.ts +++ b/src/relayer/RelayerClientHelper.ts @@ -51,11 +51,14 @@ export async function constructRelayerClients( // We only use the API client to load /limits for chains so we should remove any chains that are not included in the // destination chain list. - const destinationSpokePoolClients = Object.fromEntries( - Object.keys(spokePoolClients) - .filter((chainId) => config.relayerDestinationChains.includes(Number(chainId))) - .map((chainId) => [chainId, spokePoolClients[chainId]]) - ); + const destinationSpokePoolClients = + config.relayerDestinationChains.length === 0 + ? spokePoolClients + : Object.fromEntries( + Object.keys(spokePoolClients) + .filter((chainId) => config.relayerDestinationChains.includes(Number(chainId))) + .map((chainId) => [chainId, spokePoolClients[chainId]]) + ); const acrossApiClient = new AcrossApiClient( logger, From 640ef8c44a2a59d8379d1edc19d19dcaa3f406d0 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:16:53 +0200 Subject: [PATCH 3/5] improve: Update MockProfitClient ahead of changes (#976) Done in preparation for follow-on changes relating to Across messaging: - Automatically setup gas tokens for known test chains, rather than requiring the user to call MockProfitClient.testInit(). - Add singular setter-like methods for token prices and gas costs. - Remove some redundant type annotations. --- src/clients/ProfitClient.ts | 6 ++-- test/Relayer.BasicFill.ts | 12 ++++--- test/Relayer.RefundRequests.ts | 1 - test/Relayer.TokenShortfall.ts | 1 - test/Relayer.UnfilledDeposits.ts | 1 - test/mocks/MockProfitClient.ts | 61 +++++++++++++++++++++++--------- 6 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/clients/ProfitClient.ts b/src/clients/ProfitClient.ts index 6ba204268..303dd92b2 100644 --- a/src/clients/ProfitClient.ts +++ b/src/clients/ProfitClient.ts @@ -97,9 +97,9 @@ export class ProfitClient { readonly hubPoolClient: HubPoolClient, spokePoolClients: SpokePoolClientsByChain, readonly enabledChainIds: number[], - readonly defaultMinRelayerFeePct: BigNumber = toBNWei(constants.RELAYER_MIN_FEE_PCT), - readonly debugProfitability: boolean = false, - protected gasMultiplier: BigNumber = toBNWei(1) + readonly defaultMinRelayerFeePct = toBNWei(constants.RELAYER_MIN_FEE_PCT), + readonly debugProfitability = false, + protected gasMultiplier = toBNWei(1) ) { // Require 1% <= gasMultiplier <= 400% assert( diff --git a/test/Relayer.BasicFill.ts b/test/Relayer.BasicFill.ts index ecad12c18..4402f5ec3 100644 --- a/test/Relayer.BasicFill.ts +++ b/test/Relayer.BasicFill.ts @@ -38,7 +38,9 @@ import { winston, } from "./utils"; import { generateNoOpSpokePoolClientsForDefaultChainIndices } from "./utils/UBAUtils"; -import { clients } from "@across-protocol/sdk-v2"; +import { clients, utils as sdkUtils } from "@across-protocol/sdk-v2"; + +const { bnOne } = sdkUtils; let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract; let hubPool: Contract, configStore: Contract, l1Token: Contract; @@ -117,9 +119,8 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () { [destinationChainId]: spokePoolClient_2, }; - for (const spokePoolClient of Object.values(spokePoolClients)) { - await spokePoolClient.update(); - } + // Update all SpokePoolClient instances. + await Promise.all(Object.values(spokePoolClients).map((spokePoolClient) => spokePoolClient.update())); // We will need to update the config store client at least once await configStoreClient.update(); @@ -131,7 +132,8 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () { ); tokenClient = new TokenClient(spyLogger, relayer.address, spokePoolClients, hubPoolClient); profitClient = new MockProfitClient(spyLogger, hubPoolClient, spokePoolClients, []); - profitClient.testInit(); + profitClient.setTokenPrice(l1Token.address, bnOne); + Object.values(spokePoolClients).map((spokePoolClient) => profitClient.setGasCost(spokePoolClient.chainId, bnOne)); relayerInstance = new Relayer( relayer.address, diff --git a/test/Relayer.RefundRequests.ts b/test/Relayer.RefundRequests.ts index 0e113079a..14f59bd0f 100644 --- a/test/Relayer.RefundRequests.ts +++ b/test/Relayer.RefundRequests.ts @@ -161,7 +161,6 @@ describe("Relayer: Request refunds for cross-chain repayments", async function ( ); tokenClient = new TokenClient(spyLogger, relayer.address, spokePoolClients, hubPoolClient); profitClient = new MockProfitClient(spyLogger, hubPoolClient, spokePoolClients, []); - profitClient.testInit(); relayerInstance = new Relayer( relayer.address, diff --git a/test/Relayer.TokenShortfall.ts b/test/Relayer.TokenShortfall.ts index 3adfa6a88..8e106453b 100644 --- a/test/Relayer.TokenShortfall.ts +++ b/test/Relayer.TokenShortfall.ts @@ -109,7 +109,6 @@ describe("Relayer: Token balance shortfall", async function () { const spokePoolClients = { [originChainId]: spokePoolClient_1, [destinationChainId]: spokePoolClient_2 }; tokenClient = new TokenClient(spyLogger, relayer.address, spokePoolClients, hubPoolClient); profitClient = new MockProfitClient(spyLogger, hubPoolClient, spokePoolClients, []); - profitClient.testInit(); relayerInstance = new Relayer( relayer.address, diff --git a/test/Relayer.UnfilledDeposits.ts b/test/Relayer.UnfilledDeposits.ts index 077d5881b..8314de9a7 100644 --- a/test/Relayer.UnfilledDeposits.ts +++ b/test/Relayer.UnfilledDeposits.ts @@ -109,7 +109,6 @@ describe("Relayer: Unfilled Deposits", async function () { multiCallerClient = new MockedMultiCallerClient(spyLogger); tokenClient = new TokenClient(spyLogger, relayer.address, spokePoolClients, hubPoolClient); profitClient = new MockProfitClient(spyLogger, hubPoolClient, spokePoolClients, []); - profitClient.testInit(); relayerInstance = new Relayer( relayer.address, spyLogger, diff --git a/test/mocks/MockProfitClient.ts b/test/mocks/MockProfitClient.ts index a104d44c4..eef920c81 100644 --- a/test/mocks/MockProfitClient.ts +++ b/test/mocks/MockProfitClient.ts @@ -1,16 +1,54 @@ -import { BigNumber, toBN, toBNWei } from "../utils"; -import { GAS_TOKEN_BY_CHAIN_ID, ProfitClient, WETH } from "../../src/clients"; +import { utils as sdkUtils } from "@across-protocol/sdk-v2"; +import { GAS_TOKEN_BY_CHAIN_ID, HubPoolClient, ProfitClient, WETH } from "../../src/clients"; +import { SpokePoolClientsByChain } from "../../src/interfaces"; +import { BigNumber, toBNWei, winston } from "../utils"; export class MockProfitClient extends ProfitClient { + constructor( + logger: winston.Logger, + hubPoolClient: HubPoolClient, + spokePoolClients: SpokePoolClientsByChain, + enabledChainIds: number[], + defaultMinRelayerFeePct?: BigNumber, + debugProfitability?: boolean, + gasMultiplier?: BigNumber + ) { + super( + logger, + hubPoolClient, + spokePoolClients, + enabledChainIds, + defaultMinRelayerFeePct, + debugProfitability, + gasMultiplier + ); + + // Some tests run against mocked chains, so hack in the necessary parts + const chainIds = [666, 1337]; + chainIds.forEach((chainId) => { + GAS_TOKEN_BY_CHAIN_ID[chainId] = WETH; + this.setGasCost(chainId, toBNWei(chainId)); + }); + this.setTokenPrice(WETH, sdkUtils.bnOne); + } + + setTokenPrice(l1Token: string, price: BigNumber | undefined): void { + if (price) { + this.tokenPrices[l1Token] = price; + } else { + delete this.tokenPrices[l1Token]; + } + } + setTokenPrices(tokenPrices: { [l1Token: string]: BigNumber }): void { this.tokenPrices = tokenPrices; } - getPriceOfToken(l1Token: string): BigNumber { - if (this.tokenPrices[l1Token] === undefined) { - return toBNWei(1); + setGasCost(chainId: number, gas: BigNumber | undefined): void { + if (gas) { + this.totalGasCosts[chainId] = gas; } else { - return this.tokenPrices[l1Token]; + delete this.totalGasCosts[chainId]; } } @@ -22,17 +60,6 @@ export class MockProfitClient extends ProfitClient { this.gasMultiplier = gasMultiplier; } - // Some tests run against mocked chains, so hack in the necessary parts - testInit(): void { - GAS_TOKEN_BY_CHAIN_ID[666] = WETH; - GAS_TOKEN_BY_CHAIN_ID[1337] = WETH; - - this.setGasCosts({ - 666: toBN(100_000), - 1337: toBN(100_000), - }); - } - // eslint-disable-next-line @typescript-eslint/no-empty-function async update(): Promise {} } From c1dd7f63c3077441e2f45481b6b7f89369e371dd Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:17:36 +0200 Subject: [PATCH 4/5] refactor: Clean up ProfitClient test (#977) Done in preparation for follow-on changes relating to Across messaging: - Automatically setup gas tokens for known test chains, rather than requiring the user to call MockProfitClient.testInit(). - Add singular setter-like methods for token prices and gas costs. - Remove some redundant type annotations. --- test/ProfitClient.ConsiderProfitability.ts | 221 ++++++++++----------- 1 file changed, 106 insertions(+), 115 deletions(-) diff --git a/test/ProfitClient.ConsiderProfitability.ts b/test/ProfitClient.ConsiderProfitability.ts index 97431c878..6b8455a33 100644 --- a/test/ProfitClient.ConsiderProfitability.ts +++ b/test/ProfitClient.ConsiderProfitability.ts @@ -1,3 +1,4 @@ +import { assert } from "chai"; import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import { ConfigStoreClient, @@ -24,11 +25,18 @@ import { winston, } from "./utils"; -const { fixedPointAdjustment: fixedPoint } = sdkUtils; +const { bnZero, fixedPointAdjustment: fixedPoint, toGWei } = sdkUtils; const { formatEther } = ethers.utils; -const chainIds: number[] = [1, 10, 137, 288, 42161]; -const zeroRefundFee = toBN(0); +const chainIds = [originChainId, destinationChainId]; +const zeroRefundFee = bnZero; + +const minRelayerFeeBps = 3; +const minRelayerFeePct = toBNWei(minRelayerFeeBps).div(1e4); + +// relayerFeePct clamped at 50 bps by each SpokePool. +const maxRelayerFeeBps = 50; +const maxRelayerFeePct = toBNWei(maxRelayerFeeBps).div(1e4); const tokens: { [symbol: string]: L1Token } = { MATIC: { address: MATIC, decimals: 18, symbol: "MATIC" }, @@ -47,8 +55,8 @@ const tokenPrices: { [symbol: string]: BigNumber } = { // Quirk: Use the chainId as the gas price in Gwei. This gives a range of // gas prices to test with, since there's a spread in the chainId numbers. const gasCost: { [chainId: number]: BigNumber } = Object.fromEntries( - chainIds.map((chainId: number) => { - const nativeGasPrice = toBN(chainId).mul(1e9); // Gwei + chainIds.map((chainId) => { + const nativeGasPrice = toGWei(chainId); const gasConsumed = toBN(100_000); // Assume 100k gas for a single fill return [chainId, gasConsumed.mul(nativeGasPrice)]; }) @@ -61,17 +69,6 @@ function setDefaultTokenPrices(profitClient: MockProfitClient): void { ); } -const minRelayerFeeBps = 3; -const minRelayerFeePct = toBNWei(minRelayerFeeBps).div(1e4); - -// relayerFeePct clamped at 50 bps by each SpokePool. -const maxRelayerFeeBps = 50; -const maxRelayerFeePct = toBNWei(maxRelayerFeeBps).div(1e4); - -// Define LOG_IN_TEST for logging to console. -const { spyLogger }: { spyLogger: winston.Logger } = createSpyLogger(); -let hubPoolClient: MockHubPoolClient, profitClient: MockProfitClient; - function testProfitability( deposit: Deposit, fillAmountUsd: BigNumber, @@ -98,8 +95,12 @@ function testProfitability( } as FillProfit; } -describe("ProfitClient: Consider relay profit", async function () { - beforeEach(async function () { +describe("ProfitClient: Consider relay profit", () => { + // Define LOG_IN_TEST for logging to console. + const { spyLogger }: { spyLogger: winston.Logger } = createSpyLogger(); + let hubPoolClient: MockHubPoolClient, profitClient: MockProfitClient; + + beforeEach(async () => { const [owner] = await ethers.getSigners(); const logger = createSpyLogger().spyLogger; @@ -113,33 +114,27 @@ describe("ProfitClient: Consider relay profit", async function () { await hubPoolClient.update(); - const { spokePool: spokePool_1, deploymentBlock: spokePool1DeploymentBlock } = await deploySpokePoolWithToken( - originChainId, - destinationChainId - ); - const { spokePool: spokePool_2, deploymentBlock: spokePool2DeploymentBlock } = await deploySpokePoolWithToken( - destinationChainId, - originChainId - ); - - const spokePoolClient_1 = new SpokePoolClient( - spyLogger, - spokePool_1.connect(owner), - null, - originChainId, - spokePool1DeploymentBlock - ); - const spokePoolClient_2 = new SpokePoolClient( - spyLogger, - spokePool_2.connect(owner), - null, - destinationChainId, - spokePool2DeploymentBlock + const chainIds = [originChainId, destinationChainId]; + assert(chainIds.length === 2, "SpokePool deployment requires only 2 chainIds"); + const spokePoolClients = Object.fromEntries( + await sdkUtils.mapAsync(chainIds, async (spokeChainId, idx) => { + const { spokePool, deploymentBlock } = await deploySpokePoolWithToken( + spokeChainId, + chainIds[(idx + 1) % 2] // @dev Only works for 2 chainIds. + ); + const spokePoolClient = new SpokePoolClient( + spyLogger, + spokePool.connect(owner), + null, + spokeChainId, + deploymentBlock + ); + + return [spokeChainId, spokePoolClient]; + }) ); - const spokePoolClients = { [originChainId]: spokePoolClient_1, [destinationChainId]: spokePoolClient_2 }; const debugProfitability = true; - profitClient = new MockProfitClient( spyLogger, hubPoolClient, @@ -151,35 +146,35 @@ describe("ProfitClient: Consider relay profit", async function () { // Load per-chain gas cost (gas consumed * gas price in Wei), in native gas token. profitClient.setGasCosts(gasCost); - setDefaultTokenPrices(profitClient); }); // Verify gas cost calculation first, so we can leverage it in all subsequent tests. - it("Verify gas cost estimation", function () { - chainIds.forEach((chainId: number) => { + it("Verify gas cost estimation", () => { + chainIds.forEach((chainId) => { spyLogger.debug({ message: `Verifying USD fill cost calculation for chain ${chainId}.` }); const nativeGasCost = profitClient.getTotalGasCost(chainId); expect(nativeGasCost.eq(0)).to.be.false; expect(nativeGasCost.eq(gasCost[chainId])).to.be.true; - const gasTokenAddr: string = GAS_TOKEN_BY_CHAIN_ID[chainId]; - const gasToken: L1Token = Object.values(tokens).find((token: L1Token) => gasTokenAddr === token.address); + const gasTokenAddr = GAS_TOKEN_BY_CHAIN_ID[chainId]; + let gasToken = Object.values(tokens).find((token) => gasTokenAddr === token.address); expect(gasToken).to.not.be.undefined; + gasToken = gasToken as L1Token; const gasPriceUsd = tokenPrices[gasToken.symbol]; expect(gasPriceUsd.eq(tokenPrices[gasToken.symbol])).to.be.true; - const estimate: { [key: string]: BigNumber } = profitClient.estimateFillCost(chainId); + const estimate = profitClient.estimateFillCost(chainId); expect(estimate.nativeGasCost.eq(gasCost[chainId])).to.be.true; expect(estimate.gasPriceUsd.eq(tokenPrices[gasToken.symbol])).to.be.true; expect(estimate.gasCostUsd.eq(gasPriceUsd.mul(nativeGasCost).div(toBN(10).pow(gasToken.decimals)))).to.be.true; }); }); - it("Verify gas multiplier", function () { - chainIds.forEach((chainId: number) => { + it("Verify gas multiplier", () => { + chainIds.forEach((chainId) => { spyLogger.debug({ message: `Verifying gas multiplier for chainId ${chainId}.` }); const nativeGasCost = profitClient.getTotalGasCost(chainId); @@ -190,7 +185,9 @@ describe("ProfitClient: Consider relay profit", async function () { profitClient.setGasMultiplier(toBNWei(gasMultiplier)); const gasTokenAddr = GAS_TOKEN_BY_CHAIN_ID[chainId]; - const gasToken: L1Token = Object.values(tokens).find((token: L1Token) => gasTokenAddr === token.address); + let gasToken = Object.values(tokens).find((token) => gasTokenAddr === token.address); + expect(gasToken).to.not.be.undefined; + gasToken = gasToken as L1Token; const expectedFillCostUsd = nativeGasCost .mul(tokenPrices[gasToken.symbol]) @@ -203,43 +200,42 @@ describe("ProfitClient: Consider relay profit", async function () { }); }); - it("Return 0 when gas cost fails to be fetched", async function () { - profitClient.setGasCosts({ 137: undefined }); - expect(profitClient.getTotalGasCost(137)).to.equal(toBN(0)); + it("Return 0 when gas cost fails to be fetched", () => { + const destinationChainId = 137; + profitClient.setGasCost(destinationChainId, undefined); + expect(profitClient.getTotalGasCost(destinationChainId)).to.equal(bnZero); }); - it("Verify token price and gas cost lookup failures", function () { - const l1Token: L1Token = tokens["WETH"]; + it("Verify token price and gas cost lookup failures", () => { const fillAmount = toBNWei(1); - + const l1Token = tokens["WETH"]; hubPoolClient.setTokenInfoToReturn(l1Token); - chainIds.forEach((_chainId: number) => { - const chainId = Number(_chainId); - const deposit = { - relayerFeePct: toBNWei("0.0003"), - destinationChainId: chainId, - } as Deposit; + chainIds.forEach((destinationChainId) => { + const deposit = { destinationChainId, relayerFeePct: toBNWei("0.0003") } as Deposit; // Verify that it works before we break it. expect(() => profitClient.calculateFillProfitability(deposit, fillAmount, zeroRefundFee, l1Token, minRelayerFeePct) ).to.not.throw(); - spyLogger.debug({ message: `Verifying exception on gas cost estimation lookup failure on chain ${chainId}.` }); - profitClient.setGasCosts({}); + spyLogger.debug({ message: `Verify exception on chain ${destinationChainId} gas cost estimation failure.` }); + const destinationGasCost = profitClient.getTotalGasCost(destinationChainId); + profitClient.setGasCost(destinationChainId, undefined); expect(() => profitClient.calculateFillProfitability(deposit, fillAmount, zeroRefundFee, l1Token, minRelayerFeePct) ).to.throw(); - profitClient.setGasCosts(gasCost); + profitClient.setGasCost(destinationChainId, destinationGasCost); + + spyLogger.debug({ message: `Verifying exception on chain ${destinationChainId} token price lookup failure.` }); + const l1TokenPrice = profitClient.getPriceOfToken(l1Token.address); + profitClient.setTokenPrice(l1Token.address, undefined); - spyLogger.debug({ message: `Verifying exception on token price lookup failure on chain ${chainId}.` }); - profitClient.setTokenPrices({ [l1Token.address]: toBN(0) }); // Setting price to 0 causes a downstream error in calculateFillProfitability. expect(() => profitClient.calculateFillProfitability(deposit, fillAmount, zeroRefundFee, l1Token, minRelayerFeePct) ).to.throw(); - setDefaultTokenPrices(profitClient); + profitClient.setTokenPrice(l1Token.address, l1TokenPrice); // Verify we left everything as we found it. expect(() => @@ -262,17 +258,17 @@ describe("ProfitClient: Consider relay profit", async function () { * corner cases. This approach does however require fairly careful analysis of * the test results to make sure that they are sane. Sampling is recommended. */ - it("Considers gas cost when computing profitability", async function () { + it("Considers gas cost when computing profitability", () => { const fillAmounts = [".001", "0.1", 1, 10, 100, 1_000, 100_000]; - chainIds.forEach((destinationChainId: number) => { + chainIds.forEach((destinationChainId) => { const { gasCostUsd } = profitClient.estimateFillCost(destinationChainId); - Object.values(tokens).forEach((l1Token: L1Token) => { + Object.values(tokens).forEach((l1Token) => { const tokenPriceUsd = profitClient.getPriceOfToken(l1Token.address); hubPoolClient.setTokenInfoToReturn(l1Token); - fillAmounts.forEach((_fillAmount: number | string) => { + fillAmounts.forEach((_fillAmount) => { const fillAmount = toBNWei(_fillAmount); const nativeFillAmount = toBNWei(_fillAmount, l1Token.decimals); spyLogger.debug({ message: `Testing fillAmount ${formatEther(fillAmount)}.` }); @@ -280,9 +276,9 @@ describe("ProfitClient: Consider relay profit", async function () { const fillAmountUsd = fillAmount.mul(tokenPriceUsd).div(fixedPoint); const gasCostPct = gasCostUsd.mul(fixedPoint).div(fillAmountUsd); - const relayerFeePcts: BigNumber[] = [ + const relayerFeePcts = [ toBNWei(-1), - toBNWei(0), + bnZero, minRelayerFeePct.div(4), minRelayerFeePct.div(2), minRelayerFeePct, @@ -291,7 +287,7 @@ describe("ProfitClient: Consider relay profit", async function () { minRelayerFeePct.add(gasCostPct).add(toBNWei(1)), ]; - relayerFeePcts.forEach((_relayerFeePct: BigNumber) => { + relayerFeePcts.forEach((_relayerFeePct) => { const relayerFeePct = _relayerFeePct.gt(maxRelayerFeePct) ? maxRelayerFeePct : _relayerFeePct; const deposit = { relayerFeePct, destinationChainId } as Deposit; @@ -318,14 +314,14 @@ describe("ProfitClient: Consider relay profit", async function () { }); }); - it("Considers refund fees when computing profitability", async function () { + it("Considers refund fees when computing profitability", () => { const fillAmounts = [".001", "0.1", 1, 10, 100, 1_000, 100_000]; const refundFeeMultipliers = ["-0.1", "-0.01", "-0.001", "-0.0001", 0.0001, 0.001, 0.01, 0.1, 1]; - chainIds.forEach((destinationChainId: number) => { + chainIds.forEach((destinationChainId) => { const { gasCostUsd } = profitClient.estimateFillCost(destinationChainId); - Object.values(tokens).forEach((l1Token: L1Token) => { + Object.values(tokens).forEach((l1Token) => { const tokenPriceUsd = profitClient.getPriceOfToken(l1Token.address); hubPoolClient.setTokenInfoToReturn(l1Token); @@ -372,7 +368,7 @@ describe("ProfitClient: Consider relay profit", async function () { }); }); - it("Allows per-route and per-token fee configuration", async function () { + it("Allows per-route and per-token fee configuration", () => { // Setup custom USDC pricing to Optimism. chainIds.forEach((srcChainId) => { process.env[`MIN_RELAYER_FEE_PCT_USDC_${srcChainId}_10`] = Math.random().toPrecision(10).toString(); @@ -382,44 +378,39 @@ describe("ProfitClient: Consider relay profit", async function () { const envPrefix = "MIN_RELAYER_FEE_PCT"; ["USDC", "DAI", "WETH", "WBTC"].forEach((symbol) => { chainIds.forEach((srcChainId) => { - chainIds.forEach((dstChainId) => { - if (srcChainId === dstChainId) { - return; - } - - const envVar = `${envPrefix}_${symbol}_${srcChainId}_${dstChainId}`; - const routeFee = process.env[envVar]; - const routeMinRelayerFeePct = routeFee ? toBNWei(routeFee) : minRelayerFeePct; - const computedMinRelayerFeePct = profitClient.minRelayerFeePct(symbol, srcChainId, dstChainId); - spyLogger.debug({ - message: `Expect relayerFeePct === ${routeMinRelayerFeePct}`, - routeFee, - symbol, - srcChainId, - dstChainId, - computedMinRelayerFeePct, - }); + chainIds + .filter((chainId) => chainId !== srcChainId) + .forEach((dstChainId) => { + const envVar = `${envPrefix}_${symbol}_${srcChainId}_${dstChainId}`; + const routeFee = process.env[envVar]; + const routeMinRelayerFeePct = routeFee ? toBNWei(routeFee) : minRelayerFeePct; + const computedMinRelayerFeePct = profitClient.minRelayerFeePct(symbol, srcChainId, dstChainId); + spyLogger.debug({ + message: `Expect relayerFeePct === ${routeMinRelayerFeePct}`, + routeFee, + symbol, + srcChainId, + dstChainId, + computedMinRelayerFeePct, + }); - // Cleanup env as we go. - if (routeFee) { - process.env[envVar] = undefined; - } + // Cleanup env as we go. + if (routeFee) { + process.env[envVar] = undefined; + } - expect(computedMinRelayerFeePct.eq(routeMinRelayerFeePct)).to.be.true; - }); + expect(computedMinRelayerFeePct.eq(routeMinRelayerFeePct)).to.be.true; + }); }); }); }); - it("Considers deposits with newRelayerFeePct", async function () { - const l1Token: L1Token = tokens["WETH"]; + it("Considers deposits with newRelayerFeePct", async () => { + const l1Token = tokens["WETH"]; hubPoolClient.setTokenInfoToReturn(l1Token); const fillAmount = toBNWei(1); - const deposit = { - relayerFeePct: toBNWei("0.001"), - destinationChainId: 1, - } as Deposit; + const deposit = { relayerFeePct: toBNWei("0.001"), destinationChainId } as Deposit; let fill: FillProfit; fill = profitClient.calculateFillProfitability(deposit, fillAmount, zeroRefundFee, l1Token, minRelayerFeePct); @@ -430,14 +421,14 @@ describe("ProfitClient: Consider relay profit", async function () { expect(fill.grossRelayerFeePct.eq(deposit.newRelayerFeePct)).to.be.true; }); - it("Ignores newRelayerFeePct if it's lower than original relayerFeePct", async function () { - const l1Token: L1Token = tokens["WETH"]; + it("Ignores newRelayerFeePct if it's lower than original relayerFeePct", async () => { + const l1Token = tokens["WETH"]; hubPoolClient.setTokenInfoToReturn(l1Token); const deposit = { relayerFeePct: toBNWei("0.1"), newRelayerFeePct: toBNWei("0.01"), - destinationChainId: 1, + destinationChainId, } as Deposit; const fillAmount = toBNWei(1); @@ -446,12 +437,12 @@ describe("ProfitClient: Consider relay profit", async function () { expect(fill.grossRelayerFeePct.eq(deposit.relayerFeePct)).to.be.true; deposit.relayerFeePct = toBNWei(".001"); - expect(deposit.relayerFeePct.lt(deposit.newRelayerFeePct)).to.be.true; // Sanity check + expect(deposit.relayerFeePct.lt(deposit.newRelayerFeePct as BigNumber)).to.be.true; // Sanity check fill = profitClient.calculateFillProfitability(deposit, fillAmount, zeroRefundFee, l1Token, minRelayerFeePct); - expect(fill.grossRelayerFeePct.eq(deposit.newRelayerFeePct)).to.be.true; + expect(fill.grossRelayerFeePct.eq(deposit.newRelayerFeePct as BigNumber)).to.be.true; }); - it("Captures unprofitable fills", async function () { + it("Captures unprofitable fills", () => { const deposit = { relayerFeePct: toBNWei("0.003"), originChainId: 1, depositId: 42 } as DepositWithBlock; profitClient.captureUnprofitableFill(deposit, toBNWei(1)); expect(profitClient.getUnprofitableFills()).to.deep.equal({ 1: [{ deposit, fillAmount: toBNWei(1) }] }); From 1650bfb1b9e7c7c89ac232ccbb90c34e4993d7de Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:22:36 +0200 Subject: [PATCH 5/5] chore: Update SpokePool script supported testnets (#978) --- scripts/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils.ts b/scripts/utils.ts index 99ab46d7f..af7bfde93 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -10,7 +10,7 @@ type ERC20 = { symbol: string; }; -export const testChains = [5, 280]; +export const testChains = [5, 280, 80001, 421613]; export const chains = [1, 10, 137, 324, 8453, 42161]; // Public RPC endpoints to be used if preferred providers are not defined in the environment.