diff --git a/package.json b/package.json index a793fbcdf..3d43f5b57 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "relayer-v2", - "version": "0.1.1", - "description": "Across Protocol V2 Relayer Bot", + "version": "0.2.0", + "description": "Across Protocol V3 Relayer Bot", "repository": "git@github.com:across-protocol/relayer-v2.git", "author": "UMA Team", "license": "AGPL-3.0-only", diff --git a/src/clients/BundleDataClient.ts b/src/clients/BundleDataClient.ts index 69e2b39ae..a1c38103a 100644 --- a/src/clients/BundleDataClient.ts +++ b/src/clients/BundleDataClient.ts @@ -1,10 +1,8 @@ import * as _ from "lodash"; import { - FillsToRefund, ProposedRootBundle, SlowFillRequestWithBlock, SpokePoolClientsByChain, - UnfilledDepositsForOriginChain, V3DepositWithBlock, V3FillWithBlock, FillType, @@ -17,14 +15,7 @@ import { winston, BigNumber, bnZero, - assignValidFillToFillsToRefund, getRefundInformationFromFill, - updateTotalRefundAmount, - updateTotalRealizedLpFeePct, - flattenAndFilterUnfilledDepositsByOriginChain, - updateUnfilledDepositsWithMatchedDeposit, - getUniqueDepositsInRange, - getUniqueEarlyDepositsInRange, queryHistoricalDepositForFill, assign, assert, @@ -36,13 +27,12 @@ import { getBlockRangeForChain, getImpliedBundleBlockRanges, getEndBlockBuffers, - prettyPrintSpokePoolEvents, prettyPrintV3SpokePoolEvents, getRefundsFromBundle, CombinedRefunds, } from "../dataworker/DataworkerUtils"; import { getWidestPossibleExpectedBlockRange, isChainDisabled } from "../dataworker/PoolRebalanceUtils"; -import { typechain, utils } from "@across-protocol/sdk-v2"; +import { utils } from "@across-protocol/sdk-v2"; import { BundleDepositsV3, BundleExcessSlowFills, @@ -203,12 +193,12 @@ export class BundleDataClient { this.clients.configStoreClient, bundle ); - const { fillsToRefund, bundleFillsV3, expiredDepositsToRefundV3 } = await this.loadData( + const { bundleFillsV3, expiredDepositsToRefundV3 } = await this.loadData( bundleEvaluationBlockRanges, this.spokePoolClients, false ); - const combinedRefunds = getRefundsFromBundle(bundleFillsV3, fillsToRefund, expiredDepositsToRefundV3); + const combinedRefunds = getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3); // The latest proposed bundle's refund leaves might have already been partially or entirely executed. // We have to deduct the executed amounts from the total refund amounts. @@ -238,18 +228,61 @@ export class BundleDataClient { ); // Refunds that will be processed in the next bundle that will be proposed after the current pending bundle // (if any) has been fully executed. - const { fillsToRefund, bundleFillsV3, expiredDepositsToRefundV3 } = await this.loadData( + const { bundleFillsV3, expiredDepositsToRefundV3 } = await this.loadData( futureBundleEvaluationBlockRanges, this.spokePoolClients, false ); - return getRefundsFromBundle(bundleFillsV3, fillsToRefund, expiredDepositsToRefundV3); + return getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3); + } + + getExecutedRefunds( + spokePoolClient: SpokePoolClient, + relayerRefundRoot: string + ): { + [tokenAddress: string]: { + [relayer: string]: BigNumber; + }; + } { + // @dev Search from right to left since there can be multiple root bundles with the same relayer refund root. + // The caller should take caution if they're trying to use this function to find matching refunds for older + // root bundles as opposed to more recent ones. + const bundle = _.findLast( + spokePoolClient.getRootBundleRelays(), + (bundle) => bundle.relayerRefundRoot === relayerRefundRoot + ); + if (bundle === undefined) { + return {}; + } + + const executedRefundLeaves = spokePoolClient + .getRelayerRefundExecutions() + .filter((leaf) => leaf.rootBundleId === bundle.rootBundleId); + const executedRefunds: { [tokenAddress: string]: { [relayer: string]: BigNumber } } = {}; + for (const refundLeaf of executedRefundLeaves) { + const tokenAddress = refundLeaf.l2TokenAddress; + if (executedRefunds[tokenAddress] === undefined) { + executedRefunds[tokenAddress] = {}; + } + const executedTokenRefunds = executedRefunds[tokenAddress]; + + for (let i = 0; i < refundLeaf.refundAddresses.length; i++) { + const relayer = refundLeaf.refundAddresses[i]; + const refundAmount = refundLeaf.refundAmounts[i]; + if (executedTokenRefunds[relayer] === undefined) { + executedTokenRefunds[relayer] = bnZero; + } + executedTokenRefunds[relayer] = executedTokenRefunds[relayer].add(refundAmount); + } + } + return executedRefunds; } deductExecutedRefunds(allRefunds: CombinedRefunds, bundleContainingRefunds: ProposedRootBundle): CombinedRefunds { for (const chainIdStr of Object.keys(allRefunds)) { const chainId = Number(chainIdStr); - const executedRefunds = this.spokePoolClients[chainId].getExecutedRefunds( + const executedRefunds = this.getExecutedRefunds( + this.spokePoolClients[chainId], bundleContainingRefunds.relayerRefundRoot ); @@ -327,14 +360,6 @@ export class BundleDataClient { ); } - const unfilledDepositsForOriginChain: UnfilledDepositsForOriginChain = {}; - const fillsToRefund: FillsToRefund = {}; - const allRelayerRefunds: { repaymentChain: number; repaymentToken: string }[] = []; - const deposits: V2DepositWithBlock[] = []; - const allValidFills: V2FillWithBlock[] = []; - const allInvalidFills: V2FillWithBlock[] = []; - const earlyDeposits: typechain.FundsDepositedEvent[] = []; - // V3 specific objects: const bundleDepositsV3: BundleDepositsV3 = {}; // Deposits in bundle block range. const bundleFillsV3: BundleFillsV3 = {}; // Fills to refund in bundle block range. @@ -349,89 +374,6 @@ export class BundleDataClient { // (2) the fill deadline has passed. We'll need to decrement running balances for these deposits on the // destination chain where the slow fill would have been executed. - // Save refund in-memory for validated fill. - const addRefundForValidFill = ( - fillWithBlock: V2FillWithBlock, - matchedDeposit: V2DepositWithBlock, - blockRangeForChain: number[] - ) => { - // Extra check for duplicate fills. These should be blocked at the contract level but might still be included - // by the RPC so its worth checking here. - const duplicateFill = allValidFills.find( - (existingFill) => - existingFill.originChainId === fillWithBlock.originChainId && - existingFill.depositId === fillWithBlock.depositId && - utils.getTotalFilledAmount(existingFill).eq(utils.getTotalFilledAmount(fillWithBlock)) - ); - if (duplicateFill !== undefined) { - this.logger.warn({ - at: "BundleDataClient#loadData", - message: "Tried to add refund for duplicate fill. Skipping.", - duplicateFill, - matchedDeposit, - }); - return; - } - // Fill was validated. Save it under all validated fills list with the block number so we can sort it by - // time. Note that its important we don't skip fills earlier than the block range at this step because - // we use allValidFills to find the first fill in the entire history associated with a fill in the block - // range, in order to determine if we already sent a slow fill for it. - allValidFills.push(fillWithBlock); - - // If fill is outside block range, we can skip it now since we're not going to add a refund for it. - if (fillWithBlock.blockNumber < blockRangeForChain[0]) { - return; - } - - // Now create a copy of fill with block data removed, and use its data to update the fills to refund obj. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { blockNumber, transactionIndex, transactionHash, logIndex, ...fill } = fillWithBlock; - const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill( - fill, - this.clients.hubPoolClient, - blockRangesForChains, - chainIds - ); - - // Fills to refund includes both slow and non-slow fills and they both should increase the - // total realized LP fee %. - assignValidFillToFillsToRefund(fillsToRefund, fill, chainToSendRefundTo, repaymentToken); - allRelayerRefunds.push({ repaymentToken, repaymentChain: chainToSendRefundTo }); - - updateTotalRealizedLpFeePct(fillsToRefund, fill, chainToSendRefundTo, repaymentToken); - - // Save deposit as one that is eligible for a slow fill, since there is a fill - // for the deposit in this epoch. We save whether this fill is the first fill for the deposit, because - // if a deposit has its first fill in this block range, then we can send a slow fill payment to complete - // the deposit. If other fills end up completing this deposit, then we'll remove it from the unfilled - // deposits later. - updateUnfilledDepositsWithMatchedDeposit(fill, matchedDeposit, unfilledDepositsForOriginChain); - - // Update total refund counter for convenience when constructing relayer refund leaves - updateTotalRefundAmount(fillsToRefund, fill, chainToSendRefundTo, repaymentToken); - }; - - const validateFillAndSaveData = async (fill: V2FillWithBlock, blockRangeForChain: number[]): Promise => { - const originClient = spokePoolClients[fill.originChainId]; - const matchedDeposit = originClient.getDepositForFill(fill); - if (matchedDeposit) { - assert(utils.isV2Deposit(matchedDeposit)); - addRefundForValidFill(fill, matchedDeposit, blockRangeForChain); - } else { - // Matched deposit for fill was not found in spoke client. This situation should be rare so let's - // send some extra RPC requests to blocks older than the spoke client's initial event search config - // to find the deposit if it exists. - const spokePoolClient = spokePoolClients[fill.originChainId]; - const historicalDeposit = await queryHistoricalDepositForFill(spokePoolClient, fill); - if (historicalDeposit.found) { - assert(utils.isV2Deposit(historicalDeposit.deposit)); - addRefundForValidFill(fill, historicalDeposit.deposit, blockRangeForChain); - } else { - allInvalidFills.push(fill); - } - } - }; - const _isChainDisabled = (chainId: number): boolean => { const blockRangeForChain = getBlockRangeForChain(blockRangesForChains, chainId, chainIds); return isChainDisabled(blockRangeForChain); @@ -469,71 +411,6 @@ export class BundleDataClient { bundleBlockTimestamps = _cachedBundleTimestamps; } - /** ***************************** - * - * Handle LEGACY events - * - * *****************************/ - for (const originChainId of allChainIds) { - if (_isChainDisabled(originChainId)) { - continue; - } - - const originClient = spokePoolClients[originChainId]; - - // Loop over all other SpokePoolClient's to find deposits whose destination chain is the selected origin chain. - for (const destinationChainId of allChainIds) { - if (originChainId === destinationChainId) { - continue; - } - if (_isChainDisabled(destinationChainId)) { - continue; - } - - const destinationClient = spokePoolClients[destinationChainId]; - - // Store all deposits in range, for use in constructing a pool rebalance root. Save deposits with - // their quote time block numbers so we can pull the L1 token counterparts for the quote timestamp. - // We can safely filter `deposits` by the bundle block range because its only used to decrement running - // balances in the pool rebalance root. This array is NOT used when matching fills with deposits. For that, - // we use the wider event search config of the origin client. - deposits.push( - ...getUniqueDepositsInRange( - blockRangesForChains, - Number(originChainId), - Number(destinationChainId), - chainIds, - originClient, - deposits - ) - ); - - // TODO: replace this logic with something more clear where all deposits can be queried at once, - // but separated into early and not after the initial filter/query. - earlyDeposits.push( - ...getUniqueEarlyDepositsInRange( - blockRangesForChains, - Number(originChainId), - Number(destinationChainId), - chainIds, - originClient, - earlyDeposits - ) - ); - - const blockRangeForChain = getBlockRangeForChain(blockRangesForChains, Number(destinationChainId), chainIds); - - // Find all valid fills matching a deposit on the origin chain and sent on the destination chain. - // Don't include any fills past the bundle end block for the chain, otherwise the destination client will - // return fill events that are younger than the bundle end block. - const fillsForOriginChain = destinationClient - .getFillsForOriginChain(Number(originChainId)) - .filter((fillWithBlock) => fillWithBlock.blockNumber <= blockRangeForChain[1]) - .filter(utils.isV2Fill); - await Promise.all(fillsForOriginChain.map((fill) => validateFillAndSaveData(fill, blockRangeForChain))); - } - } - /** ***************************** * * Handle V3 events @@ -720,10 +597,11 @@ export class BundleDataClient { // older deposit in case the spoke pool client's lookback isn't old enough to find the matching deposit. if (fill.blockNumber >= destinationChainBlockRange[0]) { const historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); - if (!historicalDeposit.found || !utils.isV3Deposit(historicalDeposit.deposit)) { + if (!historicalDeposit.found) { bundleInvalidFillsV3.push(fill); } else { - const matchedDeposit: V3DepositWithBlock = historicalDeposit.deposit; + assert(utils.isV3Deposit(historicalDeposit.deposit)); + const matchedDeposit = historicalDeposit.deposit; // @dev Since queryHistoricalDepositForFill validates the fill by checking individual // object property values against the deposit's, we // sanity check it here by comparing the full relay hashes. If there's an error here then the @@ -1012,22 +890,6 @@ export class BundleDataClient { updateBundleExcessSlowFills(unexecutableSlowFills, { ...deposit, realizedLpFeePct }); }); - // Note: We do not check for duplicate slow fills here since `addRefundForValidFill` already checks for duplicate - // fills and is the function that populates the `unfilledDeposits` dictionary. Therefore, if there are no duplicate - // fills, then there won't be duplicate `matchedDeposits` used to populate `unfilledDeposits`. - // For each deposit with a matched fill, figure out the unfilled amount that we need to slow relay. We will filter - // out any deposits that are fully filled. - const unfilledDeposits = flattenAndFilterUnfilledDepositsByOriginChain(unfilledDepositsForOriginChain); - - const spokeEventsReadable = prettyPrintSpokePoolEvents( - blockRangesForChains, - chainIds, - deposits, - allValidFills, - allRelayerRefunds, - unfilledDeposits, - allInvalidFills - ); const v3SpokeEventsReadable = prettyPrintV3SpokePoolEvents( bundleDepositsV3, bundleFillsV3, @@ -1038,12 +900,6 @@ export class BundleDataClient { ); if (logData) { const mainnetRange = getBlockRangeForChain(blockRangesForChains, this.clients.hubPoolClient.chainId, chainIds); - this.logger.debug({ - at: "BundleDataClient#loadData", - message: `Finished loading spoke pool data for the equivalent of mainnet range: [${mainnetRange[0]}, ${mainnetRange[1]}]`, - blockRangesForChains, - ...spokeEventsReadable, - }); this.logger.debug({ at: "BundleDataClient#loadData", message: `Finished loading V3 spoke pool data for the equivalent of mainnet range: [${mainnetRange[0]}, ${mainnetRange[1]}]`, @@ -1052,16 +908,6 @@ export class BundleDataClient { }); } - if (Object.keys(spokeEventsReadable.allInvalidFillsInRangeByDestinationChain).length > 0) { - this.logger.debug({ - at: "BundleDataClient#loadData", - message: "Finished loading spoke pool data and found some invalid fills in range", - blockRangesForChains, - allInvalidFillsInRangeByDestinationChain: spokeEventsReadable.allInvalidFillsInRangeByDestinationChain, - allInvalidFills, - }); - } - if (bundleInvalidFillsV3.length > 0) { this.logger.debug({ at: "BundleDataClient#loadData", @@ -1072,11 +918,6 @@ export class BundleDataClient { } return { - fillsToRefund, - deposits, - unfilledDeposits, - allValidFills, - earlyDeposits, bundleDepositsV3, expiredDepositsToRefundV3, bundleFillsV3, diff --git a/src/dataworker/Dataworker.ts b/src/dataworker/Dataworker.ts index 7d53c8c31..1afeb08c7 100644 --- a/src/dataworker/Dataworker.ts +++ b/src/dataworker/Dataworker.ts @@ -8,29 +8,23 @@ import { sortEventsDescending, BigNumber, getNetworkName, - getRefund, MerkleTree, sortEventsAscending, isDefined, toBNWei, - getFillsInRange, ZERO_ADDRESS, } from "../utils"; import { - FillsToRefund, ProposedRootBundle, RootBundleRelayWithBlock, - SlowFillLeaf, SpokePoolClientsByChain, - UnfilledDeposit, PendingRootBundle, RunningBalances, PoolRebalanceLeaf, RelayerRefundLeaf, - V2SlowFillLeaf, V3SlowFillLeaf, - V2DepositWithBlock, V2FillWithBlock, + V3FillWithBlock, } from "../interfaces"; import { DataworkerClients } from "./DataworkerClientHelper"; import { SpokePoolClient, BalanceAllocator } from "../clients"; @@ -66,9 +60,9 @@ const IGNORE_DISPUTE_REASONS = new Set(["bundle-end-block-buffer"]); const ERROR_DISPUTE_REASONS = new Set(["insufficient-dataworker-lookback", "out-of-date-config-store-version"]); // Create a type for storing a collection of roots -type RootBundle = { - leaves: SlowFillLeaf[]; - tree: MerkleTree; +type SlowRootBundle = { + leaves: V3SlowFillLeaf[]; + tree: MerkleTree; }; export type BundleDataToPersistToDALayerType = { @@ -85,8 +79,8 @@ type ProposeRootBundleReturnType = { poolRebalanceTree: MerkleTree; relayerRefundLeaves: RelayerRefundLeaf[]; relayerRefundTree: MerkleTree; - slowFillLeaves: SlowFillLeaf[]; - slowFillTree: MerkleTree; + slowFillLeaves: V3SlowFillLeaf[]; + slowFillTree: MerkleTree; dataToPersistToDALayer: BundleDataToPersistToDALayerType; }; @@ -148,12 +142,9 @@ export class Dataworker { async buildSlowRelayRoot( blockRangesForChains: number[][], spokePoolClients: { [chainId: number]: SpokePoolClient } - ): Promise { - const { unfilledDeposits, bundleSlowFillsV3 } = await this.clients.bundleDataClient.loadData( - blockRangesForChains, - spokePoolClients - ); - return _buildSlowRelayRoot(unfilledDeposits, bundleSlowFillsV3); + ): Promise { + const { bundleSlowFillsV3 } = await this.clients.bundleDataClient.loadData(blockRangesForChains, spokePoolClients); + return _buildSlowRelayRoot(bundleSlowFillsV3); } async buildRelayerRefundRoot( @@ -171,7 +162,7 @@ export class Dataworker { this.chainIdListForBundleEvaluationBlockNumbers )[1]; - const { fillsToRefund, bundleFillsV3, expiredDepositsToRefundV3 } = await this.clients.bundleDataClient.loadData( + const { bundleFillsV3, expiredDepositsToRefundV3 } = await this.clients.bundleDataClient.loadData( blockRangesForChains, spokePoolClients ); @@ -180,7 +171,6 @@ export class Dataworker { : this.clients.configStoreClient.getMaxRefundCountForRelayerRefundLeafForBlock(endBlockForMainnet); return _buildRelayerRefundRoot( endBlockForMainnet, - fillsToRefund, bundleFillsV3, expiredDepositsToRefundV3, poolRebalanceLeaves, @@ -196,47 +186,24 @@ export class Dataworker { spokePoolClients: SpokePoolClientsByChain, latestMainnetBlock?: number ): Promise { - const { - fillsToRefund, - deposits, - allValidFills, - unfilledDeposits, - earlyDeposits, - bundleDepositsV3, - bundleFillsV3, - bundleSlowFillsV3, - unexecutableSlowFills, - expiredDepositsToRefundV3, - } = await this.clients.bundleDataClient.loadData(blockRangesForChains, spokePoolClients); + const { bundleDepositsV3, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3 } = + await this.clients.bundleDataClient.loadData(blockRangesForChains, spokePoolClients); const mainnetBundleEndBlock = getBlockRangeForChain( blockRangesForChains, this.clients.hubPoolClient.chainId, this.chainIdListForBundleEvaluationBlockNumbers )[1]; - const allValidFillsInRange = getFillsInRange( - allValidFills, - blockRangesForChains, - this.chainIdListForBundleEvaluationBlockNumbers - ); return await this._getPoolRebalanceRoot( - spokePoolClients, blockRangesForChains, latestMainnetBlock ?? mainnetBundleEndBlock, mainnetBundleEndBlock, - fillsToRefund, - deposits, - allValidFills, - allValidFillsInRange, - unfilledDeposits, - earlyDeposits, bundleDepositsV3, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, - expiredDepositsToRefundV3, - true + expiredDepositsToRefundV3 ); } @@ -507,18 +474,8 @@ export class Dataworker { logData = false ): Promise { const timerStart = Date.now(); - const { - fillsToRefund, - deposits, - allValidFills, - unfilledDeposits, - earlyDeposits, - bundleDepositsV3, - bundleFillsV3, - bundleSlowFillsV3, - unexecutableSlowFills, - expiredDepositsToRefundV3, - } = await this.clients.bundleDataClient.loadData(blockRangesForProposal, spokePoolClients, logData); + const { bundleDepositsV3, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, expiredDepositsToRefundV3 } = + await this.clients.bundleDataClient.loadData(blockRangesForProposal, spokePoolClients, logData); // Prepare information about what we need to store to // Arweave for the bundle. We will be doing this at a // later point so that we can confirm that this data is @@ -531,31 +488,20 @@ export class Dataworker { unexecutableSlowFills, bundleSlowFillsV3, }; - const [mainnetBundleStartBlock, mainnetBundleEndBlock] = blockRangesForProposal[0]; - const chainIds = this.clients.configStoreClient.getChainIdIndicesForBlock(mainnetBundleStartBlock); - const allValidFillsInRange = getFillsInRange(allValidFills, blockRangesForProposal, chainIds); + const [, mainnetBundleEndBlock] = blockRangesForProposal[0]; const poolRebalanceRoot = await this._getPoolRebalanceRoot( - spokePoolClients, blockRangesForProposal, latestMainnetBundleEndBlock, mainnetBundleEndBlock, - fillsToRefund, - deposits, - allValidFills, - allValidFillsInRange, - unfilledDeposits, - earlyDeposits, bundleDepositsV3, bundleFillsV3, bundleSlowFillsV3, unexecutableSlowFills, - expiredDepositsToRefundV3, - true + expiredDepositsToRefundV3 ); const relayerRefundRoot = _buildRelayerRefundRoot( mainnetBundleEndBlock, - fillsToRefund, bundleFillsV3, expiredDepositsToRefundV3, poolRebalanceRoot.leaves, @@ -565,7 +511,7 @@ export class Dataworker { ? this.maxRefundCountOverride : this.clients.configStoreClient.getMaxRefundCountForRelayerRefundLeafForBlock(mainnetBundleEndBlock) ); - const slowRelayRoot = _buildSlowRelayRoot(unfilledDeposits, bundleSlowFillsV3); + const slowRelayRoot = _buildSlowRelayRoot(bundleSlowFillsV3); if (logData) { this.logger.debug({ @@ -698,8 +644,8 @@ export class Dataworker { leaves: RelayerRefundLeaf[]; }; slowRelayTree: { - tree: MerkleTree; - leaves: SlowFillLeaf[]; + tree: MerkleTree; + leaves: V3SlowFillLeaf[]; }; }; } @@ -717,8 +663,8 @@ export class Dataworker { leaves: RelayerRefundLeaf[]; }; slowRelayTree: { - tree: MerkleTree; - leaves: SlowFillLeaf[]; + tree: MerkleTree; + leaves: V3SlowFillLeaf[]; }; }; } @@ -1140,10 +1086,10 @@ export class Dataworker { } async _executeSlowFillLeaf( - _leaves: SlowFillLeaf[], + _leaves: V3SlowFillLeaf[], balanceAllocator: BalanceAllocator, client: SpokePoolClient, - slowRelayTree: MerkleTree, + slowRelayTree: MerkleTree, submitExecution: boolean, rootBundleId?: number ): Promise { @@ -1156,9 +1102,7 @@ export class Dataworker { // If there is a message, we ignore the leaf and log an error. if (!sdk.utils.isMessageEmpty(message)) { - const { method, args } = sdkUtils.isV2SlowFillLeaf(leaf) - ? this.encodeV2SlowFillLeaf(slowRelayTree, rootBundleId, leaf) - : this.encodeV3SlowFillLeaf(slowRelayTree, rootBundleId, leaf); + const { method, args } = this.encodeV3SlowFillLeaf(slowRelayTree, rootBundleId, leaf); this.logger.warn({ at: "Dataworker#_executeSlowFillLeaf", @@ -1193,9 +1137,9 @@ export class Dataworker { const chainId = client.chainId; - const sortedFills = client.getFills(); + const sortedFills = client.getFills().filter(sdkUtils.isV3Fill); const latestFills = leaves.map((slowFill) => { - const { relayData } = slowFill; + const { relayData, chainId: slowFillChainId } = slowFill; // Start with the most recent fills and search backwards. const fill = _.findLast(sortedFills, (fill) => { @@ -1203,33 +1147,12 @@ export class Dataworker { !( fill.depositId === relayData.depositId && fill.originChainId === relayData.originChainId && - fill.destinationChainId === sdkUtils.getSlowFillLeafChainId(slowFill) && - fill.depositor === relayData.depositor && - fill.recipient === relayData.recipient && - (sdkUtils.isV3Fill(fill) && sdkUtils.isV3RelayData(relayData) - ? fill.inputToken === relayData.inputToken - : true) && - sdkUtils.getFillOutputToken(fill) === sdkUtils.getRelayDataOutputToken(relayData) && - sdkUtils.getFillOutputAmount(fill).eq(sdkUtils.getRelayDataOutputAmount(relayData)) && - fill.message === relayData.message + sdkUtils.getV3RelayHash(fill, chainId) === sdkUtils.getV3RelayHash(relayData, slowFillChainId) ) ) { return false; } - - if (sdkUtils.isV2Fill(fill) && sdkUtils.isV2RelayData(relayData)) { - return fill.realizedLpFeePct.eq(relayData.realizedLpFeePct) && fill.relayerFeePct.eq(relayData.relayerFeePct); - } - - if (sdkUtils.isV3Fill(fill) && sdkUtils.isV3RelayData(relayData)) { - return ( - fill.fillDeadline === relayData.fillDeadline && - fill.exclusivityDeadline === relayData.exclusivityDeadline && - fill.exclusiveRelayer === relayData.exclusiveRelayer - ); - } - - return false; + return true; }); return fill; @@ -1244,26 +1167,16 @@ export class Dataworker { throw new Error(`Leaf chainId does not match input chainId (${destinationChainId} != ${chainId})`); } - const outputAmount = sdkUtils.getRelayDataOutputAmount(slowFill.relayData); + const { outputAmount } = slowFill.relayData; const fill = latestFills[idx]; - let amountRequired: BigNumber; - if (sdkUtils.isV3SlowFillLeaf(slowFill)) { - amountRequired = isDefined(fill) ? bnZero : slowFill.updatedOutputAmount; - } else { - // If the most recent fill is not found, just make the most conservative assumption: a 0-sized fill. - const totalFilledAmount = isDefined(fill) ? sdkUtils.getTotalFilledAmount(fill) : bnZero; - - // Note: the getRefund function just happens to perform the same math we need. - // A refund is the total fill amount minus LP fees, which is the same as the payout for a slow relay! - amountRequired = getRefund(outputAmount.sub(totalFilledAmount), slowFill.relayData.realizedLpFeePct); - } + const amountRequired = isDefined(fill) ? bnZero : slowFill.updatedOutputAmount; // If the fill has been completed there's no need to execute the slow fill leaf. if (amountRequired.eq(bnZero)) { return undefined; } - const outputToken = sdkUtils.getRelayDataOutputToken(slowFill.relayData); + const { outputToken } = slowFill.relayData; const success = await balanceAllocator.requestBalanceAllocation( destinationChainId, l2TokensToCountTowardsSpokePoolLeafExecutionCapital(outputToken, destinationChainId), @@ -1297,7 +1210,7 @@ export class Dataworker { assert(sdkUtils.getSlowFillLeafChainId(leaf) === chainId); const { relayData } = leaf; - const outputAmount = sdkUtils.getRelayDataOutputAmount(relayData); + const { outputAmount } = relayData; const mrkdwn = `rootBundleId: ${rootBundleId}\n` + `slowRelayRoot: ${slowRelayTree.getHexRoot()}\n` + @@ -1307,9 +1220,7 @@ export class Dataworker { `amount: ${outputAmount.toString()}`; if (submitExecution) { - const { method, args } = sdkUtils.isV2SlowFillLeaf(leaf) - ? this.encodeV2SlowFillLeaf(slowRelayTree, rootBundleId, leaf) - : this.encodeV3SlowFillLeaf(slowRelayTree, rootBundleId, leaf); + const { method, args } = this.encodeV3SlowFillLeaf(slowRelayTree, rootBundleId, leaf); this.clients.multiCallerClient.enqueueTransaction({ contract: client.spokePool, @@ -1334,35 +1245,8 @@ export class Dataworker { }); } - encodeV2SlowFillLeaf( - slowRelayTree: MerkleTree, - rootBundleId: number, - leaf: V2SlowFillLeaf - ): { method: string; args: (number | BigNumber | string | string[])[] } { - const { relayData, payoutAdjustmentPct } = leaf; - - const method = "executeSlowRelayLeaf"; - const proof = slowRelayTree.getHexProof({ relayData, payoutAdjustmentPct }); - const args = [ - relayData.depositor, - relayData.recipient, - relayData.destinationToken, - relayData.amount, - relayData.originChainId, - relayData.realizedLpFeePct, - relayData.relayerFeePct, - relayData.depositId, - rootBundleId, - relayData.message, - leaf.payoutAdjustmentPct, - proof, - ]; - - return { method, args }; - } - encodeV3SlowFillLeaf( - slowRelayTree: MerkleTree, + slowRelayTree: MerkleTree, rootBundleId: number, leaf: V3SlowFillLeaf ): { method: string; args: (number | string[] | V3SlowFillLeaf)[] } { @@ -2197,7 +2081,7 @@ export class Dataworker { poolRebalanceRoot: string, relayerRefundLeaves: RelayerRefundLeaf[], relayerRefundRoot: string, - slowRelayLeaves: SlowFillLeaf[], + slowRelayLeaves: V3SlowFillLeaf[], slowRelayRoot: string ): void { try { @@ -2253,22 +2137,14 @@ export class Dataworker { } async _getPoolRebalanceRoot( - spokePoolClients: SpokePoolClientsByChain, blockRangesForChains: number[][], latestMainnetBlock: number, mainnetBundleEndBlock: number, - fillsToRefund: FillsToRefund, - deposits: V2DepositWithBlock[], - allValidFills: V2FillWithBlock[], - allValidFillsInRange: V2FillWithBlock[], - unfilledDeposits: UnfilledDeposit[], - earlyDeposits: sdk.typechain.FundsDepositedEvent[], bundleV3Deposits: BundleDepositsV3, bundleV3Fills: BundleFillsV3, bundleSlowFills: BundleSlowFills, unexecutableSlowFills: BundleExcessSlowFills, - expiredDepositsToRefundV3: ExpiredDepositsToRefundV3, - logSlowFillExcessData = false + expiredDepositsToRefundV3: ExpiredDepositsToRefundV3 ): Promise { const key = JSON.stringify(blockRangesForChains); // FIXME: Temporary fix to disable root cache rebalancing and to keep the @@ -2278,21 +2154,13 @@ export class Dataworker { this.rootCache[key] = _buildPoolRebalanceRoot( latestMainnetBlock, mainnetBundleEndBlock, - fillsToRefund, - deposits, - allValidFills, - allValidFillsInRange, - unfilledDeposits, - earlyDeposits, bundleV3Deposits, bundleV3Fills, bundleSlowFills, unexecutableSlowFills, expiredDepositsToRefundV3, this.clients, - spokePoolClients, - this.maxL1TokenCountOverride, - logSlowFillExcessData ? this.logger : undefined + this.maxL1TokenCountOverride ); } diff --git a/src/dataworker/DataworkerClientHelper.ts b/src/dataworker/DataworkerClientHelper.ts index 42fe0d463..5d8f9cf0a 100644 --- a/src/dataworker/DataworkerClientHelper.ts +++ b/src/dataworker/DataworkerClientHelper.ts @@ -96,7 +96,6 @@ export async function constructSpokePoolClientsForFastDataworker( endBlocks ); await updateSpokePoolClients(spokePoolClients, [ - "FilledRelay", "EnabledDepositRoute", "RelayedRootBundle", "ExecutedRelayerRefundRoot", diff --git a/src/dataworker/DataworkerUtils.ts b/src/dataworker/DataworkerUtils.ts index 73cf2e911..577a101c5 100644 --- a/src/dataworker/DataworkerUtils.ts +++ b/src/dataworker/DataworkerUtils.ts @@ -1,20 +1,13 @@ import assert from "assert"; -import { utils, typechain, interfaces, caching } from "@across-protocol/sdk-v2"; +import { utils, interfaces, caching } from "@across-protocol/sdk-v2"; import { SpokePoolClient } from "../clients"; import { spokesThatHoldEthAndWeth } from "../common/Constants"; import { CONTRACT_ADDRESSES } from "../common/ContractAddresses"; import { - FillsToRefund, PoolRebalanceLeaf, RelayerRefundLeaf, RelayerRefundLeafWithGroup, RunningBalances, - SlowFillLeaf, - SpokePoolClientsByChain, - UnfilledDeposit, - V2DepositWithBlock, - V2FillWithBlock, - V2SlowFillLeaf, V3FillWithBlock, V3SlowFillLeaf, } from "../interfaces"; @@ -27,10 +20,7 @@ import { count2DDictionaryValues, count3DDictionaryValues, fixedPointAdjustment, - getDepositPath, - getFillsInRange, getTimestampsForBundleEndBlocks, - groupObjectCountsByProp, groupObjectCountsByTwoProps, isDefined, MerkleTree, @@ -40,13 +30,9 @@ import { PoolRebalanceRoot } from "./Dataworker"; import { DataworkerClients } from "./DataworkerClientHelper"; import { addLastRunningBalance, - addSlowFillsToRunningBalances, constructPoolRebalanceLeaves, - initializeRunningBalancesFromRelayerRepayments, - subtractExcessFromPreviousSlowFillsFromRunningBalances, updateRunningBalance, updateRunningBalanceForDeposit, - updateRunningBalanceForEarlyDeposit, } from "./PoolRebalanceUtils"; import { getAmountToReturnForRelayerRefundLeaf, @@ -157,51 +143,6 @@ export async function blockRangesAreInvalidForSpokeClients( }); } -export function prettyPrintSpokePoolEvents( - blockRangesForChains: number[][], - chainIdListForBundleEvaluationBlockNumbers: number[], - deposits: V2DepositWithBlock[], - allValidFills: V2FillWithBlock[], - allRelayerRefunds: { repaymentChain: number; repaymentToken: string }[], - unfilledDeposits: UnfilledDeposit[], - allInvalidFills: V2FillWithBlock[] -): AnyObject { - const allInvalidFillsInRange = getFillsInRange( - allInvalidFills, - blockRangesForChains, - chainIdListForBundleEvaluationBlockNumbers - ); - const allValidFillsInRange = getFillsInRange( - allValidFills, - blockRangesForChains, - chainIdListForBundleEvaluationBlockNumbers - ); - return { - depositsInRangeByOriginChain: groupObjectCountsByTwoProps(deposits, "originChainId", (deposit) => - getDepositPath(deposit) - ), - allValidFillsInRangeByDestinationChain: groupObjectCountsByTwoProps( - allValidFillsInRange, - "destinationChainId", - (fill) => `${fill.originChainId}-->${utils.getFillOutputToken(fill)}` - ), - fillsToRefundInRangeByRepaymentChain: groupObjectCountsByTwoProps( - allRelayerRefunds, - "repaymentChain", - (repayment) => repayment.repaymentToken - ), - unfilledDepositsByDestinationChain: groupObjectCountsByProp( - unfilledDeposits, - (unfilledDeposit) => unfilledDeposit.deposit.destinationChainId - ), - allInvalidFillsInRangeByDestinationChain: groupObjectCountsByTwoProps( - allInvalidFillsInRange, - "destinationChainId", - (fill) => `${fill.originChainId}-->${utils.getFillOutputToken(fill)}` - ), - }; -} - export function prettyPrintV3SpokePoolEvents( bundleDepositsV3: BundleDepositsV3, bundleFillsV3: BundleFillsV3, @@ -224,16 +165,11 @@ export function prettyPrintV3SpokePoolEvents( }; } -export function _buildSlowRelayRoot( - unfilledDeposits: UnfilledDeposit[], - bundleSlowFillsV3: BundleSlowFills -): { - leaves: SlowFillLeaf[]; - tree: MerkleTree; +export function _buildSlowRelayRoot(bundleSlowFillsV3: BundleSlowFills): { + leaves: V3SlowFillLeaf[]; + tree: MerkleTree; } { - const slowRelayLeaves: SlowFillLeaf[] = unfilledDeposits.map((deposit: UnfilledDeposit) => - buildV2SlowFillLeaf(deposit) - ); + const slowRelayLeaves: V3SlowFillLeaf[] = []; // Append V3 slow fills to the V2 leaf list Object.values(bundleSlowFillsV3).forEach((depositsForChain) => { @@ -262,29 +198,7 @@ export function _buildSlowRelayRoot( }; } -function buildV2SlowFillLeaf(unfilledDeposit: UnfilledDeposit): V2SlowFillLeaf { - const { deposit } = unfilledDeposit; - assert(utils.isV2Deposit(deposit)); - - return { - relayData: { - depositor: deposit.depositor, - recipient: deposit.recipient, - destinationToken: deposit.destinationToken, - amount: deposit.amount, - originChainId: deposit.originChainId, - destinationChainId: deposit.destinationChainId, - realizedLpFeePct: deposit.realizedLpFeePct, - relayerFeePct: deposit.relayerFeePct, - depositId: deposit.depositId, - message: deposit.message, - }, - payoutAdjustmentPct: unfilledDeposit.relayerBalancingFee?.toString() ?? "0", - }; -} - function buildV3SlowFillLeaf(deposit: interfaces.V3Deposit): V3SlowFillLeaf { - assert(utils.isV3Deposit(deposit)); const lpFee = deposit.inputAmount.mul(deposit.realizedLpFeePct).div(fixedPointAdjustment); return { @@ -317,7 +231,6 @@ export type CombinedRefunds = { // and expired deposits. export function getRefundsFromBundle( bundleFillsV3: BundleFillsV3, - fillsToRefund: FillsToRefund, expiredDepositsToRefundV3: ExpiredDepositsToRefundV3 ): CombinedRefunds { const combinedRefunds: { @@ -359,31 +272,11 @@ export function getRefundsFromBundle( }); }); }); - Object.entries(fillsToRefund).forEach(([repaymentChainId, fillsForChain]) => { - combinedRefunds[repaymentChainId] ??= {}; - Object.entries(fillsForChain).forEach(([l2TokenAddress, { refunds }]) => { - // refunds can be undefined if these fills were all slow fill executions. - if (refunds === undefined) { - return; - } - if (combinedRefunds[repaymentChainId][l2TokenAddress] === undefined) { - combinedRefunds[repaymentChainId][l2TokenAddress] = refunds; - } else { - Object.entries(refunds).forEach(([refundAddress, refundAmount]) => { - const existingRefundAmount = combinedRefunds[repaymentChainId][l2TokenAddress][refundAddress]; - combinedRefunds[repaymentChainId][l2TokenAddress][refundAddress] = refundAmount.add( - existingRefundAmount ?? bnZero - ); - }); - } - }); - }); return combinedRefunds; } export function _buildRelayerRefundRoot( endBlockForMainnet: number, - fillsToRefund: FillsToRefund, bundleFillsV3: BundleFillsV3, expiredDepositsToRefundV3: ExpiredDepositsToRefundV3, poolRebalanceLeaves: PoolRebalanceLeaf[], @@ -396,7 +289,7 @@ export function _buildRelayerRefundRoot( } { const relayerRefundLeaves: RelayerRefundLeafWithGroup[] = []; - const combinedRefunds = getRefundsFromBundle(bundleFillsV3, fillsToRefund, expiredDepositsToRefundV3); + const combinedRefunds = getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3); // We'll construct a new leaf for each { repaymentChainId, L2TokenAddress } unique combination. Object.entries(combinedRefunds).forEach(([_repaymentChainId, refundsForChain]) => { @@ -493,21 +386,13 @@ export function _buildRelayerRefundRoot( export async function _buildPoolRebalanceRoot( latestMainnetBlock: number, mainnetBundleEndBlock: number, - fillsToRefund: FillsToRefund, - deposits: V2DepositWithBlock[], - allValidFills: V2FillWithBlock[], - allValidFillsInRange: V2FillWithBlock[], - unfilledDeposits: UnfilledDeposit[], - earlyDeposits: typechain.FundsDepositedEvent[], bundleV3Deposits: BundleDepositsV3, bundleFillsV3: BundleFillsV3, bundleSlowFillsV3: BundleSlowFills, unexecutableSlowFills: BundleExcessSlowFills, expiredDepositsToRefundV3: ExpiredDepositsToRefundV3, clients: DataworkerClients, - spokePoolClients: SpokePoolClientsByChain, - maxL1TokenCountOverride: number | undefined, - logger?: winston.Logger + maxL1TokenCountOverride: number | undefined ): Promise { // Running balances are the amount of tokens that we need to send to each SpokePool to pay for all instant and // slow relay refunds. They are decreased by the amount of funds already held by the SpokePool. Balances are keyed @@ -524,14 +409,6 @@ export async function _buildPoolRebalanceRoot( * REFUNDS FOR FAST FILLS */ - initializeRunningBalancesFromRelayerRepayments( - runningBalances, - realizedLpFees, - mainnetBundleEndBlock, - clients.hubPoolClient, - fillsToRefund - ); - // Add running balances and lp fees for v3 relayer refunds using BundleDataClient.bundleFillsV3. Refunds // should be equal to inputAmount - lpFees so that relayers get to keep the relayer fee. Add the refund amount // to the running balance for the repayment chain. @@ -555,9 +432,6 @@ export async function _buildPoolRebalanceRoot( * PAYMENTS SLOW FILLS */ - // Add payments to execute slow fills. - addSlowFillsToRunningBalances(mainnetBundleEndBlock, runningBalances, clients.hubPoolClient, unfilledDeposits); - // Add running balances and lp fees for v3 slow fills using BundleDataClient.bundleSlowFillsV3. // Slow fills should still increment bundleLpFees and updatedOutputAmount should be equal to inputAmount - lpFees. // Increment the updatedOutputAmount to the destination chain. @@ -582,18 +456,6 @@ export async function _buildPoolRebalanceRoot( * EXCESSES FROM UNEXECUTABLE SLOW FILLS */ - // For certain fills associated with another partial fill from a previous root bundle, we need to adjust running - // balances because the prior partial fill would have triggered a refund to be sent to the spoke pool to refund - // a slow fill. - const fillsTriggeringExcesses = await subtractExcessFromPreviousSlowFillsFromRunningBalances( - mainnetBundleEndBlock, - runningBalances, - clients.hubPoolClient, - spokePoolClients, - allValidFills, - allValidFillsInRange - ); - // Subtract destination chain running balances for BundleDataClient.unexecutableSlowFills. // These are all slow fills that are impossible to execute and therefore the amount to return would be // the updatedOutputAmount = inputAmount - lpFees. @@ -614,40 +476,10 @@ export async function _buildPoolRebalanceRoot( }); }); - if (logger && Object.keys(fillsTriggeringExcesses).length > 0) { - logger.debug({ - at: "Dataworker#DataworkerUtils", - message: "Fills triggering excess returns from L2", - fillsTriggeringExcesses, - }); - } - /** * DEPOSITS */ - // Map each deposit event to its L1 token and origin chain ID and subtract deposited amounts from running - // balances. Note that we do not care if the deposit is matched with a fill for this epoch or not since all - // deposit events lock funds in the spoke pool and should decrease running balances accordingly. However, - // its important that `deposits` are all in this current block range. - deposits.forEach((deposit: V2DepositWithBlock) => { - const inputAmount = utils.getDepositInputAmount(deposit); - updateRunningBalanceForDeposit(runningBalances, clients.hubPoolClient, deposit, inputAmount.mul(-1)); - }); - - earlyDeposits.forEach((earlyDeposit) => { - updateRunningBalanceForEarlyDeposit( - runningBalances, - clients.hubPoolClient, - earlyDeposit, - // TODO: fix this. - // Because cloneDeep drops the non-array elements of args, we have to use the index rather than the name. - // As a fix, earlyDeposits should be treated similarly to other events and transformed at ingestion time - // into a type that is more digestable rather than a raw event. - earlyDeposit.args[0].mul(-1) - ); - }); - // Handle v3Deposits. These decrement running balances from the origin chain equal to the inputAmount. // There should not be early deposits in v3. Object.entries(bundleV3Deposits).forEach(([, depositsForChain]) => { diff --git a/src/dataworker/PoolRebalanceUtils.ts b/src/dataworker/PoolRebalanceUtils.ts index d7cc22751..e191ed976 100644 --- a/src/dataworker/PoolRebalanceUtils.ts +++ b/src/dataworker/PoolRebalanceUtils.ts @@ -1,5 +1,4 @@ -import assert from "assert"; -import { typechain, utils as sdkUtils } from "@across-protocol/sdk-v2"; +import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import { ConfigStoreClient, HubPoolClient, SpokePoolClient } from "../clients"; import { Clients } from "../common"; import * as interfaces from "../interfaces"; @@ -7,31 +6,24 @@ import { PendingRootBundle, PoolRebalanceLeaf, RelayerRefundLeaf, - RunningBalances, - SlowFillLeaf, - SpokePoolClientsByChain, SpokePoolTargetBalance, - UnfilledDeposit, + V3SlowFillLeaf, } from "../interfaces"; import { - AnyObject, bnZero, BigNumber, fixedPointAdjustment as fixedPoint, MerkleTree, - assign, compareAddresses, convertFromWei, formatFeePct, - getFillDataForSlowFillFromPreviousRootBundle, - getNetworkName, - getRefund, shortenHexString, shortenHexStrings, - spreadEventWithBlockNumber, toBN, toBNWei, winston, + assert, + getNetworkName, } from "../utils"; import { DataworkerClients } from "./DataworkerClientHelper"; @@ -53,56 +45,6 @@ export function updateRunningBalance( } } -export function updateRunningBalanceForFill( - endBlockForMainnet: number, - runningBalances: interfaces.RunningBalances, - hubPoolClient: HubPoolClient, - fill: interfaces.FillWithBlock, - updateAmount: BigNumber -): void { - const l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock( - sdkUtils.getFillOutputToken(fill), - fill.destinationChainId, - endBlockForMainnet - ); - updateRunningBalance(runningBalances, fill.destinationChainId, l1TokenCounterpart, updateAmount); -} - -export function updateRunningBalanceForDeposit( - runningBalances: interfaces.RunningBalances, - hubPoolClient: HubPoolClient, - deposit: interfaces.DepositWithBlock, - updateAmount: BigNumber -): void { - const l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock( - sdkUtils.getDepositInputToken(deposit), - deposit.originChainId, - deposit.quoteBlockNumber - ); - updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart, updateAmount); -} - -export function updateRunningBalanceForEarlyDeposit( - runningBalances: interfaces.RunningBalances, - hubPoolClient: HubPoolClient, - depositEvent: typechain.FundsDepositedEvent, - updateAmount: BigNumber -): void { - const deposit = { ...spreadEventWithBlockNumber(depositEvent) } as interfaces.DepositWithBlock; - const { originChainId } = deposit; - - const l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock( - sdkUtils.getDepositInputToken(deposit), - originChainId, - // TODO: this must be handled s.t. it doesn't depend on when this is run. - // For now, tokens do not change their mappings often, so this will work, but - // to keep the system resilient, this must be updated. - hubPoolClient.latestBlockSearched - ); - - updateRunningBalance(runningBalances, originChainId, l1TokenCounterpart, updateAmount); -} - export function addLastRunningBalance( latestMainnetBlock: number, runningBalances: interfaces.RunningBalances, @@ -122,58 +64,18 @@ export function addLastRunningBalance( }); } -export function initializeRunningBalancesFromRelayerRepayments( - runningBalances: RunningBalances, - realizedLpFees: RunningBalances, - latestMainnetBlock: number, - hubPoolClient: HubPoolClient, - fillsToRefund: interfaces.FillsToRefund -): void { - Object.entries(fillsToRefund).forEach(([_repaymentChainId, fillsForChain]) => { - const repaymentChainId = Number(_repaymentChainId); - Object.entries(fillsForChain).forEach( - ([l2TokenAddress, { realizedLpFees: totalRealizedLpFee, totalRefundAmount }]) => { - const l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock( - l2TokenAddress, - repaymentChainId, - latestMainnetBlock - ); - - // Realized LP fees is only affected by relayer repayments so we'll return a brand new dictionary of those - // mapped to each { repaymentChainId, repaymentToken } combination. - assign(realizedLpFees, [repaymentChainId, l1TokenCounterpart], totalRealizedLpFee); - - // Add total repayment amount to running balances. Note: totalRefundAmount won't exist for chains that - // only had slow fills, so we should explicitly check for it. - if (totalRefundAmount) { - assign(runningBalances, [repaymentChainId, l1TokenCounterpart], totalRefundAmount); - } else { - assign(runningBalances, [repaymentChainId, l1TokenCounterpart], bnZero); - } - } - ); - }); -} - -export function addSlowFillsToRunningBalances( - latestMainnetBlock: number, +export function updateRunningBalanceForDeposit( runningBalances: interfaces.RunningBalances, hubPoolClient: HubPoolClient, - unfilledDeposits: UnfilledDeposit[] + deposit: interfaces.V3DepositWithBlock, + updateAmount: BigNumber ): void { - unfilledDeposits.forEach(({ deposit, unfilledAmount }) => { - const l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock( - sdkUtils.getDepositInputToken(deposit), - deposit.originChainId, - latestMainnetBlock - ); - updateRunningBalance( - runningBalances, - deposit.destinationChainId, - l1TokenCounterpart, - getRefund(unfilledAmount, deposit.realizedLpFeePct) - ); - }); + const l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock( + deposit.inputToken, + deposit.originChainId, + deposit.quoteBlockNumber + ); + updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart, updateAmount); } // TODO: Is summing up absolute values really the best way to compute a root bundle's "volume"? Said another way, @@ -213,112 +115,6 @@ export async function computePoolRebalanceUsdVolume( }, bnZero); } -export async function subtractExcessFromPreviousSlowFillsFromRunningBalances( - mainnetBundleEndBlock: number, - runningBalances: interfaces.RunningBalances, - hubPoolClient: HubPoolClient, - spokePoolClientsByChain: SpokePoolClientsByChain, - allValidFills: interfaces.V2FillWithBlock[], - allValidFillsInRange: interfaces.V2FillWithBlock[] -): Promise { - const excesses = {}; - // We need to subtract excess from any fills that might replaced a slow fill sent to the fill destination chain. - // This can only happen if the fill was the last fill for a deposit. Otherwise, its still possible that the slow fill - // for the deposit can be executed, so we'll defer the excess calculation until the hypothetical slow fill executes. - // In addition to fills that are not the last fill for a deposit, we can ignore fills that completely fill a deposit - // as the first fill. These fills could never have triggered a deposit since there were no partial fills for it. - // This assumption depends on the rule that slow fills can only be sent after a partial fill for a non zero amount - // of the deposit. This is why "1 wei" fills are important, otherwise we'd never know which fills originally - // triggered a slow fill payment to be sent to the destination chain. - await Promise.all( - allValidFillsInRange - .filter((fill) => { - const outputAmount = sdkUtils.getFillOutputAmount(fill); - const fillAmount = sdkUtils.getFillAmount(fill); - const totalFilledAmount = sdkUtils.getTotalFilledAmount(fill); - return totalFilledAmount.eq(outputAmount) && !fillAmount.eq(outputAmount); - }) - .map(async (fill) => { - const { lastMatchingFillInSameBundle, rootBundleEndBlockContainingFirstFill } = - await getFillDataForSlowFillFromPreviousRootBundle( - hubPoolClient.latestBlockSearched, - fill, - allValidFills, - hubPoolClient, - spokePoolClientsByChain - ); - - // Now that we have the last fill sent in a previous root bundle that also sent a slow fill, we can compute - // the excess that we need to decrease running balances by. This excess only exists in the case where the - // current fill completed a deposit. There will be an excess if (1) the slow fill was never executed, and (2) - // the slow fill was executed, but not before some partial fills were sent. - - // Note, if there is NO fill from a previous root bundle for the same deposit as this fill, then there has been - // no slow fill payment sent to the spoke pool yet, so we can exit early. - if (lastMatchingFillInSameBundle === undefined) { - return; - } - - // If first fill for this deposit is in this epoch, then no slow fill has been sent so we can ignore this fill. - // We can check this by searching for a ProposeRootBundle event with a bundle block range that contains the - // first fill for this deposit. If it is the same as the ProposeRootBundle event containing the - // current fill, then the first fill is in the current bundle and we can exit early. - const rootBundleEndBlockContainingFullFill = hubPoolClient.getRootBundleEvalBlockNumberContainingBlock( - hubPoolClient.latestBlockSearched, - fill.blockNumber, - fill.destinationChainId - ); - if (rootBundleEndBlockContainingFirstFill === rootBundleEndBlockContainingFullFill) { - return; - } - - // Recompute how much the matched root bundle sent for this slow fill. - const outputAmount = sdkUtils.getFillOutputAmount(lastMatchingFillInSameBundle); - const totalFilledAmount = sdkUtils.getTotalFilledAmount(lastMatchingFillInSameBundle); - const preFeeAmountSentForSlowFill = outputAmount.sub(totalFilledAmount); - - // If this fill is a slow fill, then the excess remaining in the contract is equal to the amount sent originally - // for this slow fill, and the amount filled. If this fill was not a slow fill, then that means the slow fill - // was never sent, so we need to send the full slow fill back. - const excess = getRefund( - sdkUtils.isSlowFill(fill) - ? preFeeAmountSentForSlowFill.sub(sdkUtils.getFillAmount(fill)) - : preFeeAmountSentForSlowFill, - fill.realizedLpFeePct - ); - if (excess.eq(bnZero)) { - return; - } - - // Log excesses for debugging since this logic is so complex. - const outputToken = sdkUtils.getFillOutputToken(fill); - excesses[fill.destinationChainId] ??= {}; - excesses[fill.destinationChainId][outputToken] ??= []; - excesses[fill.destinationChainId][outputToken].push({ - excess: excess.toString(), - lastMatchingFillInSameBundle, - rootBundleEndBlockContainingFirstFill, - rootBundleEndBlockContainingFullFill: rootBundleEndBlockContainingFullFill - ? rootBundleEndBlockContainingFullFill - : "N/A", - finalFill: fill, - }); - - updateRunningBalanceForFill(mainnetBundleEndBlock, runningBalances, hubPoolClient, fill, excess.mul(-1)); - }) - ); - - // Sort excess entries by block number, most recent first. - Object.keys(excesses).forEach((chainId) => { - Object.keys(excesses[chainId]).forEach((token) => { - excesses[chainId][token] = excesses[chainId][token].sort( - (ex, ey) => ey.finalFill.blockNumber - ex.finalFill.blockNumber - ); - }); - }); - return excesses; -} - export function constructPoolRebalanceLeaves( latestMainnetBlock: number, runningBalances: interfaces.RunningBalances, @@ -534,8 +330,7 @@ export function generateMarkdownForRootBundle( // eslint-disable-next-line @typescript-eslint/no-explicit-any relayerRefundLeaves: any[], relayerRefundRoot: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - slowRelayLeaves: SlowFillLeaf[], + slowRelayLeaves: V3SlowFillLeaf[], slowRelayRoot: string ): string { // Create helpful logs to send to slack transport @@ -600,8 +395,18 @@ export function generateMarkdownForRootBundle( const outputTokenDecimals = hubPoolClient.getTokenInfo(destinationChainId, outputToken).decimals; const lpFeePct = sdkUtils.getSlowFillLeafLpFeePct(leaf); + // Scale amounts to 18 decimals for realizedLpFeePct computation. + const scaleBy = toBN(10).pow(18 - outputTokenDecimals); + const inputAmount = leaf.relayData.inputAmount.mul(scaleBy); + const updatedOutputAmount = leaf.updatedOutputAmount.mul(scaleBy); + assert( + inputAmount.gte(updatedOutputAmount), + "Unexpected output amount for slow fill on" + + ` ${getNetworkName(leaf.relayData.originChainId)} depositId ${leaf.relayData.depositId}` + ); + // @todo: When v2 types are removed, update the slowFill definition to be more precise about the memebr fields. - const slowFill: Record = { + const slowFill = { // Shorten select keys for ease of reading from Slack. depositor: shortenHexString(leaf.relayData.depositor), recipient: shortenHexString(leaf.relayData.recipient), @@ -611,27 +416,10 @@ export function generateMarkdownForRootBundle( message: leaf.relayData.message, // Fee decimals is always 18. 1e18 = 100% so 1e16 = 1%. realizedLpFeePct: `${formatFeePct(lpFeePct)}%`, + outputToken, + outputAmount: convertFromWei(updatedOutputAmount.toString(), 18), // tokens were scaled to 18 decimals. }; - if (sdkUtils.isV2SlowFillLeaf(leaf)) { - slowFill.destinationToken = convertTokenAddressToSymbol(destinationChainId, outputToken); - slowFill.amount = convertFromWei(leaf.relayData.amount.toString(), outputTokenDecimals); - slowFill.payoutAdjustmentPct = `${formatFeePct(toBN(leaf.payoutAdjustmentPct))}%`; - } else { - // Scale amounts to 18 decimals for realizedLpFeePct computation. - const scaleBy = toBN(10).pow(18 - outputTokenDecimals); - const inputAmount = leaf.relayData.inputAmount.mul(scaleBy); - const updatedOutputAmount = leaf.updatedOutputAmount.mul(scaleBy); - assert( - inputAmount.gte(updatedOutputAmount), - "Unexpected output amount for slow fill on" + - ` ${getNetworkName(leaf.relayData.originChainId)} depositId ${leaf.relayData.depositId}` - ); - - slowFill.outputToken = outputToken; - slowFill.outputAmount = convertFromWei(updatedOutputAmount.toString(), 18); // tokens were scaled to 18 decimals. - } - slowRelayLeavesPretty += `\n\t\t\t${index}: ${JSON.stringify(slowFill)}`; }); @@ -654,8 +442,8 @@ export function generateMarkdownForRootBundle( export function prettyPrintLeaves( logger: winston.Logger, - tree: MerkleTree | MerkleTree | MerkleTree, - leaves: PoolRebalanceLeaf[] | RelayerRefundLeaf[] | SlowFillLeaf[], + tree: MerkleTree | MerkleTree | MerkleTree, + leaves: PoolRebalanceLeaf[] | RelayerRefundLeaf[] | V3SlowFillLeaf[], logType = "Pool rebalance" ): void { leaves.forEach((leaf, index) => { diff --git a/src/interfaces/BundleData.ts b/src/interfaces/BundleData.ts index e58fc95df..c206276c3 100644 --- a/src/interfaces/BundleData.ts +++ b/src/interfaces/BundleData.ts @@ -1,4 +1,4 @@ -import { interfaces, typechain } from "@across-protocol/sdk-v2"; +import { interfaces } from "@across-protocol/sdk-v2"; import { BigNumber } from "../utils"; export type ExpiredDepositsToRefundV3 = { [originChainId: number]: { @@ -39,11 +39,6 @@ export type BundleSlowFills = { }; export type LoadDataReturnValue = { - unfilledDeposits: interfaces.UnfilledDeposit[]; - fillsToRefund: interfaces.FillsToRefund; - allValidFills: interfaces.V2FillWithBlock[]; - deposits: interfaces.V2DepositWithBlock[]; - earlyDeposits: typechain.FundsDepositedEvent[]; bundleDepositsV3: BundleDepositsV3; expiredDepositsToRefundV3: ExpiredDepositsToRefundV3; bundleFillsV3: BundleFillsV3; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 704fffbc2..e064ccab9 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -48,15 +48,11 @@ export type FillWithBlock = interfaces.FillWithBlock; export type SpeedUp = interfaces.SpeedUp; export type SlowFillRequest = interfaces.SlowFillRequest; export type SlowFillRequestWithBlock = interfaces.SlowFillRequestWithBlock; -export type SlowFillLeaf = interfaces.SlowFillLeaf; export type RootBundleRelay = interfaces.RootBundleRelay; export type RootBundleRelayWithBlock = interfaces.RootBundleRelayWithBlock; export type RelayerRefundExecution = interfaces.RelayerRefundExecution; export type RelayerRefundExecutionWithBlock = interfaces.RelayerRefundExecutionWithBlock; -export type UnfilledDeposit = interfaces.UnfilledDeposit; -export type UnfilledDepositsForOriginChain = interfaces.UnfilledDepositsForOriginChain; export type Refund = interfaces.Refund; -export type FillsToRefund = interfaces.FillsToRefund; export type RunningBalances = interfaces.RunningBalances; export type TokensBridged = interfaces.TokensBridged; export const { FillType, FillStatus } = interfaces; @@ -76,5 +72,4 @@ export type V2Fill = interfaces.V2Fill; export type V3Fill = interfaces.V3Fill; export type V2FillWithBlock = interfaces.V2FillWithBlock; export type V3FillWithBlock = interfaces.V3FillWithBlock; -export type V2SlowFillLeaf = interfaces.V2SlowFillLeaf; export type V3SlowFillLeaf = interfaces.V3SlowFillLeaf; diff --git a/src/scripts/validateRunningBalances.ts b/src/scripts/validateRunningBalances.ts index c0ddfa3ac..d2d2e3e92 100644 --- a/src/scripts/validateRunningBalances.ts +++ b/src/scripts/validateRunningBalances.ts @@ -19,10 +19,8 @@ // which also indicates an amount of tokens that need to be taken out of the spoke pool to execute those refunds // - excess_t_c_{i,i+1,i+2,...} should therefore be consistent unless tokens are dropped onto the spoke pool. -import assert from "assert"; import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import { - BigNumber, bnZero, winston, config, @@ -38,7 +36,6 @@ import { sortEventsDescending, paginatedEventQuery, ZERO_ADDRESS, - getRefund, disconnectRedisClients, Signer, getSigner, @@ -46,7 +43,7 @@ import { import { createDataworker } from "../dataworker"; import { getWidestPossibleExpectedBlockRange } from "../dataworker/PoolRebalanceUtils"; import { getBlockForChain, getEndBlockBuffers } from "../dataworker/DataworkerUtils"; -import { ProposedRootBundle, SlowFillLeaf, SpokePoolClientsByChain } from "../interfaces"; +import { ProposedRootBundle, SpokePoolClientsByChain, V3SlowFillLeaf } from "../interfaces"; import { CONTRACT_ADDRESSES, constructSpokePoolClientsWithStartBlocks, updateSpokePoolClients } from "../common"; import { createConsoleTransport } from "@uma/financial-templates-lib"; @@ -254,11 +251,8 @@ export async function runScript(_logger: winston.Logger, baseSigner: Signer): Pr ); // Compute how much the slow fill will execute by checking if any fills were sent after the slow fill amount // was sent to the spoke pool. This would reduce the amount transferred when when the slow fill is executed. - // For v2, pre-fee amounts are computed and are normalised to post-fee amounts afterwards. const slowFillsForPoolRebalanceLeaf = slowFills.filter( - (f) => - sdkUtils.getSlowFillLeafChainId(f) === leaf.chainId && - sdkUtils.getRelayDataOutputToken(f.relayData) === l2Token + (f) => f.chainId === leaf.chainId && f.relayData.outputToken === l2Token ); if (slowFillsForPoolRebalanceLeaf.length > 0) { @@ -272,33 +266,16 @@ export async function runScript(_logger: winston.Logger, baseSigner: Signer): Pr f.depositId === slowFillForChain.relayData.depositId ); - let unexecutedAmount: BigNumber; const lastFill = sortEventsDescending(fillsForSameDeposit)[0]; - if (sdkUtils.isV2SlowFillLeaf(slowFillForChain)) { - assert(isDefined(lastFill)); - // For v2 there must be at least one partial fill, but there may be multiple. The deposit - // may be filled anywhere up to the outstanding ammount included in the slow fill. - const outputAmount = sdkUtils.getRelayDataOutputAmount(slowFillForChain.relayData); - const totalFilledAmount = sdkUtils.getTotalFilledAmount(lastFill); - unexecutedAmount = outputAmount.sub(totalFilledAmount); - } else { - // For v3 slow fills there _may_ be a fast fill, and if so, the fill is completed. - unexecutedAmount = isDefined(lastFill) ? bnZero : slowFillForChain.updatedOutputAmount; - } - - // For v2 fills, the amounts computed so far were pre-fees. Subtract the LP fee to determine how much - // remains to be executed. In v3 the post-fee amount is known, so use it directly. + // For v3 slow fills if there is a matching fast fill, then the fill is completed. + const unexecutedAmount = isDefined(lastFill) ? bnZero : slowFillForChain.updatedOutputAmount; if (unexecutedAmount.gt(bnZero)) { - const deductionForSlowFill = sdkUtils.isV3SlowFillLeaf(slowFillForChain) - ? unexecutedAmount - : getRefund(unexecutedAmount, slowFillForChain.relayData.realizedLpFeePct); - mrkdwn += `\n\t\t- subtracting leftover amount from previous bundle's unexecuted slow fill: ${fromWei( - deductionForSlowFill.toString(), + unexecutedAmount.toString(), decimals )}`; - tokenBalanceAtBundleEndBlock = tokenBalanceAtBundleEndBlock.sub(deductionForSlowFill); + tokenBalanceAtBundleEndBlock = tokenBalanceAtBundleEndBlock.sub(unexecutedAmount); } } } @@ -310,50 +287,30 @@ export async function runScript(_logger: winston.Logger, baseSigner: Signer): Pr // The slow fill amount will be captured in the netSendAmount as a positive value, so we need to cancel that out. // Not many bundles are expected to have slow fills so we can load them as necessary. - const { slowFills, bundleSpokePoolClients } = await _constructSlowRootForBundle( + const { slowFills } = await _constructSlowRootForBundle( mostRecentValidatedBundle, validatedBundles[x + 1 + 2], mostRecentValidatedBundle ); const slowFillsForPoolRebalanceLeaf = slowFills.filter( - (f) => - sdkUtils.getSlowFillLeafChainId(f) === leaf.chainId && - sdkUtils.getRelayDataOutputToken(f.relayData) === l2Token + (f) => f.chainId === leaf.chainId && f.relayData.outputToken === l2Token ); if (slowFillsForPoolRebalanceLeaf.length > 0) { for (const slowFillForChain of slowFillsForPoolRebalanceLeaf) { - const destinationChainId = sdkUtils.getSlowFillLeafChainId(slowFillForChain); - let amountSentForSlowFill: BigNumber; - - if (sdkUtils.isV3SlowFillLeaf(slowFillForChain)) { - amountSentForSlowFill = slowFillForChain.updatedOutputAmount; - } else { - const fillsForSameDeposit = bundleSpokePoolClients[destinationChainId] - .getFillsForOriginChain(slowFillForChain.relayData.originChainId) - .filter((f) => f.depositId === slowFillForChain.relayData.depositId); - - const lastFill = sortEventsDescending(fillsForSameDeposit)[0]; - const outputAmount = sdkUtils.getRelayDataOutputAmount(slowFillForChain.relayData); - const totalFilledAmount = sdkUtils.getTotalFilledAmount(lastFill); - amountSentForSlowFill = outputAmount.sub(totalFilledAmount); - } - + const amountSentForSlowFill = slowFillForChain.updatedOutputAmount; if (amountSentForSlowFill.gt(0)) { - const deductionForSlowFill = sdkUtils.isV3SlowFillLeaf(slowFillForChain) - ? amountSentForSlowFill - : getRefund(amountSentForSlowFill, slowFillForChain.relayData.realizedLpFeePct); - mrkdwn += `\n\t\t- subtracting amount sent for slow fill: ${fromWei( - deductionForSlowFill.toString(), + amountSentForSlowFill.toString(), decimals )}`; - tokenBalanceAtBundleEndBlock = tokenBalanceAtBundleEndBlock.sub(deductionForSlowFill); + tokenBalanceAtBundleEndBlock = tokenBalanceAtBundleEndBlock.sub(amountSentForSlowFill); } } } } - const relayedRoot = spokePoolClients[leaf.chainId].getExecutedRefunds( + const relayedRoot = dataworker.clients.bundleDataClient.getExecutedRefunds( + spokePoolClients[leaf.chainId], mostRecentValidatedBundle.relayerRefundRoot ); @@ -439,7 +396,7 @@ export async function runScript(_logger: winston.Logger, baseSigner: Signer): Pr bundle: ProposedRootBundle, olderBundle: ProposedRootBundle, futureBundle: ProposedRootBundle - ): Promise<{ slowFills: SlowFillLeaf[]; bundleSpokePoolClients: SpokePoolClientsByChain }> { + ): Promise<{ slowFills: V3SlowFillLeaf[]; bundleSpokePoolClients: SpokePoolClientsByChain }> { // Construct custom spoke pool clients to query events needed to build slow roots. const spokeClientFromBlocks = Object.fromEntries( dataworker.chainIdListForBundleEvaluationBlockNumbers.map((chainId, i) => { @@ -488,7 +445,14 @@ export async function runScript(_logger: winston.Logger, baseSigner: Signer): Pr spokeClientFromBlocks, spokeClientToBlocks ); - await updateSpokePoolClients(spokePoolClientsForBundle); + await updateSpokePoolClients(spokePoolClientsForBundle, [ + "EnabledDepositRoute", + "RelayedRootBundle", + "ExecutedRelayerRefundRoot", + "V3FundsDeposited", + "RequestedV3SlowFill", + "FilledV3Relay", + ]); // Reconstruct bundle block range for bundle. const mainnetBundleEndBlock = getBlockForChain( diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 5fa56836a..60679dfce 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -1,135 +1,7 @@ -import { utils, typechain } from "@across-protocol/sdk-v2"; -import { - Deposit, - Fill, - SlowFillRequest, - UnfilledDeposit, - UnfilledDepositsForOriginChain, - V2DepositWithBlock, - V3DepositWithBlock, -} from "../interfaces"; +import { utils } from "@across-protocol/sdk-v2"; +import { Fill, SlowFillRequest } from "../interfaces"; import { SpokePoolClient } from "../clients"; -import { assign, isFirstFillForDeposit, getRedisCache } from "./"; -import { bnZero } from "./SDKUtils"; -import { getBlockRangeForChain } from "../dataworker/DataworkerUtils"; - -export function getDepositPath(deposit: Deposit): string { - const inputToken = utils.getDepositInputToken(deposit); - return `${inputToken}-->${deposit.destinationChainId}`; -} - -export function updateUnfilledDepositsWithMatchedDeposit( - matchedFill: Fill, - matchedDeposit: Deposit, - unfilledDepositsForOriginChain: UnfilledDepositsForOriginChain -): void { - const outputAmount = utils.getFillOutputAmount(matchedFill); - const totalFilledAmount = utils.getTotalFilledAmount(matchedFill); - const unfilledAmount = outputAmount.sub(totalFilledAmount); - - const depositKey = `${matchedDeposit.originChainId}+${matchedFill.depositId}`; - assign( - unfilledDepositsForOriginChain, - [depositKey], - [ - { - unfilledAmount, - deposit: matchedDeposit, - // A first partial fill for a deposit is characterized by one whose total filled amount post-fill - // is equal to the amount sent in the fill, and where the fill amount is greater than zero. - hasFirstPartialFill: isFirstFillForDeposit(matchedFill), - }, - ] - ); -} - -export function flattenAndFilterUnfilledDepositsByOriginChain( - unfilledDepositsForOriginChain: UnfilledDepositsForOriginChain -): UnfilledDeposit[] { - return ( - Object.values(unfilledDepositsForOriginChain) - .map((_unfilledDeposits: UnfilledDeposit[]): UnfilledDeposit => { - // Remove deposits with no matched fills. - if (_unfilledDeposits.length === 0) { - return { unfilledAmount: bnZero, deposit: undefined }; - } - // Remove deposits where there isn't a fill with fillAmount == totalFilledAmount && fillAmount > 0. This ensures - // that we'll only be slow relaying deposits where the first fill (i.e. the one with - // fillAmount == totalFilledAmount) is in this epoch. We assume that we already included slow fills in a - // previous epoch for these ignored deposits. - if ( - !_unfilledDeposits.some((_unfilledDeposit: UnfilledDeposit) => _unfilledDeposit.hasFirstPartialFill === true) - ) { - return { unfilledAmount: bnZero, deposit: undefined }; - } - // For each deposit, identify the smallest unfilled amount remaining after a fill since each fill can - // only decrease the unfilled amount. - _unfilledDeposits.sort((unfilledDepositA, unfilledDepositB) => - unfilledDepositA.unfilledAmount.gt(unfilledDepositB.unfilledAmount) - ? 1 - : unfilledDepositA.unfilledAmount.lt(unfilledDepositB.unfilledAmount) - ? -1 - : 0 - ); - return { unfilledAmount: _unfilledDeposits[0].unfilledAmount, deposit: _unfilledDeposits[0].deposit }; - }) - // Remove deposits that are fully filled - .filter((unfilledDeposit: UnfilledDeposit) => unfilledDeposit.unfilledAmount.gt(0)) - ); -} - -export function getUniqueDepositsInRange( - blockRangesForChains: number[][], - originChain: number, - destinationChain: number, - chainIdListForBundleEvaluationBlockNumbers: number[], - originClient: SpokePoolClient, - existingUniqueDeposits: V2DepositWithBlock[] -): V2DepositWithBlock[] { - const originChainBlockRange = getBlockRangeForChain( - blockRangesForChains, - originChain, - chainIdListForBundleEvaluationBlockNumbers - ); - return originClient - .getDepositsForDestinationChain(destinationChain) - .filter( - (deposit) => - deposit.blockNumber <= originChainBlockRange[1] && - deposit.blockNumber >= originChainBlockRange[0] && - !existingUniqueDeposits.some( - (existingDeposit) => - existingDeposit.originChainId === deposit.originChainId && existingDeposit.depositId === deposit.depositId - ) - ) - .filter(utils.isV2Deposit); -} - -export function getUniqueEarlyDepositsInRange( - blockRangesForChains: number[][], - originChain: number, - destinationChain: number, - chainIdListForBundleEvaluationBlockNumbers: number[], - originClient: SpokePoolClient, - existingUniqueDeposits: typechain.FundsDepositedEvent[] -): typechain.FundsDepositedEvent[] { - const originChainBlockRange = getBlockRangeForChain( - blockRangesForChains, - originChain, - chainIdListForBundleEvaluationBlockNumbers - ); - return (originClient["earlyDeposits"] as unknown as typechain.FundsDepositedEvent[]).filter( - (deposit: typechain.FundsDepositedEvent) => - deposit.blockNumber <= originChainBlockRange[1] && - deposit.blockNumber >= originChainBlockRange[0] && - deposit.args.destinationChainId.toString() === destinationChain.toString() && - !existingUniqueDeposits.some( - (existingDeposit) => - existingDeposit.args.originChainId.toString() === deposit.args.originChainId.toString() && - existingDeposit.args.depositId.toString() === deposit.args.depositId.toString() - ) - ); -} +import { getRedisCache } from "./"; // Load a deposit for a fill if the fill's deposit ID is outside this client's search range. // This can be used by the Dataworker to determine whether to give a relayer a refund for a fill diff --git a/src/utils/FillMathUtils.ts b/src/utils/FillMathUtils.ts deleted file mode 100644 index 27fdd3ca4..000000000 --- a/src/utils/FillMathUtils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import assert from "assert"; -import { utils as sdkUtils } from "@across-protocol/sdk-v2"; -import { Fill } from "../interfaces"; -import { bnZero, fixedPointAdjustment as fixedPoint } from "./SDKUtils"; -import { BigNumber } from "."; - -export function _getRefundForFill(fill: Fill): BigNumber { - assert(sdkUtils.isV2Fill(fill)); - return fill.fillAmount.mul(fixedPoint.sub(fill.realizedLpFeePct)).div(fixedPoint); -} - -export function _getFeeAmount(fillAmount: BigNumber, feePct: BigNumber): BigNumber { - return fillAmount.mul(feePct).div(fixedPoint); -} - -export function _getRealizedLpFeeForFill(fill: Fill): BigNumber { - assert(sdkUtils.isV2Fill(fill)); - return fill.fillAmount.mul(fill.realizedLpFeePct).div(fixedPoint); -} - -export function getRefund(fillAmount: BigNumber, realizedLpFeePct: BigNumber): BigNumber { - return fillAmount.mul(fixedPoint.sub(realizedLpFeePct)).div(fixedPoint); -} - -export function getFillAmountMinusFees( - fillAmount: BigNumber, - realizedLpFeePct: BigNumber, - relayerFeePct: BigNumber -): BigNumber { - return fillAmount.mul(fixedPoint.sub(realizedLpFeePct).sub(relayerFeePct)).div(fixedPoint); -} - -export function getRefundForFills(fills: Fill[]): BigNumber { - return fills.reduce((acc, fill) => acc.add(_getRefundForFill(fill)), bnZero); -} - -export function getRealizedLpFeeForFills(fills: Fill[]): BigNumber { - return fills.reduce((acc, fill) => acc.add(_getRealizedLpFeeForFill(fill)), bnZero); -} diff --git a/src/utils/FillUtils.ts b/src/utils/FillUtils.ts index 32d250893..457db033b 100644 --- a/src/utils/FillUtils.ts +++ b/src/utils/FillUtils.ts @@ -1,28 +1,9 @@ import assert from "assert"; import { utils as sdkUtils } from "@across-protocol/sdk-v2"; import { HubPoolClient, SpokePoolClient } from "../clients"; -import { - Fill, - FillsToRefund, - FillStatus, - FillWithBlock, - SpokePoolClientsByChain, - V2DepositWithBlock, - V2FillWithBlock, - V3DepositWithBlock, - V3FillWithBlock, -} from "../interfaces"; -import { getBlockForTimestamp, getRedisCache, queryHistoricalDepositForFill } from "../utils"; -import { - BigNumber, - bnZero, - assign, - getRealizedLpFeeForFills, - getRefundForFills, - isDefined, - sortEventsDescending, - sortEventsAscending, -} from "./"; +import { Fill, FillStatus, SpokePoolClientsByChain, V2DepositWithBlock, V3DepositWithBlock } from "../interfaces"; +import { getBlockForTimestamp, getRedisCache } from "../utils"; +import { isDefined } from "./"; import { getBlockRangeForChain } from "../dataworker/DataworkerUtils"; export function getRefundInformationFromFill( @@ -68,195 +49,6 @@ export function getRefundInformationFromFill( repaymentToken, }; } -export function assignValidFillToFillsToRefund( - fillsToRefund: FillsToRefund, - fill: Fill, - chainToSendRefundTo: number, - repaymentToken: string -): void { - assign(fillsToRefund, [chainToSendRefundTo, repaymentToken, "fills"], [fill]); -} - -export function updateTotalRealizedLpFeePct( - fillsToRefund: FillsToRefund, - fill: Fill, - chainToSendRefundTo: number, - repaymentToken: string -): void { - const refundObj = fillsToRefund[chainToSendRefundTo][repaymentToken]; - refundObj.realizedLpFees = refundObj.realizedLpFees - ? refundObj.realizedLpFees.add(getRealizedLpFeeForFills([fill])) - : getRealizedLpFeeForFills([fill]); -} - -export function updateTotalRefundAmount( - fillsToRefund: FillsToRefund, - fill: Fill, - chainToSendRefundTo: number, - repaymentToken: string -): void { - // Don't count slow relays in total refund amount, since we use this amount to conveniently construct - // relayer refund leaves. - if (sdkUtils.isSlowFill(fill)) { - return; - } - - const refund = getRefundForFills([fill]); - updateTotalRefundAmountRaw(fillsToRefund, refund, chainToSendRefundTo, fill.relayer, repaymentToken); -} - -export function updateTotalRefundAmountRaw( - fillsToRefund: FillsToRefund, - amount: BigNumber, - chainToSendRefundTo: number, - recipient: string, - repaymentToken: string -): void { - if (!fillsToRefund?.[chainToSendRefundTo]?.[repaymentToken]) { - assign(fillsToRefund, [chainToSendRefundTo, repaymentToken], {}); - } - const refundObj = fillsToRefund[chainToSendRefundTo][repaymentToken]; - refundObj.totalRefundAmount = refundObj.totalRefundAmount ? refundObj.totalRefundAmount.add(amount) : amount; - - // Instantiate dictionary if it doesn't exist. - if (!refundObj.refunds) { - assign(fillsToRefund, [chainToSendRefundTo, repaymentToken, "refunds"], {}); - } - - if (refundObj.refunds[recipient]) { - refundObj.refunds[recipient] = refundObj.refunds[recipient].add(amount); - } else { - refundObj.refunds[recipient] = amount; - } -} - -export function isFirstFillForDeposit(fill: Fill): boolean { - if (sdkUtils.isV3Fill(fill)) { - return true; - } - - const fillAmount = sdkUtils.getFillAmount(fill); - const totalFilledAmount = sdkUtils.getTotalFilledAmount(fill); - return fillAmount.eq(totalFilledAmount) && fillAmount.gt(bnZero); -} - -export function getLastMatchingFillBeforeBlock( - fillToMatch: Fill, - allFills: FillWithBlock[], - lastBlock: number -): FillWithBlock { - return sortEventsDescending(allFills).find( - (fill: FillWithBlock) => sdkUtils.filledSameDeposit(fillToMatch, fill) && lastBlock >= fill.blockNumber - ) as FillWithBlock; -} - -export async function getFillDataForSlowFillFromPreviousRootBundle( - latestMainnetBlock: number, - fill: V2FillWithBlock, - allValidFills: V2FillWithBlock[], - hubPoolClient: HubPoolClient, - spokePoolClientsByChain: SpokePoolClientsByChain -): Promise<{ - lastMatchingFillInSameBundle: FillWithBlock; - rootBundleEndBlockContainingFirstFill: number; -}> { - assert(sdkUtils.isV2Fill(fill)); - assert(allValidFills.every(sdkUtils.isV2Fill)); - - // Can use spokeClient.queryFillsForDeposit(_fill, spokePoolClient.eventSearchConfig.fromBlock) - // if allValidFills doesn't contain the deposit's first fill to efficiently find the first fill for a deposit. - // Note that allValidFills should only include fills later than than eventSearchConfig.fromBlock. - - // Find the first fill chronologically for matched deposit for the input fill. - const allMatchingFills = sortEventsAscending( - allValidFills.filter((_fill) => _fill.depositId === fill.depositId && sdkUtils.filledSameDeposit(_fill, fill)) - ); - let firstFillForSameDeposit = allMatchingFills.find((_fill) => isFirstFillForDeposit(_fill)); - - // If `allValidFills` doesn't contain the first fill for this deposit then we have to perform a historical query to - // find it. This is inefficient, but should be rare. Save any other fills we find to the - // allMatchingFills array, at the end of this block, allMatchingFills should contain all fills for the same - // deposit as the input fill. - if (!firstFillForSameDeposit) { - const depositForFill = await queryHistoricalDepositForFill(spokePoolClientsByChain[fill.originChainId], fill); - assert(depositForFill.found && sdkUtils.isV2Deposit(depositForFill.deposit)); - const matchingFills = ( - await spokePoolClientsByChain[fill.destinationChainId].queryHistoricalMatchingFills( - fill, - depositForFill.found ? depositForFill.deposit : undefined, - allMatchingFills[0].blockNumber - ) - ).filter(sdkUtils.isV2Fill); - - spokePoolClientsByChain[fill.destinationChainId].logger.debug({ - at: "FillUtils#getFillDataForSlowFillFromPreviousRootBundle", - message: "Queried for partial fill that triggered an unused slow fill, in order to compute slow fill excess", - fillThatCompletedDeposit: fill, - depositForFill, - matchingFills, - }); - - firstFillForSameDeposit = sortEventsAscending(matchingFills).find((_fill) => isFirstFillForDeposit(_fill)); - if (firstFillForSameDeposit === undefined) { - throw new Error( - "FillUtils#getFillDataForSlowFillFromPreviousRootBundle:" + - ` Cannot find first fill for for deposit ${fill.depositId}` + - ` on chain ${fill.destinationChainId} after querying historical fills` - ); - } - // Add non-duplicate fills. - allMatchingFills.push( - ...matchingFills.filter((_fill) => { - const totalFilledAmount = sdkUtils.getTotalFilledAmount(_fill); - return !allMatchingFills.find((existingFill) => - sdkUtils.getTotalFilledAmount(existingFill).eq(totalFilledAmount) - ); - }) - ); - } - - // Find ending block number for chain from ProposeRootBundle event that should have included a slow fill - // refund for this first fill. This will be undefined if there is no block range containing the first fill. - const rootBundleEndBlockContainingFirstFill = hubPoolClient.getRootBundleEvalBlockNumberContainingBlock( - latestMainnetBlock, - firstFillForSameDeposit.blockNumber, - firstFillForSameDeposit.destinationChainId - ); - // Using bundle block number for chain from ProposeRootBundleEvent, find latest fill in the root bundle. - let lastMatchingFillInSameBundle: FillWithBlock; - if (rootBundleEndBlockContainingFirstFill !== undefined) { - lastMatchingFillInSameBundle = getLastMatchingFillBeforeBlock( - fill, - sortEventsDescending(allMatchingFills), - rootBundleEndBlockContainingFirstFill - ); - } - return { - lastMatchingFillInSameBundle, - rootBundleEndBlockContainingFirstFill, - }; -} - -export function getFillsInRange( - fills: V2FillWithBlock[], - blockRangesForChains: number[][], - chainIdListForBundleEvaluationBlockNumbers: number[] -): V2FillWithBlock[] { - const blockRanges = Object.fromEntries( - chainIdListForBundleEvaluationBlockNumbers.map((chainId) => [ - chainId, - getBlockRangeForChain(blockRangesForChains, chainId, chainIdListForBundleEvaluationBlockNumbers), - ]) - ); - return fills.filter((fill) => { - const [startBlock, endBlock] = blockRanges[fill.destinationChainId]; - return fill.blockNumber >= startBlock && fill.blockNumber <= endBlock; - }); -} - -// @dev This type can be confused with UnfilledDeposit from sdk-v2/interfaces, but is different -// due to the additional members and the use of DepositWithBlock instead of Deposit. -// @todo Better alignment with the upstream UnfilledDeposit type. export type RelayerUnfilledDeposit = { deposit: V3DepositWithBlock; fillStatus: number; diff --git a/src/utils/MerkleTreeUtils.ts b/src/utils/MerkleTreeUtils.ts index 73596f375..72384fb75 100644 --- a/src/utils/MerkleTreeUtils.ts +++ b/src/utils/MerkleTreeUtils.ts @@ -1,11 +1,10 @@ import { MerkleTree, EMPTY_MERKLE_ROOT } from "@across-protocol/contracts-v2"; -import { utils as sdkUtils } from "@across-protocol/sdk-v2"; -import { PoolRebalanceLeaf, RelayerRefundLeaf, RelayerRefundLeafWithGroup, SlowFillLeaf } from "../interfaces"; +import { PoolRebalanceLeaf, RelayerRefundLeaf, RelayerRefundLeafWithGroup, V3SlowFillLeaf } from "../interfaces"; import { getParamType, utils } from "."; -export function buildSlowRelayTree(relays: SlowFillLeaf[]): MerkleTree { - const hashFn = (input: SlowFillLeaf) => { - const verifyFn = sdkUtils.isV2SlowFillLeaf(input) ? "verifySlowRelayFulfillment" : "verifyV3SlowRelayFulfillment"; +export function buildSlowRelayTree(relays: V3SlowFillLeaf[]): MerkleTree { + const hashFn = (input: V3SlowFillLeaf) => { + const verifyFn = "verifyV3SlowRelayFulfillment"; const paramType = getParamType("MerkleLibTest", verifyFn, "slowFill"); return utils.keccak256(utils.defaultAbiCoder.encode([paramType], [input])); }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 93c815f2d..db3b94c51 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -63,7 +63,6 @@ export * from "./NetworkUtils"; export * from "./TransactionUtils"; export * from "./MerkleTreeUtils"; export * from "./AddressUtils"; -export * from "./FillMathUtils"; export * from "./GckmsUtils"; export * from "./TimeUtils"; export * from "./TypeGuards"; diff --git a/test/Dataworker.buildRoots.ts b/test/Dataworker.buildRoots.ts index 5925848fc..760d72cd6 100644 --- a/test/Dataworker.buildRoots.ts +++ b/test/Dataworker.buildRoots.ts @@ -1,7 +1,5 @@ -import { ConfigStoreClient, HubPoolClient, SpokePoolClient } from "../src/clients"; +import { HubPoolClient, SpokePoolClient } from "../src/clients"; import { - Deposit, - Fill, RelayerRefundLeaf, RunningBalances, V2DepositWithBlock, @@ -9,61 +7,22 @@ import { V3DepositWithBlock, V3FillWithBlock, } from "../src/interfaces"; -import { - EMPTY_MERKLE_ROOT, - assert, - bnZero, - compareAddresses, - fixedPointAdjustment, - getRealizedLpFeeForFills, - getRefund, - getRefundForFills, -} from "../src/utils"; -import { - CHAIN_ID_TEST_LIST, - MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF, - MAX_REFUNDS_PER_RELAYER_REFUND_LEAF, - amountToDeposit, - destinationChainId, - mockTreeRoot, - originChainId, - refundProposalLiveness, - repaymentChainId, - sampleRateModel, -} from "./constants"; +import { assert, bnZero, fixedPointAdjustment } from "../src/utils"; +import { amountToDeposit, destinationChainId, mockTreeRoot, originChainId, repaymentChainId } from "./constants"; import { setupFastDataworker } from "./fixtures/Dataworker.Fixture"; import { BigNumber, Contract, SignerWithAddress, - buildDeposit, - buildFill, - buildFillForRepaymentChain, - buildPoolRebalanceLeafTree, - buildPoolRebalanceLeaves, - buildRelayerRefundTreeWithUnassignedLeafIds, - buildSlowFill, - buildSlowRelayLeaves, - buildSlowRelayTree, buildV3SlowRelayLeaves, - constructPoolRebalanceTree, - createSpyLogger, - deployNewTokenMapping, depositV3, - enableRoutesOnHubPool, ethers, expect, fillV3, - fillV3Relay, getDefaultBlockRange, - lastSpyLogIncludes, requestSlowFill, - setupTokensForWallet, - sinon, toBN, - toBNWei, buildV3SlowRelayTree, - updateDeposit, } from "./utils"; import { utils as sdkUtils, interfaces } from "@across-protocol/sdk-v2"; @@ -71,44 +30,72 @@ import { utils as sdkUtils, interfaces } from "@across-protocol/sdk-v2"; import { Dataworker } from "../src/dataworker/Dataworker"; let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract; -let l1Token_1: Contract, hubPool: Contract, timer: Contract, configStore: Contract; -let depositor: SignerWithAddress, relayer: SignerWithAddress, dataworker: SignerWithAddress; +let l1Token_1: Contract; +let depositor: SignerWithAddress, relayer: SignerWithAddress; -let hubPoolClient: HubPoolClient, configStoreClient: ConfigStoreClient; +let hubPoolClient: HubPoolClient; let dataworkerInstance: Dataworker; let spokePoolClients: { [chainId: number]: SpokePoolClient }; -let spy: sinon.SinonSpy; - let updateAllClients: () => Promise; describe("Dataworker: Build merkle roots", async function () { beforeEach(async function () { const fastDataworkerResult = await setupFastDataworker(ethers); - configStoreClient = fastDataworkerResult.configStoreClient as unknown as ConfigStoreClient; ({ - hubPool, spokePool_1, erc20_1, spokePool_2, erc20_2, - configStore, hubPoolClient, l1Token_1, depositor, relayer, dataworkerInstance, - dataworker, - timer, spokePoolClients, - spy, updateAllClients, } = fastDataworkerResult); + await updateAllClients(); + const poolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot( + getDefaultBlockRange(1), + spokePoolClients + ); + expect(poolRebalanceRoot.runningBalances).to.deep.equal({}); + expect(poolRebalanceRoot.realizedLpFees).to.deep.equal({}); + const relayerRefundRoot = await dataworkerInstance.buildRelayerRefundRoot( + getDefaultBlockRange(1), + spokePoolClients, + poolRebalanceRoot.leaves, + poolRebalanceRoot.runningBalances + ); + expect(relayerRefundRoot.leaves).to.deep.equal([]); }); - it("Build slow relay root", async function () { + it("Subtracts bundle deposits from origin chain running balances", async function () { + const deposit = await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit + ); await updateAllClients(); + const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); - const deposit1 = await depositV3( + // Deposits should not add to bundle LP fees. + const expectedRunningBalances: RunningBalances = { + [originChainId]: { + [l1Token_1.address]: deposit.inputAmount.mul(-1), + }, + }; + expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); + expect({}).to.deep.equal(merkleRoot1.realizedLpFees); + }); + it("Adds bundle fills to repayment chain running balances", async function () { + // Send two deposits so we can fill with two different origin chains to test that the BundleDataClient + // batch computes lp fees correctly for different origin chains. + await depositV3( spokePool_1, destinationChainId, depositor, @@ -117,8 +104,7 @@ describe("Dataworker: Build merkle roots", async function () { erc20_2.address, amountToDeposit ); - - const deposit2 = await depositV3( + await depositV3( spokePool_2, originChainId, depositor, @@ -127,8 +113,56 @@ describe("Dataworker: Build merkle roots", async function () { erc20_1.address, amountToDeposit ); - - const deposit3 = await depositV3( + await updateAllClients(); + const deposit1 = spokePoolClients[originChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + const deposit2 = spokePoolClients[destinationChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + await fillV3(spokePool_2, relayer, deposit1, repaymentChainId); + await fillV3(spokePool_1, relayer, deposit2, repaymentChainId); + await updateAllClients(); + const fill1 = spokePoolClients[destinationChainId] + .getFills() + .filter(sdkUtils.isV3Fill)[0]; + const fill2 = spokePoolClients[originChainId] + .getFills() + .filter(sdkUtils.isV3Fill)[0]; + const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); + + // Deposits should not add to bundle LP fees, but fills should. LP fees are taken out of running balances + // and added to realized LP fees, for fills. + const lpFeePct1 = ( + await hubPoolClient.computeRealizedLpFeePct({ ...deposit1, paymentChainId: fill1.destinationChainId }) + ).realizedLpFeePct; + const lpFeePct2 = ( + await hubPoolClient.computeRealizedLpFeePct({ ...deposit2, paymentChainId: fill2.destinationChainId }) + ).realizedLpFeePct; + assert(lpFeePct1.gt(0) && lpFeePct2.gt(0), "LP fee pct should be greater than 0"); + const lpFee1 = lpFeePct1.mul(fill1.inputAmount).div(fixedPointAdjustment); + const lpFee2 = lpFeePct2.mul(fill2.inputAmount).div(fixedPointAdjustment); + const expectedRunningBalances: RunningBalances = { + [originChainId]: { + [l1Token_1.address]: deposit1.inputAmount.mul(-1), + }, + [destinationChainId]: { + [l1Token_1.address]: deposit2.inputAmount.mul(-1), + }, + [repaymentChainId]: { + [l1Token_1.address]: lpFee1.mul(-1).add(fill1.inputAmount).add(lpFee2.mul(-1).add(fill2.inputAmount)), + }, + }; + const expectedRealizedLpFees: RunningBalances = { + [repaymentChainId]: { + [l1Token_1.address]: lpFee1.add(lpFee2), + }, + }; + expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); + expect(expectedRealizedLpFees).to.deep.equal(merkleRoot1.realizedLpFees); + }); + it("Adds bundle slow fills to destination chain running balances", async function () { + await depositV3( spokePool_1, destinationChainId, depositor, @@ -137,1736 +171,332 @@ describe("Dataworker: Build merkle roots", async function () { erc20_2.address, amountToDeposit ); - - const deposit4 = await depositV3( - spokePool_2, - originChainId, + await updateAllClients(); + const deposit = spokePoolClients[originChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + await requestSlowFill(spokePool_2, relayer, deposit); + await updateAllClients(); + const slowFillRequest = spokePoolClients[destinationChainId].getSlowFillRequestsForOriginChain(originChainId)[0]; + const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); + + // Slow fills should not add to bundle LP fees. + const lpFeePct = ( + await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) + ).realizedLpFeePct; + const lpFee = lpFeePct.mul(slowFillRequest.inputAmount).div(fixedPointAdjustment); + const expectedRunningBalances: RunningBalances = { + [originChainId]: { + [l1Token_1.address]: deposit.inputAmount.mul(-1), + }, + [destinationChainId]: { + [l1Token_1.address]: slowFillRequest.inputAmount.sub(lpFee), + }, + }; + expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); + expect({}).to.deep.equal(merkleRoot1.realizedLpFees); + }); + it("Subtracts unexecutable slow fill amounts from destination chain running balances", async function () { + // Send slow fill in first bundle block range: + await depositV3( + spokePool_1, + destinationChainId, depositor, - erc20_2.address, - amountToDeposit, erc20_1.address, + amountToDeposit, + erc20_2.address, amountToDeposit ); - await hubPoolClient.update(); - - // Reuest slow fills for each deposit so dataworker includes deposits as slow relays. - await requestSlowFill(spokePool_2, relayer, deposit1); - await requestSlowFill(spokePool_1, relayer, deposit2); - await requestSlowFill(spokePool_2, relayer, deposit3); - await requestSlowFill(spokePool_1, relayer, deposit4); + await updateAllClients(); + const deposit = spokePoolClients[originChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + await requestSlowFill(spokePool_2, relayer, deposit); + await updateAllClients(); - const deposits = [deposit1, deposit2, deposit3, deposit4]; - const lpFees = await hubPoolClient.batchComputeRealizedLpFeePct( - deposits.map((deposit) => ({ ...deposit, paymentChainId: deposit.destinationChainId })) + // Propose first bundle with a destination chain block range that includes up to the slow fiil block. + const slowFillRequest = spokePoolClients[destinationChainId].getSlowFillRequestsForOriginChain(originChainId)[0]; + const destinationChainBlockRange = [0, slowFillRequest.blockNumber]; + const blockRange1 = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.map( + () => destinationChainBlockRange ); + const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(blockRange1, spokePoolClients); + + const lpFeePct = ( + await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) + ).realizedLpFeePct; + const lpFee = lpFeePct.mul(slowFillRequest.inputAmount).div(fixedPointAdjustment); + const expectedRunningBalances: RunningBalances = { + [originChainId]: { + [l1Token_1.address]: deposit.inputAmount.mul(-1), + }, + [destinationChainId]: { + [l1Token_1.address]: slowFillRequest.inputAmount.sub(lpFee), + }, + }; + expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); + expect({}).to.deep.equal(merkleRoot1.realizedLpFees); - // Slow relays should be sorted by origin chain ID and deposit ID. - const expectedSlowRelayLeaves = [ - ...buildV3SlowRelayLeaves([deposit1], lpFees[0].realizedLpFeePct), - ...buildV3SlowRelayLeaves([deposit3], lpFees[2].realizedLpFeePct), - ...buildV3SlowRelayLeaves([deposit2], lpFees[1].realizedLpFeePct), - ...buildV3SlowRelayLeaves([deposit4], lpFees[3].realizedLpFeePct), - ]; - - // Returns expected merkle root where leaves are ordered by origin chain ID and then deposit ID - // (ascending). + // Send a fast fill in a second bundle block range. + await fillV3(spokePool_2, relayer, deposit, repaymentChainId); await updateAllClients(); - const expectedMerkleRoot1 = await buildV3SlowRelayTree(expectedSlowRelayLeaves); - const merkleRoot1 = (await dataworkerInstance.buildSlowRelayRoot(getDefaultBlockRange(0), spokePoolClients)).tree; - expect(merkleRoot1.getHexRoot()).to.equal(expectedMerkleRoot1.getHexRoot()); - - // Speeding up a deposit should have no effect on the slow root: - await updateDeposit( - spokePool_1, - { - ...deposit1, - updatedMessage: deposit1.message, - updatedOutputAmount: deposit1.outputAmount.sub(1), - updatedRecipient: deposit1.recipient, + const fill = spokePoolClients[destinationChainId] + .getFills() + .filter(sdkUtils.isV3Fill)[0]; + expect(fill.relayExecutionInfo.fillType).to.equal(interfaces.FillType.ReplacedSlowFill); + const blockRange2 = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.map((_chain, index) => [ + blockRange1[index][1] + 1, + getDefaultBlockRange(2)[index][1], + ]); + const merkleRoot2 = await dataworkerInstance.buildPoolRebalanceRoot(blockRange2, spokePoolClients); + + // Add fill to repayment chain running balance and remove slow fill amount from destination chain. + const slowFillAmount = lpFee.mul(-1).add(fill.inputAmount); + const expectedRunningBalances2: RunningBalances = { + // Note: There should be no origin chain entry here since there were no deposits. + [destinationChainId]: { + [l1Token_1.address]: slowFillAmount.mul(-1), + }, + [repaymentChainId]: { + [l1Token_1.address]: slowFillAmount, }, - depositor + }; + const expectedRealizedLpFees2: RunningBalances = { + [repaymentChainId]: { + [l1Token_1.address]: lpFee, + }, + }; + expect(expectedRunningBalances2).to.deep.equal(merkleRoot2.runningBalances); + expect(expectedRealizedLpFees2).to.deep.equal(merkleRoot2.realizedLpFees); + }); + it("Adds expired deposits to origin chain running balances", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(2), + spokePoolClients ); + const elapsedFillDeadline = bundleBlockTimestamps[destinationChainId][1] - 1; - await updateAllClients(); - expect(merkleRoot1.getHexRoot()).to.equal((await buildV3SlowRelayTree(expectedSlowRelayLeaves)).getHexRoot()); - - // Fill deposits such that there are no unfilled deposits remaining. - await fillV3Relay(spokePool_2, deposit1, relayer); - await fillV3Relay(spokePool_1, deposit2, relayer); - await fillV3Relay(spokePool_2, deposit3, relayer); - await fillV3Relay(spokePool_1, deposit4, relayer); - await updateAllClients(); - expect( - (await dataworkerInstance.buildSlowRelayRoot(getDefaultBlockRange(1), spokePoolClients)).leaves - ).to.deep.equal([]); - expect( - (await dataworkerInstance.buildSlowRelayRoot(getDefaultBlockRange(2), spokePoolClients)).tree.getHexRoot() - ).to.equal(EMPTY_MERKLE_ROOT); - - // Includes slow fills triggered by "zero" (i.e. 1 wei) fills - const deposit5 = await depositV3( - spokePool_2, - originChainId, + await depositV3( + spokePool_1, + destinationChainId, depositor, + erc20_1.address, + amountToDeposit, erc20_2.address, amountToDeposit, + { + fillDeadline: elapsedFillDeadline, + } + ); + await updateAllClients(); + const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); + + // Origin chain running balance is incremented by refunded deposit which cancels out the subtraction for + // bundle deposit. + const expectedRunningBalances: RunningBalances = { + // Note: There should be no origin chain entry here since there were no deposits. + [originChainId]: { + [l1Token_1.address]: BigNumber.from(0), + }, + }; + expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); + expect({}).to.deep.equal(merkleRoot1.realizedLpFees); + }); + it("Adds fills to relayer refund root", async function () { + await depositV3( + spokePool_1, + destinationChainId, + depositor, erc20_1.address, + amountToDeposit, + erc20_2.address, amountToDeposit ); - - // Trigger slow fill with a partial fill: - await requestSlowFill(spokePool_1, relayer, deposit5); await updateAllClients(); - const merkleRoot2 = await dataworkerInstance.buildSlowRelayRoot(getDefaultBlockRange(3), spokePoolClients); - const { realizedLpFeePct: lpFeePct } = await hubPoolClient.computeRealizedLpFeePct({ - ...deposit5, - paymentChainId: deposit5.destinationChainId, - }); - const expectedMerkleRoot2 = await buildV3SlowRelayTree(buildV3SlowRelayLeaves([deposit5], lpFeePct)); - expect(merkleRoot2.tree.getHexRoot()).to.equal(expectedMerkleRoot2.getHexRoot()); - }); - - describe("Build relayer refund root", function () { - it("amountToReturn is 0", async function () { - // Set spoke target balance thresholds above deposit amounts so that amountToReturn is always 0. - await configStore.updateTokenConfig( - l1Token_1.address, - JSON.stringify({ - rateModel: sampleRateModel, - spokeTargetBalances: { - [originChainId]: { - // Threshold above the deposit amount. - threshold: amountToDeposit.mul(10).toString(), - target: amountToDeposit.div(2).toString(), - }, - [destinationChainId]: { - // Threshold above the deposit amount. - threshold: amountToDeposit.mul(10).toString(), - target: amountToDeposit.div(2).toString(), - }, - }, - }) - ); - await updateAllClients(); - const poolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(0), - spokePoolClients - ); - expect( - ( - await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(0), - spokePoolClients, - poolRebalanceRoot.leaves, - poolRebalanceRoot.runningBalances - ) - ).leaves - ).to.deep.equal([]); - expect( - ( - await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(0), - spokePoolClients, - poolRebalanceRoot.leaves, - poolRebalanceRoot.runningBalances - ) - ).tree.getHexRoot() - ).to.equal(EMPTY_MERKLE_ROOT); - - // Submit deposits for multiple L2 tokens. - const deposit1 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - const deposit2 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - const deposit3 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit - ); - - // Submit fills for two relayers on one repayment chain and one destination token. Note: we know that - // depositor address is alphabetically lower than relayer address, so submit fill from depositor first and test - // that data worker sorts on refund address. - await enableRoutesOnHubPool(hubPool, [ - { destinationChainId: 100, destinationToken: erc20_2, l1Token: l1Token_1 }, - { destinationChainId: 99, destinationToken: erc20_1, l1Token: l1Token_1 }, - { destinationChainId: 98, destinationToken: erc20_1, l1Token: l1Token_1 }, - ]); - await updateAllClients(); - await buildFillForRepaymentChain(spokePool_2, depositor, deposit2, 0.25, destinationChainId); - await buildFillForRepaymentChain(spokePool_2, depositor, deposit2, 1, destinationChainId); - await buildFillForRepaymentChain(spokePool_2, relayer, deposit1, 0.25, destinationChainId); - await buildFillForRepaymentChain(spokePool_2, relayer, deposit1, 1, destinationChainId); - - const depositorBeforeRelayer = toBN(depositor.address).lt(toBN(relayer.address)); - const leaf1 = { - chainId: destinationChainId, - amountToReturn: toBN(0), - l2TokenAddress: erc20_2.address, - refundAddresses: [ - depositorBeforeRelayer ? depositor.address : relayer.address, - depositorBeforeRelayer ? relayer.address : depositor.address, - ], // Sorted ascending alphabetically - refundAmounts: [ - getRefund(deposit1.amount, deposit1.realizedLpFeePct), - getRefund(deposit3.amount, deposit3.realizedLpFeePct), - ], // Refund amounts should aggregate across all fills. - }; - - await updateAllClients(); - const poolRebalanceRoot1 = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(1), - spokePoolClients - ); - const merkleRoot1 = ( - await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(1), - spokePoolClients, - poolRebalanceRoot1.leaves, - poolRebalanceRoot1.runningBalances - ) - ).tree; - const expectedMerkleRoot1 = await buildRelayerRefundTreeWithUnassignedLeafIds([leaf1]); - expect(merkleRoot1.getHexRoot()).to.equal(expectedMerkleRoot1.getHexRoot()); - - // Submit fills for multiple repayment chains. Note: Send the fills for destination tokens in the - // reverse order of the fills we sent above to test that the data worker is correctly sorting leaves - // by L2 token address in ascending order. Also set repayment chain ID lower than first few leaves to test - // that these leaves come first. - // Note: We can set a repayment chain for this fill because it is a full fill. - await buildFillForRepaymentChain(spokePool_1, relayer, deposit3, 1, 99); - const leaf2 = { - chainId: 99, - amountToReturn: toBN(0), - l2TokenAddress: erc20_1.address, - refundAddresses: [relayer.address], - refundAmounts: [getRefund(deposit3.amount, deposit3.realizedLpFeePct)], - }; - await updateAllClients(); - const poolRebalanceRoot2 = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(2), - spokePoolClients - ); - const merkleRoot2 = ( - await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(2), - spokePoolClients, - poolRebalanceRoot2.leaves, - poolRebalanceRoot2.runningBalances - ) - ).tree; - const expectedMerkleRoot2 = await buildRelayerRefundTreeWithUnassignedLeafIds([leaf2, leaf1]); - expect(merkleRoot2.getHexRoot()).to.equal(expectedMerkleRoot2.getHexRoot()); - - // Splits leaf into multiple leaves if refunds > MAX_REFUNDS_PER_RELAYER_REFUND_LEAF. - const deposit4 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - // @dev: slice(10) so we don't have duplicates between allSigners and depositor/relayer/etc. - const allSigners: SignerWithAddress[] = (await ethers.getSigners()).slice(10); - expect( - allSigners.length >= MAX_REFUNDS_PER_RELAYER_REFUND_LEAF + 1, - "ethers.getSigners doesn't have enough signers" - ).to.be.true; - for (let i = 0; i < MAX_REFUNDS_PER_RELAYER_REFUND_LEAF + 1; i++) { - await setupTokensForWallet(spokePool_2, allSigners[i], [erc20_2]); - await buildFillForRepaymentChain(spokePool_2, allSigners[i], deposit4, 0.01 + i * 0.01, destinationChainId); - } - // Note: Higher refund amounts for same chain and L2 token should come first, so we test that by increasing - // the fill amount in the above loop for each fill. Ultimately, the latest fills send the most tokens and - // should come in the first leaf. - await updateAllClients(); - const poolRebalanceRoot3 = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(3), - spokePoolClients - ); - const merkleRoot3 = ( - await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(3), - spokePoolClients, - poolRebalanceRoot3.leaves, - poolRebalanceRoot3.runningBalances - ) - ).tree; - - // The order should be: - // - Sort by repayment chain ID in ascending order, so leaf2 goes first since its the only one with an overridden - // repayment chain ID. - // - Sort by refund amount. So, the refund addresses in leaf1 go first, then the latest refunds. Each leaf can - // have a maximum number of refunds so add the latest refund (recall the latest fills from the last loop - // were the largest). - leaf1.refundAddresses.push(allSigners[3].address); - leaf1.refundAmounts.push( - getRefund(deposit4.amount, deposit4.realizedLpFeePct).mul(toBNWei("0.04")).div(toBNWei("1")) - ); - const leaf3 = { - chainId: destinationChainId, - amountToReturn: toBN(0), - l2TokenAddress: erc20_2.address, - refundAddresses: [allSigners[2].address, allSigners[1].address, allSigners[0].address], - refundAmounts: [ - getRefund(deposit4.amount, deposit4.realizedLpFeePct).mul(toBNWei("0.03")).div(toBNWei("1")), - getRefund(deposit4.amount, deposit4.realizedLpFeePct).mul(toBNWei("0.02")).div(toBNWei("1")), - getRefund(deposit4.amount, deposit4.realizedLpFeePct).mul(toBNWei("0.01")).div(toBNWei("1")), - ], - }; - const expectedMerkleRoot3 = await buildRelayerRefundTreeWithUnassignedLeafIds([leaf2, leaf1, leaf3]); - expect(merkleRoot3.getHexRoot()).to.equal(expectedMerkleRoot3.getHexRoot()); - - // TODO: Add V3 fill to refund and expired deposit to see if they get added to relayer refund root. - }); - it("amountToReturn is non 0", async function () { - await updateAllClients(); - - // Submit 1 deposit to make `netSendAmount` for one chain negative. - await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - - await enableRoutesOnHubPool(hubPool, [ - { destinationChainId: 100, destinationToken: erc20_2, l1Token: l1Token_1 }, - ]); - await updateAllClients(); + const deposit = spokePoolClients[originChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + await fillV3(spokePool_2, relayer, deposit, repaymentChainId); + await updateAllClients(); + const { runningBalances, leaves } = await dataworkerInstance.buildPoolRebalanceRoot( + getDefaultBlockRange(2), + spokePoolClients + ); + const merkleRoot1 = await dataworkerInstance.buildRelayerRefundRoot( + getDefaultBlockRange(2), + spokePoolClients, + leaves, + runningBalances + ); - // Since there was 1 unfilled deposit, there should be 1 relayer refund root for the deposit origin chain - // where amountToReturn = -netSendAmount. - const leaf1 = { + // Origin chain should have negative running balance and therefore positive amount to return. + const lpFeePct = ( + await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) + ).realizedLpFeePct; + const lpFee = lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); + const refundAmount = deposit.inputAmount.sub(lpFee); + const expectedLeaves: RelayerRefundLeaf[] = [ + { chainId: originChainId, - amountToReturn: amountToDeposit, + amountToReturn: runningBalances[originChainId][l1Token_1.address].mul(-1), l2TokenAddress: erc20_1.address, + leafId: 0, refundAddresses: [], refundAmounts: [], - }; + }, + { + chainId: repaymentChainId, + amountToReturn: bnZero, + l2TokenAddress: l1Token_1.address, + leafId: 1, + refundAddresses: [relayer.address], + refundAmounts: [refundAmount], + }, + ]; + expect(expectedLeaves).to.deep.equal(merkleRoot1.leaves); + }); + it("All fills are slow fill executions", async function () { + await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit + ); + await updateAllClients(); + const deposit = spokePoolClients[originChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + const lpFeePct = ( + await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) + ).realizedLpFeePct; + const slowFills = buildV3SlowRelayLeaves([deposit], lpFeePct); + + // Relay slow root to destination chain + const slowFillTree = await buildV3SlowRelayTree(slowFills); + await spokePool_2.relayRootBundle(mockTreeRoot, slowFillTree.getHexRoot()); + await erc20_2.mint(spokePool_2.address, deposit.inputAmount); + await spokePool_2.executeV3SlowRelayLeaf(slowFills[0], 0, slowFillTree.getHexProof(slowFills[0])); + await updateAllClients(); + const poolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot( + getDefaultBlockRange(2), + spokePoolClients + ); - await updateAllClients(); - const poolRebalanceRoot1 = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(0), - spokePoolClients - ); - const merkleRoot1 = ( - await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(0), - spokePoolClients, - poolRebalanceRoot1.leaves, - poolRebalanceRoot1.runningBalances - ) - ).tree; - const expectedMerkleRoot1 = await buildRelayerRefundTreeWithUnassignedLeafIds([leaf1]); - expect(merkleRoot1.getHexRoot()).to.equal(expectedMerkleRoot1.getHexRoot()); + // Slow fill executions should add to bundle LP fees, but not refunds. So, there should be + // change to running balances on the destination chain, but there should be an entry for it + // so that bundle lp fees get accumulated. + const lpFee = lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); + const expectedRunningBalances: RunningBalances = { + [originChainId]: { + [l1Token_1.address]: deposit.inputAmount.mul(-1), + }, + [destinationChainId]: { + [l1Token_1.address]: toBN(0), + }, + }; + const expectedRealizedLpFees: RunningBalances = { + [destinationChainId]: { + [l1Token_1.address]: lpFee, + }, + }; + expect(expectedRunningBalances).to.deep.equal(poolRebalanceRoot.runningBalances); + expect(expectedRealizedLpFees).to.deep.equal(poolRebalanceRoot.realizedLpFees); - // Now, submit fills on the origin chain such that the refunds for the origin chain need to be split amongst - // more than one leaves. Moreover, make sure not to fully fill the deposit so that the netSendAmount is negative, - // and check that the amountToReturn is 0 for all except the first leaf. - const deposit1 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit - ); - const allSigners: SignerWithAddress[] = await ethers.getSigners(); - expect( - allSigners.length >= MAX_REFUNDS_PER_RELAYER_REFUND_LEAF + 1, - "ethers.getSigners doesn't have enough signers" - ).to.be.true; - const sortedAllSigners = [...allSigners].sort((x, y) => compareAddresses(x.address, y.address)); - const fills = []; - for (let i = 0; i < MAX_REFUNDS_PER_RELAYER_REFUND_LEAF + 1; i++) { - await setupTokensForWallet(spokePool_1, sortedAllSigners[i], [erc20_1]); - fills.push(await buildFillForRepaymentChain(spokePool_1, sortedAllSigners[i], deposit1, 0.1, originChainId)); - } - const unfilledAmount = getRefund( - amountToDeposit.sub(fills[fills.length - 1].totalFilledAmount), - deposit1.realizedLpFeePct - ); - const refundAmountPerFill = getRefund(deposit1.amount, deposit1.realizedLpFeePct) - .mul(toBNWei("0.1")) - .div(toBNWei("1")); - const newLeaf1 = { - chainId: originChainId, - // amountToReturn should be deposit amount minus ALL fills for origin chain minus unfilled amount for slow fill. - amountToReturn: amountToDeposit - .sub(refundAmountPerFill.mul(toBN(MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF + 1))) - .sub(unfilledAmount), - l2TokenAddress: erc20_1.address, - refundAddresses: sortedAllSigners.slice(0, MAX_REFUNDS_PER_RELAYER_REFUND_LEAF).map((x) => x.address), - refundAmounts: Array(MAX_REFUNDS_PER_RELAYER_REFUND_LEAF).fill(refundAmountPerFill), - }; - const leaf2 = { + const refundRoot = await dataworkerInstance.buildRelayerRefundRoot( + getDefaultBlockRange(2), + spokePoolClients, + poolRebalanceRoot.leaves, + poolRebalanceRoot.runningBalances + ); + const expectedLeaves: RelayerRefundLeaf[] = [ + { chainId: originChainId, - amountToReturn: toBN(0), + amountToReturn: poolRebalanceRoot.runningBalances[originChainId][l1Token_1.address].mul(-1), l2TokenAddress: erc20_1.address, - refundAddresses: [sortedAllSigners[MAX_REFUNDS_PER_RELAYER_REFUND_LEAF].address], - refundAmounts: [refundAmountPerFill], - }; - - // There should also be a new leaf for the second deposit we submitted on the destination chain. - const leaf3 = { - chainId: destinationChainId, - amountToReturn: amountToDeposit, - l2TokenAddress: erc20_2.address, + leafId: 0, refundAddresses: [], refundAmounts: [], - }; - - await updateAllClients(); - const poolRebalanceRoot2 = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(1), - spokePoolClients - ); - const merkleRoot2 = ( - await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(1), - spokePoolClients, - poolRebalanceRoot2.leaves, - poolRebalanceRoot2.runningBalances - ) - ).tree; - const expectedMerkleRoot2 = await buildRelayerRefundTreeWithUnassignedLeafIds([newLeaf1, leaf2, leaf3]); - expect(merkleRoot2.getHexRoot()).to.equal(expectedMerkleRoot2.getHexRoot()); - }); + }, + // No leaf for destination chain. + ]; + expect(expectedLeaves).to.deep.equal(refundRoot.leaves); }); - describe("Build pool rebalance root", function () { - it("One L1 token full lifecycle: testing runningBalances and realizedLpFees counters", async function () { - // Helper function we'll use in this lifecycle test to keep track of updated counter variables. - const updateAndCheckExpectedPoolRebalanceCounters = ( - expectedRunningBalances: RunningBalances, - expectedRealizedLpFees: RunningBalances, - runningBalanceDelta: BigNumber, - realizedLpFeeDelta: BigNumber, - l2Chains: number[], - l1Tokens: string[], - test: { runningBalances: RunningBalances; realizedLpFees: RunningBalances } - ): void => { - l2Chains.forEach((l2Chain: number) => - l1Tokens.forEach((l1Token: string) => { - expectedRunningBalances[l2Chain][l1Token] = - expectedRunningBalances[l2Chain][l1Token].add(runningBalanceDelta); - expectedRealizedLpFees[l2Chain][l1Token] = expectedRealizedLpFees[l2Chain][l1Token].add(realizedLpFeeDelta); - }) - ); - expect(test.runningBalances).to.deep.equal(expectedRunningBalances); - expect(test.realizedLpFees).to.deep.equal(expectedRealizedLpFees); - }; - - await updateAllClients(); - expect( - (await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(0), spokePoolClients)).leaves - ).to.deep.equal([]); - expect( - (await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(0), spokePoolClients)).tree.getHexRoot() - ).to.equal(EMPTY_MERKLE_ROOT); - - // Submit deposits for multiple L2 tokens. - const deposit1 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - const deposit2 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit.mul(toBN(2)) - ); - const deposit3 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit - ); - await updateAllClients(); - - const deposits = [deposit1, deposit2, deposit3]; - - // Note: Submit fills with repayment chain set to one of the origin or destination chains since we have spoke - // pools deployed on those chains. - - // Partial fill deposit1 - const fill1 = await buildFillForRepaymentChain(spokePool_2, relayer, deposit1, 0.5, destinationChainId); - const fill1Block = await spokePool_2.provider.getBlockNumber(); - const unfilledAmount1 = getRefund(deposit1.amount.sub(fill1.totalFilledAmount), fill1.realizedLpFeePct); - - // Partial fill deposit2 - const fill2 = await buildFillForRepaymentChain(spokePool_1, depositor, deposit2, 0.3, originChainId); - const fill3 = await buildFillForRepaymentChain(spokePool_1, depositor, deposit2, 0.2, originChainId); - const unfilledAmount3 = getRefund(deposit2.amount.sub(fill3.totalFilledAmount), fill3.realizedLpFeePct); - - // Partial fill deposit3 - const fill4 = await buildFillForRepaymentChain(spokePool_1, depositor, deposit3, 0.5, originChainId); - const unfilledAmount4 = getRefund(deposit3.amount.sub(fill4.totalFilledAmount), fill4.realizedLpFeePct); - const blockOfLastFill = await hubPool.provider.getBlockNumber(); - - // Prior to root bundle being executed, running balances should be: - // - deposited amount - // + partial fill refund - // + slow fill amount - const expectedRunningBalances: RunningBalances = { - [destinationChainId]: { - [l1Token_1.address]: getRefundForFills([fill1]) - .sub(deposit2.amount.add(deposit3.amount)) - .add(unfilledAmount1), - }, - [originChainId]: { - [l1Token_1.address]: getRefundForFills([fill2, fill3, fill4]) - .sub(deposit1.amount) - .add(unfilledAmount3) - .add(unfilledAmount4), - }, - }; - const expectedRealizedLpFees: RunningBalances = { - [destinationChainId]: { - [l1Token_1.address]: getRealizedLpFeeForFills([fill1]), - }, - [originChainId]: { - [l1Token_1.address]: getRealizedLpFeeForFills([fill2, fill3, fill4]), - }, - }; - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(1), spokePoolClients); - expect(merkleRoot1.runningBalances).to.deep.equal(expectedRunningBalances); - expect(merkleRoot1.realizedLpFees).to.deep.equal(expectedRealizedLpFees); - - // Construct slow relay root which should include deposit1 and deposit2 - const slowRelayLeaves = buildSlowRelayLeaves(deposits); - const expectedSlowRelayTree = await buildSlowRelayTree(slowRelayLeaves); - - // Construct pool rebalance root so we can publish slow relay root to spoke pool: - const { tree: poolRebalanceTree, leaves: poolRebalanceLeaves } = await constructPoolRebalanceTree( - expectedRunningBalances, - expectedRealizedLpFees - ); - - // Propose root bundle so that we can test that the dataworker looks up ProposeRootBundle events properly. - await hubPool.connect(dataworker).proposeRootBundle( - Array(CHAIN_ID_TEST_LIST.length).fill(blockOfLastFill), // Set current block number as end block for bundle. Its important that we set a block for every possible chain ID. - // so all fills to this point are before these blocks. - poolRebalanceLeaves.length, // poolRebalanceLeafCount. - poolRebalanceTree.getHexRoot(), // poolRebalanceRoot - mockTreeRoot, // relayerRefundRoot - expectedSlowRelayTree.getHexRoot() // slowRelayRoot - ); - const pendingRootBundle = await hubPool.rootBundleProposal(); - - // Execute root bundle so that this root bundle is not ignored by dataworker. - await timer.connect(dataworker).setCurrentTime(pendingRootBundle.challengePeriodEndTimestamp + 1); - for (const leaf of poolRebalanceLeaves) { - await hubPool - .connect(dataworker) - .executeRootBundle(...Object.values(leaf), poolRebalanceTree.getHexProof(leaf)); - } - - await updateAllClients(); - for (const leaf of poolRebalanceLeaves) { - const { runningBalance } = hubPoolClient.getRunningBalanceBeforeBlockForChain( - await hubPool.provider.getBlockNumber(), - leaf.chainId.toNumber(), - l1Token_1.address - ); - // Since we fully executed the root bundle, we need to add the running balance for the chain to the expected - // running balances since the data worker adds these prior running balances. - expectedRunningBalances[leaf.chainId.toNumber()][l1Token_1.address] = - expectedRunningBalances[leaf.chainId.toNumber()][l1Token_1.address].add(runningBalance); - } - - // Execute 1 slow relay leaf: - // Before we can execute the leaves on the spoke pool, we need to actually publish them since we're using a mock - // adapter that doesn't send messages as you'd expect in executeRootBundle. - await spokePool_1.relayRootBundle(mockTreeRoot, expectedSlowRelayTree.getHexRoot()); - const chainToExecuteSlowRelay = await spokePool_1.chainId(); - const slowFill2 = await buildSlowFill( - spokePool_1, - fill3, - dataworker, - expectedSlowRelayTree.getHexProof( - // We can only execute a slow relay on its destination chain, so look up the relay data with - // destination chain equal to the deployed spoke pool that we are calling. - slowRelayLeaves.find((_) => _.relayData.destinationChainId === chainToExecuteSlowRelay.toString()) - ) - ); - await updateAllClients(); - - // The fill that fully executed the deposit was the slow fill, however, there were no partial fills sent - // between when the slow fill amount was sent to the spoke pool (i.e. the unfilledAmount3), and the slow - // fill execution. Therefore, the excess is 0. The unfilledAmount3 also needs to be subtracted from the - // running balances since slowFill2 fully filled the matching deposit. - updateAndCheckExpectedPoolRebalanceCounters( - expectedRunningBalances, - expectedRealizedLpFees, - unfilledAmount3.mul(toBN(-1)), - getRealizedLpFeeForFills([slowFill2]), - [slowFill2.destinationChainId], - [l1Token_1.address], - await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients) - ); - - // Now, partially fill a deposit whose slow fill has NOT been executed yet. - const fill5 = await buildFillForRepaymentChain(spokePool_2, relayer, deposit1, 0.25, destinationChainId); - await updateAllClients(); - - // Execute second slow relay leaf: - // Before we can execute the leaves on the spoke pool, we need to actually publish them since we're using a mock - // adapter that doesn't send messages as you'd expect in executeRootBundle. - await spokePool_2.relayRootBundle(mockTreeRoot, expectedSlowRelayTree.getHexRoot()); - const secondChainToExecuteSlowRelay = await spokePool_2.chainId(); - const slowFill1 = await buildSlowFill( - spokePool_2, - fill5, - dataworker, - expectedSlowRelayTree.getHexProof( - // We can only execute a slow relay on its destination chain, so look up the relay data with - // destination chain equal to the deployed spoke pool that we are calling. - slowRelayLeaves.find((_) => _.relayData.destinationChainId === secondChainToExecuteSlowRelay.toString()) - ) - ); - await updateAllClients(); - - // Test that we can still look up the excess if the first fill for the same deposit as the one slow filed - // is older than the spoke pool client's lookback window. - const { spy, spyLogger } = createSpyLogger(); - const shortRangeSpokePoolClient = new SpokePoolClient( - spyLogger, - spokePool_2, - hubPoolClient, - destinationChainId, - spokePoolClients[destinationChainId].deploymentBlock, - { fromBlock: fill1Block + 1 } // Set fromBlock to now, after first fill for same deposit as the slowFill1 - ); - await shortRangeSpokePoolClient.update(); - expect(shortRangeSpokePoolClient.getFills().length).to.equal(2); // We should only be able to see 2 fills - await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(3), { - ...spokePoolClients, - [destinationChainId]: shortRangeSpokePoolClient, - }); - - // Should have queried for historical fills that it can no longer see. - expect(lastSpyLogIncludes(spy, "Queried for partial fill that triggered an unused slow fill")).to.be.true; - - // The excess amount in the contract is now equal to the partial fill amount sent before the slow fill. - // Again, now that the slowFill1 was sent, the unfilledAmount1 can be subtracted from running balances since its - // no longer associated with an unfilled deposit. - const excess = getRefund(fill5.fillAmount, fill5.realizedLpFeePct); - - updateAndCheckExpectedPoolRebalanceCounters( - expectedRunningBalances, - expectedRealizedLpFees, - getRefundForFills([fill5]).sub(unfilledAmount1).sub(excess), - getRealizedLpFeeForFills([slowFill1, fill5]), - [fill5.destinationChainId], - [l1Token_1.address], - await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(4), spokePoolClients) - ); - - // Before executing the last slow relay leaf, completely fill the deposit. This will leave the full slow fill - // amount remaining in the spoke pool. We also need to subtract running balances by the unfilled amount - // for deposit3, because its now fully filled. This means we need to subtract the unfilledAmount4 twice - // from running balances. - const fill6 = await buildFillForRepaymentChain(spokePool_1, relayer, deposit3, 1, originChainId); - await updateAllClients(); - updateAndCheckExpectedPoolRebalanceCounters( - expectedRunningBalances, - expectedRealizedLpFees, - getRefundForFills([fill6]).sub(unfilledAmount4.mul(toBN(2))), - getRealizedLpFeeForFills([fill6]), - [fill6.destinationChainId], - [l1Token_1.address], - await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(5), spokePoolClients) - ); - - // Now demonstrate that for a deposit whose first fill is NOT contained in a ProposeRootBundle event, it won't - // affect running balance: - // Submit another deposit and partial fill, this should mine at a block after the ending block of the last - // ProposeRootBundle block range. - // Fully fill the deposit. - // Update client and construct root. This should increase running balance by total deposit amount, to refund - // the slow relay. - const deposit4 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit - ); - expectedRunningBalances[deposit4.originChainId][l1Token_1.address] = expectedRunningBalances[ - deposit4.originChainId - ][l1Token_1.address].sub(deposit4.amount); - - const fill7 = await buildFillForRepaymentChain(spokePool_1, depositor, deposit4, 0.5, originChainId); - const fill8 = await buildFillForRepaymentChain(spokePool_1, depositor, deposit4, 1, originChainId); - await updateAllClients(); - updateAndCheckExpectedPoolRebalanceCounters( - expectedRunningBalances, - expectedRealizedLpFees, - getRefundForFills([fill7, fill8]), - getRealizedLpFeeForFills([fill7, fill8]), - [fill7.destinationChainId], - [l1Token_1.address], - await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(6), spokePoolClients) - ); - - // Even after a ProposeRootBundle is submitted with a block range containing both fill7 and fill8, nothing changes - // because the fills are contained in the same bundle. - await hubPool.connect(dataworker).proposeRootBundle( - Array(CHAIN_ID_TEST_LIST.length).fill(await hubPool.provider.getBlockNumber()), - 1, - mockTreeRoot, // Roots don't matter in this test - mockTreeRoot, - mockTreeRoot - ); - await updateAllClients(); - const merkleRoot7 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(7), spokePoolClients); - expect(merkleRoot7.runningBalances).to.deep.equal(expectedRunningBalances); - expect(merkleRoot7.realizedLpFees).to.deep.equal(expectedRealizedLpFees); - - // A full fill not following any partial fills is treated as a normal fill: increases the running balance. - const deposit5 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit - ); - expectedRunningBalances[deposit5.originChainId][l1Token_1.address] = expectedRunningBalances[ - deposit5.originChainId - ][l1Token_1.address].sub(deposit5.amount); - - const fill9 = await buildFillForRepaymentChain(spokePool_1, depositor, deposit5, 1, originChainId); - await updateAllClients(); - updateAndCheckExpectedPoolRebalanceCounters( - expectedRunningBalances, - expectedRealizedLpFees, - getRefundForFills([fill9]), - getRealizedLpFeeForFills([fill9]), - [fill9.destinationChainId], - [l1Token_1.address], - await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(8), spokePoolClients) - ); - }); - it("Loads fills needed to compute slow fill excesses", async function () { - await updateAllClients(); - - // Send deposit - // Send two partial fills - const deposit1 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - await buildFillForRepaymentChain(spokePool_2, relayer, deposit1, 0.5, destinationChainId); - const lastFillBeforeSlowFill = await buildFillForRepaymentChain( - spokePool_2, - relayer, - deposit1, - 0.25, - destinationChainId - ); - const fill2Block = await spokePool_2.provider.getBlockNumber(); - - // Produce bundle and execute pool leaves. Should produce a slow fill. Don't execute it. - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(1), spokePoolClients); - await hubPool - .connect(dataworker) - .proposeRootBundle( - Array(CHAIN_ID_TEST_LIST.length).fill(fill2Block), - merkleRoot1.leaves.length, - merkleRoot1.tree.getHexRoot(), - mockTreeRoot, - mockTreeRoot - ); - - const pendingRootBundle = await hubPool.rootBundleProposal(); - await timer.connect(dataworker).setCurrentTime(pendingRootBundle.challengePeriodEndTimestamp + 1); - for (let i = 0; i < merkleRoot1.leaves.length; i++) { - const leaf = merkleRoot1.leaves[i]; - await hubPool - .connect(dataworker) - .executeRootBundle( - leaf.chainId, - leaf.groupIndex, - leaf.bundleLpFees, - leaf.netSendAmounts, - leaf.runningBalances, - i, - leaf.l1Tokens, - merkleRoot1.tree.getHexProof(leaf) - ); - } - - // Create new spoke client with a search range that would miss fill1. - const destinationChainSpokePoolClient = new SpokePoolClient( - createSpyLogger().spyLogger, - spokePool_2, - hubPoolClient, - destinationChainId, - spokePoolClients[destinationChainId].deploymentBlock, - { fromBlock: fill2Block + 1 } - ); - - // Send a third partial fill, this will produce an excess since a slow fill is already in flight for the deposit. - await buildFillForRepaymentChain(spokePool_2, relayer, deposit1, 0.25, destinationChainId); - await updateAllClients(); - await destinationChainSpokePoolClient.update(); - expect(destinationChainSpokePoolClient.getFills().length).to.equal(1); - const blockRange2 = Array(CHAIN_ID_TEST_LIST.length).fill([ - fill2Block + 1, - await spokePool_2.provider.getBlockNumber(), - ]); - await dataworkerInstance.buildPoolRebalanceRoot(blockRange2, { - ...spokePoolClients, - [destinationChainId]: destinationChainSpokePoolClient, - }); - expect(lastSpyLogIncludes(spy, "Fills triggering excess returns from L2")).to.be.true; - const expectedExcess = getRefund( - lastFillBeforeSlowFill.amount.sub(lastFillBeforeSlowFill.totalFilledAmount), - lastFillBeforeSlowFill.realizedLpFeePct - ); - expect( - spy.getCall(-1).lastArg.fillsTriggeringExcesses[destinationChainId][lastFillBeforeSlowFill.destinationToken][0] - .excess - ).to.equal(expectedExcess.toString()); - }); - it("Many L1 tokens, testing leaf order and root construction", async function () { - // In this test, each L1 token will have one deposit and fill associated with it. - const depositsForL1Token: { [l1Token: string]: Deposit } = {}; - const fillsForL1Token: { [l1Token: string]: Fill } = {}; - - for (let i = 0; i < MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF + 1; i++) { - const { l1Token, l2Token } = await deployNewTokenMapping( - depositor, - relayer, - spokePool_1, - spokePool_2, - configStore, - hubPool, - amountToDeposit.mul(toBN(100)) - ); + it("Adds expired deposit refunds to relayer refund root", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(2), + spokePoolClients + ); + const elapsedFillDeadline = bundleBlockTimestamps[destinationChainId][1] - 1; - await updateAllClients(); // Update client to be aware of new token mapping so we can build deposit correctly. - const deposit = await buildDeposit( - hubPoolClient, - spokePool_1, - l2Token, - l1Token, - depositor, - destinationChainId, - amountToDeposit.mul(i + 1) // Increase deposit amount each time so that L1 tokens - // all have different running balances. - ); - depositsForL1Token[l1Token.address] = deposit; - await updateAllClients(); - fillsForL1Token[l1Token.address] = await buildFillForRepaymentChain( - spokePool_2, - depositor, - deposit, - 1, - destinationChainId - ); + await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit, + { + fillDeadline: elapsedFillDeadline, } - const sortedL1Tokens = Object.keys(depositsForL1Token).sort((x, y) => compareAddresses(x, y)); - - // Since there are more L1 tokens than the max allowed per chain, dataworker should split the L1's into two - // leaves. There should be 4 L1 tokens for the origin and destination chain since a fill and deposit was sent - // for each newly created token mapping. Check that the leaves are sorted by L2 chain ID and then by L1 token - // address. - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(0), spokePoolClients); - const orderedChainIds = [originChainId, destinationChainId].sort((x, y) => x - y); - const expectedLeaves = orderedChainIds - .map((chainId) => { - // For each chain, there should be two leaves since we sent exactly 1 more deposit + fill than the max L1 - // token allowed per pool rebalance leaf. The first leaf will have the full capacity of L1 tokens and the second - // will have just 1. - return [ - sortedL1Tokens.slice(0, MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF), - [sortedL1Tokens[sortedL1Tokens.length - 1]], - ].map((l1TokensToIncludeInLeaf: string[]) => { - return { - chainId, - // Realized LP fees are 0 for origin chain since no fill was submitted to it, only deposits. - bundleLpFees: - chainId === originChainId - ? Array(l1TokensToIncludeInLeaf.length).fill(toBN(0)) - : l1TokensToIncludeInLeaf.map((l1Token) => getRealizedLpFeeForFills([fillsForL1Token[l1Token]])), - // Running balances are straightforward to compute because deposits are sent to origin chain and fills - // are sent to destination chain only. The spoke pool threshold is 0 by default so running balances - // should be 0. - netSendAmounts: - chainId === originChainId - ? l1TokensToIncludeInLeaf.map((l1Token) => depositsForL1Token[l1Token].amount.mul(toBN(-1))) - : l1TokensToIncludeInLeaf.map((l1Token) => getRefundForFills([fillsForL1Token[l1Token]])), - runningBalances: l1TokensToIncludeInLeaf.map(() => toBN(0)), - l1Tokens: l1TokensToIncludeInLeaf, - }; - }); - }) - .flat() - .map((leaf, i) => { - return { ...leaf, leafId: i }; - }); - - expect(merkleRoot1.leaves).excludingEvery(["groupIndex"]).to.deep.equal(expectedLeaves); - const expectedMerkleRoot = await buildPoolRebalanceLeafTree( - merkleRoot1.leaves.map((leaf) => { - return { ...leaf, chainId: toBN(leaf.chainId), groupIndex: toBN(leaf.groupIndex), leafId: toBN(leaf.leafId) }; - }) - ); - expect( - (await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(1), spokePoolClients)).tree.getHexRoot() - ).to.equal(expectedMerkleRoot.getHexRoot()); - }); - it("Two L1 tokens, one repayment", async function () { - // This test checks that the dataworker can handle the case where there are multiple L1 tokens on a chain but - // at least one of the L1 tokens doesn't have any `bundleLpFees`. Failing this test suggests that the dataworker - // is incorrectly assuming that all repayments occur on the same chain. - const { l1Token: l1TokenNew, l2Token: l2TokenNew } = await deployNewTokenMapping( - depositor, - relayer, - spokePool_1, - spokePool_2, - configStore, - hubPool, - amountToDeposit.mul(toBN(100)) - ); - await updateAllClients(); // Update client to be aware of new token mapping so we can build deposit correctly. - - const depositA = await buildDeposit( - hubPoolClient, - spokePool_1, - l2TokenNew, - l1TokenNew, - depositor, - destinationChainId, - amountToDeposit - ); - - // Send a second deposit on the same origin chain so that the pool rebalance leaf for the origin chain has - // running balances and bundle LP fees for two L1 tokens. This allows us to create the situation where one of - // the bundle LP fees and running balances is zero for an L1 token. - const depositB = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - - // Submit fill linked to new L1 token with repayment chain set to the origin chain. Since the deposits in this - // test were submitted on the origin chain, we want to be refunded on the same chain to force the dataworker - // to construct a leaf with multiple L1 tokens, only one of which has bundleLpFees. - const fillA = await buildFillForRepaymentChain(spokePool_2, depositor, depositA, 1, originChainId); + ); + await updateAllClients(); + const deposit = spokePoolClients[originChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + const { runningBalances, leaves } = await dataworkerInstance.buildPoolRebalanceRoot( + getDefaultBlockRange(2), + spokePoolClients + ); + const merkleRoot1 = await dataworkerInstance.buildRelayerRefundRoot( + getDefaultBlockRange(2), + spokePoolClients, + leaves, + runningBalances + ); - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(0), spokePoolClients); - const orderedL1Tokens = [l1Token_1.address, l1TokenNew.address].sort((addressA, addressB) => - compareAddresses(addressA, addressB) - ); - const expectedLeaf = { + // Origin chain running balance starts negative because of the deposit + // but is cancelled out by the refund such that the running balance is 0 + // and there is no amount to return. + const expectedLeaves: RelayerRefundLeaf[] = [ + { chainId: originChainId, - bundleLpFees: [ - orderedL1Tokens[0] === l1Token_1.address ? toBN(0) : getRealizedLpFeeForFills([fillA]), - orderedL1Tokens[0] === l1TokenNew.address ? toBN(0) : getRealizedLpFeeForFills([fillA]), - ], - netSendAmounts: [ - orderedL1Tokens[0] === l1Token_1.address - ? depositB.amount.mul(toBN(-1)) - : depositA.amount.sub(getRefundForFills([fillA])).mul(toBN(-1)), - orderedL1Tokens[0] === l1TokenNew.address - ? depositB.amount.mul(toBN(-1)) - : depositA.amount.sub(getRefundForFills([fillA])).mul(toBN(-1)), - ], - runningBalances: [toBN(0), toBN(0)], - l1Tokens: orderedL1Tokens, - }; - expect(merkleRoot1.leaves).excludingEvery(["groupIndex", "leafId"]).to.deep.equal([expectedLeaf]); - }); - it("Adds latest running balances to next", async function () { - await updateAllClients(); - - // Fully execute a bundle so we can have a history of running balances. - const startingRunningBalances = amountToDeposit.mul(5); - const initialPoolRebalanceLeaves = buildPoolRebalanceLeaves( - [originChainId, destinationChainId, repaymentChainId, 1], - [[l1Token_1.address], [l1Token_1.address], [l1Token_1.address], [l1Token_1.address]], - [[toBN(0)], [toBN(0)], [toBN(0)], [toBN(0)]], - [[toBN(0)], [toBN(0)], [toBN(0)], [toBN(0)]], - [[startingRunningBalances], [startingRunningBalances], [startingRunningBalances], [startingRunningBalances]], - [0, 0, 0, 0] - ); - const startingBlock = await hubPool.provider.getBlockNumber(); - const startingTree = await buildPoolRebalanceLeafTree(initialPoolRebalanceLeaves); - await hubPool - .connect(dataworker) - .proposeRootBundle( - [startingBlock, startingBlock, startingBlock, startingBlock], - initialPoolRebalanceLeaves.length, - startingTree.getHexRoot(), - mockTreeRoot, - mockTreeRoot - ); - await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1); - // Only execute first two leaves for this test. - for (const leaf of initialPoolRebalanceLeaves.slice(0, 2)) { - await hubPool.connect(dataworker).executeRootBundle(...Object.values(leaf), startingTree.getHexProof(leaf)); - } - - // Submit a deposit on origin chain. Next running balance should be previous minus deposited amount. - const deposit = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - await updateAllClients(); - - // Should have 1 running balance leaf: - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(0), spokePoolClients); - const expectedLeaves1 = [ - { - chainId: originChainId, - bundleLpFees: [toBN(0)], - netSendAmounts: [startingRunningBalances.sub(amountToDeposit)], - runningBalances: [toBN(0)], - l1Tokens: [l1Token_1.address], - }, - ]; - expect(merkleRoot1.leaves).excludingEvery(["groupIndex", "leafId"]).to.deep.equal(expectedLeaves1); - - // Submit a partial fill on destination chain. This tests that the previous running balance is added to - // running balances modified by repayments, slow fills, and deposits. - const fill = await buildFillForRepaymentChain(spokePool_2, depositor, deposit, 0.5, destinationChainId); - const slowFillPayment = getRefund(deposit.amount.sub(fill.totalFilledAmount), deposit.realizedLpFeePct); - await updateAllClients(); - const expectedLeaves2 = [ - { - chainId: originChainId, - bundleLpFees: [toBN(0)], - netSendAmounts: [startingRunningBalances.sub(amountToDeposit)], - runningBalances: [toBN(0)], - l1Tokens: [l1Token_1.address], - }, - { - chainId: destinationChainId, - bundleLpFees: [getRealizedLpFeeForFills([fill])], - netSendAmounts: [startingRunningBalances.add(getRefundForFills([fill])).add(slowFillPayment)], - runningBalances: [toBN(0)], - l1Tokens: [l1Token_1.address], - }, - ]; - const merkleRoot2 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(1), spokePoolClients); - expect(merkleRoot2.leaves).excludingEvery(["groupIndex", "leafId"]).to.deep.equal(expectedLeaves2); - }); - it("Spoke pool balance threshold, above and below", async function () { - await updateAllClients(); - const deposit = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - await updateAllClients(); - const fill = await buildFillForRepaymentChain(spokePool_2, depositor, deposit, 1, destinationChainId); - await updateAllClients(); - await configStore.updateTokenConfig( - l1Token_1.address, - JSON.stringify({ - rateModel: sampleRateModel, - spokeTargetBalances: { - [originChainId]: { - // Threshold above the deposit amount. - threshold: amountToDeposit.mul(2).toString(), - target: amountToDeposit.div(2).toString(), - }, - [destinationChainId]: { - // Threshold above the deposit amount. - threshold: amountToDeposit.mul(2).toString(), - target: amountToDeposit.div(2).toString(), - }, - }, - }) - ); - await configStoreClient.update(); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(0), spokePoolClients); - - const orderedChainIds = [originChainId, destinationChainId].sort((x, y) => x - y); - const expectedLeaves1 = orderedChainIds.map((chainId) => { - return { - chainId, - bundleLpFees: chainId === originChainId ? [toBN(0)] : [getRealizedLpFeeForFills([fill])], - // Running balance is <<< spoke pool balance threshold, so running balance should be non-zero and net send - // amount should be 0 for the origin chain. This should _not affect_ the destination chain, since spoke - // pool balance thresholds only apply to funds being sent from spoke to hub. - runningBalances: chainId === originChainId ? [deposit.amount.mul(toBN(-1))] : [toBN(0)], - netSendAmounts: chainId === originChainId ? [toBN(0)] : [getRefundForFills([fill])], - l1Tokens: [l1Token_1.address], - }; - }); - expect(merkleRoot1.leaves).excludingEvery(["groupIndex", "leafId"]).to.deep.equal(expectedLeaves1); - - // Now set the threshold much lower than the running balance and check that running balances for all - // chains gets set to 0 and net send amount is equal to the running balance. This also tests that the - // dataworker is comparing the absolute value of the running balance with the threshold, not the signed value. - await configStore.updateTokenConfig( - l1Token_1.address, - JSON.stringify({ - rateModel: sampleRateModel, - spokeTargetBalances: { - [originChainId]: { - // Threshold is equal to the deposit amount. - threshold: amountToDeposit.toString(), - target: amountToDeposit.div(2).toString(), - }, - [destinationChainId]: { - // Threshold above the deposit amount. - threshold: amountToDeposit.mul(2).toString(), - target: amountToDeposit.div(2).toString(), - }, - }, - }) - ); - await configStoreClient.update(); - const merkleRoot2 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(1), spokePoolClients); - // We expect to have the target remaining on the spoke. - // We expect to transfer the total deposit amount minus the remaining spoke balance. - const expectedSpokeBalance = amountToDeposit.div(2); - const expectedTransferAmount = amountToDeposit.sub(expectedSpokeBalance); - const expectedLeaves2 = expectedLeaves1.map((leaf) => { - return { - ...leaf, - runningBalances: leaf.chainId === originChainId ? [expectedSpokeBalance.mul(-1)] : leaf.runningBalances, - netSendAmounts: leaf.chainId === originChainId ? [expectedTransferAmount.mul(-1)] : leaf.netSendAmounts, - }; - }); - expect(merkleRoot2.leaves).excludingEvery(["groupIndex", "leafId"]).to.deep.equal(expectedLeaves2); - }); + amountToReturn: bnZero, + l2TokenAddress: erc20_1.address, + leafId: 0, + refundAddresses: [deposit.depositor], + refundAmounts: [deposit.inputAmount], + }, + ]; + expect(expectedLeaves).to.deep.equal(merkleRoot1.leaves); }); - describe("Handles V3 BundleData", function () { - beforeEach(async function () { - await updateAllClients(); - await updateAllClients(); - const poolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(1), - spokePoolClients - ); - expect(poolRebalanceRoot.runningBalances).to.deep.equal({}); - expect(poolRebalanceRoot.realizedLpFees).to.deep.equal({}); - const relayerRefundRoot = await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(1), - spokePoolClients, - poolRebalanceRoot.leaves, - poolRebalanceRoot.runningBalances - ); - expect(relayerRefundRoot.leaves).to.deep.equal([]); - }); - it("Subtracts bundle deposits from origin chain running balances", async function () { - const deposit = await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); - - // Deposits should not add to bundle LP fees. - const expectedRunningBalances: RunningBalances = { - [originChainId]: { - [l1Token_1.address]: deposit.inputAmount.mul(-1), - }, - }; - expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); - expect({}).to.deep.equal(merkleRoot1.realizedLpFees); - }); - it("Adds bundle fills to repayment chain running balances", async function () { - // Send two deposits so we can fill with two different origin chains to test that the BundleDataClient - // batch computes lp fees correctly for different origin chains. - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await depositV3( - spokePool_2, - originChainId, - depositor, - erc20_2.address, - amountToDeposit, - erc20_1.address, - amountToDeposit - ); - await updateAllClients(); - const deposit1 = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - const deposit2 = spokePoolClients[destinationChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - await fillV3(spokePool_2, relayer, deposit1, repaymentChainId); - await fillV3(spokePool_1, relayer, deposit2, repaymentChainId); - await updateAllClients(); - const fill1 = spokePoolClients[destinationChainId] - .getFills() - .filter(sdkUtils.isV3Fill)[0]; - const fill2 = spokePoolClients[originChainId] - .getFills() - .filter(sdkUtils.isV3Fill)[0]; - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); - - // Deposits should not add to bundle LP fees, but fills should. LP fees are taken out of running balances - // and added to realized LP fees, for fills. - const lpFeePct1 = ( - await hubPoolClient.computeRealizedLpFeePct({ ...deposit1, paymentChainId: fill1.destinationChainId }) - ).realizedLpFeePct; - const lpFeePct2 = ( - await hubPoolClient.computeRealizedLpFeePct({ ...deposit2, paymentChainId: fill2.destinationChainId }) - ).realizedLpFeePct; - assert(lpFeePct1.gt(0) && lpFeePct2.gt(0), "LP fee pct should be greater than 0"); - const lpFee1 = lpFeePct1.mul(fill1.inputAmount).div(fixedPointAdjustment); - const lpFee2 = lpFeePct2.mul(fill2.inputAmount).div(fixedPointAdjustment); - const expectedRunningBalances: RunningBalances = { - [originChainId]: { - [l1Token_1.address]: deposit1.inputAmount.mul(-1), - }, - [destinationChainId]: { - [l1Token_1.address]: deposit2.inputAmount.mul(-1), - }, - [repaymentChainId]: { - [l1Token_1.address]: lpFee1.mul(-1).add(fill1.inputAmount).add(lpFee2.mul(-1).add(fill2.inputAmount)), - }, - }; - const expectedRealizedLpFees: RunningBalances = { - [repaymentChainId]: { - [l1Token_1.address]: lpFee1.add(lpFee2), - }, - }; - expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); - expect(expectedRealizedLpFees).to.deep.equal(merkleRoot1.realizedLpFees); - }); - it("Adds bundle slow fills to destination chain running balances", async function () { - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - const deposit = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - await requestSlowFill(spokePool_2, relayer, deposit); - await updateAllClients(); - const slowFillRequest = spokePoolClients[destinationChainId].getSlowFillRequestsForOriginChain(originChainId)[0]; - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); - - // Slow fills should not add to bundle LP fees. - const lpFeePct = ( - await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) - ).realizedLpFeePct; - const lpFee = lpFeePct.mul(slowFillRequest.inputAmount).div(fixedPointAdjustment); - const expectedRunningBalances: RunningBalances = { - [originChainId]: { - [l1Token_1.address]: deposit.inputAmount.mul(-1), - }, - [destinationChainId]: { - [l1Token_1.address]: slowFillRequest.inputAmount.sub(lpFee), - }, - }; - expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); - expect({}).to.deep.equal(merkleRoot1.realizedLpFees); - }); - it("Subtracts unexecutable slow fill amounts from destination chain running balances", async function () { - // Send slow fill in first bundle block range: - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - const deposit = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - await requestSlowFill(spokePool_2, relayer, deposit); - await updateAllClients(); - - // Propose first bundle with a destination chain block range that includes up to the slow fiil block. - const slowFillRequest = spokePoolClients[destinationChainId].getSlowFillRequestsForOriginChain(originChainId)[0]; - const destinationChainBlockRange = [0, slowFillRequest.blockNumber]; - const blockRange1 = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.map( - () => destinationChainBlockRange - ); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(blockRange1, spokePoolClients); - - const lpFeePct = ( - await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) - ).realizedLpFeePct; - const lpFee = lpFeePct.mul(slowFillRequest.inputAmount).div(fixedPointAdjustment); - const expectedRunningBalances: RunningBalances = { - [originChainId]: { - [l1Token_1.address]: deposit.inputAmount.mul(-1), - }, - [destinationChainId]: { - [l1Token_1.address]: slowFillRequest.inputAmount.sub(lpFee), - }, - }; - expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); - expect({}).to.deep.equal(merkleRoot1.realizedLpFees); - - // Send a fast fill in a second bundle block range. - await fillV3(spokePool_2, relayer, deposit, repaymentChainId); - await updateAllClients(); - const fill = spokePoolClients[destinationChainId] - .getFills() - .filter(sdkUtils.isV3Fill)[0]; - expect(fill.relayExecutionInfo.fillType).to.equal(interfaces.FillType.ReplacedSlowFill); - const blockRange2 = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.map((_chain, index) => [ - blockRange1[index][1] + 1, - getDefaultBlockRange(2)[index][1], - ]); - const merkleRoot2 = await dataworkerInstance.buildPoolRebalanceRoot(blockRange2, spokePoolClients); - - // Add fill to repayment chain running balance and remove slow fill amount from destination chain. - const slowFillAmount = lpFee.mul(-1).add(fill.inputAmount); - const expectedRunningBalances2: RunningBalances = { - // Note: There should be no origin chain entry here since there were no deposits. - [destinationChainId]: { - [l1Token_1.address]: slowFillAmount.mul(-1), - }, - [repaymentChainId]: { - [l1Token_1.address]: slowFillAmount, - }, - }; - const expectedRealizedLpFees2: RunningBalances = { - [repaymentChainId]: { - [l1Token_1.address]: lpFee, - }, - }; - expect(expectedRunningBalances2).to.deep.equal(merkleRoot2.runningBalances); - expect(expectedRealizedLpFees2).to.deep.equal(merkleRoot2.realizedLpFees); - }); - it("Adds expired deposits to origin chain running balances", async function () { - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(2), - spokePoolClients - ); - const elapsedFillDeadline = bundleBlockTimestamps[destinationChainId][1] - 1; - - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit, - { - fillDeadline: elapsedFillDeadline, - } - ); - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildPoolRebalanceRoot(getDefaultBlockRange(2), spokePoolClients); - - // Origin chain running balance is incremented by refunded deposit which cancels out the subtraction for - // bundle deposit. - const expectedRunningBalances: RunningBalances = { - // Note: There should be no origin chain entry here since there were no deposits. - [originChainId]: { - [l1Token_1.address]: BigNumber.from(0), - }, - }; - expect(expectedRunningBalances).to.deep.equal(merkleRoot1.runningBalances); - expect({}).to.deep.equal(merkleRoot1.realizedLpFees); - }); - it("Adds fills to relayer refund root", async function () { - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - const deposit = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - await fillV3(spokePool_2, relayer, deposit, repaymentChainId); - await updateAllClients(); - const { runningBalances, leaves } = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(2), - spokePoolClients - ); - const merkleRoot1 = await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(2), - spokePoolClients, - leaves, - runningBalances - ); - - // Origin chain should have negative running balance and therefore positive amount to return. - const lpFeePct = ( - await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) - ).realizedLpFeePct; - const lpFee = lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); - const refundAmount = deposit.inputAmount.sub(lpFee); - const expectedLeaves: RelayerRefundLeaf[] = [ - { - chainId: originChainId, - amountToReturn: runningBalances[originChainId][l1Token_1.address].mul(-1), - l2TokenAddress: erc20_1.address, - leafId: 0, - refundAddresses: [], - refundAmounts: [], - }, - { - chainId: repaymentChainId, - amountToReturn: bnZero, - l2TokenAddress: l1Token_1.address, - leafId: 1, - refundAddresses: [relayer.address], - refundAmounts: [refundAmount], - }, - ]; - expect(expectedLeaves).to.deep.equal(merkleRoot1.leaves); - }); - it("All fills are slow fill executions", async function () { - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - const deposit = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - const lpFeePct = ( - await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) - ).realizedLpFeePct; - const slowFills = buildV3SlowRelayLeaves([deposit], lpFeePct); - - // Relay slow root to destination chain - const slowFillTree = await buildV3SlowRelayTree(slowFills); - await spokePool_2.relayRootBundle(mockTreeRoot, slowFillTree.getHexRoot()); - await erc20_2.mint(spokePool_2.address, deposit.inputAmount); - await spokePool_2.executeV3SlowRelayLeaf(slowFills[0], 0, slowFillTree.getHexProof(slowFills[0])); - await updateAllClients(); - const poolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(2), - spokePoolClients - ); - - // Slow fill executions should add to bundle LP fees, but not refunds. So, there should be - // change to running balances on the destination chain, but there should be an entry for it - // so that bundle lp fees get accumulated. - const lpFee = lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment); - const expectedRunningBalances: RunningBalances = { - [originChainId]: { - [l1Token_1.address]: deposit.inputAmount.mul(-1), - }, - [destinationChainId]: { - [l1Token_1.address]: toBN(0), - }, - }; - const expectedRealizedLpFees: RunningBalances = { - [destinationChainId]: { - [l1Token_1.address]: lpFee, - }, - }; - expect(expectedRunningBalances).to.deep.equal(poolRebalanceRoot.runningBalances); - expect(expectedRealizedLpFees).to.deep.equal(poolRebalanceRoot.realizedLpFees); - - const refundRoot = await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(2), - spokePoolClients, - poolRebalanceRoot.leaves, - poolRebalanceRoot.runningBalances - ); - const expectedLeaves: RelayerRefundLeaf[] = [ - { - chainId: originChainId, - amountToReturn: poolRebalanceRoot.runningBalances[originChainId][l1Token_1.address].mul(-1), - l2TokenAddress: erc20_1.address, - leafId: 0, - refundAddresses: [], - refundAmounts: [], - }, - // No leaf for destination chain. - ]; - expect(expectedLeaves).to.deep.equal(refundRoot.leaves); - }); - it("Adds expired deposit refunds to relayer refund root", async function () { - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(2), - spokePoolClients - ); - const elapsedFillDeadline = bundleBlockTimestamps[destinationChainId][1] - 1; - - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit, - { - fillDeadline: elapsedFillDeadline, - } - ); - await updateAllClients(); - const deposit = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - const { runningBalances, leaves } = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(2), - spokePoolClients - ); - const merkleRoot1 = await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(2), - spokePoolClients, - leaves, - runningBalances - ); - - // Origin chain running balance starts negative because of the deposit - // but is cancelled out by the refund such that the running balance is 0 - // and there is no amount to return. - const expectedLeaves: RelayerRefundLeaf[] = [ - { - chainId: originChainId, - amountToReturn: bnZero, - l2TokenAddress: erc20_1.address, - leafId: 0, - refundAddresses: [deposit.depositor], - refundAmounts: [deposit.inputAmount], - }, - ]; - expect(expectedLeaves).to.deep.equal(merkleRoot1.leaves); - }); - it("Combines V3 and V2 fills to refund in same refund root", async function () { - // Note: take repayments on destination chain for this test. - const depositV2 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - await buildFillForRepaymentChain(spokePool_2, relayer, depositV2, 1, destinationChainId); - const _depositV3 = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - await fillV3(spokePool_2, relayer, _depositV3, destinationChainId); - await updateAllClients(); - const { runningBalances, leaves } = await dataworkerInstance.buildPoolRebalanceRoot( - getDefaultBlockRange(2), - spokePoolClients - ); - const merkleRoot1 = await dataworkerInstance.buildRelayerRefundRoot( - getDefaultBlockRange(2), - spokePoolClients, - leaves, - runningBalances - ); - - const fillV2 = spokePoolClients[destinationChainId] - .getFills() - .filter(sdkUtils.isV2Fill)[0]; - const lpFeeV2 = fillV2.realizedLpFeePct.mul(fillV2.amount).div(fixedPointAdjustment); - const fillV2RefundAmount = fillV2.amount.sub(lpFeeV2); - const lpFeePctV3 = ( - await hubPoolClient.computeRealizedLpFeePct({ ..._depositV3, paymentChainId: _depositV3.destinationChainId }) - ).realizedLpFeePct; - const lpFeeV3 = lpFeePctV3.mul(_depositV3.inputAmount).div(fixedPointAdjustment); - const fillV3RefundAmount = lpFeeV3.mul(-1).add(_depositV3.inputAmount); - const expectedLeaves: RelayerRefundLeaf[] = [ - { - chainId: originChainId, - amountToReturn: runningBalances[originChainId][l1Token_1.address].mul(-1), - l2TokenAddress: erc20_1.address, - leafId: 0, - refundAddresses: [], - refundAmounts: [], - }, - { - chainId: destinationChainId, - amountToReturn: toBN(0), - l2TokenAddress: erc20_2.address, - leafId: 1, - refundAddresses: [relayer.address], - refundAmounts: [fillV3RefundAmount.add(fillV2RefundAmount)], - }, - ]; - expect(expectedLeaves).to.deep.equal(merkleRoot1.leaves); - }); - it("Adds bundle slow fills to slow fill root", async function () { - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - const deposit = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - await requestSlowFill(spokePool_2, relayer, deposit); - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildSlowRelayRoot(getDefaultBlockRange(2), spokePoolClients); - - const lpFeePct = ( - await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) - ).realizedLpFeePct; - const expectedSlowFillLeaves = buildV3SlowRelayLeaves([deposit], lpFeePct); - expect(merkleRoot1.leaves).to.deep.equal(expectedSlowFillLeaves); - }); - it("Combines V3 and V2 slow fills in same slow fill root", async function () { - await updateAllClients(); - - // Submit V2 deposit - const depositV2 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - const v2SlowFillLeaves = buildSlowRelayLeaves([depositV2]); - // V2 slow fill type is all messed up, for now we cast it to correct types here. - v2SlowFillLeaves[0].relayData = { - ...v2SlowFillLeaves[0].relayData, - originChainId: Number(v2SlowFillLeaves[0].relayData.originChainId), - destinationChainId: Number(v2SlowFillLeaves[0].relayData.destinationChainId), - depositId: Number(v2SlowFillLeaves[0].relayData.depositId), - }; - v2SlowFillLeaves[0].payoutAdjustmentPct = v2SlowFillLeaves[0].payoutAdjustmentPct.toString(); - - // Submit V3 deposit - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - const _depositV3 = spokePoolClients[originChainId] - .getDeposits() - .filter(sdkUtils.isV3Deposit)[0]; - const lpFeePct = ( - await hubPoolClient.computeRealizedLpFeePct({ ..._depositV3, paymentChainId: _depositV3.destinationChainId }) - ).realizedLpFeePct; - const v3SlowFillLeaves = buildV3SlowRelayLeaves([_depositV3], lpFeePct); - - // Partial fill the V2 deposit - await buildFill(spokePool_2, erc20_2, depositor, relayer, depositV2, 0.1); - - // Request to slow fill the V3 deposit - await requestSlowFill(spokePool_2, relayer, _depositV3); + it("Adds bundle slow fills to slow fill root", async function () { + await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit + ); + await updateAllClients(); + const deposit = spokePoolClients[originChainId] + .getDeposits() + .filter(sdkUtils.isV3Deposit)[0]; + await requestSlowFill(spokePool_2, relayer, deposit); + await updateAllClients(); + const merkleRoot1 = await dataworkerInstance.buildSlowRelayRoot(getDefaultBlockRange(2), spokePoolClients); - // Returns expected merkle root where leaves are ordered by origin chain ID and then deposit ID - // (ascending). - const expectedLeaves = [...v2SlowFillLeaves, ...v3SlowFillLeaves]; - await updateAllClients(); - const merkleRoot1 = await dataworkerInstance.buildSlowRelayRoot(getDefaultBlockRange(2), spokePoolClients); - expect(merkleRoot1.leaves).to.deep.equal(expectedLeaves); - }); + const lpFeePct = ( + await hubPoolClient.computeRealizedLpFeePct({ ...deposit, paymentChainId: deposit.destinationChainId }) + ).realizedLpFeePct; + const expectedSlowFillLeaves = buildV3SlowRelayLeaves([deposit], lpFeePct); + expect(merkleRoot1.leaves).to.deep.equal(expectedSlowFillLeaves); }); }); diff --git a/test/Dataworker.executeRelayerRefunds.ts b/test/Dataworker.executeRelayerRefunds.ts index e45a59272..cb95319f3 100644 --- a/test/Dataworker.executeRelayerRefunds.ts +++ b/test/Dataworker.executeRelayerRefunds.ts @@ -1,13 +1,14 @@ -import { MultiCallerClient, SpokePoolClient } from "../src/clients"; -import { MAX_UINT_VAL } from "../src/utils"; +import { BundleDataClient, HubPoolClient, MultiCallerClient, SpokePoolClient } from "../src/clients"; +import { MAX_UINT_VAL, toBN } from "../src/utils"; import { MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF, MAX_REFUNDS_PER_RELAYER_REFUND_LEAF, amountToDeposit, destinationChainId, + repaymentChainId, } from "./constants"; import { setupDataworker } from "./fixtures/Dataworker.Fixture"; -import { Contract, SignerWithAddress, depositV3, ethers, fillV3 } from "./utils"; +import { Contract, SignerWithAddress, depositV3, ethers, expect, fillV3 } from "./utils"; // Tested import { BalanceAllocator } from "../src/clients/BalanceAllocator"; @@ -15,7 +16,7 @@ import { spokePoolClientsToProviders } from "../src/common"; import { Dataworker } from "../src/dataworker/Dataworker"; let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract; -let l1Token_1: Contract, hubPool: Contract; +let l1Token_1: Contract, hubPool: Contract, hubPoolClient: HubPoolClient; let depositor: SignerWithAddress; let dataworkerInstance: Dataworker, multiCallerClient: MultiCallerClient; @@ -24,9 +25,17 @@ let spokePoolClients: { [chainId: number]: SpokePoolClient }; let updateAllClients: () => Promise; describe("Dataworker: Execute relayer refunds", async function () { + const getNewBalanceAllocator = async (): Promise => { + const providers = { + ...spokePoolClientsToProviders(spokePoolClients), + [(await hubPool.provider.getNetwork()).chainId]: hubPool.provider, + }; + return new BalanceAllocator(providers); + }; beforeEach(async function () { ({ hubPool, + hubPoolClient, spokePool_1, erc20_1, spokePool_2, @@ -38,6 +47,7 @@ describe("Dataworker: Execute relayer refunds", async function () { updateAllClients, spokePoolClients, } = await setupDataworker(ethers, MAX_REFUNDS_PER_RELAYER_REFUND_LEAF, MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF, 0)); + await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); }); it("Simple lifecycle", async function () { await updateAllClients(); @@ -56,33 +66,208 @@ describe("Dataworker: Execute relayer refunds", async function () { await fillV3(spokePool_2, depositor, deposit, destinationChainId); await updateAllClients(); - const providers = { - ...spokePoolClientsToProviders(spokePoolClients), - [(await hubPool.provider.getNetwork()).chainId]: hubPool.provider, - }; await dataworkerInstance.proposeRootBundle(spokePoolClients); // Execute queue and check that root bundle is pending: - await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); await multiCallerClient.executeTransactionQueue(); // Advance time and execute rebalance leaves: await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); await updateAllClients(); - await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, new BalanceAllocator(providers)); + await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, await getNewBalanceAllocator()); await multiCallerClient.executeTransactionQueue(); - // TEST 3: - // Submit another root bundle proposal and check bundle block range. There should be no leaves in the new range - // yet. In the bundle block range, all chains should have increased their start block, including those without - // pool rebalance leaves because they should use the chain's end block from the latest fully executed proposed - // root bundle, which should be the bundle block in expectedPoolRebalanceRoot2 + 1. + // Manually relay the roots to spoke pools since adapter is a dummy and won't actually relay messages. await updateAllClients(); - await dataworkerInstance.proposeRootBundle(spokePoolClients); + const validatedRootBundles = hubPoolClient.getValidatedRootBundles(); + expect(validatedRootBundles.length).to.equal(1); + const rootBundle = validatedRootBundles[0]; + await spokePool_1.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); + await spokePool_2.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); + await updateAllClients(); + await dataworkerInstance.executeRelayerRefundLeaves(spokePoolClients, await getNewBalanceAllocator()); + + // Note: without sending tokens, only one of the leaves will be executable. + // This is the leaf with the deposit that is being pulled back to the hub pool. + expect(multiCallerClient.transactionCount()).to.equal(1); + await multiCallerClient.executeTransactionQueue(); - // Advance time and execute leaves: - await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); await updateAllClients(); - await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, new BalanceAllocator(providers)); + + // Note: we need to manually supply the tokens since the L1 tokens won't be recognized in the spoke pool. + await erc20_2.mint(spokePool_2.address, amountToDeposit); + await dataworkerInstance.executeRelayerRefundLeaves(spokePoolClients, await getNewBalanceAllocator()); + + // The other transaction should now be enqueued. + expect(multiCallerClient.transactionCount()).to.equal(1); + + await multiCallerClient.executeTransactionQueue(); + }); + describe("Computing refunds for bundles", function () { + let relayer: SignerWithAddress; + let bundleDataClient: BundleDataClient; + + beforeEach(async function () { + relayer = depositor; + bundleDataClient = dataworkerInstance.clients.bundleDataClient; + await updateAllClients(); + + const deposit1 = await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit + ); + + await updateAllClients(); + + // Submit a valid fill. + await fillV3(spokePool_2, relayer, deposit1, destinationChainId); + + await updateAllClients(); + }); + it("No validated bundle refunds", async function () { + // Propose a bundle: + await dataworkerInstance.proposeRootBundle(spokePoolClients); + await multiCallerClient.executeTransactionQueue(); + await updateAllClients(); + + // No bundle is validated so no refunds. + const refunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); + expect(bundleDataClient.getTotalRefund(refunds, relayer.address, destinationChainId, erc20_2.address)).to.equal( + toBN(0) + ); + }); + it("Get refunds from validated bundles", async function () { + await updateAllClients(); + // Propose a bundle: + await dataworkerInstance.proposeRootBundle(spokePoolClients); + await multiCallerClient.executeTransactionQueue(); + + // Advance time and execute leaves: + await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); + await updateAllClients(); + await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, await getNewBalanceAllocator()); + await multiCallerClient.executeTransactionQueue(); + + // Before relayer refund leaves are not executed, should have pending refunds: + await updateAllClients(); + const validatedRootBundles = hubPoolClient.getValidatedRootBundles(); + expect(validatedRootBundles.length).to.equal(1); + const refunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); + const totalRefund1 = bundleDataClient.getTotalRefund( + refunds, + relayer.address, + destinationChainId, + erc20_2.address + ); + expect(totalRefund1).to.gt(0); + + // Test edge cases of `getTotalRefund` that should return BN(0) + expect(bundleDataClient.getTotalRefund(refunds, relayer.address, repaymentChainId, erc20_2.address)).to.equal( + toBN(0) + ); + expect(bundleDataClient.getTotalRefund(refunds, relayer.address, destinationChainId, erc20_1.address)).to.equal( + toBN(0) + ); + expect(bundleDataClient.getTotalRefund(refunds, hubPool.address, destinationChainId, erc20_2.address)).to.equal( + toBN(0) + ); + + // Manually relay the roots to spoke pools since adapter is a dummy and won't actually relay messages. + const rootBundle = validatedRootBundles[0]; + await spokePool_1.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); + await spokePool_2.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); + await updateAllClients(); + + // Execute relayer refund leaves. Send funds to spoke pools to execute the leaves. + await erc20_2.mint(spokePool_2.address, amountToDeposit); + await dataworkerInstance.executeRelayerRefundLeaves(spokePoolClients, await getNewBalanceAllocator()); + await multiCallerClient.executeTransactionQueue(); + + // Should now have zero pending refunds + await updateAllClients(); + // If we call `getPendingRefundsFromLatestBundle` multiple times, there should be no error. If there is an error, + // then it means that `getPendingRefundsFromLatestBundle` is mutating the return value of `.loadData` which is + // stored in the bundle data client's cache. `getPendingRefundsFromLatestBundle` should instead be using a + // deep cloned copy of `.loadData`'s output. + await bundleDataClient.getPendingRefundsFromValidBundles(2); + const postExecutionRefunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); + expect( + bundleDataClient.getTotalRefund(postExecutionRefunds, relayer.address, destinationChainId, erc20_2.address) + ).to.equal(toBN(0)); + + // Submit fill2 and propose another bundle: + const newDepositAmount = amountToDeposit.mul(2); + const deposit2 = await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + newDepositAmount, + erc20_2.address, + amountToDeposit + ); + await updateAllClients(); + + // Submit a valid fill. + await fillV3(spokePool_2, relayer, deposit2, destinationChainId); + await updateAllClients(); + + // Validate another bundle: + await dataworkerInstance.proposeRootBundle(spokePoolClients); + await multiCallerClient.executeTransactionQueue(); + await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); + await updateAllClients(); + await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, await getNewBalanceAllocator()); + await multiCallerClient.executeTransactionQueue(); + await updateAllClients(); + + expect(hubPoolClient.getValidatedRootBundles().length).to.equal(2); + + // Should include refunds for most recently validated bundle but not count first one + // since they were already refunded. + const refunds2 = await bundleDataClient.getPendingRefundsFromValidBundles(2); + expect(bundleDataClient.getTotalRefund(refunds2, relayer.address, destinationChainId, erc20_2.address)).to.gt(0); + }); + it("Refunds in next bundle", async function () { + // Before proposal should show refunds: + expect( + bundleDataClient.getRefundsFor( + await bundleDataClient.getNextBundleRefunds(), + relayer.address, + destinationChainId, + erc20_2.address + ) + ).to.gt(0); + + // Propose a bundle: + await dataworkerInstance.proposeRootBundle(spokePoolClients); + await multiCallerClient.executeTransactionQueue(); + await updateAllClients(); + + // After proposal but before execution should show upcoming refund: + expect( + bundleDataClient.getRefundsFor( + await bundleDataClient.getNextBundleRefunds(), + relayer.address, + destinationChainId, + erc20_2.address + ) + ).to.gt(0); + + // Advance time and execute root bundle: + await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); + await updateAllClients(); + await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, await getNewBalanceAllocator()); + await multiCallerClient.executeTransactionQueue(); + + // Should reset to no refunds in "next bundle", though these will show up in pending bundle. + await updateAllClients(); + expect(await bundleDataClient.getNextBundleRefunds()).to.deep.equal({}); + }); }); }); diff --git a/test/Dataworker.executeSlowRelay.ts b/test/Dataworker.executeSlowRelay.ts index 1180e50d5..325903854 100644 --- a/test/Dataworker.executeSlowRelay.ts +++ b/test/Dataworker.executeSlowRelay.ts @@ -44,53 +44,6 @@ describe("Dataworker: Execute slow relays", async function () { spokePoolClients, } = await setupDataworker(ethers, MAX_REFUNDS_PER_RELAYER_REFUND_LEAF, MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF, 0)); }); - it("Simple lifecycle", async function () { - await updateAllClients(); - - // Send a deposit and a fill so that dataworker builds simple roots. - const deposit = await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await updateAllClients(); - await fillV3(spokePool_2, depositor, deposit, destinationChainId); - await updateAllClients(); - - const providers = { - ...spokePoolClientsToProviders(spokePoolClients), - [(await hubPool.provider.getNetwork()).chainId]: hubPool.provider, - }; - - await dataworkerInstance.proposeRootBundle(spokePoolClients); - - // Execute queue and check that root bundle is pending: - await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); - await multiCallerClient.executeTransactionQueue(); - - // Advance time and execute rebalance leaves: - await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); - await updateAllClients(); - await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, new BalanceAllocator(providers)); - await multiCallerClient.executeTransactionQueue(); - - // TEST 3: - // Submit another root bundle proposal and check bundle block range. There should be no leaves in the new range - // yet. In the bundle block range, all chains should have increased their start block, including those without - // pool rebalance leaves because they should use the chain's end block from the latest fully executed proposed - // root bundle, which should be the bundle block in expectedPoolRebalanceRoot2 + 1. - await updateAllClients(); - await dataworkerInstance.proposeRootBundle(spokePoolClients); - - // Advance time and execute leaves: - await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); - await updateAllClients(); - await dataworkerInstance.executePoolRebalanceLeaves(spokePoolClients, new BalanceAllocator(providers)); - }); it("Executes V3 slow fills", async function () { await updateAllClients(); @@ -154,7 +107,7 @@ describe("Dataworker: Execute slow relays", async function () { it("Ignores V3 slow fills that were replaced by a fast fill", async function () { await updateAllClients(); - await depositV3( + const deposit = await depositV3( spokePool_1, destinationChainId, depositor, @@ -164,7 +117,6 @@ describe("Dataworker: Execute slow relays", async function () { amountToDeposit ); await updateAllClients(); - const deposit = spokePoolClients[originChainId].getDeposits()[0]; await requestSlowFill(spokePool_2, depositor, deposit); await updateAllClients(); @@ -206,7 +158,7 @@ describe("Dataworker: Execute slow relays", async function () { expect(multiCallerClient.transactionCount()).to.equal(1); // Replace slow fill, and check that it no longer tries to get executed by dataworker. - await fillV3(spokePool_2, relayer, deposit, deposit.realizedLpFeePct); + await fillV3(spokePool_2, relayer, deposit); await updateAllClients(); multiCallerClient.clearTransactionQueue(); await dataworkerInstance.executeSlowRelayLeaves(spokePoolClients, new BalanceAllocator(providers)); diff --git a/test/Dataworker.loadData.ts b/test/Dataworker.loadData.ts index 8c0d845e5..d0de7e664 100644 --- a/test/Dataworker.loadData.ts +++ b/test/Dataworker.loadData.ts @@ -1,19 +1,5 @@ -import { - BalanceAllocator, - BundleDataClient, - ConfigStoreClient, - HubPoolClient, - MultiCallerClient, - SpokePoolClient, -} from "../src/clients"; -import { - CHAIN_ID_TEST_LIST, - IMPOSSIBLE_BLOCK_RANGE, - amountToDeposit, - destinationChainId, - originChainId, - repaymentChainId, -} from "./constants"; +import { BundleDataClient, ConfigStoreClient, HubPoolClient, SpokePoolClient } from "../src/clients"; +import { amountToDeposit, destinationChainId, originChainId, repaymentChainId } from "./constants"; import { setupDataworker } from "./fixtures/Dataworker.Fixture"; import { Contract, @@ -21,19 +7,11 @@ import { SignerWithAddress, V3FillFromDeposit, assertPromiseError, - buildDeposit, - buildFill, - buildFillForRepaymentChain, - buildModifiedFill, - buildSlowFill, - buildSlowRelayLeaves, - buildSlowRelayTree, depositV3, ethers, expect, fillV3, getDefaultBlockRange, - getLastBlockNumber, mineRandomBlocks, randomAddress, requestSlowFill, @@ -42,33 +20,18 @@ import { spyLogIncludes, } from "./utils"; -import { spokePoolClientsToProviders } from "../src/common"; import { Dataworker } from "../src/dataworker/Dataworker"; // Tested -import { Deposit, DepositWithBlock, Fill } from "../src/interfaces"; -import { - MAX_UINT_VAL, - getCurrentTime, - getRealizedLpFeeForFills, - getRefundForFills, - toBN, - Event, - bnZero, - toBNWei, - fixedPointAdjustment, - assert, - ZERO_ADDRESS, -} from "../src/utils"; +import { getCurrentTime, toBN, Event, bnZero, toBNWei, fixedPointAdjustment, assert, ZERO_ADDRESS } from "../src/utils"; import { MockHubPoolClient, MockSpokePoolClient } from "./mocks"; import { interfaces, utils as sdkUtils } from "@across-protocol/sdk-v2"; import { cloneDeep } from "lodash"; let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract; -let l1Token_1: Contract, l1Token_2: Contract, hubPool: Contract; +let l1Token_1: Contract; let depositor: SignerWithAddress, relayer: SignerWithAddress; let spokePoolClient_1: SpokePoolClient, spokePoolClient_2: SpokePoolClient, bundleDataClient: BundleDataClient; let hubPoolClient: HubPoolClient, configStoreClient: ConfigStoreClient; -let multiCallerClient: MultiCallerClient; let dataworkerInstance: Dataworker; let spokePoolClients: { [chainId: number]: SpokePoolClient }; @@ -76,13 +39,10 @@ let spy: sinon.SinonSpy; let updateAllClients: () => Promise; -const ignoredDepositParams = ["logIndex", "transactionHash", "transactionIndex", "blockTimestamp"]; - // TODO: Rename this file to BundleDataClient describe("Dataworker: Load data used in all functions", async function () { beforeEach(async function () { ({ - hubPool, spokePool_1, erc20_1, spokePool_2, @@ -90,7 +50,6 @@ describe("Dataworker: Load data used in all functions", async function () { configStoreClient, hubPoolClient, l1Token_1, - l1Token_2, depositor, relayer, dataworkerInstance, @@ -101,7 +60,6 @@ describe("Dataworker: Load data used in all functions", async function () { spy, } = await setupDataworker(ethers, 25, 25, 0)); bundleDataClient = dataworkerInstance.clients.bundleDataClient; - multiCallerClient = dataworkerInstance.clients.multiCallerClient; }); it("Default conditions", async function () { @@ -118,583 +76,13 @@ describe("Dataworker: Load data used in all functions", async function () { // Before any deposits, returns empty dictionaries. await updateAllClients(); expect(await bundleDataClient.loadData(getDefaultBlockRange(1), spokePoolClients)).to.deep.equal({ - unfilledDeposits: [], - deposits: [], - fillsToRefund: {}, - allValidFills: [], bundleDepositsV3: {}, expiredDepositsToRefundV3: {}, - earlyDeposits: [], bundleFillsV3: {}, unexecutableSlowFills: {}, bundleSlowFillsV3: {}, }); }); - describe("Computing refunds for bundles", function () { - let fill1: Fill; - let deposit1: Deposit; - - beforeEach(async function () { - await updateAllClients(); - - deposit1 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - - await updateAllClients(); - - // Submit a valid fill. - fill1 = await buildFillForRepaymentChain( - spokePool_2, - relayer, - deposit1, - 0.5, - destinationChainId, - erc20_2.address - ); - await updateAllClients(); - }); - it("No validated bundle refunds", async function () { - // Propose a bundle: - await dataworkerInstance.proposeRootBundle(spokePoolClients); - await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); - await multiCallerClient.executeTransactionQueue(); - await updateAllClients(); - - // No bundle is validated so no refunds. - const refunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); - expect(bundleDataClient.getTotalRefund(refunds, relayer.address, destinationChainId, erc20_2.address)).to.equal( - toBN(0) - ); - }); - it("1 validated bundle with refunds", async function () { - // Propose a bundle: - await dataworkerInstance.proposeRootBundle(spokePoolClients); - await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); - await multiCallerClient.executeTransactionQueue(); - await updateAllClients(); - - const latestBlock = await hubPool.provider.getBlockNumber(); - const blockRange = CHAIN_ID_TEST_LIST.map(() => [0, latestBlock]); - const expectedPoolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot(blockRange, spokePoolClients); - await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); - for (const leaf of expectedPoolRebalanceRoot.leaves) { - await hubPool.executeRootBundle( - leaf.chainId, - leaf.groupIndex, - leaf.bundleLpFees, - leaf.netSendAmounts, - leaf.runningBalances, - leaf.leafId, - leaf.l1Tokens, - expectedPoolRebalanceRoot.tree.getHexProof(leaf) - ); - } - - // Before relayer refund leaves are executed, should have pending refunds: - await updateAllClients(); - const refunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); - expect(bundleDataClient.getTotalRefund(refunds, relayer.address, destinationChainId, erc20_2.address)).to.equal( - getRefundForFills([fill1]) - ); - - // Test edge cases of `getTotalRefund` that should return BN(0) - expect(bundleDataClient.getTotalRefund(refunds, relayer.address, repaymentChainId, erc20_2.address)).to.equal( - toBN(0) - ); - expect(bundleDataClient.getTotalRefund(refunds, relayer.address, destinationChainId, erc20_1.address)).to.equal( - toBN(0) - ); - expect(bundleDataClient.getTotalRefund(refunds, hubPool.address, destinationChainId, erc20_2.address)).to.equal( - toBN(0) - ); - - // Manually relay the roots to spoke pools since adapter is a dummy and won't actually relay messages. - const validatedRootBundles = hubPoolClient.getValidatedRootBundles(); - for (const rootBundle of validatedRootBundles) { - await spokePool_1.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); - await spokePool_2.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); - } - await updateAllClients(); - - // Execute relayer refund leaves. Send funds to spoke pools to execute the leaves. - await erc20_2.mint(spokePool_2.address, getRefundForFills([fill1])); - const providers = { - ...spokePoolClientsToProviders(spokePoolClients), - [(await hubPool.provider.getNetwork()).chainId]: hubPool.provider, - }; - await dataworkerInstance.executeRelayerRefundLeaves(spokePoolClients, new BalanceAllocator(providers)); - await multiCallerClient.executeTransactionQueue(); - - // Should now have zero pending refunds - await updateAllClients(); - // If we call `getPendingRefundsFromLatestBundle` multiple times, there should be no error. If there is an error, - // then it means that `getPendingRefundsFromLatestBundle` is mutating the return value of `.loadData` which is - // stored in the bundle data client's cache. `getPendingRefundsFromLatestBundle` should instead be using a - // deep cloned copy of `.loadData`'s output. - await bundleDataClient.getPendingRefundsFromValidBundles(2); - const postExecutionRefunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); - expect( - bundleDataClient.getTotalRefund(postExecutionRefunds, relayer.address, destinationChainId, erc20_2.address) - ).to.equal(toBN(0)); - }); - it("2 validated bundles with refunds", async function () { - // Propose and execute bundle containing fill1: - await dataworkerInstance.proposeRootBundle(spokePoolClients); - await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); - await multiCallerClient.executeTransactionQueue(); - await updateAllClients(); - - const latestBlock = await hubPool.provider.getBlockNumber(); - const blockRange = CHAIN_ID_TEST_LIST.map(() => [0, latestBlock]); - const expectedPoolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot(blockRange, spokePoolClients); - await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); - for (const leaf of expectedPoolRebalanceRoot.leaves) { - await hubPool.executeRootBundle( - leaf.chainId, - leaf.groupIndex, - leaf.bundleLpFees, - leaf.netSendAmounts, - leaf.runningBalances, - leaf.leafId, - leaf.l1Tokens, - expectedPoolRebalanceRoot.tree.getHexProof(leaf) - ); - } - - // Submit fill2 and propose another bundle: - const fill2 = await buildFillForRepaymentChain( - spokePool_2, - relayer, - deposit1, - 0.2, - destinationChainId, - erc20_2.address - ); - await updateAllClients(); - - // Propose another bundle: - await dataworkerInstance.proposeRootBundle(spokePoolClients); - await multiCallerClient.executeTransactionQueue(); - await updateAllClients(); - - // Check that pending refunds include both fills after pool leaves are executed - await bundleDataClient.getPendingRefundsFromValidBundles(2); - expect( - bundleDataClient.getTotalRefund( - await bundleDataClient.getPendingRefundsFromValidBundles(2), - relayer.address, - destinationChainId, - erc20_2.address - ) - ).to.equal(getRefundForFills([fill1])); - const latestBlock2 = await hubPool.provider.getBlockNumber(); - const blockRange2 = CHAIN_ID_TEST_LIST.map(() => [latestBlock + 1, latestBlock2]); - const expectedPoolRebalanceRoot2 = await dataworkerInstance.buildPoolRebalanceRoot(blockRange2, spokePoolClients); - await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); - for (const leaf of expectedPoolRebalanceRoot2.leaves) { - await hubPool.executeRootBundle( - leaf.chainId, - leaf.groupIndex, - leaf.bundleLpFees, - leaf.netSendAmounts, - leaf.runningBalances, - leaf.leafId, - leaf.l1Tokens, - expectedPoolRebalanceRoot2.tree.getHexProof(leaf) - ); - } - await updateAllClients(); - const postSecondProposalRefunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); - expect( - bundleDataClient.getTotalRefund(postSecondProposalRefunds, relayer.address, destinationChainId, erc20_2.address) - ).to.equal(getRefundForFills([fill1, fill2])); - - // Manually relay the roots to spoke pools since adapter is a dummy and won't actually relay messages. - const validatedRootBundles = hubPoolClient.getValidatedRootBundles(); - for (const rootBundle of validatedRootBundles) { - await spokePool_1.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); - await spokePool_2.relayRootBundle(rootBundle.relayerRefundRoot, rootBundle.slowRelayRoot); - } - await updateAllClients(); - - // Execute refunds and test that pending refund amounts are decreasing. - await erc20_2.mint(spokePool_2.address, getRefundForFills([fill1, fill2])); - const providers = { - ...spokePoolClientsToProviders(spokePoolClients), - [(await hubPool.provider.getNetwork()).chainId]: hubPool.provider, - }; - await dataworkerInstance.executeRelayerRefundLeaves(spokePoolClients, new BalanceAllocator(providers)); - await multiCallerClient.executeTransactionQueue(); - await updateAllClients(); - const postExecutedRefunds = await bundleDataClient.getPendingRefundsFromValidBundles(2); - expect( - bundleDataClient.getTotalRefund(postExecutedRefunds, relayer.address, destinationChainId, erc20_2.address) - ).to.equal(toBN(0)); - }); - it("Refunds in next bundle", async function () { - // When there is no history of proposed root bundles: - const refunds = await bundleDataClient.getNextBundleRefunds(); - expect(bundleDataClient.getRefundsFor(refunds, relayer.address, destinationChainId, erc20_2.address)).to.equal( - getRefundForFills([fill1]) - ); - - // Propose a bundle: - await dataworkerInstance.proposeRootBundle(spokePoolClients); - await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); - await multiCallerClient.executeTransactionQueue(); - await updateAllClients(); - - // Execute the bundle: - const latestBlock = await hubPool.provider.getBlockNumber(); - const blockRange = CHAIN_ID_TEST_LIST.map(() => [0, latestBlock]); - const expectedPoolRebalanceRoot = await dataworkerInstance.buildPoolRebalanceRoot(blockRange, spokePoolClients); - await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + Number(await hubPool.liveness()) + 1); - for (const leaf of expectedPoolRebalanceRoot.leaves) { - await hubPool.executeRootBundle( - leaf.chainId, - leaf.groupIndex, - leaf.bundleLpFees, - leaf.netSendAmounts, - leaf.runningBalances, - leaf.leafId, - leaf.l1Tokens, - expectedPoolRebalanceRoot.tree.getHexProof(leaf) - ); - } - - // Next bundle should include fill2. - const fill2 = await buildFillForRepaymentChain( - spokePool_2, - relayer, - deposit1, - 0.4, - destinationChainId, - erc20_2.address - ); - await updateAllClients(); - expect( - bundleDataClient.getRefundsFor( - await bundleDataClient.getNextBundleRefunds(), - relayer.address, - destinationChainId, - erc20_2.address - ) - ).to.equal(getRefundForFills([fill2])); - - // If we now propose a bundle including fill2, then next bundle will still fill2 - // because the bundle hasn't been fully executed yet. So its conceivable that this latest bundle is disputed, - // so we still want to capture fill2 in the potential "next bundle" category. - await updateAllClients(); - await dataworkerInstance.proposeRootBundle(spokePoolClients); - await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); - await multiCallerClient.executeTransactionQueue(); - await updateAllClients(); - expect( - bundleDataClient.getRefundsFor( - await bundleDataClient.getNextBundleRefunds(), - relayer.address, - destinationChainId, - erc20_2.address - ) - ).to.equal(getRefundForFills([fill2])); - }); - }); - it("Returns unfilled deposits", async function () { - await updateAllClients(); - - const deposit1 = { - ...(await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - )), - blockNumber: await getLastBlockNumber(), - } as DepositWithBlock; - deposit1.quoteBlockNumber = (await hubPoolClient.computeRealizedLpFeePct(deposit1, l1Token_1.address)).quoteBlock; - - const deposit2 = { - ...(await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_2, - depositor, - originChainId, - amountToDeposit - )), - blockNumber: await getLastBlockNumber(), - } as DepositWithBlock; - deposit2.quoteBlockNumber = (await hubPoolClient.computeRealizedLpFeePct(deposit2, l1Token_2.address)).quoteBlock; - - // Unfilled deposits are ignored. - await updateAllClients(); - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(0), spokePoolClients); - expect(data1.unfilledDeposits).to.deep.equal([]); - - // Two deposits with no fills per destination chain ID. - await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit.mul(toBN(2)) - ); - await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_2, - depositor, - originChainId, - amountToDeposit.mul(toBN(2)) - ); - await updateAllClients(); - const data2 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(1), spokePoolClients); - expect(data2.unfilledDeposits).to.deep.equal([]); - - // Fills that don't match deposits do not affect unfilledAmount counter. - // Note: We switch the spoke pool address in the following fills from the fills that eventually do match with - // the deposits. - await buildFill(spokePool_2, erc20_2, depositor, relayer, { ...deposit1, depositId: 99 }, 0.5); - await buildFill(spokePool_1, erc20_1, depositor, relayer, { ...deposit2, depositId: 99 }, 0.25); - - // One partially filled deposit per destination chain ID. - const fill1 = await buildFill(spokePool_2, erc20_2, depositor, relayer, deposit1, 0.5); - const fill2 = await buildFill(spokePool_1, erc20_1, depositor, relayer, deposit2, 0.25); - await updateAllClients(); - const data3 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(3), spokePoolClients); - expect(data3.unfilledDeposits) - .excludingEvery(ignoredDepositParams) - .to.deep.equal([ - { - unfilledAmount: amountToDeposit.sub(fill1.fillAmount), - deposit: deposit1, - }, - { - unfilledAmount: amountToDeposit.sub(fill2.fillAmount), - deposit: deposit2, - }, - ]); - - // If block range does not cover fills, then unfilled deposits are not included. - expect( - (await dataworkerInstance.clients.bundleDataClient.loadData(IMPOSSIBLE_BLOCK_RANGE, spokePoolClients)) - .unfilledDeposits - ).to.deep.equal([]); - - // All deposits are fulfilled; unfilled deposits that are fully filled should be ignored. - await buildFill(spokePool_2, erc20_2, depositor, relayer, deposit1, 1); - await buildFill(spokePool_1, erc20_1, depositor, relayer, deposit2, 1); - await updateAllClients(); - const data5 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(4), spokePoolClients); - expect(data5.unfilledDeposits).to.deep.equal([]); - - // TODO: Add test where deposit has matched fills but none were the first ever fill for that deposit (i.e. where - // fill.amount != fill.totalAmountFilled). This can only be done after adding in block range constraints on Fill - // events queried. - - // Fill events emitted by slow relays are included in unfilled amount calculations. - const deposit5 = { - ...(await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit - )), - blockNumber: await getLastBlockNumber(), - blockTimestamp: (await spokePool_2.provider.getBlock(await getLastBlockNumber())).timestamp, - } as DepositWithBlock; - deposit5.quoteBlockNumber = (await hubPoolClient.computeRealizedLpFeePct(deposit5, l1Token_1.address)).quoteBlock; - const fill3 = await buildFill(spokePool_1, erc20_1, depositor, relayer, deposit5, 0.25); - - // One unfilled deposit that we're going to slow fill: - await updateAllClients(); - const data6 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); - expect(data6.unfilledDeposits) - .excludingEvery(ignoredDepositParams) - .to.deep.equal([{ unfilledAmount: amountToDeposit.sub(fill3.fillAmount), deposit: deposit5 }]); - - const slowRelays = buildSlowRelayLeaves([deposit5]); - const tree = await buildSlowRelayTree(slowRelays); - await spokePool_1.relayRootBundle(tree.getHexRoot(), tree.getHexRoot()); - await buildSlowFill(spokePool_1, fill3, depositor, []); - - // The unfilled deposit has now been fully filled. - await updateAllClients(); - const data7 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(6), spokePoolClients); - expect(data7.unfilledDeposits).to.deep.equal([]); - }); - it("Returns fills to refund", async function () { - await updateAllClients(); - - const deposit1 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - const deposit2 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_2, - depositor, - originChainId, - amountToDeposit - ); - - // Submit a valid fill. - const fill1 = { - ...(await buildFill(spokePool_2, erc20_2, depositor, relayer, deposit1, 0.5)), - blockTimestamp: (await spokePool_2.provider.getBlock(await getLastBlockNumber())).timestamp, - }; - await updateAllClients(); - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(0), spokePoolClients); - expect(data1.fillsToRefund) - .excludingEvery(ignoredDepositParams) - .to.deep.equal({ - [destinationChainId]: { - [erc20_2.address]: { - fills: [fill1], - refunds: { [relayer.address]: getRefundForFills([fill1]) }, - totalRefundAmount: getRefundForFills([fill1]), - realizedLpFees: getRealizedLpFeeForFills([fill1]), - }, - }, - }); - - // If block range does not cover fills, then they are not included - expect( - (await dataworkerInstance.clients.bundleDataClient.loadData(IMPOSSIBLE_BLOCK_RANGE, spokePoolClients)) - .fillsToRefund - ).to.deep.equal({}); - - // Submit fills without matching deposits. These should be ignored by the client. - // Note: Switch just the relayer fee % to make fills invalid. This also ensures that client is correctly - // distinguishing between valid speed up fills with modified relayer fee %'s and invalid fill relay calls - // with all correct fill params except for the relayer fee %. - await buildFill(spokePool_1, erc20_1, depositor, relayer, { ...deposit2, relayerFeePct: toBN(0) }, 0.5); - await updateAllClients(); - const data3 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(1), spokePoolClients); - expect(data3.fillsToRefund).to.deep.equal(data1.fillsToRefund); - - // Submit fills that match deposit in all properties except for realized lp fee % or l1 token. These should be - // ignored because the rate model client deems them invalid. These are the two properties added to the deposit - // object by the spoke pool client. - // Note: This fill has identical deposit data to fill2 except for the realized lp fee %. - await buildFill( - spokePool_1, - erc20_1, - depositor, - relayer, - { ...deposit2, realizedLpFeePct: deposit2.realizedLpFeePct?.div(toBN(2)) }, - 0.25 - ); - // Note: This fill has identical deposit data to fill2 except for the destination token being different - await buildFill(spokePool_1, erc20_2, depositor, relayer, deposit2, 0.25); - await updateAllClients(); - const data4 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(2), spokePoolClients); - expect(data4.fillsToRefund).to.deep.equal(data1.fillsToRefund); - - // Slow relay fills are added. - const deposit3 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_2, - depositor, - originChainId, - amountToDeposit - ); - const fill3 = { - ...(await buildFill(spokePool_1, erc20_1, depositor, relayer, deposit3, 0.25)), - blockTimestamp: (await spokePool_1.provider.getBlock(await getLastBlockNumber())).timestamp, - }; - - const slowRelays = buildSlowRelayLeaves([deposit3]); - const tree = await buildSlowRelayTree(slowRelays); - await spokePool_1.relayRootBundle(tree.getHexRoot(), tree.getHexRoot()); - const slowFill3 = { - ...(await buildSlowFill(spokePool_1, fill3, depositor, [])), - blockTimestamp: (await spokePool_1.provider.getBlock(await getLastBlockNumber())).timestamp, - }; - - await updateAllClients(); - const data5 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(3), spokePoolClients); - const expectedData5 = { - ...data4.fillsToRefund, - [slowFill3.destinationChainId]: { - [erc20_1.address]: { - fills: [fill3, slowFill3], // Slow fill gets added to fills list - realizedLpFees: getRealizedLpFeeForFills([fill3, slowFill3]), // Slow fill does affect realized LP fee - totalRefundAmount: getRefundForFills([fill3]), - refunds: { [relayer.address]: getRefundForFills([fill3]) }, - }, - }, - }; - expect(data5.fillsToRefund).excludingEvery(ignoredDepositParams).to.deep.equal(expectedData5); - - // Speed up relays are included. Re-use the same fill information - const fill4 = await buildModifiedFill(spokePool_2, depositor, relayer, fill1, 2, 0.1); - expect(fill4.totalFilledAmount.gt(fill4.fillAmount), "speed up fill didn't match original deposit").to.be.true; - await updateAllClients(); - const data6 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(4), spokePoolClients); - expect(data6.fillsToRefund[destinationChainId][erc20_2.address].totalRefundAmount).to.equal( - getRefundForFills([fill1, fill4]) - ); - expect(data6.fillsToRefund[destinationChainId][erc20_2.address].realizedLpFees).to.equal( - getRealizedLpFeeForFills([fill1, fill4]) - ); - }); - it("Returns deposits", async function () { - await updateAllClients(); - - const deposit1 = await buildDeposit( - hubPoolClient, - spokePool_2, - erc20_2, - l1Token_1, - depositor, - originChainId, - amountToDeposit - ); - const blockNumber = await spokePool_2.provider.getBlockNumber(); - const realizedLpFeePctData = await hubPoolClient.computeRealizedLpFeePct( - { ...deposit1, blockNumber }, - l1Token_1.address - ); - - // Should include all deposits, even those not matched by a relay - await updateAllClients(); - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); - expect(data1.deposits) - .excludingEvery(["blockTimestamp", "logIndex", "transactionHash", "transactionIndex"]) - .to.deep.equal([{ ...deposit1, quoteBlockNumber: realizedLpFeePctData.quoteBlock, blockNumber }]); - - // If block range does not cover deposits, then they are not included - expect( - (await dataworkerInstance.clients.bundleDataClient.loadData(IMPOSSIBLE_BLOCK_RANGE, spokePoolClients)).deposits - ).to.deep.equal([]); - }); describe("V3 Events", function () { let mockOriginSpokePoolClient: MockSpokePoolClient, mockDestinationSpokePoolClient: MockSpokePoolClient; @@ -754,16 +142,6 @@ describe("Dataworker: Load data used in all functions", async function () { dataworkerInstance.blockRangeEndBlockBuffer ); }); - function generateV2Deposit(): Event { - return mockOriginSpokePoolClient.deposit({ - originToken: erc20_1.address, - message: "0x", - quoteTimestamp: getCurrentTime() - 10, - destinationChainId, - blockNumber: spokePoolClient_1.latestBlockSearched, // @dev use latest block searched from non-mocked client - // so that mocked client's latestBlockSearched gets set to the same value. - } as interfaces.V2DepositWithBlock); - } function generateV3Deposit(eventOverride?: Partial): Event { return mockOriginSpokePoolClient.depositV3({ inputToken: erc20_1.address, @@ -836,34 +214,6 @@ describe("Dataworker: Load data used in all functions", async function () { } as interfaces.SlowFillRequest); } - it("Separates V3 and V2 deposits", async function () { - // Inject a series of v2DepositWithBlock and v3DepositWithBlock events. - const depositV3Events: Event[] = []; - const depositV2Events: Event[] = []; - - for (let idx = 0; idx < 10; ++idx) { - const random = Math.random(); - if (random < 0.5) { - depositV3Events.push(generateV3Deposit()); - } else { - depositV2Events.push(generateV2Deposit()); - } - } - await mockOriginSpokePoolClient.update(["FundsDeposited", "V3FundsDeposited"]); - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( - getDefaultBlockRange(5), - spokePoolClients - ); - - // client correctly sorts V2/V3 events into expected dictionaries. - expect(data1.deposits.map((deposit) => deposit.depositId)).to.deep.equal( - depositV2Events.map((event) => event.args.depositId) - ); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].map((deposit) => deposit.depositId)).to.deep.equal( - depositV3Events.map((event) => event.args.depositId) - ); - }); it("Filters expired deposits", async function () { const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( [originChainId, destinationChainId], @@ -1016,7 +366,7 @@ describe("Dataworker: Load data used in all functions", async function () { [originChainId]: spokePoolClient_1, [destinationChainId]: spokePoolClient_2, }); - expect(spyLogIncludes(spy, -3, "Located V3 deposit outside of SpokePoolClient's search range")).is.true; + expect(spyLogIncludes(spy, -2, "Located V3 deposit outside of SpokePoolClient's search range")).is.true; expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); expect(data1.bundleDepositsV3).to.deep.equal({}); }); @@ -1378,7 +728,7 @@ describe("Dataworker: Load data used in all functions", async function () { [originChainId]: spokePoolClient_1, [destinationChainId]: spokePoolClient_2, }); - expect(spyLogIncludes(spy, -3, "Located V3 deposit outside of SpokePoolClient's search range")).is.true; + expect(spyLogIncludes(spy, -2, "Located V3 deposit outside of SpokePoolClient's search range")).is.true; expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address].length).to.equal(1); expect(data1.bundleDepositsV3).to.deep.equal({}); }); @@ -1519,7 +869,7 @@ describe("Dataworker: Load data used in all functions", async function () { }); // Here we can see that the historical query for the deposit actually succeeds, but the deposit itself // was not one eligible to be slow filled. - expect(spyLogIncludes(spy, -3, "Located V3 deposit outside of SpokePoolClient's search range")).is.true; + expect(spyLogIncludes(spy, -2, "Located V3 deposit outside of SpokePoolClient's search range")).is.true; expect(data1.bundleSlowFillsV3).to.deep.equal({}); expect(data1.bundleDepositsV3).to.deep.equal({}); @@ -1746,57 +1096,4 @@ describe("Dataworker: Load data used in all functions", async function () { expect(dataworkerInstance.clients.bundleDataClient.getBundleTimestampsFromCache(key3)).to.deep.equal(cache3); }); }); - - it("Can fetch historical deposits not found in spoke pool client's memory", async function () { - // Send a deposit. - await updateAllClients(); - const deposit1 = await buildDeposit( - hubPoolClient, - spokePool_1, - erc20_1, - l1Token_1, - depositor, - destinationChainId, - amountToDeposit - ); - const deposit1Block = await spokePool_1.provider.getBlockNumber(); - - // Construct a spoke pool client with a small search range that would not include the deposit. - spokePoolClient_1.firstBlockToSearch = deposit1Block + 1; - spokePoolClient_1.eventSearchConfig.fromBlock = spokePoolClient_1.firstBlockToSearch; - await updateAllClients(); - expect(spokePoolClient_1.getDeposits().length).to.equal(0); - - // Send a fill now and force the bundle data client to query for the historical deposit. - const fill1 = await buildFill(spokePool_2, erc20_2, depositor, relayer, deposit1, 0.5); - await updateAllClients(); - expect(spokePoolClient_2.getFills().length).to.equal(1); - expect(spokePoolClient_1.getDeposits().length).to.equal(0); - - // For queryHistoricalDepositForFill to work we need to have a deployment block set for the spoke pool client. - const bundleData = await bundleDataClient.loadData(getDefaultBlockRange(0), spokePoolClients); - expect(spyLogIncludes(spy, -3, "Located deposit outside of SpokePoolClient's search range")).is.true; - expect(bundleData.fillsToRefund) - .excludingEvery(["blockTimestamp"]) - .to.deep.equal({ - [destinationChainId]: { - [erc20_2.address]: { - fills: [fill1], - refunds: { [relayer.address]: getRefundForFills([fill1]) }, - totalRefundAmount: getRefundForFills([fill1]), - realizedLpFees: getRealizedLpFeeForFills([fill1]), - }, - }, - }); - expect(bundleData.deposits).to.deep.equal([]); - expect(bundleData.allValidFills.length).to.equal(1); - expect(bundleData.unfilledDeposits) - .excludingEvery(["logIndex", "transactionHash", "transactionIndex", "blockNumber", "quoteBlockNumber"]) - .to.deep.equal([ - { - unfilledAmount: amountToDeposit.sub(fill1.fillAmount), - deposit: { ...deposit1 }, - }, - ]); - }); }); diff --git a/test/Dataworker.proposeRootBundle.ts b/test/Dataworker.proposeRootBundle.ts index d0f02aaff..479169383 100644 --- a/test/Dataworker.proposeRootBundle.ts +++ b/test/Dataworker.proposeRootBundle.ts @@ -62,13 +62,16 @@ describe("Dataworker: Propose root bundle", async function () { .sort((logA: unknown, logB: unknown) => logB["callId"] - logA["callId"]) // Sort by callId in descending order .find((log: unknown) => log["lastArg"]["message"].includes(message)).lastArg; }; + const getMostRecentLoadDataResults = (): { blockRangesForChains: number[] } => { + return getMostRecentLog(spy, "Finished loading V3 spoke pool data"); + }; // TEST 1: // Before submitting any spoke pool transactions, check that dataworker behaves as expected with no roots. const latestBlock1 = await hubPool.provider.getBlockNumber(); await dataworkerInstance.proposeRootBundle(spokePoolClients); expect(lastSpyLogIncludes(spy, "No pool rebalance leaves, cannot propose")).to.be.true; - const loadDataResults1 = getMostRecentLog(spy, "Finished loading spoke pool data"); + const loadDataResults1 = getMostRecentLoadDataResults(); expect(loadDataResults1.blockRangesForChains).to.deep.equal(CHAIN_ID_TEST_LIST.map(() => [0, latestBlock1])); // TEST 2: @@ -98,7 +101,7 @@ describe("Dataworker: Propose root bundle", async function () { ); const expectedSlowRelayRefundRoot2 = await dataworkerInstance.buildSlowRelayRoot(blockRange2, spokePoolClients); await dataworkerInstance.proposeRootBundle(spokePoolClients); - const loadDataResults2 = getMostRecentLog(spy, "Finished loading spoke pool data"); + const loadDataResults2 = getMostRecentLoadDataResults(); expect(loadDataResults2.blockRangesForChains).to.deep.equal(blockRange2); // Should have enqueued a new transaction: expect(lastSpyLogIncludes(spy, "Enqueing new root bundle proposal txn")).to.be.true; @@ -146,7 +149,7 @@ describe("Dataworker: Propose root bundle", async function () { [latestBlock2 + 1, latestBlock3], ]; expect(lastSpyLogIncludes(spy, "No pool rebalance leaves, cannot propose")).to.be.true; - const loadDataResults3 = getMostRecentLog(spy, "Finished loading spoke pool data"); + const loadDataResults3 = getMostRecentLoadDataResults(); expect(loadDataResults3.blockRangesForChains).to.deep.equal(blockRange3); // TEST 4: @@ -176,7 +179,7 @@ describe("Dataworker: Propose root bundle", async function () { // TEST 4: cont. await dataworkerInstance.proposeRootBundle(spokePoolClients); - const loadDataResults4 = getMostRecentLog(spy, "Finished loading spoke pool data"); + const loadDataResults4 = getMostRecentLoadDataResults(); expect(loadDataResults4.blockRangesForChains).to.deep.equal(blockRange4); // Should have enqueued a new transaction: expect(lastSpyLogIncludes(spy, "Enqueing new root bundle proposal txn")).to.be.true;