diff --git a/package.json b/package.json index b06afdd09..53ad79196 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.17", + "@across-protocol/sdk": "^3.4.18", "@arbitrum/sdk": "^4.0.2", "@consensys/linea-sdk": "^0.2.1", "@defi-wonderland/smock": "^2.3.5", diff --git a/test/Dataworker.loadData.fill.ts b/test/Dataworker.loadData.fill.ts index 8e9027d03..a9135c3ee 100644 --- a/test/Dataworker.loadData.fill.ts +++ b/test/Dataworker.loadData.fill.ts @@ -903,7 +903,7 @@ describe("Dataworker: Load data used in all functions", async function () { expect(data1.bundleFillsV3[repaymentChainId][l1Token_1.address].fills.map((e) => e.lpFeePct)).to.deep.equal( fillV3Events.slice(0, nValidFills).map(() => lpFeePct) ); - expect(spyLogIncludes(spy, -2, "invalid V3 fills in range")).to.be.true; + 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 () { @@ -1014,7 +1014,7 @@ describe("Dataworker: Load data used in all functions", async function () { expect(data1.bundleFillsV3[originChainId][erc20_1.address].fills.map((e) => e.lpFeePct)).to.deep.equal( fillV3Events.slice(0, nValidFills).map(() => lpFeePct) ); - expect(spyLogIncludes(spy, -2, "invalid V3 fills in range")).to.be.true; + expect(spy.getCalls().filter((e) => e.lastArg.message.includes("unrepayable")).length).to.equal(1); }); }); }); diff --git a/test/Dataworker.loadData.slowFill.ts b/test/Dataworker.loadData.slowFill.ts index 2fa7c5755..db3dc0b53 100644 --- a/test/Dataworker.loadData.slowFill.ts +++ b/test/Dataworker.loadData.slowFill.ts @@ -27,9 +27,9 @@ import { } from "./utils"; import { Dataworker } from "../src/dataworker/Dataworker"; // Tested -import { getCurrentTime, Event, toBNWei, assert, ZERO_ADDRESS } from "../src/utils"; +import { getCurrentTime, Event, toBNWei, assert, ZERO_ADDRESS, bnZero } from "../src/utils"; import { MockConfigStoreClient, MockHubPoolClient, MockSpokePoolClient } from "./mocks"; -import { interfaces, utils as sdkUtils } from "@across-protocol/sdk"; +import { interfaces, providers, utils as sdkUtils } from "@across-protocol/sdk"; import { cloneDeep } from "lodash"; import { INFINITE_FILL_DEADLINE } from "../src/common"; @@ -429,6 +429,89 @@ 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"]); diff --git a/yarn.lock b/yarn.lock index 6f94648b1..e1af6b42e 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.17": - version "3.4.17" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.17.tgz#00e1291437dc0d22201765f32e31b1d5ab11bccf" - integrity sha512-aWg4JG3z8srN1NkxBF/T9tdr0KgXecs/5zpB1DGujC5Fpo+Blpv0jguhK/taSdmx+hf1Ez0HY8Blxz6YBR66fw== +"@across-protocol/sdk@^3.4.18": + version "3.4.18" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.18.tgz#a01efad8c24b36a4a725aace4edb307b8fab8083" + integrity sha512-KQG4BEWIxKYUF12B529E7FAvwVZg84T1c+4RJFClRlxGpV7Kk8jydFbFqWWee4bG1eOaIgFbX26Nq9DQOFK7lA== dependencies: "@across-protocol/across-token" "^1.0.0" "@across-protocol/constants" "^3.1.30"