From 110bcb1e903ed70d38ca6c83e78921f6b88b7ca7 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:28:15 +0200 Subject: [PATCH 01/66] refactor(RelayerConfig): Simplify InventoryConfig parsing - Only append unwrapWethThreshold for WETH. - Dereference the l1Token/chainId pair for less repetition. --- src/relayer/RelayerConfig.ts | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index ffd7bd867..a0c99d4a3 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -1,5 +1,15 @@ import { typeguards } from "@across-protocol/sdk-v2"; -import { BigNumber, toBNWei, assert, isDefined, readFileSync, toBN, replaceAddressCase, ethers } from "../utils"; +import { + BigNumber, + toBNWei, + assert, + isDefined, + readFileSync, + toBN, + replaceAddressCase, + ethers, + TOKEN_SYMBOLS_MAP, +} from "../utils"; import { CommonConfig, ProcessEnv } from "../common"; import * as Constants from "../common/Constants"; import { InventoryConfig } from "../interfaces"; @@ -133,8 +143,9 @@ export class RelayerConfig extends CommonConfig { }); Object.keys(this.inventoryConfig?.tokenConfig ?? {}).forEach((l1Token) => { Object.keys(this.inventoryConfig.tokenConfig[l1Token]).forEach((chainId) => { - const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = - this.inventoryConfig.tokenConfig[l1Token][chainId]; + const tokenConfig = this.inventoryConfig.tokenConfig[l1Token][chainId]; + + const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = tokenConfig; assert( targetPct !== undefined && thresholdPct !== undefined, `Bad config. Must specify targetPct, thresholdPct for ${l1Token} on ${chainId}` @@ -143,20 +154,19 @@ export class RelayerConfig extends CommonConfig { toBN(thresholdPct).lte(toBN(targetPct)), `Bad config. thresholdPct<=targetPct for ${l1Token} on ${chainId}` ); - this.inventoryConfig.tokenConfig[l1Token][chainId].targetPct = toBNWei(targetPct).div(100); - this.inventoryConfig.tokenConfig[l1Token][chainId].thresholdPct = toBNWei(thresholdPct).div(100); + tokenConfig.targetPct = toBNWei(targetPct).div(100); + tokenConfig.thresholdPct = toBNWei(thresholdPct).div(100); // Default to 150% the targetPct. targetOverageBuffer does not have to be defined so that no existing configs // are broken. This is a reasonable default because it allows the relayer to be a bit more flexible in // holding more tokens than the targetPct, but perhaps a better default is 100% - this.inventoryConfig.tokenConfig[l1Token][chainId].targetOverageBuffer = toBNWei( - targetOverageBuffer ?? "1.5" - ); - if (unwrapWethThreshold !== undefined) { - this.inventoryConfig.tokenConfig[l1Token][chainId].unwrapWethThreshold = toBNWei(unwrapWethThreshold); + tokenConfig.targetOverageBuffer = toBNWei(targetOverageBuffer ?? "1.5"); + + if (l1Token === TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolChainId]) { + if (unwrapWethThreshold !== undefined) { + tokenConfig.unwrapWethThreshold = toBNWei(unwrapWethThreshold); + } + tokenConfig.unwrapWethTarget = toBNWei(unwrapWethTarget ?? 2); } - this.inventoryConfig.tokenConfig[l1Token][chainId].unwrapWethTarget = unwrapWethTarget - ? toBNWei(unwrapWethTarget) - : toBNWei(2); }); }); } From 703f028d6d010f24eb868b22262c3f5e4ba307eb Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:06:24 +0200 Subject: [PATCH 02/66] Additional simplification --- src/relayer/RelayerConfig.ts | 56 +++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index a0c99d4a3..db8f45320 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -105,44 +105,48 @@ export class RelayerConfig extends CommonConfig { if (Object.keys(this.inventoryConfig).length > 0) { this.inventoryConfig = replaceAddressCase(this.inventoryConfig); // Cast any non-address case addresses. - 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 ??= {}; + + const { inventoryConfig } = this; + + // Default to 1 Eth on the target chains and wrapping the rest to WETH. + inventoryConfig.wrapEtherThreshold = toBNWei(inventoryConfig.wrapEtherThreshold ?? 1); + + inventoryConfig.wrapEtherThresholdPerChain ??= {}; + inventoryConfig.wrapEtherTarget = inventoryConfig.wrapEtherTarget + ? toBNWei(inventoryConfig.wrapEtherTarget) + : inventoryConfig.wrapEtherThreshold; // default to wrapping ETH to threshold, same as target. + + inventoryConfig.wrapEtherTargetPerChain ??= {}; assert( - this.inventoryConfig.wrapEtherThreshold.gte(this.inventoryConfig.wrapEtherTarget), - `default wrapEtherThreshold ${this.inventoryConfig.wrapEtherThreshold} must be >= default wrapEtherTarget ${this.inventoryConfig.wrapEtherTarget}` + inventoryConfig.wrapEtherThreshold.gte(inventoryConfig.wrapEtherTarget), + `default wrapEtherThreshold ${inventoryConfig.wrapEtherThreshold} must be >= default wrapEtherTarget ${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] - ); + const wrapThresholds = inventoryConfig.wrapEtherThresholdPerChain; + const wrapTargets = inventoryConfig.wrapEtherTargetPerChain;; + Object.keys(inventoryConfig.wrapEtherThresholdPerChain).forEach((chainId) => { + if (wrapThresholds[chainId] !== undefined) { + wrapThresholds[chainId] = toBNWei(wrapThresholds[chainId]); // Promote to 18 decimals. } }); - Object.keys(this.inventoryConfig.wrapEtherTargetPerChain).forEach((chainId) => { - if (this.inventoryConfig.wrapEtherTargetPerChain[chainId] !== undefined) { - this.inventoryConfig.wrapEtherTargetPerChain[chainId] = toBNWei( - this.inventoryConfig.wrapEtherTargetPerChain[chainId] - ); + + Object.keys(inventoryConfig.wrapEtherTargetPerChain).forEach((chainId) => { + if (wrapTargets[chainId] !== undefined) { + wrapTargets[chainId] = toBNWei(wrapTargets[chainId]); // Promote to 18 decimals. + // Check newly set target against threshold - const threshold = - this.inventoryConfig.wrapEtherThresholdPerChain[chainId] ?? this.inventoryConfig.wrapEtherThreshold; - const target = this.inventoryConfig.wrapEtherTargetPerChain[chainId]; + const threshold = wrapThresholds[chainId] ?? inventoryConfig.wrapEtherThreshold; + const target = wrapTargets[chainId]; assert( threshold.gte(target), - `wrapEtherThresholdPerChain ${threshold.toString()} must be >= wrapEtherTargetPerChain ${target}` + `Chain ${chainId} wrapEtherThresholdPerChain ${threshold} must be >= wrapEtherTargetPerChain ${target}` ); } }); - Object.keys(this.inventoryConfig?.tokenConfig ?? {}).forEach((l1Token) => { - Object.keys(this.inventoryConfig.tokenConfig[l1Token]).forEach((chainId) => { + + Object.keys(inventoryConfig?.tokenConfig ?? {}).forEach((l1Token) => { + Object.keys(inventoryConfig.tokenConfig[l1Token]).forEach((chainId) => { const tokenConfig = this.inventoryConfig.tokenConfig[l1Token][chainId]; const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = tokenConfig; From b2fa39a5fc00f6173489042fa0c60a9f4d87b527 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:18:34 +0200 Subject: [PATCH 03/66] Drop --- src/relayer/RelayerConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index db8f45320..03c6865a4 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -147,7 +147,7 @@ export class RelayerConfig extends CommonConfig { Object.keys(inventoryConfig?.tokenConfig ?? {}).forEach((l1Token) => { Object.keys(inventoryConfig.tokenConfig[l1Token]).forEach((chainId) => { - const tokenConfig = this.inventoryConfig.tokenConfig[l1Token][chainId]; + const tokenConfig = inventoryConfig.tokenConfig[l1Token][chainId]; const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = tokenConfig; assert( From dd5e453e812e0c6107895d51c269ddc7ba8a2ad1 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:53:52 +0200 Subject: [PATCH 04/66] Lint & chill --- src/relayer/RelayerConfig.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index 03c6865a4..1cb1132ee 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -124,7 +124,7 @@ export class RelayerConfig extends CommonConfig { // Validate the per chain target and thresholds for wrapping ETH: const wrapThresholds = inventoryConfig.wrapEtherThresholdPerChain; - const wrapTargets = inventoryConfig.wrapEtherTargetPerChain;; + const wrapTargets = inventoryConfig.wrapEtherTargetPerChain; Object.keys(inventoryConfig.wrapEtherThresholdPerChain).forEach((chainId) => { if (wrapThresholds[chainId] !== undefined) { wrapThresholds[chainId] = toBNWei(wrapThresholds[chainId]); // Promote to 18 decimals. @@ -160,11 +160,13 @@ export class RelayerConfig extends CommonConfig { ); tokenConfig.targetPct = toBNWei(targetPct).div(100); tokenConfig.thresholdPct = toBNWei(thresholdPct).div(100); + // Default to 150% the targetPct. targetOverageBuffer does not have to be defined so that no existing configs // are broken. This is a reasonable default because it allows the relayer to be a bit more flexible in // holding more tokens than the targetPct, but perhaps a better default is 100% tokenConfig.targetOverageBuffer = toBNWei(targetOverageBuffer ?? "1.5"); + // For WETH, also consider any unwrap target/threshold. if (l1Token === TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolChainId]) { if (unwrapWethThreshold !== undefined) { tokenConfig.unwrapWethThreshold = toBNWei(unwrapWethThreshold); From 9efd8fe5097fad19796746266e83b89980544b88 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:51:46 +0200 Subject: [PATCH 05/66] improve(RelayerConfig): Permit symbol-based token config This change permits the InventoryConfig to be specified in either HubPool token address or token symbol. Token symbol is preferred because it's simpler to configure and reduces the chance of mistakes. During config, any symbols are resolved to their underlying token addresses on the HubPool chain, so no subsequent changes are required for any accesses into the post-processed inventory config. --- src/relayer/RelayerConfig.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index 1cb1132ee..b542955eb 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -1,3 +1,4 @@ +import { utils as ethersUtils } from "ethers"; import { typeguards } from "@across-protocol/sdk-v2"; import { BigNumber, @@ -145,11 +146,22 @@ export class RelayerConfig extends CommonConfig { } }); - Object.keys(inventoryConfig?.tokenConfig ?? {}).forEach((l1Token) => { - Object.keys(inventoryConfig.tokenConfig[l1Token]).forEach((chainId) => { - const tokenConfig = inventoryConfig.tokenConfig[l1Token][chainId]; + const rawTokenConfigs = inventoryConfig?.tokenConfig ?? {}; + const tokenConfigs = (inventoryConfig.tokenConfig = {}); + Object.keys(rawTokenConfigs).forEach((l1Token) => { + // If the l1Token is a symbol, resolve the correct address. + const effectiveL1Token = ethersUtils.isAddress(l1Token) + ? l1Token + : TOKEN_SYMBOLS_MAP[l1Token].addresses[this.hubPoolChainId]; + + Object.keys(rawTokenConfigs[l1Token]).forEach((chainId) => { + const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = + rawTokenConfigs[l1Token][chainId]; + + tokenConfigs[effectiveL1Token] ??= {}; + tokenConfigs[effectiveL1Token][chainId] ??= { targetPct, thresholdPct, targetOverageBuffer }; + const tokenConfig = tokenConfigs[effectiveL1Token][chainId]; - const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = tokenConfig; assert( targetPct !== undefined && thresholdPct !== undefined, `Bad config. Must specify targetPct, thresholdPct for ${l1Token} on ${chainId}` @@ -167,7 +179,7 @@ export class RelayerConfig extends CommonConfig { tokenConfig.targetOverageBuffer = toBNWei(targetOverageBuffer ?? "1.5"); // For WETH, also consider any unwrap target/threshold. - if (l1Token === TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolChainId]) { + if (effectiveL1Token === TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolChainId]) { if (unwrapWethThreshold !== undefined) { tokenConfig.unwrapWethThreshold = toBNWei(unwrapWethThreshold); } From 20da41359ed18046e3c1a03acdfbfe3683f45e08 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:19:42 +0200 Subject: [PATCH 06/66] feat(InventoryClient): Support 1:many HubPool mappings This update to the InventoryClient permits multiple tokens to map to a single HubPool token. This will be used with USDC/CCTP, where the existing USDC deployment will map to both native USDC and bridged USDC on the various non-mainnet chains supported by Across. See below for an example of how this is configured. Note that the symbol _USDC is the temporary label for native USDC. "USDC": { "USDC.e": { "10": { "targetPct": 8, "thresholdPct": 4 }, "137": { "targetPct": 4, "thresholdPct": 1 }, "324": { "targetPct": 8, "thresholdPct": 4 }, "42161": { "targetPct": 8, "thresholdPct": 4 }, "59144": { "targetPct": 5, "thresholdPct": 2 } }, "USDbC": { "8453": { "targetPct": 5, "thresholdPct": 2 } }, "_USDC": { "137": { "targetPct": 1, "thresholdPct": 1 } } }, --- src/clients/InventoryClient.ts | 136 ++++++++++++++++++-------- src/interfaces/InventoryManagement.ts | 43 +++++--- src/relayer/RelayerConfig.ts | 92 ++++++++++------- 3 files changed, 183 insertions(+), 88 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 37440fecd..5090fe803 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -24,7 +24,8 @@ import { } from "../utils"; import { HubPoolClient, TokenClient, BundleDataClient } from "."; import { AdapterManager, CrossChainTransferClient } from "./bridges"; -import { InventoryConfig, V3Deposit } from "../interfaces"; +import { V3Deposit } from "../interfaces"; +import { InventoryConfig, isAliasConfig, TokenInventoryConfig } from "../interfaces/InventoryManagement"; import lodash from "lodash"; import { CONTRACT_ADDRESSES, SLOW_WITHDRAWAL_CHAINS } from "../common"; import { CombinedRefunds } from "../dataworker/DataworkerUtils"; @@ -68,6 +69,21 @@ export class InventoryClient { this.formatWei = createFormatFunction(2, 4, false, 18); } + getTokenConfig(hubPoolToken: string, chainId: number, spokePoolToken?: string): TokenInventoryConfig | undefined { + const tokenConfig = this.inventoryConfig.tokenConfig[hubPoolToken]; + assert(isDefined(tokenConfig), `getTokenConfig: No token config found for ${hubPoolToken}.`); + + if (isAliasConfig(tokenConfig)) { + assert( + isDefined(spokePoolToken), + `Cannot resolve ambiguous ${getNetworkName(chainId)} token config for ${hubPoolToken}` + ); + return tokenConfig[spokePoolToken]?.[chainId]; + } else { + return tokenConfig[chainId]; + } + } + // Get the total balance across all chains, considering any outstanding cross chain transfers as a virtual balance on that chain. getCumulativeBalance(l1Token: string): BigNumber { return this.getEnabledChains() @@ -83,9 +99,12 @@ export class InventoryClient { return bnZero; } + const balances = this.getDestinationTokensForL1Token(l1Token, chainId).map( + (token) => this.tokenClient.getBalance(Number(chainId), token) || bnZero + ); + // If the chain does not have this token (EG BOBA on Optimism) then 0. - const balance = - this.tokenClient.getBalance(chainId, this.getDestinationTokenForL1Token(l1Token, chainId)) || bnZero; + const balance = balances.reduce((acc, curr) => acc.add(curr), bnZero); // Consider any L1->L2 transfers that are currently pending in the canonical bridge. return balance.add( @@ -134,13 +153,25 @@ export class InventoryClient { // Find how short a given chain is for a desired L1Token. getTokenShortFall(l1Token: string, chainId: number): BigNumber { - return this.tokenClient.getShortfallTotalRequirement(chainId, this.getDestinationTokenForL1Token(l1Token, chainId)); + return this.getDestinationTokensForL1Token(l1Token, chainId) + .map((token) => this.tokenClient.getShortfallTotalRequirement(chainId, token)) + .reduce((acc, curr) => acc.add(curr), bnZero); } - getDestinationTokenForL1Token(l1Token: string, chainId: number | string): string { + getRepaymentTokenForL1Token(l1Token: string, chainId: number | string): string { return this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId)); } + getDestinationTokensForL1Token(l1Token: string, chainId: number | string): string[] { + const tokenConfig = this.inventoryConfig.tokenConfig[l1Token]; + + if (isAliasConfig(tokenConfig)) { + return Object.keys(tokenConfig).filter((k) => isDefined(tokenConfig[k][chainId])); + } + + return [this.getRepaymentTokenForL1Token(l1Token, chainId)]; + } + getEnabledChains(): number[] { return this.chainIdList; } @@ -213,9 +244,9 @@ export class InventoryClient { const totalRefundsPerChain = this.getEnabledChains().reduce( (refunds: { [chainId: string]: BigNumber }, chainId) => { if (!this.hubPoolClient.l2TokenEnabledForL1Token(l1Token, chainId)) { - refunds[chainId] = toBN(0); + refunds[chainId] = bnZero; } else { - const destinationToken = this.getDestinationTokenForL1Token(l1Token, chainId); + const destinationToken = this.getRepaymentTokenForL1Token(l1Token, chainId); refunds[chainId] = this.bundleDataClient.getTotalRefund( refundsToConsider, this.relayer, @@ -293,8 +324,8 @@ export class InventoryClient { ` (${inputToken} != ${outputToken})` ); } + l1Token ??= this.hubPoolClient.getL1TokenForL2TokenAtBlock(inputToken, originChainId); - const tokenConfig = this.inventoryConfig?.tokenConfig?.[l1Token]; // Consider any refunds from executed and to-be executed bundles. If bundle data client doesn't return in // time, return an object with zero refunds for all chains. @@ -350,7 +381,12 @@ export class InventoryClient { for (const _chain of chainsToEvaluate) { assert(this._l1TokenEnabledForChain(l1Token, _chain), `Token ${l1Token} not enabled for chain ${_chain}`); // Destination chain: + + // @todo: Determine whether this should be reduced over all repayment token balances, or only the repayment token. + // Per current implementation, it returns the shortfall for the sum of all destination tokens that map to l1Token, + // not necessarily just the repayment token. const chainShortfall = this.getTokenShortFall(l1Token, _chain); + const chainVirtualBalance = this.getBalanceOnChainForL1Token(_chain, l1Token); const chainVirtualBalanceWithShortfall = chainVirtualBalance.sub(chainShortfall); let cumulativeVirtualBalanceWithShortfall = cumulativeVirtualBalance.sub(chainShortfall); @@ -376,8 +412,10 @@ export class InventoryClient { .div(cumulativeVirtualBalanceWithShortfallPostRelay); // Consider configured buffer for target to allow relayer to support slight overages. - const thresholdPct = toBN(this.inventoryConfig.tokenConfig[l1Token][_chain].targetPct) - .mul(tokenConfig[_chain].targetOverageBuffer ?? toBNWei("1")) + const tokenConfig = this.getTokenConfig(l1Token, destinationChainId, outputToken); + assert(isDefined(tokenConfig), `No tokenConfig for ${l1Token} on ${_chain}.`); + const thresholdPct = toBN(tokenConfig.targetPct) + .mul(tokenConfig.targetOverageBuffer ?? toBNWei("1")) .div(fixedPointAdjustment); this.log( `Evaluated taking repayment on ${ @@ -447,12 +485,13 @@ export class InventoryClient { } else { runningBalanceForToken = leaf.runningBalances[l1TokenIndex]; } + // Approximate latest running balance as last known proposed running balance... // - minus total deposit amount on chain since the latest end block proposed // - plus total refund amount on chain since the latest end block proposed const upcomingDeposits = this.bundleDataClient.getUpcomingDepositAmount( chainId, - this.getDestinationTokenForL1Token(l1Token, chainId), + this.getRepaymentTokenForL1Token(l1Token, chainId), blockRange[1] ); // Grab refunds that are not included in any bundle proposed on-chain. These are refunds that have not @@ -462,7 +501,7 @@ export class InventoryClient { // If a chain didn't exist in the last bundle or a spoke pool client isn't defined, then // one of the refund entries for a chain can be undefined. const upcomingRefundForChain = Object.values( - upcomingRefunds?.[chainId]?.[this.getDestinationTokenForL1Token(l1Token, chainId)] ?? {} + upcomingRefunds?.[chainId]?.[this.getRepaymentTokenForL1Token(l1Token, chainId)] ?? {} ).reduce((acc, curr) => acc.add(curr), bnZero); // Updated running balance is last known running balance minus deposits plus upcoming refunds. @@ -588,24 +627,28 @@ export class InventoryClient { continue; } - const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId); - const { thresholdPct, targetPct } = this.inventoryConfig.tokenConfig[l1Token][chainId]; - if (currentAllocPct.lt(thresholdPct)) { - const deltaPct = targetPct.sub(currentAllocPct); - const amount = deltaPct.mul(cumulativeBalance).div(this.scalar); - const balance = this.tokenClient.getBalance(1, l1Token); - // Divide by scalar because allocation percent was multiplied by it to avoid rounding errors. - rebalancesRequired.push({ - chainId, - l1Token, - currentAllocPct, - thresholdPct, - targetPct, - balance, - cumulativeBalance, - amount, - }); - } + const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); + l2Tokens.forEach((l2Token) => { + const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId); + const tokenConfig = this.getTokenConfig(l1Token, chainId, l2Token); + const { thresholdPct, targetPct } = tokenConfig; + if (currentAllocPct.lt(thresholdPct)) { + const deltaPct = targetPct.sub(currentAllocPct); + const amount = deltaPct.mul(cumulativeBalance).div(this.scalar); + const balance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token); + // Divide by scalar because allocation percent was multiplied by it to avoid rounding errors. + rebalancesRequired.push({ + chainId, + l1Token, + currentAllocPct, + thresholdPct, + targetPct, + balance, + cumulativeBalance, + amount, + }); + } + }); } } return rebalancesRequired; @@ -760,6 +803,10 @@ export class InventoryClient { } async unwrapWeth(): Promise { + if (!this.isInventoryManagementEnabled()) { + return; + } + // Note: these types are just used inside this method, so they are declared in-line. type ChainInfo = { chainId: number; @@ -775,16 +822,14 @@ export class InventoryClient { const executedTransactions: ExecutedUnwrap[] = []; try { - if (!this.isInventoryManagementEnabled()) { - return; - } const l1Weth = TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolClient.chainId]; const chains = await Promise.all( this.getEnabledChains() .map((chainId) => { - const unwrapWethThreshold = - this.inventoryConfig.tokenConfig?.[l1Weth]?.[chainId.toString()]?.unwrapWethThreshold; - const unwrapWethTarget = this.inventoryConfig.tokenConfig?.[l1Weth]?.[chainId.toString()]?.unwrapWethTarget; + const tokenConfig = this.getTokenConfig(l1Weth, chainId); + assert(isDefined(tokenConfig)); + + const { unwrapWethThreshold, unwrapWethTarget } = tokenConfig; // Ignore chains where ETH isn't the native gas token. Returning null will result in these being filtered. if (chainId === CHAIN_IDs.POLYGON || unwrapWethThreshold === undefined || unwrapWethTarget === undefined) { @@ -807,7 +852,7 @@ export class InventoryClient { chains.forEach((chainInfo) => { const { chainId, unwrapWethThreshold, unwrapWethTarget, balance } = chainInfo; - const l2WethBalance = this.tokenClient.getBalance(chainId, this.getDestinationTokenForL1Token(l1Weth, chainId)); + const l2WethBalance = this.tokenClient.getBalance(chainId, this.getRepaymentTokenForL1Token(l1Weth, chainId)); if (balance.lt(unwrapWethThreshold)) { const amountToUnwrap = unwrapWethTarget.sub(balance); @@ -836,7 +881,7 @@ export class InventoryClient { // is already complex logic and most of the time we'll not be sending batches of rebalance transactions. for (const { chainInfo, amount } of unwrapsRequired) { const { chainId } = chainInfo; - const l2Weth = this.getDestinationTokenForL1Token(l1Weth, chainId); + const l2Weth = this.getRepaymentTokenForL1Token(l1Weth, chainId); this.tokenClient.decrementLocalBalance(chainId, l2Weth, amount); const receipt = await this._unwrapWeth(chainId, l2Weth, amount); executedTransactions.push({ chainInfo, amount, hash: receipt.hash }); @@ -865,7 +910,7 @@ export class InventoryClient { "- WETH unwrap blocked. Required to send " + `${formatter(amount.toString())} but relayer has ` + `${formatter( - this.tokenClient.getBalance(chainId, this.getDestinationTokenForL1Token(l1Weth, chainId)).toString() + this.tokenClient.getBalance(chainId, this.getRepaymentTokenForL1Token(l1Weth, chainId)).toString() )} WETH balance.\n`; } @@ -988,7 +1033,18 @@ export class InventoryClient { } _l1TokenEnabledForChain(l1Token: string, chainId: number): boolean { - return this.inventoryConfig.tokenConfig?.[l1Token]?.[String(chainId)] !== undefined; + const tokenConfig = this.inventoryConfig?.tokenConfig?.[l1Token]; + if (!isDefined(tokenConfig)) { + return false; + } + + // If tokenConfig directly references chainId, token is enabled. + if (!isAliasConfig(tokenConfig) && isDefined(tokenConfig[chainId])) { + return true; + } + + // If any of the mapped symbols reference chainId, token is enabled. + return Object.keys(tokenConfig).some((symbol) => isDefined(tokenConfig[symbol][chainId])); } /** diff --git a/src/interfaces/InventoryManagement.ts b/src/interfaces/InventoryManagement.ts index 9cd9303fb..7ed0df3c5 100644 --- a/src/interfaces/InventoryManagement.ts +++ b/src/interfaces/InventoryManagement.ts @@ -1,20 +1,27 @@ -import { BigNumber } from "ethers"; +import { BigNumber, utils as ethersUtils } from "ethers"; +import { TOKEN_SYMBOLS_MAP } from "../utils"; + +export type TokenInventoryConfig = { + targetOverageBuffer: BigNumber; // Max multiplier for targetPct, to give flexibility in repayment chain selection. + targetPct: BigNumber; // The desired amount of the given token on the L2 chainId. + thresholdPct: BigNumber; // Threshold, below which, we will execute a rebalance. + unwrapWethThreshold?: BigNumber; // Threshold for ETH to trigger WETH unwrapping to maintain ETH balance. + unwrapWethTarget?: BigNumber; // Amount of WETH to unwrap to refill ETH. Unused if unwrapWethThreshold is undefined. +}; + +export type ChainTokenConfig = { + [chainId: string]: TokenInventoryConfig; +}; + +// AliasConfig permits a single HubPool token to map onto multiple tokens on a remote chain. +export type ChainTokenInventory = { + [symbol: string]: ChainTokenConfig; +}; export interface InventoryConfig { - tokenConfig: { - [l1Token: string]: { - [chainId: string]: { - targetOverageBuffer: BigNumber; // The relayer will be allowed to hold this multiple times the targetPct - // of the full token balance on this chain. - targetPct: BigNumber; // The desired amount of the given token on the L2 chainId. - thresholdPct: BigNumber; // Threshold, below which, we will execute a rebalance. - unwrapWethThreshold?: BigNumber; // Threshold for ETH on this chain to trigger WETH unwrapping to maintain - // ETH balance - unwrapWethTarget?: BigNumber; // Amount of WETH to unwrap to refill ETH balance. Unused if unwrapWethThreshold - // is undefined. - }; - }; - }; + // tokenConfig can map to a single token allocation, or a set of allocations that all map to the same HubPool token. + tokenConfig: { [l1Token: string]: ChainTokenConfig } | { [l1Token: string]: ChainTokenInventory }; + // If ETH balance on chain is above threshold, wrap the excess over the target to WETH. wrapEtherTargetPerChain: { [chainId: number]: BigNumber; @@ -25,3 +32,9 @@ export interface InventoryConfig { }; wrapEtherThreshold: BigNumber; } + +export function isAliasConfig(config: ChainTokenConfig | ChainTokenInventory): config is ChainTokenInventory { + return ( + Object.keys(config).every((k) => ethersUtils.isAddress(k)) || Object.keys(config).every((k) => TOKEN_SYMBOLS_MAP[k]) + ); +} diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index b542955eb..f15a019e9 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -13,7 +13,7 @@ import { } from "../utils"; import { CommonConfig, ProcessEnv } from "../common"; import * as Constants from "../common/Constants"; -import { InventoryConfig } from "../interfaces"; +import { InventoryConfig, TokenInventoryConfig, isAliasConfig } from "../interfaces/InventoryManagement"; export class RelayerConfig extends CommonConfig { readonly externalIndexer: boolean; @@ -146,6 +146,41 @@ export class RelayerConfig extends CommonConfig { } }); + const parseTokenConfig = ( + l1Token: string, + chainId: string, + rawTokenConfig: TokenInventoryConfig + ): TokenInventoryConfig => { + const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = rawTokenConfig; + const tokenConfig: TokenInventoryConfig = { targetPct, thresholdPct, targetOverageBuffer }; + + assert( + targetPct !== undefined && thresholdPct !== undefined, + `Bad config. Must specify targetPct, thresholdPct for ${l1Token} on ${chainId}` + ); + assert( + toBN(thresholdPct).lte(toBN(targetPct)), + `Bad config. thresholdPct<=targetPct for ${l1Token} on ${chainId}` + ); + tokenConfig.targetPct = toBNWei(targetPct).div(100); + tokenConfig.thresholdPct = toBNWei(thresholdPct).div(100); + + // Default to 150% the targetPct. targetOverageBuffer does not have to be defined so that no existing configs + // are broken. This is a reasonable default because it allows the relayer to be a bit more flexible in + // holding more tokens than the targetPct, but perhaps a better default is 100% + tokenConfig.targetOverageBuffer = toBNWei(targetOverageBuffer ?? "1.5"); + + // For WETH, also consider any unwrap target/threshold. + if (l1Token === TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolChainId]) { + if (unwrapWethThreshold !== undefined) { + tokenConfig.unwrapWethThreshold = toBNWei(unwrapWethThreshold); + } + tokenConfig.unwrapWethTarget = toBNWei(unwrapWethTarget ?? 2); + } + + return tokenConfig; + }; + const rawTokenConfigs = inventoryConfig?.tokenConfig ?? {}; const tokenConfigs = (inventoryConfig.tokenConfig = {}); Object.keys(rawTokenConfigs).forEach((l1Token) => { @@ -154,38 +189,29 @@ export class RelayerConfig extends CommonConfig { ? l1Token : TOKEN_SYMBOLS_MAP[l1Token].addresses[this.hubPoolChainId]; - Object.keys(rawTokenConfigs[l1Token]).forEach((chainId) => { - const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = - rawTokenConfigs[l1Token][chainId]; - - tokenConfigs[effectiveL1Token] ??= {}; - tokenConfigs[effectiveL1Token][chainId] ??= { targetPct, thresholdPct, targetOverageBuffer }; - const tokenConfig = tokenConfigs[effectiveL1Token][chainId]; - - assert( - targetPct !== undefined && thresholdPct !== undefined, - `Bad config. Must specify targetPct, thresholdPct for ${l1Token} on ${chainId}` - ); - assert( - toBN(thresholdPct).lte(toBN(targetPct)), - `Bad config. thresholdPct<=targetPct for ${l1Token} on ${chainId}` - ); - tokenConfig.targetPct = toBNWei(targetPct).div(100); - tokenConfig.thresholdPct = toBNWei(thresholdPct).div(100); - - // Default to 150% the targetPct. targetOverageBuffer does not have to be defined so that no existing configs - // are broken. This is a reasonable default because it allows the relayer to be a bit more flexible in - // holding more tokens than the targetPct, but perhaps a better default is 100% - tokenConfig.targetOverageBuffer = toBNWei(targetOverageBuffer ?? "1.5"); - - // For WETH, also consider any unwrap target/threshold. - if (effectiveL1Token === TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolChainId]) { - if (unwrapWethThreshold !== undefined) { - tokenConfig.unwrapWethThreshold = toBNWei(unwrapWethThreshold); - } - tokenConfig.unwrapWethTarget = toBNWei(unwrapWethTarget ?? 2); - } - }); + tokenConfigs[effectiveL1Token] ??= {}; + const hubTokenConfig = rawTokenConfigs[l1Token]; + + if (isAliasConfig(hubTokenConfig)) { + Object.keys(hubTokenConfig).forEach((symbol) => { + Object.keys(hubTokenConfig[symbol]).forEach((chainId) => { + const rawTokenConfig = hubTokenConfig[symbol][chainId]; + const effectiveSpokeToken = TOKEN_SYMBOLS_MAP[symbol].addresses[chainId]; + + tokenConfigs[effectiveL1Token][effectiveSpokeToken] ??= {}; + tokenConfigs[effectiveL1Token][effectiveSpokeToken][chainId] = parseTokenConfig( + l1Token, + chainId, + rawTokenConfig + ); + }); + }); + } else { + Object.keys(hubTokenConfig).forEach((chainId) => { + const rawTokenConfig = hubTokenConfig[chainId]; + tokenConfigs[effectiveL1Token][chainId] = parseTokenConfig(l1Token, chainId, rawTokenConfig); + }); + } }); } From e3b6665b45d5a15c140e22b21630cfe8d0caa4d7 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 1 May 2024 14:31:40 +0200 Subject: [PATCH 07/66] Update repayment shortfall check --- src/clients/InventoryClient.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 5090fe803..bea8e01ff 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -382,10 +382,7 @@ export class InventoryClient { assert(this._l1TokenEnabledForChain(l1Token, _chain), `Token ${l1Token} not enabled for chain ${_chain}`); // Destination chain: - // @todo: Determine whether this should be reduced over all repayment token balances, or only the repayment token. - // Per current implementation, it returns the shortfall for the sum of all destination tokens that map to l1Token, - // not necessarily just the repayment token. - const chainShortfall = this.getTokenShortFall(l1Token, _chain); + const chainShortfall = this.tokenClient.getShortfallTotalRequirement(_chain, outputToken); const chainVirtualBalance = this.getBalanceOnChainForL1Token(_chain, l1Token); const chainVirtualBalanceWithShortfall = chainVirtualBalance.sub(chainShortfall); From ec623d62744c83dca5525fed9d0c2eba2a1eeed2 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 1 May 2024 14:38:17 +0200 Subject: [PATCH 08/66] Update --- src/clients/InventoryClient.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index bea8e01ff..7cf0d960e 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -112,6 +112,18 @@ export class InventoryClient { ); } + // @note: l1Token is current needed because not all l2 tokens (USDC.e, USDbC, ...) map back to an L1 token. + getBalanceOnChain(chainId: number, l1Token: string, l2Token?: string): BigNumber { + l2Token ??= this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, chainId); + const balance = this.tokenClient.getBalance(chainId, l2Token); + + // @todo: This will resolve all outstanding transfers for the L1 token, but it should be filtered for _only_ + // transfers that apply to the specific L2 token. This needs to be fixed in the crossChainTransferClient + return balance.add( + this.crossChainTransferClient.getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token) + ); + } + // Get the fraction of funds allocated on each chain. getChainDistribution(l1Token: string): { [chainId: number]: BigNumber } { const cumulativeBalance = this.getCumulativeBalance(l1Token); @@ -147,6 +159,7 @@ export class InventoryClient { const shortfall = this.getTokenShortFall(l1Token, chainId); const currentBalance = this.getBalanceOnChainForL1Token(chainId, l1Token).sub(shortfall); + // Multiply by scalar to avoid rounding errors. return currentBalance.mul(this.scalar).div(cumulativeBalance); } @@ -381,10 +394,8 @@ export class InventoryClient { for (const _chain of chainsToEvaluate) { assert(this._l1TokenEnabledForChain(l1Token, _chain), `Token ${l1Token} not enabled for chain ${_chain}`); // Destination chain: - const chainShortfall = this.tokenClient.getShortfallTotalRequirement(_chain, outputToken); - - const chainVirtualBalance = this.getBalanceOnChainForL1Token(_chain, l1Token); + const chainVirtualBalance = this.getBalanceOnChain(_chain, l1Token, outputToken); const chainVirtualBalanceWithShortfall = chainVirtualBalance.sub(chainShortfall); let cumulativeVirtualBalanceWithShortfall = cumulativeVirtualBalance.sub(chainShortfall); // @dev No need to factor in outputAmount when computing origin chain balance since funds only leave relayer From a48be1fb882c220d0baad60a685e0e3687699f92 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 1 May 2024 19:39:43 +0200 Subject: [PATCH 09/66] Update allocations & l2 token requirements --- src/clients/InventoryClient.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 7cf0d960e..0f66791a2 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -150,14 +150,16 @@ export class InventoryClient { } // Get the balance of a given token on a given chain, including shortfalls and any pending cross chain transfers. - getCurrentAllocationPct(l1Token: string, chainId: number): BigNumber { + getCurrentAllocationPct(l1Token: string, chainId: number, l2Token?: string): BigNumber { + l2Token ??= this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, chainId); + // If there is nothing over all chains, return early. const cumulativeBalance = this.getCumulativeBalance(l1Token); if (cumulativeBalance.eq(bnZero)) { return bnZero; } - const shortfall = this.getTokenShortFall(l1Token, chainId); + const shortfall = this.tokenClient.getShortfallTotalRequirement(chainId, l2Token); const currentBalance = this.getBalanceOnChainForL1Token(chainId, l1Token).sub(shortfall); // Multiply by scalar to avoid rounding errors. @@ -637,7 +639,7 @@ export class InventoryClient { const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); l2Tokens.forEach((l2Token) => { - const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId); + const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId, l2Token); const tokenConfig = this.getTokenConfig(l1Token, chainId, l2Token); const { thresholdPct, targetPct } = tokenConfig; if (currentAllocPct.lt(thresholdPct)) { From 56abc3c1a3be3eb1a6249b8d5d7f48ba341b4adc Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 00:19:39 +0200 Subject: [PATCH 10/66] WIP --- src/clients/InventoryClient.ts | 72 +++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index b31fe0b37..28a8272ca 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -36,6 +36,7 @@ type TokenDistributionPerL1Token = { [l1Token: string]: { [chainId: number]: Big export type Rebalance = { chainId: number; l1Token: string; + l2Token: string; thresholdPct: BigNumber; targetPct: BigNumber; currentAllocPct: BigNumber; @@ -126,6 +127,13 @@ export class InventoryClient { } // Get the fraction of funds allocated on each chain. + _getChainDistribution(l1Token: string, chainId: number, l2Token: string): BigNumber { + const balance = this.getBalanceOnChain(chainId, l1Token, l2Token); + const cumulativeBalance = this.getCumulativeBalance(l1Token); + + return cumulativeBalance.gt(bnZero) ? balance.mul(this.scalar).div(cumulativeBalance) : bnZero; + } + getChainDistribution(l1Token: string): { [chainId: number]: BigNumber } { const cumulativeBalance = this.getCumulativeBalance(l1Token); const distribution: { [chainId: number]: BigNumber } = {}; @@ -651,25 +659,21 @@ export class InventoryClient { } getPossibleRebalances(): Rebalance[] { - const tokenDistributionPerL1Token = this.getTokenDistributionPerL1Token(); - return this._getPossibleRebalances(tokenDistributionPerL1Token); - } - - _getPossibleRebalances(tokenDistributionPerL1Token: TokenDistributionPerL1Token): Rebalance[] { + const chainIds = this.getEnabledChains(); + const l1Tokens = this.getL1Tokens(); const rebalancesRequired: Rebalance[] = []; // First, compute the rebalances that we would do assuming we have sufficient tokens on L1. - for (const l1Token of Object.keys(tokenDistributionPerL1Token)) { + l1Tokens.forEach((l1Token) => { const cumulativeBalance = this.getCumulativeBalance(l1Token); if (cumulativeBalance.eq(bnZero)) { - continue; + return; } - for (const chainId of this.getEnabledL2Chains()) { - // Skip if there's no configuration for l1Token on chainId. This is the case for BOBA and BADGER - // as they're not present on all L2s. + chainIds.forEach((chainId) => { + // Skip if there's no configuration for l1Token on chainId. if (!this._l1TokenEnabledForChain(l1Token, chainId)) { - continue; + return; } const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); @@ -677,25 +681,29 @@ export class InventoryClient { const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId, l2Token); const tokenConfig = this.getTokenConfig(l1Token, chainId, l2Token); const { thresholdPct, targetPct } = tokenConfig; - if (currentAllocPct.lt(thresholdPct)) { - const deltaPct = targetPct.sub(currentAllocPct); - const amount = deltaPct.mul(cumulativeBalance).div(this.scalar); - const balance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token); - // Divide by scalar because allocation percent was multiplied by it to avoid rounding errors. - rebalancesRequired.push({ - chainId, - l1Token, - currentAllocPct, - thresholdPct, - targetPct, - balance, - cumulativeBalance, - amount, - }); + + if (currentAllocPct.gte(thresholdPct)) { + return; } + + const deltaPct = targetPct.sub(currentAllocPct); + const amount = deltaPct.mul(cumulativeBalance).div(this.scalar); + const balance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token); + rebalancesRequired.push({ + chainId, + l1Token, + l2Token, + currentAllocPct, + thresholdPct, + targetPct, + balance, + cumulativeBalance, + amount, + }); }); - } - } + }); + }); + return rebalancesRequired; } @@ -715,14 +723,13 @@ export class InventoryClient { const tokenDistributionPerL1Token = this.getTokenDistributionPerL1Token(); this.constructConsideringRebalanceDebugLog(tokenDistributionPerL1Token); - const rebalancesRequired = this._getPossibleRebalances(tokenDistributionPerL1Token); + const rebalancesRequired = this.getPossibleRebalances(); if (rebalancesRequired.length === 0) { this.log("No rebalances required"); return; } // Next, evaluate if we have enough tokens on L1 to actually do these rebalances. - for (const rebalance of rebalancesRequired) { const { balance, amount, l1Token, chainId } = rebalance; @@ -811,12 +818,13 @@ export class InventoryClient { for (const [_chainId, rebalances] of Object.entries(groupedUnexecutedRebalances)) { const chainId = Number(_chainId); mrkdwn += `*Insufficient amount to rebalance to ${getNetworkName(chainId)}:*\n`; - for (const { l1Token, balance, cumulativeBalance, amount } of rebalances) { + for (const { l1Token, l2Token, balance, cumulativeBalance, amount } of rebalances) { const tokenInfo = this.hubPoolClient.getTokenInfoForL1Token(l1Token); if (!tokenInfo) { throw new Error(`InventoryClient::rebalanceInventoryIfNeeded no L1 token info for token ${l1Token}`); } const { symbol, decimals } = tokenInfo; + const distribution = this._getChainDistribution(l1Token, chainId, l2Token); const formatter = createFormatFunction(2, 4, false, decimals); mrkdwn += `- ${symbol} transfer blocked. Required to send ` + @@ -824,7 +832,7 @@ export class InventoryClient { `${formatter(balance.toString())} on L1. There is currently ` + `${formatter(this.getBalanceOnChainForL1Token(chainId, l1Token).toString())} ${symbol} on ` + `${getNetworkName(chainId)} which is ` + - `${this.formatWei(tokenDistributionPerL1Token[l1Token][chainId].mul(100).toString())}% of the total ` + + `${this.formatWei(distribution.mul(100).toString())}% of the total ` + `${formatter(cumulativeBalance.toString())} ${symbol}.` + " This chain's pending L1->L2 transfer amount is " + `${formatter( From cb5518bbc25db3c4b379a99bb90788388bb50958 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 00:29:53 +0200 Subject: [PATCH 11/66] wip... --- src/clients/InventoryClient.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 28a8272ca..e0c19a867 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -151,6 +151,20 @@ export class InventoryClient { return distribution; } + // // Get the distribution of all tokens, spread over all chains. + // getTokenDistribution(l1Token: string): TokenDistribution { + // const tokenDistribution: TokenDistribution = {}; + // const chainIds = this.getEnabledChains(); + // + // chainIds.forEach((chainId) => { + // tokenDistribution[chainId] = {}; + // this.getDestinationTokensForL1Token(l1Token, chainId).forEach((l2Token) => { + // tokenDistribution[chainId][l2Token] = this._getChainDistribution(l1Token, chainId, l2Token); + // }); + // }); + // return tokenDistribution; + // } + // Get the distribution of all tokens, spread over all chains. getTokenDistributionPerL1Token(): TokenDistributionPerL1Token { const distributionPerL1Token: TokenDistributionPerL1Token = {}; @@ -258,7 +272,7 @@ export class InventoryClient { // Increase virtual balance by pending relayer refunds from the latest valid bundle and the // upcoming bundle. We can assume that all refunds from the second latest valid bundle have already // been executed. - let startTimer; + let startTimer: number; if (!isDefined(this.bundleRefundsPromise)) { startTimer = performance.now(); // @dev Save this as a promise so that other parallel calls to this function don't make the same call. From 2e2f03b6accc9b04cbed873535b77f28f30f9818 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 17:09:21 +0200 Subject: [PATCH 12/66] Revert "wip..." This reverts commit cb5518bbc25db3c4b379a99bb90788388bb50958. --- src/clients/InventoryClient.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 94345d6e5..c64b9fc09 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -155,20 +155,6 @@ export class InventoryClient { return distribution; } - // // Get the distribution of all tokens, spread over all chains. - // getTokenDistribution(l1Token: string): TokenDistribution { - // const tokenDistribution: TokenDistribution = {}; - // const chainIds = this.getEnabledChains(); - // - // chainIds.forEach((chainId) => { - // tokenDistribution[chainId] = {}; - // this.getDestinationTokensForL1Token(l1Token, chainId).forEach((l2Token) => { - // tokenDistribution[chainId][l2Token] = this._getChainDistribution(l1Token, chainId, l2Token); - // }); - // }); - // return tokenDistribution; - // } - // Get the distribution of all tokens, spread over all chains. getTokenDistributionPerL1Token(): TokenDistributionPerL1Token { const distributionPerL1Token: TokenDistributionPerL1Token = {}; @@ -276,7 +262,7 @@ export class InventoryClient { // Increase virtual balance by pending relayer refunds from the latest valid bundle and the // upcoming bundle. We can assume that all refunds from the second latest valid bundle have already // been executed. - let startTimer: number; + let startTimer; if (!isDefined(this.bundleRefundsPromise)) { startTimer = performance.now(); // @dev Save this as a promise so that other parallel calls to this function don't make the same call. From 564e12ea15a4bd48700266d86c16086d3f80ce01 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 17:09:29 +0200 Subject: [PATCH 13/66] Cleanup --- src/clients/InventoryClient.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index c64b9fc09..b7b99f5b9 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -127,14 +127,6 @@ export class InventoryClient { ); } - // Get the fraction of funds allocated on each chain. - _getChainDistribution(l1Token: string, chainId: number, l2Token: string): BigNumber { - const balance = this.getBalanceOnChain(chainId, l1Token, l2Token); - const cumulativeBalance = this.getCumulativeBalance(l1Token); - - return cumulativeBalance.gt(bnZero) ? balance.mul(this.scalar).div(cumulativeBalance) : bnZero; - } - getChainDistribution(l1Token: string): { [chainId: number]: TokenDistribution } { const cumulativeBalance = this.getCumulativeBalance(l1Token); const distribution: { [chainId: number]: TokenDistribution } = {}; @@ -143,13 +135,18 @@ export class InventoryClient { // If token doesn't have entry on chain, skip creating an entry for it since we'll likely run into an error // later trying to grab the chain equivalent of the L1 token via the HubPoolClient. if (chainId === this.hubPoolClient.chainId || this._l1TokenEnabledForChain(l1Token, chainId)) { - const l2Token = this.getDestinationTokenForL1Token(l1Token, chainId); - if (cumulativeBalance.gt(bnZero)) { - distribution[chainId] ??= {}; - distribution[chainId][l2Token] = this.getBalanceOnChainForL1Token(chainId, l1Token) - .mul(this.scalar) - .div(cumulativeBalance); + distribution[chainId] ??= {}; + + if (cumulativeBalance.eq(bnZero)) { + return; } + + const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); + l2Tokens.forEach((l2Token) => { + // THe effective balance is the current balance + inbound bridge transfers. + const effectiveBalance = this.getBalanceOnChain(chainId, l1Token, l2Token); + distribution[chainId][l2Token] = effectiveBalance.mul(this.scalar).div(cumulativeBalance); + }); } }); return distribution; @@ -262,7 +259,7 @@ export class InventoryClient { // Increase virtual balance by pending relayer refunds from the latest valid bundle and the // upcoming bundle. We can assume that all refunds from the second latest valid bundle have already // been executed. - let startTimer; + let startTimer: number; if (!isDefined(this.bundleRefundsPromise)) { startTimer = performance.now(); // @dev Save this as a promise so that other parallel calls to this function don't make the same call. @@ -691,7 +688,6 @@ export class InventoryClient { const deltaPct = targetPct.sub(currentAllocPct); const amount = deltaPct.mul(cumulativeBalance).div(this.scalar); const balance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token); - const l2Token = this.getDestinationTokenForL1Token(l1Token, chainId); rebalancesRequired.push({ chainId, l1Token, @@ -705,7 +701,7 @@ export class InventoryClient { }); }); }); - }); + } return rebalancesRequired; } From d1d0f79e658639e3528779bfac615e9309fd9ad9 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 17:34:45 +0200 Subject: [PATCH 14/66] Typo --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index b7b99f5b9..3deb15345 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -143,7 +143,7 @@ export class InventoryClient { const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); l2Tokens.forEach((l2Token) => { - // THe effective balance is the current balance + inbound bridge transfers. + // The effective balance is the current balance + inbound bridge transfers. const effectiveBalance = this.getBalanceOnChain(chainId, l1Token, l2Token); distribution[chainId][l2Token] = effectiveBalance.mul(this.scalar).div(cumulativeBalance); }); From 5159d122a6b38beed68e8f4e058bacc471fdc073 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 21:39:13 +0200 Subject: [PATCH 15/66] Update getBalanceOnChain --- src/clients/InventoryClient.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 3deb15345..201f74c8b 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -115,10 +115,26 @@ export class InventoryClient { ); } - // @note: l1Token is current needed because not all l2 tokens (USDC.e, USDbC, ...) map back to an L1 token. + /** + * Determine the effective/virtual balance of an l1 token that has been deployed to another chain. + * Includes both the actual balance on the chain and any pending inbound transfers to the target chain. + * If l2Token is supplied, return its balance on the specified chain. Otherwise, return the total allocation + * of l1Token on the specified chain. + * @param chainId Chain to query token balance on. + * @param l1Token L1 token to query on chainId (after mapping). + * @param l2Token Optional l2 token address to narrow the balance reporting. + * @returns Balance of l1Token on chainId. + */ getBalanceOnChain(chainId: number, l1Token: string, l2Token?: string): BigNumber { - l2Token ??= this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, chainId); - const balance = this.tokenClient.getBalance(chainId, l2Token); + let balance: BigNumber; + if (isDefined(l2Token)) { + balance = balance.add(this.tokenClient.getBalance(chainId, l2Token)); + } else { + const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); + balance = l2Tokens + .map((l2Token) => this.tokenClient.getBalance(chainId, l2Token)) + .reduce((acc, curr) => acc.add(curr), bnZero); + } // @todo: This will resolve all outstanding transfers for the L1 token, but it should be filtered for _only_ // transfers that apply to the specific L2 token. This needs to be fixed in the crossChainTransferClient From 8bef2e35b4b3f1b416383d8e7c18f2cc592bc910 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 22:16:19 +0200 Subject: [PATCH 16/66] Migrate balance checker --- src/clients/InventoryClient.ts | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 201f74c8b..d1d76ee67 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -90,31 +90,10 @@ export class InventoryClient { // Get the total balance across all chains, considering any outstanding cross chain transfers as a virtual balance on that chain. getCumulativeBalance(l1Token: string): BigNumber { return this.getEnabledChains() - .map((chainId) => this.getBalanceOnChainForL1Token(chainId, l1Token)) + .map((chainId) => this.getBalanceOnChain(chainId, l1Token)) .reduce((acc, curr) => acc.add(curr), bnZero); } - // Get the balance of a given l1 token on a target chain, considering any outstanding cross chain transfers as a virtual balance on that chain. - getBalanceOnChainForL1Token(chainId: number | string, l1Token: string): BigNumber { - // We want to skip any l2 token that is not present in the inventory config. - chainId = Number(chainId); - if (chainId !== this.hubPoolClient.chainId && !this._l1TokenEnabledForChain(l1Token, chainId)) { - return bnZero; - } - - const balances = this.getDestinationTokensForL1Token(l1Token, chainId).map( - (token) => this.tokenClient.getBalance(Number(chainId), token) || bnZero - ); - - // If the chain does not have this token (EG BOBA on Optimism) then 0. - const balance = balances.reduce((acc, curr) => acc.add(curr), bnZero); - - // Consider any L1->L2 transfers that are currently pending in the canonical bridge. - return balance.add( - this.crossChainTransferClient.getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token) - ); - } - /** * Determine the effective/virtual balance of an l1 token that has been deployed to another chain. * Includes both the actual balance on the chain and any pending inbound transfers to the target chain. @@ -143,6 +122,11 @@ export class InventoryClient { ); } + /** + * Determine the allocation of an l1 token across all configured remote chain IDs. + * @param l1Token L1 token to query. + * @returns Distribution of l1Token by chain ID and l2Token. + */ getChainDistribution(l1Token: string): { [chainId: number]: TokenDistribution } { const cumulativeBalance = this.getCumulativeBalance(l1Token); const distribution: { [chainId: number]: TokenDistribution } = {}; @@ -186,7 +170,7 @@ export class InventoryClient { } const shortfall = this.tokenClient.getShortfallTotalRequirement(chainId, l2Token); - const currentBalance = this.getBalanceOnChainForL1Token(chainId, l1Token).sub(shortfall); + const currentBalance = this.getBalanceOnChain(chainId, l1Token).sub(shortfall); // Multiply by scalar to avoid rounding errors. return currentBalance.mul(this.scalar).div(cumulativeBalance); @@ -845,7 +829,7 @@ export class InventoryClient { `- ${symbol} transfer blocked. Required to send ` + `${formatter(amount.toString())} but relayer has ` + `${formatter(balance.toString())} on L1. There is currently ` + - `${formatter(this.getBalanceOnChainForL1Token(chainId, l1Token).toString())} ${symbol} on ` + + `${formatter(this.getBalanceOnChain(chainId, l1Token).toString())} ${symbol} on ` + `${getNetworkName(chainId)} which is ` + `${this.formatWei(distributionPct.toString())}% of the total ` + `${formatter(cumulativeBalance.toString())} ${symbol}.` + @@ -1024,7 +1008,7 @@ export class InventoryClient { const chainId = Number(_chainId); Object.entries(distributionForToken[chainId]).forEach(([l2Token, amount]) => { - const balanceOnChain = this.getBalanceOnChainForL1Token(chainId, l1Token); + const balanceOnChain = this.getBalanceOnChain(chainId, l1Token, l2Token); const transfers = this.crossChainTransferClient.getOutstandingCrossChainTransferAmount( this.relayer, chainId, From 2efc0f9c61032849dcfaad94ffa1ac69f6ac503e Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 22:24:33 +0200 Subject: [PATCH 17/66] Fix --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index d1d76ee67..6f692f48a 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -107,7 +107,7 @@ export class InventoryClient { getBalanceOnChain(chainId: number, l1Token: string, l2Token?: string): BigNumber { let balance: BigNumber; if (isDefined(l2Token)) { - balance = balance.add(this.tokenClient.getBalance(chainId, l2Token)); + balance = this.tokenClient.getBalance(chainId, l2Token); } else { const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); balance = l2Tokens From b064deea826fc1dfa5e7d563613a971b4dd8aec6 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 23:15:21 +0200 Subject: [PATCH 18/66] Catch undefined destination tokens --- src/clients/InventoryClient.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 6f692f48a..183840d51 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -183,8 +183,12 @@ export class InventoryClient { .reduce((acc, curr) => acc.add(curr), bnZero); } - getRepaymentTokenForL1Token(l1Token: string, chainId: number | string): string { - return this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId)); + getRepaymentTokenForL1Token(l1Token: string, chainId: number | string): string | undefined { + try { + return this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId)); + } catch { + return undefined; + } } getDestinationTokensForL1Token(l1Token: string, chainId: number | string): string[] { @@ -194,7 +198,12 @@ export class InventoryClient { return Object.keys(tokenConfig).filter((k) => isDefined(tokenConfig[k][chainId])); } - return [this.getRepaymentTokenForL1Token(l1Token, chainId)]; + const destinationToken = this.getRepaymentTokenForL1Token(l1Token, chainId); + if (!isDefined(destinationToken)) { + return []; + } + + return [destinationToken]; } getEnabledChains(): number[] { From f435d81df0b00ea245a459bdee69e1ed0e3c156a Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 16:23:14 +0200 Subject: [PATCH 19/66] Fix test --- src/clients/InventoryClient.ts | 5 +++-- test/InventoryClient.InventoryRebalance.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 183840d51..c08bee1cf 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -475,8 +475,9 @@ export class InventoryClient { .div(cumulativeVirtualBalanceWithShortfallPostRelay); // Consider configured buffer for target to allow relayer to support slight overages. - const tokenConfig = this.getTokenConfig(l1Token, destinationChainId, outputToken); - assert(isDefined(tokenConfig), `No tokenConfig for ${l1Token} on ${_chain}.`); + const repaymentToken = this.getRepaymentTokenForL1Token(l1Token, _chain); + const tokenConfig = this.getTokenConfig(l1Token, _chain, repaymentToken); + assert(isDefined(tokenConfig), `No ${outputToken} tokenConfig for ${l1Token} on ${_chain}.`); const thresholdPct = toBN(tokenConfig.targetPct) .mul(tokenConfig.targetOverageBuffer ?? toBNWei("1")) .div(fixedPointAdjustment); diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index d8059152d..8dd4c4202 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -130,7 +130,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { const tokenDistribution = inventoryClient.getTokenDistributionPerL1Token(); for (const chainId of enabledChainIds) { for (const l1Token of inventoryClient.getL1Tokens()) { - expect(inventoryClient.getBalanceOnChainForL1Token(chainId, l1Token)).to.equal( + expect(inventoryClient.getBalanceOnChain(chainId, l1Token)).to.equal( initialAllocation[chainId][l1Token] ); expect( From 33c3fac3b0edbef650301f5cc0f41daccb4405c4 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 17:44:16 +0200 Subject: [PATCH 20/66] lint --- test/InventoryClient.InventoryRebalance.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 8dd4c4202..f606c890c 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -130,9 +130,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { const tokenDistribution = inventoryClient.getTokenDistributionPerL1Token(); for (const chainId of enabledChainIds) { for (const l1Token of inventoryClient.getL1Tokens()) { - expect(inventoryClient.getBalanceOnChain(chainId, l1Token)).to.equal( - initialAllocation[chainId][l1Token] - ); + expect(inventoryClient.getBalanceOnChain(chainId, l1Token)).to.equal(initialAllocation[chainId][l1Token]); expect( inventoryClient.crossChainTransferClient.getOutstandingCrossChainTransferAmount( owner.address, From aefabe821780e40252586d3627432668c1393c53 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 17:58:39 +0200 Subject: [PATCH 21/66] Fix test --- src/clients/InventoryClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index c08bee1cf..f6f096dda 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -446,7 +446,8 @@ export class InventoryClient { assert(this._l1TokenEnabledForChain(l1Token, _chain), `Token ${l1Token} not enabled for chain ${_chain}`); // Destination chain: const chainShortfall = this.tokenClient.getShortfallTotalRequirement(_chain, outputToken); - const chainVirtualBalance = this.getBalanceOnChain(_chain, l1Token, outputToken); + const repaymentToken = this.getRepaymentTokenForL1Token(l1Token, _chain); + const chainVirtualBalance = this.getBalanceOnChain(_chain, l1Token, repaymentToken); const chainVirtualBalanceWithShortfall = chainVirtualBalance.sub(chainShortfall); let cumulativeVirtualBalanceWithShortfall = cumulativeVirtualBalance.sub(chainShortfall); // @dev No need to factor in outputAmount when computing origin chain balance since funds only leave relayer @@ -475,7 +476,6 @@ export class InventoryClient { .div(cumulativeVirtualBalanceWithShortfallPostRelay); // Consider configured buffer for target to allow relayer to support slight overages. - const repaymentToken = this.getRepaymentTokenForL1Token(l1Token, _chain); const tokenConfig = this.getTokenConfig(l1Token, _chain, repaymentToken); assert(isDefined(tokenConfig), `No ${outputToken} tokenConfig for ${l1Token} on ${_chain}.`); const thresholdPct = toBN(tokenConfig.targetPct) From 8c8abb6c40d0fb9bd7849f6ca8af7d2cdf5aa675 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 18:17:23 +0200 Subject: [PATCH 22/66] Add comments --- src/clients/InventoryClient.ts | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index f6f096dda..fc8200bfe 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -72,22 +72,31 @@ export class InventoryClient { this.formatWei = createFormatFunction(2, 4, false, 18); } - getTokenConfig(hubPoolToken: string, chainId: number, spokePoolToken?: string): TokenInventoryConfig | undefined { - const tokenConfig = this.inventoryConfig.tokenConfig[hubPoolToken]; - assert(isDefined(tokenConfig), `getTokenConfig: No token config found for ${hubPoolToken}.`); + /** + * Resolve the token balance configuration for `l1Token` on `chainId`. If `l1Token` maps to multiple tokens on + * `chainId` then `l2Token` must be supplied. + * @param l1Token L1 token address to query. + * @param chainId Chain ID to query on + * @param l2Token Optional L2 token address when l1Token maps to multiple l2Token addresses. + */ + getTokenConfig(l1Token: string, chainId: number, l2Token?: string): TokenInventoryConfig | undefined { + const tokenConfig = this.inventoryConfig.tokenConfig[l1Token]; + assert(isDefined(tokenConfig), `getTokenConfig: No token config found for ${l1Token}.`); if (isAliasConfig(tokenConfig)) { - assert( - isDefined(spokePoolToken), - `Cannot resolve ambiguous ${getNetworkName(chainId)} token config for ${hubPoolToken}` - ); - return tokenConfig[spokePoolToken]?.[chainId]; + assert(isDefined(l2Token), `Cannot resolve ambiguous ${getNetworkName(chainId)} token config for ${l1Token}`); + return tokenConfig[l2Token]?.[chainId]; } else { return tokenConfig[chainId]; } } - // Get the total balance across all chains, considering any outstanding cross chain transfers as a virtual balance on that chain. + /* + * Get the total balance for an L1 token across all chains, considering any outstanding cross chain transfers as a + * virtual balance on that chain. + * @param l1Token L1 token address to query. + * returns Cumulative balance of l1Token across all inventory-managed chains. + */ getCumulativeBalance(l1Token: string): BigNumber { return this.getEnabledChains() .map((chainId) => this.getBalanceOnChain(chainId, l1Token)) @@ -152,7 +161,11 @@ export class InventoryClient { return distribution; } - // Get the distribution of all tokens, spread over all chains. + /** + * Determine the allocation of an l1 token across all configured remote chain IDs. + * @param l1Token L1 token to query. + * @returns Distribution of l1Token by chain ID and l2Token. + */ getTokenDistributionPerL1Token(): TokenDistributionPerL1Token { const distributionPerL1Token: TokenDistributionPerL1Token = {}; this.getL1Tokens().forEach((l1Token) => (distributionPerL1Token[l1Token] = this.getChainDistribution(l1Token))); From 47fec546fced53b045f95ca81f9cb37f9aded7ee Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 21:45:36 +0200 Subject: [PATCH 23/66] Fix test --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index fc8200bfe..d2503e4c5 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -458,8 +458,8 @@ export class InventoryClient { for (const _chain of chainsToEvaluate) { assert(this._l1TokenEnabledForChain(l1Token, _chain), `Token ${l1Token} not enabled for chain ${_chain}`); // Destination chain: - const chainShortfall = this.tokenClient.getShortfallTotalRequirement(_chain, outputToken); const repaymentToken = this.getRepaymentTokenForL1Token(l1Token, _chain); + const chainShortfall = this.tokenClient.getShortfallTotalRequirement(_chain, repaymentToken); const chainVirtualBalance = this.getBalanceOnChain(_chain, l1Token, repaymentToken); const chainVirtualBalanceWithShortfall = chainVirtualBalance.sub(chainShortfall); let cumulativeVirtualBalanceWithShortfall = cumulativeVirtualBalance.sub(chainShortfall); From 5a308733e72cd16aecedb346bc03087d78899122 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 22:28:12 +0200 Subject: [PATCH 24/66] Fix test --- test/mocks/MockInventoryClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mocks/MockInventoryClient.ts b/test/mocks/MockInventoryClient.ts index 0916da06c..74da30567 100644 --- a/test/mocks/MockInventoryClient.ts +++ b/test/mocks/MockInventoryClient.ts @@ -33,7 +33,7 @@ export class MockInventoryClient extends InventoryClient { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async determineRefundChainId(_deposit: Deposit): Promise { + override async determineRefundChainId(_deposit: Deposit): Promise { return this.inventoryConfig === null ? 1 : super.determineRefundChainId(_deposit); } @@ -53,7 +53,7 @@ export class MockInventoryClient extends InventoryClient { this.possibleRebalances = []; } - getPossibleRebalances(): Rebalance[] { + override getPossibleRebalances(): Rebalance[] { return this.possibleRebalances; } @@ -61,7 +61,7 @@ export class MockInventoryClient extends InventoryClient { this.balanceOnChain = newBalance; } - getBalanceOnChainForL1Token(): BigNumber { + override getBalanceOnChain(): BigNumber { return this.balanceOnChain; } } From d26d1ac7b01e657aec46f387688f1e0b59c31e58 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 22:33:53 +0200 Subject: [PATCH 25/66] Revert return --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index d2503e4c5..b12d32555 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -689,7 +689,7 @@ export class InventoryClient { for (const l1Token of this.getL1Tokens()) { const cumulativeBalance = this.getCumulativeBalance(l1Token); if (cumulativeBalance.eq(bnZero)) { - return; + continue; } chainIds.forEach((chainId) => { From 4f872bf00cf31f7f255690de299278a6a9b09d2d Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 3 May 2024 23:03:25 +0200 Subject: [PATCH 26/66] Rename type --- src/clients/InventoryClient.ts | 4 ++-- src/interfaces/InventoryManagement.ts | 4 ++-- src/relayer/RelayerConfig.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index b12d32555..93bb07372 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -26,7 +26,7 @@ import { import { HubPoolClient, TokenClient, BundleDataClient } from "."; import { AdapterManager, CrossChainTransferClient } from "./bridges"; import { V3Deposit } from "../interfaces"; -import { InventoryConfig, isAliasConfig, TokenInventoryConfig } from "../interfaces/InventoryManagement"; +import { InventoryConfig, isAliasConfig, TokenBalanceConfig } from "../interfaces/InventoryManagement"; import lodash from "lodash"; import { CONTRACT_ADDRESSES, SLOW_WITHDRAWAL_CHAINS } from "../common"; import { CombinedRefunds } from "../dataworker/DataworkerUtils"; @@ -79,7 +79,7 @@ export class InventoryClient { * @param chainId Chain ID to query on * @param l2Token Optional L2 token address when l1Token maps to multiple l2Token addresses. */ - getTokenConfig(l1Token: string, chainId: number, l2Token?: string): TokenInventoryConfig | undefined { + getTokenConfig(l1Token: string, chainId: number, l2Token?: string): TokenBalanceConfig | undefined { const tokenConfig = this.inventoryConfig.tokenConfig[l1Token]; assert(isDefined(tokenConfig), `getTokenConfig: No token config found for ${l1Token}.`); diff --git a/src/interfaces/InventoryManagement.ts b/src/interfaces/InventoryManagement.ts index 7ed0df3c5..2b2d80dc8 100644 --- a/src/interfaces/InventoryManagement.ts +++ b/src/interfaces/InventoryManagement.ts @@ -1,7 +1,7 @@ import { BigNumber, utils as ethersUtils } from "ethers"; import { TOKEN_SYMBOLS_MAP } from "../utils"; -export type TokenInventoryConfig = { +export type TokenBalanceConfig = { targetOverageBuffer: BigNumber; // Max multiplier for targetPct, to give flexibility in repayment chain selection. targetPct: BigNumber; // The desired amount of the given token on the L2 chainId. thresholdPct: BigNumber; // Threshold, below which, we will execute a rebalance. @@ -10,7 +10,7 @@ export type TokenInventoryConfig = { }; export type ChainTokenConfig = { - [chainId: string]: TokenInventoryConfig; + [chainId: string]: TokenBalanceConfig; }; // AliasConfig permits a single HubPool token to map onto multiple tokens on a remote chain. diff --git a/src/relayer/RelayerConfig.ts b/src/relayer/RelayerConfig.ts index 38855f3e8..7787c8184 100644 --- a/src/relayer/RelayerConfig.ts +++ b/src/relayer/RelayerConfig.ts @@ -13,7 +13,7 @@ import { } from "../utils"; import { CommonConfig, ProcessEnv } from "../common"; import * as Constants from "../common/Constants"; -import { InventoryConfig, TokenInventoryConfig, isAliasConfig } from "../interfaces/InventoryManagement"; +import { InventoryConfig, TokenBalanceConfig, isAliasConfig } from "../interfaces/InventoryManagement"; export class RelayerConfig extends CommonConfig { readonly externalIndexer: boolean; @@ -149,10 +149,10 @@ export class RelayerConfig extends CommonConfig { const parseTokenConfig = ( l1Token: string, chainId: string, - rawTokenConfig: TokenInventoryConfig - ): TokenInventoryConfig => { + rawTokenConfig: TokenBalanceConfig + ): TokenBalanceConfig => { const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget, targetOverageBuffer } = rawTokenConfig; - const tokenConfig: TokenInventoryConfig = { targetPct, thresholdPct, targetOverageBuffer }; + const tokenConfig: TokenBalanceConfig = { targetPct, thresholdPct, targetOverageBuffer }; assert( targetPct !== undefined && thresholdPct !== undefined, From 26d8dfdc50296672c1b53dea72511e703db60aa6 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 11:50:14 +0200 Subject: [PATCH 27/66] Initial tests --- test/InventoryClient.InventoryRebalance.ts | 113 +++++++++++++++++---- 1 file changed, 96 insertions(+), 17 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index f606c890c..2c6d4147f 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -21,7 +21,7 @@ import { ConfigStoreClient, InventoryClient } from "../src/clients"; // Tested import { CrossChainTransferClient } from "../src/clients/bridges"; import { InventoryConfig } from "../src/interfaces"; import { MockAdapterManager, MockBundleDataClient, MockHubPoolClient, MockTokenClient } from "./mocks/"; -import { ERC20 } from "../src/utils"; +import { bnZero, CHAIN_IDs, ERC20, TOKEN_SYMBOLS_MAP } from "../src/utils"; const toMegaWei = (num: string | number | BigNumber) => ethers.utils.parseUnits(num.toString(), 6); @@ -31,10 +31,9 @@ let owner: SignerWithAddress, spy: sinon.SinonSpy, spyLogger: winston.Logger; let inventoryClient: InventoryClient; // tested let crossChainTransferClient: CrossChainTransferClient; -const enabledChainIds = [1, 10, 137, 42161]; - -const mainnetWeth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; -const mainnetUsdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const enabledChainIds = [CHAIN_IDs.MAINNET, CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.ARBITRUM]; +const mainnetWeth = TOKEN_SYMBOLS_MAP.WETH.addresses[1]; +const mainnetUsdc = TOKEN_SYMBOLS_MAP.USDC.addresses[1]; let mainnetWethContract: FakeContract; let mainnetUsdcContract: FakeContract; @@ -48,29 +47,32 @@ enabledChainIds.slice(1).forEach((chainId) => { }); // Configure target percentages as 80% mainnet, 10% optimism, 5% polygon and 5% Arbitrum. +const targetOverageBuffer = toWei(1); const inventoryConfig: InventoryConfig = { + wrapEtherTargetPerChain: {}, + wrapEtherTarget: toWei(1), + wrapEtherThresholdPerChain: {}, + wrapEtherThreshold: toWei(1), tokenConfig: { [mainnetWeth]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1) }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, + 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [mainnetUsdc]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1) }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, + 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, }, - wrapEtherThreshold: toWei(1), }; // Construct an initial distribution that keeps these values within the above thresholds. const initialAllocation = { - 1: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC on Mainnet - 10: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC on Optimism - 137: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Polygon - 42161: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Arbitrum + [CHAIN_IDs.MAINNET]: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC + [CHAIN_IDs.OPTIMISM]: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC + [CHAIN_IDs.POLYGON]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC + [CHAIN_IDs.ARBITRUM]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC }; const initialWethTotal = toWei(140); // Sum over all 4 chains is 140 @@ -298,6 +300,83 @@ describe("InventoryClient: Rebalancing inventory", async function () { await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "Executed Inventory rebalances")).to.be.true; }); + + describe("Remote chain token mappings", async function () { + const nativeUSDC = TOKEN_SYMBOLS_MAP.USDC.addresses; + const bridgedUSDC = { ...TOKEN_SYMBOLS_MAP["USDC.e"].addresses, ...TOKEN_SYMBOLS_MAP["USDbC"].addresses }; + + beforeEach(async function () { + // Sub in a nested USDC config for the existing USDC config. + const usdcConfig = { + [nativeUSDC[CHAIN_IDs.OPTIMISM]]: { + [CHAIN_IDs.OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [nativeUSDC[CHAIN_IDs.POLYGON]]: { + [CHAIN_IDs.POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [nativeUSDC[CHAIN_IDs.BASE]]: { + [CHAIN_IDs.BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [nativeUSDC[CHAIN_IDs.ARBITRUM]]: { + [CHAIN_IDs.ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[CHAIN_IDs.OPTIMISM]]: { + [CHAIN_IDs.OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [bridgedUSDC[CHAIN_IDs.POLYGON]]: { + [CHAIN_IDs.POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[CHAIN_IDs.BASE]]: { + [CHAIN_IDs.BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[CHAIN_IDs.ARBITRUM]]: { + [CHAIN_IDs.ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + } + }; + inventoryConfig.tokenConfig[mainnetUsdc] = usdcConfig; + }); + + it("Correctly resolves 1:many token mappings", async function () { + // Caller must specify l2Token for 1:many mappings. + expect(() => inventoryClient.getTokenConfig(mainnetUsdc, CHAIN_IDs.BASE)).to.throw; + + [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach((chainId) => { + let config = inventoryClient.getTokenConfig(mainnetUsdc, chainId, bridgedUSDC[chainId]); + expect(config).to.exist; + + let expectedConfig = inventoryConfig.tokenConfig[mainnetUsdc][bridgedUSDC[chainId]][chainId]; + expect(expectedConfig).to.exist; + expect(expectedConfig).to.deep.equal(expectedConfig); + }); + }); + + it("Correctly isolates 1:many token balances", async function () { + [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach(async (chainId) => { + let balance = inventoryClient.getCumulativeBalance(mainnetUsdc); + expect(balance).to.be.greaterThan(bnZero); + expect(balance).to.equal(initialUsdcTotal); + + tokenClient.setTokenData(chainId, nativeUSDC[chainId], initialUsdcTotal.mul(2), toBN(0)); + await tokenClient.update(); + + balance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); + expect(balance).to.equal(initialUsdcTotal.mul(2)); + }); + }); + + it("Correctly sums 1:many token balances", async function () { + [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach(async (chainId) => { + let balance = inventoryClient.getCumulativeBalance(mainnetUsdc); + expect(balance).to.equal(initialUsdcTotal); + + tokenClient.setTokenData(chainId, nativeUSDC[chainId], initialUsdcTotal.mul(2), toBN(0)); + await tokenClient.update(); + + const newBalance = inventoryClient.getCumulativeBalance(mainnetUsdc); + expect(newBalance).to.equal(initialUsdcTotal.add(balance)); + }); + }); + }); }); function seedMocks(seedBalances: { [chainId: string]: { [token: string]: BigNumber } }) { From 9f0def954f02cb6eef8f626e071cee8404c95c34 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 12:10:53 +0200 Subject: [PATCH 28/66] Fix tests --- test/InventoryClient.InventoryRebalance.ts | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 2c6d4147f..5c5fa7572 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -21,7 +21,7 @@ import { ConfigStoreClient, InventoryClient } from "../src/clients"; // Tested import { CrossChainTransferClient } from "../src/clients/bridges"; import { InventoryConfig } from "../src/interfaces"; import { MockAdapterManager, MockBundleDataClient, MockHubPoolClient, MockTokenClient } from "./mocks/"; -import { bnZero, CHAIN_IDs, ERC20, TOKEN_SYMBOLS_MAP } from "../src/utils"; +import { bnZero, CHAIN_IDs, ERC20, fixedPointAdjustment as fixedPoint, TOKEN_SYMBOLS_MAP } from "../src/utils"; const toMegaWei = (num: string | number | BigNumber) => ethers.utils.parseUnits(num.toString(), 6); @@ -352,28 +352,33 @@ describe("InventoryClient: Rebalancing inventory", async function () { it("Correctly isolates 1:many token balances", async function () { [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach(async (chainId) => { - let balance = inventoryClient.getCumulativeBalance(mainnetUsdc); - expect(balance).to.be.greaterThan(bnZero); - expect(balance).to.equal(initialUsdcTotal); + const bridgedBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, bridgedUSDC[chainId]); + expect(bridgedBalance.eq(bnZero)).to.be.true; - tokenClient.setTokenData(chainId, nativeUSDC[chainId], initialUsdcTotal.mul(2), toBN(0)); + // Non-zero bridged USDC balance, zero native balance. + let nativeBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); + expect(nativeBalance.eq(bnZero)).to.be.true; + + // Add native balance. + tokenClient.setTokenData(chainId, nativeUSDC[chainId], bridgedBalance, toBN(0)); await tokenClient.update(); - balance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); - expect(balance).to.equal(initialUsdcTotal.mul(2)); + // Native balance should now match bridged balance. + nativeBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); + expect(nativeBalance.eq(bridgedBalance)).to.be.true; }); }); it("Correctly sums 1:many token balances", async function () { [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach(async (chainId) => { let balance = inventoryClient.getCumulativeBalance(mainnetUsdc); - expect(balance).to.equal(initialUsdcTotal); + expect(balance.eq(initialUsdcTotal)).to.be.true; tokenClient.setTokenData(chainId, nativeUSDC[chainId], initialUsdcTotal.mul(2), toBN(0)); await tokenClient.update(); const newBalance = inventoryClient.getCumulativeBalance(mainnetUsdc); - expect(newBalance).to.equal(initialUsdcTotal.add(balance)); + expect(newBalance.eq(initialUsdcTotal.add(balance))).to.be.true; }); }); }); From 6fb41ea235017a757f0595bb83222c67fadff3b6 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 22:47:08 +0200 Subject: [PATCH 29/66] Overhaul test --- src/clients/InventoryClient.ts | 6 +- test/InventoryClient.InventoryRebalance.ts | 278 +++++++++++++-------- 2 files changed, 174 insertions(+), 110 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 93bb07372..d73acf1c5 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -183,7 +183,7 @@ export class InventoryClient { } const shortfall = this.tokenClient.getShortfallTotalRequirement(chainId, l2Token); - const currentBalance = this.getBalanceOnChain(chainId, l1Token).sub(shortfall); + const currentBalance = this.getBalanceOnChain(chainId, l1Token, l2Token).sub(shortfall); // Multiply by scalar to avoid rounding errors. return currentBalance.mul(this.scalar).div(cumulativeBalance); @@ -205,6 +205,10 @@ export class InventoryClient { } getDestinationTokensForL1Token(l1Token: string, chainId: number | string): string[] { + if (chainId === this.hubPoolClient.chainId) { + return [l1Token]; + } + const tokenConfig = this.inventoryConfig.tokenConfig[l1Token]; if (isAliasConfig(tokenConfig)) { diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 5c5fa7572..7a35614e7 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -8,7 +8,6 @@ import { expect, hubPoolFixture, lastSpyLogIncludes, - randomAddress, sinon, smock, spyLogIncludes, @@ -31,7 +30,8 @@ let owner: SignerWithAddress, spy: sinon.SinonSpy, spyLogger: winston.Logger; let inventoryClient: InventoryClient; // tested let crossChainTransferClient: CrossChainTransferClient; -const enabledChainIds = [CHAIN_IDs.MAINNET, CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.ARBITRUM]; +const { MAINNET, OPTIMISM, POLYGON, BASE, ARBITRUM } = CHAIN_IDs; +const enabledChainIds = [MAINNET, OPTIMISM, POLYGON, BASE, ARBITRUM]; const mainnetWeth = TOKEN_SYMBOLS_MAP.WETH.addresses[1]; const mainnetUsdc = TOKEN_SYMBOLS_MAP.USDC.addresses[1]; @@ -39,12 +39,14 @@ let mainnetWethContract: FakeContract; let mainnetUsdcContract: FakeContract; // construct two mappings of chainId to token address. Set the l1 token address to the "real" token address. -const l2TokensForWeth = { 1: mainnetWeth }; -const l2TokensForUsdc = { 1: mainnetUsdc }; -enabledChainIds.slice(1).forEach((chainId) => { - l2TokensForWeth[chainId] = randomAddress(); - l2TokensForUsdc[chainId] = randomAddress(); -}); +const l2TokensForWeth = { [MAINNET]: mainnetWeth }; +const l2TokensForUsdc = { [MAINNET]: mainnetUsdc }; +enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => { + l2TokensForWeth[chainId] = TOKEN_SYMBOLS_MAP.WETH.addresses[chainId]; + l2TokensForUsdc[chainId] = TOKEN_SYMBOLS_MAP.USDC.addresses[chainId]; + }); // Configure target percentages as 80% mainnet, 10% optimism, 5% polygon and 5% Arbitrum. const targetOverageBuffer = toWei(1); @@ -55,28 +57,31 @@ const inventoryConfig: InventoryConfig = { wrapEtherThreshold: toWei(1), tokenConfig: { [mainnetWeth]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, [mainnetUsdc]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, }, }; // Construct an initial distribution that keeps these values within the above thresholds. const initialAllocation = { - [CHAIN_IDs.MAINNET]: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC - [CHAIN_IDs.OPTIMISM]: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC - [CHAIN_IDs.POLYGON]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC - [CHAIN_IDs.ARBITRUM]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC + [MAINNET]: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC + [OPTIMISM]: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC + [POLYGON]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC + [BASE]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC + [ARBITRUM]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC }; -const initialWethTotal = toWei(140); // Sum over all 4 chains is 140 -const initialUsdcTotal = toMegaWei(14000); // Sum over all 4 chains is 14000 +const initialWethTotal = toWei(150); // Sum over all 5 chains is 150 +const initialUsdcTotal = toMegaWei(15000); // Sum over all 5 chains is 15000 const initialTotals = { [mainnetWeth]: initialWethTotal, [mainnetUsdc]: initialUsdcTotal }; describe("InventoryClient: Rebalancing inventory", async function () { @@ -123,10 +128,10 @@ describe("InventoryClient: Rebalancing inventory", async function () { it("Accessors work as expected", async function () { expect(inventoryClient.getEnabledChains()).to.deep.equal(enabledChainIds); expect(inventoryClient.getL1Tokens()).to.deep.equal(Object.keys(inventoryConfig.tokenConfig)); - expect(inventoryClient.getEnabledL2Chains()).to.deep.equal([10, 137, 42161]); + expect(inventoryClient.getEnabledL2Chains()).to.deep.equal([OPTIMISM, POLYGON, BASE, ARBITRUM]); - expect(inventoryClient.getCumulativeBalance(mainnetWeth)).to.equal(initialWethTotal); - expect(inventoryClient.getCumulativeBalance(mainnetUsdc)).to.equal(initialUsdcTotal); + expect(inventoryClient.getCumulativeBalance(mainnetWeth).eq(initialWethTotal)).to.be.true; + expect(inventoryClient.getCumulativeBalance(mainnetUsdc).eq(initialUsdcTotal)).to.be.true; // Check the allocation matches to what is expected in the seed state of the mock. Check more complex matchers. const tokenDistribution = inventoryClient.getTokenDistributionPerL1Token(); @@ -134,12 +139,10 @@ describe("InventoryClient: Rebalancing inventory", async function () { for (const l1Token of inventoryClient.getL1Tokens()) { expect(inventoryClient.getBalanceOnChain(chainId, l1Token)).to.equal(initialAllocation[chainId][l1Token]); expect( - inventoryClient.crossChainTransferClient.getOutstandingCrossChainTransferAmount( - owner.address, - chainId, - l1Token - ) - ).to.equal(toBN(0)); // For now no cross-chain transfers + inventoryClient.crossChainTransferClient + .getOutstandingCrossChainTransferAmount(owner.address, chainId, l1Token) + .eq(bnZero) + ).to.be.true; // For now no cross-chain transfers const expectedShare = initialAllocation[chainId][l1Token].mul(toWei(1)).div(initialTotals[l1Token]); const l2Token = (l1Token === mainnetWeth ? l2TokensForWeth : l2TokensForUsdc)[chainId]; @@ -156,54 +159,54 @@ describe("InventoryClient: Rebalancing inventory", async function () { expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; // Now, simulate the re-allocation of funds. Say that the USDC on arbitrum is half used up. This will leave arbitrum - // with 500 USDC, giving a percentage of 500/14000 = 0.035. This is below the threshold of 0.5 so we should see + // with 500 USDC, giving a percentage of 500/15000 = 0.035. This is below the threshold of 0.5 so we should see // a re-balance executed in size of the target allocation + overshoot percentage. - const initialBalance = initialAllocation[42161][mainnetUsdc]; - expect(tokenClient.getBalance(42161, l2TokensForUsdc[42161])).to.equal(initialBalance); + const initialBalance = initialAllocation[ARBITRUM][mainnetUsdc]; + expect(tokenClient.getBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM])).to.equal(initialBalance); const withdrawAmount = toMegaWei(500); - tokenClient.decrementLocalBalance(42161, l2TokensForUsdc[42161], withdrawAmount); - expect(tokenClient.getBalance(42161, l2TokensForUsdc[42161])).to.equal(withdrawAmount); + tokenClient.decrementLocalBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM], withdrawAmount); + expect(tokenClient.getBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM])).to.equal(withdrawAmount); // The allocation of this should now be below the threshold of 5% so the inventory client should instruct a rebalance. const expectedAlloc = withdrawAmount.mul(toWei(1)).div(initialUsdcTotal.sub(withdrawAmount)); - expect(inventoryClient.getCurrentAllocationPct(mainnetUsdc, 42161)).to.equal(expectedAlloc); + expect(inventoryClient.getCurrentAllocationPct(mainnetUsdc, ARBITRUM)).to.equal(expectedAlloc); // Execute rebalance. Check logs and enqueued transaction in Adapter manager. Given the total amount over all chains // and the amount still on arbitrum we would expect the module to instruct the relayer to send over: - // (0.05 + 0.02) * (14000 - 500) - 500 = 445. Note the -500 component is there as arbitrum already has 500. our left + // (0.05 + 0.02) * (15000 - 500) - 500 = 515. Note the -500 component is there as arbitrum already has 500 remaining // post previous relay. - const expectedBridgedAmount = toMegaWei(445); + const expectedBridgedAmount = toMegaWei(515); await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "Executed Inventory rebalances")).to.be.true; expect(lastSpyLogIncludes(spy, "Rebalances sent to Arbitrum")).to.be.true; - expect(lastSpyLogIncludes(spy, "445.00 USDC rebalanced")).to.be.true; // cast to formatting expected by client. + expect(lastSpyLogIncludes(spy, "515.00 USDC rebalanced")).to.be.true; // cast to formatting expected by client. expect(lastSpyLogIncludes(spy, "This meets target allocation of 7.00%")).to.be.true; // config from client. // The mock adapter manager should have been called with the expected transaction. - expect(adapterManager.tokensSentCrossChain[42161][mainnetUsdc].amount).to.equal(expectedBridgedAmount); + expect(adapterManager.tokensSentCrossChain[ARBITRUM][mainnetUsdc].amount).to.equal(expectedBridgedAmount); // Now, mock these funds having entered the canonical bridge. - adapterManager.setMockedOutstandingCrossChainTransfers(42161, owner.address, mainnetUsdc, expectedBridgedAmount); + adapterManager.setMockedOutstandingCrossChainTransfers(ARBITRUM, owner.address, mainnetUsdc, expectedBridgedAmount); // Now that funds are "in the bridge" re-running the rebalance should not execute any transactions. await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; - expect(spyLogIncludes(spy, -2, '"outstandingTransfers":"445.00"')).to.be.true; + expect(spyLogIncludes(spy, -2, '"outstandingTransfers":"515.00"')).to.be.true; // Now mock that funds have finished coming over the bridge and check behavior is as expected. - adapterManager.setMockedOutstandingCrossChainTransfers(42161, owner.address, mainnetUsdc, toBN(0)); // zero the transfer. mock conclusion. + adapterManager.setMockedOutstandingCrossChainTransfers(ARBITRUM, owner.address, mainnetUsdc, bnZero); // zero the transfer. mock conclusion. // Balance after the relay concludes should be initial - withdrawn + bridged as 1000-500+445=945 const expectedPostRelayBalance = initialBalance.sub(withdrawAmount).add(expectedBridgedAmount); - tokenClient.setTokenData(42161, l2TokensForUsdc[42161], expectedPostRelayBalance, toBN(0)); + tokenClient.setTokenData(ARBITRUM, l2TokensForUsdc[ARBITRUM], expectedPostRelayBalance, bnZero); await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; // We should see a log for chain 42161 that shows the actual balance after the relay concluded and the share. - // actual balance should be listed above at 945. share should be 945/(13500) =0.7 (initial total - withdrawAmount). - expect(spyLogIncludes(spy, -2, '"42161":{"actualBalanceOnChain":"945.00"')).to.be.true; + // actual balance should be listed above at 1015. share should be 1015/(14500) = 0.7 (initial total - withdrawAmount). + expect(spyLogIncludes(spy, -2, `"${ARBITRUM}":{"actualBalanceOnChain":"1,015.00"`)).to.be.true; expect(spyLogIncludes(spy, -2, '"proRataShare":"7.00%"')).to.be.true; }); @@ -212,45 +215,49 @@ describe("InventoryClient: Rebalancing inventory", async function () { await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); - expect(tokenClient.getBalance(137, l2TokensForWeth[137])).to.equal(toWei(10)); // Starting balance. + expect(tokenClient.getBalance(POLYGON, l2TokensForWeth[POLYGON])).to.equal(toWei(10)); // Starting balance. // Construct a token shortfall of 18. const shortfallAmount = toWei(18); - tokenClient.setTokenShortFallData(137, l2TokensForWeth[137], [6969], shortfallAmount); + tokenClient.setTokenShortFallData(POLYGON, l2TokensForWeth[POLYGON], [6969], shortfallAmount); await inventoryClient.update(); // If we now consider how much should be sent over the bridge. The spoke pool, considering the shortfall, has an - // allocation of -5.7%. The target is, however, 5% of the total supply. factoring in the overshoot parameter we - // should see a transfer of 5 + 2 - (-5.7)=12.714% of total inventory. This should be an amount of 0.127*140=17.79. - const expectedBridgedAmount = toBN("17799999999999999880"); + // allocation of -5.3%. The target is, however, 5% of the total supply. factoring in the overshoot parameter we + // should see a transfer of 5 + 2 - (-5.3)=12.3% of total inventory. This should be an amount of 0.1233*150=18.49. + const expectedBridgedAmount = toBN("18499999999999999950"); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "Executed Inventory rebalances")).to.be.true; expect(lastSpyLogIncludes(spy, "Rebalances sent to Polygon")).to.be.true; - expect(lastSpyLogIncludes(spy, "17.79 WETH rebalanced")).to.be.true; // expected bridge amount rounded for logs. + expect(lastSpyLogIncludes(spy, "18.49 WETH rebalanced")).to.be.true; // expected bridge amount rounded for logs. expect(lastSpyLogIncludes(spy, "This meets target allocation of 7.00%")).to.be.true; // config from client. // Note that there should be some additional state updates that we should check. In particular the token balance // on L1 should have been decremented by the amount sent over the bridge and the Inventory client should be tracking // the cross-chain transfers. - expect(tokenClient.getBalance(1, mainnetWeth)).to.equal(toWei(100).sub(expectedBridgedAmount)); + expect(tokenClient.getBalance(MAINNET, mainnetWeth)).to.equal(toWei(100).sub(expectedBridgedAmount)); expect( - inventoryClient.crossChainTransferClient.getOutstandingCrossChainTransferAmount(owner.address, 137, mainnetWeth) + inventoryClient.crossChainTransferClient.getOutstandingCrossChainTransferAmount( + owner.address, + POLYGON, + mainnetWeth + ) ).to.equal(expectedBridgedAmount); // The mock adapter manager should have been called with the expected transaction. - expect(adapterManager.tokensSentCrossChain[137][mainnetWeth].amount).to.equal(expectedBridgedAmount); + expect(adapterManager.tokensSentCrossChain[POLYGON][mainnetWeth].amount).to.equal(expectedBridgedAmount); // Now, mock these funds having entered the canonical bridge. - adapterManager.setMockedOutstandingCrossChainTransfers(137, owner.address, mainnetWeth, expectedBridgedAmount); + adapterManager.setMockedOutstandingCrossChainTransfers(POLYGON, owner.address, mainnetWeth, expectedBridgedAmount); // Now that funds are "in the bridge" re-running the rebalance should not execute any transactions as the util // should consider the funds in transit as part of the balance and therefore should not send more. await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; - expect(spyLogIncludes(spy, -2, '"outstandingTransfers":"17.79"')).to.be.true; + expect(spyLogIncludes(spy, -2, '"outstandingTransfers":"18.49"')).to.be.true; expect(spyLogIncludes(spy, -2, '"actualBalanceOnChain":"10.00"')).to.be.true; - expect(spyLogIncludes(spy, -2, '"virtualBalanceOnChain":"27.79"')).to.be.true; + expect(spyLogIncludes(spy, -2, '"virtualBalanceOnChain":"28.49"')).to.be.true; // Now mock that funds have finished coming over the bridge and check behavior is as expected. // Zero the transfer. mock conclusion. @@ -302,84 +309,137 @@ describe("InventoryClient: Rebalancing inventory", async function () { }); describe("Remote chain token mappings", async function () { - const nativeUSDC = TOKEN_SYMBOLS_MAP.USDC.addresses; + const nativeUSDC = TOKEN_SYMBOLS_MAP._USDC.addresses; const bridgedUSDC = { ...TOKEN_SYMBOLS_MAP["USDC.e"].addresses, ...TOKEN_SYMBOLS_MAP["USDbC"].addresses }; beforeEach(async function () { // Sub in a nested USDC config for the existing USDC config. const usdcConfig = { - [nativeUSDC[CHAIN_IDs.OPTIMISM]]: { - [CHAIN_IDs.OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [nativeUSDC[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, }, - [nativeUSDC[CHAIN_IDs.POLYGON]]: { - [CHAIN_IDs.POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [nativeUSDC[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [nativeUSDC[CHAIN_IDs.BASE]]: { - [CHAIN_IDs.BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [nativeUSDC[BASE]]: { + [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [nativeUSDC[CHAIN_IDs.ARBITRUM]]: { - [CHAIN_IDs.ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [nativeUSDC[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [bridgedUSDC[CHAIN_IDs.OPTIMISM]]: { - [CHAIN_IDs.OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [bridgedUSDC[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, }, - [bridgedUSDC[CHAIN_IDs.POLYGON]]: { - [CHAIN_IDs.POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [bridgedUSDC[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [bridgedUSDC[CHAIN_IDs.BASE]]: { - [CHAIN_IDs.BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [bridgedUSDC[BASE]]: { + [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [bridgedUSDC[CHAIN_IDs.ARBITRUM]]: { - [CHAIN_IDs.ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - } }; inventoryConfig.tokenConfig[mainnetUsdc] = usdcConfig; }); it("Correctly resolves 1:many token mappings", async function () { // Caller must specify l2Token for 1:many mappings. - expect(() => inventoryClient.getTokenConfig(mainnetUsdc, CHAIN_IDs.BASE)).to.throw; - - [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach((chainId) => { - let config = inventoryClient.getTokenConfig(mainnetUsdc, chainId, bridgedUSDC[chainId]); - expect(config).to.exist; - - let expectedConfig = inventoryConfig.tokenConfig[mainnetUsdc][bridgedUSDC[chainId]][chainId]; - expect(expectedConfig).to.exist; - expect(expectedConfig).to.deep.equal(expectedConfig); - }); + expect(() => inventoryClient.getTokenConfig(mainnetUsdc, BASE)).to.throw; + + enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => { + const config = inventoryClient.getTokenConfig(mainnetUsdc, chainId, bridgedUSDC[chainId]); + expect(config).to.exist; + + const expectedConfig = inventoryConfig.tokenConfig[mainnetUsdc][bridgedUSDC[chainId]][chainId]; + expect(expectedConfig).to.exist; + expect(expectedConfig).to.deep.equal(expectedConfig); + }); }); it("Correctly isolates 1:many token balances", async function () { - [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach(async (chainId) => { - const bridgedBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, bridgedUSDC[chainId]); - expect(bridgedBalance.eq(bnZero)).to.be.true; - - // Non-zero bridged USDC balance, zero native balance. - let nativeBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); - expect(nativeBalance.eq(bnZero)).to.be.true; - - // Add native balance. - tokenClient.setTokenData(chainId, nativeUSDC[chainId], bridgedBalance, toBN(0)); - await tokenClient.update(); - - // Native balance should now match bridged balance. - nativeBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); - expect(nativeBalance.eq(bridgedBalance)).to.be.true; - }); + enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => { + const bridgedBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, bridgedUSDC[chainId]); + expect(bridgedBalance.gt(bnZero)).to.be.true; + + // Non-zero bridged USDC balance, zero native balance. + let nativeBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); + expect(nativeBalance.eq(bnZero)).to.be.true; + + // Add native balance. + tokenClient.setTokenData(chainId, nativeUSDC[chainId], bridgedBalance); + + // Native balance should now match bridged balance. + nativeBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); + expect(nativeBalance.eq(bridgedBalance)).to.be.true; + }); }); it("Correctly sums 1:many token balances", async function () { - [CHAIN_IDs.OPTIMISM, CHAIN_IDs.POLYGON, CHAIN_IDs.BASE, CHAIN_IDs.ARBITRUM].forEach(async (chainId) => { - let balance = inventoryClient.getCumulativeBalance(mainnetUsdc); - expect(balance.eq(initialUsdcTotal)).to.be.true; + enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => { + const bridgedBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, bridgedUSDC[chainId]); + expect(bridgedBalance.gt(bnZero)).to.be.true; + + const nativeBalance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); + expect(nativeBalance.eq(bnZero)).to.be.true; - tokenClient.setTokenData(chainId, nativeUSDC[chainId], initialUsdcTotal.mul(2), toBN(0)); - await tokenClient.update(); + const cumulativeBalance = inventoryClient.getCumulativeBalance(mainnetUsdc); + expect(cumulativeBalance.eq(initialUsdcTotal)).to.be.true; + + tokenClient.setTokenData(chainId, nativeUSDC[chainId], bridgedBalance); + + const newBalance = inventoryClient.getCumulativeBalance(mainnetUsdc); + expect(newBalance.eq(initialUsdcTotal.add(bridgedBalance))).to.be.true; + + // Revert to 0 balance for native USDC. + tokenClient.setTokenData(chainId, nativeUSDC[chainId], bnZero); + }); + }); - const newBalance = inventoryClient.getCumulativeBalance(mainnetUsdc); - expect(newBalance.eq(initialUsdcTotal.add(balance))).to.be.true; - }); + it("Correctly tracks 1:many token distributions", async function () { + enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => { + // Total USDC across all chains. + let cumulativeBalance = inventoryClient.getCumulativeBalance(mainnetUsdc); + expect(cumulativeBalance.gt(bnZero)).to.be.true; + expect(cumulativeBalance.eq(initialUsdcTotal)).to.be.true; + + // The initial allocation is all bridged USDC, 0 native. + const bridgedAllocation = inventoryClient.getCurrentAllocationPct(mainnetUsdc, chainId, bridgedUSDC[chainId]); + expect(bridgedAllocation.gt(bnZero)).to.be.true; + let balance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, bridgedUSDC[chainId]); + expect(bridgedAllocation.eq(balance.mul(fixedPoint).div(cumulativeBalance))).to.be.true; + + let nativeAllocation = inventoryClient.getCurrentAllocationPct(mainnetUsdc, chainId, nativeUSDC[chainId]); + expect(nativeAllocation.eq(bnZero)).to.be.true; + + balance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]); + expect(nativeAllocation.eq(bnZero)).to.be.true; + + // Add native USDC, same amount as bridged USDC. + balance = inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, bridgedUSDC[chainId]); + tokenClient.setTokenData(chainId, nativeUSDC[chainId], balance); + expect(inventoryClient.getBalanceOnChain(chainId, mainnetUsdc, nativeUSDC[chainId]).eq(balance)).to.be.true; + expect(nativeAllocation.eq(bnZero)).to.be.true; + + // Native USDC allocation should now be non-zero. + nativeAllocation = inventoryClient.getCurrentAllocationPct(mainnetUsdc, chainId, nativeUSDC[chainId]); + expect(nativeAllocation.gt(bnZero)).to.be.true; + + expect(inventoryClient.getCumulativeBalance(mainnetUsdc).gt(cumulativeBalance)).to.be.true; + cumulativeBalance = inventoryClient.getCumulativeBalance(mainnetUsdc); + expect(cumulativeBalance.gt(initialUsdcTotal)).to.be.true; + + // Return native USDC balance to 0 for next loop. + tokenClient.setTokenData(chainId, nativeUSDC[chainId], bnZero); + }); }); }); }); From 8d44904e84190a321e37c863ddb110394f3e2b2a Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 22:54:50 +0200 Subject: [PATCH 30/66] Fix minor merge error --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index ba9b4f605..d31597e4a 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -573,6 +573,7 @@ export class InventoryClient { } else { runningBalanceForToken = leaf.runningBalances[l1TokenIndex]; } + const l2Token = this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId)); // Approximate latest running balance as last known proposed running balance... // - minus total deposit amount on chain since the latest end block proposed @@ -582,7 +583,6 @@ export class InventoryClient { this.getRepaymentTokenForL1Token(l1Token, chainId), blockRange[1] ); - const l2Token = this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId)); // Grab refunds that are not included in any bundle proposed on-chain. These are refunds that have not // been accounted for in the latest running balance set in `runningBalanceForToken`. From bd18e016303c409a4676222bc95723faebac773e Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 22:55:29 +0200 Subject: [PATCH 31/66] Tidy up --- src/clients/InventoryClient.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index d31597e4a..4aebea310 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -578,11 +578,7 @@ export class InventoryClient { // Approximate latest running balance as last known proposed running balance... // - minus total deposit amount on chain since the latest end block proposed // - plus total refund amount on chain since the latest end block proposed - const upcomingDeposits = this.bundleDataClient.getUpcomingDepositAmount( - chainId, - this.getRepaymentTokenForL1Token(l1Token, chainId), - blockRange[1] - ); + const upcomingDeposits = this.bundleDataClient.getUpcomingDepositAmount(chainId, l2Token, blockRange[1]); // Grab refunds that are not included in any bundle proposed on-chain. These are refunds that have not // been accounted for in the latest running balance set in `runningBalanceForToken`. @@ -590,9 +586,10 @@ export class InventoryClient { const upcomingRefunds = allBundleRefunds.pop(); // @dev upcoming refunds are always pushed last into this list. // If a chain didn't exist in the last bundle or a spoke pool client isn't defined, then // one of the refund entries for a chain can be undefined. - const upcomingRefundForChain = Object.values( - upcomingRefunds?.[chainId]?.[l2Token] ?? {} - ).reduce((acc, curr) => acc.add(curr), bnZero); + const upcomingRefundForChain = Object.values(upcomingRefunds?.[chainId]?.[l2Token] ?? {}).reduce( + (acc, curr) => acc.add(curr), + bnZero + ); // Updated running balance is last known running balance minus deposits plus upcoming refunds. const latestRunningBalance = runningBalanceForToken.sub(upcomingDeposits).add(upcomingRefundForChain); From 5b88f19b7af52826256a46d1a6291f8b2537605c Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 22:56:43 +0200 Subject: [PATCH 32/66] Fix balance check --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 4aebea310..d181e99a2 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -862,7 +862,7 @@ export class InventoryClient { `- ${symbol} transfer blocked. Required to send ` + `${formatter(amount.toString())} but relayer has ` + `${formatter(balance.toString())} on L1. There is currently ` + - `${formatter(this.getBalanceOnChain(chainId, l1Token).toString())} ${symbol} on ` + + `${formatter(this.getBalanceOnChain(chainId, l1Token, l2Token).toString())} ${symbol} on ` + `${getNetworkName(chainId)} which is ` + `${this.formatWei(distributionPct.toString())}% of the total ` + `${formatter(cumulativeBalance.toString())} ${symbol}.` + From 0c40936b2896225bf32b9856b081e3e03b06ab87 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 22:58:32 +0200 Subject: [PATCH 33/66] Sub in chain alias --- test/InventoryClient.InventoryRebalance.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 7a35614e7..79f7fd91d 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -32,8 +32,8 @@ let crossChainTransferClient: CrossChainTransferClient; const { MAINNET, OPTIMISM, POLYGON, BASE, ARBITRUM } = CHAIN_IDs; const enabledChainIds = [MAINNET, OPTIMISM, POLYGON, BASE, ARBITRUM]; -const mainnetWeth = TOKEN_SYMBOLS_MAP.WETH.addresses[1]; -const mainnetUsdc = TOKEN_SYMBOLS_MAP.USDC.addresses[1]; +const mainnetWeth = TOKEN_SYMBOLS_MAP.WETH.addresses[MAINNET]; +const mainnetUsdc = TOKEN_SYMBOLS_MAP.USDC.addresses[MAINNET]; let mainnetWethContract: FakeContract; let mainnetUsdcContract: FakeContract; From 5e80d91eec73e0dbd4f2375a9478a494c38cd9f8 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 23:38:40 +0200 Subject: [PATCH 34/66] Add l2Token --- src/clients/InventoryClient.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index d181e99a2..1cb49d6ee 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -763,7 +763,7 @@ export class InventoryClient { // Next, evaluate if we have enough tokens on L1 to actually do these rebalances. for (const rebalance of rebalancesRequired) { - const { balance, amount, l1Token, chainId } = rebalance; + const { balance, amount, l1Token, l2Token, chainId } = rebalance; // This is the balance left after any assumed rebalances from earlier loop iterations. const unallocatedBalance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token); @@ -782,6 +782,7 @@ export class InventoryClient { at: "InventoryClient", message: "🚧 Token balance on Ethereum changed before sending transaction, skipping rebalance", l1Token, + l2Token, l2ChainId: chainId, balance, currentBalance, @@ -792,6 +793,7 @@ export class InventoryClient { at: "InventoryClient", message: "Token balance in relayer on Ethereum is as expected, sending cross chain transfer", l1Token, + l2Token, l2ChainId: chainId, balance, }); From bca53b8649779b3eeaac018533518ff8cb8a1d98 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 23:45:59 +0200 Subject: [PATCH 35/66] refactor(test): InventoryClient simplifications Partly required as a byproduct of the pending USDC/CCTP updates. --- test/InventoryClient.InventoryRebalance.ts | 104 +++++++++++---------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index d8059152d..5feb3beaa 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -8,7 +8,6 @@ import { expect, hubPoolFixture, lastSpyLogIncludes, - randomAddress, sinon, smock, spyLogIncludes, @@ -21,7 +20,7 @@ import { ConfigStoreClient, InventoryClient } from "../src/clients"; // Tested import { CrossChainTransferClient } from "../src/clients/bridges"; import { InventoryConfig } from "../src/interfaces"; import { MockAdapterManager, MockBundleDataClient, MockHubPoolClient, MockTokenClient } from "./mocks/"; -import { ERC20 } from "../src/utils"; +import { bnZero, CHAIN_IDs, ERC20, fixedPointAdjustment as fixedPoint, TOKEN_SYMBOLS_MAP } from "../src/utils"; const toMegaWei = (num: string | number | BigNumber) => ethers.utils.parseUnits(num.toString(), 6); @@ -31,46 +30,51 @@ let owner: SignerWithAddress, spy: sinon.SinonSpy, spyLogger: winston.Logger; let inventoryClient: InventoryClient; // tested let crossChainTransferClient: CrossChainTransferClient; -const enabledChainIds = [1, 10, 137, 42161]; - -const mainnetWeth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; -const mainnetUsdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const { MAINNET, OPTIMISM, POLYGON, ARBITRUM } = CHAIN_IDs; +const enabledChainIds = [MAINNET, OPTIMISM, POLYGON, ARBITRUM]; +const mainnetWeth = TOKEN_SYMBOLS_MAP.WETH.addresses[MAINNET]; +const mainnetUsdc = TOKEN_SYMBOLS_MAP.USDC.addresses[MAINNET]; let mainnetWethContract: FakeContract; let mainnetUsdcContract: FakeContract; // construct two mappings of chainId to token address. Set the l1 token address to the "real" token address. -const l2TokensForWeth = { 1: mainnetWeth }; -const l2TokensForUsdc = { 1: mainnetUsdc }; -enabledChainIds.slice(1).forEach((chainId) => { - l2TokensForWeth[chainId] = randomAddress(); - l2TokensForUsdc[chainId] = randomAddress(); -}); +const l2TokensForWeth = { [MAINNET]: mainnetWeth }; +const l2TokensForUsdc = { [MAINNET]: mainnetUsdc }; +enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => { + l2TokensForWeth[chainId] = TOKEN_SYMBOLS_MAP.WETH.addresses[chainId]; + l2TokensForUsdc[chainId] = TOKEN_SYMBOLS_MAP.USDC.addresses[chainId]; + }); // Configure target percentages as 80% mainnet, 10% optimism, 5% polygon and 5% Arbitrum. +const targetOverageBuffer = toWei(1); const inventoryConfig: InventoryConfig = { + wrapEtherTargetPerChain: {}, + wrapEtherTarget: toWei(1), + wrapEtherThresholdPerChain: {}, + wrapEtherThreshold: toWei(1), tokenConfig: { [mainnetWeth]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1) }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [mainnetUsdc]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1) }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, }, - wrapEtherThreshold: toWei(1), }; // Construct an initial distribution that keeps these values within the above thresholds. const initialAllocation = { - 1: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC on Mainnet - 10: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC on Optimism - 137: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Polygon - 42161: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Arbitrum + [MAINNET]: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC + [OPTIMISM]: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC + [POLYGON]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC + [ARBITRUM]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC }; const initialWethTotal = toWei(140); // Sum over all 4 chains is 140 @@ -121,10 +125,10 @@ describe("InventoryClient: Rebalancing inventory", async function () { it("Accessors work as expected", async function () { expect(inventoryClient.getEnabledChains()).to.deep.equal(enabledChainIds); expect(inventoryClient.getL1Tokens()).to.deep.equal(Object.keys(inventoryConfig.tokenConfig)); - expect(inventoryClient.getEnabledL2Chains()).to.deep.equal([10, 137, 42161]); + expect(inventoryClient.getEnabledL2Chains()).to.deep.equal([OPTIMISM, POLYGON, ARBITRUM]); - expect(inventoryClient.getCumulativeBalance(mainnetWeth)).to.equal(initialWethTotal); - expect(inventoryClient.getCumulativeBalance(mainnetUsdc)).to.equal(initialUsdcTotal); + expect(inventoryClient.getCumulativeBalance(mainnetWeth).eq(initialWethTotal)).to.be.true; + expect(inventoryClient.getCumulativeBalance(mainnetUsdc).eq(initialUsdcTotal)).to.be.true; // Check the allocation matches to what is expected in the seed state of the mock. Check more complex matchers. const tokenDistribution = inventoryClient.getTokenDistributionPerL1Token(); @@ -134,12 +138,10 @@ describe("InventoryClient: Rebalancing inventory", async function () { initialAllocation[chainId][l1Token] ); expect( - inventoryClient.crossChainTransferClient.getOutstandingCrossChainTransferAmount( - owner.address, - chainId, - l1Token - ) - ).to.equal(toBN(0)); // For now no cross-chain transfers + inventoryClient.crossChainTransferClient + .getOutstandingCrossChainTransferAmount(owner.address, chainId, l1Token) + .eq(bnZero) + ).to.be.true; // For now no cross-chain transfers const expectedShare = initialAllocation[chainId][l1Token].mul(toWei(1)).div(initialTotals[l1Token]); const l2Token = (l1Token === mainnetWeth ? l2TokensForWeth : l2TokensForUsdc)[chainId]; @@ -158,15 +160,15 @@ describe("InventoryClient: Rebalancing inventory", async function () { // Now, simulate the re-allocation of funds. Say that the USDC on arbitrum is half used up. This will leave arbitrum // with 500 USDC, giving a percentage of 500/14000 = 0.035. This is below the threshold of 0.5 so we should see // a re-balance executed in size of the target allocation + overshoot percentage. - const initialBalance = initialAllocation[42161][mainnetUsdc]; - expect(tokenClient.getBalance(42161, l2TokensForUsdc[42161])).to.equal(initialBalance); + const initialBalance = initialAllocation[ARBITRUM][mainnetUsdc]; + expect(tokenClient.getBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM]).eq(initialBalance)).to.be.true; const withdrawAmount = toMegaWei(500); - tokenClient.decrementLocalBalance(42161, l2TokensForUsdc[42161], withdrawAmount); - expect(tokenClient.getBalance(42161, l2TokensForUsdc[42161])).to.equal(withdrawAmount); + tokenClient.decrementLocalBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM], withdrawAmount); + expect(tokenClient.getBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM]).eq(withdrawAmount)).to.be.true; // The allocation of this should now be below the threshold of 5% so the inventory client should instruct a rebalance. const expectedAlloc = withdrawAmount.mul(toWei(1)).div(initialUsdcTotal.sub(withdrawAmount)); - expect(inventoryClient.getCurrentAllocationPct(mainnetUsdc, 42161)).to.equal(expectedAlloc); + expect(inventoryClient.getCurrentAllocationPct(mainnetUsdc, ARBITRUM).eq(expectedAlloc)).to.be.true; // Execute rebalance. Check logs and enqueued transaction in Adapter manager. Given the total amount over all chains // and the amount still on arbitrum we would expect the module to instruct the relayer to send over: @@ -181,10 +183,10 @@ describe("InventoryClient: Rebalancing inventory", async function () { expect(lastSpyLogIncludes(spy, "This meets target allocation of 7.00%")).to.be.true; // config from client. // The mock adapter manager should have been called with the expected transaction. - expect(adapterManager.tokensSentCrossChain[42161][mainnetUsdc].amount).to.equal(expectedBridgedAmount); + expect(adapterManager.tokensSentCrossChain[ARBITRUM][mainnetUsdc].amount.eq(expectedBridgedAmount)).to.be.true; // Now, mock these funds having entered the canonical bridge. - adapterManager.setMockedOutstandingCrossChainTransfers(42161, owner.address, mainnetUsdc, expectedBridgedAmount); + adapterManager.setMockedOutstandingCrossChainTransfers(ARBITRUM, owner.address, mainnetUsdc, expectedBridgedAmount); // Now that funds are "in the bridge" re-running the rebalance should not execute any transactions. await inventoryClient.update(); @@ -193,17 +195,17 @@ describe("InventoryClient: Rebalancing inventory", async function () { expect(spyLogIncludes(spy, -2, '"outstandingTransfers":"445.00"')).to.be.true; // Now mock that funds have finished coming over the bridge and check behavior is as expected. - adapterManager.setMockedOutstandingCrossChainTransfers(42161, owner.address, mainnetUsdc, toBN(0)); // zero the transfer. mock conclusion. + adapterManager.setMockedOutstandingCrossChainTransfers(ARBITRUM, owner.address, mainnetUsdc, bnZero); // zero the transfer. mock conclusion. // Balance after the relay concludes should be initial - withdrawn + bridged as 1000-500+445=945 const expectedPostRelayBalance = initialBalance.sub(withdrawAmount).add(expectedBridgedAmount); - tokenClient.setTokenData(42161, l2TokensForUsdc[42161], expectedPostRelayBalance, toBN(0)); + tokenClient.setTokenData(ARBITRUM, l2TokensForUsdc[ARBITRUM], expectedPostRelayBalance, bnZero); await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; // We should see a log for chain 42161 that shows the actual balance after the relay concluded and the share. // actual balance should be listed above at 945. share should be 945/(13500) =0.7 (initial total - withdrawAmount). - expect(spyLogIncludes(spy, -2, '"42161":{"actualBalanceOnChain":"945.00"')).to.be.true; + expect(spyLogIncludes(spy, -2, `"${ARBITRUM}":{"actualBalanceOnChain":"945.00"`)).to.be.true; expect(spyLogIncludes(spy, -2, '"proRataShare":"7.00%"')).to.be.true; }); @@ -212,11 +214,11 @@ describe("InventoryClient: Rebalancing inventory", async function () { await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); - expect(tokenClient.getBalance(137, l2TokensForWeth[137])).to.equal(toWei(10)); // Starting balance. + expect(tokenClient.getBalance(POLYGON, l2TokensForWeth[POLYGON]).eq(toWei(10))).to.be.true; // Starting balance. // Construct a token shortfall of 18. const shortfallAmount = toWei(18); - tokenClient.setTokenShortFallData(137, l2TokensForWeth[137], [6969], shortfallAmount); + tokenClient.setTokenShortFallData(POLYGON, l2TokensForWeth[POLYGON], [6969], shortfallAmount); await inventoryClient.update(); // If we now consider how much should be sent over the bridge. The spoke pool, considering the shortfall, has an @@ -232,16 +234,20 @@ describe("InventoryClient: Rebalancing inventory", async function () { // Note that there should be some additional state updates that we should check. In particular the token balance // on L1 should have been decremented by the amount sent over the bridge and the Inventory client should be tracking // the cross-chain transfers. - expect(tokenClient.getBalance(1, mainnetWeth)).to.equal(toWei(100).sub(expectedBridgedAmount)); + expect(tokenClient.getBalance(MAINNET, mainnetWeth).eq(toWei(100).sub(expectedBridgedAmount))).to.be.true; expect( - inventoryClient.crossChainTransferClient.getOutstandingCrossChainTransferAmount(owner.address, 137, mainnetWeth) + inventoryClient.crossChainTransferClient.getOutstandingCrossChainTransferAmount( + owner.address, + POLYGON, + mainnetWeth + ) ).to.equal(expectedBridgedAmount); // The mock adapter manager should have been called with the expected transaction. - expect(adapterManager.tokensSentCrossChain[137][mainnetWeth].amount).to.equal(expectedBridgedAmount); + expect(adapterManager.tokensSentCrossChain[POLYGON][mainnetWeth].amount.eq(expectedBridgedAmount)).to.be.true; // Now, mock these funds having entered the canonical bridge. - adapterManager.setMockedOutstandingCrossChainTransfers(137, owner.address, mainnetWeth, expectedBridgedAmount); + adapterManager.setMockedOutstandingCrossChainTransfers(POLYGON, owner.address, mainnetWeth, expectedBridgedAmount); // Now that funds are "in the bridge" re-running the rebalance should not execute any transactions as the util // should consider the funds in transit as part of the balance and therefore should not send more. From 1b89118b13ac06362cc0d961b9b43ff65fdc2766 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 6 May 2024 23:53:03 +0200 Subject: [PATCH 36/66] lint --- test/InventoryClient.InventoryRebalance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 5feb3beaa..03e080b63 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -20,7 +20,7 @@ import { ConfigStoreClient, InventoryClient } from "../src/clients"; // Tested import { CrossChainTransferClient } from "../src/clients/bridges"; import { InventoryConfig } from "../src/interfaces"; import { MockAdapterManager, MockBundleDataClient, MockHubPoolClient, MockTokenClient } from "./mocks/"; -import { bnZero, CHAIN_IDs, ERC20, fixedPointAdjustment as fixedPoint, TOKEN_SYMBOLS_MAP } from "../src/utils"; +import { bnZero, CHAIN_IDs, ERC20, TOKEN_SYMBOLS_MAP } from "../src/utils"; const toMegaWei = (num: string | number | BigNumber) => ethers.utils.parseUnits(num.toString(), 6); From cb9db3979d5a615b48338a35fe15137d407f2af7 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 7 May 2024 00:00:50 +0200 Subject: [PATCH 37/66] Simplify --- src/clients/InventoryClient.ts | 28 +++++++--------------- test/InventoryClient.InventoryRebalance.ts | 2 +- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 1cb49d6ee..ed3cc621e 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -777,26 +777,14 @@ export class InventoryClient { // RPC's returning slowly, leading to concurrent/overlapping instances of the bot running. const tokenContract = new Contract(l1Token, ERC20.abi, this.hubPoolClient.hubPool.signer); const currentBalance = await tokenContract.balanceOf(this.relayer); - if (!balance.eq(currentBalance)) { - this.logger.warn({ - at: "InventoryClient", - message: "🚧 Token balance on Ethereum changed before sending transaction, skipping rebalance", - l1Token, - l2Token, - l2ChainId: chainId, - balance, - currentBalance, - }); - continue; - } else { - this.logger.debug({ - at: "InventoryClient", - message: "Token balance in relayer on Ethereum is as expected, sending cross chain transfer", - l1Token, - l2Token, - l2ChainId: chainId, - balance, - }); + + const balanceChanged = !balance.eq(currentBalance); + const [message, log] = balanceChanged + ? ["🚧 Token balance on mainnet changed, skipping rebalance", this.logger.warn] + : ["Token balance in relayer on mainnet is as expected, sending cross chain transfer", this.logger.debug]; + log({ at: "InventoryClient", message, l1Token, l2Token, l2ChainId: chainId, balance, currentBalance }); + + if (!balanceChanged) { possibleRebalances.push(rebalance); // Decrement token balance in client for this chain and increment cross chain counter. this.trackCrossChainTransfer(l1Token, amount, chainId); diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 70b24e60d..88d13cd79 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -300,7 +300,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { .whenCalledWith(owner.address) .returns(initialAllocation[1][mainnetUsdc].sub(toMegaWei(1))); await inventoryClient.rebalanceInventoryIfNeeded(); - expect(spyLogIncludes(spy, -2, "Token balance on Ethereum changed")).to.be.true; + expect(spyLogIncludes(spy, -2, "Token balance on mainnet changed")).to.be.true; // Reset and check again. mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[1][mainnetUsdc]); From eb11de6caa303532da3b194e2af73aa68f9ca734 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 00:51:30 +0200 Subject: [PATCH 38/66] refactor(CrossChainTransferClient): Support unique L2 tokens --- .../bridges/CrossChainTransferClient.ts | 57 ++++++++++++------- src/interfaces/index.ts | 12 +++- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/clients/bridges/CrossChainTransferClient.ts b/src/clients/bridges/CrossChainTransferClient.ts index d69f0600f..fe05a88ae 100644 --- a/src/clients/bridges/CrossChainTransferClient.ts +++ b/src/clients/bridges/CrossChainTransferClient.ts @@ -12,14 +12,35 @@ export class CrossChainTransferClient { ) {} // Get any funds currently in the canonical bridge. - getOutstandingCrossChainTransferAmount(address: string, chainId: number | string, l1Token: string): BigNumber { - const amount = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]?.totalAmount; - return amount ? toBN(amount) : bnZero; + getOutstandingCrossChainTransferAmount(address: string, chainId: number | string, l1Token: string, l2Token?: string): BigNumber { + const transfers = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]; + if (!transfers) { + return bnZero; + } + + if (l2Token) { + return transfers[l2Token]?.totalAmount ?? bnZero; + } + + // No specific l2Token specified; return the sum of all l1Token transfers to chainId. + return Object.values(transfers) + .map(({ totalAmount }) => totalAmount) + .flat() + .reduce((acc, curr) => acc.add(curr), bnZero); } - getOutstandingCrossChainTransferTxs(address: string, chainId: number | string, l1Token: string): string[] { - const txHashes = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]?.depositTxHashes; - return txHashes ? txHashes : []; + getOutstandingCrossChainTransferTxs(address: string, chainId: number | string, l1Token: string, l2Token?: string): string[] { + const transfers = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]; + if (!transfers) { + return []; + } + + if (l2Token) { + return transfers[l2Token]?.depositTxHashes ?? []; + } + + // No specific l2Token specified; return the set of all l1Token transfers to chainId. + return Object.values(transfers).map(({ depositTxHashes }) => depositTxHashes).flat(); } getEnabledChains(): number[] { @@ -30,26 +51,18 @@ export class CrossChainTransferClient { return this.getEnabledChains().filter((chainId) => chainId !== 1); } - increaseOutstandingTransfer(address: string, l1Token: string, rebalance: BigNumber, chainId: number): void { - if (!this.outstandingCrossChainTransfers[chainId]) { - this.outstandingCrossChainTransfers[chainId] = {}; - } - const transfers = this.outstandingCrossChainTransfers[chainId]; - if (transfers[address] === undefined) { - transfers[address] = {}; - } - if (transfers[address][l1Token] === undefined) { - transfers[address][l1Token] = { - totalAmount: bnZero, - depositTxHashes: [], - }; - } + increaseOutstandingTransfer(address: string, l1Token: string, l2Token: string, rebalance: BigNumber, chainId: number): void { + const transfers = this.outstandingCrossChainTransfers[chainId] ??= {}; + transfers[address] ??= {}; + transfers[address][l1Token] ??= {}; + transfers[address][l1Token][l2Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; // TODO: Require a tx hash here so we can track it as well. - transfers[address][l1Token].totalAmount = this.getOutstandingCrossChainTransferAmount( + transfers[address][l1Token][l2Token].totalAmount = this.getOutstandingCrossChainTransferAmount( address, chainId, - l1Token + l1Token, + l2Token ).add(rebalance); } diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 243985ddc..c081f5763 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -1,3 +1,4 @@ +import { BigNumber } from "ethers"; import { interfaces } from "@across-protocol/sdk-v2"; export * from "./InventoryManagement"; @@ -8,7 +9,16 @@ export * from "./Report"; export * from "./Arweave"; // Bridge interfaces -export type OutstandingTransfers = interfaces.OutstandingTransfers; +export interface OutstandingTransfers { + [address: string]: { + [l1Token: string]: { + [l2Token: string]: { + totalAmount: BigNumber; + depositTxHashes: string[]; + }; + }; + }; +} // Common interfaces export type SortableEvent = interfaces.SortableEvent; From 85cab0ab96fa8d99306136ae80c46b4a23f178d6 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 00:54:29 +0200 Subject: [PATCH 39/66] lint --- .../bridges/CrossChainTransferClient.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/clients/bridges/CrossChainTransferClient.ts b/src/clients/bridges/CrossChainTransferClient.ts index fe05a88ae..85f99e175 100644 --- a/src/clients/bridges/CrossChainTransferClient.ts +++ b/src/clients/bridges/CrossChainTransferClient.ts @@ -12,7 +12,12 @@ export class CrossChainTransferClient { ) {} // Get any funds currently in the canonical bridge. - getOutstandingCrossChainTransferAmount(address: string, chainId: number | string, l1Token: string, l2Token?: string): BigNumber { + getOutstandingCrossChainTransferAmount( + address: string, + chainId: number | string, + l1Token: string, + l2Token?: string + ): BigNumber { const transfers = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]; if (!transfers) { return bnZero; @@ -29,7 +34,12 @@ export class CrossChainTransferClient { .reduce((acc, curr) => acc.add(curr), bnZero); } - getOutstandingCrossChainTransferTxs(address: string, chainId: number | string, l1Token: string, l2Token?: string): string[] { + getOutstandingCrossChainTransferTxs( + address: string, + chainId: number | string, + l1Token: string, + l2Token?: string + ): string[] { const transfers = this.outstandingCrossChainTransfers[Number(chainId)]?.[address]?.[l1Token]; if (!transfers) { return []; @@ -40,7 +50,9 @@ export class CrossChainTransferClient { } // No specific l2Token specified; return the set of all l1Token transfers to chainId. - return Object.values(transfers).map(({ depositTxHashes }) => depositTxHashes).flat(); + return Object.values(transfers) + .map(({ depositTxHashes }) => depositTxHashes) + .flat(); } getEnabledChains(): number[] { @@ -51,8 +63,14 @@ export class CrossChainTransferClient { return this.getEnabledChains().filter((chainId) => chainId !== 1); } - increaseOutstandingTransfer(address: string, l1Token: string, l2Token: string, rebalance: BigNumber, chainId: number): void { - const transfers = this.outstandingCrossChainTransfers[chainId] ??= {}; + increaseOutstandingTransfer( + address: string, + l1Token: string, + l2Token: string, + rebalance: BigNumber, + chainId: number + ): void { + const transfers = (this.outstandingCrossChainTransfers[chainId] ??= {}); transfers[address] ??= {}; transfers[address][l1Token] ??= {}; transfers[address][l1Token][l2Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; From 4da8064fd78e15ea7f5a71a23c0b360c21f21ba4 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 2 May 2024 01:05:29 +0200 Subject: [PATCH 40/66] Add InventoryClient shims --- src/clients/InventoryClient.ts | 13 ++++++++++--- src/clients/bridges/CrossChainTransferClient.ts | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 2777a58fb..814653e88 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -171,9 +171,15 @@ export class InventoryClient { } // Decrement Tokens Balance And Increment Cross Chain Transfer - trackCrossChainTransfer(l1Token: string, rebalance: BigNumber, chainId: number | string): void { + trackCrossChainTransfer(l1Token: string, l2Token: string, rebalance: BigNumber, chainId: number | string): void { this.tokenClient.decrementLocalBalance(this.hubPoolClient.chainId, l1Token, rebalance); - this.crossChainTransferClient.increaseOutstandingTransfer(this.relayer, l1Token, rebalance, Number(chainId)); + this.crossChainTransferClient.increaseOutstandingTransfer( + this.relayer, + l1Token, + l2Token, + rebalance, + Number(chainId) + ); } async getAllBundleRefunds(): Promise { @@ -711,7 +717,8 @@ export class InventoryClient { }); possibleRebalances.push(rebalance); // Decrement token balance in client for this chain and increment cross chain counter. - this.trackCrossChainTransfer(l1Token, amount, chainId); + const l2Token = this.getDestinationTokenForL1Token(l1Token, chainId); + this.trackCrossChainTransfer(l1Token, l2Token, amount, chainId); } } else { // Extract unexecutable rebalances for logging. diff --git a/src/clients/bridges/CrossChainTransferClient.ts b/src/clients/bridges/CrossChainTransferClient.ts index 85f99e175..4f681c989 100644 --- a/src/clients/bridges/CrossChainTransferClient.ts +++ b/src/clients/bridges/CrossChainTransferClient.ts @@ -1,4 +1,4 @@ -import { BigNumber, bnZero, winston, assign, toBN, DefaultLogLevels, AnyObject } from "../../utils"; +import { BigNumber, bnZero, winston, assign, DefaultLogLevels, AnyObject } from "../../utils"; import { AdapterManager } from "./AdapterManager"; import { OutstandingTransfers } from "../../interfaces"; From bb9343acff1dacb72cbd859120eedcf6bd23c83d Mon Sep 17 00:00:00 2001 From: "James Morris, MS" <96435344+james-a-morris@users.noreply.github.com> Date: Thu, 2 May 2024 11:29:50 -0400 Subject: [PATCH 41/66] fix: resolve linea (#1472) Signed-off-by: james-a-morris --- src/clients/bridges/LineaAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index 86e5aad47..d5d1061d0 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -139,7 +139,7 @@ export class LineaAdapter extends BaseAdapter { : this.getL1TokenBridge(); } - async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { + async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { const outstandingTransfers: OutstandingTransfers = {}; const { l1SearchConfig, l2SearchConfig } = this.getUpdatedSearchConfigs(); const supportedL1Tokens = this.filterSupportedTokens(l1Tokens); From baf3cbd238d1568527d06195d58e7d19e2e0ccd2 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 7 May 2024 15:40:43 +0200 Subject: [PATCH 42/66] refactor(test): Cleanup InventoryClient refund chain test ...prior to introducing new tests. --- test/InventoryClient.RefundChain.ts | 163 +++++++++++++++------------- test/utils/utils.ts | 10 +- 2 files changed, 93 insertions(+), 80 deletions(-) diff --git a/test/InventoryClient.RefundChain.ts b/test/InventoryClient.RefundChain.ts index e60da3ea9..3fb64effc 100644 --- a/test/InventoryClient.RefundChain.ts +++ b/test/InventoryClient.RefundChain.ts @@ -9,9 +9,7 @@ import { expect, hubPoolFixture, lastSpyLogIncludes, - randomAddress, sinon, - toBN, toBNWei, toWei, winston, @@ -20,7 +18,7 @@ import { import { ConfigStoreClient, InventoryClient } from "../src/clients"; // Tested import { CrossChainTransferClient } from "../src/clients/bridges"; import { V3Deposit, InventoryConfig } from "../src/interfaces"; -import { ZERO_ADDRESS, bnZero, getNetworkName, TOKEN_SYMBOLS_MAP } from "../src/utils"; +import { CHAIN_IDs, ZERO_ADDRESS, bnZero, getNetworkName, TOKEN_SYMBOLS_MAP } from "../src/utils"; import { MockAdapterManager, MockBundleDataClient, @@ -30,9 +28,10 @@ import { } from "./mocks"; describe("InventoryClient: Refund chain selection", async function () { - const enabledChainIds = [1, 10, 137, 42161]; - const mainnetWeth = TOKEN_SYMBOLS_MAP.WETH.addresses[1]; - const mainnetUsdc = TOKEN_SYMBOLS_MAP.USDC.addresses[1]; + const { MAINNET, OPTIMISM, POLYGON, ARBITRUM } = CHAIN_IDs; + const enabledChainIds = [MAINNET, OPTIMISM, POLYGON, ARBITRUM]; + const mainnetWeth = TOKEN_SYMBOLS_MAP.WETH.addresses[MAINNET]; + const mainnetUsdc = TOKEN_SYMBOLS_MAP.USDC.addresses[MAINNET]; let hubPoolClient: MockHubPoolClient, adapterManager: MockAdapterManager, tokenClient: MockTokenClient; let bundleDataClient: MockBundleDataClient; @@ -42,46 +41,52 @@ describe("InventoryClient: Refund chain selection", async function () { let crossChainTransferClient: CrossChainTransferClient; // construct two mappings of chainId to token address. Set the l1 token address to the "real" token address. - const l2TokensForWeth = { 1: mainnetWeth }; - const l2TokensForUsdc = { 1: mainnetUsdc }; - enabledChainIds.slice(1).forEach((chainId) => { - l2TokensForWeth[chainId] = randomAddress(); - l2TokensForUsdc[chainId] = randomAddress(); - }); + const l2TokensForWeth = { [MAINNET]: mainnetWeth }; + const l2TokensForUsdc = { [MAINNET]: mainnetUsdc }; + enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => { + l2TokensForWeth[chainId] = TOKEN_SYMBOLS_MAP.WETH.addresses[chainId]; + l2TokensForUsdc[chainId] = TOKEN_SYMBOLS_MAP["USDC.e"].addresses[chainId]; + }); const toMegaWei = (num: string | number | BigNumber) => ethers.utils.parseUnits(num.toString(), 6); // Configure thresholds percentages as 10% optimism, 5% polygon and 5% Arbitrum with a target being threshold +2%. + const targetOverageBuffer = toWei(1); const inventoryConfig: InventoryConfig = { + wrapEtherTargetPerChain: {}, + wrapEtherTarget: toWei(1), + wrapEtherThresholdPerChain: {}, + wrapEtherThreshold: toWei(1), tokenConfig: { [mainnetWeth]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1) }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, [mainnetUsdc]: { - 10: { targetPct: toWei(0.12), thresholdPct: toWei(0.1) }, - 137: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, - 42161: { targetPct: toWei(0.07), thresholdPct: toWei(0.05) }, + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, }, - wrapEtherThreshold: toWei(1), }; // Construct an initial distribution that keeps these values within the above thresholds. const initialAllocation = { - 1: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC on Mainnet - 10: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC on Optimism - 137: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Polygon - 42161: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Arbitrum + [MAINNET]: { [mainnetWeth]: toWei(100), [mainnetUsdc]: toMegaWei(10000) }, // seed 100 WETH and 10000 USDC on Mainnet + [OPTIMISM]: { [mainnetWeth]: toWei(20), [mainnetUsdc]: toMegaWei(2000) }, // seed 20 WETH and 2000 USDC on Optimism + [POLYGON]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Polygon + [ARBITRUM]: { [mainnetWeth]: toWei(10), [mainnetUsdc]: toMegaWei(1000) }, // seed 10 WETH and 1000 USDC on Arbitrum }; const seedMocks = (seedBalances: { [chainId: string]: { [token: string]: BigNumber } }) => { hubPoolClient.addL1Token({ address: mainnetWeth, decimals: 18, symbol: "WETH" }); hubPoolClient.addL1Token({ address: mainnetUsdc, decimals: 6, symbol: "USDC" }); enabledChainIds.forEach((chainId) => { - adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetWeth, toBN(0)); - adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetUsdc, toBN(0)); + adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetWeth, bnZero); + adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetUsdc, bnZero); tokenClient.setTokenData(chainId, l2TokensForWeth[chainId], seedBalances[chainId][mainnetWeth]); tokenClient.setTokenData(chainId, l2TokensForUsdc[chainId], seedBalances[chainId][mainnetUsdc]); hubPoolClient.setTokenMapping(mainnetWeth, chainId, l2TokensForWeth[chainId]); @@ -134,13 +139,13 @@ describe("InventoryClient: Refund chain selection", async function () { const inputAmount = toBNWei(1); sampleDepositData = { depositId: 0, - originChainId: 1, - destinationChainId: 10, + originChainId: MAINNET, + destinationChainId: OPTIMISM, depositor: owner.address, recipient: owner.address, inputToken: mainnetWeth, inputAmount, - outputToken: l2TokensForWeth[10], + outputToken: l2TokensForWeth[OPTIMISM], outputAmount: inputAmount, message: "0x", quoteTimestamp: hubPoolClient.currentTime!, @@ -181,13 +186,13 @@ describe("InventoryClient: Refund chain selection", async function () { // fictitious relay that exceeds all outstanding liquidity on the target chain(Arbitrum) of 15 Weth (target only) // has 10 WETH in it. const largeRelayAmount = toWei(15); - tokenClient.setTokenShortFallData(42161, l2TokensForWeth[42161], [6969], largeRelayAmount); // Mock the shortfall. + tokenClient.setTokenShortFallData(ARBITRUM, l2TokensForWeth[ARBITRUM], [6969], largeRelayAmount); // Mock the shortfall. // The expected cross chain transfer amount is (0.05+0.02-(10-15)/140)*140=14.8 // Mock the cross-chain transfer // leaving L1 to go to arbitrum by adding it to the mock cross chain transfers and removing from l1 balance. const bridgedAmount = toWei(14.8); - adapterManager.setMockedOutstandingCrossChainTransfers(42161, owner.address, mainnetWeth, bridgedAmount); + adapterManager.setMockedOutstandingCrossChainTransfers(ARBITRUM, owner.address, mainnetWeth, bridgedAmount); await inventoryClient.update(); - tokenClient.setTokenData(1, mainnetWeth, initialAllocation[1][mainnetWeth].sub(bridgedAmount)); + tokenClient.setTokenData(MAINNET, mainnetWeth, initialAllocation[MAINNET][mainnetWeth].sub(bridgedAmount)); // Now, consider that the bot is run while these funds for the above deposit are in the canonical bridge and cant // be filled yet. When it runs it picks up a relay that it can do, of size 1.69 WETH. Each part of the computation @@ -207,11 +212,11 @@ describe("InventoryClient: Refund chain selection", async function () { // the buffer then refund on L1. if it is below the threshold then refund on the target chain. As this number is // is below the buffer plus the threshold then the bot should refund on L2. - sampleDepositData.destinationChainId = 42161; - sampleDepositData.outputToken = l2TokensForWeth[42161]; + sampleDepositData.destinationChainId = ARBITRUM; + sampleDepositData.outputToken = l2TokensForWeth[ARBITRUM]; sampleDepositData.inputAmount = toWei(1.69); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(42161); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(ARBITRUM); expect(lastSpyLogIncludes(spy, 'chainShortfall":"15000000000000000000"')).to.be.true; expect(lastSpyLogIncludes(spy, 'chainVirtualBalance":"24800000000000000000"')).to.be.true; // (10+14.8)=24.8 @@ -229,7 +234,7 @@ describe("InventoryClient: Refund chain selection", async function () { // relay allocation is 4.8/120 = 0.04. This is below the threshold of 0.05 so the bot should refund on the target. sampleDepositData.inputAmount = toWei(5); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(42161); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(ARBITRUM); // Check only the final step in the computation. expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"40000000000000000"')).to.be.true; // 4.8/120 = 0.04 @@ -239,8 +244,12 @@ describe("InventoryClient: Refund chain selection", async function () { // chain virtual balance with shortfall post relay is 9.8 - 5 + 10 = 14.8. cumulative virtual balance with shortfall // post relay is 125 - 5 + 10 = 130. Expected post relay allocation is 14.8/130 = 0.11. This is above the threshold // of 0.05 so the bot should refund on L1. - tokenClient.setTokenData(42161, l2TokensForWeth[42161], initialAllocation[42161][mainnetWeth].add(toWei(10))); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(1); + tokenClient.setTokenData( + ARBITRUM, + l2TokensForWeth[ARBITRUM], + initialAllocation[ARBITRUM][mainnetWeth].add(toWei(10)) + ); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(MAINNET); }); it("Correctly decides where to refund based on upcoming refunds", async function () { @@ -252,23 +261,23 @@ describe("InventoryClient: Refund chain selection", async function () { sampleDepositData.inputAmount = toWei(5); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); bundleDataClient.setReturnedPendingBundleRefunds({ - 1: createRefunds(owner.address, toWei(5), mainnetWeth), - 10: createRefunds(owner.address, toWei(5), l2TokensForWeth[10]), + [MAINNET]: createRefunds(owner.address, toWei(5), mainnetWeth), + [OPTIMISM]: createRefunds(owner.address, toWei(5), l2TokensForWeth[OPTIMISM]), }); bundleDataClient.setReturnedNextBundleRefunds({ - 10: createRefunds(owner.address, toWei(5), l2TokensForWeth[10]), + [OPTIMISM]: createRefunds(owner.address, toWei(5), l2TokensForWeth[OPTIMISM]), }); // We need HubPoolClient.l2TokenEnabledForL1Token() to return true for a given // L1 token and destination chain ID, otherwise it won't be counted in upcoming // refunds. hubPoolClient.setEnableAllL2Tokens(true); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(1); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(MAINNET); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"166666666666666666"')).to.be.true; // (20-5)/(140-5)=0.11 // If we set this to false in this test, the destination chain will be default used since the refund data // will be ignored. hubPoolClient.setEnableAllL2Tokens(false); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(10); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(OPTIMISM); }); it("Correctly throws when Deposit tokens are not equivalent", async function () { @@ -323,13 +332,13 @@ describe("InventoryClient: Refund chain selection", async function () { const inputAmount = toBNWei(1); sampleDepositData = { depositId: 0, - originChainId: 137, - destinationChainId: 10, + originChainId: POLYGON, + destinationChainId: OPTIMISM, depositor: owner.address, recipient: owner.address, - inputToken: l2TokensForWeth[137], + inputToken: l2TokensForWeth[POLYGON], inputAmount, - outputToken: l2TokensForWeth[10], + outputToken: l2TokensForWeth[OPTIMISM], outputAmount: inputAmount, message: "0x", quoteTimestamp: hubPoolClient.currentTime!, @@ -340,7 +349,7 @@ describe("InventoryClient: Refund chain selection", async function () { }); it("Both origin and destination chain allocations are below target", async function () { // Set Polygon allocation lower than target: - tokenClient.setTokenData(137, l2TokensForWeth[137], toWei(9)); + tokenClient.setTokenData(POLYGON, l2TokensForWeth[POLYGON], toWei(9)); // Post relay allocations: // Optimism (destination chain): (20-5)/(139-5)=11.1% < 12% @@ -348,7 +357,7 @@ describe("InventoryClient: Refund chain selection", async function () { // Relayer should choose to refund on destination over origin if both are under allocated sampleDepositData.inputAmount = toWei(5); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(10); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(OPTIMISM); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"111940298507462686"')).to.be.true; }); it("Origin chain allocation does not depend on subtracting from numerator", async function () { @@ -357,8 +366,8 @@ describe("InventoryClient: Refund chain selection", async function () { // Set Polygon allocation just higher than target. This is set so that any subtractions // from the numerator would break this test. - tokenClient.setTokenData(137, l2TokensForWeth[137], toWei(10)); - tokenClient.setTokenData(10, l2TokensForWeth[10], toWei(30)); + tokenClient.setTokenData(POLYGON, l2TokensForWeth[POLYGON], toWei(10)); + tokenClient.setTokenData(OPTIMISM, l2TokensForWeth[OPTIMISM], toWei(30)); // Post relay allocations: // Optimism (destination chain): (30-10)/(150-10)=14.3% > 12% @@ -366,14 +375,14 @@ describe("InventoryClient: Refund chain selection", async function () { // Relayer should default to hub chain. sampleDepositData.inputAmount = toWei(10); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(1); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(MAINNET); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"71428571428571428"')).to.be.true; }); it("Origin allocation is below target", async function () { // Set Polygon allocation lower than target: - tokenClient.setTokenData(137, l2TokensForWeth[137], toWei(5)); + tokenClient.setTokenData(POLYGON, l2TokensForWeth[POLYGON], toWei(5)); // Set Optimism allocation higher than target: - tokenClient.setTokenData(10, l2TokensForWeth[10], toWei(30)); + tokenClient.setTokenData(OPTIMISM, l2TokensForWeth[OPTIMISM], toWei(30)); // Post relay allocations: // Optimism (destination chain): (30-5)/(150-5)=17.2% > 12% @@ -381,14 +390,14 @@ describe("InventoryClient: Refund chain selection", async function () { // Relayer should choose to refund origin since destination isn't an option. sampleDepositData.inputAmount = toWei(5); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(137); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(POLYGON); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"35714285714285714"')).to.be.true; }); it("Origin allocation depends on outstanding transfers", async function () { // Set Polygon allocation lower than target: - tokenClient.setTokenData(137, l2TokensForWeth[137], toWei(5)); + tokenClient.setTokenData(POLYGON, l2TokensForWeth[POLYGON], toWei(5)); // Set Optimism allocation higher than target: - tokenClient.setTokenData(10, l2TokensForWeth[10], toWei(30)); + tokenClient.setTokenData(OPTIMISM, l2TokensForWeth[OPTIMISM], toWei(30)); // Post relay allocations: // Optimism (destination chain): (30-5)/(150-5)=17.2% > 12% @@ -396,40 +405,40 @@ describe("InventoryClient: Refund chain selection", async function () { // Relayer should choose to refund origin since destination isn't an option. sampleDepositData.inputAmount = toWei(5); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(137); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(POLYGON); // Now add outstanding transfers to Polygon that make the allocation above the target. Note that this // increases cumulative balance a bit. - adapterManager.setMockedOutstandingCrossChainTransfers(137, owner.address, mainnetWeth, toWei(10)); + adapterManager.setMockedOutstandingCrossChainTransfers(POLYGON, owner.address, mainnetWeth, toWei(10)); await inventoryClient.update(); // Post relay allocations: // Optimism (destination chain): (30-5)/(160-5)=16.1% > 12% // Polygon (origin chain): (15)/(160-5)=9.6% > 7% // Relayer should now default to hub chain. - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(1); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(MAINNET); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"100000000000000000"')).to.be.true; }); it("Origin allocation depends on short falls", async function () { // Set Polygon allocation lower than target: - tokenClient.setTokenData(137, l2TokensForWeth[137], toWei(5)); + tokenClient.setTokenData(POLYGON, l2TokensForWeth[POLYGON], toWei(5)); // Set Optimism allocation higher than target: - tokenClient.setTokenData(10, l2TokensForWeth[10], toWei(30)); + tokenClient.setTokenData(OPTIMISM, l2TokensForWeth[OPTIMISM], toWei(30)); // Shortfalls are subtracted from both numerator and denominator. - tokenClient.setTokenShortFallData(137, l2TokensForWeth[137], [6969], toWei(5)); // Mock the shortfall. + tokenClient.setTokenShortFallData(POLYGON, l2TokensForWeth[POLYGON], [6969], toWei(5)); // Mock the shortfall. // Post relay allocations: // Optimism (destination chain): (25-5)/(145-5)=14.3% > 12% // Polygon (origin chain): (0)/(145-5)=0% < 7% // Relayer should still use origin chain - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(137); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(POLYGON); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"0"')).to.be.true; // (20-5)/(140-5)=0.11 }); it("Origin allocation depends on upcoming refunds", async function () { // Set Polygon allocation lower than target: - tokenClient.setTokenData(137, l2TokensForWeth[137], toWei(5)); + tokenClient.setTokenData(POLYGON, l2TokensForWeth[POLYGON], toWei(5)); // Set Optimism allocation higher than target: - tokenClient.setTokenData(10, l2TokensForWeth[10], toWei(30)); + tokenClient.setTokenData(OPTIMISM, l2TokensForWeth[OPTIMISM], toWei(30)); // Post relay allocations: // Optimism (destination chain): (30-5)/(150-5)=17.2% > 12% @@ -439,7 +448,7 @@ describe("InventoryClient: Refund chain selection", async function () { sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); bundleDataClient.setReturnedPendingBundleRefunds({ - 137: createRefunds(owner.address, toWei(5), l2TokensForWeth[137]), + 137: createRefunds(owner.address, toWei(5), l2TokensForWeth[POLYGON]), }); // We need HubPoolClient.l2TokenEnabledForL1Token() to return true for a given // L1 token and destination chain ID, otherwise it won't be counted in upcoming @@ -450,12 +459,12 @@ describe("InventoryClient: Refund chain selection", async function () { // Optimism (destination chain): (30-5)/(155-5)=16.7% > 12% // Polygon (origin chain): (10)/(155-5)=6.7% > 7% // Relayer should still pick origin chain but compute a different allocation. - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(137); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(POLYGON); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"68965517241379310"')).to.be.true; }); it("includes origin, destination and hub chain in repayment chain list", async function () { const possibleRepaymentChains = inventoryClient.getPossibleRepaymentChainIds(sampleDepositData); - [sampleDepositData.originChainId, sampleDepositData.destinationChainId, 1].forEach((chainId) => { + [sampleDepositData.originChainId, sampleDepositData.destinationChainId, MAINNET].forEach((chainId) => { expect(possibleRepaymentChains).to.include(chainId); }); expect(possibleRepaymentChains.length).to.equal(3); @@ -469,8 +478,8 @@ describe("InventoryClient: Refund chain selection", async function () { // as possible repayment chains. hubPoolClient.setEnableAllL2Tokens(true); excessRunningBalances = { - [10]: toWei("0.1"), - [42161]: toWei("0.2"), + [OPTIMISM]: toWei("0.1"), + [ARBITRUM]: toWei("0.2"), }; // Fill in rest of slow withdrawal chains with 0 excess since we won't test them. inventoryClient.getSlowWithdrawalRepaymentChains(mainnetWeth).forEach((chainId) => { @@ -493,13 +502,13 @@ describe("InventoryClient: Refund chain selection", async function () { const inputAmount = toBNWei(1); sampleDepositData = { depositId: 0, - originChainId: 137, - destinationChainId: 1, + originChainId: POLYGON, + destinationChainId: MAINNET, depositor: owner.address, recipient: owner.address, - inputToken: l2TokensForWeth[137], + inputToken: l2TokensForWeth[POLYGON], inputAmount, - outputToken: l2TokensForWeth[1], + outputToken: l2TokensForWeth[MAINNET], outputAmount: inputAmount, message: "0x", quoteTimestamp: hubPoolClient.currentTime!, @@ -511,13 +520,13 @@ describe("InventoryClient: Refund chain selection", async function () { it("selects slow withdrawal chain with excess running balance and under relayer allocation", async function () { // Initial allocations are all under allocated so the first slow withdrawal chain should be selected since it has // the highest overage. - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(42161); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(ARBITRUM); // If we instead drop the excess on 42161 to 0, then we should take repayment on // the next slow withdrawal chain. - excessRunningBalances[42161] = toWei("0"); + excessRunningBalances[ARBITRUM] = toWei("0"); (inventoryClient as MockInventoryClient).setExcessRunningBalances(mainnetWeth, excessRunningBalances); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(10); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(OPTIMISM); }); it("includes slow withdrawal chains in possible repayment chain list", async function () { const possibleRepaymentChains = inventoryClient.getPossibleRepaymentChainIds(sampleDepositData); diff --git a/test/utils/utils.ts b/test/utils/utils.ts index b63c6edba..dbc6aaba8 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -476,10 +476,14 @@ export function getDefaultBlockRange(toBlockOffset: number): number[][] { return DEFAULT_BLOCK_RANGE_FOR_CHAIN.map((range) => [range[0], range[1] + toBlockOffset]); } -export function createRefunds(address: string, refundAmount: BigNumber, token: string): CombinedRefunds { +export function createRefunds( + outputToken: string, + refundAmount: BigNumber, + repaymentToken: string +): { [repaymentToken: string]: { [outputToken: string]: BigNumber } } { return { - [token]: { - [address]: refundAmount, + [repaymentToken]: { + [outputToken]: refundAmount, }, }; } From f9c3bf1b0f8ec549b7a70e55df25760f88f8556e Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 7 May 2024 15:49:30 +0200 Subject: [PATCH 43/66] lint --- test/utils/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/utils/utils.ts b/test/utils/utils.ts index dbc6aaba8..21a86c9b2 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -18,7 +18,6 @@ import { sampleRateModel, } from "../constants"; import { SpokePoolDeploymentResult, SpyLoggerResult } from "../types"; -import { CombinedRefunds } from "../../src/dataworker/DataworkerUtils"; export { SpyTransport, From fc15beb8dedcd16c78d989dfcdf44f05b40fc5be Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 7 May 2024 15:41:04 +0200 Subject: [PATCH 44/66] Initial repayment chain test --- src/clients/InventoryClient.ts | 1 + test/InventoryClient.RefundChain.ts | 77 ++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index ed3cc621e..79ed8ba6d 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -484,6 +484,7 @@ export class InventoryClient { this.hubPoolClient.areTokensEquivalent(inputToken, originChainId, outputToken, destinationChainId) ? chainVirtualBalanceWithShortfall.sub(outputAmount) : chainVirtualBalanceWithShortfall; + // Add upcoming refunds: chainVirtualBalanceWithShortfallPostRelay = chainVirtualBalanceWithShortfallPostRelay.add( totalRefundsPerChain[_chain] diff --git a/test/InventoryClient.RefundChain.ts b/test/InventoryClient.RefundChain.ts index 3fb64effc..ef1c5690a 100644 --- a/test/InventoryClient.RefundChain.ts +++ b/test/InventoryClient.RefundChain.ts @@ -64,7 +64,6 @@ describe("InventoryClient: Refund chain selection", async function () { [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, }, - [mainnetUsdc]: { [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, @@ -539,4 +538,80 @@ describe("InventoryClient: Refund chain selection", async function () { expect(possibleRepaymentChains.length).to.equal(4); }); }); + + describe("In-protocol swap", async function () { + const nativeUSDC = TOKEN_SYMBOLS_MAP._USDC.addresses; + const bridgedUSDC = { ...TOKEN_SYMBOLS_MAP["USDC.e"].addresses, ...TOKEN_SYMBOLS_MAP["USDbC"].addresses }; + + beforeEach(async function () { + // Sub in a nested USDC config for the existing USDC config. + const usdcConfig = { + [nativeUSDC[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [nativeUSDC[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [nativeUSDC[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [bridgedUSDC[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + }; + inventoryConfig.tokenConfig[mainnetUsdc] = usdcConfig; + + const inputAmount = toMegaWei(100); + sampleDepositData = { + depositId: 0, + originChainId: ARBITRUM, + destinationChainId: OPTIMISM, + depositor: owner.address, + recipient: owner.address, + inputToken: nativeUSDC[ARBITRUM], + inputAmount, + outputToken: bridgedUSDC[OPTIMISM], + outputAmount: inputAmount, + message: "0x", + quoteTimestamp: hubPoolClient.currentTime!, + fillDeadline: 0, + exclusivityDeadline: 0, + exclusiveRelayer: ZERO_ADDRESS, + }; + }); + + it("outputToken is not supported as a repaymentToken", async function () { + // Verify that there is no native USDC anywhere. The relayer is responsible for ensuring that it can make the fill. + enabledChainIds.filter((chainId) => chainId !== MAINNET).forEach((chainId) => + expect(tokenClient.getBalance(chainId, nativeUSDC[chainId]).eq(bnZero)).to.be.true + ); + + // All chains are at target balance; cumulative balance will go down but repaymentToken balances on all chains are unaffected. + expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.equal(MAINNET); + expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"71942446043165467"')).to.be.true; // (10000-0)/(14000-100)=0.71942 + + // Even when the output amount is equal to the destination's entire balance, take repayment on mainnet. + sampleDepositData.outputAmount = inventoryClient.getBalanceOnChain(OPTIMISM, mainnetUsdc); + expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.equal(MAINNET); + expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"83333333333333333"')).to.be.true; // (10000-0)/(14000-2000)=0.8333 + + // Drop the relayer's repaymentToken balance on Optimism. Repayment chain should now be Optimism. + let balance = tokenClient.getBalance(OPTIMISM, bridgedUSDC[OPTIMISM]); + tokenClient.setTokenData(OPTIMISM, bridgedUSDC[OPTIMISM], bnZero); + expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.equal(OPTIMISM); + + // Restore the Optimism balance and drop the Arbitrum balance. Repayment chain should now be Arbitrum. + tokenClient.setTokenData(OPTIMISM, bridgedUSDC[OPTIMISM], balance); + + balance = tokenClient.getBalance(ARBITRUM, bridgedUSDC[ARBITRUM]); + tokenClient.setTokenData(ARBITRUM, bridgedUSDC[ARBITRUM], bnZero); + expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.equal(ARBITRUM); + }); + }); }); From d04f442859af75c31b3e01f9fae7a85a449bb2a8 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 7 May 2024 23:09:14 +0200 Subject: [PATCH 45/66] Stragglers --- test/InventoryClient.RefundChain.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/InventoryClient.RefundChain.ts b/test/InventoryClient.RefundChain.ts index 3fb64effc..757a8f2ef 100644 --- a/test/InventoryClient.RefundChain.ts +++ b/test/InventoryClient.RefundChain.ts @@ -161,7 +161,7 @@ describe("InventoryClient: Refund chain selection", async function () { // above the threshold of 12 and so the bot should choose to be refunded on L1. sampleDepositData.inputAmount = toWei(1); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(1); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(MAINNET); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"136690647482014388"')).to.be.true; // (20-1)/(140-1)=0.136 // Now consider a case where the relayer is filling a marginally larger relay of size 5 WETH. Now the post relay @@ -169,7 +169,7 @@ describe("InventoryClient: Refund chain selection", async function () { // choose to refund on the L2. sampleDepositData.inputAmount = toWei(5); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(10); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(OPTIMISM); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"111111111111111111"')).to.be.true; // (20-5)/(140-5)=0.11 // Now consider a bigger relay that should force refunds on the L2 chain. Set the relay size to 10 WETH. now post @@ -177,7 +177,7 @@ describe("InventoryClient: Refund chain selection", async function () { // set the refund on L2. sampleDepositData.inputAmount = toWei(10); sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); - expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(10); + expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(OPTIMISM); expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"76923076923076923"')).to.be.true; // (20-10)/(140-10)=0.076 }); From 06ede93b4d7204b5349448d527db4ff10f40907b Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 7 May 2024 23:10:19 +0200 Subject: [PATCH 46/66] lint --- test/InventoryClient.RefundChain.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/InventoryClient.RefundChain.ts b/test/InventoryClient.RefundChain.ts index 9effdf69d..e90df0562 100644 --- a/test/InventoryClient.RefundChain.ts +++ b/test/InventoryClient.RefundChain.ts @@ -588,9 +588,9 @@ describe("InventoryClient: Refund chain selection", async function () { it("outputToken is not supported as a repaymentToken", async function () { // Verify that there is no native USDC anywhere. The relayer is responsible for ensuring that it can make the fill. - enabledChainIds.filter((chainId) => chainId !== MAINNET).forEach((chainId) => - expect(tokenClient.getBalance(chainId, nativeUSDC[chainId]).eq(bnZero)).to.be.true - ); + enabledChainIds + .filter((chainId) => chainId !== MAINNET) + .forEach((chainId) => expect(tokenClient.getBalance(chainId, nativeUSDC[chainId]).eq(bnZero)).to.be.true); // All chains are at target balance; cumulative balance will go down but repaymentToken balances on all chains are unaffected. expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.equal(MAINNET); From e3da99e35ccab928dc8156e9e42e657fc53deb65 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 00:57:34 +0200 Subject: [PATCH 47/66] Add rebalancing test --- test/InventoryClient.InventoryRebalance.ts | 122 ++++++++++++++++----- 1 file changed, 92 insertions(+), 30 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 88d13cd79..126df4e21 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -20,7 +20,15 @@ import { ConfigStoreClient, InventoryClient } from "../src/clients"; // Tested import { CrossChainTransferClient } from "../src/clients/bridges"; import { InventoryConfig } from "../src/interfaces"; import { MockAdapterManager, MockBundleDataClient, MockHubPoolClient, MockTokenClient } from "./mocks/"; -import { bnZero, CHAIN_IDs, ERC20, fixedPointAdjustment as fixedPoint, TOKEN_SYMBOLS_MAP } from "../src/utils"; +import { + bnZero, + CHAIN_IDs, + createFormatFunction, + ERC20, + fixedPointAdjustment as fixedPoint, + getNetworkName, + TOKEN_SYMBOLS_MAP, +} from "../src/utils"; const toMegaWei = (num: string | number | BigNumber) => ethers.utils.parseUnits(num.toString(), 6); @@ -119,8 +127,8 @@ describe("InventoryClient: Rebalancing inventory", async function () { mainnetWethContract = await smock.fake(ERC20.abi, { address: mainnetWeth }); mainnetUsdcContract = await smock.fake(ERC20.abi, { address: mainnetUsdc }); - mainnetWethContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[1][mainnetWeth]); - mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[1][mainnetUsdc]); + mainnetWethContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[MAINNET][mainnetWeth]); + mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[MAINNET][mainnetUsdc]); seedMocks(initialAllocation); }); @@ -311,35 +319,35 @@ describe("InventoryClient: Rebalancing inventory", async function () { describe("Remote chain token mappings", async function () { const nativeUSDC = TOKEN_SYMBOLS_MAP._USDC.addresses; const bridgedUSDC = { ...TOKEN_SYMBOLS_MAP["USDC.e"].addresses, ...TOKEN_SYMBOLS_MAP["USDbC"].addresses }; + const usdcConfig = { + [nativeUSDC[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [nativeUSDC[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [nativeUSDC[BASE]]: { + [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [nativeUSDC[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [bridgedUSDC[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[BASE]]: { + [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [bridgedUSDC[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + }; beforeEach(async function () { - // Sub in a nested USDC config for the existing USDC config. - const usdcConfig = { - [nativeUSDC[OPTIMISM]]: { - [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, - }, - [nativeUSDC[POLYGON]]: { - [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - }, - [nativeUSDC[BASE]]: { - [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - }, - [nativeUSDC[ARBITRUM]]: { - [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - }, - [bridgedUSDC[OPTIMISM]]: { - [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, - }, - [bridgedUSDC[POLYGON]]: { - [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - }, - [bridgedUSDC[BASE]]: { - [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - }, - [bridgedUSDC[ARBITRUM]]: { - [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - }, - }; + // Sub in a nested USDC config for the existing USDC single-token config. inventoryConfig.tokenConfig[mainnetUsdc] = usdcConfig; }); @@ -441,6 +449,60 @@ describe("InventoryClient: Rebalancing inventory", async function () { tokenClient.setTokenData(chainId, nativeUSDC[chainId], bnZero); }); }); + + it("Correctly rebalances mainnet USDC into non-repayment USDC", async function () { + // Unset all native USDC allocations. + for (const chainId of [OPTIMISM, POLYGON, BASE, ARBITRUM]) { + const l2Token = nativeUSDC[chainId]; + delete inventoryConfig.tokenConfig[mainnetUsdc][l2Token]; + } + + await inventoryClient.update(); + await inventoryClient.rebalanceInventoryIfNeeded(); + expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; + + const cumulativeUSDC = inventoryClient.getCumulativeBalance(mainnetUsdc); + const targetPct = toWei(0.1); + const thresholdPct = toWei(0.05); + const expectedRebalance = cumulativeUSDC.mul(targetPct).div(fixedPoint); + const { decimals } = TOKEN_SYMBOLS_MAP.USDC; + const formatter = createFormatFunction(2, 4, false, decimals); + const formattedAmount = formatter(expectedRebalance.toString()); + + let virtualMainnetBalance = initialAllocation[MAINNET][mainnetUsdc]; + + for (const chainId of [OPTIMISM, POLYGON, BASE, ARBITRUM]) { + const chain = getNetworkName(chainId); + await inventoryClient.update(); + const l2Token = nativeUSDC[chainId]; + + // Apply a new target balance for native USDC. + inventoryConfig.tokenConfig[mainnetUsdc][l2Token] = { + [chainId]: { targetPct, thresholdPct, targetOverageBuffer }, + }; + + await inventoryClient.update(); + await inventoryClient.rebalanceInventoryIfNeeded(); + expect(lastSpyLogIncludes(spy, `Rebalances sent to ${chain}`)).to.be.true; + expect(lastSpyLogIncludes(spy, `${formattedAmount} USDC rebalanced`)).to.be.true; + expect(lastSpyLogIncludes(spy, "This meets target allocation of 10.00%")).to.be.true; // config from client. + + // Decrement the mainnet USDC balance to simulate the rebalance. + virtualMainnetBalance = virtualMainnetBalance.sub(expectedRebalance); + mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(virtualMainnetBalance); + + // The mock adapter manager should have been called with the expected transaction. + expect(adapterManager.tokensSentCrossChain[chainId][mainnetUsdc].amount.eq(expectedRebalance)).to.be.true; + + // Now, mock these funds having entered the relevant bridge. + adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetUsdc, expectedRebalance); + + await inventoryClient.update(); + await inventoryClient.rebalanceInventoryIfNeeded(); + expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; + expect(spyLogIncludes(spy, -2, `"outstandingTransfers":"${formattedAmount}"`)).to.be.true; + } + }); }); }); From bbd8fb9bc7368718c51c1a56112513981b5ef6bc Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 01:05:38 +0200 Subject: [PATCH 48/66] Additional --- test/InventoryClient.InventoryRebalance.ts | 40 +++++++++++----------- test/InventoryClient.RefundChain.ts | 4 +-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 03e080b63..7770aa982 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -116,8 +116,8 @@ describe("InventoryClient: Rebalancing inventory", async function () { mainnetWethContract = await smock.fake(ERC20.abi, { address: mainnetWeth }); mainnetUsdcContract = await smock.fake(ERC20.abi, { address: mainnetUsdc }); - mainnetWethContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[1][mainnetWeth]); - mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[1][mainnetUsdc]); + mainnetWethContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[MAINNET][mainnetWeth]); + mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[MAINNET][mainnetUsdc]); seedMocks(initialAllocation); }); @@ -203,7 +203,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; - // We should see a log for chain 42161 that shows the actual balance after the relay concluded and the share. + // We should see a log for chain ARBITRUM that shows the actual balance after the relay concluded and the share. // actual balance should be listed above at 945. share should be 945/(13500) =0.7 (initial total - withdrawAmount). expect(spyLogIncludes(spy, -2, `"${ARBITRUM}":{"actualBalanceOnChain":"945.00"`)).to.be.true; expect(spyLogIncludes(spy, -2, '"proRataShare":"7.00%"')).to.be.true; @@ -260,20 +260,20 @@ describe("InventoryClient: Rebalancing inventory", async function () { // Now mock that funds have finished coming over the bridge and check behavior is as expected. // Zero the transfer. mock conclusion. - adapterManager.setMockedOutstandingCrossChainTransfers(137, owner.address, mainnetWeth, toBN(0)); + adapterManager.setMockedOutstandingCrossChainTransfers(POLYGON, owner.address, mainnetWeth, bnZero); // Balance after the relay concludes should be initial + bridged amount as 10+17.9=27.9 const expectedPostRelayBalance = toWei(10).add(expectedBridgedAmount); - tokenClient.setTokenData(137, l2TokensForWeth[137], expectedPostRelayBalance, toBN(0)); + tokenClient.setTokenData(POLYGON, l2TokensForWeth[POLYGON], expectedPostRelayBalance, bnZero); // The token shortfall should now no longer be an issue. This means we can fill the relay of 18 size now. - tokenClient.setTokenShortFallData(137, l2TokensForWeth[137], [6969], toBN(0)); - tokenClient.decrementLocalBalance(137, l2TokensForWeth[137], shortfallAmount); // mock the relay actually filling. + tokenClient.setTokenShortFallData(POLYGON, l2TokensForWeth[POLYGON], [6969], bnZero); + tokenClient.decrementLocalBalance(POLYGON, l2TokensForWeth[POLYGON], shortfallAmount); // mock the relay actually filling. await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; - // We should see a log for chain 42161 that shows the actual balance after the relay concluded and the share. + // We should see a log for chain ARBITRUM that shows the actual balance after the relay concluded and the share. // actual balance should be listed above at 945. share should be 945/(13500) =0.7 (initial total - withdrawAmount). - // expect(spyLogIncludes(spy, -2, `"42161":{"actualBalanceOnChain":"945.00"`)).to.be.true; + // expect(spyLogIncludes(spy, -2, `"ARBITRUM":{"actualBalanceOnChain":"945.00"`)).to.be.true; // expect(spyLogIncludes(spy, -2, `"proRataShare":"7.00%"`)).to.be.true; }); @@ -284,25 +284,25 @@ describe("InventoryClient: Rebalancing inventory", async function () { // Now, simulate the re-allocation of funds. Say that the USDC on arbitrum is half used up. This will leave arbitrum // with 500 USDC, giving a percentage of 500/14000 = 0.035. This is below the threshold of 0.5 so we should see // a re-balance executed in size of the target allocation + overshoot percentage. - const initialBalance = initialAllocation[42161][mainnetUsdc]; - expect(tokenClient.getBalance(42161, l2TokensForUsdc[42161])).to.equal(initialBalance); + const initialBalance = initialAllocation[ARBITRUM][mainnetUsdc]; + expect(tokenClient.getBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM])).to.equal(initialBalance); const withdrawAmount = toMegaWei(500); - tokenClient.decrementLocalBalance(42161, l2TokensForUsdc[42161], withdrawAmount); - expect(tokenClient.getBalance(42161, l2TokensForUsdc[42161])).to.equal(withdrawAmount); + tokenClient.decrementLocalBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM], withdrawAmount); + expect(tokenClient.getBalance(ARBITRUM, l2TokensForUsdc[ARBITRUM])).to.equal(withdrawAmount); // The allocation of this should now be below the threshold of 5% so the inventory client should instruct a rebalance. const expectedAlloc = withdrawAmount.mul(toWei(1)).div(initialUsdcTotal.sub(withdrawAmount)); - expect(inventoryClient.getCurrentAllocationPct(mainnetUsdc, 42161)).to.equal(expectedAlloc); + expect(inventoryClient.getCurrentAllocationPct(mainnetUsdc, ARBITRUM)).to.equal(expectedAlloc); // Set USDC balance to be lower than expected. mainnetUsdcContract.balanceOf .whenCalledWith(owner.address) - .returns(initialAllocation[1][mainnetUsdc].sub(toMegaWei(1))); + .returns(initialAllocation[MAINNET][mainnetUsdc].sub(toMegaWei(1))); await inventoryClient.rebalanceInventoryIfNeeded(); expect(spyLogIncludes(spy, -2, "Token balance on Ethereum changed")).to.be.true; // Reset and check again. - mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[1][mainnetUsdc]); + mainnetUsdcContract.balanceOf.whenCalledWith(owner.address).returns(initialAllocation[MAINNET][mainnetUsdc]); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "Executed Inventory rebalances")).to.be.true; }); @@ -312,10 +312,10 @@ function seedMocks(seedBalances: { [chainId: string]: { [token: string]: BigNumb hubPoolClient.addL1Token({ address: mainnetWeth, decimals: 18, symbol: "WETH" }); hubPoolClient.addL1Token({ address: mainnetUsdc, decimals: 6, symbol: "USDC" }); enabledChainIds.forEach((chainId) => { - adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetWeth, toBN(0)); - adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetUsdc, toBN(0)); - tokenClient.setTokenData(chainId, l2TokensForWeth[chainId], seedBalances[chainId][mainnetWeth], toBN(0)); - tokenClient.setTokenData(chainId, l2TokensForUsdc[chainId], seedBalances[chainId][mainnetUsdc], toBN(0)); + adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetWeth, bnZero); + adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetUsdc, bnZero); + tokenClient.setTokenData(chainId, l2TokensForWeth[chainId], seedBalances[chainId][mainnetWeth], bnZero); + tokenClient.setTokenData(chainId, l2TokensForUsdc[chainId], seedBalances[chainId][mainnetUsdc], bnZero); hubPoolClient.setTokenMapping(mainnetWeth, chainId, l2TokensForWeth[chainId]); hubPoolClient.setTokenMapping(mainnetUsdc, chainId, l2TokensForUsdc[chainId]); }); diff --git a/test/InventoryClient.RefundChain.ts b/test/InventoryClient.RefundChain.ts index 757a8f2ef..c569b1278 100644 --- a/test/InventoryClient.RefundChain.ts +++ b/test/InventoryClient.RefundChain.ts @@ -448,7 +448,7 @@ describe("InventoryClient: Refund chain selection", async function () { sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData); bundleDataClient.setReturnedPendingBundleRefunds({ - 137: createRefunds(owner.address, toWei(5), l2TokensForWeth[POLYGON]), + [POLYGON]: createRefunds(owner.address, toWei(5), l2TokensForWeth[POLYGON]), }); // We need HubPoolClient.l2TokenEnabledForL1Token() to return true for a given // L1 token and destination chain ID, otherwise it won't be counted in upcoming @@ -522,7 +522,7 @@ describe("InventoryClient: Refund chain selection", async function () { // the highest overage. expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.equal(ARBITRUM); - // If we instead drop the excess on 42161 to 0, then we should take repayment on + // If we instead drop the excess on Arbitrum to 0, then we should take repayment on // the next slow withdrawal chain. excessRunningBalances[ARBITRUM] = toWei("0"); (inventoryClient as MockInventoryClient).setExcessRunningBalances(mainnetWeth, excessRunningBalances); From 6cb87f32c055b2b47715255d62bf67f9e6a55599 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 01:07:05 +0200 Subject: [PATCH 49/66] Tweak --- test/InventoryClient.InventoryRebalance.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 7770aa982..fc6a262d0 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -271,9 +271,9 @@ describe("InventoryClient: Rebalancing inventory", async function () { await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; - // We should see a log for chain ARBITRUM that shows the actual balance after the relay concluded and the share. + // We should see a log for chain Arbitrum that shows the actual balance after the relay concluded and the share. // actual balance should be listed above at 945. share should be 945/(13500) =0.7 (initial total - withdrawAmount). - // expect(spyLogIncludes(spy, -2, `"ARBITRUM":{"actualBalanceOnChain":"945.00"`)).to.be.true; + // expect(spyLogIncludes(spy, -2, `"${ARBITRUM}":{"actualBalanceOnChain":"945.00"`)).to.be.true; // expect(spyLogIncludes(spy, -2, `"proRataShare":"7.00%"`)).to.be.true; }); From 20ad61070de8ab076669657db1730f4b2373b2e8 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 01:15:48 +0200 Subject: [PATCH 50/66] Doc --- src/interfaces/InventoryManagement.ts | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/interfaces/InventoryManagement.ts b/src/interfaces/InventoryManagement.ts index 2b2d80dc8..4f2e76c11 100644 --- a/src/interfaces/InventoryManagement.ts +++ b/src/interfaces/InventoryManagement.ts @@ -18,6 +18,38 @@ export type ChainTokenInventory = { [symbol: string]: ChainTokenConfig; }; +/** + * Example configuration: + * - DAI on chains 10 & 42161. + * - Bridged USDC (USDC.e, USDbC) on chains 10, 137, 324, 8453, 42161 & 59144. + * - Native USDC on Polygon. + * + * All token allocations are "global", so Polygon will be allocated a total of 8% of all USDC: + * - 4% of global USDC as Native USDC, and + * - 4% as Bridged USDC. + * + * "tokenConfig": { + * "DAI": { + * "10": { "targetPct": 8, "thresholdPct": 4 }, + * "42161": { "targetPct": 8, "thresholdPct": 4 }, + * }, + * "USDC": { + * "USDC.e": { + * "10": { "targetPct": 8, "thresholdPct": 4 }, + * "137": { "targetPct": 4, "thresholdPct": 2 }, + * "324": { "targetPct": 8, "thresholdPct": 4 }, + * "42161": { "targetPct": 8, "thresholdPct": 4 }, + * "59144": { "targetPct": 5, "thresholdPct": 2 } + * }, + * "USDbC": { + * "8453": { "targetPct": 5, "thresholdPct": 2 } + * }, + * "USDC": { + * "137": { "targetPct": 4, "thresholdPct": 2 } + * } + * } + * } + */ export interface InventoryConfig { // tokenConfig can map to a single token allocation, or a set of allocations that all map to the same HubPool token. tokenConfig: { [l1Token: string]: ChainTokenConfig } | { [l1Token: string]: ChainTokenInventory }; From 2e1ce4c9aab4efbed42c4b2abaf33586c4087f7f Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 01:19:21 +0200 Subject: [PATCH 51/66] Update method name & document --- src/clients/InventoryClient.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 79ed8ba6d..0650fbdb7 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -118,7 +118,7 @@ export class InventoryClient { if (isDefined(l2Token)) { balance = this.tokenClient.getBalance(chainId, l2Token); } else { - const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); + const l2Tokens = this.getRemoteTokensForL1Token(l1Token, chainId); balance = l2Tokens .map((l2Token) => this.tokenClient.getBalance(chainId, l2Token)) .reduce((acc, curr) => acc.add(curr), bnZero); @@ -150,7 +150,7 @@ export class InventoryClient { return; } - const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); + const l2Tokens = this.getRemoteTokensForL1Token(l1Token, chainId); l2Tokens.forEach((l2Token) => { // The effective balance is the current balance + inbound bridge transfers. const effectiveBalance = this.getBalanceOnChain(chainId, l1Token, l2Token); @@ -191,7 +191,7 @@ export class InventoryClient { // Find how short a given chain is for a desired L1Token. getTokenShortFall(l1Token: string, chainId: number): BigNumber { - return this.getDestinationTokensForL1Token(l1Token, chainId) + return this.getRemoteTokensForL1Token(l1Token, chainId) .map((token) => this.tokenClient.getShortfallTotalRequirement(chainId, token)) .reduce((acc, curr) => acc.add(curr), bnZero); } @@ -211,7 +211,15 @@ export class InventoryClient { } } - getDestinationTokensForL1Token(l1Token: string, chainId: number | string): string[] { + /** + * From an L1Token and remote chain ID, resolve all supported corresponding tokens. + * This should include at least the relevant repayment token on the relevant chain, but may also include other + * "equivalent" tokens (i.e. as with Bridged & Native USDC). + * @param l1Token Mainnet token to query. + * @param chainId Remove chain to query. + * @returns An array of supported tokens on chainId that map back to l1Token on mainnet. + */ + getRemoteTokensForL1Token(l1Token: string, chainId: number | string): string[] { if (chainId === this.hubPoolClient.chainId) { return [l1Token]; } @@ -709,7 +717,7 @@ export class InventoryClient { return; } - const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId); + const l2Tokens = this.getRemoteTokensForL1Token(l1Token, chainId); l2Tokens.forEach((l2Token) => { const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId, l2Token); const tokenConfig = this.getTokenConfig(l1Token, chainId, l2Token); From 4bdc0305fa92744d476b2c1e46dc268afea87b82 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 10:09:32 +0200 Subject: [PATCH 52/66] Make l2Token required --- src/clients/InventoryClient.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 0650fbdb7..40aa89cf7 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -173,9 +173,7 @@ export class InventoryClient { } // Get the balance of a given token on a given chain, including shortfalls and any pending cross chain transfers. - getCurrentAllocationPct(l1Token: string, chainId: number, l2Token?: string): BigNumber { - l2Token ??= this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, chainId); - + getCurrentAllocationPct(l1Token: string, chainId: number, l2Token: string): BigNumber { // If there is nothing over all chains, return early. const cumulativeBalance = this.getCumulativeBalance(l1Token); if (cumulativeBalance.eq(bnZero)) { From 4886cf2c2e8547e3c0f736fda261821f246a4849 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 10:14:50 +0200 Subject: [PATCH 53/66] Relocate instantiation --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 40aa89cf7..7f80980cd 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -144,12 +144,12 @@ export class InventoryClient { // If token doesn't have entry on chain, skip creating an entry for it since we'll likely run into an error // later trying to grab the chain equivalent of the L1 token via the HubPoolClient. if (chainId === this.hubPoolClient.chainId || this._l1TokenEnabledForChain(l1Token, chainId)) { - distribution[chainId] ??= {}; if (cumulativeBalance.eq(bnZero)) { return; } + distribution[chainId] ??= {}; const l2Tokens = this.getRemoteTokensForL1Token(l1Token, chainId); l2Tokens.forEach((l2Token) => { // The effective balance is the current balance + inbound bridge transfers. From c17b6147f5fc6521cc5c4b10460670d73c9bf49d Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 11:04:23 +0200 Subject: [PATCH 54/66] lint --- src/clients/InventoryClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 7f80980cd..0d89f3128 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -144,7 +144,6 @@ export class InventoryClient { // If token doesn't have entry on chain, skip creating an entry for it since we'll likely run into an error // later trying to grab the chain equivalent of the L1 token via the HubPoolClient. if (chainId === this.hubPoolClient.chainId || this._l1TokenEnabledForChain(l1Token, chainId)) { - if (cumulativeBalance.eq(bnZero)) { return; } From b9f0b4b2fff9b34de32eeb5db2f0312fd2fe25ef Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Wed, 8 May 2024 11:14:57 +0200 Subject: [PATCH 55/66] Identify todo Prompted by Matt. --- src/clients/InventoryClient.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 0d89f3128..f33968b4f 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -194,13 +194,7 @@ export class InventoryClient { } getRepaymentTokenForL1Token(l1Token: string, chainId: number | string): string | undefined { - // TODO: Need to replace calling into the HubPoolClient with calling into TOKEN_SYMBOLS_MAP. For example, - // imagine there is a utility function getL2TokenInfo(l1Token: string, chainId: number): L1Token that - // looks into TOKEN_SYMBOLS_MAP and returns an L1Token object using a token entry that contains the l1Token address. - // We'd need to be able to tie-break between tokens that map to the same L1Token (USDC.e, USDC), so maybe - // this function would either return multiple L1Token objects or we'd need to pass in a symbol/l2TokenAddress. - - // return getL2TokenInfo(l1Token, chainId).address + // @todo: Update HubPoolClient.getL2TokenForL1TokenAtBlock() such that it returns `undefined` instead of throwing. try { return this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId)); } catch { From 7fcbb421911b54f0c67243fb7297d1dd60200000 Mon Sep 17 00:00:00 2001 From: "James Morris, MS" <96435344+james-a-morris@users.noreply.github.com> Date: Wed, 8 May 2024 12:02:44 -0400 Subject: [PATCH 56/66] improve(accounting): account for l2 routes on non-op-stack bridges (#1475) * refactor(CrossChainTransferClient): Support unique L2 tokens in adapters & cctc --------- Signed-off-by: james-a-morris Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> --- src/clients/InventoryClient.ts | 18 ++--- src/clients/bridges/AdapterManager.ts | 24 +++--- src/clients/bridges/ArbitrumAdapter.ts | 7 +- src/clients/bridges/BaseAdapter.ts | 76 +++++++++++-------- src/clients/bridges/CCTPAdapter.ts | 9 --- .../bridges/CrossChainTransferClient.ts | 14 ++-- src/clients/bridges/LineaAdapter.ts | 11 ++- src/clients/bridges/PolygonAdapter.ts | 9 ++- src/clients/bridges/ZKSyncAdapter.ts | 11 ++- src/clients/bridges/index.ts | 2 + .../bridges/op-stack/DefaultErc20Bridge.ts | 48 ++++++------ .../bridges/op-stack/OpStackAdapter.ts | 17 ++--- .../op-stack/OpStackBridgeInterface.ts | 41 ++++++++-- .../bridges/op-stack/UsdcCCTPBridge.ts | 52 ++++++------- .../op-stack/UsdcTokenSplitterBridge.ts | 50 +++++++----- src/clients/bridges/op-stack/WethBridge.ts | 59 ++++++++------ .../op-stack/optimism/DaiOptimismBridge.ts | 56 +++++++------- .../op-stack/optimism/SnxOptimismBridge.ts | 54 +++++++------ src/utils/AddressUtils.ts | 30 +++++++- ...utstandingCrossChainTokenTransferAmount.ts | 6 +- test/Monitor.ts | 4 +- test/cross-chain-adapters/Linea.ts | 27 ++++--- test/cross-chain-adapters/OpStack.ts | 48 +++++++----- test/mocks/MockAdapterManager.ts | 29 ++++--- 24 files changed, 412 insertions(+), 290 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index 814653e88..da364ca1b 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -683,7 +683,7 @@ export class InventoryClient { // Next, evaluate if we have enough tokens on L1 to actually do these rebalances. for (const rebalance of rebalancesRequired) { - const { balance, amount, l1Token, chainId } = rebalance; + const { balance, amount, l1Token, l2Token, chainId } = rebalance; // This is the balance left after any assumed rebalances from earlier loop iterations. const unallocatedBalance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token); @@ -717,7 +717,6 @@ export class InventoryClient { }); possibleRebalances.push(rebalance); // Decrement token balance in client for this chain and increment cross chain counter. - const l2Token = this.getDestinationTokenForL1Token(l1Token, chainId); this.trackCrossChainTransfer(l1Token, l2Token, amount, chainId); } } else { @@ -736,8 +735,8 @@ export class InventoryClient { // sends each transaction one after the other with incrementing nonce. this will be left for a follow on PR as this // is already complex logic and most of the time we'll not be sending batches of rebalance transactions. for (const rebalance of possibleRebalances) { - const { chainId, l1Token, amount } = rebalance; - const { hash } = await this.sendTokenCrossChain(chainId, l1Token, amount, this.simMode); + const { chainId, l1Token, l2Token, amount } = rebalance; + const { hash } = await this.sendTokenCrossChain(chainId, l1Token, amount, this.simMode, l2Token); executedTransactions.push({ ...rebalance, hash }); } @@ -986,20 +985,21 @@ export class InventoryClient { }); } - async sendTokenCrossChain( + sendTokenCrossChain( chainId: number | string, l1Token: string, amount: BigNumber, - simMode = false + simMode = false, + l2Token?: string ): Promise { - return await this.adapterManager.sendTokenCrossChain(this.relayer, Number(chainId), l1Token, amount, simMode); + return this.adapterManager.sendTokenCrossChain(this.relayer, Number(chainId), l1Token, amount, simMode, l2Token); } - async _unwrapWeth(chainId: number, _l2Weth: string, amount: BigNumber): Promise { + _unwrapWeth(chainId: number, _l2Weth: string, amount: BigNumber): Promise { const l2Signer = this.tokenClient.spokePoolClients[chainId].spokePool.signer; const l2Weth = new Contract(_l2Weth, CONTRACT_ADDRESSES[1].weth.abi, l2Signer); this.log("Unwrapping WETH", { amount: amount.toString() }); - return await runTransaction(this.logger, l2Weth, "withdraw", [amount]); + return runTransaction(this.logger, l2Weth, "withdraw", [amount]); } async setL1TokenApprovals(): Promise { diff --git a/src/clients/bridges/AdapterManager.ts b/src/clients/bridges/AdapterManager.ts index c1d3029f8..cfd74a4bf 100644 --- a/src/clients/bridges/AdapterManager.ts +++ b/src/clients/bridges/AdapterManager.ts @@ -1,12 +1,19 @@ import { BigNumber, isDefined, winston, Signer, getL2TokenAddresses, TransactionResponse, assert } from "../../utils"; import { SpokePoolClient, HubPoolClient } from "../"; -import { OptimismAdapter, ArbitrumAdapter, PolygonAdapter, BaseAdapter, ZKSyncAdapter } from "./"; +import { + OptimismAdapter, + ArbitrumAdapter, + PolygonAdapter, + BaseAdapter, + ZKSyncAdapter, + BaseChainAdapter, + LineaAdapter, +} from "./"; import { InventoryConfig, OutstandingTransfers } from "../../interfaces"; import { utils } from "@across-protocol/sdk-v2"; import { CHAIN_IDs } from "@across-protocol/constants-v2"; -import { BaseChainAdapter } from "./op-stack/base/BaseChainAdapter"; import { spokesThatHoldEthAndWeth } from "../../common/Constants"; -import { LineaAdapter } from "./LineaAdapter"; + export class AdapterManager { public adapters: { [chainId: number]: BaseAdapter } = {}; @@ -71,10 +78,7 @@ export class AdapterManager { return Object.keys(this.adapters).map((chainId) => Number(chainId)); } - async getOutstandingCrossChainTokenTransferAmount( - chainId: number, - l1Tokens: string[] - ): Promise { + getOutstandingCrossChainTokenTransferAmount(chainId: number, l1Tokens: string[]): Promise { const adapter = this.adapters[chainId]; // @dev The adapter should filter out tokens that are not supported by the adapter, but we do it here as well. const adapterSupportedL1Tokens = l1Tokens.filter((token) => @@ -86,10 +90,10 @@ export class AdapterManager { adapterSupportedL1Tokens, searchConfigs: adapter.getUpdatedSearchConfigs(), }); - return await this.adapters[chainId].getOutstandingCrossChainTransfers(adapterSupportedL1Tokens); + return this.adapters[chainId].getOutstandingCrossChainTransfers(adapterSupportedL1Tokens); } - async sendTokenCrossChain( + sendTokenCrossChain( address: string, chainId: number | string, l1Token: string, @@ -100,7 +104,7 @@ export class AdapterManager { chainId = Number(chainId); // Ensure chainId is a number before using. this.logger.debug({ at: "AdapterManager", message: "Sending token cross-chain", chainId, l1Token, amount }); l2Token ??= this.l2TokenForL1Token(l1Token, Number(chainId)); - return await this.adapters[chainId].sendTokenToTargetChain(address, l1Token, l2Token, amount, simMode); + return this.adapters[chainId].sendTokenToTargetChain(address, l1Token, l2Token, amount, simMode); } // Check how much ETH is on the target chain and if it is above the threshold the wrap it to WETH. Note that this only diff --git a/src/clients/bridges/ArbitrumAdapter.ts b/src/clients/bridges/ArbitrumAdapter.ts index 887a1c774..d5fdac167 100644 --- a/src/clients/bridges/ArbitrumAdapter.ts +++ b/src/clients/bridges/ArbitrumAdapter.ts @@ -161,12 +161,15 @@ export class ArbitrumAdapter extends CCTPAdapter { }; }); const eventsStorage = index % 2 === 0 ? this.l1DepositInitiatedEvents : this.l2DepositFinalizedEvents; - assign(eventsStorage, [monitoredAddress, l1Token], events); + const l2Token = this.resolveL2TokenAddress(l1Token, false); // This codepath will never have native USDC - therefore we should pass `false`. + assign(eventsStorage, [monitoredAddress, l1Token, l2Token], events); }); if (isDefined(resultingCCTPEvents[monitoredAddress])) { + const usdcL1Token = TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]; + const usdcL2Token = this.resolveL2TokenAddress(usdcL1Token, true); // Must specifically be native USDC assign( this.l1DepositInitiatedEvents, - [monitoredAddress, TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]], + [monitoredAddress, usdcL1Token, usdcL2Token], resultingCCTPEvents[monitoredAddress] ); } diff --git a/src/clients/bridges/BaseAdapter.ts b/src/clients/bridges/BaseAdapter.ts index 3f8d91281..05ce6ed53 100644 --- a/src/clients/bridges/BaseAdapter.ts +++ b/src/clients/bridges/BaseAdapter.ts @@ -28,6 +28,7 @@ import { BigNumberish, TOKEN_SYMBOLS_MAP, getRedisCache, + getTokenAddressWithCCTP, } from "../../utils"; import { utils } from "@across-protocol/sdk-v2"; @@ -40,7 +41,7 @@ export interface DepositEvent extends SortableEvent { interface Events { [address: string]: { - [l1Token: string]: DepositEvent[]; + [l1Token: string]: { [l2Token: string]: DepositEvent[] }; }; } @@ -117,6 +118,10 @@ export abstract class BaseAdapter { ).getAddress()}_targetContract:${targetContract}`; } + resolveL2TokenAddress(l1Token: string, isNativeUsdc = false): string { + return getTokenAddressWithCCTP(l1Token, this.hubChainId, this.chainId, isNativeUsdc); + } + async checkAndSendTokenApprovals(address: string, l1Tokens: string[], associatedL1Bridges: string[]): Promise { this.log("Checking and sending token approvals", { l1Tokens, associatedL1Bridges }); @@ -202,12 +207,9 @@ export abstract class BaseAdapter { continue; } - if (outstandingTransfers[monitoredAddress] === undefined) { - outstandingTransfers[monitoredAddress] = {}; - } - if (this.l2DepositFinalizedEvents[monitoredAddress] === undefined) { - this.l2DepositFinalizedEvents[monitoredAddress] = {}; - } + outstandingTransfers[monitoredAddress] ??= {}; + + this.l2DepositFinalizedEvents[monitoredAddress] ??= {}; for (const l1Token of l1Tokens) { // Skip if there has been no deposits for this token. @@ -216,35 +218,43 @@ export abstract class BaseAdapter { } // It's okay to not have any finalization events. In that case, all deposits are outstanding. - if (this.l2DepositFinalizedEvents[monitoredAddress][l1Token] === undefined) { - this.l2DepositFinalizedEvents[monitoredAddress][l1Token] = []; - } - const l2FinalizationSet = this.l2DepositFinalizedEvents[monitoredAddress][l1Token]; - - // Match deposits and finalizations by amount. We're only doing a limited lookback of events so collisions - // should be unlikely. - const finalizedAmounts = l2FinalizationSet.map((finalization) => finalization.amount.toString()); - const pendingDeposits = this.l1DepositInitiatedEvents[monitoredAddress][l1Token].filter((deposit) => { - // Remove the first match. This handles scenarios where are collisions by amount. - const index = finalizedAmounts.indexOf(deposit.amount.toString()); - if (index > -1) { - finalizedAmounts.splice(index, 1); - return false; + this.l2DepositFinalizedEvents[monitoredAddress][l1Token] ??= {}; + + // We want to iterate over the deposit events that have been initiated. We'll then match them with the + // finalization events to determine which deposits are still outstanding. + for (const l2Token of Object.keys(this.l1DepositInitiatedEvents[monitoredAddress][l1Token])) { + this.l2DepositFinalizedEvents[monitoredAddress][l1Token][l2Token] ??= []; + const l2FinalizationSet = this.l2DepositFinalizedEvents[monitoredAddress][l1Token][l2Token]; + + // Match deposits and finalizations by amount. We're only doing a limited lookback of events so collisions + // should be unlikely. + const finalizedAmounts = l2FinalizationSet.map((finalization) => finalization.amount.toString()); + const pendingDeposits = this.l1DepositInitiatedEvents[monitoredAddress][l1Token][l2Token].filter( + (deposit) => { + // Remove the first match. This handles scenarios where are collisions by amount. + const index = finalizedAmounts.indexOf(deposit.amount.toString()); + if (index > -1) { + finalizedAmounts.splice(index, 1); + return false; + } + return true; + } + ); + + // Short circuit early if there are no pending deposits. + if (pendingDeposits.length === 0) { + continue; } - return true; - }); - // Short circuit early if there are no pending deposits. - if (pendingDeposits.length === 0) { - continue; - } + outstandingTransfers[monitoredAddress][l1Token] ??= {}; - const totalAmount = pendingDeposits.reduce((acc, curr) => acc.add(curr.amount), bnZero); - const depositTxHashes = pendingDeposits.map((deposit) => deposit.transactionHash); - outstandingTransfers[monitoredAddress][l1Token] = { - totalAmount, - depositTxHashes, - }; + const totalAmount = pendingDeposits.reduce((acc, curr) => acc.add(curr.amount), bnZero); + const depositTxHashes = pendingDeposits.map((deposit) => deposit.transactionHash); + outstandingTransfers[monitoredAddress][l1Token][l2Token] = { + totalAmount, + depositTxHashes, + }; + } } } diff --git a/src/clients/bridges/CCTPAdapter.ts b/src/clients/bridges/CCTPAdapter.ts index fd98a87bf..556b56f6b 100644 --- a/src/clients/bridges/CCTPAdapter.ts +++ b/src/clients/bridges/CCTPAdapter.ts @@ -23,15 +23,6 @@ import { BaseAdapter } from "./BaseAdapter"; * to be used to bridge USDC via CCTP. */ export abstract class CCTPAdapter extends BaseAdapter { - /** - * Get the CCTP domain of the hub chain. This is used to determine the source - * domain of a CCTP message. - * @returns The CCTP domain of the hub chain - */ - private get l1SourceDomain(): number { - return chainIdsToCctpDomains[this.hubChainId]; - } - /** * Get the CCTP domain of the target chain. This is used to determine the destination * domain of a CCTP message. diff --git a/src/clients/bridges/CrossChainTransferClient.ts b/src/clients/bridges/CrossChainTransferClient.ts index 4f681c989..16a844a0d 100644 --- a/src/clients/bridges/CrossChainTransferClient.ts +++ b/src/clients/bridges/CrossChainTransferClient.ts @@ -1,4 +1,4 @@ -import { BigNumber, bnZero, winston, assign, DefaultLogLevels, AnyObject } from "../../utils"; +import { BigNumber, bnZero, winston, DefaultLogLevels, AnyObject } from "../../utils"; import { AdapterManager } from "./AdapterManager"; import { OutstandingTransfers } from "../../interfaces"; @@ -89,14 +89,12 @@ export class CrossChainTransferClient { this.log("Updating cross chain transfers", { monitoredChains }); const outstandingTransfersPerChain = await Promise.all( - monitoredChains.map((chainId) => - this.adapterManager.getOutstandingCrossChainTokenTransferAmount(chainId, l1Tokens) - ) + monitoredChains.map(async (chainId) => [ + chainId, + await this.adapterManager.getOutstandingCrossChainTokenTransferAmount(chainId, l1Tokens), + ]) ); - outstandingTransfersPerChain.forEach((outstandingTransfers, index) => { - assign(this.outstandingCrossChainTransfers, [monitoredChains[index]], outstandingTransfers); - }); - + this.outstandingCrossChainTransfers = Object.fromEntries(outstandingTransfersPerChain); this.log("Updated cross chain transfers", { outstandingCrossChainTransfers: this.outstandingCrossChainTransfers }); } diff --git a/src/clients/bridges/LineaAdapter.ts b/src/clients/bridges/LineaAdapter.ts index d5d1061d0..df435ff17 100644 --- a/src/clients/bridges/LineaAdapter.ts +++ b/src/clients/bridges/LineaAdapter.ts @@ -266,15 +266,18 @@ export class LineaAdapter extends BaseAdapter { l1Token: string, transferEvents: Event[] ): void { + const l2Token = this.resolveL2TokenAddress(l1Token, false); // There's no native USDC on Linea + assert(!isDefined(TOKEN_SYMBOLS_MAP._USDC.addresses[this.chainId])); // We can blow up if this eventually stops being true transferEvents.forEach((event) => { const txHash = event.transactionHash; // @dev WETH events have a _value field, while ERC20 events have an amount field. const amount = event.args._value ?? event.args.amount; outstandingTransfers[monitoredAddress] ??= {}; - outstandingTransfers[monitoredAddress][l1Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; - outstandingTransfers[monitoredAddress][l1Token] = { - totalAmount: outstandingTransfers[monitoredAddress][l1Token].totalAmount.add(amount), - depositTxHashes: [...outstandingTransfers[monitoredAddress][l1Token].depositTxHashes, txHash], + outstandingTransfers[monitoredAddress][l1Token] ??= {}; + outstandingTransfers[monitoredAddress][l1Token][l2Token] ??= { totalAmount: bnZero, depositTxHashes: [] }; + outstandingTransfers[monitoredAddress][l1Token][l2Token] = { + totalAmount: outstandingTransfers[monitoredAddress][l1Token][l2Token].totalAmount.add(amount), + depositTxHashes: [...outstandingTransfers[monitoredAddress][l1Token][l2Token].depositTxHashes, txHash], }; }); } diff --git a/src/clients/bridges/PolygonAdapter.ts b/src/clients/bridges/PolygonAdapter.ts index ed3285509..4712ba8ac 100644 --- a/src/clients/bridges/PolygonAdapter.ts +++ b/src/clients/bridges/PolygonAdapter.ts @@ -232,12 +232,17 @@ export class PolygonAdapter extends CCTPAdapter { }; }); const eventsStorage = index % 2 === 0 ? this.l1DepositInitiatedEvents : this.l2DepositFinalizedEvents; - assign(eventsStorage, [monitoredAddress, l1Token], events); + const l2Token = this.resolveL2TokenAddress(l1Token, false); // these are all either normal L2 tokens or bridged USDC + assign(eventsStorage, [monitoredAddress, l1Token, l2Token], events); }); if (isDefined(resultingCCTPEvents[monitoredAddress])) { assign( this.l1DepositInitiatedEvents, - [monitoredAddress, TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]], + [ + monitoredAddress, + TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId], + TOKEN_SYMBOLS_MAP._USDC.addresses[this.chainId], // Must map to the USDC Native L2 token address + ], resultingCCTPEvents[monitoredAddress] ); } diff --git a/src/clients/bridges/ZKSyncAdapter.ts b/src/clients/bridges/ZKSyncAdapter.ts index 0e845c048..705f7601e 100644 --- a/src/clients/bridges/ZKSyncAdapter.ts +++ b/src/clients/bridges/ZKSyncAdapter.ts @@ -9,7 +9,6 @@ import { assign, Event, ZERO_ADDRESS, - getTokenAddress, TOKEN_SYMBOLS_MAP, bnZero, } from "../../utils"; @@ -78,6 +77,7 @@ export class ZKSyncAdapter extends BaseAdapter { // Resolve whether the token is WETH or not. const isWeth = this.isWeth(l1TokenAddress); + const l2Token = this.resolveL2TokenAddress(l1TokenAddress, false); // CCTP doesn't exist on ZkSync. if (isWeth) { [initiatedQueryResult, finalizedQueryResult, wrapQueryResult] = await Promise.all([ // If sending WETH from EOA, we can assume the EOA is unwrapping ETH and sending it through the @@ -121,7 +121,6 @@ export class ZKSyncAdapter extends BaseAdapter { finalizedQueryResult = matchL2EthDepositAndWrapEvents(finalizedQueryResult, wrapQueryResult); } } else { - const l2Token = getTokenAddress(l1TokenAddress, this.hubChainId, this.chainId); [initiatedQueryResult, finalizedQueryResult] = await Promise.all([ // Filter on 'from' and 'to' address paginatedEventQuery( @@ -141,13 +140,17 @@ export class ZKSyncAdapter extends BaseAdapter { assign( this.l1DepositInitiatedEvents, - [address, l1TokenAddress], + [address, l1TokenAddress, l2Token], // An initiatedQueryResult could be a zkSync DepositInitiated or an AtomicDepositor // ZkSyncEthDepositInitiated event, subject to whether the deposit token was WETH or not. // A ZkSyncEthDepositInitiated event doesn't have a token or l1Token param. initiatedQueryResult.map(processEvent).filter((e) => isWeth || e.l1Token === l1TokenAddress) ); - assign(this.l2DepositFinalizedEvents, [address, l1TokenAddress], finalizedQueryResult.map(processEvent)); + assign( + this.l2DepositFinalizedEvents, + [address, l1TokenAddress, l2Token], + finalizedQueryResult.map(processEvent) + ); }); }); diff --git a/src/clients/bridges/index.ts b/src/clients/bridges/index.ts index d2bb55e17..c8ce57d9a 100644 --- a/src/clients/bridges/index.ts +++ b/src/clients/bridges/index.ts @@ -1,7 +1,9 @@ export * from "./BaseAdapter"; export * from "./AdapterManager"; export * from "./op-stack/optimism/OptimismAdapter"; +export * from "./op-stack/base/BaseChainAdapter"; export * from "./ArbitrumAdapter"; export * from "./PolygonAdapter"; export * from "./CrossChainTransferClient"; export * from "./ZKSyncAdapter"; +export * from "./LineaAdapter"; diff --git a/src/clients/bridges/op-stack/DefaultErc20Bridge.ts b/src/clients/bridges/op-stack/DefaultErc20Bridge.ts index fed56c2c5..a80bb85a3 100644 --- a/src/clients/bridges/op-stack/DefaultErc20Bridge.ts +++ b/src/clients/bridges/op-stack/DefaultErc20Bridge.ts @@ -1,12 +1,16 @@ -import { Contract, BigNumber, paginatedEventQuery, Event, Signer, EventSearchConfig, Provider } from "../../../utils"; +import { Contract, BigNumber, paginatedEventQuery, Signer, EventSearchConfig, Provider } from "../../../utils"; import { CONTRACT_ADDRESSES } from "../../../common"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; -export class DefaultERC20Bridge implements OpStackBridge { +export class DefaultERC20Bridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; - constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`].address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`]; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -14,10 +18,6 @@ export class DefaultERC20Bridge implements OpStackBridge { this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); } - get l1Gateways(): string[] { - return [this.l1Bridge.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -32,27 +32,31 @@ export class DefaultERC20Bridge implements OpStackBridge { }; } - queryL1BridgeInitiationEvents( + async queryL1BridgeInitiationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l1Bridge, - this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), + eventConfig + ), + }; } - queryL2BridgeFinalizationEvents( + async queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l2Bridge, - this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), + eventConfig + ), + }; } } diff --git a/src/clients/bridges/op-stack/OpStackAdapter.ts b/src/clients/bridges/op-stack/OpStackAdapter.ts index 9530d3e4d..da61d233d 100644 --- a/src/clients/bridges/op-stack/OpStackAdapter.ts +++ b/src/clients/bridges/op-stack/OpStackAdapter.ts @@ -99,16 +99,13 @@ export class OpStackAdapter extends BaseAdapter { bridge.queryL2BridgeFinalizationEvents(l1Token, monitoredAddress, l2SearchConfig), ]); - assign( - this.l1DepositInitiatedEvents, - [monitoredAddress, l1Token], - depositInitiatedResults.map(processEvent) - ); - assign( - this.l2DepositFinalizedEvents, - [monitoredAddress, l1Token], - depositFinalizedResults.map(processEvent) - ); + Object.entries(depositInitiatedResults).forEach(([l2Token, events]) => { + assign(this.l1DepositInitiatedEvents, [monitoredAddress, l1Token, l2Token], events.map(processEvent)); + }); + + Object.entries(depositFinalizedResults).forEach(([l2Token, events]) => { + assign(this.l2DepositFinalizedEvents, [monitoredAddress, l1Token, l2Token], events.map(processEvent)); + }); }) ) ) diff --git a/src/clients/bridges/op-stack/OpStackBridgeInterface.ts b/src/clients/bridges/op-stack/OpStackBridgeInterface.ts index 508d97c10..407fe1e27 100644 --- a/src/clients/bridges/op-stack/OpStackBridgeInterface.ts +++ b/src/clients/bridges/op-stack/OpStackBridgeInterface.ts @@ -1,4 +1,12 @@ -import { Contract, BigNumber, Event, EventSearchConfig } from "../../../utils"; +import { + Contract, + BigNumber, + Event, + EventSearchConfig, + Signer, + Provider, + getTokenAddressWithCCTP, +} from "../../../utils"; export interface BridgeTransactionDetails { readonly contract: Contract; @@ -6,19 +14,38 @@ export interface BridgeTransactionDetails { readonly args: unknown[]; } -export interface OpStackBridge { - readonly l1Gateways: string[]; - constructL1ToL2Txn( +export type OpStackEvents = { [l2Token: string]: Event[] }; + +export abstract class OpStackBridge { + constructor( + protected l2chainId: number, + protected hubChainId: number, + protected l1Signer: Signer, + protected l2SignerOrProvider: Signer | Provider, + readonly l1Gateways: string[] + ) {} + + abstract constructL1ToL2Txn( toAddress: string, l1Token: string, l2Token: string, amount: BigNumber, l2Gas: number ): BridgeTransactionDetails; - queryL1BridgeInitiationEvents(l1Token: string, fromAddress: string, eventConfig: EventSearchConfig): Promise; - queryL2BridgeFinalizationEvents( + + abstract queryL1BridgeInitiationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise; + + abstract queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise; + ): Promise; + + protected resolveL2TokenAddress(l1Token: string): string { + return getTokenAddressWithCCTP(l1Token, this.hubChainId, this.l2chainId, false); + } } diff --git a/src/clients/bridges/op-stack/UsdcCCTPBridge.ts b/src/clients/bridges/op-stack/UsdcCCTPBridge.ts index a5ce7cd37..6aeacec14 100644 --- a/src/clients/bridges/op-stack/UsdcCCTPBridge.ts +++ b/src/clients/bridges/op-stack/UsdcCCTPBridge.ts @@ -1,19 +1,18 @@ -import { BigNumber, Contract, Event, Signer } from "ethers"; +import { BigNumber, Contract, Signer } from "ethers"; import { CONTRACT_ADDRESSES, chainIdsToCctpDomains } from "../../../common"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; import { EventSearchConfig, Provider, TOKEN_SYMBOLS_MAP } from "../../../utils"; import { cctpAddressToBytes32, retrieveOutstandingCCTPBridgeUSDCTransfers } from "../../../utils/CCTPUtils"; -export class UsdcCCTPBridge implements OpStackBridge { +export class UsdcCCTPBridge extends OpStackBridge { private readonly l1CctpTokenBridge: Contract; private readonly l2CctpMessageTransmitter: Contract; - constructor( - private l2chainId: number, - private hubChainId: number, - l1Signer: Signer, - l2SignerOrProvider: Signer | Provider - ) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].cctpTokenMessenger.address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].cctpTokenMessenger; this.l1CctpTokenBridge = new Contract(l1Address, l1Abi, l1Signer); @@ -29,14 +28,11 @@ export class UsdcCCTPBridge implements OpStackBridge { return TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId]; } - private get l2UsdcTokenAddress(): string { + protected resolveL2TokenAddress(l1Token: string): string { + l1Token; return TOKEN_SYMBOLS_MAP._USDC.addresses[this.l2chainId]; } - get l1Gateways(): string[] { - return [this.l1CctpTokenBridge.address]; - } - constructL1ToL2Txn( toAddress: string, _l1Token: string, @@ -53,25 +49,27 @@ export class UsdcCCTPBridge implements OpStackBridge { } async queryL1BridgeInitiationEvents( - _l1Token: string, + l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return retrieveOutstandingCCTPBridgeUSDCTransfers( - this.l1CctpTokenBridge, - this.l2CctpMessageTransmitter, - eventConfig, - this.l1UsdcTokenAddress, - this.hubChainId, - this.l2chainId, - fromAddress - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await retrieveOutstandingCCTPBridgeUSDCTransfers( + this.l1CctpTokenBridge, + this.l2CctpMessageTransmitter, + eventConfig, + this.l1UsdcTokenAddress, + this.hubChainId, + this.l2chainId, + fromAddress + ), + }; } queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { + ): Promise { // Lint Appeasement l1Token; fromAddress; @@ -80,6 +78,6 @@ export class UsdcCCTPBridge implements OpStackBridge { // Per the documentation of the BaseAdapter's computeOutstandingCrossChainTransfers method, we can return an empty array here // and only return the relevant outstanding events from queryL1BridgeInitiationEvents. // Relevant link: https://github.com/across-protocol/relayer-v2/blob/master/src/clients/bridges/BaseAdapter.ts#L189 - return Promise.resolve([]); + return Promise.resolve({}); } } diff --git a/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts b/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts index ed4cbdcf7..79395853f 100644 --- a/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts +++ b/src/clients/bridges/op-stack/UsdcTokenSplitterBridge.ts @@ -1,19 +1,19 @@ -import { BigNumber, Event, Signer } from "ethers"; +import { BigNumber, Signer } from "ethers"; import { DefaultERC20Bridge } from "./DefaultErc20Bridge"; import { UsdcCCTPBridge } from "./UsdcCCTPBridge"; import { EventSearchConfig, Provider, TOKEN_SYMBOLS_MAP, assert, compareAddressesSimple } from "../../../utils"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; +import { CONTRACT_ADDRESSES } from "../../../common"; -export class UsdcTokenSplitterBridge implements OpStackBridge { +export class UsdcTokenSplitterBridge extends OpStackBridge { private readonly cctpBridge: UsdcCCTPBridge; private readonly canonicalBridge: DefaultERC20Bridge; - constructor( - private l2chainId: number, - private hubChainId: number, - l1Signer: Signer, - l2SignerOrProvider: Signer | Provider - ) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].cctpTokenMessenger.address, + CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`].address, + ]); this.cctpBridge = new UsdcCCTPBridge(l2chainId, hubChainId, l1Signer, l2SignerOrProvider); this.canonicalBridge = new DefaultERC20Bridge(l2chainId, hubChainId, l1Signer, l2SignerOrProvider); } @@ -45,31 +45,47 @@ export class UsdcTokenSplitterBridge implements OpStackBridge { l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { + ): Promise { // We should *only* be calling this class for USDC tokens assert(compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId])); const events = await Promise.all([ this.cctpBridge.queryL1BridgeInitiationEvents(l1Token, fromAddress, eventConfig), this.canonicalBridge.queryL1BridgeInitiationEvents(l1Token, fromAddress, eventConfig), ]); - return events.flat(); + // Reduce the events to a single Object. If there are any duplicate keys, merge the events. + return events.reduce((acc, event) => { + Object.entries(event).forEach(([l2Token, events]) => { + if (l2Token in acc) { + acc[l2Token] = acc[l2Token].concat(events); + } else { + acc[l2Token] = events; + } + }); + return acc; + }, {}); } async queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { + ): Promise { // We should *only* be calling this class for USDC tokens assert(compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP._USDC.addresses[this.hubChainId])); const events = await Promise.all([ this.cctpBridge.queryL2BridgeFinalizationEvents(l1Token, fromAddress, eventConfig), this.canonicalBridge.queryL2BridgeFinalizationEvents(l1Token, fromAddress, eventConfig), ]); - return events.flat(); - } - - get l1Gateways(): string[] { - return [...this.cctpBridge.l1Gateways, ...this.canonicalBridge.l1Gateways]; + // Reduce the events to a single object. If there are any duplicate keys, merge the events. + return events.reduce((acc, event) => { + Object.entries(event).forEach(([l2Token, events]) => { + if (l2Token in acc) { + acc[l2Token] = acc[l2Token].concat(events); + } else { + acc[l2Token] = events; + } + }); + return acc; + }, {}); } } diff --git a/src/clients/bridges/op-stack/WethBridge.ts b/src/clients/bridges/op-stack/WethBridge.ts index 6d7970d0f..aea3c8e25 100644 --- a/src/clients/bridges/op-stack/WethBridge.ts +++ b/src/clients/bridges/op-stack/WethBridge.ts @@ -1,31 +1,36 @@ import { Contract, BigNumber, - Event, EventSearchConfig, paginatedEventQuery, Signer, Provider, ZERO_ADDRESS, + Event, + TOKEN_SYMBOLS_MAP, } from "../../../utils"; import { CONTRACT_ADDRESSES } from "../../../common"; -import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; import { matchL2EthDepositAndWrapEvents } from "../utils"; import { utils } from "@across-protocol/sdk-v2"; +import { BridgeTransactionDetails, OpStackBridge, OpStackEvents } from "./OpStackBridgeInterface"; -export class WethBridge implements OpStackBridge { +export class WethBridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; private readonly atomicDepositor: Contract; private readonly l2Weth: Contract; private readonly hubPoolAddress: string; - constructor( - private l2chainId: number, - readonly hubChainId: number, - l1Signer: Signer, - l2SignerOrProvider: Signer | Provider - ) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super( + l2chainId, + hubChainId, + l1Signer, + l2SignerOrProvider, + // To keep existing logic, we should use ataomic depositor as the l1 bridge + [CONTRACT_ADDRESSES[hubChainId].atomicDepositor.address] + ); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`]; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -41,10 +46,6 @@ export class WethBridge implements OpStackBridge { this.hubPoolAddress = CONTRACT_ADDRESSES[this.hubChainId]?.hubPool?.address; } - get l1Gateways(): string[] { - return [this.atomicDepositor.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -59,12 +60,18 @@ export class WethBridge implements OpStackBridge { }; } + private convertEventListToOpStackEvents(events: Event[]): OpStackEvents { + return { + [this.resolveL2TokenAddress(TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubChainId])]: events, + }; + } + async queryL1BridgeInitiationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig, l1Bridge = this.l1Bridge - ): Promise { + ): Promise { // We need to be smart about the filtering here because the ETHDepositInitiated event does not // index on the `toAddress` which is the `fromAddress` that we pass in here and the address we want // to actually filter on. So we make some simplifying assumptions: @@ -76,7 +83,7 @@ export class WethBridge implements OpStackBridge { // Since we can only index on the `fromAddress` for the ETHDepositInitiated event, we can't support // monitoring the spoke pool address if (isL2ChainContract || (isContract && fromAddress !== this.hubPoolAddress)) { - return []; + return this.convertEventListToOpStackEvents([]); } const events = await paginatedEventQuery( @@ -87,9 +94,9 @@ export class WethBridge implements OpStackBridge { // If EOA sent the ETH via the AtomicDepositor, then remove any events where the // toAddress is not the EOA so we don't get confused with other users using the AtomicDepositor if (!isContract) { - return events.filter((event) => event.args._to === fromAddress); + return this.convertEventListToOpStackEvents(events.filter((event) => event.args._to === fromAddress)); } - return events; + return this.convertEventListToOpStackEvents(events); } async queryL2BridgeFinalizationEvents( @@ -98,14 +105,14 @@ export class WethBridge implements OpStackBridge { eventConfig: EventSearchConfig, l2Bridge = this.l2Bridge, l2Weth = this.l2Weth - ): Promise { + ): Promise { // Check if the sender is a contract on the L1 network. const isContract = await this.isHubChainContract(fromAddress); // See above for why we don't want to monitor the spoke pool contract. const isL2ChainContract = await this.isL2ChainContract(fromAddress); if (isL2ChainContract || (isContract && fromAddress !== this.hubPoolAddress)) { - return []; + return this.convertEventListToOpStackEvents([]); } if (!isContract) { @@ -132,18 +139,20 @@ export class WethBridge implements OpStackBridge { // on L1 and received as ETH on L2 by the recipient, which is finally wrapped into WETH on the L2 by the // recipient--the L2 signer in this class. const l2EthWrapEvents = await this.queryL2WrapEthEvents(fromAddress, eventConfig, l2Weth); - return matchL2EthDepositAndWrapEvents(l2EthDepositEvents, l2EthWrapEvents); + return this.convertEventListToOpStackEvents(matchL2EthDepositAndWrapEvents(l2EthDepositEvents, l2EthWrapEvents)); } else { // Since we can only index on the `fromAddress` for the DepositFinalized event, we can't support // monitoring the spoke pool address if (fromAddress !== this.hubPoolAddress) { - return []; + return this.convertEventListToOpStackEvents([]); } - return await paginatedEventQuery( - l2Bridge, - l2Bridge.filters.DepositFinalized(ZERO_ADDRESS, undefined, fromAddress), - eventConfig + return this.convertEventListToOpStackEvents( + await paginatedEventQuery( + l2Bridge, + l2Bridge.filters.DepositFinalized(ZERO_ADDRESS, undefined, fromAddress), + eventConfig + ) ); } } diff --git a/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts b/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts index eedc9950b..6d84a374d 100644 --- a/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts +++ b/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts @@ -1,20 +1,16 @@ -import { - Contract, - BigNumber, - paginatedEventQuery, - Event, - EventSearchConfig, - Signer, - Provider, -} from "../../../../utils"; +import { Contract, BigNumber, paginatedEventQuery, EventSearchConfig, Signer, Provider } from "../../../../utils"; import { CONTRACT_ADDRESSES } from "../../../../common"; -import { OpStackBridge, BridgeTransactionDetails } from "../OpStackBridgeInterface"; +import { OpStackBridge, BridgeTransactionDetails, OpStackEvents } from "../OpStackBridgeInterface"; -export class DaiOptimismBridge implements OpStackBridge { +export class DaiOptimismBridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; - constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].daiOptimismBridge.address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].daiOptimismBridge; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -22,10 +18,6 @@ export class DaiOptimismBridge implements OpStackBridge { this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); } - get l1Gateways(): string[] { - return [this.l1Bridge.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -40,27 +32,31 @@ export class DaiOptimismBridge implements OpStackBridge { }; } - queryL1BridgeInitiationEvents( + async queryL1BridgeInitiationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l1Bridge, - this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), + eventConfig + ), + }; } - queryL2BridgeFinalizationEvents( + async queryL2BridgeFinalizationEvents( l1Token: string, fromAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery( - this.l2Bridge, - this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), - eventConfig - ); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), + eventConfig + ), + }; } } diff --git a/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts b/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts index e903cc44b..fdfc542c8 100644 --- a/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts +++ b/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts @@ -1,20 +1,16 @@ -import { - Contract, - BigNumber, - paginatedEventQuery, - Event, - EventSearchConfig, - Signer, - Provider, -} from "../../../../utils"; +import { Contract, BigNumber, paginatedEventQuery, EventSearchConfig, Signer, Provider } from "../../../../utils"; import { CONTRACT_ADDRESSES } from "../../../../common"; -import { OpStackBridge, BridgeTransactionDetails } from "../OpStackBridgeInterface"; +import { OpStackBridge, BridgeTransactionDetails, OpStackEvents } from "../OpStackBridgeInterface"; -export class SnxOptimismBridge implements OpStackBridge { +export class SnxOptimismBridge extends OpStackBridge { private readonly l1Bridge: Contract; private readonly l2Bridge: Contract; - constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [ + CONTRACT_ADDRESSES[hubChainId].snxOptimismBridge.address, + ]); + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].snxOptimismBridge; this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); @@ -22,10 +18,6 @@ export class SnxOptimismBridge implements OpStackBridge { this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); } - get l1Gateways(): string[] { - return [this.l1Bridge.address]; - } - constructL1ToL2Txn( toAddress: string, l1Token: string, @@ -41,21 +33,33 @@ export class SnxOptimismBridge implements OpStackBridge { }; } - queryL1BridgeInitiationEvents(l1Token: string, toAddress: string, eventConfig: EventSearchConfig): Promise { + async queryL1BridgeInitiationEvents( + l1Token: string, + toAddress: string, + eventConfig: EventSearchConfig + ): Promise { // @dev For the SnxBridge, only the `toAddress` is indexed on the L2 event so we treat the `fromAddress` as the // toAddress when fetching the L1 event. - return paginatedEventQuery( - this.l1Bridge, - this.l1Bridge.filters.DepositInitiated(undefined, toAddress), - eventConfig - ); + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.DepositInitiated(undefined, toAddress), + eventConfig + ), + }; } - queryL2BridgeFinalizationEvents( + async queryL2BridgeFinalizationEvents( l1Token: string, toAddress: string, eventConfig: EventSearchConfig - ): Promise { - return paginatedEventQuery(this.l2Bridge, this.l2Bridge.filters.DepositFinalized(toAddress), eventConfig); + ): Promise { + return { + [this.resolveL2TokenAddress(l1Token)]: await paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(toAddress), + eventConfig + ), + }; } } diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 589e9e2c8..7d0b429c0 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -1,5 +1,5 @@ -import { TOKEN_SYMBOLS_MAP } from "@across-protocol/constants-v2"; -import { BigNumber, ethers } from "."; +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants-v2"; +import { BigNumber, ethers, isDefined } from "."; export function compareAddresses(addressA: string, addressB: string): 1 | -1 | 0 { // Convert address strings to BigNumbers and then sort numerical value of the BigNumber, which sorts the addresses @@ -15,13 +15,20 @@ export function compareAddresses(addressA: string, addressB: string): 1 | -1 | 0 } } -export function compareAddressesSimple(addressA: string, addressB: string): boolean { +export function compareAddressesSimple(addressA?: string, addressB?: string): boolean { if (addressA === undefined || addressB === undefined) { return false; } return addressA.toLowerCase() === addressB.toLowerCase(); } +export function includesAddressSimple(address: string | undefined, list: string[]): boolean { + if (!isDefined(address)) { + return false; + } + return list.filter((listAddress) => compareAddressesSimple(address, listAddress)).length > 0; +} + /** * Match the token symbol for the given token address and chain ID. * @param tokenAddress The token address to resolve the symbol for. @@ -76,6 +83,23 @@ export function getTokenAddress(tokenAddress: string, chainId: number, targetCha return targetAddress; } +export function getTokenAddressWithCCTP( + l1Token: string, + hubChainId: number, + l2ChainId: number, + isNativeUsdc = false +): string { + // Base Case + if (hubChainId === l2ChainId) { + return l1Token; + } + if (compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP._USDC.addresses[hubChainId])) { + const onBase = l2ChainId === CHAIN_IDs.BASE || l2ChainId === CHAIN_IDs.BASE_SEPOLIA; + return TOKEN_SYMBOLS_MAP[isNativeUsdc ? "_USDC" : onBase ? "USDbC" : "USDC.e"].addresses[l2ChainId]; + } + return getTokenAddress(l1Token, hubChainId, l2ChainId); +} + export function checkAddressChecksum(tokenAddress: string): boolean { return ethers.utils.getAddress(tokenAddress) === tokenAddress; } diff --git a/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts b/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts index 29f7fe49f..17593930a 100644 --- a/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts +++ b/test/AdapterManager.getOutstandingCrossChainTokenTransferAmount.ts @@ -21,14 +21,14 @@ class TestAdapter extends BaseAdapter { const deposits = amounts.map((amount) => { return { amount: toBN(amount) }; }); - this.l1DepositInitiatedEvents = { "0xmonitored": { token: deposits as unknown as DepositEvent[] } }; + this.l1DepositInitiatedEvents = { "0xmonitored": { token: { token: deposits as unknown as DepositEvent[] } } }; } public setFinalizationEvents(amounts: number[]) { const deposits = amounts.map((amount) => { return { amount: toBN(amount) }; }); - this.l2DepositFinalizedEvents = { "0xmonitored": { token: deposits as unknown as DepositEvent[] } }; + this.l2DepositFinalizedEvents = { "0xmonitored": { token: { token: deposits as unknown as DepositEvent[] } } }; } getOutstandingCrossChainTransfers(): Promise { @@ -80,6 +80,6 @@ describe("AdapterManager: Get outstanding cross chain token transfer amounts", a }); const expectOutstandingTransfersAmount = (transfers: OutstandingTransfers, amount: number) => { - const actualAmount = transfers["0xmonitored"]?.["token"]?.totalAmount || toBN(0); + const actualAmount = transfers["0xmonitored"]?.["token"]?.["token"]?.totalAmount || toBN(0); expect(actualAmount).to.eq(toBN(amount)); }; diff --git a/test/Monitor.ts b/test/Monitor.ts index 65344af15..6dd54a5da 100644 --- a/test/Monitor.ts +++ b/test/Monitor.ts @@ -292,6 +292,7 @@ describe("Monitor", async function () { crossChainTransferClient.increaseOutstandingTransfer( depositor.address, l1Token.address, + l2Token.address, toBN(5), destinationChainId ); @@ -333,7 +334,8 @@ describe("Monitor", async function () { originChainId, spokePool_1.address, l1Token.address, - toBN(5) + toBN(5), + l2Token.address ); await updateAllClients(); await monitorInstance.update(); diff --git a/test/cross-chain-adapters/Linea.ts b/test/cross-chain-adapters/Linea.ts index 94f5d7ca5..971393641 100644 --- a/test/cross-chain-adapters/Linea.ts +++ b/test/cross-chain-adapters/Linea.ts @@ -9,7 +9,7 @@ import { CONTRACT_ADDRESSES } from "../../src/common"; describe("Cross Chain Adapter: Linea", async function () { let adapter: LineaAdapter; let monitoredEoa: string; - let l1Token: string; + let l1Token, l1USDCToken, l1WETHToken: string; let wethBridgeContract: Contract; let usdcBridgeContract: Contract; @@ -24,7 +24,9 @@ describe("Cross Chain Adapter: Linea", async function () { const [deployer] = await ethers.getSigners(); monitoredEoa = randomAddress(); - l1Token = randomAddress(); + l1Token = TOKEN_SYMBOLS_MAP.WBTC.addresses[CHAIN_IDs.MAINNET]; + l1USDCToken = TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]; + l1WETHToken = TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]; const spokePool = await (await getContractFactory("MockSpokePool", deployer)).deploy(ZERO_ADDRESS); @@ -85,13 +87,15 @@ describe("Cross Chain Adapter: Linea", async function () { let outstandingTransfers = {}; // 1. If l1 and l2 events pair off, outstanding transfers will be empty - adapter.matchWethDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1Token); + adapter.matchWethDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1WETHToken); expect(outstandingTransfers).to.deep.equal({}); // 2. If finalized event is missing, there will be an outstanding transfer. outstandingTransfers = {}; - adapter.matchWethDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1Token); - expect(outstandingTransfers[monitoredEoa][l1Token]).to.deep.equal({ + adapter.matchWethDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1WETHToken); + expect( + outstandingTransfers[monitoredEoa][l1WETHToken][TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.LINEA]] + ).to.deep.equal({ totalAmount: toBN(1), depositTxHashes: l1Events.map((e) => e.transactionHash), }); @@ -121,13 +125,15 @@ describe("Cross Chain Adapter: Linea", async function () { let outstandingTransfers = {}; // 1. If l1 and l2 events pair off, outstanding transfers will be empty - adapter.matchUsdcDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1Token); + adapter.matchUsdcDepositEvents(l1Events, l2Events, outstandingTransfers, monitoredEoa, l1USDCToken); expect(outstandingTransfers).to.deep.equal({}); // 2. If finalized event is missing, there will be an outstanding transfer. outstandingTransfers = {}; - adapter.matchUsdcDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1Token); - expect(outstandingTransfers[monitoredEoa][l1Token]).to.deep.equal({ + adapter.matchUsdcDepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1USDCToken); + expect( + outstandingTransfers[monitoredEoa][l1USDCToken][TOKEN_SYMBOLS_MAP["USDC.e"].addresses[CHAIN_IDs.LINEA]] + ).to.deep.equal({ totalAmount: toBN(0), depositTxHashes: l1Events.map((e) => e.transactionHash), }); @@ -164,7 +170,6 @@ describe("Cross Chain Adapter: Linea", async function () { expect(result[0].args.nativeToken).to.equal(l1Token); }); it("Matches L1 and L2 events", async function () { - const l1Token = randomAddress(); await erc20BridgeContract.emitBridgingInitiated(randomAddress(), monitoredEoa, l1Token); await erc20BridgeContract.emitBridgingFinalized(l1Token, monitoredEoa); const l1Events = await adapter.getErc20DepositInitiatedEvents( @@ -189,7 +194,9 @@ describe("Cross Chain Adapter: Linea", async function () { // 2. If finalized event is missing, there will be an outstanding transfer. outstandingTransfers = {}; adapter.matchErc20DepositEvents(l1Events, [], outstandingTransfers, monitoredEoa, l1Token); - expect(outstandingTransfers[monitoredEoa][l1Token]).to.deep.equal({ + expect( + outstandingTransfers[monitoredEoa][l1Token][TOKEN_SYMBOLS_MAP.WBTC.addresses[CHAIN_IDs.LINEA]] + ).to.deep.equal({ totalAmount: toBN(0), depositTxHashes: l1Events.map((e) => e.transactionHash), }); diff --git a/test/cross-chain-adapters/OpStack.ts b/test/cross-chain-adapters/OpStack.ts index 8f6b158af..7cb83c9e5 100644 --- a/test/cross-chain-adapters/OpStack.ts +++ b/test/cross-chain-adapters/OpStack.ts @@ -1,9 +1,9 @@ -import { CHAIN_IDs } from "@across-protocol/constants-v2"; +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants-v2"; import { ethers, getContractFactory, Contract, randomAddress, expect } from "../utils"; import { utils } from "@across-protocol/sdk-v2"; import { CONTRACT_ADDRESSES } from "../../src/common"; import { WethBridge } from "../../src/clients/bridges/op-stack/WethBridge"; -import { Signer } from "ethers"; +import { Event, Signer } from "ethers"; describe("Cross Chain Adapter: OP Stack", async function () { let monitoredEoa: string; @@ -39,12 +39,14 @@ describe("Cross Chain Adapter: OP Stack", async function () { await wethBridgeContract.emitDepositInitiated(randomAddress(), monitoredEoa, 1); await wethBridgeContract.emitDepositInitiated(atomicDepositorAddress, randomAddress(), 1); await wethBridgeContract.emitDepositInitiated(atomicDepositorAddress, monitoredEoa, 1); - const result = await wethBridge.queryL1BridgeInitiationEvents( - wethContract.address, - monitoredEoa, - searchConfig, - wethBridgeContract - ); + const result = ( + await wethBridge.queryL1BridgeInitiationEvents( + wethContract.address, + monitoredEoa, + searchConfig, + wethBridgeContract + ) + )[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.OPTIMISM]]; expect(result.length).to.equal(1); expect(result[0].args._from).to.equal(atomicDepositorAddress); expect(result[0].args._to).to.equal(monitoredEoa); @@ -56,23 +58,29 @@ describe("Cross Chain Adapter: OP Stack", async function () { // Counts only finalized events preceding a WETH wrap event. // For EOA's, weth transfer from address should be atomic depositor address await wethBridgeContract.emitDepositFinalized(atomicDepositorAddress, monitoredEoa, 1); - const emptyResult = await wethBridge.queryL2BridgeFinalizationEvents( - wethContract.address, - monitoredEoa, - searchConfig, - wethBridgeContract, - wethContract + const convertResults = (result: Record) => + result[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.OPTIMISM]]; + const emptyResult = convertResults( + await wethBridge.queryL2BridgeFinalizationEvents( + wethContract.address, + monitoredEoa, + searchConfig, + wethBridgeContract, + wethContract + ) ); expect(emptyResult.length).to.equal(0); // Mine Deposit event now. await wethContract.connect(monitoredEoaAccount).deposit({ value: 0 }); - const result = await wethBridge.queryL2BridgeFinalizationEvents( - wethContract.address, - monitoredEoa, - searchConfig, - wethBridgeContract, - wethContract + const result = convertResults( + await wethBridge.queryL2BridgeFinalizationEvents( + wethContract.address, + monitoredEoa, + searchConfig, + wethBridgeContract, + wethContract + ) ); expect(result.length).to.equal(1); expect(result[0].args._from).to.equal(atomicDepositorAddress); diff --git a/test/mocks/MockAdapterManager.ts b/test/mocks/MockAdapterManager.ts index ef24f28e2..c8edf0878 100644 --- a/test/mocks/MockAdapterManager.ts +++ b/test/mocks/MockAdapterManager.ts @@ -1,5 +1,5 @@ import { AdapterManager } from "../../src/clients/bridges"; -import { BigNumber, TransactionResponse } from "../../src/utils"; +import { BigNumber, TransactionResponse, getTokenAddressWithCCTP } from "../../src/utils"; import { createRandomBytes32 } from "../utils"; import { OutstandingTransfers } from "../../src/interfaces"; @@ -40,14 +40,25 @@ export class MockAdapterManager extends AdapterManager { return this.mockedOutstandingCrossChainTransfers[chainId]; } - setMockedOutstandingCrossChainTransfers(chainId: number, address: string, l1Token: string, amount: BigNumber): void { - if (!this.mockedOutstandingCrossChainTransfers[chainId]) { - this.mockedOutstandingCrossChainTransfers[chainId] = {}; - } + setMockedOutstandingCrossChainTransfers( + chainId: number, + address: string, + l1Token: string, + amount: BigNumber, + l2Token?: string + ): void { + this.mockedOutstandingCrossChainTransfers[chainId] ??= {}; + const transfers = this.mockedOutstandingCrossChainTransfers[chainId]; - if (!transfers[address]) { - transfers[address] = {}; - } - transfers[address][l1Token] = { totalAmount: amount, depositTxHashes: [] }; + + transfers[address] ??= {}; + transfers[address][l1Token] ??= {}; + + l2Token ??= getTokenAddressWithCCTP(l1Token, 1, chainId, false); + + transfers[address][l1Token][l2Token] = { + totalAmount: amount, + depositTxHashes: [], + }; } } From eeb8641d3dbb32304c16d465aacb9dda3f1b6eaa Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Wed, 8 May 2024 18:01:05 -0400 Subject: [PATCH 57/66] chore: bump package --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 485c4ad5c..bb9f46da0 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "node": ">=16.18.0" }, "dependencies": { - "@across-protocol/constants-v2": "1.0.22", + "@across-protocol/constants-v2": "1.0.24", "@across-protocol/contracts-v2": "2.5.6", "@across-protocol/sdk-v2": "0.23.8", "@arbitrum/sdk": "^3.1.3", diff --git a/yarn.lock b/yarn.lock index 44e7e0dac..1b324eaa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,10 +11,10 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants-v2@1.0.22": - version "1.0.22" - resolved "https://registry.yarnpkg.com/@across-protocol/constants-v2/-/constants-v2-1.0.22.tgz#b3ca8c6a6276fa36c9ac5fbf17b9f57e4730fa63" - integrity sha512-j559oRNY5xNOo2YYb94cPS1sR6dSxKa2YE2rY8SAdagW11tc7aDflV3AAxmCRPyuI7WpB6w5IklBx0JvwNAO0w== +"@across-protocol/constants-v2@1.0.24": + version "1.0.24" + resolved "https://registry.yarnpkg.com/@across-protocol/constants-v2/-/constants-v2-1.0.24.tgz#30b7dd898ff6d4848717fd6cf393a10f1f2f40b0" + integrity sha512-lDu6rMoTMUolGHwUnX/YkeMcIKB1avd6MnBcyfa4z5DkuF0HCo+7Mbviri2LXW0SOTg/pE26Dvfr1P+HmBUD9A== "@across-protocol/constants-v2@^1.0.19": version "1.0.20" From 6db6e6068f3df1389655acd03df22b0e89337bdb Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 9 May 2024 14:07:45 +0200 Subject: [PATCH 58/66] Add missing l2Token arguments --- src/clients/InventoryClient.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index da364ca1b..d10534959 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -789,7 +789,7 @@ export class InventoryClient { " This chain's pending L1->L2 transfer amount is " + `${formatter( this.crossChainTransferClient - .getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token) + .getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token, l2Token) .toString() )}.\n`; } @@ -963,7 +963,8 @@ export class InventoryClient { const transfers = this.crossChainTransferClient.getOutstandingCrossChainTransferAmount( this.relayer, chainId, - l1Token + l1Token, + l2Token, ); const actualBalanceOnChain = this.tokenClient.getBalance(chainId, l2Token); From d68728e98d87300a49d7d84b23423c2e87f0ddf3 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 9 May 2024 14:11:54 +0200 Subject: [PATCH 59/66] Update getBalanceOnChain --- src/clients/InventoryClient.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index a53aabd89..505fff30d 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -114,20 +114,24 @@ export class InventoryClient { * @returns Balance of l1Token on chainId. */ getBalanceOnChain(chainId: number, l1Token: string, l2Token?: string): BigNumber { + const { crossChainTransferClient, relayer, tokenClient } = this; let balance: BigNumber; + + // Return the balance for a specific l2 token on the remote chain. if (isDefined(l2Token)) { - balance = this.tokenClient.getBalance(chainId, l2Token); - } else { - const l2Tokens = this.getRemoteTokensForL1Token(l1Token, chainId); - balance = l2Tokens - .map((l2Token) => this.tokenClient.getBalance(chainId, l2Token)) - .reduce((acc, curr) => acc.add(curr), bnZero); + balance = tokenClient.getBalance(chainId, l2Token); + return balance.add( + crossChainTransferClient.getOutstandingCrossChainTransferAmount(relayer, chainId, l1Token, l2Token) + ); } - // @todo: This will resolve all outstanding transfers for the L1 token, but it should be filtered for _only_ - // transfers that apply to the specific L2 token. This needs to be fixed in the crossChainTransferClient + const l2Tokens = this.getRemoteTokensForL1Token(l1Token, chainId); + balance = l2Tokens + .map((l2Token) => tokenClient.getBalance(chainId, l2Token)) + .reduce((acc, curr) => acc.add(curr), bnZero); + return balance.add( - this.crossChainTransferClient.getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token) + crossChainTransferClient.getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token) ); } From f5184749f4c56a94ebda3df843f1fab13c9bc031 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 9 May 2024 14:31:31 +0200 Subject: [PATCH 60/66] Fix test --- test/InventoryClient.InventoryRebalance.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index d1665c24d..4eef3a668 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -494,9 +494,6 @@ describe("InventoryClient: Rebalancing inventory", async function () { // The mock adapter manager should have been called with the expected transaction. expect(adapterManager.tokensSentCrossChain[chainId][mainnetUsdc].amount.eq(expectedRebalance)).to.be.true; - // Now, mock these funds having entered the relevant bridge. - adapterManager.setMockedOutstandingCrossChainTransfers(chainId, owner.address, mainnetUsdc, expectedRebalance); - await inventoryClient.update(); await inventoryClient.rebalanceInventoryIfNeeded(); expect(lastSpyLogIncludes(spy, "No rebalances required")).to.be.true; From d56505e9aec2e0cea3ff941c7520518bf2c62b93 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 9 May 2024 09:24:18 -0400 Subject: [PATCH 61/66] nit: lint --- src/clients/InventoryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index d10534959..174bacad4 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -964,7 +964,7 @@ export class InventoryClient { this.relayer, chainId, l1Token, - l2Token, + l2Token ); const actualBalanceOnChain = this.tokenClient.getBalance(chainId, l2Token); From 3d03d779d3802c2b3bf0a466aaaf2b0097c0ae30 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 9 May 2024 13:46:55 -0400 Subject: [PATCH 62/66] nit: optimize call --- src/clients/bridges/CrossChainTransferClient.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/clients/bridges/CrossChainTransferClient.ts b/src/clients/bridges/CrossChainTransferClient.ts index 16a844a0d..e6c67839d 100644 --- a/src/clients/bridges/CrossChainTransferClient.ts +++ b/src/clients/bridges/CrossChainTransferClient.ts @@ -28,10 +28,7 @@ export class CrossChainTransferClient { } // No specific l2Token specified; return the sum of all l1Token transfers to chainId. - return Object.values(transfers) - .map(({ totalAmount }) => totalAmount) - .flat() - .reduce((acc, curr) => acc.add(curr), bnZero); + return Object.values(transfers).reduce((acc, { totalAmount }) => acc.add(totalAmount), bnZero); } getOutstandingCrossChainTransferTxs( @@ -50,9 +47,7 @@ export class CrossChainTransferClient { } // No specific l2Token specified; return the set of all l1Token transfers to chainId. - return Object.values(transfers) - .map(({ depositTxHashes }) => depositTxHashes) - .flat(); + return Object.values(transfers).flatMap(({ depositTxHashes }) => depositTxHashes); } getEnabledChains(): number[] { From 36e2287a4d783ea35f1c9911e272c9d1cabbe75a Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 9 May 2024 13:59:29 -0400 Subject: [PATCH 63/66] nit: re-run test From 0d759cbba2566cd989018ed319da001b1822aa74 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 9 May 2024 20:43:01 +0200 Subject: [PATCH 64/66] lint --- src/clients/InventoryClient.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index eae8273fb..92496aabc 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -130,9 +130,7 @@ export class InventoryClient { .map((l2Token) => tokenClient.getBalance(chainId, l2Token)) .reduce((acc, curr) => acc.add(curr), bnZero); - return balance.add( - crossChainTransferClient.getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token) - ); + return balance.add(crossChainTransferClient.getOutstandingCrossChainTransferAmount(this.relayer, chainId, l1Token)); } /** From 24cc6f69e5368c1f4e25f09e18b41855ef958fdd Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 9 May 2024 15:12:04 -0400 Subject: [PATCH 65/66] nit: re-run test From 7911dc1e6dbcb9bf963ee4b4b4326dd927f0921b Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 9 May 2024 21:20:02 +0200 Subject: [PATCH 66/66] fix(test): Avoid external RPC call Identified w/ James. --- test/Relayer.IndexedSpokePoolClient.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/Relayer.IndexedSpokePoolClient.ts b/test/Relayer.IndexedSpokePoolClient.ts index a5925e491..119994b82 100644 --- a/test/Relayer.IndexedSpokePoolClient.ts +++ b/test/Relayer.IndexedSpokePoolClient.ts @@ -3,11 +3,10 @@ import winston from "winston"; import { Result } from "@ethersproject/abi"; import { CHAIN_IDs } from "@across-protocol/constants-v2"; import { constants, utils as sdkUtils } from "@across-protocol/sdk-v2"; -import * as utils from "../scripts/utils"; import { IndexedSpokePoolClient } from "../src/clients"; import { mangleEventArgs, sortEventsAscending, sortEventsAscendingInPlace } from "../src/utils"; import { SpokePoolClientMessage } from "../src/clients/SpokePoolClient"; -import { assertPromiseError, createSpyLogger, expect, randomAddress } from "./utils"; +import { assertPromiseError, createSpyLogger, deploySpokePoolWithToken, expect, randomAddress } from "./utils"; type Block = providers.Block; type TransactionReceipt = providers.TransactionReceipt; @@ -122,7 +121,7 @@ describe("IndexedSpokePoolClient: Update", async function () { beforeEach(async function () { ({ spyLogger: logger } = createSpyLogger()); - spokePool = await utils.getSpokePoolContract(chainId); + ({ spokePool } = await deploySpokePoolWithToken(chainId, Number.MAX_SAFE_INTEGER.toString())); spokePoolClient = new MockIndexedSpokePoolClient(logger, spokePool, null, chainId, 0); depositId = 1; currentTime = Math.round(Date.now() / 1000);