diff --git a/src/clients/BundleDataClient/utils/DataworkerUtils.ts b/src/clients/BundleDataClient/utils/DataworkerUtils.ts index 7a940553..e1173253 100644 --- a/src/clients/BundleDataClient/utils/DataworkerUtils.ts +++ b/src/clients/BundleDataClient/utils/DataworkerUtils.ts @@ -18,6 +18,7 @@ import { fixedPointAdjustment, count2DDictionaryValues, count3DDictionaryValues, + isDefined, } from "../../../utils"; import { addLastRunningBalance, @@ -160,8 +161,10 @@ export function _buildPoolRebalanceRoot( mainnetBundleEndBlock ); - updateRunningBalance(runningBalances, repaymentChainId, l1TokenCounterpart, totalRefundAmount); - updateRunningBalance(realizedLpFees, repaymentChainId, l1TokenCounterpart, totalRealizedLpFee); + if (isDefined(l1TokenCounterpart)) { + updateRunningBalance(runningBalances, repaymentChainId, l1TokenCounterpart, totalRefundAmount); + updateRunningBalance(realizedLpFees, repaymentChainId, l1TokenCounterpart, totalRealizedLpFee); + } } ); }); @@ -182,10 +185,12 @@ export function _buildPoolRebalanceRoot( destinationChainId, mainnetBundleEndBlock ); - const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); - updateRunningBalance(runningBalances, destinationChainId, l1TokenCounterpart, deposit.inputAmount.sub(lpFee)); - // Slow fill LP fees are accounted for when the slow fill executes and a V3FilledRelay is emitted. i.e. when - // the slow fill execution is included in bundleFillsV3. + if (isDefined(l1TokenCounterpart)) { + const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); + updateRunningBalance(runningBalances, destinationChainId, l1TokenCounterpart, deposit.inputAmount.sub(lpFee)); + // Slow fill LP fees are accounted for when the slow fill executes and a V3FilledRelay is emitted. i.e. when + // the slow fill execution is included in bundleFillsV3. + } }); }); }); @@ -206,10 +211,12 @@ export function _buildPoolRebalanceRoot( destinationChainId, mainnetBundleEndBlock ); - const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); - updateRunningBalance(runningBalances, destinationChainId, l1TokenCounterpart, lpFee.sub(deposit.inputAmount)); - // Slow fills don't add to lpFees, only when the slow fill is executed and a V3FilledRelay is emitted, so - // we don't need to subtract it here. Moreover, the HubPoole expects bundleLpFees to be > 0. + if (isDefined(l1TokenCounterpart)) { + const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); + updateRunningBalance(runningBalances, destinationChainId, l1TokenCounterpart, lpFee.sub(deposit.inputAmount)); + // Slow fills don't add to lpFees, only when the slow fill is executed and a V3FilledRelay is emitted, so + // we don't need to subtract it here. Moreover, the HubPoole expects bundleLpFees to be > 0. + } }); }); }); @@ -242,7 +249,9 @@ export function _buildPoolRebalanceRoot( originChainId, mainnetBundleEndBlock ); - updateRunningBalance(runningBalances, originChainId, l1TokenCounterpart, deposit.inputAmount); + if (isDefined(l1TokenCounterpart)) { + updateRunningBalance(runningBalances, originChainId, l1TokenCounterpart, deposit.inputAmount); + } }); }); }); diff --git a/src/clients/BundleDataClient/utils/FillUtils.ts b/src/clients/BundleDataClient/utils/FillUtils.ts index 7fb0c9df..50f92176 100644 --- a/src/clients/BundleDataClient/utils/FillUtils.ts +++ b/src/clients/BundleDataClient/utils/FillUtils.ts @@ -1,5 +1,5 @@ import { Fill } from "../../../interfaces"; -import { getBlockRangeForChain, isSlowFill } from "../../../utils"; +import { getBlockRangeForChain, isSlowFill, assert, isDefined } from "../../../utils"; import { HubPoolClient } from "../../HubPoolClient"; export function getRefundInformationFromFill( @@ -32,13 +32,15 @@ export function getRefundInformationFromFill( fill.inputToken, fill.originChainId, endBlockForMainnet - ); + )!; + assert(isDefined(l1TokenCounterpart), "There must be an l1 token counterpart for a filled deposit"); const repaymentToken = hubPoolClient.getL2TokenForL1TokenAtBlock( l1TokenCounterpart, chainToSendRefundTo, endBlockForMainnet - ); + )!; + assert(isDefined(repaymentToken), "There must be defined repayment token for a filled deposit"); return { chainToSendRefundTo, repaymentToken, diff --git a/src/clients/BundleDataClient/utils/PoolRebalanceUtils.ts b/src/clients/BundleDataClient/utils/PoolRebalanceUtils.ts index 01c6cbb4..1cc3996b 100644 --- a/src/clients/BundleDataClient/utils/PoolRebalanceUtils.ts +++ b/src/clients/BundleDataClient/utils/PoolRebalanceUtils.ts @@ -2,7 +2,7 @@ import { MerkleTree } from "@across-protocol/contracts/dist/utils/MerkleTree"; import { RunningBalances, PoolRebalanceLeaf, Clients, SpokePoolTargetBalance } from "../../../interfaces"; import { SpokePoolClient } from "../../SpokePoolClient"; import { BigNumber } from "ethers"; -import { bnZero, compareAddresses } from "../../../utils"; +import { bnZero, compareAddresses, isDefined } from "../../../utils"; import { HubPoolClient } from "../../HubPoolClient"; import { V3DepositWithBlock } from "./shims"; import { AcrossConfigStoreClient } from "../../AcrossConfigStoreClient"; @@ -171,7 +171,9 @@ export function updateRunningBalanceForDeposit( deposit.originChainId, deposit.quoteBlockNumber ); - updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart, updateAmount); + if (isDefined(l1TokenCounterpart)) { + updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart, updateAmount); + } } export function constructPoolRebalanceLeaves( diff --git a/src/clients/HubPoolClient.ts b/src/clients/HubPoolClient.ts index 4c2e3eca..605cdac1 100644 --- a/src/clients/HubPoolClient.ts +++ b/src/clients/HubPoolClient.ts @@ -31,7 +31,6 @@ import { fetchTokenInfo, getCachedBlockForTimestamp, getCurrentTime, - getNetworkName, isDefined, mapAsync, paginatedEventQuery, @@ -184,24 +183,15 @@ export class HubPoolClient extends BaseAbstractClient { l1Token: string, destinationChainId: number, latestHubBlock = Number.MAX_SAFE_INTEGER - ): string { + ): string | undefined { if (!this.l1TokensToDestinationTokensWithBlock?.[l1Token]?.[destinationChainId]) { - const chain = getNetworkName(destinationChainId); - const { symbol } = this.l1Tokens.find(({ address }) => address === l1Token) ?? { symbol: l1Token }; - throw new Error(`Could not find SpokePool mapping for ${symbol} on ${chain} and L1 token ${l1Token}`); + return undefined; } // Find the last mapping published before the target block. const l2Token: DestinationTokenWithBlock | undefined = sortEventsDescending( this.l1TokensToDestinationTokensWithBlock[l1Token][destinationChainId] ).find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= latestHubBlock); - if (!l2Token) { - const chain = getNetworkName(destinationChainId); - const { symbol } = this.l1Tokens.find(({ address }) => address === l1Token) ?? { symbol: l1Token }; - throw new Error( - `Could not find SpokePool mapping for ${symbol} on ${chain} at or before HubPool block ${latestHubBlock}!` - ); - } - return l2Token.l2Token; + return l2Token?.l2Token; } // Returns the latest L1 token to use for an L2 token as of the input hub block. @@ -209,7 +199,7 @@ export class HubPoolClient extends BaseAbstractClient { l2Token: string, destinationChainId: number, latestHubBlock = Number.MAX_SAFE_INTEGER - ): string { + ): string | undefined { const l2Tokens = Object.keys(this.l1TokensToDestinationTokensWithBlock) .filter((l1Token) => this.l2TokenEnabledForL1Token(l1Token, destinationChainId)) .map((l1Token) => { @@ -219,14 +209,9 @@ export class HubPoolClient extends BaseAbstractClient { ); }) .flat(); - if (l2Tokens.length === 0) { - const chain = getNetworkName(destinationChainId); - throw new Error( - `Could not find HubPool mapping for ${l2Token} on ${chain} at or before HubPool block ${latestHubBlock}!` - ); - } + // Find the last mapping published before the target block. - return sortEventsDescending(l2Tokens)[0].l1Token; + return sortEventsDescending(l2Tokens)[0]?.l1Token; } /** @@ -236,7 +221,9 @@ export class HubPoolClient extends BaseAbstractClient { * @param deposit Deposit event * @param returns string L1 token counterpart for Deposit */ - getL1TokenForDeposit(deposit: Pick): string { + getL1TokenForDeposit( + deposit: Pick + ): string | undefined { // L1-->L2 token mappings are set via PoolRebalanceRoutes which occur on mainnet, // so we use the latest token mapping. This way if a very old deposit is filled, the relayer can use the // latest L2 token mapping to find the L1 token counterpart. @@ -247,16 +234,18 @@ export class HubPoolClient extends BaseAbstractClient { * Returns the L2 token that should be used as a counterpart to a deposit event. For example, the caller * might want to know what the refund token will be on l2ChainId for the deposit event. * @param l2ChainId Chain where caller wants to get L2 token counterpart for - * @param event Deposit event + * @param event Deposit eventn * @returns string L2 token counterpart on l2ChainId */ getL2TokenForDeposit( deposit: Pick, l2ChainId = deposit.destinationChainId - ): string { + ): string | undefined { const l1Token = this.getL1TokenForDeposit(deposit); // Use the latest hub block number to find the L2 token counterpart. - return this.getL2TokenForL1TokenAtBlock(l1Token, l2ChainId, deposit.quoteBlockNumber); + return isDefined(l1Token) + ? this.getL2TokenForL1TokenAtBlock(l1Token, l2ChainId, deposit.quoteBlockNumber) + : undefined; } l2TokenEnabledForL1Token(l1Token: string, destinationChainId: number): boolean { @@ -378,11 +367,23 @@ export class HubPoolClient extends BaseAbstractClient { // Map SpokePool token addresses to HubPool token addresses. // Note: Should only be accessed via `getHubPoolToken()` or `getHubPoolTokens()`. const hubPoolTokens: { [k: string]: string } = {}; - const getHubPoolToken = (deposit: LpFeeRequest, quoteBlockNumber: number): string => { + const getHubPoolToken = (deposit: LpFeeRequest, quoteBlockNumber: number): string | undefined => { const tokenKey = `${deposit.originChainId}-${deposit.inputToken}`; - return (hubPoolTokens[tokenKey] ??= this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber })); + if (isDefined(hubPoolTokens[tokenKey])) { + return hubPoolTokens[tokenKey]; + } + const l1Token = this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber }); + if (!isDefined(l1Token)) { + this.logger.warn({ + at: "HubPoolClient", + message: `Unable to determine an appropriate L1 Token for deposit ${deposit}`, + }); + } + hubPoolTokens[tokenKey] = l1Token; + return l1Token; }; - const getHubPoolTokens = (): string[] => dedupArray(Object.values(hubPoolTokens)); + const getHubPoolTokens = (): string[] => + dedupArray(Object.values(hubPoolTokens)).filter((token) => isDefined(token)); // Helper to resolve the unqiue hubPoolToken & quoteTimestamp mappings. const resolveUniqueQuoteTimestamps = (deposit: LpFeeRequest): void => { @@ -391,6 +392,9 @@ export class HubPoolClient extends BaseAbstractClient { // Resolve the HubPool token address for this origin chainId/token pair, if it isn't already known. const quoteBlockNumber = quoteBlocks[quoteTimestamp]; const hubPoolToken = getHubPoolToken(deposit, quoteBlockNumber); + if (!isDefined(hubPoolToken)) { + return; + } // Append the quoteTimestamp for this HubPool token, if it isn't already enqueued. utilizationTimestamps[hubPoolToken] ??= []; @@ -431,11 +435,11 @@ export class HubPoolClient extends BaseAbstractClient { const { originChainId, paymentChainId, inputAmount, quoteTimestamp } = deposit; const quoteBlock = quoteBlocks[quoteTimestamp]; - if (paymentChainId === undefined) { + const hubPoolToken = getHubPoolToken(deposit, quoteBlock); + if (!isDefined(paymentChainId) || !isDefined(hubPoolToken)) { return { quoteBlock, realizedLpFeePct: bnZero }; } - const hubPoolToken = getHubPoolToken(deposit, quoteBlock); const rateModel = this.configStoreClient.getRateModelForBlockNumber( hubPoolToken, originChainId, @@ -495,13 +499,16 @@ export class HubPoolClient extends BaseAbstractClient { getL1TokenInfoForL2Token(l2Token: string, chainId: number): L1Token | undefined { const l1TokenCounterpart = this.getL1TokenForL2TokenAtBlock(l2Token, chainId, this.latestBlockSearched); - return this.getTokenInfoForL1Token(l1TokenCounterpart); + return isDefined(l1TokenCounterpart) ? this.getTokenInfoForL1Token(l1TokenCounterpart) : undefined; } getTokenInfoForDeposit(deposit: Deposit): L1Token | undefined { - return this.getTokenInfoForL1Token( - this.getL1TokenForL2TokenAtBlock(deposit.inputToken, deposit.originChainId, this.latestBlockSearched) + const l1Token = this.getL1TokenForL2TokenAtBlock( + deposit.inputToken, + deposit.originChainId, + this.latestBlockSearched ); + return isDefined(l1Token) ? this.getTokenInfoForL1Token(l1Token) : undefined; } getTokenInfo(chainId: number | string, tokenAddress: string): L1Token | undefined { @@ -524,10 +531,14 @@ export class HubPoolClient extends BaseAbstractClient { return false; } - // Resolve both HubPool tokens back to a current SpokePool token and verify that they match. - const _tokenA = this.getL2TokenForL1TokenAtBlock(l1TokenA, chainIdA, hubPoolBlock); - const _tokenB = this.getL2TokenForL1TokenAtBlock(l1TokenB, chainIdB, hubPoolBlock); - return tokenA === _tokenA && tokenB === _tokenB; + if (isDefined(l1TokenA) && isDefined(l1TokenB)) { + // Resolve both HubPool tokens back to a current SpokePool token and verify that they match. + const _tokenA = this.getL2TokenForL1TokenAtBlock(l1TokenA, chainIdA, hubPoolBlock); + const _tokenB = this.getL2TokenForL1TokenAtBlock(l1TokenB, chainIdB, hubPoolBlock); + return tokenA === _tokenA && tokenB === _tokenB; + } else { + return false; + } } catch { return false; // One or both input tokens were not recognised. } diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 75c08286..ebb70eae 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -769,7 +769,8 @@ export class SpokePoolClient extends BaseAbstractClient { return ZERO_ADDRESS; } - return this.hubPoolClient.getL2TokenForDeposit(deposit); + // If there is no l2 token for the deposit also return the zero address. + return this.hubPoolClient.getL2TokenForDeposit(deposit) ?? ZERO_ADDRESS; } /** diff --git a/src/clients/mocks/MockHubPoolClient.ts b/src/clients/mocks/MockHubPoolClient.ts index ca35d307..6ab37631 100644 --- a/src/clients/mocks/MockHubPoolClient.ts +++ b/src/clients/mocks/MockHubPoolClient.ts @@ -100,7 +100,7 @@ export class MockHubPoolClient extends HubPoolClient { this.spokePoolTokens[l1Token][chainId] = l2Token; } - getL1TokenForL2TokenAtBlock(l2Token: string, chainId: number, blockNumber: number): string { + getL1TokenForL2TokenAtBlock(l2Token: string, chainId: number, blockNumber: number): string | undefined { const l1Token = Object.keys(this.spokePoolTokens).find( (l1Token) => this.spokePoolTokens[l1Token]?.[chainId] === l2Token ); diff --git a/test/HubPoolClient.DepositToDestinationToken.ts b/test/HubPoolClient.DepositToDestinationToken.ts index ad900cee..2b52d961 100644 --- a/test/HubPoolClient.DepositToDestinationToken.ts +++ b/test/HubPoolClient.DepositToDestinationToken.ts @@ -47,16 +47,12 @@ describe("HubPoolClient: Deposit to Destination Token", function () { }); it("Gets L2 token counterpart", async function () { - expect(() => hubPoolClient.getL2TokenForL1TokenAtBlock(randomL1Token, destinationChainId, 0)).to.throw( - /Could not find SpokePool mapping/ - ); + expect(hubPoolClient.getL2TokenForL1TokenAtBlock(randomL1Token, destinationChainId, 0)).to.be.undefined; const e1 = hubPoolClient.setPoolRebalanceRoute(destinationChainId, randomL1Token, randomDestinationToken); await hubPoolClient.update(); // If input hub pool block is before all events, should throw. - expect(() => hubPoolClient.getL2TokenForL1TokenAtBlock(randomL1Token, destinationChainId, 0)).to.throw( - /Could not find SpokePool mapping/ - ); + expect(hubPoolClient.getL2TokenForL1TokenAtBlock(randomL1Token, destinationChainId, 0)).to.be.undefined; expect(hubPoolClient.getL2TokenForL1TokenAtBlock(randomL1Token, destinationChainId, e1.blockNumber)).to.equal( randomDestinationToken ); @@ -73,16 +69,12 @@ describe("HubPoolClient: Deposit to Destination Token", function () { ); }); it("Gets L1 token counterpart", async function () { - expect(() => hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, destinationChainId, 0)).to.throw( - /Could not find HubPool mapping/ - ); + expect(hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, destinationChainId, 0)).to.be.undefined; const e1 = hubPoolClient.setPoolRebalanceRoute(destinationChainId, randomL1Token, randomDestinationToken); await hubPoolClient.update(); - // If input hub pool block is before all events, should throw. - expect(() => hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, destinationChainId, 0)).to.throw( - /Could not find HubPool mapping/ - ); + // If input hub pool block is before all events, should resolve no l1 token. + expect(hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, destinationChainId, 0)).to.be.undefined; expect( hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, destinationChainId, e1.blockNumber) ).to.equal(randomL1Token); @@ -98,13 +90,11 @@ describe("HubPoolClient: Deposit to Destination Token", function () { hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, destinationChainId, e1.blockNumber) ).to.equal(randomL1Token); - // If L2 token mapping doesn't exist, throw. - expect(() => hubPoolClient.getL1TokenForL2TokenAtBlock(randomL1Token, destinationChainId, e2.blockNumber)).to.throw( - /Could not find HubPool mapping/ - ); - expect(() => - hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, originChainId, e2.blockNumber) - ).to.throw(/Could not find HubPool mapping/); + // If L2 token mapping doesn't exist, return undefined. + expect(hubPoolClient.getL1TokenForL2TokenAtBlock(randomL1Token, destinationChainId, e2.blockNumber)).to.be + .undefined; + expect(hubPoolClient.getL1TokenForL2TokenAtBlock(randomDestinationToken, originChainId, e2.blockNumber)).to.be + .undefined; }); it("Gets L1 token for deposit", async function () { const depositData = { @@ -119,18 +109,15 @@ describe("HubPoolClient: Deposit to Destination Token", function () { ); // quote block too early - expect(() => hubPoolClient.getL1TokenForDeposit({ ...depositData, quoteBlockNumber: 0 })).to.throw( - /Could not find HubPool mapping/ - ); - + expect(hubPoolClient.getL1TokenForDeposit({ ...depositData, quoteBlockNumber: 0 })).to.be.undefined; // no deposit with matching origin token - expect(() => + expect( hubPoolClient.getL1TokenForDeposit({ ...depositData, inputToken: randomL1Token, quoteBlockNumber: e0.blockNumber, }) - ).to.throw(/Could not find HubPool mapping/); + ).to.be.undefined; const e1 = hubPoolClient.setPoolRebalanceRoute(originChainId, randomOriginToken, randomOriginToken); await hubPoolClient.update(); @@ -152,24 +139,22 @@ describe("HubPoolClient: Deposit to Destination Token", function () { ).to.equal(randomDestinationToken); // origin chain token is set but none for destination chain yet, as of e0. - expect(() => - hubPoolClient.getL2TokenForDeposit({ ...depositData, destinationChainId, quoteBlockNumber: e0.blockNumber }) - ).to.throw(/Could not find SpokePool mapping/); + expect(hubPoolClient.getL2TokenForDeposit({ ...depositData, destinationChainId, quoteBlockNumber: e0.blockNumber })) + .to.be.undefined; // quote block too early - expect(() => - hubPoolClient.getL2TokenForDeposit({ ...depositData, destinationChainId, quoteBlockNumber: 0 }) - ).to.throw(/Could not find HubPool mapping/); + expect(hubPoolClient.getL2TokenForDeposit({ ...depositData, destinationChainId, quoteBlockNumber: 0 })).to.be + .undefined; // No deposit with matching token. - expect(() => + expect( hubPoolClient.getL2TokenForDeposit({ ...depositData, destinationChainId, inputToken: randomL1Token, quoteBlockNumber: e0.blockNumber, }) - ).to.throw(/Could not find HubPool mapping/); + ).to.be.undefined; const e2 = hubPoolClient.setPoolRebalanceRoute(destinationChainId, randomL1Token, randomL1Token); await hubPoolClient.update();