@across-protocol/sdk 4.0.0-beta.3 → 4.0.0-beta.30

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 (132) hide show
  1. package/dist/cjs/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  2. package/dist/cjs/clients/BundleDataClient/BundleDataClient.js +340 -174
  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 +5 -1
  8. package/dist/cjs/clients/BundleDataClient/utils/FillUtils.js +47 -1
  9. package/dist/cjs/clients/BundleDataClient/utils/FillUtils.js.map +1 -1
  10. package/dist/cjs/clients/BundleDataClient/utils/SuperstructUtils.d.ts +4 -4
  11. package/dist/cjs/clients/SpokePoolClient.d.ts +1 -0
  12. package/dist/cjs/clients/SpokePoolClient.js +13 -4
  13. package/dist/cjs/clients/SpokePoolClient.js.map +1 -1
  14. package/dist/cjs/clients/mocks/MockSpokePoolClient.d.ts +2 -1
  15. package/dist/cjs/clients/mocks/MockSpokePoolClient.js +11 -0
  16. package/dist/cjs/clients/mocks/MockSpokePoolClient.js.map +1 -1
  17. package/dist/cjs/constants.d.ts +1 -1
  18. package/dist/cjs/constants.js +2 -2
  19. package/dist/cjs/constants.js.map +1 -1
  20. package/dist/cjs/providers/index.d.ts +1 -0
  21. package/dist/cjs/providers/index.js +2 -0
  22. package/dist/cjs/providers/index.js.map +1 -1
  23. package/dist/cjs/providers/mockProvider.d.ts +19 -0
  24. package/dist/cjs/providers/mockProvider.js +70 -0
  25. package/dist/cjs/providers/mockProvider.js.map +1 -0
  26. package/dist/cjs/utils/AddressUtils.d.ts +2 -0
  27. package/dist/cjs/utils/AddressUtils.js +19 -1
  28. package/dist/cjs/utils/AddressUtils.js.map +1 -1
  29. package/dist/cjs/utils/CachingUtils.js +1 -1
  30. package/dist/cjs/utils/CachingUtils.js.map +1 -1
  31. package/dist/cjs/utils/DepositUtils.d.ts +2 -1
  32. package/dist/cjs/utils/DepositUtils.js +13 -4
  33. package/dist/cjs/utils/DepositUtils.js.map +1 -1
  34. package/dist/cjs/utils/EventUtils.js +21 -0
  35. package/dist/cjs/utils/EventUtils.js.map +1 -1
  36. package/dist/cjs/utils/NetworkUtils.d.ts +1 -0
  37. package/dist/cjs/utils/NetworkUtils.js +6 -1
  38. package/dist/cjs/utils/NetworkUtils.js.map +1 -1
  39. package/dist/cjs/utils/SpokeUtils.d.ts +1 -0
  40. package/dist/cjs/utils/SpokeUtils.js +18 -11
  41. package/dist/cjs/utils/SpokeUtils.js.map +1 -1
  42. package/dist/cjs/utils/common.d.ts +1 -0
  43. package/dist/cjs/utils/common.js +2 -1
  44. package/dist/cjs/utils/common.js.map +1 -1
  45. package/dist/esm/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  46. package/dist/esm/clients/BundleDataClient/BundleDataClient.js +410 -208
  47. package/dist/esm/clients/BundleDataClient/BundleDataClient.js.map +1 -1
  48. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.d.ts +1 -2
  49. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.js +2 -3
  50. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.js.map +1 -1
  51. package/dist/esm/clients/BundleDataClient/utils/FillUtils.d.ts +5 -1
  52. package/dist/esm/clients/BundleDataClient/utils/FillUtils.js +54 -1
  53. package/dist/esm/clients/BundleDataClient/utils/FillUtils.js.map +1 -1
  54. package/dist/esm/clients/BundleDataClient/utils/SuperstructUtils.d.ts +4 -4
  55. package/dist/esm/clients/SpokePoolClient.d.ts +8 -0
  56. package/dist/esm/clients/SpokePoolClient.js +20 -4
  57. package/dist/esm/clients/SpokePoolClient.js.map +1 -1
  58. package/dist/esm/clients/mocks/MockSpokePoolClient.d.ts +2 -1
  59. package/dist/esm/clients/mocks/MockSpokePoolClient.js +11 -0
  60. package/dist/esm/clients/mocks/MockSpokePoolClient.js.map +1 -1
  61. package/dist/esm/constants.d.ts +1 -1
  62. package/dist/esm/constants.js +2 -2
  63. package/dist/esm/constants.js.map +1 -1
  64. package/dist/esm/providers/index.d.ts +1 -0
  65. package/dist/esm/providers/index.js +2 -0
  66. package/dist/esm/providers/index.js.map +1 -1
  67. package/dist/esm/providers/mockProvider.d.ts +23 -0
  68. package/dist/esm/providers/mockProvider.js +73 -0
  69. package/dist/esm/providers/mockProvider.js.map +1 -0
  70. package/dist/esm/utils/AddressUtils.d.ts +2 -0
  71. package/dist/esm/utils/AddressUtils.js +25 -0
  72. package/dist/esm/utils/AddressUtils.js.map +1 -1
  73. package/dist/esm/utils/CachingUtils.js +1 -1
  74. package/dist/esm/utils/CachingUtils.js.map +1 -1
  75. package/dist/esm/utils/DepositUtils.d.ts +2 -1
  76. package/dist/esm/utils/DepositUtils.js +14 -5
  77. package/dist/esm/utils/DepositUtils.js.map +1 -1
  78. package/dist/esm/utils/EventUtils.js +29 -1
  79. package/dist/esm/utils/EventUtils.js.map +1 -1
  80. package/dist/esm/utils/NetworkUtils.d.ts +6 -0
  81. package/dist/esm/utils/NetworkUtils.js +10 -0
  82. package/dist/esm/utils/NetworkUtils.js.map +1 -1
  83. package/dist/esm/utils/SpokeUtils.d.ts +1 -0
  84. package/dist/esm/utils/SpokeUtils.js +17 -11
  85. package/dist/esm/utils/SpokeUtils.js.map +1 -1
  86. package/dist/esm/utils/common.d.ts +1 -0
  87. package/dist/esm/utils/common.js +1 -0
  88. package/dist/esm/utils/common.js.map +1 -1
  89. package/dist/types/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  90. package/dist/types/clients/BundleDataClient/BundleDataClient.d.ts.map +1 -1
  91. package/dist/types/clients/BundleDataClient/utils/DataworkerUtils.d.ts +1 -2
  92. package/dist/types/clients/BundleDataClient/utils/DataworkerUtils.d.ts.map +1 -1
  93. package/dist/types/clients/BundleDataClient/utils/FillUtils.d.ts +5 -1
  94. package/dist/types/clients/BundleDataClient/utils/FillUtils.d.ts.map +1 -1
  95. package/dist/types/clients/BundleDataClient/utils/SuperstructUtils.d.ts +4 -4
  96. package/dist/types/clients/SpokePoolClient.d.ts +8 -0
  97. package/dist/types/clients/SpokePoolClient.d.ts.map +1 -1
  98. package/dist/types/clients/mocks/MockSpokePoolClient.d.ts +2 -1
  99. package/dist/types/clients/mocks/MockSpokePoolClient.d.ts.map +1 -1
  100. package/dist/types/constants.d.ts +1 -1
  101. package/dist/types/constants.d.ts.map +1 -1
  102. package/dist/types/providers/index.d.ts +1 -0
  103. package/dist/types/providers/index.d.ts.map +1 -1
  104. package/dist/types/providers/mockProvider.d.ts +24 -0
  105. package/dist/types/providers/mockProvider.d.ts.map +1 -0
  106. package/dist/types/utils/AddressUtils.d.ts +2 -0
  107. package/dist/types/utils/AddressUtils.d.ts.map +1 -1
  108. package/dist/types/utils/DepositUtils.d.ts +2 -1
  109. package/dist/types/utils/DepositUtils.d.ts.map +1 -1
  110. package/dist/types/utils/EventUtils.d.ts.map +1 -1
  111. package/dist/types/utils/NetworkUtils.d.ts +6 -0
  112. package/dist/types/utils/NetworkUtils.d.ts.map +1 -1
  113. package/dist/types/utils/SpokeUtils.d.ts +1 -0
  114. package/dist/types/utils/SpokeUtils.d.ts.map +1 -1
  115. package/dist/types/utils/common.d.ts +1 -0
  116. package/dist/types/utils/common.d.ts.map +1 -1
  117. package/package.json +1 -1
  118. package/src/clients/BundleDataClient/BundleDataClient.ts +413 -184
  119. package/src/clients/BundleDataClient/utils/DataworkerUtils.ts +0 -8
  120. package/src/clients/BundleDataClient/utils/FillUtils.ts +66 -2
  121. package/src/clients/SpokePoolClient.ts +19 -6
  122. package/src/clients/mocks/MockSpokePoolClient.ts +14 -0
  123. package/src/constants.ts +3 -3
  124. package/src/providers/index.ts +1 -0
  125. package/src/providers/mockProvider.ts +77 -0
  126. package/src/utils/AddressUtils.ts +26 -0
  127. package/src/utils/CachingUtils.ts +1 -1
  128. package/src/utils/DepositUtils.ts +14 -5
  129. package/src/utils/EventUtils.ts +29 -1
  130. package/src/utils/NetworkUtils.ts +11 -0
  131. package/src/utils/SpokeUtils.ts +27 -13
  132. package/src/utils/common.ts +2 -0
