@across-protocol/sdk 4.0.0-beta.2 → 4.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/cjs/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  2. package/dist/cjs/clients/BundleDataClient/BundleDataClient.js +320 -179
  3. package/dist/cjs/clients/BundleDataClient/BundleDataClient.js.map +1 -1
  4. package/dist/cjs/clients/BundleDataClient/utils/DataworkerUtils.d.ts +1 -2
  5. package/dist/cjs/clients/BundleDataClient/utils/DataworkerUtils.js +1 -2
  6. package/dist/cjs/clients/BundleDataClient/utils/DataworkerUtils.js.map +1 -1
  7. package/dist/cjs/clients/BundleDataClient/utils/FillUtils.d.ts +3 -1
  8. package/dist/cjs/clients/BundleDataClient/utils/FillUtils.js +33 -1
  9. package/dist/cjs/clients/BundleDataClient/utils/FillUtils.js.map +1 -1
  10. package/dist/cjs/clients/SpokePoolClient.d.ts +1 -0
  11. package/dist/cjs/clients/SpokePoolClient.js +13 -4
  12. package/dist/cjs/clients/SpokePoolClient.js.map +1 -1
  13. package/dist/cjs/constants.d.ts +1 -0
  14. package/dist/cjs/constants.js +2 -1
  15. package/dist/cjs/constants.js.map +1 -1
  16. package/dist/cjs/providers/index.d.ts +1 -0
  17. package/dist/cjs/providers/index.js +2 -0
  18. package/dist/cjs/providers/index.js.map +1 -1
  19. package/dist/cjs/providers/mockProvider.d.ts +19 -0
  20. package/dist/cjs/providers/mockProvider.js +70 -0
  21. package/dist/cjs/providers/mockProvider.js.map +1 -0
  22. package/dist/cjs/utils/AddressUtils.d.ts +1 -0
  23. package/dist/cjs/utils/AddressUtils.js +14 -1
  24. package/dist/cjs/utils/AddressUtils.js.map +1 -1
  25. package/dist/cjs/utils/CachingUtils.js +1 -1
  26. package/dist/cjs/utils/CachingUtils.js.map +1 -1
  27. package/dist/cjs/utils/DepositUtils.d.ts +2 -1
  28. package/dist/cjs/utils/DepositUtils.js +12 -3
  29. package/dist/cjs/utils/DepositUtils.js.map +1 -1
  30. package/dist/cjs/utils/NetworkUtils.d.ts +1 -0
  31. package/dist/cjs/utils/NetworkUtils.js +6 -1
  32. package/dist/cjs/utils/NetworkUtils.js.map +1 -1
  33. package/dist/cjs/utils/SpokeUtils.js +3 -3
  34. package/dist/cjs/utils/SpokeUtils.js.map +1 -1
  35. package/dist/esm/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  36. package/dist/esm/clients/BundleDataClient/BundleDataClient.js +382 -218
  37. package/dist/esm/clients/BundleDataClient/BundleDataClient.js.map +1 -1
  38. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.d.ts +1 -2
  39. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.js +2 -3
  40. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.js.map +1 -1
  41. package/dist/esm/clients/BundleDataClient/utils/FillUtils.d.ts +3 -1
  42. package/dist/esm/clients/BundleDataClient/utils/FillUtils.js +42 -1
  43. package/dist/esm/clients/BundleDataClient/utils/FillUtils.js.map +1 -1
  44. package/dist/esm/clients/SpokePoolClient.d.ts +8 -0
  45. package/dist/esm/clients/SpokePoolClient.js +20 -4
  46. package/dist/esm/clients/SpokePoolClient.js.map +1 -1
  47. package/dist/esm/constants.d.ts +1 -0
  48. package/dist/esm/constants.js +2 -1
  49. package/dist/esm/constants.js.map +1 -1
  50. package/dist/esm/providers/index.d.ts +1 -0
  51. package/dist/esm/providers/index.js +2 -0
  52. package/dist/esm/providers/index.js.map +1 -1
  53. package/dist/esm/providers/mockProvider.d.ts +23 -0
  54. package/dist/esm/providers/mockProvider.js +73 -0
  55. package/dist/esm/providers/mockProvider.js.map +1 -0
  56. package/dist/esm/utils/AddressUtils.d.ts +1 -0
  57. package/dist/esm/utils/AddressUtils.js +16 -0
  58. package/dist/esm/utils/AddressUtils.js.map +1 -1
  59. package/dist/esm/utils/CachingUtils.js +1 -1
  60. package/dist/esm/utils/CachingUtils.js.map +1 -1
  61. package/dist/esm/utils/DepositUtils.d.ts +2 -1
  62. package/dist/esm/utils/DepositUtils.js +12 -3
  63. package/dist/esm/utils/DepositUtils.js.map +1 -1
  64. package/dist/esm/utils/NetworkUtils.d.ts +6 -0
  65. package/dist/esm/utils/NetworkUtils.js +10 -0
  66. package/dist/esm/utils/NetworkUtils.js.map +1 -1
  67. package/dist/esm/utils/SpokeUtils.js +4 -4
  68. package/dist/esm/utils/SpokeUtils.js.map +1 -1
  69. package/dist/types/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  70. package/dist/types/clients/BundleDataClient/BundleDataClient.d.ts.map +1 -1
  71. package/dist/types/clients/BundleDataClient/utils/DataworkerUtils.d.ts +1 -2
  72. package/dist/types/clients/BundleDataClient/utils/DataworkerUtils.d.ts.map +1 -1
  73. package/dist/types/clients/BundleDataClient/utils/FillUtils.d.ts +3 -1
  74. package/dist/types/clients/BundleDataClient/utils/FillUtils.d.ts.map +1 -1
  75. package/dist/types/clients/SpokePoolClient.d.ts +8 -0
  76. package/dist/types/clients/SpokePoolClient.d.ts.map +1 -1
  77. package/dist/types/constants.d.ts +1 -0
  78. package/dist/types/constants.d.ts.map +1 -1
  79. package/dist/types/providers/index.d.ts +1 -0
  80. package/dist/types/providers/index.d.ts.map +1 -1
  81. package/dist/types/providers/mockProvider.d.ts +24 -0
  82. package/dist/types/providers/mockProvider.d.ts.map +1 -0
  83. package/dist/types/utils/AddressUtils.d.ts +1 -0
  84. package/dist/types/utils/AddressUtils.d.ts.map +1 -1
  85. package/dist/types/utils/DepositUtils.d.ts +2 -1
  86. package/dist/types/utils/DepositUtils.d.ts.map +1 -1
  87. package/dist/types/utils/NetworkUtils.d.ts +6 -0
  88. package/dist/types/utils/NetworkUtils.d.ts.map +1 -1
  89. package/dist/types/utils/SpokeUtils.d.ts.map +1 -1
  90. package/package.json +1 -1
  91. package/src/clients/BundleDataClient/BundleDataClient.ts +383 -217
  92. package/src/clients/BundleDataClient/utils/DataworkerUtils.ts +0 -8
  93. package/src/clients/BundleDataClient/utils/FillUtils.ts +47 -2
  94. package/src/clients/SpokePoolClient.ts +19 -6
  95. package/src/constants.ts +3 -1
  96. package/src/providers/index.ts +1 -0
  97. package/src/providers/mockProvider.ts +77 -0
  98. package/src/utils/AddressUtils.ts +16 -0
  99. package/src/utils/CachingUtils.ts +1 -1
  100. package/src/utils/DepositUtils.ts +12 -3
  101. package/src/utils/NetworkUtils.ts +11 -0
  102. package/src/utils/SpokeUtils.ts +6 -5
