diff --git a/src/clients/BundleDataApproxClient.ts b/src/clients/BundleDataApproxClient.ts index 4c16f9a498..6813c76a05 100644 --- a/src/clients/BundleDataApproxClient.ts +++ b/src/clients/BundleDataApproxClient.ts @@ -5,8 +5,17 @@ import { SpokePoolManager } from "."; import { SpokePoolClientsByChain } from "../interfaces"; -import { assert, BigNumber, isDefined, winston, ConvertDecimals, getTokenInfo } from "../utils"; -import { Address, bnZero, getL1TokenAddress } from "../utils/SDKUtils"; +import { + assert, + BigNumber, + isDefined, + winston, + ConvertDecimals, + getTokenInfo, + getInventoryEquivalentL1TokenAddress, + getInventoryBalanceContributorTokens, +} from "../utils"; +import { Address, bnZero } from "../utils/SDKUtils"; import { HubPoolClient } from "./HubPoolClient"; export type BundleDataState = { @@ -14,11 +23,16 @@ export type BundleDataState = { upcomingRefunds: { [l1Token: string]: { [chainId: number]: { [relayer: string]: BigNumber } } }; }; +// This client is used to approximate running balances and the refunds and deposits for a given L1 token. Running balances +// can easily be estimated by taking the last validated running balance for a chain and subtracting the total deposit amount +// on that chain since the last validated end block and adding the total refund amount on that chain since the last validated +// end block. export class BundleDataApproxClient { private upcomingRefunds: { [l1Token: string]: { [chainId: number]: { [relayer: string]: BigNumber } } } = undefined; private upcomingDeposits: { [l1Token: string]: { [chainId: number]: BigNumber } } = undefined; private readonly spokePoolManager: SpokePoolManager; + private readonly protocolChainIdIndices: number[]; constructor( spokePoolClients: SpokePoolClientsByChain, private readonly hubPoolClient: HubPoolClient, @@ -27,6 +41,7 @@ export class BundleDataApproxClient { private readonly logger: winston.Logger ) { this.spokePoolManager = new SpokePoolManager(logger, spokePoolClients); + this.protocolChainIdIndices = this.hubPoolClient.configStoreClient.getChainIdIndicesForBlock(); } /** @@ -49,10 +64,14 @@ export class BundleDataApproxClient { this.upcomingRefunds = upcomingRefunds; } - // Return sum of refunds for all fills sent after the fromBlocks. - // Makes a simple assumption that all fills that were sent after the last executed bundle - // are valid and will be refunded on the repayment chain selected. Assume additionally that the repayment chain - // set is a valid one for the deposit. + /** + * Return sum of refunds for all fills sent after the fromBlocks. + * Makes a simple assumption that all fills that were sent by this relayer after the last executed bundle + * are valid and will be refunded on the repayment chain selected. + * @param l1Token L1 token to get refunds for all inventory-equivalent L2 tokens on each chain. + * @param fromBlocks Blocks to start counting refunds from. + * @returns Refunds grouped by relayer for each chain. Refunds are denominated in L1 token decimals. + */ protected getApproximateRefundsForToken( l1Token: Address, fromBlocks: { [chainId: number]: number } @@ -64,51 +83,32 @@ export class BundleDataApproxClient { if (!isDefined(spokePoolClient)) { continue; } - spokePoolClient - .getFills() - .filter((fill) => { - if (fill.blockNumber < fromBlocks[chainId]) { - return false; - } - const expectedL1Token = this.getL1TokenAddress(fill.inputToken, fill.originChainId); - if (!isDefined(expectedL1Token) || !expectedL1Token.eq(l1Token)) { - return false; - } - // A simple check that this fill is *probably* valid is to check that the output and input token - // map to the same L1 token. This means that this method will ignore refunds for swaps, but these - // are currently very rare in practice. This prevents invalid fills with very large input amounts - // from skewing the numbers. - const outputMappedL1Token = this.getL1TokenAddress(fill.outputToken, fill.destinationChainId); - if (!isDefined(outputMappedL1Token) || !outputMappedL1Token.eq(expectedL1Token)) { - return false; - } + spokePoolClient.getFills().forEach((fill) => { + const { inputAmount: _refundAmount, originChainId, repaymentChainId, relayer, inputToken, blockNumber } = fill; + if (blockNumber < fromBlocks[chainId]) { + return; + } - return true; - }) - .forEach((fill) => { - const { inputAmount: _refundAmount, originChainId, repaymentChainId, relayer, inputToken } = fill; - // This call to `getTokenInfo` should not throw since we just filtered out all input tokens for - // which there is no output token. - const { decimals: inputTokenDecimals } = getTokenInfo(inputToken, originChainId); - const inputL1Token = this.getL1TokenAddress(inputToken, originChainId); + // Fills get refunded in the input token currency so need to check that the input token + // and the l1Token parameter are the same. If the input token is equivalent from an inventory management + // perspective to the l1Token then we can count it here because in this case the refund for the fill + // will essentially be in an equivalent l1Token currency on the repayment chain (i.e. getting repaid + // in this currency is just as good as getting repaid in the l1Token currency). + const expectedL1Token = this.getL1TokenAddress(fill.inputToken, fill.originChainId); + if (!isDefined(expectedL1Token) || !expectedL1Token.eq(l1Token)) { + return; + } - assert(inputL1Token.isEVM()); - const inputTokenOnRepaymentChain = this.hubPoolClient.getL2TokenForL1TokenAtBlock( - inputL1Token, - repaymentChainId - ); - if (!isDefined(inputTokenOnRepaymentChain)) { - return; - } - // If the repayment token is defined, then that means an entry exists in our token symbols mapping, - // so this is also a "safe" call to `getTokenInfo.` - const { decimals: repaymentTokenDecimals } = getTokenInfo(inputTokenOnRepaymentChain, repaymentChainId); - const refundAmount = ConvertDecimals(inputTokenDecimals, repaymentTokenDecimals)(_refundAmount); - refundsForChain[repaymentChainId] ??= {}; - refundsForChain[repaymentChainId][relayer.toNative()] ??= bnZero; - refundsForChain[repaymentChainId][relayer.toNative()] = - refundsForChain[repaymentChainId][relayer.toNative()].add(refundAmount); - }); + const { decimals: inputTokenDecimals } = getTokenInfo(inputToken, originChainId); + const refundAmount = ConvertDecimals( + inputTokenDecimals, + getTokenInfo(l1Token, this.hubPoolClient.chainId).decimals + )(_refundAmount); + refundsForChain[repaymentChainId] ??= {}; + refundsForChain[repaymentChainId][relayer.toNative()] ??= bnZero; + refundsForChain[repaymentChainId][relayer.toNative()] = + refundsForChain[repaymentChainId][relayer.toNative()].add(refundAmount); + }); } return refundsForChain; } @@ -134,11 +134,7 @@ export class BundleDataApproxClient { return true; } - // Make sure the leaf for this specific L1 token on the chain from the root bundle relay has been executed. - const l2Token = this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, chainId); - if (!isDefined(l2Token)) { - return false; - } + const l2Tokens = getInventoryBalanceContributorTokens(l1Token, chainId, this.hubPoolClient.chainId); return isDefined( spokePoolClient.getRelayerRefundExecutions().findLast((execution) => { if (!isDefined(execution)) { @@ -149,7 +145,10 @@ export class BundleDataApproxClient { // likelihood, all leaves were executed in the same transaction. If they were not, then this client // will underestimate the upcoming refunds until that leaf is executed. Since this client is ultimately // an approximation, this is acceptable. - return execution.rootBundleId === relay.rootBundleId && execution.l2TokenAddress.eq(l2Token); + return ( + execution.rootBundleId === relay.rootBundleId && + l2Tokens.some((l2Token) => l2Token.eq(execution.l2TokenAddress)) + ); }) ); }); @@ -172,7 +171,7 @@ export class BundleDataApproxClient { const bundleEndBlock = this.hubPoolClient.getBundleEndBlockForChain( correspondingProposedRootBundle, chainId, - this.chainIdList + this.protocolChainIdIndices ); return [chainId, bundleEndBlock > 0 ? bundleEndBlock + 1 : 0]; }) @@ -185,6 +184,12 @@ export class BundleDataApproxClient { return refundsForChain; } + /** + * Return sum of deposits for all deposits sent after the fromBlocks. + * @param l1Token L1 token to get deposits for all inventory-equivalent L2 tokens on each chain. + * @param fromBlocks Blocks to start counting deposits from. + * @returns Deposits grouped by chain. Deposits are denominated in L1 token decimals. + */ private getApproximateDepositsForToken( l1Token: Address, fromBlocks: { [chainId: number]: number } @@ -199,14 +204,24 @@ export class BundleDataApproxClient { spokePoolClient .getDeposits() .filter((deposit) => { + if (deposit.blockNumber < fromBlocks[chainId]) { + return false; + } + // We are ok to group together deposits for inventory-equivalent tokens because these approximate + // deposits and refunds are usually computed and summed together to approximate running balances. So we should + // use the same methodology for equating input and l1 tokens as we do in the getApproximateRefundsForToken method. const expectedL1Token = this.getL1TokenAddress(deposit.inputToken, deposit.originChainId); if (!isDefined(expectedL1Token)) { return false; } - return l1Token.eq(expectedL1Token) && deposit.blockNumber >= fromBlocks[chainId]; + return l1Token.eq(expectedL1Token); }) .forEach((deposit) => { - depositsForChain[chainId] = depositsForChain[chainId].add(deposit.inputAmount); + const depositAmount = ConvertDecimals( + getTokenInfo(deposit.inputToken, deposit.originChainId).decimals, + getTokenInfo(l1Token, this.hubPoolClient.chainId).decimals + )(deposit.inputAmount); + depositsForChain[chainId] = depositsForChain[chainId].add(depositAmount); }); } return depositsForChain; @@ -223,7 +238,7 @@ export class BundleDataApproxClient { protected getL1TokenAddress(l2Token: Address, chainId: number): Address | undefined { try { - return getL1TokenAddress(l2Token, chainId); + return getInventoryEquivalentL1TokenAddress(l2Token, chainId, this.hubPoolClient.chainId); } catch { return undefined; } @@ -245,6 +260,14 @@ export class BundleDataApproxClient { }); } + /** + * Return refunds for a given L1 token on a given chain for all inventory-equivalent L2 tokens on that chain. + * Refunds are denominated in L1 token decimals. + * @param chainId Chain ID to get refunds for. + * @param l1Token L1 token to get refunds for. + * @param relayer Optional relayer to get refunds for. If not provided, returns the sum of refunds for all relayers. + * @returns Refunds for the given L1 token on the given chain for all inventory-equivalent L2 tokens on that chain. Refunds are denominated in L1 token decimals. + */ getUpcomingRefunds(chainId: number, l1Token: Address, relayer?: Address): BigNumber { assert( isDefined(this.upcomingRefunds), @@ -263,6 +286,13 @@ export class BundleDataApproxClient { ); } + /** + * Return deposits for a given L1 token on a given chain for all inventory-equivalent L2 tokens on that chain. + * Deposits are denominated in L1 token decimals. + * @param chainId Chain ID to get deposits for. + * @param l1Token L1 token to get deposits for. + * @returns Deposits for the given L1 token on the given chain for all inventory-equivalent L2 tokens on that chain. Deposits are denominated in L1 token decimals. + */ getUpcomingDeposits(chainId: number, l1Token: Address): BigNumber { assert( isDefined(this.upcomingDeposits), diff --git a/src/clients/InventoryClient.ts b/src/clients/InventoryClient.ts index ae549ac556..628b5a5ce1 100644 --- a/src/clients/InventoryClient.ts +++ b/src/clients/InventoryClient.ts @@ -26,7 +26,7 @@ import { assert, Profiler, getNativeTokenSymbol, - getL1TokenAddress, + getInventoryEquivalentL1TokenAddress, depositForcesOriginChainRepayment, getRemoteTokenForL1Token, getTokenInfo, @@ -37,10 +37,12 @@ import { repaymentChainCanBeQuicklyRebalanced, forEachAsync, max, + getLatestRunningBalances, + getInventoryBalanceContributorTokens, } from "../utils"; import { BundleDataApproxClient, BundleDataState } from "./BundleDataApproxClient"; import { HubPoolClient, TokenClient, TransactionClient } from "."; -import { Deposit, ProposedRootBundle } from "../interfaces"; +import { Deposit, TokenInfo } from "../interfaces"; import { InventoryConfig, isAliasConfig, TokenBalanceConfig } from "../interfaces/InventoryManagement"; import lodash from "lodash"; import { SLOW_WITHDRAWAL_CHAINS } from "../common"; @@ -162,6 +164,10 @@ export class InventoryClient { getInventoryCacheKey(inventoryTopic: string): string { return `${inventoryTopic}-${this.relayer}`; } + protected getTokenInfo(token: Address, chainId: number): TokenInfo { + return getTokenInfo(token, chainId); + } + /** * Resolve the token balance configuration for `l1Token` on `chainId`. If `l1Token` maps to multiple tokens on * `chainId` then `l2Token` must be supplied. @@ -221,16 +227,9 @@ export class InventoryClient { */ getCumulativeBalanceWithApproximateUpcomingRefunds(l1Token: EvmAddress): BigNumber { const totalRefundsPerChain: { [chainId: number]: BigNumber } = {}; - const { decimals: l1TokenDecimals } = getTokenInfo(l1Token, this.hubPoolClient.chainId); for (const chainId of this.chainIdList) { - const repaymentToken = this.getRemoteTokenForL1Token(l1Token, chainId); - if (!repaymentToken) { - continue; - } - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(repaymentToken, chainId); const refundAmount = this.getUpcomingRefunds(chainId, l1Token, this.relayer); - const convertedRefundAmount = sdkUtils.ConvertDecimals(l2TokenDecimals, l1TokenDecimals)(refundAmount); - totalRefundsPerChain[chainId] = convertedRefundAmount; + totalRefundsPerChain[chainId] = refundAmount; } const cumulativeRefunds = Object.values(totalRefundsPerChain).reduce((acc, curr) => acc.add(curr), bnZero); const cumulativeVirtualBalance = this.getCumulativeBalance(l1Token); @@ -267,7 +266,7 @@ export class InventoryClient { const { crossChainTransferClient, relayer, tokenClient } = this; let balance = bnZero; - const { decimals: l1TokenDecimals, symbol: l1TokenSymbol } = getTokenInfo(l1Token, this.hubPoolClient.chainId); + const { decimals: l1TokenDecimals, symbol: l1TokenSymbol } = this.getTokenInfo(l1Token, this.hubPoolClient.chainId); // If chain is L1, add all pending L2->L1 withdrawals. if (chainId === this.hubPoolClient.chainId) { @@ -284,9 +283,13 @@ export class InventoryClient { // Add in any pending swap rebalances. Pending Rebalances are currently only supported for the canonical L2 tokens // mapped to each L1 token (i.e. the L2 token for an L1 token returned by getRemoteTokenForL1Token()) const pendingRebalancesForChain = this.pendingRebalances[chainId]; - if (isDefined(pendingRebalancesForChain)) { - const _l2Token = l2Token ?? this.getRemoteTokenForL1Token(l1Token, chainId); - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(_l2Token, chainId); + const canonicalL2Token = getRemoteTokenForL1Token(l1Token, chainId, this.hubPoolClient.chainId); + if ( + isDefined(pendingRebalancesForChain) && + isDefined(canonicalL2Token) && + (!isDefined(l2Token) || l2Token.eq(canonicalL2Token)) + ) { + const { decimals: l2TokenDecimals } = this.getTokenInfo(canonicalL2Token, chainId); const pendingRebalancesForToken = pendingRebalancesForChain[l1TokenSymbol]; if (isDefined(pendingRebalancesForToken)) { balance = balance.add(sdkUtils.ConvertDecimals(l2TokenDecimals, l1TokenDecimals)(pendingRebalancesForToken)); @@ -295,7 +298,7 @@ export class InventoryClient { // Return the balance for a specific l2 token on the remote chain. if (isDefined(l2Token)) { - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId); + const { decimals: l2TokenDecimals } = this.getTokenInfo(l2Token, chainId); balance = balance.add( sdkUtils.ConvertDecimals(l2TokenDecimals, l1TokenDecimals)(tokenClient.getBalance(chainId, l2Token)) ); @@ -310,7 +313,7 @@ export class InventoryClient { balance = balance.add( l2Tokens .map((l2Token) => { - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId); + const { decimals: l2TokenDecimals } = this.getTokenInfo(l2Token, chainId); return sdkUtils.ConvertDecimals(l2TokenDecimals, l1TokenDecimals)(tokenClient.getBalance(chainId, l2Token)); }) .reduce((acc, curr) => acc.add(curr), bnZero) @@ -379,8 +382,8 @@ export class InventoryClient { return bnZero; } - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId); - const { decimals: l1TokenDecimals } = getTokenInfo(l1Token, this.hubPoolClient.chainId); + const { decimals: l2TokenDecimals } = this.getTokenInfo(l2Token, chainId); + const { decimals: l1TokenDecimals } = this.getTokenInfo(l1Token, this.hubPoolClient.chainId); const shortfall = sdkUtils.ConvertDecimals( l2TokenDecimals, l1TokenDecimals @@ -423,12 +426,16 @@ export class InventoryClient { .map((token) => toAddressType(token, chainId)); } - const destinationToken = this.getRemoteTokenForL1Token(l1Token, chainId); - if (!isDefined(destinationToken)) { + const inventoryEquivalentTokens = getInventoryBalanceContributorTokens( + l1Token, + chainId, + this.hubPoolClient.chainId + ); + if (inventoryEquivalentTokens.length === 0) { return []; } - return [destinationToken]; + return inventoryEquivalentTokens; } getEnabledChains(): number[] { @@ -468,10 +475,6 @@ export class InventoryClient { return this.bundleDataApproxClient.getUpcomingRefunds(chainId, l1Token, relayer); } - getUpcomingDeposits(chainId: number, l1Token: EvmAddress): BigNumber { - return this.bundleDataApproxClient.getUpcomingDeposits(chainId, l1Token); - } - /** * Returns possible repayment chain options for a deposit. This is designed to be called by the relayer * so that it can batch compute LP fees for all possible repayment chains. By locating this function @@ -557,13 +560,7 @@ export class InventoryClient { */ getL1TokenAddress(l2Token: Address, chainId: number): EvmAddress | undefined { try { - // Add exception for USDC-like tokens which only exist on L2. - // @todo Add support for equivalence mappings. - const l2TokenInfo = getTokenInfo(l2Token, chainId); - if (["pathUSD"].includes(l2TokenInfo.symbol)) { - return EvmAddress.from(TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubPoolClient.chainId]); - } - return getL1TokenAddress(l2Token, chainId); + return getInventoryEquivalentL1TokenAddress(l2Token, chainId, this.hubPoolClient.chainId); } catch { return undefined; } @@ -700,21 +697,15 @@ export class InventoryClient { } } - const { decimals: l1TokenDecimals } = getTokenInfo(l1Token, this.hubPoolClient.chainId); - const { decimals: inputTokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(inputToken, originChainId); + const { decimals: l1TokenDecimals } = this.getTokenInfo(l1Token, this.hubPoolClient.chainId); + const { decimals: inputTokenDecimals } = this.getTokenInfo(inputToken, originChainId); const inputAmountInL1TokenDecimals = sdkUtils.ConvertDecimals(inputTokenDecimals, l1TokenDecimals)(inputAmount); - // Consider any upcoming refunds. Convert all refunds to same precision as L1 token. + // Consider any upcoming refunds. const totalRefundsPerChain: { [chainId: number]: BigNumber } = {}; for (const chainId of this.chainIdList) { - const repaymentToken = chainId === originChainId ? inputToken : this.getRemoteTokenForL1Token(l1Token, chainId); - if (!repaymentToken) { - continue; - } - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(repaymentToken, chainId); const refundAmount = this.getUpcomingRefunds(chainId, l1Token, this.relayer); - const convertedRefundAmount = sdkUtils.ConvertDecimals(l2TokenDecimals, l1TokenDecimals)(refundAmount); - totalRefundsPerChain[chainId] = convertedRefundAmount; + totalRefundsPerChain[chainId] = refundAmount; } // @dev: The following async call to `getExcessRunningBalancePcts` should be very fast compared to the above @@ -797,7 +788,7 @@ export class InventoryClient { } else { repaymentToken = inputToken; } - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(repaymentToken, chainId); + const { decimals: l2TokenDecimals } = this.getTokenInfo(repaymentToken, chainId); const chainShortfall = sdkUtils.ConvertDecimals( l2TokenDecimals, l1TokenDecimals @@ -914,77 +905,11 @@ export class InventoryClient { chainsToEvaluate: number[] ): Promise<{ [chainId: number]: BigNumber }> { const mark = this.profiler.start("getLatestRunningBalances"); - const chainIds = this.hubPoolClient.configStoreClient.getChainIdIndicesForBlock(); - const l1TokenDecimals = getTokenInfo(l1Token, this.hubPoolClient.chainId).decimals; - const runningBalances = Object.fromEntries( - await sdkUtils.mapAsync(chainsToEvaluate, async (chainId) => { - const chainIdIndex = chainIds.indexOf(chainId); - - // We need to find the latest validated running balance for this chain and token. - const lastValidatedRunningBalance = this.hubPoolClient.getRunningBalanceBeforeBlockForChain( - this.hubPoolClient.latestHeightSearched, - chainId, - l1Token - ).runningBalance; - - // Approximate latest running balance for a chain as last known validated running balance... - // - minus total deposit amount on chain since the latest validated end block - // - plus total refund amount on chain since the latest validated end block - const latestValidatedBundle = this.hubPoolClient.getLatestExecutedRootBundleContainingL1Token( - this.hubPoolClient.latestHeightSearched, - chainId, - l1Token - ); - const l2Token = this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId)); - const l2TokenDecimals = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId).decimals; - const l2AmountToL1Amount = sdkUtils.ConvertDecimals(l2TokenDecimals, l1TokenDecimals); - - // If there is no ExecutedRootBundle event in the hub pool client's lookback for the token and chain, then - // default the bundle end block to 0. This will force getUpcomingDepositAmount to count any deposit - // seen in the spoke pool client's lookback. It would be very odd however for there to be deposits or refunds - // for a token and chain without there being a validated root bundle containing the token, so really the - // following check will be hit if the chain's running balance is very stale. The best way to check - // its running balance at that point is to query the token balance directly but this is still potentially - // inaccurate if someone sent tokens directly to the contract, and it incurs an extra RPC call so we avoid - // it for now. The default running balance will be 0, and this function is primarily designed to choose - // which chains have too many running balances and therefore should be selected for repayment, so returning - // 0 here means this chain will never be selected for repayment as a "slow withdrawal" chain. - let lastValidatedBundleEndBlock = 0; - let proposedRootBundle: ProposedRootBundle | undefined; - if (latestValidatedBundle) { - proposedRootBundle = this.hubPoolClient.getLatestFullyExecutedRootBundle( - latestValidatedBundle.blockNumber // The ProposeRootBundle event must precede the ExecutedRootBundle - // event we grabbed above. However, it might not exist if the ExecutedRootBundle event is old enough - // that the preceding ProposeRootBundle is older than the lookback. In this case, leave the - // last validated bundle end block as 0, since it must be before the earliest lookback block since it was - // before the ProposeRootBundle event and we can't even find that. - ); - if (proposedRootBundle) { - lastValidatedBundleEndBlock = proposedRootBundle.bundleEvaluationBlockNumbers[chainIdIndex].toNumber(); - } - } - const upcomingDepositsAfterLastValidatedBundle = l2AmountToL1Amount(this.getUpcomingDeposits(chainId, l1Token)); - const upcomingRefundsAfterLastValidatedBundle = l2AmountToL1Amount(this.getUpcomingRefunds(chainId, l1Token)); - - // Updated running balance is last known running balance minus deposits plus upcoming refunds. - const latestRunningBalance = lastValidatedRunningBalance - .sub(upcomingDepositsAfterLastValidatedBundle) - .add(upcomingRefundsAfterLastValidatedBundle); - // A negative running balance means that the spoke has a balance. If the running balance is positive, then the hub - // owes it funds and its below target so we don't want to take additional repayment. - const absLatestRunningBalance = latestRunningBalance.lt(0) ? latestRunningBalance.abs() : toBN(0); - return [ - chainId, - { - absLatestRunningBalance, - lastValidatedRunningBalance, - upcomingDeposits: upcomingDepositsAfterLastValidatedBundle, - upcomingRefunds: upcomingRefundsAfterLastValidatedBundle, - bundleEndBlock: lastValidatedBundleEndBlock, - proposedRootBundle: proposedRootBundle?.txnRef, - }, - ]; - }) + const runningBalances = await getLatestRunningBalances( + l1Token, + chainsToEvaluate, + this.hubPoolClient, + this.bundleDataApproxClient ); mark.stop({ message: `Time to get running balances for ${l1Token}`, @@ -1139,8 +1064,8 @@ export class InventoryClient { } _getPossibleShortfallRebalances(l1Token: EvmAddress, chainId: number, l2Token: Address): Rebalance[] { - const { decimals: l1TokenDecimals } = getTokenInfo(l1Token, this.hubPoolClient.chainId); - const { decimals: l2TokenDecimals } = getTokenInfo(l2Token, chainId); + const { decimals: l1TokenDecimals } = this.getTokenInfo(l1Token, this.hubPoolClient.chainId); + const { decimals: l2TokenDecimals } = this.getTokenInfo(l2Token, chainId); // Order unfilled amounts from largest to smallest to prioritize larger shortfalls. const unfilledDepositAmounts = this.tokenClient .getUnfilledDepositAmounts(chainId, l2Token) @@ -1283,14 +1208,14 @@ export class InventoryClient { const chainId = Number(_chainId); mrkdwn += `*Rebalances sent to ${getNetworkName(chainId)}:*\n`; for (const { l1Token, l2Token, amount, hash, chainId, isShortfallRebalance } of rebalances) { - const tokenInfo = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId); + const tokenInfo = this.getTokenInfo(l2Token, chainId); assert( isDefined(tokenInfo), `InventoryClient::rebalanceInventoryIfNeeded no token info for L2 token ${l2Token} on chain ${chainId}` ); const { symbol, decimals } = tokenInfo; const l2TokenFormatter = createFormatFunction(2, 4, false, decimals); - const l1TokenInfo = getTokenInfo(l1Token, this.hubPoolClient.chainId); + const l1TokenInfo = this.getTokenInfo(l1Token, this.hubPoolClient.chainId); const l1Formatter = createFormatFunction(2, 4, false, l1TokenInfo.decimals); const cumulativeBalance = this.getCumulativeBalance(l1Token); @@ -1317,13 +1242,13 @@ export class InventoryClient { const chainId = Number(_chainId); mrkdwn += `*Insufficient amount to rebalance to ${getNetworkName(chainId)}:*\n`; for (const { l1Token, l2Token, balance, amount } of rebalances) { - const tokenInfo = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId); + const tokenInfo = this.getTokenInfo(l2Token, chainId); if (!tokenInfo) { throw new Error( `InventoryClient::rebalanceInventoryIfNeeded no token info for L2 token ${l2Token} on chain ${chainId}` ); } - const l1TokenInfo = getTokenInfo(l1Token, this.hubPoolClient.chainId); + const l1TokenInfo = this.getTokenInfo(l1Token, this.hubPoolClient.chainId); const l1Formatter = createFormatFunction(2, 4, false, l1TokenInfo.decimals); const { symbol, decimals } = tokenInfo; @@ -1503,7 +1428,7 @@ export class InventoryClient { const chainMrkdwns: { [chainId: number]: string } = {}; await sdkUtils.forEachAsync(this.getL1Tokens(), async (l1Token) => { - const l1TokenInfo = getTokenInfo(l1Token, this.hubPoolClient.chainId); + const l1TokenInfo = this.getTokenInfo(l1Token, this.hubPoolClient.chainId); const formatter = createFormatFunction(2, 4, false, l1TokenInfo.decimals); // We do not currently count any outstanding L2->L1 pending withdrawal balance in the cumulative balance @@ -1522,7 +1447,7 @@ export class InventoryClient { const l2Tokens = this.getRemoteTokensForL1Token(l1Token, chainId); await sdkUtils.forEachAsync(l2Tokens, async (l2Token) => { - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId); + const { decimals: l2TokenDecimals } = this.getTokenInfo(l2Token, chainId); const l2TokenFormatter = createFormatFunction(2, 4, false, l2TokenDecimals); const l2BalanceFromL1Decimals = sdkUtils.ConvertDecimals(l1TokenInfo.decimals, l2TokenDecimals); const tokenConfig = this.getTokenConfig(l1Token, chainId, l2Token); @@ -1685,7 +1610,7 @@ export class InventoryClient { message: `L2->L1 withdrawals on ${getNetworkName(chainId)} submitted`, chainId, withdrawalsRequired: withdrawalsRequired[chainId].map((withdrawal: L2Withdrawal) => { - const l2TokenInfo = this.hubPoolClient.getTokenInfoForAddress(withdrawal.l2Token, Number(chainId)); + const l2TokenInfo = this.getTokenInfo(withdrawal.l2Token, Number(chainId)); const formatter = createFormatFunction(2, 4, false, l2TokenInfo.decimals); return { @@ -1715,7 +1640,7 @@ export class InventoryClient { } = {}; const cumulativeBalances: { [symbol: string]: string } = {}; Object.entries(distribution).forEach(([l1Token, distributionForToken]) => { - const tokenInfo = getTokenInfo(EvmAddress.from(l1Token), this.hubPoolClient.chainId); + const tokenInfo = this.getTokenInfo(EvmAddress.from(l1Token), this.hubPoolClient.chainId); if (tokenInfo === undefined) { throw new Error( `InventoryClient::constructConsideringRebalanceDebugLog info not found for L1 token ${l1Token}` @@ -1732,7 +1657,7 @@ export class InventoryClient { Object.entries(distributionForToken[chainId]).forEach(([_l2Token, amount]) => { const l2Token = toAddressType(_l2Token, chainId); - const { decimals: l2TokenDecimals } = this.hubPoolClient.getTokenInfoForAddress(l2Token, chainId); + const { decimals: l2TokenDecimals } = this.getTokenInfo(l2Token, chainId); const l2Formatter = createFormatFunction(2, 4, false, l2TokenDecimals); const l1TokenAddr = EvmAddress.from(l1Token); const balanceOnChain = this.getBalanceOnChain(chainId, l1TokenAddr, l2Token); @@ -1834,7 +1759,9 @@ export class InventoryClient { l1Token ); Object.keys(pendingWithdrawalBalances).forEach((chainId) => { - this.pendingL2Withdrawals[l1Token.toNative()][Number(chainId)] = pendingWithdrawalBalances[Number(chainId)]; + if (pendingWithdrawalBalances[Number(chainId)].gt(bnZero)) { + this.pendingL2Withdrawals[l1Token.toNative()][Number(chainId)] = pendingWithdrawalBalances[Number(chainId)]; + } }); }); this.logger.debug({ diff --git a/src/clients/TokenClient.ts b/src/clients/TokenClient.ts index 9b92df47fd..4bb67bdf91 100644 --- a/src/clients/TokenClient.ts +++ b/src/clients/TokenClient.ts @@ -19,7 +19,7 @@ import { toBN, winston, getRedisCache, - TOKEN_SYMBOLS_MAP, + getInventoryBalanceContributorTokens, getRemoteTokenForL1Token, getTokenInfo, isEVMSpokePoolClient, @@ -249,26 +249,19 @@ export class TokenClient { } const tokens = hubPoolTokens - .map(({ symbol, address }) => { + .map(({ address }) => { let tokenAddrs: string[] = []; try { - const spokePoolToken = getRemoteTokenForL1Token(address, chainId, this.hubPoolClient.chainId); - tokenAddrs.push(spokePoolToken.toEvmAddress()); + tokenAddrs = dedupArray( + getInventoryBalanceContributorTokens(address, chainId, this.hubPoolClient.chainId).map((token) => + token.toEvmAddress() + ) + ); } catch { // No known deployment for this token on the SpokePool. // note: To be overhauled subject to https://github.com/across-protocol/sdk/pull/643 } - - // If the HubPool token is USDC then it might map to multiple tokens on the destination chain. - if (symbol === "USDC") { - ["USDC.e", "USDbC", "USDzC", "pathUSD"] - .map((symbol) => TOKEN_SYMBOLS_MAP[symbol]?.addresses[chainId]) - .filter(isDefined) - .forEach((address) => tokenAddrs.push(address)); - tokenAddrs = dedupArray(tokenAddrs); - } - - return tokenAddrs.filter(isDefined).map((address) => erc20.attach(address)); + return tokenAddrs.filter(isDefined).map((tokenAddress) => erc20.attach(tokenAddress)); }) .flat(); diff --git a/test/BundleDataApproxClient.ts b/test/BundleDataApproxClient.ts index 43e0665263..e8c59cebfe 100644 --- a/test/BundleDataApproxClient.ts +++ b/test/BundleDataApproxClient.ts @@ -290,6 +290,111 @@ describe("BundleDataApproxClient: Accounting for unexecuted, upcoming relayer re }); }); + describe("Aggregates balances across multiple inventory contributor tokens", function () { + // Test that when two different L2 tokens on the same chain both map to the same L1 token + // (e.g. USDC.e and native USDC both mapping to L1 USDC on Optimism), getUpcomingRefunds and + // getUpcomingDeposits correctly aggregate balances from both contributor tokens. + const nativeUsdcOnOptimism = TOKEN_SYMBOLS_MAP.USDC.addresses[OPTIMISM]; + + beforeEach(function () { + // Update the token mapping so both USDC.e and native USDC on Optimism map to L1 USDC. + (bundleDataClient as MockBundleDataApproxClient).setTokenMapping({ + [mainnetWeth]: { + [MAINNET]: mainnetWeth, + [OPTIMISM]: l2TokensForWeth[OPTIMISM], + [BSC]: l2TokensForWeth[BSC], + }, + [mainnetUsdc]: { + [MAINNET]: mainnetUsdc, + [OPTIMISM]: [l2TokensForUsdc[OPTIMISM], nativeUsdcOnOptimism], + [BSC]: l2TokensForUsdc[BSC], + }, + }); + }); + + it("getUpcomingRefunds aggregates fills from both contributor tokens", async function () { + const fillAmount1 = toBNWei(100, 6); + const fillAmount2 = toBNWei(50, 6); + + // Fill with the primary USDC.e token on Optimism, repaid on Optimism. + await generateFill("USDC", OPTIMISM, OPTIMISM, owner.address, fillAmount1); + + // Fill with the second contributor token on Optimism, repaid on Optimism. + // Use the raw MockSpokePoolClient.fillRelay to specify a custom inputToken. + const spokePoolClient = spokePoolClients[OPTIMISM]; + (spokePoolClient as unknown as MockSpokePoolClient).fillRelay({ + message: "0x", + messageHash: ZERO_BYTES, + inputToken: toAddressType(nativeUsdcOnOptimism, OPTIMISM), + outputToken: toAddressType(l2TokensForUsdc[OPTIMISM], OPTIMISM), + destinationChainId: OPTIMISM, + depositor: toAddressType(owner.address, OPTIMISM), + recipient: toAddressType(owner.address, OPTIMISM), + exclusivityDeadline: getCurrentTime() + 14400, + depositId: bnZero, + exclusiveRelayer: toAddressType(owner.address, OPTIMISM), + inputAmount: fillAmount2, + outputAmount: fillAmount2, + fillDeadline: getCurrentTime() + 14400, + blockNumber: spokePoolClient.latestHeightSearched, + txnIndex: 0, + logIndex: 1, + txnRef: "0x", + relayer: toAddressType(owner.address, MAINNET), + originChainId: OPTIMISM, + repaymentChainId: OPTIMISM, + relayExecutionInfo: { + updatedRecipient: toAddressType(owner.address, OPTIMISM), + updatedMessage: "0x", + updatedMessageHash: ZERO_BYTES, + updatedOutputAmount: fillAmount2, + fillType: interfaces.FillType.FastFill, + }, + } as interfaces.FillWithBlock); + await spokePoolClient.update(["FilledRelay"]); + + bundleDataClient.initialize(); + + // Refunds should aggregate both fills. + const totalRefunds = bundleDataClient.getUpcomingRefunds(OPTIMISM, l1Usdc); + expect(totalRefunds).to.equal(fillAmount1.add(fillAmount2)); + }); + + it("getUpcomingDeposits aggregates deposits from both contributor tokens", async function () { + const depositAmount1 = toBNWei(200, 6); + const depositAmount2 = toBNWei(75, 6); + + // Deposit with the primary USDC.e token on Optimism. + await generateDeposit("USDC", OPTIMISM, depositAmount1); + + // Deposit with the second contributor token on Optimism. + const spokePoolClient = spokePoolClients[OPTIMISM]; + (spokePoolClient as unknown as MockSpokePoolClient).deposit({ + inputToken: toAddressType(nativeUsdcOnOptimism, OPTIMISM), + inputAmount: depositAmount2, + depositor: toAddressType(owner.address, OPTIMISM), + recipient: toAddressType(owner.address, OPTIMISM), + outputToken: toAddressType(randomAddress(), OPTIMISM), + outputAmount: depositAmount2, + quoteTimestamp: getCurrentTime(), + fillDeadline: getCurrentTime() + 14400, + exclusivityDeadline: getCurrentTime() + 14400, + exclusiveRelayer: toAddressType(owner.address, OPTIMISM), + originChainId: OPTIMISM, + blockNumber: spokePoolClient.latestHeightSearched, + txnIndex: 0, + logIndex: 1, + } as interfaces.DepositWithBlock); + await spokePoolClient.update(["FundsDeposited"]); + + bundleDataClient.initialize(); + + // Deposits should aggregate both deposit amounts. + const totalDeposits = bundleDataClient.getUpcomingDeposits(OPTIMISM, l1Usdc); + expect(totalDeposits).to.equal(depositAmount1.add(depositAmount2)); + }); + }); + describe("getUnexecutedBundleStartBlocks", function () { it("Returns endBlocks+1 from last relayed bundle for chain", async function () { configStoreClient.setAvailableChains([MAINNET, OPTIMISM, BSC]); @@ -297,7 +402,7 @@ describe("BundleDataApproxClient: Accounting for unexecuted, upcoming relayer re // When there are no RootBundleRelay/ProposedRootBundle events, returns 0 for all chains. const defaultFromBlocks = (bundleDataClient as MockBundleDataApproxClient).getUnexecutedBundleStartBlocks( l1Weth, - false + true ); expect(defaultFromBlocks[MAINNET]).to.equal(0); expect(defaultFromBlocks[OPTIMISM]).to.equal(0); @@ -305,6 +410,7 @@ describe("BundleDataApproxClient: Accounting for unexecuted, upcoming relayer re const rootBundleRelays = [ { + rootBundleId: 0, relayerRefundRoot: "0x1234", bundleEvaluationBlockNumbers: [toBN(3), toBN(4), toBN(5)], }, @@ -314,10 +420,24 @@ describe("BundleDataApproxClient: Accounting for unexecuted, upcoming relayer re ); hubPoolClient.setValidatedRootBundles(rootBundleRelays as unknown as ProposedRootBundle[]); - const fromBlocks1 = (bundleDataClient as MockBundleDataApproxClient).getUnexecutedBundleStartBlocks( - l1Weth, - false - ); + // Add a matching execution so getUnexecutedBundleStartBlocks can verify the leaf was executed. + // Directly push to the internal array to avoid event parsing issues in the mock. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (spokePoolClients[MAINNET] as any).relayerRefundExecutions.push({ + rootBundleId: 0, + leafId: 0, + chainId: MAINNET, + amountToReturn: bnZero, + l2TokenAddress: toAddressType(mainnetWeth, MAINNET), + refundAddresses: [], + refundAmounts: [], + blockNumber: 0, + txnIndex: 0, + logIndex: 0, + txnRef: "0x", + }); + + const fromBlocks1 = (bundleDataClient as MockBundleDataApproxClient).getUnexecutedBundleStartBlocks(l1Weth, true); // Only the spoke pool clients that saw the RootBundleRelay events should have non-zero fromBlocks. expect(fromBlocks1[MAINNET]).to.equal(4); diff --git a/test/InventoryClient.InventoryRebalance.ts b/test/InventoryClient.InventoryRebalance.ts index 7b64282e09..bbff238919 100644 --- a/test/InventoryClient.InventoryRebalance.ts +++ b/test/InventoryClient.InventoryRebalance.ts @@ -18,7 +18,7 @@ import { import { ConfigStoreClient, InventoryClient } from "../src/clients"; // Tested import { CrossChainTransferClient } from "../src/clients/bridges"; import { InventoryConfig } from "../src/interfaces"; -import { MockAdapterManager, MockHubPoolClient, MockTokenClient } from "./mocks/"; +import { MockAdapterManager, MockHubPoolClient, MockInventoryClient, MockTokenClient } from "./mocks/"; import { bnZero, CHAIN_IDs, @@ -76,10 +76,18 @@ const inventoryConfig: InventoryConfig = { [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 }, - [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [l2TokensForUsdc[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [l2TokensForUsdc[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [l2TokensForUsdc[BASE]]: { + [BASE]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [l2TokensForUsdc[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, }, }, }; @@ -121,7 +129,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { crossChainTransferClient = new CrossChainTransferClient(spyLogger, enabledChainIds, adapterManager); mockRebalancerClient = new MockRebalancerClient(spyLogger); - inventoryClient = new InventoryClient( + inventoryClient = new MockInventoryClient( EvmAddress.from(owner.address), spyLogger, inventoryConfig, @@ -433,8 +441,9 @@ describe("InventoryClient: Rebalancing inventory", async function () { const testL2Token = toAddressType(l2TokensForUsdc[testChain], testChain); const targetOverageBuffer = toWei("2"); beforeEach(function () { - inventoryConfig.tokenConfig[testL1Token][testChain].withdrawExcessPeriod = 7200; - inventoryConfig.tokenConfig[testL1Token][testChain].targetOverageBuffer = targetOverageBuffer; + inventoryConfig.tokenConfig[testL1Token][testL2Token.toNative()][testChain].withdrawExcessPeriod = 7200; + inventoryConfig.tokenConfig[testL1Token][testL2Token.toNative()][testChain].targetOverageBuffer = + targetOverageBuffer; const mockAdapter = new MockBaseChainAdapter(); adapterManager.setAdapters(testChain, mockAdapter); }); @@ -443,7 +452,9 @@ describe("InventoryClient: Rebalancing inventory", async function () { // The threshold to trigger an excess withdrawal is when the currentAllocPct is greater than the // targetPct multiplied by the "targetPctMultiplier" const targetPctMultiplier = targetOverageBuffer.mul(toWei("0.95")).div(toWei("1")); - const excessWithdrawThresholdPct = inventoryConfig.tokenConfig[testL1Token][testChain].targetPct + const excessWithdrawThresholdPct = inventoryConfig.tokenConfig[testL1Token][testL2Token.toNative()][ + testChain + ].targetPct .mul(targetPctMultiplier) .div(toWei("1")); @@ -459,7 +470,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { await inventoryClient.withdrawExcessBalances(); const expectedWithdrawalPct = currentAllocationPct.sub( - inventoryConfig.tokenConfig[testL1Token][testChain].targetPct + inventoryConfig.tokenConfig[testL1Token][testL2Token.toNative()][testChain].targetPct ); const expectedWithdrawalAmount = expectedWithdrawalPct.mul(currentCumulativeBalance).div(toWei(1)); expect(adapterManager.withdrawalsRequired[0].amountToWithdraw).eq(expectedWithdrawalAmount); @@ -487,7 +498,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { await inventoryClient.withdrawExcessBalances(); const expectedWithdrawalPct = currentAllocationPct.sub( - BigNumber.from(inventoryConfig.tokenConfig[testL1Token][testChain].targetPct) + BigNumber.from(inventoryConfig.tokenConfig[testL1Token][testL2Token.toNative()][testChain].targetPct) ); // Expected withdrawal amount is in correct decimals: @@ -664,7 +675,7 @@ describe("InventoryClient: Rebalancing inventory", async function () { it("Correct normalizes pending rebalances to L1 token decimals", async function () { const testChain = CHAIN_IDs.OPTIMISM; - hubPoolClient.mapTokenInfo(toAddressType(bridgedUSDC[testChain], testChain), "USDC", 18); + hubPoolClient.mapTokenInfo(toAddressType(nativeUSDC[testChain], testChain), "USDC", 18); // Pending rebalance should be in chain decimals: const pendingRebalanceAmount = toWei("1"); @@ -675,11 +686,42 @@ describe("InventoryClient: Rebalancing inventory", async function () { const expectedBalance = toMegaWei("1"); expect( inventoryClient - .getBalanceOnChain(testChain, EvmAddress.from(mainnetUsdc), toAddressType(bridgedUSDC[testChain], testChain)) + .getBalanceOnChain(testChain, EvmAddress.from(mainnetUsdc), toAddressType(nativeUSDC[testChain], testChain)) .eq(expectedBalance) ).to.be.true; }); + it("Includes pending rebalances once in aggregate chain balances", async function () { + const testChain = CHAIN_IDs.OPTIMISM; + const canonicalL2Token = toAddressType(nativeUSDC[testChain], testChain); + const nonCanonicalL2Token = toAddressType(bridgedUSDC[testChain], testChain); + hubPoolClient.mapTokenInfo(canonicalL2Token, "USDC", 18); + tokenClient.setTokenData(testChain, canonicalL2Token, toWei("2000")); + + const bridgedBalance = toMegaWei("2"); + tokenClient.setTokenData(testChain, nonCanonicalL2Token, bridgedBalance); + + const pendingRebalanceAmount = toWei("1"); + mockRebalancerClient.setPendingRebalance(testChain, "USDC", pendingRebalanceAmount); + await inventoryClient.update(); + + const aggregateBalance = inventoryClient.getBalanceOnChain(testChain, EvmAddress.from(mainnetUsdc)); + const canonicalBalance = inventoryClient.getBalanceOnChain( + testChain, + EvmAddress.from(mainnetUsdc), + canonicalL2Token + ); + const nonCanonicalBalance = inventoryClient.getBalanceOnChain( + testChain, + EvmAddress.from(mainnetUsdc), + nonCanonicalL2Token + ); + + expect(aggregateBalance).to.eq(toMegaWei("2003")); + expect(canonicalBalance).to.eq(toMegaWei("2001")); + expect(nonCanonicalBalance).to.eq(bridgedBalance); + }); + it("Correctly sums 1:many token balances", async function () { enabledChainIds .filter((chainId) => chainId !== MAINNET) diff --git a/test/InventoryClient.RefundChain.ts b/test/InventoryClient.RefundChain.ts index 5e4ab8603d..4b805bd105 100644 --- a/test/InventoryClient.RefundChain.ts +++ b/test/InventoryClient.RefundChain.ts @@ -82,10 +82,18 @@ describe("InventoryClient: Refund chain selection", async function () { [BSC]: { 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 }, - [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, - [BSC]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + [l2TokensForUsdc[OPTIMISM]]: { + [OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer }, + }, + [l2TokensForUsdc[POLYGON]]: { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [l2TokensForUsdc[ARBITRUM]]: { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, + [l2TokensForUsdc[BSC]]: { + [BSC]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }, }, }, }; @@ -259,9 +267,10 @@ describe("InventoryClient: Refund chain selection", async function () { sampleDepositData.inputAmount = toWei(5); sampleDepositData.outputAmount = sdkUtils.ConvertDecimals(18, 6)(await computeOutputAmount(sampleDepositData)); + // BundleDataApproxClient now returns upcoming refunds in L1 token decimals, so mock them in L1 decimals too. (inventoryClient as MockInventoryClient).setUpcomingRefunds(mainnetWeth, { [MAINNET]: toWei(5), - [OPTIMISM]: toMegaWei(10), + [OPTIMISM]: toWei(10), }); expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.deep.equal([MAINNET]); @@ -865,12 +874,22 @@ describe("InventoryClient: Refund chain selection", async function () { // Modify mocks to be aware of native USDC, which is a "fast" rebalance token for certain routes hubPoolClient.setTokenMapping(mainnetUsdc, POLYGON, TOKEN_SYMBOLS_MAP.USDC.addresses[POLYGON]); hubPoolClient.setTokenMapping(mainnetUsdc, ARBITRUM, TOKEN_SYMBOLS_MAP.USDC.addresses[ARBITRUM]); + hubPoolClient.mapTokenInfo(toAddressType(TOKEN_SYMBOLS_MAP.USDC.addresses[POLYGON], POLYGON), "USDC", 6); + hubPoolClient.mapTokenInfo(toAddressType(TOKEN_SYMBOLS_MAP.USDC.addresses[ARBITRUM], ARBITRUM), "USDC", 6); (inventoryClient as unknown as MockInventoryClient).setTokenMapping({ [mainnetUsdc]: { [POLYGON]: TOKEN_SYMBOLS_MAP.USDC.addresses[POLYGON], [ARBITRUM]: TOKEN_SYMBOLS_MAP.USDC.addresses[ARBITRUM], }, }); + // Add native USDC entries to the alias config so that getTokenConfig finds them. + const usdcConfig = inventoryConfig.tokenConfig[mainnetUsdc]; + usdcConfig[TOKEN_SYMBOLS_MAP.USDC.addresses[POLYGON]] = { + [POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }; + usdcConfig[TOKEN_SYMBOLS_MAP.USDC.addresses[ARBITRUM]] = { + [ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer }, + }; sampleDepositData = { depositId: bnZero, fromLiteChain: false, diff --git a/test/mocks/MockBundleDataApproxClient.ts b/test/mocks/MockBundleDataApproxClient.ts index 315524f5ae..b0dc513133 100644 --- a/test/mocks/MockBundleDataApproxClient.ts +++ b/test/mocks/MockBundleDataApproxClient.ts @@ -1,7 +1,7 @@ import { BundleDataApproxClient } from "../../src/clients"; import { Address, BigNumber, EvmAddress, toAddressType } from "../../src/utils"; -type TokenMapping = { [l1Token: string]: { [chainId: number]: string } }; +type TokenMapping = { [l1Token: string]: { [chainId: number]: string | string[] } }; export class MockBundleDataApproxClient extends BundleDataApproxClient { tokenMappings: TokenMapping | undefined = undefined; @@ -11,9 +11,13 @@ export class MockBundleDataApproxClient extends BundleDataApproxClient { override getL1TokenAddress(l2Token: Address, chainId: number): EvmAddress { if (this.tokenMappings) { - const tokenMapping = Object.entries(this.tokenMappings).find( - ([, mapping]) => mapping[chainId] === l2Token.toEvmAddress() - ); + const tokenMapping = Object.entries(this.tokenMappings).find(([, mapping]) => { + const mapped = mapping[chainId]; + if (Array.isArray(mapped)) { + return mapped.includes(l2Token.toEvmAddress()); + } + return mapped === l2Token.toEvmAddress(); + }); if (tokenMapping) { return toAddressType(tokenMapping[0], chainId); } @@ -28,8 +32,7 @@ export class MockBundleDataApproxClient extends BundleDataApproxClient { return super.getApproximateRefundsForToken(l1Token, fromBlocks); } - // Return the next starting block for each chain following the bundle end block of the last executed bundle that - // was relayed to that chain. + // Expose for unit testing override getUnexecutedBundleStartBlocks(l1Token: Address, requireExecution: boolean): { [chainId: number]: number } { return super.getUnexecutedBundleStartBlocks(l1Token, requireExecution); } diff --git a/test/mocks/MockInventoryClient.ts b/test/mocks/MockInventoryClient.ts index cff6e133ad..01669526ad 100644 --- a/test/mocks/MockInventoryClient.ts +++ b/test/mocks/MockInventoryClient.ts @@ -1,7 +1,7 @@ -import { Deposit, InventoryConfig } from "../../src/interfaces"; +import { Deposit, InventoryConfig, TokenInfo } from "../../src/interfaces"; import { HubPoolClient, InventoryClient, Rebalance, TokenClient } from "../../src/clients"; import { AdapterManager, CrossChainTransferClient } from "../../src/clients/bridges"; -import { BigNumber, bnZero, EvmAddress, toAddressType } from "../../src/utils"; +import { Address, BigNumber, bnZero, EvmAddress, getTokenInfo, toAddressType } from "../../src/utils"; import winston from "winston"; import { RebalancerClient } from "../../src/rebalancer/utils/interfaces"; @@ -42,6 +42,14 @@ export class MockInventoryClient extends InventoryClient { ); } + protected override getTokenInfo(token: Address, chainId: number): TokenInfo { + try { + return this.hubPoolClient.getTokenInfoForAddress(token, chainId); + } catch { + return getTokenInfo(token, chainId); + } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars override async determineRefundChainId(_deposit: Deposit): Promise { return this.inventoryConfig === null ? [1] : super.determineRefundChainId(_deposit); @@ -72,7 +80,7 @@ export class MockInventoryClient extends InventoryClient { } override getPossibleRebalances(): Rebalance[] { - return this.possibleRebalances; + return this.possibleRebalances.length > 0 ? this.possibleRebalances : super.getPossibleRebalances(); } setBalanceOnChainForL1Token(newBalance: BigNumber | undefined): void {