@@ -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 {
@@ -47,11 +50,14 @@ import {
47
50
  getRefundsFromBundle,
48
51
  getWidestPossibleExpectedBlockRange,
49
52
  isChainDisabled,
53
+ isEvmRepaymentValid,
50
54
  PoolRebalanceRoot,
51
55
  prettyPrintV3SpokePoolEvents,
52
56
  V3DepositWithBlock,
53
57
  V3FillWithBlock,
58
+ verifyFillRepayment,
54
59
  } from "./utils";
60
+ import { PRE_FILL_MIN_CONFIG_STORE_VERSION } from "../../constants";
55
61
 
56
62
  // max(uint256) - 1
57
63
  export const INFINITE_FILL_DEADLINE = bnUint32Max;
@@ -60,6 +66,10 @@ type DataCache = Record<string, Promise<LoadDataReturnValue>>;
60
66
 
61
67
  // V3 dictionary helper functions
62
68
  function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void {
69
+ // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise.
70
+ if (chainIsEvm(deposit.originChainId) && !isValidEvmAddress(deposit.depositor)) {
71
+ return;
72
+ }
63
73
  const { originChainId, inputToken } = deposit;
64
74
  if (!dict?.[originChainId]?.[inputToken]) {
65
75
  assign(dict, [originChainId, inputToken], []);
@@ -80,8 +90,14 @@ function updateBundleFillsV3(
80
90
  fill: V3FillWithBlock,
81
91
  lpFeePct: BigNumber,
82
92
  repaymentChainId: number,
83
- repaymentToken: string
93
+ repaymentToken: string,
94
+ repaymentAddress: string
84
95
  ): void {
96
+ // We shouldn't pass any unrepayable fills into this function, so we perform an extra safety check.
97
+ assert(
98
+ chainIsEvm(repaymentChainId) && isEvmRepaymentValid(fill, repaymentChainId),
99
+ "validatedBundleV3Fills dictionary should only contain fills with valid repayment information"
100
+ );
85
101
  if (!dict?.[repaymentChainId]?.[repaymentToken]) {
86
102
  assign(dict, [repaymentChainId, repaymentToken], {
87
103
  fills: [],
@@ -91,19 +107,19 @@ function updateBundleFillsV3(
91
107
  });
92
108
  }
93
109
 
94
- const bundleFill: BundleFillV3 = { ...fill, lpFeePct };
110
+ const bundleFill: BundleFillV3 = { ...fill, lpFeePct, relayer: repaymentAddress };
95
111
 
96
112
  // Add all fills, slow and fast, to dictionary.
97
113
  assign(dict, [repaymentChainId, repaymentToken, "fills"], [bundleFill]);
98
114
 
99
115
  // All fills update the bundle LP fees.
100
116
  const refundObj = dict[repaymentChainId][repaymentToken];
101
- const realizedLpFee = fill.inputAmount.mul(bundleFill.lpFeePct).div(fixedPointAdjustment);
117
+ const realizedLpFee = bundleFill.inputAmount.mul(bundleFill.lpFeePct).div(fixedPointAdjustment);
102
118
  refundObj.realizedLpFees = refundObj.realizedLpFees ? refundObj.realizedLpFees.add(realizedLpFee) : realizedLpFee;
103
119
 
104
120
  // Only fast fills get refunded.
105
- if (!isSlowFill(fill)) {
106
- const refundAmount = fill.inputAmount.mul(fixedPointAdjustment.sub(lpFeePct)).div(fixedPointAdjustment);
121
+ if (!isSlowFill(bundleFill)) {
122
+ const refundAmount = bundleFill.inputAmount.mul(fixedPointAdjustment.sub(lpFeePct)).div(fixedPointAdjustment);
107
123
  refundObj.totalRefundAmount = refundObj.totalRefundAmount
108
124
  ? refundObj.totalRefundAmount.add(refundAmount)
109
125
  : refundAmount;
@@ -111,10 +127,10 @@ function updateBundleFillsV3(
111
127
  // Instantiate dictionary if it doesn't exist.
112
128
  refundObj.refunds ??= {};
113
129
 
114
- if (refundObj.refunds[fill.relayer]) {
115
- refundObj.refunds[fill.relayer] = refundObj.refunds[fill.relayer].add(refundAmount);
130
+ if (refundObj.refunds[bundleFill.relayer]) {
131
+ refundObj.refunds[bundleFill.relayer] = refundObj.refunds[bundleFill.relayer].add(refundAmount);
116
132
  } else {
117
- refundObj.refunds[fill.relayer] = refundAmount;
133
+ refundObj.refunds[bundleFill.relayer] = refundAmount;
118
134
  }
119
135
  }
120
136
  }
@@ -131,6 +147,9 @@ function updateBundleExcessSlowFills(
131
147
  }
132
148
 
133
149
  function updateBundleSlowFills(dict: BundleSlowFills, deposit: V3DepositWithBlock & { lpFeePct: BigNumber }): void {
150
+ if (chainIsEvm(deposit.destinationChainId) && !isValidEvmAddress(deposit.recipient)) {
151
+ return;
152
+ }
134
153
  const { destinationChainId, outputToken } = deposit;
135
154
  if (!dict?.[destinationChainId]?.[outputToken]) {
136
155
  assign(dict, [destinationChainId, outputToken], []);
@@ -234,7 +253,6 @@ export class BundleDataClient {
234
253
  bundleData: prettyPrintV3SpokePoolEvents(
235
254
  bundleData.bundleDepositsV3,
236
255
  bundleData.bundleFillsV3,
237
- [], // Invalid fills are not persisted to Arweave.
238
256
  bundleData.bundleSlowFillsV3,
239
257
  bundleData.expiredDepositsToRefundV3,
240
258
  bundleData.unexecutableSlowFills
@@ -282,7 +300,7 @@ export class BundleDataClient {
282
300
  // so as not to affect this approximate refund count.
283
301
  const arweaveData = await this.loadArweaveData(bundleEvaluationBlockRanges);
284
302
  if (arweaveData === undefined) {
285
- combinedRefunds = this.getApproximateRefundsForBlockRange(chainIds, bundleEvaluationBlockRanges);
303
+ combinedRefunds = await this.getApproximateRefundsForBlockRange(chainIds, bundleEvaluationBlockRanges);
286
304
  } else {
287
305
  const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
288
306
  combinedRefunds = getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3);
@@ -303,50 +321,72 @@ export class BundleDataClient {
303
321
  }
304
322
 
305
323
  // @dev This helper function should probably be moved to the InventoryClient
306
- getApproximateRefundsForBlockRange(chainIds: number[], blockRanges: number[][]): CombinedRefunds {
324
+ async getApproximateRefundsForBlockRange(chainIds: number[], blockRanges: number[][]): Promise<CombinedRefunds> {
307
325
  const refundsForChain: CombinedRefunds = {};
308
326
  for (const chainId of chainIds) {
309
327
  if (this.spokePoolClients[chainId] === undefined) {
310
328
  continue;
311
329
  }
312
330
  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
- }
331
+ // @dev This function does not account for pre-fill refunds as it is optimized for speed. The way to detect
332
+ // pre-fill refunds is to load all deposits that are unmatched by fills in the spoke pool client's memory
333
+ // and then query the FillStatus on-chain, but that might slow this function down too much. For now, we
334
+ // will live with this expected inaccuracy as it should be small. The pre-fill would have to precede the deposit
335
+ // by more than the caller's event lookback window which is expected to be unlikely.
336
+ const fillsToCount = await filterAsync(this.spokePoolClients[chainId].getFills(), async (fill) => {
337
+ if (
338
+ fill.blockNumber < blockRanges[chainIndex][0] ||
339
+ fill.blockNumber > blockRanges[chainIndex][1] ||
340
+ isZeroValueFillOrSlowFillRequest(fill)
341
+ ) {
342
+ return false;
343
+ }
319
344
 
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(
345
+ // If origin spoke pool client isn't defined, we can't validate it.
346
+ if (this.spokePoolClients[fill.originChainId] === undefined) {
347
+ return false;
348
+ }
349
+ const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
350
+ const hasMatchingDeposit =
351
+ matchingDeposit !== undefined &&
352
+ this.getRelayHashFromEvent(fill) === this.getRelayHashFromEvent(matchingDeposit);
353
+ if (hasMatchingDeposit) {
354
+ const validRepayment = await verifyFillRepayment(
334
355
  fill,
335
- this.clients.hubPoolClient,
336
- blockRanges,
337
- this.chainIdListForBundleEvaluationBlockNumbers,
338
- matchingDeposit!.fromLiteChain // Use ! because we've already asserted that matchingDeposit is defined.
356
+ this.spokePoolClients[fill.destinationChainId].spokePool.provider,
357
+ matchingDeposit,
358
+ // @dev: to get valid repayment chain ID's, get all chain IDs for the bundle block range and remove
359
+ // disabled block ranges.
360
+ this.clients.configStoreClient
361
+ .getChainIdIndicesForBlock(blockRanges[0][1])
362
+ .filter((_chainId, i) => !isChainDisabled(blockRanges[i]))
339
363
  );
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
- });
364
+ if (!isDefined(validRepayment)) {
365
+ return false;
366
+ }
367
+ }
368
+ return hasMatchingDeposit;
369
+ });
370
+ fillsToCount.forEach((fill) => {
371
+ const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
372
+ assert(isDefined(matchingDeposit), "Deposit not found for fill.");
373
+ const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill(
374
+ fill,
375
+ this.clients.hubPoolClient,
376
+ blockRanges,
377
+ this.chainIdListForBundleEvaluationBlockNumbers,
378
+ matchingDeposit!.fromLiteChain // Use ! because we've already asserted that matchingDeposit is defined.
379
+ );
380
+ // Assume that lp fees are 0 for the sake of speed. In the future we could batch compute
381
+ // these or make hardcoded assumptions based on the origin-repayment chain direction. This might result
382
+ // in slight over estimations of refunds, but its not clear whether underestimating or overestimating is
383
+ // worst from the relayer's perspective.
384
+ const { relayer, inputAmount: refundAmount } = fill;
385
+ refundsForChain[chainToSendRefundTo] ??= {};
386
+ refundsForChain[chainToSendRefundTo][repaymentToken] ??= {};
387
+ const existingRefundAmount = refundsForChain[chainToSendRefundTo][repaymentToken][relayer] ?? bnZero;
388
+ refundsForChain[chainToSendRefundTo][repaymentToken][relayer] = existingRefundAmount.add(refundAmount);
389
+ });
350
390
  }
351
391
  return refundsForChain;
352
392
  }
@@ -390,7 +430,7 @@ export class BundleDataClient {
390
430
  async getLatestPoolRebalanceRoot(): Promise<{ root: PoolRebalanceRoot; blockRanges: number[][] }> {
391
431
  const { bundleData, blockRanges } = await this.getLatestProposedBundleData();
392
432
  const hubPoolClient = this.clients.hubPoolClient;
393
- const root = await _buildPoolRebalanceRoot(
433
+ const root = _buildPoolRebalanceRoot(
394
434
  hubPoolClient.latestBlockSearched,
395
435
  blockRanges[0][1],
396
436
  bundleData.bundleDepositsV3,
@@ -473,7 +513,7 @@ export class BundleDataClient {
473
513
  // ok for this use case.
474
514
  const arweaveData = await this.loadArweaveData(pendingBundleBlockRanges);
475
515
  if (arweaveData === undefined) {
476
- combinedRefunds.push(this.getApproximateRefundsForBlockRange(chainIds, pendingBundleBlockRanges));
516
+ combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, pendingBundleBlockRanges));
477
517
  } else {
478
518
  const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
479
519
  combinedRefunds.push(getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3));
@@ -488,7 +528,7 @@ export class BundleDataClient {
488
528
  // - Only look up fills sent after the pending bundle's end blocks
489
529
  // - Skip LP fee computations and just assume the relayer is being refunded the full deposit.inputAmount
490
530
  const start = performance.now();
491
- combinedRefunds.push(this.getApproximateRefundsForBlockRange(chainIds, widestBundleBlockRanges));
531
+ combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, widestBundleBlockRanges));
492
532
  this.logger.debug({
493
533
  at: "BundleDataClient#getNextBundleRefunds",
494
534
  message: `Loading approximate refunds for next bundle in ${Math.round(performance.now() - start) / 1000}s.`,
@@ -655,6 +695,7 @@ export class BundleDataClient {
655
695
  const bundleDepositsV3: BundleDepositsV3 = {}; // Deposits in bundle block range.
656
696
  const bundleFillsV3: BundleFillsV3 = {}; // Fills to refund in bundle block range.
657
697
  const bundleInvalidFillsV3: V3FillWithBlock[] = []; // Fills that are not valid in this bundle.
698
+ const bundleUnrepayableFillsV3: V3FillWithBlock[] = []; // Fills that are not repayable in this bundle.
658
699
  const bundleSlowFillsV3: BundleSlowFills = {}; // Deposits that we need to send slow fills
659
700
  // for in this bundle.
660
701
  const expiredDepositsToRefundV3: ExpiredDepositsToRefundV3 = {};
@@ -741,7 +782,7 @@ export class BundleDataClient {
741
782
  // Note: Since there are no partial fills in v3, there should only be one fill per relay hash.
742
783
  // Moreover, the SpokePool blocks multiple slow fill requests, so
743
784
  // there should also only be one slow fill request per relay hash.
744
- deposit?: V3DepositWithBlock;
785
+ deposits?: V3DepositWithBlock[];
745
786
  fill?: V3FillWithBlock;
746
787
  slowFillRequest?: SlowFillRequestWithBlock;
747
788
  };
@@ -752,6 +793,29 @@ export class BundleDataClient {
752
793
  const bundleDepositHashes: string[] = [];
753
794
  const olderDepositHashes: string[] = [];
754
795
 
796
+ const decodeBundleDepositHash = (depositHash: string): { relayDataHash: string; index: number } => {
797
+ const [relayDataHash, i] = depositHash.split("@");
798
+ return { relayDataHash, index: Number(i) };
799
+ };
800
+
801
+ // We use the following toggle to aid with the migration to pre-fills. The first bundle proposed using this
802
+ // pre-fill logic can double refund pre-fills that have already been filled in the last bundle, because the
803
+ // last bundle did not recognize a fill as a pre-fill. Therefore the developer should ensure that the version
804
+ // is bumped to the PRE_FILL_MIN_CONFIG_STORE_VERSION version before the first pre-fill bundle is proposed.
805
+ // To test the following bundle after this, the developer can set the FORCE_REFUND_PREFILLS environment variable
806
+ // to "true" simulate the bundle with pre-fill refunds.
807
+ // @todo Remove this logic once we have advanced sufficiently past the pre-fill migration.
808
+ const startBlockForMainnet = getBlockRangeForChain(
809
+ blockRangesForChains,
810
+ this.clients.hubPoolClient.chainId,
811
+ this.chainIdListForBundleEvaluationBlockNumbers
812
+ )[0];
813
+ const versionAtProposalBlock = this.clients.configStoreClient.getConfigStoreVersionForBlock(startBlockForMainnet);
814
+ const canRefundPrefills =
815
+ versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION || process.env.FORCE_REFUND_PREFILLS === "true";
816
+
817
+ // Prerequisite step: Load all deposit events from the current or older bundles into the v3RelayHashes dictionary
818
+ // for convenient matching with fills.
755
819
  let depositCounter = 0;
756
820
  for (const originChainId of allChainIds) {
757
821
  const originClient = spokePoolClients[originChainId];
@@ -770,12 +834,15 @@ export class BundleDataClient {
770
834
  }
771
835
  depositCounter++;
772
836
  const relayDataHash = this.getRelayHashFromEvent(deposit);
837
+
773
838
  if (!v3RelayHashes[relayDataHash]) {
774
839
  v3RelayHashes[relayDataHash] = {
775
- deposit: deposit,
840
+ deposits: [deposit],
776
841
  fill: undefined,
777
842
  slowFillRequest: undefined,
778
843
  };
844
+ } else {
845
+ v3RelayHashes[relayDataHash].deposits!.push(deposit);
779
846
  }
780
847
 
781
848
  // Once we've saved the deposit hash into v3RelayHashes, then we can exit early here if the inputAmount
@@ -786,11 +853,20 @@ export class BundleDataClient {
786
853
  return;
787
854
  }
788
855
 
856
+ // Evaluate all expired deposits after fetching fill statuses,
857
+ // since we can't know for certain whether an expired deposit was filled a long time ago.
858
+ const newBundleDepositHash = `${relayDataHash}@${v3RelayHashes[relayDataHash].deposits!.length - 1}`;
859
+ const decodedBundleDepositHash = decodeBundleDepositHash(newBundleDepositHash);
860
+ assert(
861
+ decodedBundleDepositHash.relayDataHash === relayDataHash &&
862
+ decodedBundleDepositHash.index === v3RelayHashes[relayDataHash].deposits!.length - 1,
863
+ "Not using correct bundle deposit hash key"
864
+ );
789
865
  if (deposit.blockNumber >= originChainBlockRange[0]) {
790
- bundleDepositHashes.push(relayDataHash);
866
+ bundleDepositHashes.push(newBundleDepositHash);
791
867
  updateBundleDepositsV3(bundleDepositsV3, deposit);
792
868
  } else if (deposit.blockNumber < originChainBlockRange[0]) {
793
- olderDepositHashes.push(relayDataHash);
869
+ olderDepositHashes.push(newBundleDepositHash);
794
870
  }
795
871
  });
796
872
  }
@@ -815,6 +891,7 @@ export class BundleDataClient {
815
891
 
816
892
  const destinationClient = spokePoolClients[destinationChainId];
817
893
  const destinationChainBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
894
+ const originChainBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds);
818
895
 
819
896
  // Keep track of fast fills that replaced slow fills, which we'll use to create "unexecutable" slow fills
820
897
  // if the slow fill request was sent in a prior bundle.
@@ -829,43 +906,81 @@ export class BundleDataClient {
829
906
  (fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueFillOrSlowFillRequest(fill)
830
907
  ),
831
908
  async (fill) => {
832
- const relayDataHash = this.getRelayHashFromEvent(fill);
833
909
  fillCounter++;
834
-
910
+ const relayDataHash = this.getRelayHashFromEvent(fill);
835
911
  if (v3RelayHashes[relayDataHash]) {
836
912
  if (!v3RelayHashes[relayDataHash].fill) {
837
913
  assert(
838
- isDefined(v3RelayHashes[relayDataHash].deposit),
914
+ isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
839
915
  "Deposit should exist in relay hash dictionary."
840
916
  );
841
917
  // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit,
842
- // so this fill is validated.
918
+ // so this fill can no longer be filled on-chain.
843
919
  v3RelayHashes[relayDataHash].fill = fill;
844
920
  if (fill.blockNumber >= destinationChainBlockRange[0]) {
845
- validatedBundleV3Fills.push({
846
- ...fill,
847
- quoteTimestamp: v3RelayHashes[relayDataHash].deposit!.quoteTimestamp, // ! due to assert above
848
- });
921
+ const fillToRefund = await verifyFillRepayment(
922
+ fill,
923
+ destinationClient.spokePool.provider,
924
+ v3RelayHashes[relayDataHash].deposits![0],
925
+ allChainIds
926
+ );
927
+ if (!isDefined(fillToRefund)) {
928
+ // We won't repay the fill but the depositor has received funds so we don't need to make a
929
+ // payment.
930
+ bundleUnrepayableFillsV3.push(fill);
931
+ // We don't return here yet because we still need to mark unexecutable slow fill leaves
932
+ // or duplicate deposits. However, we won't issue a fast fill refund.
933
+ } else {
934
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
935
+ validatedBundleV3Fills.push({
936
+ ...fillToRefund,
937
+ quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp, // ! due to assert above
938
+ });
939
+
940
+ // Now that we know this deposit has been filled on-chain, identify any duplicate deposits
941
+ // sent for this fill and refund them to the filler, because this value would not be paid out
942
+ // otherwise. These deposits can no longer expire and get refunded as an expired deposit,
943
+ // and they won't trigger a pre-fill refund because the fill is in this bundle.
944
+ // Pre-fill refunds only happen when deposits are sent in this bundle and the
945
+ // fill is from a prior bundle. Paying out the filler keeps the behavior consistent for how
946
+ // we deal with duplicate deposits regardless if the deposit is matched with a pre-fill or
947
+ // a current bundle fill.
948
+ const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1);
949
+ duplicateDeposits.forEach((duplicateDeposit) => {
950
+ // If fill is a slow fill, refund deposit to depositor, otherwise refund to filler.
951
+ if (isSlowFill(fill)) {
952
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
953
+ } else {
954
+ validatedBundleV3Fills.push({
955
+ ...fillToRefund,
956
+ quoteTimestamp: duplicateDeposit.quoteTimestamp,
957
+ });
958
+ }
959
+ });
960
+ }
961
+
849
962
  // If fill replaced a slow fill request, then mark it as one that might have created an
850
963
  // unexecutable slow fill. We can't know for sure until we check the slow fill request
851
964
  // events.
852
965
  // slow fill requests for deposits from or to lite chains are considered invalid
853
966
  if (
854
967
  fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
855
- _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposit!)
968
+ _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposits![0])
856
969
  ) {
857
970
  fastFillsReplacingSlowFills.push(relayDataHash);
858
971
  }
859
972
  }
973
+ } else {
974
+ throw new Error("Duplicate fill detected");
860
975
  }
861
976
  return;
862
977
  }
863
978
 
864
979
  // At this point, there is no relay hash dictionary entry for this fill, so we need to
865
- // instantiate the entry.
980
+ // instantiate the entry. We won't modify the fill.relayer until we match it with a deposit.
866
981
  v3RelayHashes[relayDataHash] = {
867
- deposit: undefined,
868
- fill: fill,
982
+ deposits: undefined,
983
+ fill,
869
984
  slowFillRequest: undefined,
870
985
  };
871
986
 
@@ -893,16 +1008,40 @@ export class BundleDataClient {
893
1008
  bundleInvalidFillsV3.push(fill);
894
1009
  } else {
895
1010
  const matchedDeposit = historicalDeposit.deposit;
896
- // @dev Since queryHistoricalDepositForFill validates the fill by checking individual
897
- // object property values against the deposit's, we
898
- // sanity check it here by comparing the full relay hashes. If there's an error here then the
899
- // historical deposit query is not working as expected.
900
- assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match.");
901
- validatedBundleV3Fills.push({
902
- ...fill,
903
- quoteTimestamp: matchedDeposit.quoteTimestamp,
904
- });
905
- v3RelayHashes[relayDataHash].deposit = matchedDeposit;
1011
+ // If deposit is in a following bundle, then this fill will have to be refunded once that deposit
1012
+ // is in the current bundle.
1013
+ if (matchedDeposit.blockNumber > originChainBlockRange[1]) {
1014
+ bundleInvalidFillsV3.push(fill);
1015
+ return;
1016
+ }
1017
+ v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
1018
+
1019
+ const fillToRefund = await verifyFillRepayment(
1020
+ fill,
1021
+ destinationClient.spokePool.provider,
1022
+ matchedDeposit,
1023
+ allChainIds
1024
+ );
1025
+ if (!isDefined(fillToRefund)) {
1026
+ bundleUnrepayableFillsV3.push(fill);
1027
+ // Don't return yet as we still need to mark down any unexecutable slow fill leaves
1028
+ // in case this fast fill replaced a slow fill request.
1029
+ } else {
1030
+ // @dev Since queryHistoricalDepositForFill validates the fill by checking individual
1031
+ // object property values against the deposit's, we
1032
+ // sanity check it here by comparing the full relay hashes. If there's an error here then the
1033
+ // historical deposit query is not working as expected.
1034
+ assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match.");
1035
+ validatedBundleV3Fills.push({
1036
+ ...fillToRefund,
1037
+ quoteTimestamp: matchedDeposit.quoteTimestamp,
1038
+ });
1039
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
1040
+
1041
+ // No need to check for duplicate deposits here since duplicate deposits with
1042
+ // infinite deadlines are impossible to send via unsafeDeposit().
1043
+ }
1044
+
906
1045
  // slow fill requests for deposits from or to lite chains are considered invalid
907
1046
  if (
908
1047
  fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
@@ -934,15 +1073,17 @@ export class BundleDataClient {
934
1073
  v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest;
935
1074
  if (v3RelayHashes[relayDataHash].fill) {
936
1075
  // If there is a fill matching the relay hash, then this slow fill request can't be used
937
- // to create a slow fill for a filled deposit.
1076
+ // to create a slow fill for a filled deposit. This takes advantage of the fact that
1077
+ // slow fill requests must precede fills, so if there is a matching fill for this request's
1078
+ // relay data, then this slow fill will be unexecutable.
938
1079
  return;
939
1080
  }
940
1081
  assert(
941
- isDefined(v3RelayHashes[relayDataHash].deposit),
1082
+ isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
942
1083
  "Deposit should exist in relay hash dictionary."
943
1084
  );
944
1085
  // The ! is safe here because we've already checked that the deposit exists in the relay hash dictionary.
945
- const matchedDeposit = v3RelayHashes[relayDataHash].deposit!;
1086
+ const matchedDeposit = v3RelayHashes[relayDataHash].deposits![0];
946
1087
 
947
1088
  // If there is no fill matching the relay hash, then this might be a valid slow fill request
948
1089
  // that we should produce a slow fill leaf for. Check if the slow fill request is in the
@@ -957,13 +1098,15 @@ export class BundleDataClient {
957
1098
  // so this slow fill request relay data is correct.
958
1099
  validatedBundleSlowFills.push(matchedDeposit);
959
1100
  }
1101
+ } else {
1102
+ throw new Error("Duplicate slow fill request detected.");
960
1103
  }
961
1104
  return;
962
1105
  }
963
1106
 
964
1107
  // Instantiate dictionary if there is neither a deposit nor fill matching it.
965
1108
  v3RelayHashes[relayDataHash] = {
966
- deposit: undefined,
1109
+ deposits: undefined,
967
1110
  fill: undefined,
968
1111
  slowFillRequest: slowFillRequest,
969
1112
  };
@@ -981,8 +1124,8 @@ export class BundleDataClient {
981
1124
  // found using such a method) because infinite fill deadlines cannot be produced from the unsafeDepositV3()
982
1125
  // function.
983
1126
  if (
984
- slowFillRequest.blockNumber >= destinationChainBlockRange[0] &&
985
- INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline)
1127
+ INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline) &&
1128
+ slowFillRequest.blockNumber >= destinationChainBlockRange[0]
986
1129
  ) {
987
1130
  const historicalDeposit = await queryHistoricalDepositForFill(originClient, slowFillRequest);
988
1131
  if (!historicalDeposit.found) {
@@ -990,6 +1133,11 @@ export class BundleDataClient {
990
1133
  return;
991
1134
  }
992
1135
  const matchedDeposit: V3DepositWithBlock = historicalDeposit.deposit;
1136
+ // If deposit is in a following bundle, then this slow fill request will have to be created
1137
+ // once that deposit is in the current bundle.
1138
+ if (matchedDeposit.blockNumber > originChainBlockRange[1]) {
1139
+ return;
1140
+ }
993
1141
  // @dev Since queryHistoricalDepositForFill validates the slow fill request by checking individual
994
1142
  // object property values against the deposit's, we
995
1143
  // sanity check it here by comparing the full relay hashes. If there's an error here then the
@@ -998,7 +1146,7 @@ export class BundleDataClient {
998
1146
  this.getRelayHashFromEvent(matchedDeposit) === relayDataHash,
999
1147
  "Deposit relay hashes should match."
1000
1148
  );
1001
- v3RelayHashes[relayDataHash].deposit = matchedDeposit;
1149
+ v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
1002
1150
 
1003
1151
  if (
1004
1152
  !_canCreateSlowFillLeaf(matchedDeposit) ||
@@ -1019,122 +1167,154 @@ export class BundleDataClient {
1019
1167
  // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund.
1020
1168
  // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf
1021
1169
  // 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 (fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) {
1050
- // If fill is in the current bundle then we can assume there is already a refund for it, so only
1051
- // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then
1052
- // 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
- });
1057
- }
1058
- return;
1059
- }
1170
+ await mapAsync(bundleDepositHashes, async (depositHash) => {
1171
+ const { relayDataHash, index } = decodeBundleDepositHash(depositHash);
1172
+ const { deposits, fill, slowFillRequest } = v3RelayHashes[relayDataHash];
1173
+ if (!deposits || deposits.length === 0) {
1174
+ throw new Error("Deposits should exist in relay hash dictionary.");
1175
+ }
1176
+ const deposit = deposits[index];
1177
+ if (!deposit) throw new Error("Deposit should exist in relay hash dictionary.");
1178
+ if (deposit.originChainId !== originChainId || deposit.destinationChainId !== destinationChainId) {
1179
+ return;
1180
+ }
1060
1181
 
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 (_depositIsExpired(deposit)) {
1182
+ // We are willing to refund a pre-fill multiple times for each duplicate deposit.
1183
+ // This is because a duplicate deposit for a pre-fill cannot get
1184
+ // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore
1185
+ // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out
1186
+ // the pre-filler.
1187
+
1188
+ // If fill exists in memory, then the only case in which we need to create a refund is if the
1189
+ // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits.
1190
+ if (fill) {
1191
+ if (canRefundPrefills && fill.blockNumber < destinationChainBlockRange[0]) {
1192
+ // If fill is in the current bundle then we can assume there is already a refund for it, so only
1193
+ // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then
1194
+ // we won't consider it, following the previous treatment of fills after the bundle block range.
1195
+ if (!isSlowFill(fill)) {
1196
+ const fillToRefund = await verifyFillRepayment(
1197
+ fill,
1198
+ destinationClient.spokePool.provider,
1199
+ v3RelayHashes[relayDataHash].deposits![0],
1200
+ allChainIds
1201
+ );
1202
+ if (!isDefined(fillToRefund)) {
1203
+ // We won't repay the fill but the depositor has received funds so we don't need to make a
1204
+ // payment.
1205
+ bundleUnrepayableFillsV3.push(fill);
1206
+ } else {
1207
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
1208
+ validatedBundleV3Fills.push({
1209
+ ...fillToRefund,
1210
+ quoteTimestamp: deposit.quoteTimestamp,
1211
+ });
1212
+ }
1213
+ } else {
1214
+ // Slow fills cannot result in refunds to a relayer to refund the deposit. Slow fills also
1215
+ // were created after the deposit was sent, so we can assume this deposit is a duplicate.
1068
1216
  updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1069
- } else if (
1070
- slowFillRequest.blockNumber < destinationChainBlockRange[0] &&
1071
- _canCreateSlowFillLeaf(deposit)
1072
- ) {
1073
- validatedBundleSlowFills.push(deposit);
1074
1217
  }
1075
- return;
1076
1218
  }
1219
+ return;
1220
+ }
1077
1221
 
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,
1222
+ // If a slow fill request exists in memory, then we know the deposit has not been filled because fills
1223
+ // must follow slow fill requests and we would have seen the fill already if it existed. Therefore,
1224
+ // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or
1225
+ // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request
1226
+ // took place in a prior bundle otherwise we would have already created a slow fill leaf for it.
1227
+ if (slowFillRequest) {
1228
+ if (_depositIsExpired(deposit)) {
1229
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1230
+ } else if (
1231
+ canRefundPrefills &&
1232
+ slowFillRequest.blockNumber < destinationChainBlockRange[0] &&
1233
+ _canCreateSlowFillLeaf(deposit) &&
1234
+ validatedBundleSlowFills.every((d) => this.getRelayHashFromEvent(d) !== relayDataHash)
1235
+ ) {
1236
+ validatedBundleSlowFills.push(deposit);
1237
+ }
1238
+ return;
1239
+ }
1240
+
1241
+ // So at this point in the code, there is no fill or slow fill request in memory for this deposit.
1242
+ // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf.
1243
+ // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles
1244
+ // because the spoke pool client lookback would have returned this entire bundle of events and stored
1245
+ // them into the relay hash dictionary.
1246
+ const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]);
1247
+
1248
+ // If deposit was filled, then we need to issue a refund for the fill and also any duplicate deposits
1249
+ // in the same bundle.
1250
+ if (fillStatus === FillStatus.Filled) {
1251
+ // We need to find the fill event to issue a refund to the right relayer and repayment chain,
1252
+ // or msg.sender if relayer address is invalid for the repayment chain. We don't need to
1253
+ // verify the fill block is before the bundle end block on the destination chain because
1254
+ // we queried the fillStatus at the end block. Therefore, if the fill took place after the end block,
1255
+ // then we wouldn't be in this branch of the code.
1256
+ const prefill = await this.findMatchingFillEvent(deposit, destinationClient);
1257
+ assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`);
1258
+ assert(this.getRelayHashFromEvent(prefill!) === relayDataHash, "Relay hashes should match.");
1259
+ if (canRefundPrefills) {
1260
+ const verifiedFill = await verifyFillRepayment(
1261
+ prefill!,
1262
+ destinationClient.spokePool.provider,
1091
1263
  deposit,
1092
- destinationClient.deploymentBlock,
1093
- destinationClient.latestBlockSearched
1094
- )) as unknown as FillWithBlock;
1095
- if (!isSlowFill(prefill)) {
1264
+ allChainIds
1265
+ );
1266
+ if (!isDefined(verifiedFill)) {
1267
+ bundleUnrepayableFillsV3.push(prefill!);
1268
+ } else if (!isSlowFill(verifiedFill)) {
1096
1269
  validatedBundleV3Fills.push({
1097
- ...prefill,
1270
+ ...verifiedFill!,
1098
1271
  quoteTimestamp: deposit.quoteTimestamp,
1099
1272
  });
1273
+ } else {
1274
+ // Slow fills cannot result in refunds to a relayer to refund the deposit. Slow fills also
1275
+ // were created after the deposit was sent, so we can assume this deposit is a duplicate.
1276
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1100
1277
  }
1101
1278
  }
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 (_depositIsExpired(deposit)) {
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
- }
1279
+ }
1280
+ // If deposit is not filled and its newly expired, we can create a deposit refund for it.
1281
+ // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because
1282
+ // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0
1283
+ // for example. Those should be included in this bundle of refunded deposits.
1284
+ else if (_depositIsExpired(deposit)) {
1285
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1286
+ }
1287
+ // If slow fill requested, then issue a slow fill leaf for the deposit.
1288
+ else if (
1289
+ fillStatus === FillStatus.RequestedSlowFill &&
1290
+ validatedBundleSlowFills.every((d) => this.getRelayHashFromEvent(d) !== relayDataHash)
1291
+ ) {
1292
+ // Input and Output tokens must be equivalent on the deposit for this to be slow filled.
1293
+ // Slow fill requests for deposits from or to lite chains are considered invalid
1294
+ if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) {
1295
+ // If deposit newly expired, then we can't create a slow fill leaf for it but we can
1296
+ // create a deposit refund for it.
1297
+ validatedBundleSlowFills.push(deposit);
1118
1298
  }
1119
1299
  }
1120
- );
1300
+ });
1121
1301
 
1122
1302
  // For all fills that came after a slow fill request, we can now check if the slow fill request
1123
1303
  // was a valid one and whether it was created in a previous bundle. If so, then it created a slow fill
1124
1304
  // leaf that is now unexecutable.
1125
1305
  fastFillsReplacingSlowFills.forEach((relayDataHash) => {
1126
- const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1306
+ const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1127
1307
  assert(
1128
1308
  fill?.relayExecutionInfo.fillType === FillType.ReplacedSlowFill,
1129
1309
  "Fill type should be ReplacedSlowFill."
1130
1310
  );
1131
1311
  // Needed for TSC - are implicitely checking that deposit exists by making it to this point.
1132
- if (!deposit) {
1312
+ if (!deposits || deposits.length < 1) {
1133
1313
  throw new Error("Deposit should exist in relay hash dictionary.");
1134
1314
  }
1135
1315
  // We should never push fast fills involving lite chains here because slow fill requests for them are invalid:
1136
1316
  assert(
1137
- _canCreateSlowFillLeaf(deposit),
1317
+ _canCreateSlowFillLeaf(deposits[0]),
1138
1318
  "fastFillsReplacingSlowFills should contain only deposits that can be slow filled"
1139
1319
  );
1140
1320
  const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
@@ -1144,7 +1324,7 @@ export class BundleDataClient {
1144
1324
  !slowFillRequest ||
1145
1325
  slowFillRequest.blockNumber < destinationBlockRange[0]
1146
1326
  ) {
1147
- validatedBundleUnexecutableSlowFills.push(deposit);
1327
+ validatedBundleUnexecutableSlowFills.push(deposits[0]);
1148
1328
  }
1149
1329
  });
1150
1330
  }
@@ -1158,10 +1338,14 @@ export class BundleDataClient {
1158
1338
  // For all deposits older than this bundle, we need to check if they expired in this bundle and if they did,
1159
1339
  // whether there was a slow fill created for it in a previous bundle that is now unexecutable and replaced
1160
1340
  // by a new expired deposit refund.
1161
- await forEachAsync(olderDepositHashes, async (relayDataHash) => {
1162
- const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1163
- assert(isDefined(deposit), "Deposit should exist in relay hash dictionary.");
1164
- const { destinationChainId } = deposit!;
1341
+ await forEachAsync(olderDepositHashes, async (depositHash) => {
1342
+ const { relayDataHash, index } = decodeBundleDepositHash(depositHash);
1343
+ const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1344
+ if (!deposits || deposits.length < 1) {
1345
+ throw new Error("Deposit should exist in relay hash dictionary.");
1346
+ }
1347
+ const deposit = deposits[index];
1348
+ const { destinationChainId } = deposit;
1165
1349
  const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
1166
1350
 
1167
1351
  // Only look for deposits that were mined before this bundle and that are newly expired.
@@ -1210,7 +1394,7 @@ export class BundleDataClient {
1210
1394
  validatedBundleV3Fills.length > 0
1211
1395
  ? this.clients.hubPoolClient.batchComputeRealizedLpFeePct(
1212
1396
  validatedBundleV3Fills.map((fill) => {
1213
- const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit;
1397
+ const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0];
1214
1398
  assert(isDefined(matchedDeposit), "Deposit should exist in relay hash dictionary.");
1215
1399
  const { chainToSendRefundTo: paymentChainId } = getRefundInformationFromFill(
1216
1400
  fill,
@@ -1254,7 +1438,7 @@ export class BundleDataClient {
1254
1438
  });
1255
1439
  v3FillLpFees.forEach(({ realizedLpFeePct }, idx) => {
1256
1440
  const fill = validatedBundleV3Fills[idx];
1257
- const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit;
1441
+ const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0];
1258
1442
  assert(isDefined(associatedDeposit), "Deposit should exist in relay hash dictionary.");
1259
1443
  const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill(
1260
1444
  fill,
@@ -1263,10 +1447,18 @@ export class BundleDataClient {
1263
1447
  chainIds,
1264
1448
  associatedDeposit!.fromLiteChain
1265
1449
  );
1266
- updateBundleFillsV3(bundleFillsV3, fill, realizedLpFeePct, chainToSendRefundTo, repaymentToken);
1450
+ updateBundleFillsV3(bundleFillsV3, fill, realizedLpFeePct, chainToSendRefundTo, repaymentToken, fill.relayer);
1267
1451
  });
1268
1452
  v3SlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => {
1269
1453
  const deposit = validatedBundleSlowFills[idx];
1454
+ // We should not create slow fill leaves for duplicate deposit hashes and we should only create a slow
1455
+ // fill leaf for the first deposit (the quote timestamp of the deposit determines the LP fee, so its
1456
+ // important we pick out the correct deposit). Deposits are pushed into validatedBundleSlowFills in ascending
1457
+ // order so the following slice will only match the first deposit.
1458
+ const relayDataHash = this.getRelayHashFromEvent(deposit);
1459
+ if (validatedBundleSlowFills.slice(0, idx).some((d) => this.getRelayHashFromEvent(d) === relayDataHash)) {
1460
+ return;
1461
+ }
1270
1462
  updateBundleSlowFills(bundleSlowFillsV3, { ...deposit, lpFeePct });
1271
1463
  });
1272
1464
  v3UnexecutableSlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => {
@@ -1277,7 +1469,6 @@ export class BundleDataClient {
1277
1469
  const v3SpokeEventsReadable = prettyPrintV3SpokePoolEvents(
1278
1470
  bundleDepositsV3,
1279
1471
  bundleFillsV3,
1280
- bundleInvalidFillsV3,
1281
1472
  bundleSlowFillsV3,
1282
1473
  expiredDepositsToRefundV3,
1283
1474
  unexecutableSlowFills
@@ -1292,6 +1483,15 @@ export class BundleDataClient {
1292
1483
  });
1293
1484
  }
1294
1485
 
1486
+ if (bundleUnrepayableFillsV3.length > 0) {
1487
+ this.logger.debug({
1488
+ at: "BundleDataClient#loadData",
1489
+ message: "Finished loading V3 spoke pool data and found some unrepayable V3 fills in range",
1490
+ blockRangesForChains,
1491
+ bundleUnrepayableFillsV3,
1492
+ });
1493
+ }
1494
+
1295
1495
  this.logger.debug({
1296
1496
  at: "BundleDataClient#loadDataFromScratch",
1297
1497
  message: `Computed bundle data in ${Math.round(performance.now() - start) / 1000}s.`,
@@ -1311,8 +1511,24 @@ export class BundleDataClient {
1311
1511
  // keccak256 hash of the relay data, which can be used as input into the on-chain `fillStatuses()` function in the
1312
1512
  // spoke pool contract. However, this internal function is used to uniquely identify a bridging event
1313
1513
  // for speed since its easier to build a string from the event data than to hash it.
1314
- private getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string {
1315
- 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}`;
1514
+ protected getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string {
1515
+ return `${event.depositor}-${event.recipient}-${event.exclusiveRelayer}-${event.inputToken}-${event.outputToken}-${
1516
+ event.inputAmount
1517
+ }-${event.outputAmount}-${event.originChainId}-${event.depositId.toString()}-${event.fillDeadline}-${
1518
+ event.exclusivityDeadline
1519
+ }-${event.message}-${event.destinationChainId}`;
1520
+ }
1521
+
1522
+ protected async findMatchingFillEvent(
1523
+ deposit: DepositWithBlock,
1524
+ spokePoolClient: SpokePoolClient
1525
+ ): Promise<FillWithBlock | undefined> {
1526
+ return await findFillEvent(
1527
+ spokePoolClient.spokePool,
1528
+ deposit,
1529
+ spokePoolClient.deploymentBlock,
1530
+ spokePoolClient.latestBlockSearched
1531
+ );
1316
1532
  }
1317
1533
 
1318
1534
  async getBundleBlockTimestamps(
@@ -1340,13 +1556,26 @@ export class BundleDataClient {
1340
1556
  // will usually be called in production with block ranges that were validated by
1341
1557
  // DataworkerUtils.blockRangesAreInvalidForSpokeClients.
1342
1558
  const startBlockForChain = Math.min(_startBlockForChain, spokePoolClient.latestBlockSearched);
1343
- const endBlockForChain = Math.min(_endBlockForChain, spokePoolClient.latestBlockSearched);
1344
- const [startTime, endTime] = [
1559
+ // @dev Add 1 to the bundle end block. The thinking here is that there can be a gap between
1560
+ // block timestamps in subsequent blocks. The bundle data client assumes that fill deadlines expire
1561
+ // in exactly one bundle, therefore we must make sure that the bundle block timestamp for one bundle's
1562
+ // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way
1563
+ // there are no gaps in block timestamps between bundles.
1564
+ const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched);
1565
+ const [startTime, _endTime] = [
1345
1566
  await spokePoolClient.getTimestampForBlock(startBlockForChain),
1346
1567
  await spokePoolClient.getTimestampForBlock(endBlockForChain),
1347
1568
  ];
1569
+ // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also
1570
+ // no overlap, subtract 1 from the end time.
1571
+ const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0;
1572
+ const endTime = Math.max(0, _endTime - endBlockDelta);
1573
+
1348
1574
  // Sanity checks:
1349
- assert(endTime >= startTime, "End time should be greater than start time.");
1575
+ assert(
1576
+ endTime >= startTime,
1577
+ `End time for block ${endBlockForChain} should be greater than start time for block ${startBlockForChain}: ${endTime} >= ${startTime}.`
1578
+ );
1350
1579
  assert(
1351
1580
  startBlockForChain === 0 || startTime > 0,
1352
1581
  "Start timestamp must be greater than 0 if the start block is greater than 0."