@@ -33,10 +33,13 @@ import {
33
33
  getImpliedBundleBlockRanges,
34
34
  isSlowFill,
35
35
  mapAsync,
36
+ filterAsync,
36
37
  bnUint32Max,
37
38
  isZeroValueDeposit,
38
39
  findFillEvent,
39
40
  isZeroValueFillOrSlowFillRequest,
41
+ chainIsEvm,
42
+ isValidEvmAddress,
40
43
  } from "../../utils";
41
44
  import winston from "winston";
42
45
  import {
@@ -51,7 +54,9 @@ import {
51
54
  prettyPrintV3SpokePoolEvents,
52
55
  V3DepositWithBlock,
53
56
  V3FillWithBlock,
57
+ verifyFillRepayment,
54
58
  } from "./utils";
59
+ import { PRE_FILL_MIN_CONFIG_STORE_VERSION } from "../../constants";
55
60
 
56
61
  // max(uint256) - 1
57
62
  export const INFINITE_FILL_DEADLINE = bnUint32Max;
@@ -60,6 +65,10 @@ type DataCache = Record<string, Promise<LoadDataReturnValue>>;
60
65
 
61
66
  // V3 dictionary helper functions
62
67
  function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void {
68
+ // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise.
69
+ if (chainIsEvm(deposit.originChainId) && !isValidEvmAddress(deposit.depositor)) {
70
+ return;
71
+ }
63
72
  const { originChainId, inputToken } = deposit;
64
73
  if (!dict?.[originChainId]?.[inputToken]) {
65
74
  assign(dict, [originChainId, inputToken], []);
@@ -80,8 +89,13 @@ function updateBundleFillsV3(
80
89
  fill: V3FillWithBlock,
81
90
  lpFeePct: BigNumber,
82
91
  repaymentChainId: number,
83
- repaymentToken: string
92
+ repaymentToken: string,
93
+ repaymentAddress: string
84
94
  ): void {
95
+ // It is impossible to refund a deposit if the repayment chain is EVM and the relayer is a non-evm address.
96
+ if (chainIsEvm(repaymentChainId) && !isValidEvmAddress(repaymentAddress)) {
97
+ return;
98
+ }
85
99
  if (!dict?.[repaymentChainId]?.[repaymentToken]) {
86
100
  assign(dict, [repaymentChainId, repaymentToken], {
87
101
  fills: [],
@@ -91,19 +105,19 @@ function updateBundleFillsV3(
91
105
  });
92
106
  }
93
107
 
94
- const bundleFill: BundleFillV3 = { ...fill, lpFeePct };
108
+ const bundleFill: BundleFillV3 = { ...fill, lpFeePct, relayer: repaymentAddress };
95
109
 
96
110
  // Add all fills, slow and fast, to dictionary.
97
111
  assign(dict, [repaymentChainId, repaymentToken, "fills"], [bundleFill]);
98
112
 
99
113
  // All fills update the bundle LP fees.
100
114
  const refundObj = dict[repaymentChainId][repaymentToken];
101
- const realizedLpFee = fill.inputAmount.mul(bundleFill.lpFeePct).div(fixedPointAdjustment);
115
+ const realizedLpFee = bundleFill.inputAmount.mul(bundleFill.lpFeePct).div(fixedPointAdjustment);
102
116
  refundObj.realizedLpFees = refundObj.realizedLpFees ? refundObj.realizedLpFees.add(realizedLpFee) : realizedLpFee;
103
117
 
104
118
  // Only fast fills get refunded.
105
- if (!isSlowFill(fill)) {
106
- const refundAmount = fill.inputAmount.mul(fixedPointAdjustment.sub(lpFeePct)).div(fixedPointAdjustment);
119
+ if (!isSlowFill(bundleFill)) {
120
+ const refundAmount = bundleFill.inputAmount.mul(fixedPointAdjustment.sub(lpFeePct)).div(fixedPointAdjustment);
107
121
  refundObj.totalRefundAmount = refundObj.totalRefundAmount
108
122
  ? refundObj.totalRefundAmount.add(refundAmount)
109
123
  : refundAmount;
@@ -111,10 +125,10 @@ function updateBundleFillsV3(
111
125
  // Instantiate dictionary if it doesn't exist.
112
126
  refundObj.refunds ??= {};
113
127
 
114
- if (refundObj.refunds[fill.relayer]) {
115
- refundObj.refunds[fill.relayer] = refundObj.refunds[fill.relayer].add(refundAmount);
128
+ if (refundObj.refunds[bundleFill.relayer]) {
129
+ refundObj.refunds[bundleFill.relayer] = refundObj.refunds[bundleFill.relayer].add(refundAmount);
116
130
  } else {
117
- refundObj.refunds[fill.relayer] = refundAmount;
131
+ refundObj.refunds[bundleFill.relayer] = refundAmount;
118
132
  }
119
133
  }
120
134
  }
@@ -234,7 +248,6 @@ export class BundleDataClient {
234
248
  bundleData: prettyPrintV3SpokePoolEvents(
235
249
  bundleData.bundleDepositsV3,
236
250
  bundleData.bundleFillsV3,
237
- [], // Invalid fills are not persisted to Arweave.
238
251
  bundleData.bundleSlowFillsV3,
239
252
  bundleData.expiredDepositsToRefundV3,
240
253
  bundleData.unexecutableSlowFills
@@ -282,7 +295,7 @@ export class BundleDataClient {
282
295
  // so as not to affect this approximate refund count.
283
296
  const arweaveData = await this.loadArweaveData(bundleEvaluationBlockRanges);
284
297
  if (arweaveData === undefined) {
285
- combinedRefunds = this.getApproximateRefundsForBlockRange(chainIds, bundleEvaluationBlockRanges);
298
+ combinedRefunds = await this.getApproximateRefundsForBlockRange(chainIds, bundleEvaluationBlockRanges);
286
299
  } else {
287
300
  const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
288
301
  combinedRefunds = getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3);
@@ -303,50 +316,72 @@ export class BundleDataClient {
303
316
  }
304
317
 
305
318
  // @dev This helper function should probably be moved to the InventoryClient
306
- getApproximateRefundsForBlockRange(chainIds: number[], blockRanges: number[][]): CombinedRefunds {
319
+ async getApproximateRefundsForBlockRange(chainIds: number[], blockRanges: number[][]): Promise<CombinedRefunds> {
307
320
  const refundsForChain: CombinedRefunds = {};
308
321
  for (const chainId of chainIds) {
309
322
  if (this.spokePoolClients[chainId] === undefined) {
310
323
  continue;
311
324
  }
312
325
  const chainIndex = chainIds.indexOf(chainId);
313
- this.spokePoolClients[chainId]
314
- .getFills()
315
- .filter((fill) => {
316
- if (fill.blockNumber < blockRanges[chainIndex][0] || fill.blockNumber > blockRanges[chainIndex][1]) {
317
- return false;
318
- }
326
+ // @todo This function does not account for pre-fill refunds as it is optimized for speed. The way to detect
327
+ // pre-fill refunds is to load all deposits that are unmatched by fills in the spoke pool client's memory
328
+ // and then query the FillStatus on-chain, but that might slow this function down too much. For now, we
329
+ // will live with this expected inaccuracy as it should be small. The pre-fill would have to precede the deposit
330
+ // by more than the caller's event lookback window which is expected to be unlikely.
331
+ const fillsToCount = await filterAsync(this.spokePoolClients[chainId].getFills(), async (fill) => {
332
+ if (
333
+ fill.blockNumber < blockRanges[chainIndex][0] ||
334
+ fill.blockNumber > blockRanges[chainIndex][1] ||
335
+ isZeroValueFillOrSlowFillRequest(fill)
336
+ ) {
337
+ return false;
338
+ }
319
339
 
320
- // If origin spoke pool client isn't defined, we can't validate it.
321
- if (this.spokePoolClients[fill.originChainId] === undefined) {
322
- return false;
323
- }
324
- const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
325
- const hasMatchingDeposit =
326
- matchingDeposit !== undefined &&
327
- this.getRelayHashFromEvent(fill) === this.getRelayHashFromEvent(matchingDeposit);
328
- return hasMatchingDeposit;
329
- })
330
- .forEach((fill) => {
331
- const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
332
- assert(isDefined(matchingDeposit), "Deposit not found for fill.");
333
- const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill(
340
+ // If origin spoke pool client isn't defined, we can't validate it.
341
+ if (this.spokePoolClients[fill.originChainId] === undefined) {
342
+ return false;
343
+ }
344
+ const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
345
+ const hasMatchingDeposit =
346
+ matchingDeposit !== undefined &&
347
+ this.getRelayHashFromEvent(fill) === this.getRelayHashFromEvent(matchingDeposit);
348
+ if (hasMatchingDeposit) {
349
+ const validRepayment = await verifyFillRepayment(
334
350
  fill,
335
- this.clients.hubPoolClient,
336
- blockRanges,
337
- this.chainIdListForBundleEvaluationBlockNumbers,
338
- matchingDeposit!.fromLiteChain // Use ! because we've already asserted that matchingDeposit is defined.
351
+ this.spokePoolClients[fill.destinationChainId].spokePool.provider,
352
+ matchingDeposit,
353
+ // @dev: to get valid repayment chain ID's, get all chain IDs for the bundle block range and remove
354
+ // disabled block ranges.
355
+ this.clients.configStoreClient
356
+ .getChainIdIndicesForBlock(blockRanges[0][1])
357
+ .filter((_chainId, i) => !isChainDisabled(blockRanges[i]))
339
358
  );
340
- // Assume that lp fees are 0 for the sake of speed. In the future we could batch compute
341
- // these or make hardcoded assumptions based on the origin-repayment chain direction. This might result
342
- // in slight over estimations of refunds, but its not clear whether underestimating or overestimating is
343
- // worst from the relayer's perspective.
344
- const { relayer, inputAmount: refundAmount } = fill;
345
- refundsForChain[chainToSendRefundTo] ??= {};
346
- refundsForChain[chainToSendRefundTo][repaymentToken] ??= {};
347
- const existingRefundAmount = refundsForChain[chainToSendRefundTo][repaymentToken][relayer] ?? bnZero;
348
- refundsForChain[chainToSendRefundTo][repaymentToken][relayer] = existingRefundAmount.add(refundAmount);
349
- });
359
+ if (!isDefined(validRepayment)) {
360
+ return false;
361
+ }
362
+ }
363
+ return hasMatchingDeposit;
364
+ });
365
+ fillsToCount.forEach((fill) => {
366
+ const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
367
+ assert(isDefined(matchingDeposit), "Deposit not found for fill.");
368
+ const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill(
369
+ fill,
370
+ this.clients.hubPoolClient,
371
+ blockRanges,
372
+ this.chainIdListForBundleEvaluationBlockNumbers,
373
+ matchingDeposit!.fromLiteChain // Use ! because we've already asserted that matchingDeposit is defined.
374
+ );
375
+ // Assume that lp fees are 0 for the sake of speed. In the future we could batch compute
376
+ // these or make hardcoded assumptions based on the origin-repayment chain direction. This might result
377
+ // in slight over estimations of refunds, but its not clear whether underestimating or overestimating is
378
+ // worst from the relayer's perspective.
379
+ const { relayer, inputAmount: refundAmount } = fill;
380
+ refundsForChain[chainToSendRefundTo] ??= {};
381
+ refundsForChain[chainToSendRefundTo][repaymentToken] ??= {};
382
+ const existingRefundAmount = refundsForChain[chainToSendRefundTo][repaymentToken][relayer] ?? bnZero;
383
+ refundsForChain[chainToSendRefundTo][repaymentToken][relayer] = existingRefundAmount.add(refundAmount);
384
+ });
350
385
  }
351
386
  return refundsForChain;
352
387
  }
@@ -473,7 +508,7 @@ export class BundleDataClient {
473
508
  // ok for this use case.
474
509
  const arweaveData = await this.loadArweaveData(pendingBundleBlockRanges);
475
510
  if (arweaveData === undefined) {
476
- combinedRefunds.push(this.getApproximateRefundsForBlockRange(chainIds, pendingBundleBlockRanges));
511
+ combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, pendingBundleBlockRanges));
477
512
  } else {
478
513
  const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
479
514
  combinedRefunds.push(getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3));
@@ -488,7 +523,7 @@ export class BundleDataClient {
488
523
  // - Only look up fills sent after the pending bundle's end blocks
489
524
  // - Skip LP fee computations and just assume the relayer is being refunded the full deposit.inputAmount
490
525
  const start = performance.now();
491
- combinedRefunds.push(this.getApproximateRefundsForBlockRange(chainIds, widestBundleBlockRanges));
526
+ combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, widestBundleBlockRanges));
492
527
  this.logger.debug({
493
528
  at: "BundleDataClient#getNextBundleRefunds",
494
529
  message: `Loading approximate refunds for next bundle in ${Math.round(performance.now() - start) / 1000}s.`,
@@ -655,6 +690,7 @@ export class BundleDataClient {
655
690
  const bundleDepositsV3: BundleDepositsV3 = {}; // Deposits in bundle block range.
656
691
  const bundleFillsV3: BundleFillsV3 = {}; // Fills to refund in bundle block range.
657
692
  const bundleInvalidFillsV3: V3FillWithBlock[] = []; // Fills that are not valid in this bundle.
693
+ const bundleUnrepayableFillsV3: V3FillWithBlock[] = []; // Fills that are not repayable in this bundle.
658
694
  const bundleSlowFillsV3: BundleSlowFills = {}; // Deposits that we need to send slow fills
659
695
  // for in this bundle.
660
696
  const expiredDepositsToRefundV3: ExpiredDepositsToRefundV3 = {};
@@ -686,6 +722,10 @@ export class BundleDataClient {
686
722
  );
687
723
  };
688
724
 
725
+ const _depositIsExpired = (deposit: DepositWithBlock): boolean => {
726
+ return deposit.fillDeadline < bundleBlockTimestamps[deposit.destinationChainId][1];
727
+ };
728
+
689
729
  const _getFillStatusForDeposit = (deposit: Deposit, queryBlock: number): Promise<FillStatus> => {
690
730
  return spokePoolClients[deposit.destinationChainId].relayFillStatus(
691
731
  deposit,
@@ -737,7 +777,7 @@ export class BundleDataClient {
737
777
  // Note: Since there are no partial fills in v3, there should only be one fill per relay hash.
738
778
  // Moreover, the SpokePool blocks multiple slow fill requests, so
739
779
  // there should also only be one slow fill request per relay hash.
740
- deposit?: V3DepositWithBlock;
780
+ deposits?: V3DepositWithBlock[];
741
781
  fill?: V3FillWithBlock;
742
782
  slowFillRequest?: SlowFillRequestWithBlock;
743
783
  };
@@ -748,6 +788,27 @@ export class BundleDataClient {
748
788
  const bundleDepositHashes: string[] = [];
749
789
  const olderDepositHashes: string[] = [];
750
790
 
791
+ const decodeBundleDepositHash = (depositHash: string): { relayDataHash: string; index: number } => {
792
+ const [relayDataHash, i] = depositHash.split("@");
793
+ return { relayDataHash, index: Number(i) };
794
+ };
795
+
796
+ // We use the following toggle to aid with the migration to pre-fills. The first bundle proposed using this
797
+ // pre-fill logic can double refund pre-fills that have already been filled in the last bundle, because the
798
+ // last bundle did not recognize a fill as a pre-fill. Therefore the developer should ensure that the version
799
+ // is bumped to the PRE_FILL_MIN_CONFIG_STORE_VERSION version before the first pre-fill bundle is proposed.
800
+ // To test the following bundle after this, the developer can set the FORCE_REFUND_PREFILLS environment variable
801
+ // to "true" simulate the bundle with pre-fill refunds.
802
+ // @todo Remove this logic once we have advanced sufficiently past the pre-fill migration.
803
+ const startBlockForMainnet = getBlockRangeForChain(
804
+ blockRangesForChains,
805
+ this.clients.hubPoolClient.chainId,
806
+ this.chainIdListForBundleEvaluationBlockNumbers
807
+ )[0];
808
+ const versionAtProposalBlock = this.clients.configStoreClient.getConfigStoreVersionForBlock(startBlockForMainnet);
809
+ const canRefundPrefills =
810
+ versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION || process.env.FORCE_REFUND_PREFILLS === "true";
811
+
751
812
  let depositCounter = 0;
752
813
  for (const originChainId of allChainIds) {
753
814
  const originClient = spokePoolClients[originChainId];
@@ -761,17 +822,20 @@ export class BundleDataClient {
761
822
  // Only evaluate deposits that are in this bundle or in previous bundles. This means we cannot issue fill
762
823
  // refunds or slow fills here for deposits that are in future bundles (i.e. "pre-fills"). Instead, we'll
763
824
  // evaluate these pre-fills once the deposit is inside the "current" bundle block range.
764
- if (isZeroValueDeposit(deposit) || deposit.blockNumber > originChainBlockRange[1]) {
825
+ if (deposit.blockNumber > originChainBlockRange[1] || isZeroValueDeposit(deposit)) {
765
826
  return;
766
827
  }
767
828
  depositCounter++;
768
829
  const relayDataHash = this.getRelayHashFromEvent(deposit);
830
+
769
831
  if (!v3RelayHashes[relayDataHash]) {
770
832
  v3RelayHashes[relayDataHash] = {
771
- deposit: deposit,
833
+ deposits: [deposit],
772
834
  fill: undefined,
773
835
  slowFillRequest: undefined,
774
836
  };
837
+ } else {
838
+ v3RelayHashes[relayDataHash].deposits!.push(deposit);
775
839
  }
776
840
 
777
841
  // Once we've saved the deposit hash into v3RelayHashes, then we can exit early here if the inputAmount
@@ -782,11 +846,20 @@ export class BundleDataClient {
782
846
  return;
783
847
  }
784
848
 
849
+ // Evaluate all expired deposits after fetching fill statuses,
850
+ // since we can't know for certain whether an expired deposit was filled a long time ago.
851
+ const newBundleDepositHash = `${relayDataHash}@${v3RelayHashes[relayDataHash].deposits!.length - 1}`;
852
+ const decodedBundleDepositHash = decodeBundleDepositHash(newBundleDepositHash);
853
+ assert(
854
+ decodedBundleDepositHash.relayDataHash === relayDataHash &&
855
+ decodedBundleDepositHash.index === v3RelayHashes[relayDataHash].deposits!.length - 1,
856
+ "Not using correct bundle deposit hash key"
857
+ );
785
858
  if (deposit.blockNumber >= originChainBlockRange[0]) {
786
- bundleDepositHashes.push(relayDataHash);
859
+ bundleDepositHashes.push(newBundleDepositHash);
787
860
  updateBundleDepositsV3(bundleDepositsV3, deposit);
788
861
  } else if (deposit.blockNumber < originChainBlockRange[0]) {
789
- olderDepositHashes.push(relayDataHash);
862
+ olderDepositHashes.push(newBundleDepositHash);
790
863
  }
791
864
  });
792
865
  }
@@ -825,44 +898,67 @@ export class BundleDataClient {
825
898
  (fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueFillOrSlowFillRequest(fill)
826
899
  ),
827
900
  async (fill) => {
828
- const relayDataHash = this.getRelayHashFromEvent(fill);
829
901
  fillCounter++;
830
-
902
+ const relayDataHash = this.getRelayHashFromEvent(fill);
831
903
  if (v3RelayHashes[relayDataHash]) {
832
904
  if (!v3RelayHashes[relayDataHash].fill) {
833
905
  assert(
834
- isDefined(v3RelayHashes[relayDataHash].deposit),
906
+ isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
835
907
  "Deposit should exist in relay hash dictionary."
836
908
  );
837
909
  // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit,
838
- // so this fill is validated.
910
+ // so this fill can no longer be filled on-chain.
839
911
  v3RelayHashes[relayDataHash].fill = fill;
840
912
  if (fill.blockNumber >= destinationChainBlockRange[0]) {
841
- validatedBundleV3Fills.push({
842
- ...fill,
843
- quoteTimestamp: v3RelayHashes[relayDataHash].deposit!.quoteTimestamp, // ! due to assert above
844
- });
913
+ const fillToRefund = await verifyFillRepayment(
914
+ fill,
915
+ destinationClient.spokePool.provider,
916
+ v3RelayHashes[relayDataHash].deposits![0],
917
+ allChainIds
918
+ );
919
+ if (!isDefined(fillToRefund)) {
920
+ bundleUnrepayableFillsV3.push(fill);
921
+ // We don't return here yet because we still need to mark unexecutable slow fill leaves
922
+ // or duplicate deposits. However, we won't issue a fast fill refund.
923
+ } else {
924
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
925
+ validatedBundleV3Fills.push({
926
+ ...fillToRefund,
927
+ quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp, // ! due to assert above
928
+ });
929
+ }
930
+
845
931
  // If fill replaced a slow fill request, then mark it as one that might have created an
846
932
  // unexecutable slow fill. We can't know for sure until we check the slow fill request
847
933
  // events.
848
934
  // slow fill requests for deposits from or to lite chains are considered invalid
849
935
  if (
850
936
  fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
851
- !v3RelayHashes[relayDataHash].deposit!.fromLiteChain &&
852
- !v3RelayHashes[relayDataHash].deposit!.toLiteChain
937
+ _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposits![0])
853
938
  ) {
854
939
  fastFillsReplacingSlowFills.push(relayDataHash);
855
940
  }
941
+ // Now that know this deposit has been filled on-chain, identify any duplicate deposits sent for this fill and refund
942
+ // them, because they would not be refunded otherwise. These deposits can no longer expire and get
943
+ // refunded as an expired deposit, and they won't trigger a pre-fill refund because the fill is
944
+ // in this bundle. Pre-fill refunds only happen when deposits are sent in this bundle and the
945
+ // fill is from a prior bundle.
946
+ const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1);
947
+ duplicateDeposits.forEach((duplicateDeposit) => {
948
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
949
+ });
856
950
  }
951
+ } else {
952
+ throw new Error("Duplicate fill detected");
857
953
  }
858
954
  return;
859
955
  }
860
956
 
861
957
  // At this point, there is no relay hash dictionary entry for this fill, so we need to
862
- // instantiate the entry.
958
+ // instantiate the entry. We won't modify the fill.relayer until we match it with a deposit.
863
959
  v3RelayHashes[relayDataHash] = {
864
- deposit: undefined,
865
- fill: fill,
960
+ deposits: undefined,
961
+ fill,
866
962
  slowFillRequest: undefined,
867
963
  };
868
964
 
@@ -890,24 +986,42 @@ export class BundleDataClient {
890
986
  bundleInvalidFillsV3.push(fill);
891
987
  } else {
892
988
  const matchedDeposit = historicalDeposit.deposit;
893
- // @dev Since queryHistoricalDepositForFill validates the fill by checking individual
894
- // object property values against the deposit's, we
895
- // sanity check it here by comparing the full relay hashes. If there's an error here then the
896
- // historical deposit query is not working as expected.
897
- assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match.");
898
- validatedBundleV3Fills.push({
899
- ...fill,
900
- quoteTimestamp: matchedDeposit.quoteTimestamp,
901
- });
902
- v3RelayHashes[relayDataHash].deposit = matchedDeposit;
989
+ v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
990
+
991
+ const fillToRefund = await verifyFillRepayment(
992
+ fill,
993
+ destinationClient.spokePool.provider,
994
+ matchedDeposit,
995
+ allChainIds
996
+ );
997
+ if (!isDefined(fillToRefund)) {
998
+ bundleUnrepayableFillsV3.push(fill);
999
+ // Don't return yet as we still need to mark down any unexecutable slow fill leaves
1000
+ // in case this fast fill replaced a slow fill request.
1001
+ } else {
1002
+ // @dev Since queryHistoricalDepositForFill validates the fill by checking individual
1003
+ // object property values against the deposit's, we
1004
+ // sanity check it here by comparing the full relay hashes. If there's an error here then the
1005
+ // historical deposit query is not working as expected.
1006
+ assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match.");
1007
+ validatedBundleV3Fills.push({
1008
+ ...fillToRefund,
1009
+ quoteTimestamp: matchedDeposit.quoteTimestamp,
1010
+ });
1011
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
1012
+ }
1013
+
903
1014
  // slow fill requests for deposits from or to lite chains are considered invalid
904
1015
  if (
905
1016
  fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
906
- !matchedDeposit.fromLiteChain &&
907
- !matchedDeposit.toLiteChain
1017
+ _canCreateSlowFillLeaf(matchedDeposit)
908
1018
  ) {
909
1019
  fastFillsReplacingSlowFills.push(relayDataHash);
910
1020
  }
1021
+
1022
+ // No need to check for duplicate deposits here since we would have seen them in memory if they
1023
+ // had a non-infinite fill deadline, and duplicate deposits with infinite deadlines are impossible
1024
+ // to send.
911
1025
  }
912
1026
  }
913
1027
  }
@@ -932,38 +1046,40 @@ export class BundleDataClient {
932
1046
  v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest;
933
1047
  if (v3RelayHashes[relayDataHash].fill) {
934
1048
  // If there is a fill matching the relay hash, then this slow fill request can't be used
935
- // to create a slow fill for a filled deposit.
1049
+ // to create a slow fill for a filled deposit. This takes advantage of the fact that
1050
+ // slow fill requests must precede fills, so if there is a matching fill for this request's
1051
+ // relay data, then this slow fill will be unexecutable.
936
1052
  return;
937
1053
  }
938
1054
  assert(
939
- isDefined(v3RelayHashes[relayDataHash].deposit),
1055
+ isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
940
1056
  "Deposit should exist in relay hash dictionary."
941
1057
  );
942
1058
  // The ! is safe here because we've already checked that the deposit exists in the relay hash dictionary.
943
- const matchedDeposit = v3RelayHashes[relayDataHash].deposit!;
944
- if (!_canCreateSlowFillLeaf(matchedDeposit)) {
945
- return;
946
- }
1059
+ const matchedDeposit = v3RelayHashes[relayDataHash].deposits![0];
947
1060
 
948
1061
  // If there is no fill matching the relay hash, then this might be a valid slow fill request
949
1062
  // that we should produce a slow fill leaf for. Check if the slow fill request is in the
950
1063
  // destination chain block range.
951
1064
  if (
952
1065
  slowFillRequest.blockNumber >= destinationChainBlockRange[0] &&
1066
+ _canCreateSlowFillLeaf(matchedDeposit) &&
953
1067
  // Deposit must not have expired in this bundle.
954
- slowFillRequest.fillDeadline >= bundleBlockTimestamps[destinationChainId][1]
1068
+ !_depositIsExpired(matchedDeposit)
955
1069
  ) {
956
1070
  // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit,
957
1071
  // so this slow fill request relay data is correct.
958
1072
  validatedBundleSlowFills.push(matchedDeposit);
959
1073
  }
1074
+ } else {
1075
+ throw new Error("Duplicate slow fill request detected.");
960
1076
  }
961
1077
  return;
962
1078
  }
963
1079
 
964
1080
  // Instantiate dictionary if there is neither a deposit nor fill matching it.
965
1081
  v3RelayHashes[relayDataHash] = {
966
- deposit: undefined,
1082
+ deposits: undefined,
967
1083
  fill: undefined,
968
1084
  slowFillRequest: slowFillRequest,
969
1085
  };
@@ -998,12 +1114,12 @@ export class BundleDataClient {
998
1114
  this.getRelayHashFromEvent(matchedDeposit) === relayDataHash,
999
1115
  "Deposit relay hashes should match."
1000
1116
  );
1001
- v3RelayHashes[relayDataHash].deposit = matchedDeposit;
1117
+ v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
1002
1118
 
1003
1119
  if (
1004
1120
  !_canCreateSlowFillLeaf(matchedDeposit) ||
1005
1121
  // Deposit must not have expired in this bundle.
1006
- slowFillRequest.fillDeadline < bundleBlockTimestamps[destinationChainId][1]
1122
+ _depositIsExpired(matchedDeposit)
1007
1123
  ) {
1008
1124
  return;
1009
1125
  }
@@ -1019,141 +1135,163 @@ export class BundleDataClient {
1019
1135
  // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund.
1020
1136
  // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf
1021
1137
  // for this "pre-slow-fill-request" if this request took place in a previous bundle.
1022
- const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds);
1023
-
1024
- await mapAsync(
1025
- bundleDepositHashes.filter((depositHash) => {
1026
- const { deposit } = v3RelayHashes[depositHash];
1027
- return (
1028
- deposit &&
1029
- deposit.originChainId === originChainId &&
1030
- deposit.destinationChainId === destinationChainId &&
1031
- deposit.blockNumber >= originBlockRange[0] &&
1032
- deposit.blockNumber <= originBlockRange[1] &&
1033
- !isZeroValueDeposit(deposit)
1034
- );
1035
- }),
1036
- async (depositHash) => {
1037
- const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash];
1038
- if (!deposit) throw new Error("Deposit should exist in relay hash dictionary.");
1039
-
1040
- // We are willing to refund a pre-fill multiple times for each duplicate deposit.
1041
- // This is because a duplicate deposit for a pre-fill cannot get
1042
- // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore
1043
- // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out
1044
- // the pre-filler.
1045
-
1046
- // If fill exists in memory, then the only case in which we need to create a refund is if the
1047
- // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits.
1048
- if (fill) {
1049
- if (!isSlowFill(fill) && fill.blockNumber < destinationChainBlockRange[0]) {
1138
+
1139
+ // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We
1140
+ // should remove this check once we've advanced far beyond the version bump block.
1141
+ await mapAsync(bundleDepositHashes, async (depositHash, currentBundleDepositHashIndex) => {
1142
+ // We don't need to call verifyFillRepayment() here to replace the fill.relayer because this value should already
1143
+ // be overwritten because the deposit and fill both exist.
1144
+ const { relayDataHash, index } = decodeBundleDepositHash(depositHash);
1145
+ const { deposits, fill, slowFillRequest } = v3RelayHashes[relayDataHash];
1146
+ const deposit = deposits![index];
1147
+ if (!deposit) throw new Error("Deposit should exist in relay hash dictionary.");
1148
+ if (deposit.originChainId !== originChainId || deposit.destinationChainId !== destinationChainId) {
1149
+ return;
1150
+ }
1151
+ const isDuplicateDepositInBundle = bundleDepositHashes
1152
+ .slice(0, currentBundleDepositHashIndex)
1153
+ .some((_depositHash) => {
1154
+ const { relayDataHash: _relayDataHash } = decodeBundleDepositHash(_depositHash);
1155
+ return _relayDataHash === relayDataHash;
1156
+ });
1157
+ // Don't refund duplicate deposits from a prior bundle, as they should have been refunded already
1158
+ // if they coincided with another deposit in the same bundle. If they didn't, then its input
1159
+ // amount was used to refund a pre-fill.
1160
+ // We will refund any duplicate deposits the first time that we see a deposit hash in this bundle.
1161
+ // If this is the first time we are seeing this deposit hash, then refund any duplicate deposits since
1162
+ // a fill exists for it and these duplicate deposits can no longer be refunded for expiry.
1163
+ // This means unfortunately that every duplicate deposit that is sent that
1164
+ // does not accompany another deposit in the same bundle will not be refunded. This should be unlikely.
1165
+ // This rule also allows us to protect honest depositors who accidentally send duplicate deposits
1166
+ // in rapid succession in most cases, unless they are unlucky enough to send duplicate deposits
1167
+ // in different bundle block ranges.
1168
+ const duplicateDepositsInBundle = deposits!.slice(index + 1);
1169
+
1170
+ // We are willing to refund a pre-fill multiple times for each duplicate deposit.
1171
+ // This is because a duplicate deposit for a pre-fill cannot get
1172
+ // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore
1173
+ // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out
1174
+ // the pre-filler.
1175
+
1176
+ // If fill exists in memory, then the only case in which we need to create a refund is if the
1177
+ // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits.
1178
+ if (fill) {
1179
+ if (!isDuplicateDepositInBundle && fill.blockNumber < destinationChainBlockRange[0]) {
1180
+ duplicateDepositsInBundle.forEach((duplicateDeposit) => {
1181
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
1182
+ });
1183
+ if (canRefundPrefills) {
1050
1184
  // If fill is in the current bundle then we can assume there is already a refund for it, so only
1051
1185
  // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then
1052
1186
  // we won't consider it, following the previous treatment of fills after the bundle block range.
1053
- validatedBundleV3Fills.push({
1054
- ...fill,
1055
- quoteTimestamp: deposit.quoteTimestamp,
1056
- });
1187
+ if (!isSlowFill(fill)) {
1188
+ validatedBundleV3Fills.push({
1189
+ ...fill,
1190
+ quoteTimestamp: deposit.quoteTimestamp,
1191
+ });
1192
+ }
1057
1193
  }
1058
- return;
1059
1194
  }
1195
+ return;
1196
+ }
1060
1197
 
1061
- // If a slow fill request exists in memory, then we know the deposit has not been filled because fills
1062
- // must follow slow fill requests and we would have seen the fill already if it existed. Therefore,
1063
- // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or
1064
- // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request
1065
- // took place in a prior bundle otherwise we would have already created a slow fill leaf for it.
1066
- if (slowFillRequest) {
1067
- if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) {
1068
- updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1069
- } else if (
1070
- _canCreateSlowFillLeaf(deposit) &&
1071
- slowFillRequest.blockNumber < destinationChainBlockRange[0]
1072
- ) {
1073
- validatedBundleSlowFills.push(deposit);
1074
- }
1075
- return;
1198
+ // If a slow fill request exists in memory, then we know the deposit has not been filled because fills
1199
+ // must follow slow fill requests and we would have seen the fill already if it existed. Therefore,
1200
+ // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or
1201
+ // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request
1202
+ // took place in a prior bundle otherwise we would have already created a slow fill leaf for it.
1203
+ if (slowFillRequest) {
1204
+ if (_depositIsExpired(deposit)) {
1205
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1206
+ } else if (
1207
+ !isDuplicateDepositInBundle &&
1208
+ canRefundPrefills &&
1209
+ slowFillRequest.blockNumber < destinationChainBlockRange[0] &&
1210
+ _canCreateSlowFillLeaf(deposit)
1211
+ ) {
1212
+ validatedBundleSlowFills.push(deposit);
1076
1213
  }
1214
+ return;
1215
+ }
1077
1216
 
1078
- // So at this point in the code, there is no fill or slow fill request in memory for this deposit.
1079
- // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf.
1080
- // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles
1081
- // because the spoke pool client lookback would have returned this entire bundle of events and stored
1082
- // them into the relay hash dictionary.
1083
- const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]);
1084
-
1085
- // If deposit was filled, then we need to issue a refund for it.
1086
- if (fillStatus === FillStatus.Filled) {
1087
- // We need to find the fill event to issue a refund to the right relayer and repayment chain,
1088
- // or msg.sender if relayer address is invalid for the repayment chain.
1089
- const prefill = (await findFillEvent(
1090
- destinationClient.spokePool,
1217
+ // So at this point in the code, there is no fill or slow fill request in memory for this deposit.
1218
+ // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf.
1219
+ // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles
1220
+ // because the spoke pool client lookback would have returned this entire bundle of events and stored
1221
+ // them into the relay hash dictionary.
1222
+ const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]);
1223
+
1224
+ // If deposit was filled, then we need to issue a refund for the fill and also any duplicate deposits
1225
+ // in the same bundle.
1226
+ if (fillStatus === FillStatus.Filled) {
1227
+ // We need to find the fill event to issue a refund to the right relayer and repayment chain,
1228
+ // or msg.sender if relayer address is invalid for the repayment chain.
1229
+ const prefill = await this.findMatchingFillEvent(deposit, destinationClient);
1230
+ assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`);
1231
+ assert(this.getRelayHashFromEvent(prefill!) === relayDataHash, "Relay hashes should match.");
1232
+ if (!isDuplicateDepositInBundle) {
1233
+ duplicateDepositsInBundle.forEach((duplicateDeposit) => {
1234
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
1235
+ });
1236
+ const verifiedFill = await verifyFillRepayment(
1237
+ prefill!,
1238
+ destinationClient.spokePool.provider,
1091
1239
  deposit,
1092
- destinationClient.deploymentBlock,
1093
- destinationClient.latestBlockSearched
1094
- )) as unknown as FillWithBlock;
1095
- if (!isSlowFill(prefill)) {
1240
+ allChainIds
1241
+ );
1242
+ if (canRefundPrefills && isDefined(verifiedFill) && !isSlowFill(verifiedFill)) {
1096
1243
  validatedBundleV3Fills.push({
1097
- ...prefill,
1244
+ ...verifiedFill!,
1098
1245
  quoteTimestamp: deposit.quoteTimestamp,
1099
1246
  });
1100
1247
  }
1101
1248
  }
1102
- // If deposit is not filled and its newly expired, we can create a deposit refund for it.
1103
- // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because
1104
- // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0
1105
- // for example. Those should be included in this bundle of refunded deposits.
1106
- else if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) {
1107
- updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1108
- }
1109
- // If slow fill requested, then issue a slow fill leaf for the deposit.
1110
- else if (fillStatus === FillStatus.RequestedSlowFill) {
1111
- // Input and Output tokens must be equivalent on the deposit for this to be slow filled.
1112
- // Slow fill requests for deposits from or to lite chains are considered invalid
1113
- if (_canCreateSlowFillLeaf(deposit)) {
1114
- // If deposit newly expired, then we can't create a slow fill leaf for it but we can
1115
- // create a deposit refund for it.
1116
- validatedBundleSlowFills.push(deposit);
1117
- }
1249
+ }
1250
+ // If deposit is not filled and its newly expired, we can create a deposit refund for it.
1251
+ // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because
1252
+ // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0
1253
+ // for example. Those should be included in this bundle of refunded deposits.
1254
+ else if (_depositIsExpired(deposit)) {
1255
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1256
+ }
1257
+ // If slow fill requested, then issue a slow fill leaf for the deposit.
1258
+ else if (fillStatus === FillStatus.RequestedSlowFill) {
1259
+ // Input and Output tokens must be equivalent on the deposit for this to be slow filled.
1260
+ // Slow fill requests for deposits from or to lite chains are considered invalid
1261
+ if (!isDuplicateDepositInBundle && canRefundPrefills && _canCreateSlowFillLeaf(deposit)) {
1262
+ // If deposit newly expired, then we can't create a slow fill leaf for it but we can
1263
+ // create a deposit refund for it.
1264
+ validatedBundleSlowFills.push(deposit);
1118
1265
  }
1119
1266
  }
1120
- );
1267
+ });
1121
1268
 
1122
1269
  // For all fills that came after a slow fill request, we can now check if the slow fill request
1123
1270
  // was a valid one and whether it was created in a previous bundle. If so, then it created a slow fill
1124
1271
  // leaf that is now unexecutable.
1125
1272
  fastFillsReplacingSlowFills.forEach((relayDataHash) => {
1126
- const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1273
+ const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1127
1274
  assert(
1128
1275
  fill?.relayExecutionInfo.fillType === FillType.ReplacedSlowFill,
1129
1276
  "Fill type should be ReplacedSlowFill."
1130
1277
  );
1131
1278
  // Needed for TSC - are implicitely checking that deposit exists by making it to this point.
1132
- if (!deposit) {
1279
+ if (!deposits || deposits.length < 1) {
1133
1280
  throw new Error("Deposit should exist in relay hash dictionary.");
1134
1281
  }
1135
1282
  // We should never push fast fills involving lite chains here because slow fill requests for them are invalid:
1136
1283
  assert(
1137
- !deposit.fromLiteChain && !deposit.toLiteChain,
1138
- "fastFillsReplacingSlowFills should not contain lite chain deposits"
1284
+ _canCreateSlowFillLeaf(deposits[0]),
1285
+ "fastFillsReplacingSlowFills should contain only deposits that can be slow filled"
1139
1286
  );
1140
1287
  const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
1141
1288
  if (
1142
- // If the slow fill request that was replaced by this fill was in an older bundle, then we don't
1143
- // need to check if the slow fill request was valid since we can assume all bundles in the past
1144
- // were validated. However, we might as well double check.
1145
- this.clients.hubPoolClient.areTokensEquivalent(
1146
- deposit.inputToken,
1147
- deposit.originChainId,
1148
- deposit.outputToken,
1149
- deposit.destinationChainId,
1150
- deposit.quoteBlockNumber
1151
- ) &&
1152
1289
  // If there is a slow fill request in this bundle that matches the relay hash, then there was no slow fill
1153
1290
  // created that would be considered excess.
1154
- (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0])
1291
+ !slowFillRequest ||
1292
+ slowFillRequest.blockNumber < destinationBlockRange[0]
1155
1293
  ) {
1156
- validatedBundleUnexecutableSlowFills.push(deposit);
1294
+ validatedBundleUnexecutableSlowFills.push(deposits[0]);
1157
1295
  }
1158
1296
  });
1159
1297
  }
@@ -1167,10 +1305,14 @@ export class BundleDataClient {
1167
1305
  // For all deposits older than this bundle, we need to check if they expired in this bundle and if they did,
1168
1306
  // whether there was a slow fill created for it in a previous bundle that is now unexecutable and replaced
1169
1307
  // by a new expired deposit refund.
1170
- await forEachAsync(olderDepositHashes, async (relayDataHash) => {
1171
- const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1172
- assert(isDefined(deposit), "Deposit should exist in relay hash dictionary.");
1173
- const { destinationChainId } = deposit!;
1308
+ await forEachAsync(olderDepositHashes, async (depositHash) => {
1309
+ const { relayDataHash, index } = decodeBundleDepositHash(depositHash);
1310
+ const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1311
+ if (!deposits || deposits.length < 1) {
1312
+ throw new Error("Deposit should exist in relay hash dictionary.");
1313
+ }
1314
+ const deposit = deposits[index];
1315
+ const { destinationChainId } = deposit;
1174
1316
  const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
1175
1317
 
1176
1318
  // Only look for deposits that were mined before this bundle and that are newly expired.
@@ -1180,7 +1322,7 @@ export class BundleDataClient {
1180
1322
  // If there is a valid fill that we saw matching this deposit, then it does not need a refund.
1181
1323
  !fill &&
1182
1324
  isDefined(deposit) && // Needed for TSC - we check this above.
1183
- deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] &&
1325
+ _depositIsExpired(deposit) &&
1184
1326
  deposit.fillDeadline >= bundleBlockTimestamps[destinationChainId][0] &&
1185
1327
  spokePoolClients[destinationChainId] !== undefined
1186
1328
  ) {
@@ -1196,8 +1338,7 @@ export class BundleDataClient {
1196
1338
  // If fill status is RequestedSlowFill, then we might need to mark down an unexecutable
1197
1339
  // slow fill that we're going to replace with an expired deposit refund.
1198
1340
  // If deposit cannot be slow filled, then exit early.
1199
- // slow fill requests for deposits from or to lite chains are considered invalid
1200
- if (fillStatus !== FillStatus.RequestedSlowFill || deposit.fromLiteChain || deposit.toLiteChain) {
1341
+ if (fillStatus !== FillStatus.RequestedSlowFill || !_canCreateSlowFillLeaf(deposit)) {
1201
1342
  return;
1202
1343
  }
1203
1344
  // Now, check if there was a slow fill created for this deposit in a previous bundle which would now be
@@ -1206,21 +1347,9 @@ export class BundleDataClient {
1206
1347
 
1207
1348
  // If there is a slow fill request in this bundle, then the expired deposit refund will supercede
1208
1349
  // the slow fill request. If there is no slow fill request seen or its older than this bundle, then we can
1209
- // assume a slow fill leaf was created for it because its tokens are equivalent. The slow fill request was
1210
- // also sent before the fill deadline expired since we checked that above.
1211
- if (
1212
- // Since this deposit was requested for a slow fill in an older bundle at this point, we don't
1213
- // technically need to check if the slow fill request was valid since we can assume all bundles in the past
1214
- // were validated. However, we might as well double check.
1215
- this.clients.hubPoolClient.areTokensEquivalent(
1216
- deposit.inputToken,
1217
- deposit.originChainId,
1218
- deposit.outputToken,
1219
- deposit.destinationChainId,
1220
- deposit.quoteBlockNumber
1221
- ) &&
1222
- (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0])
1223
- ) {
1350
+ // assume a slow fill leaf was created for it because of the previous _canCreateSlowFillLeaf check.
1351
+ // The slow fill request was also sent before the fill deadline expired since we checked that above.
1352
+ if (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) {
1224
1353
  validatedBundleUnexecutableSlowFills.push(deposit);
1225
1354
  }
1226
1355
  }
@@ -1232,7 +1361,7 @@ export class BundleDataClient {
1232
1361
  validatedBundleV3Fills.length > 0
1233
1362
  ? this.clients.hubPoolClient.batchComputeRealizedLpFeePct(
1234
1363
  validatedBundleV3Fills.map((fill) => {
1235
- const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit;
1364
+ const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0];
1236
1365
  assert(isDefined(matchedDeposit), "Deposit should exist in relay hash dictionary.");
1237
1366
  const { chainToSendRefundTo: paymentChainId } = getRefundInformationFromFill(
1238
1367
  fill,
@@ -1276,7 +1405,7 @@ export class BundleDataClient {
1276
1405
  });
1277
1406
  v3FillLpFees.forEach(({ realizedLpFeePct }, idx) => {
1278
1407
  const fill = validatedBundleV3Fills[idx];
1279
- const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit;
1408
+ const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0];
1280
1409
  assert(isDefined(associatedDeposit), "Deposit should exist in relay hash dictionary.");
1281
1410
  const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill(
1282
1411
  fill,
@@ -1285,7 +1414,7 @@ export class BundleDataClient {
1285
1414
  chainIds,
1286
1415
  associatedDeposit!.fromLiteChain
1287
1416
  );
1288
- updateBundleFillsV3(bundleFillsV3, fill, realizedLpFeePct, chainToSendRefundTo, repaymentToken);
1417
+ updateBundleFillsV3(bundleFillsV3, fill, realizedLpFeePct, chainToSendRefundTo, repaymentToken, fill.relayer);
1289
1418
  });
1290
1419
  v3SlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => {
1291
1420
  const deposit = validatedBundleSlowFills[idx];
@@ -1299,7 +1428,6 @@ export class BundleDataClient {
1299
1428
  const v3SpokeEventsReadable = prettyPrintV3SpokePoolEvents(
1300
1429
  bundleDepositsV3,
1301
1430
  bundleFillsV3,
1302
- bundleInvalidFillsV3,
1303
1431
  bundleSlowFillsV3,
1304
1432
  expiredDepositsToRefundV3,
1305
1433
  unexecutableSlowFills
@@ -1314,6 +1442,15 @@ export class BundleDataClient {
1314
1442
  });
1315
1443
  }
1316
1444
 
1445
+ if (bundleUnrepayableFillsV3.length > 0) {
1446
+ this.logger.debug({
1447
+ at: "BundleDataClient#loadData",
1448
+ message: "Finished loading V3 spoke pool data and found some unrepayable V3 fills in range",
1449
+ blockRangesForChains,
1450
+ bundleUnrepayableFillsV3,
1451
+ });
1452
+ }
1453
+
1317
1454
  this.logger.debug({
1318
1455
  at: "BundleDataClient#loadDataFromScratch",
1319
1456
  message: `Computed bundle data in ${Math.round(performance.now() - start) / 1000}s.`,
@@ -1333,8 +1470,24 @@ export class BundleDataClient {
1333
1470
  // keccak256 hash of the relay data, which can be used as input into the on-chain `fillStatuses()` function in the
1334
1471
  // spoke pool contract. However, this internal function is used to uniquely identify a bridging event
1335
1472
  // for speed since its easier to build a string from the event data than to hash it.
1336
- private getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string {
1337
- return `${event.depositor}-${event.recipient}-${event.exclusiveRelayer}-${event.inputToken}-${event.outputToken}-${event.inputAmount}-${event.outputAmount}-${event.originChainId}-${event.depositId}-${event.fillDeadline}-${event.exclusivityDeadline}-${event.message}-${event.destinationChainId}`;
1473
+ protected getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string {
1474
+ return `${event.depositor}-${event.recipient}-${event.exclusiveRelayer}-${event.inputToken}-${event.outputToken}-${
1475
+ event.inputAmount
1476
+ }-${event.outputAmount}-${event.originChainId}-${event.depositId.toString()}-${event.fillDeadline}-${
1477
+ event.exclusivityDeadline
1478
+ }-${event.message}-${event.destinationChainId}`;
1479
+ }
1480
+
1481
+ protected async findMatchingFillEvent(
1482
+ deposit: DepositWithBlock,
1483
+ spokePoolClient: SpokePoolClient
1484
+ ): Promise<FillWithBlock | undefined> {
1485
+ return await findFillEvent(
1486
+ spokePoolClient.spokePool,
1487
+ deposit,
1488
+ spokePoolClient.deploymentBlock,
1489
+ spokePoolClient.latestBlockSearched
1490
+ );
1338
1491
  }
1339
1492
 
1340
1493
  async getBundleBlockTimestamps(
@@ -1362,13 +1515,26 @@ export class BundleDataClient {
1362
1515
  // will usually be called in production with block ranges that were validated by
1363
1516
  // DataworkerUtils.blockRangesAreInvalidForSpokeClients.
1364
1517
  const startBlockForChain = Math.min(_startBlockForChain, spokePoolClient.latestBlockSearched);
1365
- const endBlockForChain = Math.min(_endBlockForChain, spokePoolClient.latestBlockSearched);
1366
- const [startTime, endTime] = [
1518
+ // @dev Add 1 to the bundle end block. The thinking here is that there can be a gap between
1519
+ // block timestamps in subsequent blocks. The bundle data client assumes that fill deadlines expire
1520
+ // in exactly one bundle, therefore we must make sure that the bundle block timestamp for one bundle's
1521
+ // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way
1522
+ // there are no gaps in block timestamps between bundles.
1523
+ const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched);
1524
+ const [startTime, _endTime] = [
1367
1525
  await spokePoolClient.getTimestampForBlock(startBlockForChain),
1368
1526
  await spokePoolClient.getTimestampForBlock(endBlockForChain),
1369
1527
  ];
1528
+ // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also
1529
+ // no overlap, subtract 1 from the end time.
1530
+ const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0;
1531
+ const endTime = Math.max(0, _endTime - endBlockDelta);
1532
+
1370
1533
  // Sanity checks:
1371
- assert(endTime >= startTime, "End time should be greater than start time.");
1534
+ assert(
1535
+ endTime >= startTime,
1536
+ `End time for block ${endBlockForChain} should be greater than start time for block ${startBlockForChain}: ${endTime} >= ${startTime}.`
1537
+ );
1372
1538
  assert(
1373
1539
  startBlockForChain === 0 || startTime > 0,
1374
1540
  "Start timestamp must be greater than 0 if the start block is greater than 0."