diff --git a/package.json b/package.json index 32cb6f791..a4a0b1df8 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@across-protocol/constants": "^3.1.30", "@across-protocol/contracts": "^3.0.25", - "@across-protocol/sdk": "^3.4.20", + "@across-protocol/sdk": "^4.0.0", "@arbitrum/sdk": "^4.0.2", "@consensys/linea-sdk": "^0.2.1", "@defi-wonderland/smock": "^2.3.5", diff --git a/scripts/withdrawFromOpStack.ts b/scripts/withdrawFromOpStack.ts index e4ba5041d..80d0abddc 100644 --- a/scripts/withdrawFromOpStack.ts +++ b/scripts/withdrawFromOpStack.ts @@ -97,8 +97,9 @@ export async function run(): Promise { "0x", // _data ]; + const functionNameToCall = l1TokenInfo.symbol === "ETH" ? "bridgeETHTo" : "bridgeERC20To"; console.log( - `Submitting bridgeETHTo on the OVM standard bridge @ ${ovmStandardBridge.address} with the following args: `, + `Submitting ${functionNameToCall} on the OVM standard bridge @ ${ovmStandardBridge.address} with the following args: `, ...bridgeArgs ); @@ -122,9 +123,7 @@ export async function run(): Promise { if (!(await askYesNoQuestion("\nDo you want to proceed?"))) { return; } - const withdrawal = await ovmStandardBridge[l1TokenInfo.symbol === "ETH" ? "bridgeETHTo" : "bridgeERC20To"]( - ...bridgeArgs - ); + const withdrawal = await ovmStandardBridge[functionNameToCall](...bridgeArgs); console.log(`Submitted withdrawal: ${blockExplorerLink(withdrawal.hash, chainId)}.`); const receipt = await withdrawal.wait(); console.log("Receipt", receipt); diff --git a/test/Dataworker.loadData.deposit.ts b/test/Dataworker.loadData.deposit.ts new file mode 100644 index 000000000..9814fc5e3 --- /dev/null +++ b/test/Dataworker.loadData.deposit.ts @@ -0,0 +1,559 @@ +import { BundleDataClient, ConfigStoreClient, HubPoolClient, SpokePoolClient } from "../src/clients"; +import { amountToDeposit, destinationChainId, originChainId, repaymentChainId } from "./constants"; +import { setupDataworker } from "./fixtures/Dataworker.Fixture"; +import { + Contract, + FakeContract, + SignerWithAddress, + V3FillFromDeposit, + depositV3, + ethers, + expect, + getDefaultBlockRange, + getDisabledBlockRanges, + sinon, + smock, + spyLogIncludes, +} from "./utils"; + +import { Dataworker } from "../src/dataworker/Dataworker"; // Tested +import { getCurrentTime, toBNWei, ZERO_ADDRESS, BigNumber, bnZero } from "../src/utils"; +import { MockHubPoolClient, MockSpokePoolClient } from "./mocks"; +import { interfaces, utils as sdkUtils } from "@across-protocol/sdk"; + +let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: 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 dataworkerInstance: Dataworker; +let spokePoolClients: { [chainId: number]: SpokePoolClient }; + +let spy: sinon.SinonSpy; + +let updateAllClients: () => Promise; + +// TODO: Rename this file to BundleDataClient +describe("Dataworker: Load bundle data", async function () { + beforeEach(async function () { + ({ + spokePool_1, + erc20_1, + spokePool_2, + erc20_2, + configStoreClient, + hubPoolClient, + l1Token_1, + depositor, + relayer, + dataworkerInstance, + spokePoolClient_1, + spokePoolClient_2, + spokePoolClients, + updateAllClients, + spy, + } = await setupDataworker(ethers, 25, 25, 0)); + bundleDataClient = dataworkerInstance.clients.bundleDataClient; + }); + + describe("Computing bundle deposits and expired deposits to refund", function () { + let mockOriginSpokePoolClient: MockSpokePoolClient, mockDestinationSpokePoolClient: MockSpokePoolClient; + let mockHubPoolClient: MockHubPoolClient; + let mockDestinationSpokePool: FakeContract; + const lpFeePct = toBNWei("0.01"); + + beforeEach(async function () { + await updateAllClients(); + mockHubPoolClient = new MockHubPoolClient( + hubPoolClient.logger, + hubPoolClient.hubPool, + configStoreClient, + hubPoolClient.deploymentBlock, + hubPoolClient.chainId + ); + // Mock a realized lp fee pct for each deposit so we can check refund amounts and bundle lp fees. + mockHubPoolClient.setDefaultRealizedLpFeePct(lpFeePct); + mockOriginSpokePoolClient = new MockSpokePoolClient( + spokePoolClient_1.logger, + spokePoolClient_1.spokePool, + spokePoolClient_1.chainId, + spokePoolClient_1.deploymentBlock + ); + mockDestinationSpokePool = await smock.fake(spokePoolClient_2.spokePool.interface); + mockDestinationSpokePoolClient = new MockSpokePoolClient( + spokePoolClient_2.logger, + mockDestinationSpokePool as Contract, + spokePoolClient_2.chainId, + spokePoolClient_2.deploymentBlock + ); + spokePoolClients = { + ...spokePoolClients, + [originChainId]: mockOriginSpokePoolClient, + [destinationChainId]: mockDestinationSpokePoolClient, + }; + await mockHubPoolClient.update(); + await mockOriginSpokePoolClient.update(); + await mockDestinationSpokePoolClient.update(); + mockHubPoolClient.setTokenMapping(l1Token_1.address, originChainId, erc20_1.address); + mockHubPoolClient.setTokenMapping(l1Token_1.address, destinationChainId, erc20_2.address); + mockHubPoolClient.setTokenMapping(l1Token_1.address, repaymentChainId, l1Token_1.address); + const bundleDataClient = new BundleDataClient( + dataworkerInstance.logger, + { + ...dataworkerInstance.clients.bundleDataClient.clients, + hubPoolClient: mockHubPoolClient as unknown as HubPoolClient, + }, + dataworkerInstance.clients.bundleDataClient.spokePoolClients, + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers + ); + dataworkerInstance = new Dataworker( + dataworkerInstance.logger, + { ...dataworkerInstance.clients, bundleDataClient }, + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers, + dataworkerInstance.maxRefundCountOverride, + dataworkerInstance.maxL1TokenCountOverride, + dataworkerInstance.blockRangeEndBlockBuffer + ); + }); + + function generateV3Deposit(eventOverride?: Partial): interfaces.Log { + return mockOriginSpokePoolClient.depositV3({ + inputToken: erc20_1.address, + inputAmount: eventOverride?.inputAmount ?? undefined, + outputToken: eventOverride?.outputToken ?? erc20_2.address, + message: eventOverride?.message ?? "0x", + quoteTimestamp: eventOverride?.quoteTimestamp ?? getCurrentTime() - 10, + fillDeadline: eventOverride?.fillDeadline ?? getCurrentTime() + 14400, + destinationChainId, + blockNumber: eventOverride?.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.DepositWithBlock); + } + + function generateV3FillFromDeposit( + deposit: interfaces.DepositWithBlock, + fillEventOverride?: Partial, + _relayer = relayer.address, + _repaymentChainId = repaymentChainId, + fillType = interfaces.FillType.FastFill + ): interfaces.Log { + const fillObject = V3FillFromDeposit(deposit, _relayer, _repaymentChainId); + return mockDestinationSpokePoolClient.fillV3Relay({ + ...fillObject, + relayExecutionInfo: { + updatedRecipient: fillObject.relayExecutionInfo.updatedRecipient, + updatedMessage: fillObject.relayExecutionInfo.updatedMessage, + updatedOutputAmount: fillObject.relayExecutionInfo.updatedOutputAmount, + fillType, + }, + blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client + // so that mocked client's latestBlockSearched gets set to the same value. + } as interfaces.FillWithBlock); + } + + function generateV3FillFromDepositEvent( + depositEvent: interfaces.Log, + fillEventOverride?: Partial, + _relayer = relayer.address, + _repaymentChainId = repaymentChainId, + fillType = interfaces.FillType.FastFill, + outputAmount: BigNumber = depositEvent.args.outputAmount, + updatedOutputAmount: BigNumber = depositEvent.args.outputAmount + ): interfaces.Log { + const { args } = depositEvent; + return mockDestinationSpokePoolClient.fillV3Relay({ + ...args, + relayer: _relayer, + outputAmount, + repaymentChainId: _repaymentChainId, + relayExecutionInfo: { + updatedRecipient: depositEvent.args.updatedRecipient, + updatedMessage: depositEvent.args.updatedMessage, + updatedOutputAmount: updatedOutputAmount, + fillType, + }, + blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client + // so that mocked client's latestBlockSearched gets set to the same value. + } as interfaces.FillWithBlock); + } + + it("Filters expired deposits", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + // Send unexpired deposit + generateV3Deposit(); + // Send expired deposit + const expiredDeposits = [generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 })]; + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(2); + expect( + data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].map((deposit) => deposit.depositId) + ).to.deep.equal(expiredDeposits.map((event) => event.args.depositId)); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + }); + + it("Ignores disabled chains", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + // Send unexpired deposit + generateV3Deposit(); + // Send expired deposit + generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + // Returns no data if block range is undefined + const emptyData = await dataworkerInstance.clients.bundleDataClient.loadData( + getDisabledBlockRanges(), + spokePoolClients + ); + expect(emptyData.bundleDepositsV3).to.deep.equal({}); + expect(emptyData.expiredDepositsToRefundV3).to.deep.equal({}); + }); + + it("Does not consider zero value deposits", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + // Send unexpired and expired deposits in bundle block range. + generateV3Deposit({ + inputAmount: bnZero, + message: "0x", + }); + generateV3Deposit({ + fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1, + inputAmount: bnZero, + message: "0x", + }); + + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); + + it("Does not consider expired zero value deposits from prior bundle", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + // Send expired deposits + const priorBundleDeposit = generateV3Deposit({ + fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1, + inputAmount: bnZero, + message: "0x", + }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [priorBundleDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); + + it("Filters unexpired deposit out of block range", async function () { + // Send deposit behind and after origin chain block range. Should not be included in bundleDeposits. + // First generate mock deposit events with some block time between events. + const deposits = [ + generateV3Deposit({ blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 1 }), + generateV3Deposit({ blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 11 }), + generateV3Deposit({ blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 21 }), + ]; + // Create a block range that contains only the middle deposit. + const originChainBlockRange = [deposits[1].blockNumber - 1, deposits[1].blockNumber + 1]; + // Substitute origin chain bundle block range. + const bundleBlockRanges = [originChainBlockRange].concat(getDefaultBlockRange(5).slice(1)); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + expect(mockOriginSpokePoolClient.getDeposits().length).to.equal(deposits.length); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address][0].depositId).to.equal(deposits[1].args.depositId); + }); + it("Includes duplicate deposits in bundle data", async function () { + const deposit = generateV3Deposit(); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const depositToDuplicate = mockOriginSpokePoolClient.getDeposits()[0]; + const dupe1 = mockOriginSpokePoolClient.depositV3(depositToDuplicate); + const dupe2 = mockOriginSpokePoolClient.depositV3(depositToDuplicate); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + expect( + mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId).length + ).to.equal(3); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address][0].transactionHash).to.equal( + deposit.transactionHash + ); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address][1].transactionHash).to.equal(dupe1.transactionHash); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address][2].transactionHash).to.equal(dupe2.transactionHash); + }); + it("Filters duplicate deposits out of block range", async function () { + const deposit = generateV3Deposit({ blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const depositToDuplicate = mockOriginSpokePoolClient.getDeposits()[0]; + const duplicateDeposit = mockOriginSpokePoolClient.depositV3({ + ...depositToDuplicate, + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 11, + }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + expect( + mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId).length + ).to.equal(2); + const originChainBlockRange = [deposit.blockNumber, duplicateDeposit.blockNumber - 1]; + // Substitute origin chain bundle block range. + const bundleBlockRanges = [originChainBlockRange].concat(getDefaultBlockRange(5).slice(1)); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); + }); + it("Ignores expired deposits that were filled in same bundle", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + // Send deposit that expires in this bundle. + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + + // Now, send a fill for the deposit that would be in the same bundle. This should eliminate the expired + // deposit from a refund. + generateV3FillFromDepositEvent(expiredDeposit); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + const data2 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(6), + spokePoolClients + ); + expect(data2.expiredDepositsToRefundV3).to.deep.equal({}); + expect(data2.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + }); + + it("Returns prior bundle expired deposits", async function () { + // Send deposit that expires in this bundle. + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + // Send deposit that expires in this bundle. + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + + // Now, load a bundle that doesn't include the deposit in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data2 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // Now, there is no bundle deposit but still an expired deposit to refund. + expect(data2.bundleDepositsV3).to.deep.equal({}); + expect(data2.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + }); + it("Includes duplicate prior bundle expired deposits", async function () { + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + + // Send deposit that expires in this bundle and send duplicate at same block. + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(2); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(2); + + // Now, load a bundle that doesn't include the deposits in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data2 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // Now, there is no bundle deposit but still two expired deposits to refund. + expect(data2.bundleDepositsV3).to.deep.equal({}); + expect(data2.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(2); + }); + it("Handles when deposit is greater than origin bundle end block but fill is within range", async function () { + // Send deposit after origin chain block range. + const blockRanges = getDefaultBlockRange(5); + const futureDeposit = generateV3Deposit(); + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + blockRanges[originChainIndex] = [blockRanges[0][0], futureDeposit.blockNumber - 1]; + + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + generateV3FillFromDepositEvent(futureDeposit); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(blockRanges, spokePoolClients); + expect(data1.bundleDepositsV3).to.deep.equal({}); + + // Fill should not be included since we cannot validate fills when the deposit is in a following bundle. + // This fill is considered a "pre-fill" and will be validated when the deposit is included in a bundle. + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(spyLogIncludes(spy, -2, "invalid fills in range")).to.be.true; + }); + it("Does not count prior bundle expired deposits that were filled", async function () { + // Send deposit that expires in this bundle. + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + // Let's make fill status for the relay hash always return Filled. + const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); + mockDestinationSpokePool.fillStatuses.whenCalledWith(expiredDepositHash).returns(interfaces.FillStatus.Filled); + + // Now, load a bundle that doesn't include the deposit in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // There should be no expired deposit to refund because its fill status is Filled. + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); + it("Does not count prior bundle expired deposits that we queried a fill for", async function () { + // Send deposit that expires in this bundle. + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Unlike previous test, we send a fill that the spoke pool client should query which also eliminates this + // expired deposit from being refunded. + generateV3FillFromDeposit(deposits[0]); + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill", "FilledV3Relay"]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(1); + + // Now, load a bundle that doesn't include the deposit in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // There should be no expired deposit to refund. + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + }); + }); + + describe("Miscellaneous functions", function () { + it("getUpcomingDepositAmount", async function () { + // Send two deposits on different chains + await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + ZERO_ADDRESS, + amountToDeposit + ); + await depositV3( + spokePool_2, + originChainId, + depositor, + erc20_2.address, + amountToDeposit, + ZERO_ADDRESS, + amountToDeposit + ); + await updateAllClients(); + expect(await bundleDataClient.getUpcomingDepositAmount(originChainId, erc20_1.address, 0)).to.equal( + amountToDeposit + ); + expect(await bundleDataClient.getUpcomingDepositAmount(destinationChainId, erc20_2.address, 0)).to.equal( + amountToDeposit + ); + + // Removes deposits using block, token, and chain filters. + expect( + await bundleDataClient.getUpcomingDepositAmount( + originChainId, + erc20_1.address, + spokePoolClient_1.latestBlockSearched // block higher than the deposit + ) + ).to.equal(0); + expect( + await bundleDataClient.getUpcomingDepositAmount( + originChainId, + erc20_2.address, // diff token + 0 + ) + ).to.equal(0); + expect( + await bundleDataClient.getUpcomingDepositAmount( + destinationChainId, // diff chain + erc20_1.address, + 0 + ) + ).to.equal(0); + + // spoke pool client for chain not defined + expect( + await bundleDataClient.getUpcomingDepositAmount( + originChainId + destinationChainId + repaymentChainId + 1, // spoke pool client for chain is not defined in BundleDataClient + erc20_1.address, + 0 + ) + ).to.equal(0); + }); + }); +}); diff --git a/test/Dataworker.loadData.fill.ts b/test/Dataworker.loadData.fill.ts index edc1606d7..65e725cc5 100644 --- a/test/Dataworker.loadData.fill.ts +++ b/test/Dataworker.loadData.fill.ts @@ -23,13 +23,12 @@ import { sinon, smock, spyLogIncludes, - bnZero, } from "./utils"; import { Dataworker } from "../src/dataworker/Dataworker"; // Tested -import { getCurrentTime, toBN, Event, toBNWei, fixedPointAdjustment, ZERO_ADDRESS, BigNumber } from "../src/utils"; -import { MockConfigStoreClient, MockHubPoolClient, MockSpokePoolClient } from "./mocks"; -import { interfaces, utils as sdkUtils, providers } from "@across-protocol/sdk"; +import { getCurrentTime, toBN, toBNWei, fixedPointAdjustment, ZERO_ADDRESS, BigNumber, bnZero } from "../src/utils"; +import { MockBundleDataClient, MockConfigStoreClient, MockHubPoolClient, MockSpokePoolClient } from "./mocks"; +import { interfaces, constants as sdkConstants, providers } from "@across-protocol/sdk"; import { cloneDeep } from "lodash"; import { CombinedRefunds } from "../src/dataworker/DataworkerUtils"; import { INFINITE_FILL_DEADLINE } from "../src/common"; @@ -47,8 +46,7 @@ let spy: sinon.SinonSpy; let updateAllClients: () => Promise; -// TODO: Rename this file to BundleDataClient -describe("Dataworker: Load data used in all functions", async function () { +describe("Dataworker: Load bundle data", async function () { beforeEach(async function () { ({ spokePool_1, @@ -92,7 +90,7 @@ describe("Dataworker: Load data used in all functions", async function () { }); }); - describe("V3 Events", function () { + describe("Compute fills to refund", function () { let mockOriginSpokePoolClient: MockSpokePoolClient, mockDestinationSpokePoolClient: MockSpokePoolClient; let mockHubPoolClient: MockHubPoolClient; let mockDestinationSpokePool: FakeContract; @@ -144,7 +142,7 @@ describe("Dataworker: Load data used in all functions", async function () { mockHubPoolClient.setTokenMapping(l1Token_1.address, originChainId, erc20_1.address); mockHubPoolClient.setTokenMapping(l1Token_1.address, destinationChainId, erc20_2.address); mockHubPoolClient.setTokenMapping(l1Token_1.address, repaymentChainId, l1Token_1.address); - const bundleDataClient = new BundleDataClient( + const bundleDataClient = new MockBundleDataClient( dataworkerInstance.logger, { ...dataworkerInstance.clients.bundleDataClient.clients, @@ -163,11 +161,12 @@ describe("Dataworker: Load data used in all functions", async function () { ); }); - function generateV3Deposit(eventOverride?: Partial): Event { + function generateV3Deposit(eventOverride?: Partial): interfaces.Log { return mockOriginSpokePoolClient.depositV3({ inputToken: erc20_1.address, + inputAmount: eventOverride?.inputAmount ?? undefined, outputToken: eventOverride?.outputToken ?? erc20_2.address, - message: "0x", + message: eventOverride?.message ?? "0x", quoteTimestamp: eventOverride?.quoteTimestamp ?? getCurrentTime() - 10, fillDeadline: eventOverride?.fillDeadline ?? getCurrentTime() + 14400, destinationChainId, @@ -182,14 +181,14 @@ describe("Dataworker: Load data used in all functions", async function () { _relayer = relayer.address, _repaymentChainId = repaymentChainId, fillType = interfaces.FillType.FastFill - ): Event { + ): interfaces.Log { const fillObject = V3FillFromDeposit(deposit, _relayer, _repaymentChainId); return mockDestinationSpokePoolClient.fillV3Relay({ ...fillObject, relayExecutionInfo: { - updatedRecipient: fillObject.updatedRecipient, - updatedMessage: fillObject.updatedMessage, - updatedOutputAmount: fillObject.updatedOutputAmount, + updatedRecipient: fillObject.relayExecutionInfo.updatedRecipient, + updatedMessage: fillObject.relayExecutionInfo.updatedMessage, + updatedOutputAmount: fillObject.relayExecutionInfo.updatedOutputAmount, fillType, }, blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client @@ -198,14 +197,14 @@ describe("Dataworker: Load data used in all functions", async function () { } function generateV3FillFromDepositEvent( - depositEvent: Event, + depositEvent: interfaces.Log, fillEventOverride?: Partial, _relayer = relayer.address, _repaymentChainId = repaymentChainId, fillType = interfaces.FillType.FastFill, outputAmount: BigNumber = depositEvent.args.outputAmount, updatedOutputAmount: BigNumber = depositEvent.args.outputAmount - ): Event { + ): interfaces.Log { const { args } = depositEvent; return mockDestinationSpokePoolClient.fillV3Relay({ ...args, @@ -213,8 +212,8 @@ describe("Dataworker: Load data used in all functions", async function () { outputAmount, repaymentChainId: _repaymentChainId, relayExecutionInfo: { - updatedRecipient: depositEvent.updatedRecipient, - updatedMessage: depositEvent.updatedMessage, + updatedRecipient: depositEvent.args.updatedRecipient, + updatedMessage: depositEvent.args.updatedMessage, updatedOutputAmount: updatedOutputAmount, fillType, }, @@ -223,98 +222,204 @@ describe("Dataworker: Load data used in all functions", async function () { } as interfaces.FillWithBlock); } - it("Filters expired deposits", async function () { - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - // Send unexpired deposit - const unexpiredDeposits = [generateV3Deposit()]; - // Send expired deposit - const expiredDeposits = [generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 })]; - const depositEvents = [...unexpiredDeposits, ...expiredDeposits]; + it("Does not refund fills for zero value deposits", async function () { + generateV3Deposit({ + inputAmount: bnZero, + message: "0x", + }); + generateV3Deposit({ + inputAmount: bnZero, + message: "0x", + }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + generateV3FillFromDeposit(deposits[0]); + generateV3FillFromDeposit({ + ...deposits[1], + message: sdkConstants.ZERO_BYTES, + }); + + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( getDefaultBlockRange(5), spokePoolClients ); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].map((deposit) => deposit.depositId)).to.deep.equal( - depositEvents.map((event) => event.args.depositId) - ); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(2); - expect( - data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].map((deposit) => deposit.depositId) - ).to.deep.equal(expiredDeposits.map((event) => event.args.depositId)); - expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("invalid fills")).length).to.equal(0); }); - it("Ignores disabled chains", async function () { - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - // Send unexpired deposit - generateV3Deposit(); - // Send expired deposit - generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + describe("Duplicate deposits in same bundle as fill", function () { + it("Sends duplicate deposit refunds for fills in bundle", async function () { + // Send duplicate deposits. + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(3); - // Returns no data if block range is undefined - const emptyData = await dataworkerInstance.clients.bundleDataClient.loadData( - getDisabledBlockRanges(), - spokePoolClients - ); - expect(emptyData.bundleDepositsV3).to.deep.equal({}); - expect(emptyData.expiredDepositsToRefundV3).to.deep.equal({}); + // Fill deposit. + generateV3FillFromDeposit(deposits[0]); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + + // Bundle should contain all deposits. + // Bundle should refund fill. + // Bundle should refund duplicate deposits to filler + const bundleBlockRanges = getDefaultBlockRange(5); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(3); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); + + it("Sends duplicate deposit refunds for slow fills in bundle", async function () { + // Send duplicate deposits. + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const dupe1 = await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + const dupe2 = await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(3); + + // Fill deposit as slow fill + generateV3FillFromDeposit(deposits[0], {}, undefined, undefined, interfaces.FillType.SlowFill); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + + // Bundle should contain all deposits. + // Bundle should refund fill. + // Bundle should refund duplicate deposits to depositor + const bundleBlockRanges = getDefaultBlockRange(5); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3[destinationChainId][erc20_2.address].fills.length).to.equal(1); + expect(data1.bundleFillsV3[destinationChainId][erc20_2.address].refunds).to.deep.equal({}); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(2); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address][0].transactionHash).to.equal( + dupe1.transactionHash + ); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address][1].transactionHash).to.equal( + dupe2.transactionHash + ); + }); + + it("Does not account for duplicate deposit refunds for deposits after bundle block range", async function () { + generateV3Deposit({ + outputToken: randomAddress(), + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 1, + }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + await mockOriginSpokePoolClient.depositV3({ + ...mockOriginSpokePoolClient.getDeposits()[0], + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 11, + }); + await mockOriginSpokePoolClient.depositV3({ + ...mockOriginSpokePoolClient.getDeposits()[0], + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 21, + }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(3); + + const fill = generateV3FillFromDeposit(deposits[0], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber + 21, + }); + + // Create a block range that removes latest event + const destinationChainBlockRange = [fill.blockNumber - 1, fill.blockNumber + 1]; + const originChainBlockRange = [deposits[0].blockNumber, deposits[1].blockNumber]; + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = destinationChainBlockRange; + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + bundleBlockRanges[originChainIndex] = originChainBlockRange; + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(2); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(2); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); + + it("Does not send duplicate deposit refunds if relayer repayment information is invalid", async function () { + // Send duplicate deposits. + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(3); + + // Fill deposit with invalid repayment information. + const invalidRelayer = ethers.utils.randomBytes(32); + const invalidFillEvent = generateV3FillFromDeposit(deposits[0], {}, invalidRelayer); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, + // set the msg.sender as an invalid address. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + // Bundle should contain all deposits. + const bundleBlockRanges = getDefaultBlockRange(5); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); }); - it("Filters unexpired deposit out of block range", async function () { - // Send deposit behind and after origin chain block range. Should not be included in bundleDeposits. - // First generate mock deposit events with some block time between events. - const deposits = [ - generateV3Deposit({ blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 1 }), - generateV3Deposit({ blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 11 }), - generateV3Deposit({ blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 21 }), - ]; - // Create a block range that contains only the middle deposit. - const originChainBlockRange = [deposits[1].blockNumber - 1, deposits[1].blockNumber + 1]; - // Substitute origin chain bundle block range. - const bundleBlockRanges = [originChainBlockRange].concat(getDefaultBlockRange(5).slice(1)); + it("Does not create unexecutable slow fill for zero value deposit", async function () { + generateV3Deposit({ + inputAmount: bnZero, + message: "0x", + }); + generateV3Deposit({ + inputAmount: bnZero, + message: "0x", + }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - expect(mockOriginSpokePoolClient.getDeposits().length).to.equal(deposits.length); - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address][0].depositId).to.equal(deposits[1].args.depositId); - }); - it("Ignores expired deposits that were filled in same bundle", async function () { - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients + const deposits = mockOriginSpokePoolClient.getDeposits(); + generateV3FillFromDeposit( + { + ...deposits[0], + }, + undefined, + undefined, + undefined, + interfaces.FillType.ReplacedSlowFill ); - // Send deposit that expires in this bundle. - const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( - getDefaultBlockRange(5), - spokePoolClients + generateV3FillFromDeposit( + { + ...deposits[1], + message: sdkConstants.ZERO_BYTES, + }, + undefined, + undefined, + undefined, + interfaces.FillType.ReplacedSlowFill ); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); - // Now, send a fill for the deposit that would be in the same bundle. This should eliminate the expired - // deposit from a refund. - generateV3FillFromDepositEvent(expiredDeposit); await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); - const data2 = await dataworkerInstance.clients.bundleDataClient.loadData( - getDefaultBlockRange(6), + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), spokePoolClients ); - expect(data2.expiredDepositsToRefundV3).to.deep.equal({}); - expect(data2.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + + expect(data1.unexecutableSlowFills).to.deep.equal({}); }); it("Saves V3 fast fill under correct repayment chain and repayment token", async function () { const depositV3Events: Event[] = []; @@ -448,7 +553,7 @@ describe("Dataworker: Load data used in all functions", async function () { }); }); - it("Validates fill against old deposit", async function () { + it("Validates fill against old deposit if deposit is not in-memory", async function () { // For this test, we need to actually send a deposit on the spoke pool // because queryHistoricalDepositForFill eth_call's the contract. @@ -489,7 +594,54 @@ describe("Dataworker: Load data used in all functions", async function () { expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); expect(data1.bundleDepositsV3).to.deep.equal({}); }); - it("Validates fill from lite chain against old deposit", async function () { + it("Does not validate fill against deposit in future bundle if deposit is not in-memory", async function () { + // For this test, we need to actually send a deposit on the spoke pool + // because queryHistoricalDepositForFill eth_call's the contract. + + // Send a legacy deposit. + const depositObject = await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit, + { + fillDeadline: INFINITE_FILL_DEADLINE.toNumber(), + } + ); + + // Modify the block ranges such that the deposit is in a future bundle block range. This should render + // the fill invalid. + const depositBlock = await spokePool_1.provider.getBlockNumber(); + const bundleBlockRanges = getDefaultBlockRange(5); + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + bundleBlockRanges[originChainIndex] = [depositBlock - 2, depositBlock - 1]; + + // Construct a spoke pool client with a small search range that would not include the deposit. + spokePoolClient_1.firstBlockToSearch = depositBlock + 1; + spokePoolClient_1.eventSearchConfig.fromBlock = spokePoolClient_1.firstBlockToSearch; + await spokePoolClient_1.update(); + const deposits = spokePoolClient_1.getDeposits(); + expect(deposits.length).to.equal(0); + + // Send a fill now and force the bundle data client to query for the historical deposit. + await fillV3Relay(spokePool_2, depositObject, relayer, repaymentChainId); + await updateAllClients(); + const fills = spokePoolClient_2.getFills(); + expect(fills.length).to.equal(1); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, { + ...spokePoolClients, + [originChainId]: spokePoolClient_1, + [destinationChainId]: spokePoolClient_2, + }); + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("invalid fills")).length).to.equal(1); + }); + it("Validates fill from lite chain against old bundle deposit", async function () { // For this test, we need to actually send a deposit on the spoke pool // because queryHistoricalDepositForFill eth_call's the contract. @@ -592,9 +744,18 @@ describe("Dataworker: Load data used in all functions", async function () { expect(data1.bundleDepositsV3).to.deep.equal({}); }); it("Filters fills out of block range", async function () { - generateV3Deposit({ outputToken: randomAddress() }); - generateV3Deposit({ outputToken: randomAddress() }); - generateV3Deposit({ outputToken: randomAddress() }); + generateV3Deposit({ + outputToken: randomAddress(), + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 1, + }); + generateV3Deposit({ + outputToken: randomAddress(), + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 11, + }); + generateV3Deposit({ + outputToken: randomAddress(), + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 21, + }); await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); const deposits = mockOriginSpokePoolClient.getDeposits(); @@ -609,13 +770,16 @@ describe("Dataworker: Load data used in all functions", async function () { blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber + 21, }), ]; - // Create a block range that contains only the middle event. + // Create a block range that contains only the middle events. const destinationChainBlockRange = [fills[1].blockNumber - 1, fills[1].blockNumber + 1]; - // Substitute destination chain bundle block range. + const originChainBlockRange = [deposits[1].blockNumber - 1, deposits[1].blockNumber + 1]; + // Substitute bundle block ranges. const bundleBlockRanges = getDefaultBlockRange(5); const destinationChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); bundleBlockRanges[destinationChainIndex] = destinationChainBlockRange; + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + bundleBlockRanges[originChainIndex] = originChainBlockRange; await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); expect(mockDestinationSpokePoolClient.getFills().length).to.equal(fills.length); const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); @@ -659,7 +823,7 @@ describe("Dataworker: Load data used in all functions", async function () { spokePoolClients ); expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); - expect(spyLogIncludes(spy, -2, "invalid V3 fills in range")).to.be.true; + expect(spyLogIncludes(spy, -2, "invalid fills in range")).to.be.true; }); it("Matches fill with deposit with outputToken = 0x0", async function () { await depositV3( @@ -683,107 +847,6 @@ describe("Dataworker: Load data used in all functions", async function () { expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); }); - it("Returns prior bundle expired deposits", async function () { - // Send deposit that expires in this bundle. - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - // Send deposit that expires in this bundle. - const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( - getDefaultBlockRange(5), - spokePoolClients - ); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); - - // Now, load a bundle that doesn't include the deposit in its range. - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; - const bundleBlockRanges = getDefaultBlockRange(5); - bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; - const data2 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); - - // Now, there is no bundle deposit but still an expired deposit to refund. - expect(data2.bundleDepositsV3).to.deep.equal({}); - expect(data2.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); - }); - it("Handles when deposit is greater than origin bundle end block but fill is within range", async function () { - // Send deposit after origin chain block range. - const blockRanges = getDefaultBlockRange(5); - const futureDeposit = generateV3Deposit(); - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - blockRanges[originChainIndex] = [blockRanges[0][0], futureDeposit.blockNumber - 1]; - - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - - generateV3FillFromDepositEvent(futureDeposit); - await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(blockRanges, spokePoolClients); - expect(data1.bundleDepositsV3).to.deep.equal({}); - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); - expect(spyLogIncludes(spy, -2, "invalid V3 fills in range")).to.be.false; - }); - it("Does not count prior bundle expired deposits that were filled", async function () { - // Send deposit that expires in this bundle. - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - - // Let's make fill status for the relay hash always return Filled. - const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); - mockDestinationSpokePool.fillStatuses.whenCalledWith(expiredDepositHash).returns(interfaces.FillStatus.Filled); - - // Now, load a bundle that doesn't include the deposit in its range. - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; - const bundleBlockRanges = getDefaultBlockRange(5); - bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); - - // There should be no expired deposit to refund because its fill status is Filled. - expect(data1.bundleDepositsV3).to.deep.equal({}); - expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); - }); - it("Does not count prior bundle expired deposits that we queried a fill for", async function () { - // Send deposit that expires in this bundle. - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const deposits = mockOriginSpokePoolClient.getDeposits(); - - // Unlike previous test, we send a fill that the spoke pool client should query which also eliminates this - // expired deposit from being refunded. - generateV3FillFromDeposit(deposits[0]); - await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill", "FilledV3Relay"]); - expect(mockDestinationSpokePoolClient.getFills().length).to.equal(1); - - // Now, load a bundle that doesn't include the deposit in its range. - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; - const bundleBlockRanges = getDefaultBlockRange(5); - bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); - - // There should be no expired deposit to refund. - expect(data1.bundleDepositsV3).to.deep.equal({}); - expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); - }); - it("getBundleTimestampsFromCache and setBundleTimestampsInCache", async function () { // Unit test await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); @@ -804,8 +867,8 @@ describe("Dataworker: Load data used in all functions", async function () { }); describe("Bytes32 address invalid cases", async function () { it("Fallback to msg.sender when the relayer repayment address is invalid on an EVM chain", async function () { - const depositV3Events: Event[] = []; - const fillV3Events: Event[] = []; + const depositV3Events: interfaces.Log[] = []; + const fillV3Events: interfaces.Log[] = []; const destinationChainId = mockDestinationSpokePoolClient.chainId; // Create three valid deposits depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() })); @@ -862,55 +925,10 @@ describe("Dataworker: Load data used in all functions", async function () { .div(fixedPointAdjustment), }); }); - it("Treats a relayer's fill with a bytes32 address taking repayment on an EVM network as invalid", async function () { - const depositV3Events: Event[] = []; - const fillV3Events: Event[] = []; - const destinationChainId = mockDestinationSpokePoolClient.chainId; - // Create three valid deposits - depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() })); - depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() })); - depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() })); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const deposits = mockOriginSpokePoolClient.getDeposits(); - - // Fill deposits from different relayers - const invalidRelayer = ethers.utils.hexlify(ethers.utils.randomBytes(32)); - fillV3Events.push(generateV3FillFromDeposit(deposits[0])); - fillV3Events.push(generateV3FillFromDeposit(deposits[1])); - fillV3Events.push(generateV3FillFromDeposit(deposits[2], {}, invalidRelayer)); - await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); - // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so - // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, - // set the msg.sender as an invalid address. - const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); - const spokeWrapper = new Contract( - mockDestinationSpokePoolClient.spokePool.address, - mockDestinationSpokePoolClient.spokePool.interface, - provider - ); - fillV3Events.forEach((event) => provider._setTransaction(event.transactionHash, { from: invalidRelayer })); - mockDestinationSpokePoolClient.spokePool = spokeWrapper; - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( - getDefaultBlockRange(5), - spokePoolClients - ); - - // We expect the last fillV3Event to be invalid. - const nValidFills = fillV3Events.length - 1; - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(nValidFills); - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.map((e) => e.depositId)).to.deep.equal( - fillV3Events.slice(0, nValidFills).map((event) => event.args.depositId) - ); - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.map((e) => e.lpFeePct)).to.deep.equal( - fillV3Events.slice(0, nValidFills).map(() => lpFeePct) - ); - expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); - }); // This is essentially a copy of the first test in this block, with the addition of the change to the config store. it("Fill with bytes32 relayer with lite chain deposit is refunded on lite chain to msg.sender", async function () { - const depositV3Events: Event[] = []; - const fillV3Events: Event[] = []; + const depositV3Events: interfaces.Log[] = []; + const fillV3Events: interfaces.Log[] = []; const destinationChainId = mockDestinationSpokePoolClient.chainId; // Update and set the config store client. hubPoolClient.configStoreClient._updateLiteChains([mockOriginSpokePoolClient.chainId]); @@ -969,119 +987,10 @@ describe("Dataworker: Load data used in all functions", async function () { .div(fixedPointAdjustment), }); }); - // This is almost the same as the second test in this block, with the exception of the change to the config store. - it("Fill with bytes32 relayer with lite chain deposit is invalid if msg.sender is not a bytes20 address", async function () { - const depositV3Events: Event[] = []; - const fillV3Events: Event[] = []; - const destinationChainId = mockDestinationSpokePoolClient.chainId; - // Update and set the config store client. - hubPoolClient.configStoreClient._updateLiteChains([mockOriginSpokePoolClient.chainId]); - mockOriginSpokePoolClient.configStoreClient = hubPoolClient.configStoreClient; - // Create three valid deposits - depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() })); - depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() })); - depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() })); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const deposits = mockOriginSpokePoolClient.getDeposits(); - - // Fill deposits from different relayers - const invalidRelayer = ethers.utils.hexlify(ethers.utils.randomBytes(32)); - fillV3Events.push(generateV3FillFromDeposit(deposits[0])); - fillV3Events.push(generateV3FillFromDeposit(deposits[1])); - fillV3Events.push(generateV3FillFromDeposit(deposits[2], {}, invalidRelayer)); - await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); - // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so - // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, - // set the msg.sender as an invalid address. - const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); - const spokeWrapper = new Contract( - mockDestinationSpokePoolClient.spokePool.address, - mockDestinationSpokePoolClient.spokePool.interface, - provider - ); - fillV3Events.forEach((event) => provider._setTransaction(event.transactionHash, { from: invalidRelayer })); - mockDestinationSpokePoolClient.spokePool = spokeWrapper; - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( - getDefaultBlockRange(5), - spokePoolClients - ); - - // We expect the last fillV3Event to be invalid. - const nValidFills = fillV3Events.length - 1; - expect(data1.bundleFillsV3[originChainId][erc20_1.address].fills.length).to.equal(nValidFills); - expect(data1.bundleFillsV3[originChainId][erc20_1.address].fills.map((e) => e.depositId)).to.deep.equal( - fillV3Events.slice(0, nValidFills).map((event) => event.args.depositId) - ); - expect(data1.bundleFillsV3[originChainId][erc20_1.address].fills.map((e) => e.lpFeePct)).to.deep.equal( - fillV3Events.slice(0, nValidFills).map(() => lpFeePct) - ); - expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); - }); }); }); describe("Miscellaneous functions", function () { - it("getUpcomingDepositAmount", async function () { - // Send two deposits on different chains - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - ZERO_ADDRESS, - amountToDeposit - ); - await depositV3( - spokePool_2, - originChainId, - depositor, - erc20_2.address, - amountToDeposit, - ZERO_ADDRESS, - amountToDeposit - ); - await updateAllClients(); - expect(await bundleDataClient.getUpcomingDepositAmount(originChainId, erc20_1.address, 0)).to.equal( - amountToDeposit - ); - expect(await bundleDataClient.getUpcomingDepositAmount(destinationChainId, erc20_2.address, 0)).to.equal( - amountToDeposit - ); - - // Removes deposits using block, token, and chain filters. - expect( - await bundleDataClient.getUpcomingDepositAmount( - originChainId, - erc20_1.address, - spokePoolClient_1.latestBlockSearched // block higher than the deposit - ) - ).to.equal(0); - expect( - await bundleDataClient.getUpcomingDepositAmount( - originChainId, - erc20_2.address, // diff token - 0 - ) - ).to.equal(0); - expect( - await bundleDataClient.getUpcomingDepositAmount( - destinationChainId, // diff chain - erc20_1.address, - 0 - ) - ).to.equal(0); - - // spoke pool client for chain not defined - expect( - await bundleDataClient.getUpcomingDepositAmount( - originChainId + destinationChainId + repaymentChainId + 1, // spoke pool client for chain is not defined in BundleDataClient - erc20_1.address, - 0 - ) - ).to.equal(0); - }); it("getApproximateRefundsForBlockRange", async function () { // Send two deposits on different chains // Fill both deposits and request repayment on same chain diff --git a/test/Dataworker.loadData.prefill.ts b/test/Dataworker.loadData.prefill.ts new file mode 100644 index 000000000..f7df08099 --- /dev/null +++ b/test/Dataworker.loadData.prefill.ts @@ -0,0 +1,775 @@ +import { ConfigStoreClient, HubPoolClient, SpokePoolClient } from "../src/clients"; +import { amountToDeposit, destinationChainId, originChainId, repaymentChainId } from "./constants"; +import { setupDataworker } from "./fixtures/Dataworker.Fixture"; +import { + Contract, + FakeContract, + SignerWithAddress, + V3FillFromDeposit, + createRandomBytes32, + depositV3, + ethers, + expect, + fillV3Relay, + getDefaultBlockRange, + randomAddress, + smock, +} from "./utils"; +import { Dataworker } from "../src/dataworker/Dataworker"; // Tested +import { + getCurrentTime, + toBNWei, + ZERO_ADDRESS, + bnZero, + TransactionResponse, + spreadEventWithBlockNumber, +} from "../src/utils"; +import { MockBundleDataClient, MockConfigStoreClient, MockHubPoolClient, MockSpokePoolClient } from "./mocks"; +import { interfaces, utils as sdkUtils, constants as sdkConstants, providers } from "@across-protocol/sdk"; +import { FillWithBlock } from "../src/interfaces"; + +let erc20_1: Contract, erc20_2: Contract; +let l1Token_1: Contract; +let relayer: SignerWithAddress, depositor: SignerWithAddress; + +let spokePoolClient_1: SpokePoolClient, spokePoolClient_2: SpokePoolClient; +let hubPoolClient: HubPoolClient, configStoreClient: ConfigStoreClient; +let dataworkerInstance: Dataworker; +let spokePoolClients: { [chainId: number]: SpokePoolClient }; + +let spy: sinon.SinonSpy; + +let updateAllClients: () => Promise; + +describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic", async function () { + beforeEach(async function () { + ({ + erc20_1, + erc20_2, + hubPoolClient, + configStoreClient, + spy, + l1Token_1, + relayer, + depositor, + dataworkerInstance, + spokePoolClient_1, + spokePoolClient_2, + spokePoolClients, + updateAllClients, + } = await setupDataworker(ethers, 25, 25, 0)); + await updateAllClients(); + (configStoreClient as unknown as MockConfigStoreClient).setConfigStoreVersion( + sdkConstants.PRE_FILL_MIN_CONFIG_STORE_VERSION + ); + }); + + describe("Tests with real events", function () { + describe("Pre-fills", function () { + it("Fetches and refunds fill if fill status is Filled", async function () { + // In this test, there is no fill in the SpokePoolClient's memory so the BundleDataClient + // should query its fill status on-chain and then fetch it to create a refund. + const deposit = await depositV3( + spokePoolClient_1.spokePool, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit + ); + await spokePoolClient_1.update(["V3FundsDeposited"]); + + // Mine fill, update spoke pool client to advance its latest block searched far enough such that the + // fillStatuses() call and findFillEvent() queries work but ignore the fill event to force the Bundle Client + // to query fresh for the fill status. + await fillV3Relay(spokePoolClient_2.spokePool, deposit, relayer, repaymentChainId); + await spokePoolClient_2.update([]); + expect(spokePoolClient_2.getFills().length).to.equal(0); + + // The bundle data client should create a refund for the pre fill + const bundleBlockRanges = getDefaultBlockRange(5); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills[0].depositId).to.equal(deposit.depositId); + }); + }); + }); + + describe("Tests with mocked events", function () { + let mockOriginSpokePoolClient: MockSpokePoolClient, mockDestinationSpokePoolClient: MockSpokePoolClient; + let mockHubPoolClient: MockHubPoolClient; + let mockDestinationSpokePool: FakeContract; + const lpFeePct = toBNWei("0.01"); + + beforeEach(async function () { + mockHubPoolClient = new MockHubPoolClient( + hubPoolClient.logger, + hubPoolClient.hubPool, + configStoreClient, + hubPoolClient.deploymentBlock, + hubPoolClient.chainId + ); + // Mock a realized lp fee pct for each deposit so we can check refund amounts and bundle lp fees. + mockHubPoolClient.setDefaultRealizedLpFeePct(lpFeePct); + mockOriginSpokePoolClient = new MockSpokePoolClient( + spokePoolClient_1.logger, + spokePoolClient_1.spokePool, + spokePoolClient_1.chainId, + spokePoolClient_1.deploymentBlock + ); + mockDestinationSpokePool = await smock.fake(spokePoolClient_2.spokePool.interface); + mockDestinationSpokePoolClient = new MockSpokePoolClient( + spokePoolClient_2.logger, + mockDestinationSpokePool as Contract, + spokePoolClient_2.chainId, + spokePoolClient_2.deploymentBlock + ); + spokePoolClients = { + ...spokePoolClients, + [originChainId]: mockOriginSpokePoolClient, + [destinationChainId]: mockDestinationSpokePoolClient, + }; + await mockHubPoolClient.update(); + await mockOriginSpokePoolClient.update(); + await mockDestinationSpokePoolClient.update(); + mockHubPoolClient.setTokenMapping(l1Token_1.address, originChainId, erc20_1.address); + mockHubPoolClient.setTokenMapping(l1Token_1.address, destinationChainId, erc20_2.address); + mockHubPoolClient.setTokenMapping(l1Token_1.address, repaymentChainId, l1Token_1.address); + const bundleDataClient = new MockBundleDataClient( + dataworkerInstance.logger, + { + ...dataworkerInstance.clients.bundleDataClient.clients, + hubPoolClient: mockHubPoolClient as unknown as HubPoolClient, + }, + dataworkerInstance.clients.bundleDataClient.spokePoolClients, + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers + ); + dataworkerInstance = new Dataworker( + dataworkerInstance.logger, + { ...dataworkerInstance.clients, bundleDataClient }, + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers, + dataworkerInstance.maxRefundCountOverride, + dataworkerInstance.maxL1TokenCountOverride, + dataworkerInstance.blockRangeEndBlockBuffer + ); + }); + + function generateV3Deposit(eventOverride?: Partial): interfaces.Log { + return mockOriginSpokePoolClient.depositV3({ + inputToken: erc20_1.address, + outputToken: eventOverride?.outputToken ?? erc20_2.address, + message: "0x", + quoteTimestamp: eventOverride?.quoteTimestamp ?? getCurrentTime() - 10, + fillDeadline: eventOverride?.fillDeadline ?? getCurrentTime() + 14400, + destinationChainId, + blockNumber: eventOverride?.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.DepositWithBlock); + } + + function generateV3FillFromDeposit( + deposit: interfaces.DepositWithBlock, + fillEventOverride?: Partial, + _relayer = relayer.address, + _repaymentChainId = repaymentChainId, + fillType = interfaces.FillType.FastFill + ): interfaces.Log { + const fillObject = V3FillFromDeposit(deposit, _relayer, _repaymentChainId); + return mockDestinationSpokePoolClient.fillV3Relay({ + ...fillObject, + relayExecutionInfo: { + updatedRecipient: fillObject.relayExecutionInfo.updatedRecipient, + updatedMessage: fillObject.relayExecutionInfo.updatedMessage, + updatedOutputAmount: fillObject.relayExecutionInfo.updatedOutputAmount, + fillType, + }, + blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client + // so that mocked client's latestBlockSearched gets set to the same value. + } as interfaces.FillWithBlock); + } + + function generateSlowFillRequestFromDeposit( + deposit: interfaces.DepositWithBlock, + fillEventOverride?: Partial + ): interfaces.Log { + const fillObject = V3FillFromDeposit(deposit, ZERO_ADDRESS); + const { relayer, repaymentChainId, relayExecutionInfo, ...relayData } = fillObject; + return mockDestinationSpokePoolClient.requestV3SlowFill({ + ...relayData, + blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client + // so that mocked client's latestBlockSearched gets set to the same value. + } as interfaces.SlowFillRequestWithBlock); + } + + describe("Pre-fills", function () { + it("Refunds fill if fill is in-memory and in older bundle", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit fill that we won't include in the bundle block range. + const fill = generateV3FillFromDeposit(deposits[0], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber, + }); + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = [fill.blockNumber + 1, fill.blockNumber + 2]; + + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(1); + + // The fill is a pre-fill because its earlier than the bundle block range. Because its corresponding + // deposit is in the block range, we should refund it. + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills[0].depositId).to.equal( + fill.args.depositId + ); + }); + + describe("Pre-fill has invalid repayment information", function () { + it("Refunds fill to msg.sender if fill is not in-memory and repayment info is invalid", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit fill that we won't include in the bundle block range. Make sure its relayer address is invalid + // so that the bundle data client is forced to overwrite the refund recipient. + const fill = generateV3FillFromDeposit(deposits[0], {}, createRandomBytes32()); + const fillWithBlock = { + ...spreadEventWithBlockNumber(fill), + destinationChainId, + } as FillWithBlock; + (dataworkerInstance.clients.bundleDataClient as MockBundleDataClient).setMatchingFillEvent( + deposits[0], + fillWithBlock + ); + + // Don't include the fill event in the update so that the bundle data client is forced to load the event + // fresh. + await mockDestinationSpokePoolClient.update([]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(0); + + // Mock FillStatus to be Filled so that the BundleDataClient searches for event. + mockDestinationSpokePoolClient.setRelayFillStatus(deposits[0], interfaces.FillStatus.Filled); + + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid + // but the msg.sender is valid. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const validRelayerAddress = randomAddress(); + provider._setTransaction(fill.transactionHash, { + from: validRelayerAddress, + } as unknown as TransactionResponse); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + // The fill is a pre-fill because its earlier than the bundle block range. Because its corresponding + // deposit is in the block range, we should refund it. + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills[0].depositId).to.equal( + fill.args.depositId + ); + // Check its refunded to correct address: + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills[0].relayer).to.equal( + validRelayerAddress + ); + }); + + it("Refunds fill to msg.sender if fill is in-memory and repayment info is invalid", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit fill with invalid relayer address so that the bundle data client is forced to + // overwrite the refund recipient. + const fill = generateV3FillFromDeposit(deposits[0], {}, createRandomBytes32()); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(1); + + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid + // but the msg.sender is valid. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const validRelayerAddress = randomAddress(); + provider._setTransaction(fill.transactionHash, { + from: validRelayerAddress, + } as unknown as TransactionResponse); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + // The fill is a pre-fill because its earlier than the bundle block range. Because its corresponding + // deposit is in the block range, we should refund it. + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills[0].depositId).to.equal( + fill.args.depositId + ); + // Check its refunded to correct address: + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills[0].relayer).to.equal( + validRelayerAddress + ); + }); + + it("Does not refund fill to msg.sender if fill is not in-memory and repayment address and msg.sender are invalid for repayment chain", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Send fill with invalid repayment address + const invalidRelayer = ethers.utils.randomBytes(32); + const invalidFillEvent = generateV3FillFromDeposit(deposits[0], {}, invalidRelayer); + const invalidFill = { + ...spreadEventWithBlockNumber(invalidFillEvent), + destinationChainId, + } as FillWithBlock; + await mockDestinationSpokePoolClient.update([]); + + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, + // set the msg.sender as an invalid address. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + // Mock FillStatus to be Filled for any invalid fills otherwise the BundleDataClient will + // query relayStatuses() on the spoke pool. + mockDestinationSpokePoolClient.setRelayFillStatus(deposits[0], interfaces.FillStatus.Filled); + // Also mock the matched fill event so that BundleDataClient doesn't query for it. + (dataworkerInstance.clients.bundleDataClient as MockBundleDataClient).setMatchingFillEvent( + deposits[0], + invalidFill + ); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); + }); + + it("Does not refund fill to msg.sender if fill is in-memory and repayment address and msg.sender are invalid for repayment chain", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Send fill with invalid repayment address + const invalidRelayer = ethers.utils.randomBytes(32); + const invalidFillEvent = generateV3FillFromDeposit(deposits[0], {}, invalidRelayer); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, + // set the msg.sender as an invalid address. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); + }); + + it("Does not refund lite chain fill to msg.sender if fill is not in-memory and repayment address and msg.sender are invalid for origin chain", async function () { + generateV3Deposit({ outputToken: randomAddress(), fromLiteChain: true }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Fill deposits from different relayers + const invalidRelayer = ethers.utils.randomBytes(32); + const invalidFillEvent = generateV3FillFromDeposit(deposits[0], {}, invalidRelayer); + const invalidFill = { + ...spreadEventWithBlockNumber(invalidFillEvent), + destinationChainId, + } as FillWithBlock; + await mockDestinationSpokePoolClient.update([]); + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, + // set the msg.sender as an invalid address. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + // Mock FillStatus to be Filled for any invalid fills otherwise the BundleDataClient will + // query relayStatuses() on the spoke pool. + mockDestinationSpokePoolClient.setRelayFillStatus(deposits[0], interfaces.FillStatus.Filled); + // Also mock the matched fill event so that BundleDataClient doesn't query for it. + (dataworkerInstance.clients.bundleDataClient as MockBundleDataClient).setMatchingFillEvent( + deposits[0], + invalidFill + ); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); + }); + + it("Does not refund lite chain fill to msg.sender if fill is in-memory and repayment address and msg.sender are invalid for origin chain", async function () { + generateV3Deposit({ outputToken: randomAddress(), fromLiteChain: true }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Fill deposits from different relayers + const invalidRelayer = ethers.utils.randomBytes(32); + const invalidFillEvent = generateV3FillFromDeposit(deposits[0], {}, invalidRelayer); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, + // set the msg.sender as an invalid address. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); + }); + }); + + it("Refunds deposit as a duplicate if fill is not in-memory and is a slow fill", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit fill that we won't include in the bundle block range. + const fill = generateV3FillFromDeposit(deposits[0], {}, undefined, undefined, interfaces.FillType.SlowFill); + const fillWithBlock = { + ...spreadEventWithBlockNumber(fill), + destinationChainId, + } as FillWithBlock; + (dataworkerInstance.clients.bundleDataClient as MockBundleDataClient).setMatchingFillEvent( + deposits[0], + fillWithBlock + ); + + // Don't include the fill event in the update so that the bundle data client is forced to load the event + // fresh. + await mockDestinationSpokePoolClient.update([]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(0); + + // Mock FillStatus to be Filled so that the BundleDataClient searches for event. + mockDestinationSpokePoolClient.setRelayFillStatus(deposits[0], interfaces.FillStatus.Filled); + + // The fill is a pre-fill because its earlier than the bundle block range. Because its corresponding + // deposit is in the block range, we should refund it. + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleFillsV3).to.deep.equal({}); + // Should refund the deposit: + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + }); + + it("Refunds pre-fills in-memory for duplicate deposits", async function () { + // In this test, we send multiple deposits per fill. We assume you cannot send more than one fill per deposit. + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(3); + + // Submit fill that we won't include in the bundle block range. + const fill = generateV3FillFromDeposit(deposits[0], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber, + }); + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = [fill.blockNumber + 1, fill.blockNumber + 2]; + + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(1); + + // This should return one refund for each pre-fill. + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(3); + // Duplicate deposits should not be refunded to depositor + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); + + it("Refunds duplicate deposits if pre-fill is not in memory", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(3); + + // Submit fill that we won't include in the bundle block range + const fill = generateV3FillFromDeposit(deposits[0]); + const fillWithBlock = { + ...spreadEventWithBlockNumber(fill), + destinationChainId, + } as FillWithBlock; + (dataworkerInstance.clients.bundleDataClient as MockBundleDataClient).setMatchingFillEvent( + deposits[0], + fillWithBlock + ); + + // Don't include the fill event in the update so that the bundle data client is forced to load the event + // fresh. + await mockDestinationSpokePoolClient.update([]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(0); + + // Mock FillStatus to be Filled so that the BundleDataClient searches for event. + mockDestinationSpokePoolClient.setRelayFillStatus(deposits[0], interfaces.FillStatus.Filled); + + // The fill is a pre-fill because its earlier than the bundle block range. Because its corresponding + // deposit is in the block range, we refund the pre-fill once per duplicate deposit. + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData( + getDefaultBlockRange(5), + spokePoolClients + ); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(3); + expect(data1.expiredDepositsToRefundV3).to.deep.equal({}); + }); + + it("Does not refund fill if fill is in-memory but in a future bundle", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit fill that we won't include in the bundle block range but is in a future bundle + const futureFill = generateV3FillFromDeposit(deposits[0], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber + 11, + }); + + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = [futureFill.blockNumber - 2, futureFill.blockNumber - 1]; + + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(1); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3).to.deep.equal({}); + }); + + it("Refunds deposit as duplicate if fill is in-memory but its a SlowFill", async function () { + generateV3Deposit({ outputToken: randomAddress() }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit fill in an older bundle but its a Slow Fill execution. + const slowFill = generateV3FillFromDeposit( + deposits[0], + { blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber }, + undefined, + undefined, + interfaces.FillType.SlowFill + ); + + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = [slowFill.blockNumber + 1, slowFill.blockNumber + 2]; + + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + expect(mockDestinationSpokePoolClient.getFills().length).to.equal(1); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleFillsV3).to.deep.equal({}); + // Should refund the deposit: + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + }); + }); + + describe("Pre-slow-fill-requests", function () { + it("Creates slow fill leaf if slow fill request is in-memory and in older bundle", async function () { + generateV3Deposit({ outputToken: erc20_2.address }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit request that is older than the bundle block range. + const request = generateSlowFillRequestFromDeposit(deposits[0], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber, + }); + + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = [request.blockNumber + 1, request.blockNumber + 2]; + + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill"]); + expect(mockDestinationSpokePoolClient.getSlowFillRequestsForOriginChain(originChainId).length).to.equal(1); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address].length).to.equal(1); + expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address][0].depositId).to.equal( + request.args.depositId + ); + }); + + it("Creates one leaf for duplicate deposits", async function () { + const deposit = generateV3Deposit({ outputToken: erc20_2.address }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(3); + + // Submit request for prior bundle. + const request = generateSlowFillRequestFromDeposit(deposits[0], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber, + }); + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = [request.blockNumber + 1, request.blockNumber + 2]; + + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill"]); + expect(mockDestinationSpokePoolClient.getSlowFillRequestsForOriginChain(originChainId).length).to.equal(1); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address].length).to.equal(1); + expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address][0].depositId).to.equal( + deposit.args.depositId + ); + + // The first deposit should be matched which is important because the quote timestamp of the deposit is not + // in the relay data hash so it can change between duplicate deposits. + expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address][0].transactionHash).to.equal( + deposit.transactionHash + ); + }); + + it("Does not create slow fill leaf if slow fill request is in-memory but an invalid request", async function () { + generateV3Deposit({ outputToken: randomAddress() }); // Token swap + generateV3Deposit({ outputToken: erc20_2.address, fillDeadline: 0 }); // Fill deadline expired + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Submit request that is in a previous bundle but is invalid. + generateSlowFillRequestFromDeposit(deposits[0], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber, + }); + const expiredDepositRequest = generateSlowFillRequestFromDeposit(deposits[1], { + blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber, + }); + + // Substitute bundle block ranges. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = [ + expiredDepositRequest.blockNumber + 1, + expiredDepositRequest.blockNumber + 2, + ]; + + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill"]); + expect(mockDestinationSpokePoolClient.getSlowFillRequestsForOriginChain(originChainId).length).to.equal(2); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleSlowFillsV3).to.deep.equal({}); + }); + + it("Creates slow fill leaf if fill status is RequestedSlowFill", async function () { + const deposit = generateV3Deposit({ outputToken: erc20_2.address }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + await mockOriginSpokePoolClient.depositV3(mockOriginSpokePoolClient.getDeposits()[0]); // Duplicate deposit + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDepositsForDestinationChainWithDuplicates(destinationChainId); + expect(deposits.length).to.equal(2); + + const depositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); + + // Can smock the fillStatuses() value here since the BundleDataClient doesn't need the fill event + // to create a slow fill leaf. + mockDestinationSpokePool.fillStatuses + .whenCalledWith(depositHash) + .returns(interfaces.FillStatus.RequestedSlowFill); + + // Check that bundle slow fills includes leaf. + const bundleBlockRanges = getDefaultBlockRange(5); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address].length).to.equal(1); + expect(data1.bundleSlowFillsV3[destinationChainId][erc20_2.address][0].depositId).to.equal( + deposit.args.depositId + ); + }); + + it("Does not create slow fill leaf if fill status is RequestedSlowFill but slow fill request is invalid", async function () { + generateV3Deposit({ outputToken: randomAddress() }); // Token swap + generateV3Deposit({ outputToken: erc20_2.address, fillDeadline: 0 }); // Fill deadline expired + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + const depositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); + + mockDestinationSpokePool.fillStatuses + .whenCalledWith(depositHash) + .returns(interfaces.FillStatus.RequestedSlowFill); + + // The deposit is for a token swap so the slow fill request is invalid. + const bundleBlockRanges = getDefaultBlockRange(5); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + expect(data1.bundleSlowFillsV3).to.deep.equal({}); + }); + }); + }); +}); diff --git a/test/Dataworker.loadData.slowFill.ts b/test/Dataworker.loadData.slowFill.ts index db3dc0b53..8382225e1 100644 --- a/test/Dataworker.loadData.slowFill.ts +++ b/test/Dataworker.loadData.slowFill.ts @@ -15,10 +15,8 @@ import { depositV3, ethers, expect, - fillV3Relay, getDefaultBlockRange, getDisabledBlockRanges, - mineRandomBlocks, randomAddress, requestSlowFill, sinon, @@ -27,13 +25,13 @@ import { } from "./utils"; import { Dataworker } from "../src/dataworker/Dataworker"; // Tested -import { getCurrentTime, Event, toBNWei, assert, ZERO_ADDRESS, bnZero } from "../src/utils"; +import { getCurrentTime, toBNWei, ZERO_ADDRESS, bnZero } from "../src/utils"; import { MockConfigStoreClient, MockHubPoolClient, MockSpokePoolClient } from "./mocks"; -import { interfaces, providers, utils as sdkUtils } from "@across-protocol/sdk"; +import { interfaces, utils as sdkUtils, constants as sdkConstants, providers } from "@across-protocol/sdk"; import { cloneDeep } from "lodash"; import { INFINITE_FILL_DEADLINE } from "../src/common"; -describe("BundleDataClient: Slow fill handling & validation", async function () { +describe("Dataworker: Load bundle data: Computing slow fills", async function () { let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract; let l1Token_1: Contract; let depositor: SignerWithAddress, relayer: SignerWithAddress; @@ -53,11 +51,12 @@ describe("BundleDataClient: Slow fill handling & validation", async function () let mockConfigStore: MockConfigStoreClient; const lpFeePct = toBNWei("0.01"); - function generateV3Deposit(eventOverride?: Partial): Event { + function generateV3Deposit(eventOverride?: Partial): interfaces.Log { return mockOriginSpokePoolClient.depositV3({ inputToken: erc20_1.address, + inputAmount: eventOverride?.inputAmount ?? undefined, outputToken: eventOverride?.outputToken ?? erc20_2.address, - message: "0x", + message: eventOverride?.message ?? "0x", quoteTimestamp: eventOverride?.quoteTimestamp ?? getCurrentTime() - 10, fillDeadline: eventOverride?.fillDeadline ?? getCurrentTime() + 14400, destinationChainId, @@ -72,14 +71,14 @@ describe("BundleDataClient: Slow fill handling & validation", async function () _relayer = relayer.address, _repaymentChainId = repaymentChainId, fillType = interfaces.FillType.FastFill - ): Event { + ): interfaces.Log { const fillObject = V3FillFromDeposit(deposit, _relayer, _repaymentChainId); return mockDestinationSpokePoolClient.fillV3Relay({ ...fillObject, relayExecutionInfo: { - updatedRecipient: fillObject.updatedRecipient, - updatedMessage: fillObject.updatedMessage, - updatedOutputAmount: fillObject.updatedOutputAmount, + updatedRecipient: fillObject.relayExecutionInfo.updatedRecipient, + updatedMessage: fillObject.relayExecutionInfo.updatedMessage, + updatedOutputAmount: fillObject.relayExecutionInfo.updatedOutputAmount, fillType, }, blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client @@ -90,14 +89,14 @@ describe("BundleDataClient: Slow fill handling & validation", async function () function generateSlowFillRequestFromDeposit( deposit: interfaces.DepositWithBlock, fillEventOverride?: Partial - ): Event { + ): interfaces.Log { const fillObject = V3FillFromDeposit(deposit, ZERO_ADDRESS); const { relayer, repaymentChainId, relayExecutionInfo, ...relayData } = fillObject; return mockDestinationSpokePoolClient.requestV3SlowFill({ ...relayData, blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client // so that mocked client's latestBlockSearched gets set to the same value. - } as interfaces.SlowFillRequest); + } as interfaces.SlowFillRequestWithBlock); } beforeEach(async function () { @@ -118,7 +117,6 @@ describe("BundleDataClient: Slow fill handling & validation", async function () updateAllClients, spy, } = await setupDataworker(ethers, 25, 25, 0)); - await updateAllClients(); mockHubPoolClient = new MockHubPoolClient( hubPoolClient.logger, @@ -181,194 +179,6 @@ describe("BundleDataClient: Slow fill handling & validation", async function () ); }); - it("Filters for fast fills replacing slow fills from older bundles", async function () { - // Generate a deposit that cannot be slow filled, to test that its ignored as a slow fill excess. - // Generate a second deposit that can be slow filled but will be slow filled in an older bundle - // Generate a third deposit that does get slow filled but the slow fill is not "seen" by the client. - const depositWithMissingSlowFillRequest = await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ); - await requestSlowFill(spokePool_2, relayer, depositWithMissingSlowFillRequest); - const missingSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); - await mineRandomBlocks(); - - const depositsWithSlowFillRequests = [ - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_1.address, - amountToDeposit - ), - await depositV3( - spokePool_1, - destinationChainId, - depositor, - erc20_1.address, - amountToDeposit, - erc20_2.address, - amountToDeposit - ), - ]; - - await spokePoolClient_1.update(); - const deposits = spokePoolClient_1.getDeposits(); - expect(deposits.length).to.equal(3); - const eligibleSlowFills = depositsWithSlowFillRequests.filter((x) => erc20_2.address === x.outputToken); - const ineligibleSlowFills = depositsWithSlowFillRequests.filter((x) => erc20_2.address !== x.outputToken); - - // Generate slow fill requests for the slow fill-eligible deposits - await requestSlowFill(spokePool_2, relayer, eligibleSlowFills[0]); - await requestSlowFill(spokePool_2, relayer, ineligibleSlowFills[0]); - const lastSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); - await mineRandomBlocks(); - - // Now, generate fast fills replacing slow fills for all deposits. - await fillV3Relay(spokePool_2, deposits[0], relayer, repaymentChainId); - await fillV3Relay(spokePool_2, deposits[1], relayer, repaymentChainId); - await fillV3Relay(spokePool_2, deposits[2], relayer, repaymentChainId); - - // Construct a spoke pool client with a small search range that would not include the first fill. - spokePoolClient_2.firstBlockToSearch = missingSlowFillRequestBlock + 1; - spokePoolClient_2.eventSearchConfig.fromBlock = spokePoolClient_2.firstBlockToSearch; - - // There should be one "missing" slow fill request. - await spokePoolClient_2.update(); - const fills = spokePoolClient_2.getFills(); - expect(fills.length).to.equal(3); - const slowFillRequests = spokePoolClient_2.getSlowFillRequestsForOriginChain(originChainId); - expect(slowFillRequests.length).to.equal(2); - assert( - fills.every((x) => x.relayExecutionInfo.fillType === interfaces.FillType.ReplacedSlowFill), - "All fills should be replaced slow fills" - ); - assert( - fills.every((x) => x.blockNumber > lastSlowFillRequestBlock), - "Fills should be later than slow fill request" - ); - - // Create a block range that would make the slow fill requests appear to be in an "older" bundle. - const destinationChainBlockRange = [lastSlowFillRequestBlock + 1, getDefaultBlockRange(5)[0][1]]; - // Substitute destination chain bundle block range. - const bundleBlockRanges = getDefaultBlockRange(5); - const destinationChainIndex = - dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); - bundleBlockRanges[destinationChainIndex] = destinationChainBlockRange; - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, { - ...spokePoolClients, - [originChainId]: spokePoolClient_1, - [destinationChainId]: spokePoolClient_2, - }); - - // All fills and deposits are valid - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(3); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); - - // There are two "unexecutable slow fills" because there are two deposits that have "equivalent" input - // and output tokens AND: - // - one slow fill request does not get seen by the spoke pool client - // - one slow fill request is in an older bundle - expect(data1.unexecutableSlowFills[destinationChainId][erc20_2.address].length).to.equal(2); - expect( - data1.unexecutableSlowFills[destinationChainId][erc20_2.address].map((x) => x.depositId).sort() - ).to.deep.equal([depositWithMissingSlowFillRequest.depositId, eligibleSlowFills[0].depositId].sort()); - }); - - it("Handles fast fills replacing invalid slow fill request from older bundles", async function () { - // Create a Lite chain to test that slow fill requests involving lite chains are ignored. - mockConfigStore.updateGlobalConfig( - GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES, - JSON.stringify([mockOriginSpokePoolClient.chainId]) - ); - await mockConfigStore.update(); - (spokePoolClient_1 as any).configStoreClient = mockConfigStore; - (spokePoolClient_2 as any).configStoreClient = mockConfigStore; - - // Generate a deposit that cannot be slow filled, to test that its ignored as a slow fill excess. - // - first deposit is FROM lite chain - // - second deposit is TO lite chain - const depositsWithSlowFillRequests = [ - 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 spokePoolClient_1.update(); - await spokePoolClient_2.update(); - const originChainDeposit = spokePoolClient_1.getDeposits()[0]; - const destinationChainDeposit = spokePoolClient_2.getDeposits()[0]; - - // Generate slow fill requests for the slow fill-eligible deposits - await requestSlowFill(spokePool_2, relayer, depositsWithSlowFillRequests[0]); - await requestSlowFill(spokePool_1, relayer, depositsWithSlowFillRequests[1]); - const lastDestinationChainSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); - const lastOriginChainSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); - - await mineRandomBlocks(); - - // Now, generate fast fills replacing slow fills for all deposits. - await fillV3Relay(spokePool_2, originChainDeposit, relayer, repaymentChainId); - await fillV3Relay(spokePool_1, destinationChainDeposit, relayer, repaymentChainId); - - await spokePoolClient_1.update(); - await spokePoolClient_2.update(); - assert( - spokePoolClient_2.getFills().every((x) => x.relayExecutionInfo.fillType === interfaces.FillType.ReplacedSlowFill), - "All fills should be replaced slow fills" - ); - assert( - spokePoolClient_1.getFills().every((x) => x.relayExecutionInfo.fillType === interfaces.FillType.ReplacedSlowFill), - "All fills should be replaced slow fills" - ); - - // Create a block range that would make the slow fill requests appear to be in an "older" bundle. - const destinationChainBlockRange = [lastDestinationChainSlowFillRequestBlock + 1, getDefaultBlockRange(5)[0][1]]; - const originChainBlockRange = [lastOriginChainSlowFillRequestBlock + 1, getDefaultBlockRange(5)[0][1]]; - // Substitute destination chain bundle block range. - const bundleBlockRanges = getDefaultBlockRange(5); - const destinationChainIndex = - dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); - bundleBlockRanges[destinationChainIndex] = destinationChainBlockRange; - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - bundleBlockRanges[originChainIndex] = originChainBlockRange; - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, { - ...spokePoolClients, - [originChainId]: spokePoolClient_1, - [destinationChainId]: spokePoolClient_2, - }); - - // All fills are valid. Note, origin chain deposit must take repayment on origin chain. - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); - expect(data1.bundleFillsV3[originChainId][erc20_1.address].fills.length).to.equal(1); - - // There are zero "unexecutable slow fills" because the slow fill requests in an older bundle are invalid - expect(data1.unexecutableSlowFills).to.deep.equal({}); - }); - it("Saves valid slow fill requests under destination chain and token", async function () { // Only one deposit is eligible to be slow filled because its input and output tokens are equivalent. generateV3Deposit({ outputToken: randomAddress() }); @@ -387,7 +197,7 @@ describe("BundleDataClient: Slow fill handling & validation", async function () ); }); - it("Ignores disabled chains", async function () { + it("Ignores disabled chains for slow fill requests", async function () { // Only one deposit is eligible to be slow filled because its input and output tokens are equivalent. generateV3Deposit({ outputToken: randomAddress() }); generateV3Deposit({ outputToken: erc20_2.address }); @@ -429,126 +239,19 @@ describe("BundleDataClient: Slow fill handling & validation", async function () expect(data1.unexecutableSlowFills).to.deep.equal({}); }); - it("Creates unexecutable slow fill even if fast fill repayment information is invalid", async function () { - generateV3Deposit({ outputToken: erc20_2.address }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const deposits = mockOriginSpokePoolClient.getDeposits(); - - // Fill deposit but don't mine requested slow fill event. This makes BundleDataClient think that deposit's slow - // fill request was sent in a prior bundle. This simulates the situation where the slow fill request - // was sent in a prior bundle to the fast fill. - - // Fill deposits with invalid repayment information - const invalidRelayer = ethers.utils.randomBytes(32); - const invalidFillEvent = generateV3FillFromDeposit( - deposits[0], - {}, - invalidRelayer, - undefined, - interfaces.FillType.ReplacedSlowFill - ); - await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); - // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so - // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, - // set the msg.sender as an invalid address. - const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); - const spokeWrapper = new Contract( - mockDestinationSpokePoolClient.spokePool.address, - mockDestinationSpokePoolClient.spokePool.interface, - provider - ); - provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); - mockDestinationSpokePoolClient.spokePool = spokeWrapper; - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); - - // The fill cannot be refunded but there is still an unexecutable slow fill leaf we need to refund. - expect(data1.bundleFillsV3).to.deep.equal({}); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.unexecutableSlowFills[destinationChainId][erc20_2.address].length).to.equal(1); - const logs = spy.getCalls().filter((x) => x.lastArg.message.includes("unrepayable")); - expect(logs.length).to.equal(1); - }); - - it("Does not create a slow fill leaf if fast fill follows slow fill request but repayment information is invalid", async function () { - generateV3Deposit({ outputToken: erc20_2.address }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const deposits = mockOriginSpokePoolClient.getDeposits(); - - generateSlowFillRequestFromDeposit(deposits[0]); - - // Fill deposits with invalid repayment information - const invalidRelayer = ethers.utils.randomBytes(32); - const invalidFillEvent = generateV3FillFromDeposit( - deposits[0], - {}, - invalidRelayer, - undefined, - interfaces.FillType.ReplacedSlowFill - ); - await mockDestinationSpokePoolClient.update(["FilledV3Relay", "RequestedV3SlowFill"]); - // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so - // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, - // set the msg.sender as an invalid address. - const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); - const spokeWrapper = new Contract( - mockDestinationSpokePoolClient.spokePool.address, - mockDestinationSpokePoolClient.spokePool.interface, - provider - ); - provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); - mockDestinationSpokePoolClient.spokePool = spokeWrapper; - - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); - - // Slow fill request, fast fill and deposit should all be in same bundle. Test that the bundle data client is - // aware of the fill, even if its invalid, and therefore doesn't create a slow fill leaf. - - // The fill cannot be refunded but there is still an unexecutable slow fill leaf we need to refund. - expect(data1.bundleFillsV3).to.deep.equal({}); - expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.bundleSlowFillsV3).to.deep.equal({}); - const logs = spy.getCalls().filter((x) => x.lastArg.message.includes("unrepayable")); - expect(logs.length).to.equal(1); - }); - - it("Replacing a slow fill request with a fast fill in same bundle doesn't create unexecutable slow fill", async function () { - generateV3Deposit({ outputToken: erc20_2.address }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const deposits = mockOriginSpokePoolClient.getDeposits(); - - generateSlowFillRequestFromDeposit(deposits[0]); - generateV3FillFromDeposit(deposits[0], undefined, undefined, undefined, interfaces.FillType.ReplacedSlowFill); - await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill", "FilledV3Relay"]); - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); - - expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); - expect(data1.bundleSlowFillsV3).to.deep.equal({}); - expect(data1.unexecutableSlowFills).to.deep.equal({}); - }); - - it("Ignores disabled chains", async function () { - generateV3Deposit({ outputToken: erc20_2.address }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - const deposits = mockOriginSpokePoolClient.getDeposits(); - - generateSlowFillRequestFromDeposit(deposits[0]); - generateV3FillFromDeposit(deposits[0], undefined, undefined, undefined, interfaces.FillType.ReplacedSlowFill); - await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill", "FilledV3Relay"]); - - const emptyData = await dataworkerInstance.clients.bundleDataClient.loadData( - getDisabledBlockRanges(), - spokePoolClients - ); - expect(emptyData.unexecutableSlowFills).to.deep.equal({}); - expect(emptyData.bundleFillsV3).to.deep.equal({}); - expect(emptyData.bundleSlowFillsV3).to.deep.equal({}); - }); - it("Handles slow fill requests out of block range", async function () { - generateV3Deposit({ outputToken: erc20_2.address }); - generateV3Deposit({ outputToken: erc20_2.address }); - generateV3Deposit({ outputToken: erc20_2.address }); + generateV3Deposit({ + outputToken: erc20_2.address, + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 1, + }); + generateV3Deposit({ + outputToken: erc20_2.address, + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 11, + }); + generateV3Deposit({ + outputToken: erc20_2.address, + blockNumber: mockOriginSpokePoolClient.eventManager.blockNumber + 21, + }); await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); const deposits = mockOriginSpokePoolClient.getDeposits(); @@ -563,13 +266,16 @@ describe("BundleDataClient: Slow fill handling & validation", async function () blockNumber: mockDestinationSpokePoolClient.eventManager.blockNumber + 21, }), ]; - // Create a block range that contains only the middle event. + // Create a block range that contains only the middle events. const destinationChainBlockRange = [events[1].blockNumber - 1, events[1].blockNumber + 1]; - // Substitute destination chain bundle block range. + const originChainBlockRange = [deposits[1].blockNumber - 1, deposits[1].blockNumber + 1]; + // Substitute bundle block ranges. const bundleBlockRanges = getDefaultBlockRange(5); const destinationChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); bundleBlockRanges[destinationChainIndex] = destinationChainBlockRange; + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + bundleBlockRanges[originChainIndex] = originChainBlockRange; await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill"]); expect(mockDestinationSpokePoolClient.getSlowFillRequestsForOriginChain(originChainId).length).to.equal(3); const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); @@ -579,7 +285,7 @@ describe("BundleDataClient: Slow fill handling & validation", async function () ); }); - it("Validates slow fill request against old deposit", async function () { + it("Validates slow fill request against old bundle deposit if deposit is not in-memory", async function () { // For this test, we need to actually send a deposit on the spoke pool // because queryHistoricalDepositForFill eth_call's the contract. @@ -621,6 +327,54 @@ describe("BundleDataClient: Slow fill handling & validation", async function () expect(data1.bundleDepositsV3).to.deep.equal({}); }); + it("Does not validate slow fill request against future bundle deposit if deposit is not in-memory", async function () { + // For this test, we need to actually send a deposit on the spoke pool + // because queryHistoricalDepositForFill eth_call's the contract. + + // Send a legacy deposit. + const depositObject = await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit, + { + fillDeadline: INFINITE_FILL_DEADLINE.toNumber(), + } + ); + + // Modify the block ranges such that the deposit is in a future bundle block range. This should render + // the slow fill request invalid. + const depositBlock = await spokePool_1.provider.getBlockNumber(); + const bundleBlockRanges = getDefaultBlockRange(5); + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + bundleBlockRanges[originChainIndex] = [depositBlock - 2, depositBlock - 1]; + + // Construct a spoke pool client with a small search range that would not include the deposit. + spokePoolClient_1.firstBlockToSearch = depositBlock + 1; + spokePoolClient_1.eventSearchConfig.fromBlock = spokePoolClient_1.firstBlockToSearch; + await spokePoolClient_1.update(); + const deposits = spokePoolClient_1.getDeposits(); + expect(deposits.length).to.equal(0); + + // Send a slow fill request now and force the bundle data client to query for the historical deposit. + await requestSlowFill(spokePool_2, relayer, depositObject); + await updateAllClients(); + const requests = spokePoolClient_2.getSlowFillRequestsForOriginChain(originChainId); + expect(requests.length).to.equal(1); + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, { + ...spokePoolClients, + [originChainId]: spokePoolClient_1, + [destinationChainId]: spokePoolClient_2, + }); + expect(data1.bundleSlowFillsV3).to.deep.equal({}); + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("invalid slow fill requests")).length).to.equal(1); + }); + it("Handles invalid slow fill requests with mismatching params from deposit", async function () { generateV3Deposit(); await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); @@ -761,7 +515,11 @@ describe("BundleDataClient: Slow fill handling & validation", 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, -4, "Located V3 deposit outside of SpokePoolClient's search range")).is.true; + expect( + spy + .getCalls() + .find((e) => e.lastArg.message.includes("Located V3 deposit outside of SpokePoolClient's search range")) + ).to.not.be.undefined; expect(data1.bundleSlowFillsV3).to.deep.equal({}); expect(data1.bundleDepositsV3).to.deep.equal({}); @@ -964,143 +722,68 @@ describe("BundleDataClient: Slow fill handling & validation", async function () expect(data1.bundleDepositsV3).to.deep.equal({}); }); - it("Adds prior bundle expired deposits that requested a slow fill in a prior bundle to unexecutable slow fills", async function () { - // Send deposit that expires in this bundle. - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - - // Let's make fill status for the relay hash always return RequestedSlowFill. - const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); - mockDestinationSpokePool.fillStatuses - .whenCalledWith(expiredDepositHash) - .returns(interfaces.FillStatus.RequestedSlowFill); - - // Now, load a bundle that doesn't include the deposit in its range. - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; - const bundleBlockRanges = getDefaultBlockRange(5); - bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); - - // Now, there is no bundle deposit but still an expired deposit to refund. - // There is also an unexecutable slow fill. - expect(data1.bundleDepositsV3).to.deep.equal({}); - expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.unexecutableSlowFills[destinationChainId][erc20_2.address].length).to.equal(1); - }); - - it("Does not add prior bundle expired lite chain deposits that requested a slow fill in a prior bundle to unexecutable slow fills", async function () { - mockConfigStore.updateGlobalConfig( - GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES, - JSON.stringify([mockOriginSpokePoolClient.chainId]) - ); - await mockConfigStore.update(); - (mockOriginSpokePoolClient as any).configStoreClient = mockConfigStore; - (mockDestinationSpokePool as any).configStoreClient = mockConfigStore; - const updateEventTimestamp = mockConfigStore.liteChainIndicesUpdates[0].timestamp; - - // Send lite chain deposit that expires in this bundle. - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - const expiredDeposit = generateV3Deposit({ - fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1, - quoteTimestamp: updateEventTimestamp, + it("Does not create slow fill for zero value deposit", async function () { + generateV3Deposit({ + inputAmount: bnZero, + message: "0x", + }); + generateV3Deposit({ + inputAmount: bnZero, + message: "0x", }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - - const deposit = mockOriginSpokePoolClient.getDeposits()[0]; - assert(deposit.fromLiteChain, "Deposit should be from lite chain"); - - // Let's make fill status for the relay hash always return RequestedSlowFill. - const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); - mockDestinationSpokePool.fillStatuses - .whenCalledWith(expiredDepositHash) - .returns(interfaces.FillStatus.RequestedSlowFill); - // Now, load a bundle that doesn't include the deposit in its range. - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; - const bundleBlockRanges = getDefaultBlockRange(5); - bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + generateSlowFillRequestFromDeposit(deposits[0]); + generateSlowFillRequestFromDeposit({ + ...deposits[1], + message: sdkConstants.ZERO_BYTES, + }); + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill"]); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); - // Now, there is no bundle deposit but still an expired deposit to refund. - // There is NOT an unexecutable slow fill. - expect(data1.bundleDepositsV3).to.deep.equal({}); - expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.unexecutableSlowFills).to.deep.equal({}); + expect(data1.bundleSlowFillsV3).to.deep.equal({}); }); - it("Does not add prior bundle expired deposits that did not request a slow fill in a prior bundle to unexecutable slow fills", async function () { - // Send deposit that expires in this bundle. - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients - ); - const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + it("Does not create a slow fill leaf if fast fill follows slow fill request but repayment information is invalid", async function () { + generateV3Deposit({ outputToken: erc20_2.address }); await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); - // Let's make fill status for the relay hash always return Unfilled. - const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); - mockDestinationSpokePool.fillStatuses.whenCalledWith(expiredDepositHash).returns(interfaces.FillStatus.Unfilled); - - // Now, load a bundle that doesn't include the deposit in its range. - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; - const bundleBlockRanges = getDefaultBlockRange(5); - bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); - - // Now, there is no bundle deposit but still an expired deposit to refund. - // There is also an unexecutable slow fill. - expect(data1.bundleDepositsV3).to.deep.equal({}); - expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.unexecutableSlowFills).to.deep.equal({}); - }); + generateSlowFillRequestFromDeposit(deposits[0]); - it("Does not add unexecutable slow fill for prior bundle expired deposits that requested a slow fill if slow fill request is in current bundle", async function () { - // Send deposit that expires in this bundle. - const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( - [originChainId, destinationChainId], - getDefaultBlockRange(5), - spokePoolClients + // Fill deposits with invalid repayment information + const invalidRelayer = ethers.utils.randomBytes(32); + const invalidFillEvent = generateV3FillFromDeposit( + deposits[0], + {}, + invalidRelayer, + undefined, + interfaces.FillType.ReplacedSlowFill ); - const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); - await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); - - // If the slow fill request took place in the current bundle, then it is not marked as unexecutable since - // it would not have produced a slow fill request. - const deposit = mockOriginSpokePoolClient.getDeposits()[0]; - generateSlowFillRequestFromDeposit(deposit); - await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill"]); + await mockDestinationSpokePoolClient.update(["FilledV3Relay", "RequestedV3SlowFill"]); + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, + // set the msg.sender as an invalid address. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; - // Let's make fill status for the relay hash always return RequestedSlowFill. - const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); - mockDestinationSpokePool.fillStatuses - .whenCalledWith(expiredDepositHash) - .returns(interfaces.FillStatus.RequestedSlowFill); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); - // Now, load a bundle that doesn't include the deposit in its range. - const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); - const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; - const bundleBlockRanges = getDefaultBlockRange(5); - bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; - const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + // Slow fill request, fast fill and deposit should all be in same bundle. Test that the bundle data client is + // aware of the fill, even if its invalid, and therefore doesn't create a slow fill leaf. - // Now, there is no bundle deposit but still an expired deposit to refund. - // There is also no unexecutable slow fill because the slow fill request was sent in this bundle. - expect(data1.bundleDepositsV3).to.deep.equal({}); - expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); - expect(data1.unexecutableSlowFills).to.deep.equal({}); + // The fill cannot be refunded but there is still an unexecutable slow fill leaf we need to refund. + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); expect(data1.bundleSlowFillsV3).to.deep.equal({}); + const logs = spy.getCalls().filter((x) => x.lastArg.message.includes("unrepayable")); + expect(logs.length).to.equal(1); }); }); diff --git a/test/Dataworker.loadData.unexecutableSlowFill.ts b/test/Dataworker.loadData.unexecutableSlowFill.ts new file mode 100644 index 000000000..3f9acead4 --- /dev/null +++ b/test/Dataworker.loadData.unexecutableSlowFill.ts @@ -0,0 +1,580 @@ +import { + BundleDataClient, + ConfigStoreClient, + GLOBAL_CONFIG_STORE_KEYS, + HubPoolClient, + SpokePoolClient, +} from "../src/clients"; +import { amountToDeposit, destinationChainId, originChainId, repaymentChainId } from "./constants"; +import { setupDataworker } from "./fixtures/Dataworker.Fixture"; +import { + Contract, + FakeContract, + SignerWithAddress, + V3FillFromDeposit, + depositV3, + ethers, + expect, + fillV3Relay, + getDefaultBlockRange, + getDisabledBlockRanges, + mineRandomBlocks, + requestSlowFill, + smock, +} from "./utils"; + +import { Dataworker } from "../src/dataworker/Dataworker"; // Tested +import { getCurrentTime, toBNWei, assert, ZERO_ADDRESS, bnZero } from "../src/utils"; +import { MockConfigStoreClient, MockHubPoolClient, MockSpokePoolClient } from "./mocks"; +import { interfaces, providers, utils as sdkUtils } from "@across-protocol/sdk"; + +describe("Dataworker: Load bundle data: Computing unexecutable slow fills", async function () { + let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract; + let l1Token_1: Contract; + let depositor: SignerWithAddress, relayer: SignerWithAddress; + + let spokePoolClient_1: SpokePoolClient, spokePoolClient_2: SpokePoolClient; + let hubPoolClient: HubPoolClient, configStoreClient: ConfigStoreClient; + let dataworkerInstance: Dataworker; + let spokePoolClients: { [chainId: number]: SpokePoolClient }; + + let spy: sinon.SinonSpy; + + let updateAllClients: () => Promise; + + let mockOriginSpokePoolClient: MockSpokePoolClient, mockDestinationSpokePoolClient: MockSpokePoolClient; + let mockHubPoolClient: MockHubPoolClient; + let mockDestinationSpokePool: FakeContract; + let mockConfigStore: MockConfigStoreClient; + const lpFeePct = toBNWei("0.01"); + + function generateV3Deposit(eventOverride?: Partial): interfaces.Log { + return mockOriginSpokePoolClient.depositV3({ + inputToken: erc20_1.address, + inputAmount: eventOverride?.inputAmount ?? undefined, + outputToken: eventOverride?.outputToken ?? erc20_2.address, + message: eventOverride?.message ?? "0x", + quoteTimestamp: eventOverride?.quoteTimestamp ?? getCurrentTime() - 10, + fillDeadline: eventOverride?.fillDeadline ?? getCurrentTime() + 14400, + destinationChainId, + blockNumber: eventOverride?.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.DepositWithBlock); + } + + function generateV3FillFromDeposit( + deposit: interfaces.DepositWithBlock, + fillEventOverride?: Partial, + _relayer = relayer.address, + _repaymentChainId = repaymentChainId, + fillType = interfaces.FillType.FastFill + ): interfaces.Log { + const fillObject = V3FillFromDeposit(deposit, _relayer, _repaymentChainId); + return mockDestinationSpokePoolClient.fillV3Relay({ + ...fillObject, + relayExecutionInfo: { + updatedRecipient: fillObject.relayExecutionInfo.updatedRecipient, + updatedMessage: fillObject.relayExecutionInfo.updatedMessage, + updatedOutputAmount: fillObject.relayExecutionInfo.updatedOutputAmount, + fillType, + }, + blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client + // so that mocked client's latestBlockSearched gets set to the same value. + } as interfaces.FillWithBlock); + } + + function generateSlowFillRequestFromDeposit( + deposit: interfaces.DepositWithBlock, + fillEventOverride?: Partial + ): interfaces.Log { + const fillObject = V3FillFromDeposit(deposit, ZERO_ADDRESS); + const { relayer, repaymentChainId, relayExecutionInfo, ...relayData } = fillObject; + return mockDestinationSpokePoolClient.requestV3SlowFill({ + ...relayData, + blockNumber: fillEventOverride?.blockNumber ?? spokePoolClient_2.latestBlockSearched, // @dev use latest block searched from non-mocked client + // so that mocked client's latestBlockSearched gets set to the same value. + } as interfaces.SlowFillRequestWithBlock); + } + + beforeEach(async function () { + ({ + spokePool_1, + erc20_1, + spokePool_2, + erc20_2, + configStoreClient, + spy, + hubPoolClient, + l1Token_1, + depositor, + relayer, + dataworkerInstance, + spokePoolClient_1, + spokePoolClient_2, + spokePoolClients, + updateAllClients, + } = await setupDataworker(ethers, 25, 25, 0)); + await updateAllClients(); + mockHubPoolClient = new MockHubPoolClient( + hubPoolClient.logger, + hubPoolClient.hubPool, + configStoreClient, + hubPoolClient.deploymentBlock, + hubPoolClient.chainId + ); + mockConfigStore = new MockConfigStoreClient( + configStoreClient.logger, + configStoreClient.configStore, + undefined, + undefined, + undefined, + undefined, + true + ); + // Mock a realized lp fee pct for each deposit so we can check refund amounts and bundle lp fees. + mockHubPoolClient.setDefaultRealizedLpFeePct(lpFeePct); + mockOriginSpokePoolClient = new MockSpokePoolClient( + spokePoolClient_1.logger, + spokePoolClient_1.spokePool, + spokePoolClient_1.chainId, + spokePoolClient_1.deploymentBlock + ); + mockDestinationSpokePool = await smock.fake(spokePoolClient_2.spokePool.interface); + mockDestinationSpokePoolClient = new MockSpokePoolClient( + spokePoolClient_2.logger, + mockDestinationSpokePool as Contract, + spokePoolClient_2.chainId, + spokePoolClient_2.deploymentBlock + ); + spokePoolClients = { + ...spokePoolClients, + [originChainId]: mockOriginSpokePoolClient, + [destinationChainId]: mockDestinationSpokePoolClient, + }; + await mockHubPoolClient.update(); + await mockOriginSpokePoolClient.update(); + await mockDestinationSpokePoolClient.update(); + mockHubPoolClient.setTokenMapping(l1Token_1.address, originChainId, erc20_1.address); + mockHubPoolClient.setTokenMapping(l1Token_1.address, destinationChainId, erc20_2.address); + mockHubPoolClient.setTokenMapping(l1Token_1.address, repaymentChainId, l1Token_1.address); + const bundleDataClient = new BundleDataClient( + dataworkerInstance.logger, + { + ...dataworkerInstance.clients.bundleDataClient.clients, + hubPoolClient: mockHubPoolClient as unknown as HubPoolClient, + }, + dataworkerInstance.clients.bundleDataClient.spokePoolClients, + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers + ); + dataworkerInstance = new Dataworker( + dataworkerInstance.logger, + { ...dataworkerInstance.clients, bundleDataClient }, + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers, + dataworkerInstance.maxRefundCountOverride, + dataworkerInstance.maxL1TokenCountOverride, + dataworkerInstance.blockRangeEndBlockBuffer + ); + }); + + it("Filters for fast fills replacing slow fills from older bundles", async function () { + // Generate a deposit that cannot be slow filled, to test that its ignored as a slow fill excess. + // Generate a second deposit that can be slow filled but will be slow filled in an older bundle + // Generate a third deposit that does get slow filled but the slow fill is not "seen" by the client. + const depositWithMissingSlowFillRequest = await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit + ); + await requestSlowFill(spokePool_2, relayer, depositWithMissingSlowFillRequest); + const missingSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); + await mineRandomBlocks(); + + const depositsWithSlowFillRequests = [ + await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_1.address, + amountToDeposit + ), + await depositV3( + spokePool_1, + destinationChainId, + depositor, + erc20_1.address, + amountToDeposit, + erc20_2.address, + amountToDeposit + ), + ]; + + await spokePoolClient_1.update(); + const deposits = spokePoolClient_1.getDeposits(); + expect(deposits.length).to.equal(3); + const eligibleSlowFills = depositsWithSlowFillRequests.filter((x) => erc20_2.address === x.outputToken); + const ineligibleSlowFills = depositsWithSlowFillRequests.filter((x) => erc20_2.address !== x.outputToken); + + // Generate slow fill requests for the slow fill-eligible deposits + await requestSlowFill(spokePool_2, relayer, eligibleSlowFills[0]); + await requestSlowFill(spokePool_2, relayer, ineligibleSlowFills[0]); + const lastSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); + await mineRandomBlocks(); + + // Now, generate fast fills replacing slow fills for all deposits. + await fillV3Relay(spokePool_2, deposits[0], relayer, repaymentChainId); + await fillV3Relay(spokePool_2, deposits[1], relayer, repaymentChainId); + await fillV3Relay(spokePool_2, deposits[2], relayer, repaymentChainId); + + // Construct a spoke pool client with a small search range that would not include the first fill. + spokePoolClient_2.firstBlockToSearch = missingSlowFillRequestBlock + 1; + spokePoolClient_2.eventSearchConfig.fromBlock = spokePoolClient_2.firstBlockToSearch; + + // There should be one "missing" slow fill request. + await spokePoolClient_2.update(); + const fills = spokePoolClient_2.getFills(); + expect(fills.length).to.equal(3); + const slowFillRequests = spokePoolClient_2.getSlowFillRequestsForOriginChain(originChainId); + expect(slowFillRequests.length).to.equal(2); + assert( + fills.every((x) => x.relayExecutionInfo.fillType === interfaces.FillType.ReplacedSlowFill), + "All fills should be replaced slow fills" + ); + assert( + fills.every((x) => x.blockNumber > lastSlowFillRequestBlock), + "Fills should be later than slow fill request" + ); + + // Create a block range that would make the slow fill requests appear to be in an "older" bundle. + const destinationChainBlockRange = [lastSlowFillRequestBlock + 1, getDefaultBlockRange(5)[0][1]]; + // Substitute destination chain bundle block range. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = destinationChainBlockRange; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, { + ...spokePoolClients, + [originChainId]: spokePoolClient_1, + [destinationChainId]: spokePoolClient_2, + }); + + // All fills and deposits are valid + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(3); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(3); + + // There are two "unexecutable slow fills" because there are two deposits that have "equivalent" input + // and output tokens AND: + // - one slow fill request does not get seen by the spoke pool client + // - one slow fill request is in an older bundle + expect(data1.unexecutableSlowFills[destinationChainId][erc20_2.address].length).to.equal(2); + expect( + data1.unexecutableSlowFills[destinationChainId][erc20_2.address].map((x) => x.depositId).sort() + ).to.deep.equal([depositWithMissingSlowFillRequest.depositId, eligibleSlowFills[0].depositId].sort()); + }); + + it("Creates unexecutable slow fill even if fast fill repayment information is invalid", async function () { + generateV3Deposit({ outputToken: erc20_2.address }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + // Fill deposit but don't mine requested slow fill event. This makes BundleDataClient think that deposit's slow + // fill request was sent in a prior bundle. This simulates the situation where the slow fill request + // was sent in a prior bundle to the fast fill. + + // Fill deposits with invalid repayment information + const invalidRelayer = ethers.utils.randomBytes(32); + const invalidFillEvent = generateV3FillFromDeposit( + deposits[0], + {}, + invalidRelayer, + undefined, + interfaces.FillType.ReplacedSlowFill + ); + await mockDestinationSpokePoolClient.update(["FilledV3Relay"]); + // Replace the dataworker providers to use mock providers. We need to explicitly do this since we do not actually perform a contract call, so + // we must inject a transaction response into the provider to simulate the case when the relayer repayment address is invalid. In this case, + // set the msg.sender as an invalid address. + const provider = new providers.mocks.MockedProvider(bnZero, bnZero, destinationChainId); + const spokeWrapper = new Contract( + mockDestinationSpokePoolClient.spokePool.address, + mockDestinationSpokePoolClient.spokePool.interface, + provider + ); + provider._setTransaction(invalidFillEvent.transactionHash, { from: invalidRelayer }); + mockDestinationSpokePoolClient.spokePool = spokeWrapper; + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); + + // The fill cannot be refunded but there is still an unexecutable slow fill leaf we need to refund. + expect(data1.bundleFillsV3).to.deep.equal({}); + expect(data1.bundleDepositsV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.unexecutableSlowFills[destinationChainId][erc20_2.address].length).to.equal(1); + const logs = spy.getCalls().filter((x) => x.lastArg.message.includes("unrepayable")); + expect(logs.length).to.equal(1); + }); + + it("Handles fast fills replacing invalid slow fill request from older bundles", async function () { + // Create a Lite chain to test that slow fill requests involving lite chains are ignored. + mockConfigStore.updateGlobalConfig( + GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES, + JSON.stringify([mockOriginSpokePoolClient.chainId]) + ); + await mockConfigStore.update(); + (spokePoolClient_1 as any).configStoreClient = mockConfigStore; + (spokePoolClient_2 as any).configStoreClient = mockConfigStore; + + // Generate a deposit that cannot be slow filled, to test that its ignored as a slow fill excess. + // - first deposit is FROM lite chain + // - second deposit is TO lite chain + const depositsWithSlowFillRequests = [ + 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 spokePoolClient_1.update(); + await spokePoolClient_2.update(); + const originChainDeposit = spokePoolClient_1.getDeposits()[0]; + const destinationChainDeposit = spokePoolClient_2.getDeposits()[0]; + + // Generate slow fill requests for the slow fill-eligible deposits + await requestSlowFill(spokePool_2, relayer, depositsWithSlowFillRequests[0]); + await requestSlowFill(spokePool_1, relayer, depositsWithSlowFillRequests[1]); + const lastDestinationChainSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); + const lastOriginChainSlowFillRequestBlock = await spokePool_2.provider.getBlockNumber(); + + await mineRandomBlocks(); + + // Now, generate fast fills replacing slow fills for all deposits. + await fillV3Relay(spokePool_2, originChainDeposit, relayer, repaymentChainId); + await fillV3Relay(spokePool_1, destinationChainDeposit, relayer, repaymentChainId); + + await spokePoolClient_1.update(); + await spokePoolClient_2.update(); + assert( + spokePoolClient_2.getFills().every((x) => x.relayExecutionInfo.fillType === interfaces.FillType.ReplacedSlowFill), + "All fills should be replaced slow fills" + ); + assert( + spokePoolClient_1.getFills().every((x) => x.relayExecutionInfo.fillType === interfaces.FillType.ReplacedSlowFill), + "All fills should be replaced slow fills" + ); + + // Create a block range that would make the slow fill requests appear to be in an "older" bundle. + const destinationChainBlockRange = [lastDestinationChainSlowFillRequestBlock + 1, getDefaultBlockRange(5)[0][1]]; + const originChainBlockRange = [lastOriginChainSlowFillRequestBlock + 1, getDefaultBlockRange(5)[0][1]]; + // Substitute destination chain bundle block range. + const bundleBlockRanges = getDefaultBlockRange(5); + const destinationChainIndex = + dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(destinationChainId); + bundleBlockRanges[destinationChainIndex] = destinationChainBlockRange; + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + bundleBlockRanges[originChainIndex] = originChainBlockRange; + + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, { + ...spokePoolClients, + [originChainId]: spokePoolClient_1, + [destinationChainId]: spokePoolClient_2, + }); + + // All fills are valid. Note, origin chain deposit must take repayment on origin chain. + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + expect(data1.bundleFillsV3[originChainId][erc20_1.address].fills.length).to.equal(1); + + // There are zero "unexecutable slow fills" because the slow fill requests in an older bundle are invalid + expect(data1.unexecutableSlowFills).to.deep.equal({}); + }); + + it("Replacing a slow fill request with a fast fill in same bundle doesn't create unexecutable slow fill", async function () { + generateV3Deposit({ outputToken: erc20_2.address }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + generateSlowFillRequestFromDeposit(deposits[0]); + generateV3FillFromDeposit(deposits[0], undefined, undefined, undefined, interfaces.FillType.ReplacedSlowFill); + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill", "FilledV3Relay"]); + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(getDefaultBlockRange(5), spokePoolClients); + + expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.length).to.equal(1); + expect(data1.bundleSlowFillsV3).to.deep.equal({}); + expect(data1.unexecutableSlowFills).to.deep.equal({}); + }); + + it("Ignores disabled chains for unexecutable slow fills", async function () { + generateV3Deposit({ outputToken: erc20_2.address }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + const deposits = mockOriginSpokePoolClient.getDeposits(); + + generateSlowFillRequestFromDeposit(deposits[0]); + generateV3FillFromDeposit(deposits[0], undefined, undefined, undefined, interfaces.FillType.ReplacedSlowFill); + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill", "FilledV3Relay"]); + + const emptyData = await dataworkerInstance.clients.bundleDataClient.loadData( + getDisabledBlockRanges(), + spokePoolClients + ); + expect(emptyData.unexecutableSlowFills).to.deep.equal({}); + expect(emptyData.bundleFillsV3).to.deep.equal({}); + expect(emptyData.bundleSlowFillsV3).to.deep.equal({}); + }); + + it("Adds prior bundle expired deposits that requested a slow fill in a prior bundle to unexecutable slow fills", async function () { + // Send deposit that expires in this bundle. + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + // Let's make fill status for the relay hash always return RequestedSlowFill. + const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); + mockDestinationSpokePool.fillStatuses + .whenCalledWith(expiredDepositHash) + .returns(interfaces.FillStatus.RequestedSlowFill); + + // Now, load a bundle that doesn't include the deposit in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // Now, there is no bundle deposit but still an expired deposit to refund. + // There is also an unexecutable slow fill. + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.unexecutableSlowFills[destinationChainId][erc20_2.address].length).to.equal(1); + }); + + it("Does not add prior bundle expired lite chain deposits that requested a slow fill in a prior bundle to unexecutable slow fills", async function () { + mockConfigStore.updateGlobalConfig( + GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES, + JSON.stringify([mockOriginSpokePoolClient.chainId]) + ); + await mockConfigStore.update(); + (mockOriginSpokePoolClient as any).configStoreClient = mockConfigStore; + (mockDestinationSpokePool as any).configStoreClient = mockConfigStore; + const updateEventTimestamp = mockConfigStore.liteChainIndicesUpdates[0].timestamp; + + // Send lite chain deposit that expires in this bundle. + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + const expiredDeposit = generateV3Deposit({ + fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1, + quoteTimestamp: updateEventTimestamp, + }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + const deposit = mockOriginSpokePoolClient.getDeposits()[0]; + assert(deposit.fromLiteChain, "Deposit should be from lite chain"); + + // Let's make fill status for the relay hash always return RequestedSlowFill. + const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); + mockDestinationSpokePool.fillStatuses + .whenCalledWith(expiredDepositHash) + .returns(interfaces.FillStatus.RequestedSlowFill); + + // Now, load a bundle that doesn't include the deposit in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // Now, there is no bundle deposit but still an expired deposit to refund. + // There is NOT an unexecutable slow fill. + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.unexecutableSlowFills).to.deep.equal({}); + }); + + it("Does not add prior bundle expired deposits that did not request a slow fill in a prior bundle to unexecutable slow fills", async function () { + // Send deposit that expires in this bundle. + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + // Let's make fill status for the relay hash always return Unfilled. + const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); + mockDestinationSpokePool.fillStatuses.whenCalledWith(expiredDepositHash).returns(interfaces.FillStatus.Unfilled); + + // Now, load a bundle that doesn't include the deposit in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // Now, there is no bundle deposit but still an expired deposit to refund. + // There is also an unexecutable slow fill. + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.unexecutableSlowFills).to.deep.equal({}); + }); + + it("Does not add unexecutable slow fill for prior bundle expired deposits that requested a slow fill if slow fill request is in current bundle", async function () { + // Send deposit that expires in this bundle. + const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps( + [originChainId, destinationChainId], + getDefaultBlockRange(5), + spokePoolClients + ); + const expiredDeposit = generateV3Deposit({ fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1 }); + await mockOriginSpokePoolClient.update(["V3FundsDeposited"]); + + // If the slow fill request took place in the current bundle, then it is not marked as unexecutable since + // it would not have produced a slow fill request. + const deposit = mockOriginSpokePoolClient.getDeposits()[0]; + generateSlowFillRequestFromDeposit(deposit); + await mockDestinationSpokePoolClient.update(["RequestedV3SlowFill"]); + + // Let's make fill status for the relay hash always return RequestedSlowFill. + const expiredDepositHash = sdkUtils.getRelayHashFromEvent(mockOriginSpokePoolClient.getDeposits()[0]); + mockDestinationSpokePool.fillStatuses + .whenCalledWith(expiredDepositHash) + .returns(interfaces.FillStatus.RequestedSlowFill); + + // Now, load a bundle that doesn't include the deposit in its range. + const originChainIndex = dataworkerInstance.chainIdListForBundleEvaluationBlockNumbers.indexOf(originChainId); + const oldOriginChainToBlock = getDefaultBlockRange(5)[0][1]; + const bundleBlockRanges = getDefaultBlockRange(5); + bundleBlockRanges[originChainIndex] = [expiredDeposit.blockNumber + 1, oldOriginChainToBlock]; + const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(bundleBlockRanges, spokePoolClients); + + // Now, there is no bundle deposit but still an expired deposit to refund. + // There is also no unexecutable slow fill because the slow fill request was sent in this bundle. + expect(data1.bundleDepositsV3).to.deep.equal({}); + expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1); + expect(data1.unexecutableSlowFills).to.deep.equal({}); + expect(data1.bundleSlowFillsV3).to.deep.equal({}); + }); +}); diff --git a/test/mocks/MockBundleDataClient.ts b/test/mocks/MockBundleDataClient.ts index 97acac272..0b29bbb83 100644 --- a/test/mocks/MockBundleDataClient.ts +++ b/test/mocks/MockBundleDataClient.ts @@ -1,9 +1,11 @@ -import { BundleDataClient } from "../../src/clients"; +import { BundleDataClient, SpokePoolClient } from "../../src/clients"; import { CombinedRefunds } from "../../src/dataworker/DataworkerUtils"; +import { DepositWithBlock, FillWithBlock } from "../../src/interfaces"; export class MockBundleDataClient extends BundleDataClient { private pendingBundleRefunds: CombinedRefunds = {}; private nextBundleRefunds: CombinedRefunds = {}; + private matchingFillEvents: Record = {}; async getPendingRefundsFromValidBundles(): Promise { return [this.pendingBundleRefunds]; @@ -28,4 +30,19 @@ export class MockBundleDataClient extends BundleDataClient { getPersistedNextBundleRefunds(): Promise { return Promise.resolve(undefined); } + + setMatchingFillEvent(deposit: DepositWithBlock, fill: FillWithBlock): void { + const relayDataHash = this.getRelayHashFromEvent(deposit); + this.matchingFillEvents[relayDataHash] = fill; + } + + findMatchingFillEvent( + deposit: DepositWithBlock, + spokePoolClient: SpokePoolClient + ): Promise { + const relayDataHash = this.getRelayHashFromEvent(deposit); + return this.matchingFillEvents[relayDataHash] + ? Promise.resolve(this.matchingFillEvents[relayDataHash]) + : super.findMatchingFillEvent(deposit, spokePoolClient); + } } diff --git a/test/mocks/MockSpokePoolClient.ts b/test/mocks/MockSpokePoolClient.ts index 260625eda..017405fd1 100644 --- a/test/mocks/MockSpokePoolClient.ts +++ b/test/mocks/MockSpokePoolClient.ts @@ -1,7 +1,9 @@ -import { clients } from "@across-protocol/sdk"; +import { clients, interfaces } from "@across-protocol/sdk"; +import { Deposit } from "../../src/interfaces"; export class MockSpokePoolClient extends clients.mocks.MockSpokePoolClient { public maxFillDeadlineOverride?: number; public oldestBlockTimestampOverride?: number; + private relayFillStatuses: Record = {}; public setMaxFillDeadlineOverride(maxFillDeadlineOverride?: number): void { this.maxFillDeadlineOverride = maxFillDeadlineOverride; @@ -18,4 +20,19 @@ export class MockSpokePoolClient extends clients.mocks.MockSpokePoolClient { public getOldestTime(): number { return this.oldestBlockTimestampOverride ?? super.getOldestTime(); } + + public setRelayFillStatus(deposit: Deposit, fillStatus: interfaces.FillStatus): void { + const relayDataHash = deposit.depositId.toString(); + this.relayFillStatuses[relayDataHash] = fillStatus; + } + public relayFillStatus( + relayData: interfaces.RelayData, + blockTag?: number | "latest", + destinationChainId?: number + ): Promise { + const relayDataHash = relayData.depositId.toString(); + return this.relayFillStatuses[relayDataHash] + ? Promise.resolve(this.relayFillStatuses[relayDataHash]) + : super.relayFillStatus(relayData, blockTag, destinationChainId); + } } diff --git a/yarn.lock b/yarn.lock index 13f496c21..8a3393a51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,10 +53,10 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.4.20": - version "3.4.20" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.20.tgz#0fa2b223e264fc8ff9ea47ab9e65ad9cd176848d" - integrity sha512-HUgWYfbH0haa2r9nl892IE6U+z+QtowcYwHzcimiBlux2+tn6Ztq80CEOpp1367GB4gBiWz1stL5TQfRi+vxtg== +"@across-protocol/sdk@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-4.0.0.tgz#83242907471577a8fe670dcee3d633b2338d15b9" + integrity sha512-qDkOYHlQy8KhT5WStRXJybp84pFuxQc4MgMwZzOOBTvvrGWtklSpWY9NT8Uu3Rd9v9Yn+oa/MRURdFmeMsJnrg== dependencies: "@across-protocol/across-token" "^1.0.0" "@across-protocol/constants" "^3.1.30"