@across-protocol/sdk 4.0.0-beta.9 → 4.0.1

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 +345 -187
  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 +10 -1
  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 +0 -1
  18. package/dist/cjs/constants.js +1 -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 +1 -0
  27. package/dist/cjs/utils/AddressUtils.js +6 -1
  28. package/dist/cjs/utils/AddressUtils.js.map +1 -1
  29. package/dist/cjs/utils/BlockUtils.js +1 -0
  30. package/dist/cjs/utils/BlockUtils.js.map +1 -1
  31. package/dist/cjs/utils/DepositUtils.js +1 -1
  32. package/dist/cjs/utils/DepositUtils.js.map +1 -1
  33. package/dist/cjs/utils/EventUtils.js +21 -0
  34. package/dist/cjs/utils/EventUtils.js.map +1 -1
  35. package/dist/cjs/utils/NetworkUtils.js +2 -1
  36. package/dist/cjs/utils/NetworkUtils.js.map +1 -1
  37. package/dist/cjs/utils/SpokeUtils.d.ts +1 -0
  38. package/dist/cjs/utils/SpokeUtils.js +15 -8
  39. package/dist/cjs/utils/SpokeUtils.js.map +1 -1
  40. package/dist/cjs/utils/common.d.ts +1 -0
  41. package/dist/cjs/utils/common.js +2 -1
  42. package/dist/cjs/utils/common.js.map +1 -1
  43. package/dist/esm/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  44. package/dist/esm/clients/BundleDataClient/BundleDataClient.js +448 -281
  45. package/dist/esm/clients/BundleDataClient/BundleDataClient.js.map +1 -1
  46. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.d.ts +1 -2
  47. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.js +2 -3
  48. package/dist/esm/clients/BundleDataClient/utils/DataworkerUtils.js.map +1 -1
  49. package/dist/esm/clients/BundleDataClient/utils/FillUtils.d.ts +5 -1
  50. package/dist/esm/clients/BundleDataClient/utils/FillUtils.js +54 -1
  51. package/dist/esm/clients/BundleDataClient/utils/FillUtils.js.map +1 -1
  52. package/dist/esm/clients/BundleDataClient/utils/SuperstructUtils.d.ts +4 -4
  53. package/dist/esm/clients/SpokePoolClient.d.ts +8 -0
  54. package/dist/esm/clients/SpokePoolClient.js +17 -1
  55. package/dist/esm/clients/SpokePoolClient.js.map +1 -1
  56. package/dist/esm/clients/mocks/MockSpokePoolClient.d.ts +2 -1
  57. package/dist/esm/clients/mocks/MockSpokePoolClient.js +11 -0
  58. package/dist/esm/clients/mocks/MockSpokePoolClient.js.map +1 -1
  59. package/dist/esm/constants.d.ts +0 -1
  60. package/dist/esm/constants.js +0 -1
  61. package/dist/esm/constants.js.map +1 -1
  62. package/dist/esm/providers/index.d.ts +1 -0
  63. package/dist/esm/providers/index.js +2 -0
  64. package/dist/esm/providers/index.js.map +1 -1
  65. package/dist/esm/providers/mockProvider.d.ts +23 -0
  66. package/dist/esm/providers/mockProvider.js +73 -0
  67. package/dist/esm/providers/mockProvider.js.map +1 -0
  68. package/dist/esm/utils/AddressUtils.d.ts +1 -0
  69. package/dist/esm/utils/AddressUtils.js +9 -0
  70. package/dist/esm/utils/AddressUtils.js.map +1 -1
  71. package/dist/esm/utils/BlockUtils.js +1 -0
  72. package/dist/esm/utils/BlockUtils.js.map +1 -1
  73. package/dist/esm/utils/DepositUtils.js +2 -2
  74. package/dist/esm/utils/DepositUtils.js.map +1 -1
  75. package/dist/esm/utils/EventUtils.js +29 -1
  76. package/dist/esm/utils/EventUtils.js.map +1 -1
  77. package/dist/esm/utils/NetworkUtils.js +2 -1
  78. package/dist/esm/utils/NetworkUtils.js.map +1 -1
  79. package/dist/esm/utils/SpokeUtils.d.ts +1 -0
  80. package/dist/esm/utils/SpokeUtils.js +13 -7
  81. package/dist/esm/utils/SpokeUtils.js.map +1 -1
  82. package/dist/esm/utils/abi/typechain/Multicall3.d.ts +4 -1
  83. package/dist/esm/utils/abi/typechain/factories/Multicall3__factory.js.map +1 -1
  84. package/dist/esm/utils/common.d.ts +1 -0
  85. package/dist/esm/utils/common.js +1 -0
  86. package/dist/esm/utils/common.js.map +1 -1
  87. package/dist/types/clients/BundleDataClient/BundleDataClient.d.ts +5 -4
  88. package/dist/types/clients/BundleDataClient/BundleDataClient.d.ts.map +1 -1
  89. package/dist/types/clients/BundleDataClient/utils/DataworkerUtils.d.ts +1 -2
  90. package/dist/types/clients/BundleDataClient/utils/DataworkerUtils.d.ts.map +1 -1
  91. package/dist/types/clients/BundleDataClient/utils/FillUtils.d.ts +5 -1
  92. package/dist/types/clients/BundleDataClient/utils/FillUtils.d.ts.map +1 -1
  93. package/dist/types/clients/BundleDataClient/utils/SuperstructUtils.d.ts +4 -4
  94. package/dist/types/clients/SpokePoolClient.d.ts +8 -0
  95. package/dist/types/clients/SpokePoolClient.d.ts.map +1 -1
  96. package/dist/types/clients/mocks/MockSpokePoolClient.d.ts +2 -1
  97. package/dist/types/clients/mocks/MockSpokePoolClient.d.ts.map +1 -1
  98. package/dist/types/constants.d.ts +0 -1
  99. package/dist/types/constants.d.ts.map +1 -1
  100. package/dist/types/providers/index.d.ts +1 -0
  101. package/dist/types/providers/index.d.ts.map +1 -1
  102. package/dist/types/providers/mockProvider.d.ts +24 -0
  103. package/dist/types/providers/mockProvider.d.ts.map +1 -0
  104. package/dist/types/utils/AddressUtils.d.ts +1 -0
  105. package/dist/types/utils/AddressUtils.d.ts.map +1 -1
  106. package/dist/types/utils/BlockUtils.d.ts.map +1 -1
  107. package/dist/types/utils/EventUtils.d.ts.map +1 -1
  108. package/dist/types/utils/NetworkUtils.d.ts.map +1 -1
  109. package/dist/types/utils/SpokeUtils.d.ts +1 -0
  110. package/dist/types/utils/SpokeUtils.d.ts.map +1 -1
  111. package/dist/types/utils/abi/typechain/Multicall3.d.ts +4 -1
  112. package/dist/types/utils/abi/typechain/Multicall3.d.ts.map +1 -1
  113. package/dist/types/utils/abi/typechain/common.d.ts.map +1 -1
  114. package/dist/types/utils/abi/typechain/factories/Multicall3__factory.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 +3 -3
  118. package/src/clients/BundleDataClient/BundleDataClient.ts +406 -249
  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 +16 -3
  122. package/src/clients/mocks/MockSpokePoolClient.ts +14 -0
  123. package/src/constants.ts +0 -2
  124. package/src/providers/index.ts +1 -0
  125. package/src/providers/mockProvider.ts +77 -0
  126. package/src/utils/AddressUtils.ts +10 -0
  127. package/src/utils/BlockUtils.ts +1 -0
  128. package/src/utils/DepositUtils.ts +2 -2
  129. package/src/utils/EventUtils.ts +29 -1
  130. package/src/utils/NetworkUtils.ts +2 -1
  131. package/src/utils/SpokeUtils.ts +21 -8
  132. package/src/utils/common.ts +2 -0
@@ -33,6 +33,7 @@ import {
33
33
  getImpliedBundleBlockRanges,
34
34
  isSlowFill,
35
35
  mapAsync,
36
+ filterAsync,
36
37
  bnUint32Max,
37
38
  isZeroValueDeposit,
38
39
  findFillEvent,
@@ -49,10 +50,12 @@ import {
49
50
  getRefundsFromBundle,
50
51
  getWidestPossibleExpectedBlockRange,
51
52
  isChainDisabled,
53
+ isEvmRepaymentValid,
52
54
  PoolRebalanceRoot,
53
55
  prettyPrintV3SpokePoolEvents,
54
56
  V3DepositWithBlock,
55
57
  V3FillWithBlock,
58
+ verifyFillRepayment,
56
59
  } from "./utils";
57
60
  import { PRE_FILL_MIN_CONFIG_STORE_VERSION } from "../../constants";
58
61
 
@@ -87,12 +90,14 @@ function updateBundleFillsV3(
87
90
  fill: V3FillWithBlock,
88
91
  lpFeePct: BigNumber,
89
92
  repaymentChainId: number,
90
- repaymentToken: string
93
+ repaymentToken: string,
94
+ repaymentAddress: string
91
95
  ): void {
92
- // It is impossible to refund a deposit if the repayment chain is EVM and the relayer is a non-evm address.
93
- if (chainIsEvm(fill.repaymentChainId) && !isValidEvmAddress(fill.relayer)) {
94
- return;
95
- }
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
+ );
96
101
  if (!dict?.[repaymentChainId]?.[repaymentToken]) {
97
102
  assign(dict, [repaymentChainId, repaymentToken], {
98
103
  fills: [],
@@ -102,19 +107,19 @@ function updateBundleFillsV3(
102
107
  });
103
108
  }
104
109
 
105
- const bundleFill: BundleFillV3 = { ...fill, lpFeePct };
110
+ const bundleFill: BundleFillV3 = { ...fill, lpFeePct, relayer: repaymentAddress };
106
111
 
107
112
  // Add all fills, slow and fast, to dictionary.
108
113
  assign(dict, [repaymentChainId, repaymentToken, "fills"], [bundleFill]);
109
114
 
110
115
  // All fills update the bundle LP fees.
111
116
  const refundObj = dict[repaymentChainId][repaymentToken];
112
- const realizedLpFee = fill.inputAmount.mul(bundleFill.lpFeePct).div(fixedPointAdjustment);
117
+ const realizedLpFee = bundleFill.inputAmount.mul(bundleFill.lpFeePct).div(fixedPointAdjustment);
113
118
  refundObj.realizedLpFees = refundObj.realizedLpFees ? refundObj.realizedLpFees.add(realizedLpFee) : realizedLpFee;
114
119
 
115
120
  // Only fast fills get refunded.
116
- if (!isSlowFill(fill)) {
117
- 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);
118
123
  refundObj.totalRefundAmount = refundObj.totalRefundAmount
119
124
  ? refundObj.totalRefundAmount.add(refundAmount)
120
125
  : refundAmount;
@@ -122,10 +127,10 @@ function updateBundleFillsV3(
122
127
  // Instantiate dictionary if it doesn't exist.
123
128
  refundObj.refunds ??= {};
124
129
 
125
- if (refundObj.refunds[fill.relayer]) {
126
- 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);
127
132
  } else {
128
- refundObj.refunds[fill.relayer] = refundAmount;
133
+ refundObj.refunds[bundleFill.relayer] = refundAmount;
129
134
  }
130
135
  }
131
136
  }
@@ -142,6 +147,9 @@ function updateBundleExcessSlowFills(
142
147
  }
143
148
 
144
149
  function updateBundleSlowFills(dict: BundleSlowFills, deposit: V3DepositWithBlock & { lpFeePct: BigNumber }): void {
150
+ if (chainIsEvm(deposit.destinationChainId) && !isValidEvmAddress(deposit.recipient)) {
151
+ return;
152
+ }
145
153
  const { destinationChainId, outputToken } = deposit;
146
154
  if (!dict?.[destinationChainId]?.[outputToken]) {
147
155
  assign(dict, [destinationChainId, outputToken], []);
@@ -245,7 +253,6 @@ export class BundleDataClient {
245
253
  bundleData: prettyPrintV3SpokePoolEvents(
246
254
  bundleData.bundleDepositsV3,
247
255
  bundleData.bundleFillsV3,
248
- [], // Invalid fills are not persisted to Arweave.
249
256
  bundleData.bundleSlowFillsV3,
250
257
  bundleData.expiredDepositsToRefundV3,
251
258
  bundleData.unexecutableSlowFills
@@ -293,7 +300,7 @@ export class BundleDataClient {
293
300
  // so as not to affect this approximate refund count.
294
301
  const arweaveData = await this.loadArweaveData(bundleEvaluationBlockRanges);
295
302
  if (arweaveData === undefined) {
296
- combinedRefunds = this.getApproximateRefundsForBlockRange(chainIds, bundleEvaluationBlockRanges);
303
+ combinedRefunds = await this.getApproximateRefundsForBlockRange(chainIds, bundleEvaluationBlockRanges);
297
304
  } else {
298
305
  const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
299
306
  combinedRefunds = getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3);
@@ -314,56 +321,72 @@ export class BundleDataClient {
314
321
  }
315
322
 
316
323
  // @dev This helper function should probably be moved to the InventoryClient
317
- getApproximateRefundsForBlockRange(chainIds: number[], blockRanges: number[][]): CombinedRefunds {
324
+ async getApproximateRefundsForBlockRange(chainIds: number[], blockRanges: number[][]): Promise<CombinedRefunds> {
318
325
  const refundsForChain: CombinedRefunds = {};
319
326
  for (const chainId of chainIds) {
320
327
  if (this.spokePoolClients[chainId] === undefined) {
321
328
  continue;
322
329
  }
323
330
  const chainIndex = chainIds.indexOf(chainId);
324
-
325
- // @todo This function does not account for pre-fill refunds as it is optimized for speed. The way to detect
331
+ // @dev This function does not account for pre-fill refunds as it is optimized for speed. The way to detect
326
332
  // pre-fill refunds is to load all deposits that are unmatched by fills in the spoke pool client's memory
327
333
  // and then query the FillStatus on-chain, but that might slow this function down too much. For now, we
328
334
  // will live with this expected inaccuracy as it should be small. The pre-fill would have to precede the deposit
329
335
  // by more than the caller's event lookback window which is expected to be unlikely.
330
- this.spokePoolClients[chainId]
331
- .getFills()
332
- .filter((fill) => {
333
- if (fill.blockNumber < blockRanges[chainIndex][0] || fill.blockNumber > blockRanges[chainIndex][1]) {
334
- return false;
335
- }
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
+ }
336
344
 
337
- // If origin spoke pool client isn't defined, we can't validate it.
338
- if (this.spokePoolClients[fill.originChainId] === undefined) {
339
- return false;
340
- }
341
- const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
342
- const hasMatchingDeposit =
343
- matchingDeposit !== undefined &&
344
- this.getRelayHashFromEvent(fill) === this.getRelayHashFromEvent(matchingDeposit);
345
- return hasMatchingDeposit;
346
- })
347
- .forEach((fill) => {
348
- const matchingDeposit = this.spokePoolClients[fill.originChainId].getDeposit(fill.depositId);
349
- assert(isDefined(matchingDeposit), "Deposit not found for fill.");
350
- 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(
351
355
  fill,
352
- this.clients.hubPoolClient,
353
- blockRanges,
354
- this.chainIdListForBundleEvaluationBlockNumbers,
355
- 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]))
356
363
  );
357
- // Assume that lp fees are 0 for the sake of speed. In the future we could batch compute
358
- // these or make hardcoded assumptions based on the origin-repayment chain direction. This might result
359
- // in slight over estimations of refunds, but its not clear whether underestimating or overestimating is
360
- // worst from the relayer's perspective.
361
- const { relayer, inputAmount: refundAmount } = fill;
362
- refundsForChain[chainToSendRefundTo] ??= {};
363
- refundsForChain[chainToSendRefundTo][repaymentToken] ??= {};
364
- const existingRefundAmount = refundsForChain[chainToSendRefundTo][repaymentToken][relayer] ?? bnZero;
365
- refundsForChain[chainToSendRefundTo][repaymentToken][relayer] = existingRefundAmount.add(refundAmount);
366
- });
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
+ });
367
390
  }
368
391
  return refundsForChain;
369
392
  }
@@ -407,7 +430,7 @@ export class BundleDataClient {
407
430
  async getLatestPoolRebalanceRoot(): Promise<{ root: PoolRebalanceRoot; blockRanges: number[][] }> {
408
431
  const { bundleData, blockRanges } = await this.getLatestProposedBundleData();
409
432
  const hubPoolClient = this.clients.hubPoolClient;
410
- const root = await _buildPoolRebalanceRoot(
433
+ const root = _buildPoolRebalanceRoot(
411
434
  hubPoolClient.latestBlockSearched,
412
435
  blockRanges[0][1],
413
436
  bundleData.bundleDepositsV3,
@@ -490,7 +513,7 @@ export class BundleDataClient {
490
513
  // ok for this use case.
491
514
  const arweaveData = await this.loadArweaveData(pendingBundleBlockRanges);
492
515
  if (arweaveData === undefined) {
493
- combinedRefunds.push(this.getApproximateRefundsForBlockRange(chainIds, pendingBundleBlockRanges));
516
+ combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, pendingBundleBlockRanges));
494
517
  } else {
495
518
  const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
496
519
  combinedRefunds.push(getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3));
@@ -505,7 +528,7 @@ export class BundleDataClient {
505
528
  // - Only look up fills sent after the pending bundle's end blocks
506
529
  // - Skip LP fee computations and just assume the relayer is being refunded the full deposit.inputAmount
507
530
  const start = performance.now();
508
- combinedRefunds.push(this.getApproximateRefundsForBlockRange(chainIds, widestBundleBlockRanges));
531
+ combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, widestBundleBlockRanges));
509
532
  this.logger.debug({
510
533
  at: "BundleDataClient#getNextBundleRefunds",
511
534
  message: `Loading approximate refunds for next bundle in ${Math.round(performance.now() - start) / 1000}s.`,
@@ -672,6 +695,8 @@ export class BundleDataClient {
672
695
  const bundleDepositsV3: BundleDepositsV3 = {}; // Deposits in bundle block range.
673
696
  const bundleFillsV3: BundleFillsV3 = {}; // Fills to refund in bundle block range.
674
697
  const bundleInvalidFillsV3: V3FillWithBlock[] = []; // Fills that are not valid in this bundle.
698
+ const bundleUnrepayableFillsV3: V3FillWithBlock[] = []; // Fills that are not repayable in this bundle.
699
+ const bundleInvalidSlowFillRequests: SlowFillRequestWithBlock[] = []; // Slow fill requests that are not valid in this bundle.
675
700
  const bundleSlowFillsV3: BundleSlowFills = {}; // Deposits that we need to send slow fills
676
701
  // for in this bundle.
677
702
  const expiredDepositsToRefundV3: ExpiredDepositsToRefundV3 = {};
@@ -758,7 +783,7 @@ export class BundleDataClient {
758
783
  // Note: Since there are no partial fills in v3, there should only be one fill per relay hash.
759
784
  // Moreover, the SpokePool blocks multiple slow fill requests, so
760
785
  // there should also only be one slow fill request per relay hash.
761
- deposit?: V3DepositWithBlock;
786
+ deposits?: V3DepositWithBlock[];
762
787
  fill?: V3FillWithBlock;
763
788
  slowFillRequest?: SlowFillRequestWithBlock;
764
789
  };
@@ -769,6 +794,11 @@ export class BundleDataClient {
769
794
  const bundleDepositHashes: string[] = [];
770
795
  const olderDepositHashes: string[] = [];
771
796
 
797
+ const decodeBundleDepositHash = (depositHash: string): { relayDataHash: string; index: number } => {
798
+ const [relayDataHash, i] = depositHash.split("@");
799
+ return { relayDataHash, index: Number(i) };
800
+ };
801
+
772
802
  // We use the following toggle to aid with the migration to pre-fills. The first bundle proposed using this
773
803
  // pre-fill logic can double refund pre-fills that have already been filled in the last bundle, because the
774
804
  // last bundle did not recognize a fill as a pre-fill. Therefore the developer should ensure that the version
@@ -785,7 +815,8 @@ export class BundleDataClient {
785
815
  const canRefundPrefills =
786
816
  versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION || process.env.FORCE_REFUND_PREFILLS === "true";
787
817
 
788
- let depositCounter = 0;
818
+ // Prerequisite step: Load all deposit events from the current or older bundles into the v3RelayHashes dictionary
819
+ // for convenient matching with fills.
789
820
  for (const originChainId of allChainIds) {
790
821
  const originClient = spokePoolClients[originChainId];
791
822
  const originChainBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds);
@@ -795,50 +826,59 @@ export class BundleDataClient {
795
826
  continue;
796
827
  }
797
828
  originClient.getDepositsForDestinationChainWithDuplicates(destinationChainId).forEach((deposit) => {
798
- // Only evaluate deposits that are in this bundle or in previous bundles. This means we cannot issue fill
799
- // refunds or slow fills here for deposits that are in future bundles (i.e. "pre-fills"). Instead, we'll
800
- // evaluate these pre-fills once the deposit is inside the "current" bundle block range.
801
829
  if (deposit.blockNumber > originChainBlockRange[1] || isZeroValueDeposit(deposit)) {
802
830
  return;
803
831
  }
804
- depositCounter++;
805
832
  const relayDataHash = this.getRelayHashFromEvent(deposit);
806
833
 
807
- // Duplicate deposits are treated like normal deposits.
808
834
  if (!v3RelayHashes[relayDataHash]) {
809
835
  v3RelayHashes[relayDataHash] = {
810
- deposit: deposit,
836
+ deposits: [deposit],
811
837
  fill: undefined,
812
838
  slowFillRequest: undefined,
813
839
  };
840
+ } else {
841
+ v3RelayHashes[relayDataHash].deposits!.push(deposit);
814
842
  }
815
843
 
816
- // Once we've saved the deposit hash into v3RelayHashes, then we can exit early here if the inputAmount
817
- // is 0 because there can be no expired amount to refund and no unexecutable slow fill amount to return
818
- // if this deposit did expire. Input amount can only be zero at this point if the message is non-empty,
819
- // but the message doesn't matter for expired deposits and unexecutable slow fills.
820
- if (deposit.inputAmount.eq(0)) {
821
- return;
822
- }
823
-
824
- // Evaluate all expired deposits after fetching fill statuses,
825
- // since we can't know for certain whether an expired deposit was filled a long time ago.
844
+ // Account for duplicate deposits by concatenating the relayDataHash with the count of the number of times
845
+ // we have seen it so far.
846
+ const newBundleDepositHash = `${relayDataHash}@${v3RelayHashes[relayDataHash].deposits!.length - 1}`;
847
+ const decodedBundleDepositHash = decodeBundleDepositHash(newBundleDepositHash);
848
+ assert(
849
+ decodedBundleDepositHash.relayDataHash === relayDataHash &&
850
+ decodedBundleDepositHash.index === v3RelayHashes[relayDataHash].deposits!.length - 1,
851
+ "Not using correct bundle deposit hash key"
852
+ );
826
853
  if (deposit.blockNumber >= originChainBlockRange[0]) {
827
- bundleDepositHashes.push(relayDataHash);
854
+ bundleDepositHashes.push(newBundleDepositHash);
828
855
  updateBundleDepositsV3(bundleDepositsV3, deposit);
829
856
  } else if (deposit.blockNumber < originChainBlockRange[0]) {
830
- olderDepositHashes.push(relayDataHash);
857
+ olderDepositHashes.push(newBundleDepositHash);
831
858
  }
832
859
  });
833
860
  }
834
861
  }
835
862
  this.logger.debug({
836
863
  at: "BundleDataClient#loadData",
837
- message: `Processed ${depositCounter} deposits in ${performance.now() - start}ms.`,
864
+ message: `Processed ${bundleDepositHashes.length + olderDepositHashes.length} deposits in ${
865
+ performance.now() - start
866
+ }ms.`,
838
867
  });
839
868
  start = performance.now();
840
869
 
841
- // Process fills now that we've populated relay hash dictionary with deposits:
870
+ // Process fills and maintain the following the invariants:
871
+ // - Every single fill whose type is not SlowFill in the bundle block range whose relay data matches
872
+ // with a deposit in the same or an older range produces a refund to the filler,
873
+ // unless the specified filler address cannot be repaid on the repayment chain.
874
+ // - Fills can match with duplicate deposits, so for every matched fill whose type is not SlowFill
875
+ // in the bundle block range, produce a refund to the filler for each matched deposit.
876
+ // - For every SlowFill in the block range that matches with multiple deposits, produce a refund to the depositor
877
+ // for every deposit except except the first.
878
+
879
+ // Assumptions about fills:
880
+ // - Duplicate fills for the same relay data hash are impossible to send.
881
+ // - Fills can only be sent before the deposit's fillDeadline.
842
882
  const validatedBundleV3Fills: (V3FillWithBlock & { quoteTimestamp: number })[] = [];
843
883
  const validatedBundleSlowFills: V3DepositWithBlock[] = [];
844
884
  const validatedBundleUnexecutableSlowFills: V3DepositWithBlock[] = [];
@@ -852,9 +892,8 @@ export class BundleDataClient {
852
892
 
853
893
  const destinationClient = spokePoolClients[destinationChainId];
854
894
  const destinationChainBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
895
+ const originChainBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds);
855
896
 
856
- // Keep track of fast fills that replaced slow fills, which we'll use to create "unexecutable" slow fills
857
- // if the slow fill request was sent in a prior bundle.
858
897
  const fastFillsReplacingSlowFills: string[] = [];
859
898
  await forEachAsync(
860
899
  destinationClient
@@ -866,47 +905,79 @@ export class BundleDataClient {
866
905
  (fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueFillOrSlowFillRequest(fill)
867
906
  ),
868
907
  async (fill) => {
869
- const relayDataHash = this.getRelayHashFromEvent(fill);
870
908
  fillCounter++;
871
-
909
+ const relayDataHash = this.getRelayHashFromEvent(fill);
872
910
  if (v3RelayHashes[relayDataHash]) {
873
911
  if (!v3RelayHashes[relayDataHash].fill) {
874
912
  assert(
875
- isDefined(v3RelayHashes[relayDataHash].deposit),
913
+ isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
876
914
  "Deposit should exist in relay hash dictionary."
877
915
  );
878
- // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit,
879
- // so this fill is validated.
880
916
  v3RelayHashes[relayDataHash].fill = fill;
881
917
  if (fill.blockNumber >= destinationChainBlockRange[0]) {
882
- validatedBundleV3Fills.push({
883
- ...fill,
884
- quoteTimestamp: v3RelayHashes[relayDataHash].deposit!.quoteTimestamp, // ! due to assert above
885
- });
918
+ const fillToRefund = await verifyFillRepayment(
919
+ fill,
920
+ destinationClient.spokePool.provider,
921
+ v3RelayHashes[relayDataHash].deposits![0],
922
+ allChainIds
923
+ );
924
+ if (!isDefined(fillToRefund)) {
925
+ bundleUnrepayableFillsV3.push(fill);
926
+ // We don't return here yet because we still need to mark unexecutable slow fill leaves
927
+ // or duplicate deposits. However, we won't issue a fast fill refund.
928
+ } else {
929
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
930
+ validatedBundleV3Fills.push({
931
+ ...fillToRefund,
932
+ quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp,
933
+ });
934
+
935
+ // Now that we know this deposit has been filled on-chain, identify any duplicate deposits
936
+ // sent for this fill and refund them to the filler, because this value would not be paid out
937
+ // otherwise. These deposits can no longer expire and get refunded as an expired deposit,
938
+ // and they won't trigger a pre-fill refund because the fill is in this bundle.
939
+ // Pre-fill refunds only happen when deposits are sent in this bundle and the
940
+ // fill is from a prior bundle. Paying out the filler keeps the behavior consistent for how
941
+ // we deal with duplicate deposits regardless if the deposit is matched with a pre-fill or
942
+ // a current bundle fill.
943
+ const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1);
944
+ duplicateDeposits.forEach((duplicateDeposit) => {
945
+ if (isSlowFill(fill)) {
946
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
947
+ } else {
948
+ validatedBundleV3Fills.push({
949
+ ...fillToRefund,
950
+ quoteTimestamp: duplicateDeposit.quoteTimestamp,
951
+ });
952
+ }
953
+ });
954
+ }
955
+
886
956
  // If fill replaced a slow fill request, then mark it as one that might have created an
887
957
  // unexecutable slow fill. We can't know for sure until we check the slow fill request
888
958
  // events.
889
- // slow fill requests for deposits from or to lite chains are considered invalid
890
959
  if (
891
960
  fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
892
- _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposit!)
961
+ _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposits![0])
893
962
  ) {
894
963
  fastFillsReplacingSlowFills.push(relayDataHash);
895
964
  }
896
965
  }
966
+ } else {
967
+ throw new Error("Duplicate fill detected");
897
968
  }
898
969
  return;
899
970
  }
900
971
 
901
972
  // At this point, there is no relay hash dictionary entry for this fill, so we need to
902
- // instantiate the entry.
973
+ // instantiate the entry. We won't modify the fill.relayer until we match it with a deposit.
903
974
  v3RelayHashes[relayDataHash] = {
904
- deposit: undefined,
905
- fill: fill,
975
+ deposits: undefined,
976
+ fill,
906
977
  slowFillRequest: undefined,
907
978
  };
908
979
 
909
- // TODO: We might be able to remove the following historical query once we deprecate the deposit()
980
+ // TODO: We can remove the following historical query once we deprecate the deposit()
910
981
  // function since there won't be any old, unexpired deposits anymore assuming the spoke pool client
911
982
  // lookbacks have been validated, which they should be before we run this function.
912
983
 
@@ -922,18 +993,6 @@ export class BundleDataClient {
922
993
  bundleInvalidFillsV3.push(fill);
923
994
  return;
924
995
  }
925
- // If the fill's repayment address is not a valid EVM address and the repayment chain is an EVM chain, the fill is invalid.
926
- if (chainIsEvm(fill.repaymentChainId) && !isValidEvmAddress(fill.relayer)) {
927
- const fillTransaction = await originClient.spokePool.provider.getTransaction(fill.transactionHash);
928
- const originRelayer = fillTransaction.from;
929
- // Repayment chain is still an EVM chain, but the msg.sender is a bytes32 address, so the fill is invalid.
930
- if (!isValidEvmAddress(originRelayer)) {
931
- bundleInvalidFillsV3.push(fill);
932
- return;
933
- }
934
- // Otherwise, assume the relayer to be repaid is the msg.sender.
935
- fill.relayer = originRelayer;
936
- }
937
996
  // If deposit is using the deterministic relay hash feature, then the following binary search-based
938
997
  // algorithm will not work. However, it is impossible to emit an infinite fill deadline using
939
998
  // the unsafeDepositV3 function so there is no need to catch the special case.
@@ -942,17 +1001,40 @@ export class BundleDataClient {
942
1001
  bundleInvalidFillsV3.push(fill);
943
1002
  } else {
944
1003
  const matchedDeposit = historicalDeposit.deposit;
945
- // @dev Since queryHistoricalDepositForFill validates the fill by checking individual
946
- // object property values against the deposit's, we
947
- // sanity check it here by comparing the full relay hashes. If there's an error here then the
948
- // historical deposit query is not working as expected.
949
- assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match.");
950
- validatedBundleV3Fills.push({
951
- ...fill,
952
- quoteTimestamp: matchedDeposit.quoteTimestamp,
953
- });
954
- v3RelayHashes[relayDataHash].deposit = matchedDeposit;
955
- // slow fill requests for deposits from or to lite chains are considered invalid
1004
+ // If deposit is in a following bundle, then this fill will have to be refunded once that deposit
1005
+ // is in the current bundle.
1006
+ if (matchedDeposit.blockNumber > originChainBlockRange[1]) {
1007
+ bundleInvalidFillsV3.push(fill);
1008
+ return;
1009
+ }
1010
+ v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
1011
+
1012
+ const fillToRefund = await verifyFillRepayment(
1013
+ fill,
1014
+ destinationClient.spokePool.provider,
1015
+ matchedDeposit,
1016
+ allChainIds
1017
+ );
1018
+ if (!isDefined(fillToRefund)) {
1019
+ bundleUnrepayableFillsV3.push(fill);
1020
+ // Don't return yet as we still need to mark down any unexecutable slow fill leaves
1021
+ // in case this fast fill replaced a slow fill request.
1022
+ } else {
1023
+ // @dev Since queryHistoricalDepositForFill validates the fill by checking individual
1024
+ // object property values against the deposit's, we
1025
+ // sanity check it here by comparing the full relay hashes. If there's an error here then the
1026
+ // historical deposit query is not working as expected.
1027
+ assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match.");
1028
+ validatedBundleV3Fills.push({
1029
+ ...fillToRefund,
1030
+ quoteTimestamp: matchedDeposit.quoteTimestamp,
1031
+ });
1032
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
1033
+
1034
+ // No need to check for duplicate deposits here since duplicate deposits with
1035
+ // infinite deadlines are impossible to send via unsafeDeposit().
1036
+ }
1037
+
956
1038
  if (
957
1039
  fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
958
1040
  _canCreateSlowFillLeaf(matchedDeposit)
@@ -964,8 +1046,14 @@ export class BundleDataClient {
964
1046
  }
965
1047
  );
966
1048
 
967
- // Process slow fill requests. One invariant we need to maintain is that we cannot create slow fill requests
968
- // for deposits that would expire in this bundle.
1049
+ // Process slow fill requests and produce slow fill leaves while maintaining the following the invariants:
1050
+ // - Slow fill leaves cannot be produced for deposits that have expired in this bundle.
1051
+ // - Slow fill leaves cannot be produced for deposits that have been filled.
1052
+
1053
+ // Assumptions about fills:
1054
+ // - Duplicate slow fill requests for the same relay data hash are impossible to send.
1055
+ // - Slow fill requests can only be sent before the deposit's fillDeadline.
1056
+ // - Slow fill requests for a deposit that has been filled.
969
1057
  await forEachAsync(
970
1058
  destinationClient
971
1059
  .getSlowFillRequestsForOriginChain(originChainId)
@@ -978,46 +1066,40 @@ export class BundleDataClient {
978
1066
 
979
1067
  if (v3RelayHashes[relayDataHash]) {
980
1068
  if (!v3RelayHashes[relayDataHash].slowFillRequest) {
981
- // At this point, the v3RelayHashes entry already existed meaning that there is either a matching
982
- // fill or deposit.
983
1069
  v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest;
984
1070
  if (v3RelayHashes[relayDataHash].fill) {
985
- // If there is a fill matching the relay hash, then this slow fill request can't be used
986
- // to create a slow fill for a filled deposit.
1071
+ // Exiting here assumes that slow fill requests must precede fills, so if there was a fill
1072
+ // following this slow fill request, then we would have already seen it. We don't need to check
1073
+ // for a fill older than this slow fill request.
987
1074
  return;
988
1075
  }
989
1076
  assert(
990
- isDefined(v3RelayHashes[relayDataHash].deposit),
1077
+ isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
991
1078
  "Deposit should exist in relay hash dictionary."
992
1079
  );
993
- // The ! is safe here because we've already checked that the deposit exists in the relay hash dictionary.
994
- const matchedDeposit = v3RelayHashes[relayDataHash].deposit!;
1080
+ const matchedDeposit = v3RelayHashes[relayDataHash].deposits![0];
995
1081
 
996
- // If there is no fill matching the relay hash, then this might be a valid slow fill request
997
- // that we should produce a slow fill leaf for. Check if the slow fill request is in the
998
- // destination chain block range.
999
1082
  if (
1000
1083
  slowFillRequest.blockNumber >= destinationChainBlockRange[0] &&
1001
1084
  _canCreateSlowFillLeaf(matchedDeposit) &&
1002
- // Deposit must not have expired in this bundle.
1003
1085
  !_depositIsExpired(matchedDeposit)
1004
1086
  ) {
1005
- // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit,
1006
- // so this slow fill request relay data is correct.
1007
1087
  validatedBundleSlowFills.push(matchedDeposit);
1008
1088
  }
1089
+ } else {
1090
+ throw new Error("Duplicate slow fill request detected.");
1009
1091
  }
1010
1092
  return;
1011
1093
  }
1012
1094
 
1013
1095
  // Instantiate dictionary if there is neither a deposit nor fill matching it.
1014
1096
  v3RelayHashes[relayDataHash] = {
1015
- deposit: undefined,
1097
+ deposits: undefined,
1016
1098
  fill: undefined,
1017
1099
  slowFillRequest: slowFillRequest,
1018
1100
  };
1019
1101
 
1020
- // TODO: We might be able to remove the following historical query once we deprecate the deposit()
1102
+ // TODO: We can remove the following historical query once we deprecate the deposit()
1021
1103
  // function since there won't be any old, unexpired deposits anymore assuming the spoke pool client
1022
1104
  // lookbacks have been validated, which they should be before we run this function.
1023
1105
 
@@ -1029,16 +1111,23 @@ export class BundleDataClient {
1029
1111
  // want to perform a binary search lookup for it because the deposit ID is "unsafe" and cannot be
1030
1112
  // found using such a method) because infinite fill deadlines cannot be produced from the unsafeDepositV3()
1031
1113
  // function.
1032
- if (
1033
- slowFillRequest.blockNumber >= destinationChainBlockRange[0] &&
1034
- INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline)
1035
- ) {
1114
+ if (slowFillRequest.blockNumber >= destinationChainBlockRange[0]) {
1115
+ if (!INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline)) {
1116
+ bundleInvalidSlowFillRequests.push(slowFillRequest);
1117
+ return;
1118
+ }
1036
1119
  const historicalDeposit = await queryHistoricalDepositForFill(originClient, slowFillRequest);
1037
1120
  if (!historicalDeposit.found) {
1038
- // TODO: Invalid slow fill request. Maybe worth logging.
1121
+ bundleInvalidSlowFillRequests.push(slowFillRequest);
1039
1122
  return;
1040
1123
  }
1041
1124
  const matchedDeposit: V3DepositWithBlock = historicalDeposit.deposit;
1125
+ // If deposit is in a following bundle, then this slow fill request will have to be created
1126
+ // once that deposit is in the current bundle.
1127
+ if (matchedDeposit.blockNumber > originChainBlockRange[1]) {
1128
+ bundleInvalidSlowFillRequests.push(slowFillRequest);
1129
+ return;
1130
+ }
1042
1131
  // @dev Since queryHistoricalDepositForFill validates the slow fill request by checking individual
1043
1132
  // object property values against the deposit's, we
1044
1133
  // sanity check it here by comparing the full relay hashes. If there's an error here then the
@@ -1047,13 +1136,9 @@ export class BundleDataClient {
1047
1136
  this.getRelayHashFromEvent(matchedDeposit) === relayDataHash,
1048
1137
  "Deposit relay hashes should match."
1049
1138
  );
1050
- v3RelayHashes[relayDataHash].deposit = matchedDeposit;
1139
+ v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
1051
1140
 
1052
- if (
1053
- !_canCreateSlowFillLeaf(matchedDeposit) ||
1054
- // Deposit must not have expired in this bundle.
1055
- _depositIsExpired(matchedDeposit)
1056
- ) {
1141
+ if (!_canCreateSlowFillLeaf(matchedDeposit) || _depositIsExpired(matchedDeposit)) {
1057
1142
  return;
1058
1143
  }
1059
1144
  validatedBundleSlowFills.push(matchedDeposit);
@@ -1061,126 +1146,143 @@ export class BundleDataClient {
1061
1146
  }
1062
1147
  );
1063
1148
 
1064
- // Deposits can be submitted an arbitrary amount of time after matching fills and slow fill requests.
1065
- // Therefore, let's go through each deposit in this bundle again and check a few things in order:
1066
- // - Has the deposit been filled ? If so, then we need to issue a relayer refund for
1067
- // this "pre-fill" if the fill took place in a previous bundle.
1068
- // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund.
1069
- // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf
1070
- // for this "pre-slow-fill-request" if this request took place in a previous bundle.
1071
-
1072
- // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We
1073
- // should remove this check once we've advanced far beyond the version bump block.
1074
- await mapAsync(
1075
- bundleDepositHashes.filter((depositHash) => {
1076
- const { deposit } = v3RelayHashes[depositHash];
1077
- return (
1078
- deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId
1079
- );
1080
- }),
1081
- async (depositHash) => {
1082
- const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash];
1083
- if (!deposit) throw new Error("Deposit should exist in relay hash dictionary.");
1084
-
1085
- // We are willing to refund a pre-fill multiple times for each duplicate deposit.
1086
- // This is because a duplicate deposit for a pre-fill cannot get
1087
- // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore
1088
- // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out
1089
- // the pre-filler.
1090
-
1091
- // If fill exists in memory, then the only case in which we need to create a refund is if the
1092
- // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits.
1093
- if (fill) {
1094
- if (canRefundPrefills && fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) {
1095
- // If fill is in the current bundle then we can assume there is already a refund for it, so only
1096
- // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then
1097
- // we won't consider it, following the previous treatment of fills after the bundle block range.
1149
+ // Process deposits and maintain the following invariants:
1150
+ // - Deposits matching fills that are not type SlowFill from previous bundle block ranges should produce
1151
+ // refunds for those fills.
1152
+ // - Deposits matching fills that are type SlowFill from previous bundle block ranges should be refunded to the
1153
+ // depositor.
1154
+ // - All deposits expiring in this bundle, even those sent in prior bundle block ranges, should be refunded
1155
+ // to the depositor.
1156
+ // - An expired deposit cannot be refunded if the deposit was filled.
1157
+ // - If a deposit from a prior bundle expired in this bundle, had a slow fill request created for it
1158
+ // in a prior bundle, and has not been filled yet, then an unexecutable slow fill leaf has been created
1159
+ // and needs to be refunded to the HubPool.
1160
+ // - Deposits matching slow fill requests from previous bundle block ranges should produce slow fills
1161
+ // if the deposit has not been filled.
1162
+
1163
+ // Assumptions:
1164
+ // - If the deposit has a matching fill or slow fill request in the bundle then we have already stored
1165
+ // it in the relay hashes dictionary.
1166
+ // - We've created refunds for all fills in this bundle matching a deposit.
1167
+ // - We've created slow fill leaves for all slow fill requests in this bundle matching an unfilled deposit.
1168
+ // - Deposits for the same relay data hash can be sent an arbitrary amount of times.
1169
+ // - Deposits can be sent an arbitrary amount of time after a fill has been sent for the matching relay data.
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
+ }
1181
+
1182
+ // If fill is in the current bundle then we can assume there is already a refund for it, so only
1183
+ // include this pre fill if the fill is in an older bundle.
1184
+ if (fill) {
1185
+ if (canRefundPrefills && fill.blockNumber < destinationChainBlockRange[0]) {
1186
+ const fillToRefund = await verifyFillRepayment(
1187
+ fill,
1188
+ destinationClient.spokePool.provider,
1189
+ v3RelayHashes[relayDataHash].deposits![0],
1190
+ allChainIds
1191
+ );
1192
+ if (!isDefined(fillToRefund)) {
1193
+ bundleUnrepayableFillsV3.push(fill);
1194
+ } else if (!isSlowFill(fill)) {
1195
+ v3RelayHashes[relayDataHash].fill = fillToRefund;
1098
1196
  validatedBundleV3Fills.push({
1099
- ...fill,
1197
+ ...fillToRefund,
1100
1198
  quoteTimestamp: deposit.quoteTimestamp,
1101
1199
  });
1200
+ } else {
1201
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1102
1202
  }
1103
- return;
1104
1203
  }
1204
+ return;
1205
+ }
1105
1206
 
1106
- // If a slow fill request exists in memory, then we know the deposit has not been filled because fills
1107
- // must follow slow fill requests and we would have seen the fill already if it existed. Therefore,
1108
- // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or
1109
- // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request
1110
- // took place in a prior bundle otherwise we would have already created a slow fill leaf for it.
1111
- if (slowFillRequest) {
1112
- if (_depositIsExpired(deposit)) {
1113
- updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1114
- } else if (
1115
- canRefundPrefills &&
1116
- slowFillRequest.blockNumber < destinationChainBlockRange[0] &&
1117
- _canCreateSlowFillLeaf(deposit)
1118
- ) {
1119
- validatedBundleSlowFills.push(deposit);
1120
- }
1121
- return;
1207
+ // If a slow fill request exists in memory, then we know the deposit has not been filled because fills
1208
+ // must follow slow fill requests and we would have seen the fill already if it existed.,
1209
+ // We can conclude that either the deposit has expired or we need to create a slow fill leaf for the
1210
+ // deposit because it has not been filled. Slow fill leaves were already created for requests sent
1211
+ // in the current bundle so only create new slow fill leaves for prior bundle deposits.
1212
+ if (slowFillRequest) {
1213
+ if (_depositIsExpired(deposit)) {
1214
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1215
+ } else if (
1216
+ canRefundPrefills &&
1217
+ slowFillRequest.blockNumber < destinationChainBlockRange[0] &&
1218
+ _canCreateSlowFillLeaf(deposit) &&
1219
+ validatedBundleSlowFills.every((d) => this.getRelayHashFromEvent(d) !== relayDataHash)
1220
+ ) {
1221
+ validatedBundleSlowFills.push(deposit);
1122
1222
  }
1223
+ return;
1224
+ }
1123
1225
 
1124
- // So at this point in the code, there is no fill or slow fill request in memory for this deposit.
1125
- // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf.
1126
- // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles
1127
- // because the spoke pool client lookback would have returned this entire bundle of events and stored
1128
- // them into the relay hash dictionary.
1129
- const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]);
1130
-
1131
- // If deposit was filled, then we need to issue a refund for it.
1132
- if (fillStatus === FillStatus.Filled) {
1133
- // We need to find the fill event to issue a refund to the right relayer and repayment chain,
1134
- // or msg.sender if relayer address is invalid for the repayment chain.
1135
- const prefill = (await findFillEvent(
1136
- destinationClient.spokePool,
1226
+ // So at this point in the code, there is no fill or slow fill request in memory for this deposit.
1227
+ // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf.
1228
+ // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles
1229
+ // because the spoke pool client lookback would have returned this entire bundle of events and stored
1230
+ // them into the relay hash dictionary.
1231
+ const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]);
1232
+ if (fillStatus === FillStatus.Filled) {
1233
+ // We don't need to verify the fill block is before the bundle end block on the destination chain because
1234
+ // we queried the fillStatus at the end block. Therefore, if the fill took place after the end block,
1235
+ // then we wouldn't be in this branch of the code.
1236
+ const prefill = await this.findMatchingFillEvent(deposit, destinationClient);
1237
+ assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`);
1238
+ assert(this.getRelayHashFromEvent(prefill!) === relayDataHash, "Relay hashes should match.");
1239
+ if (canRefundPrefills) {
1240
+ const verifiedFill = await verifyFillRepayment(
1241
+ prefill!,
1242
+ destinationClient.spokePool.provider,
1137
1243
  deposit,
1138
- destinationClient.deploymentBlock,
1139
- destinationClient.latestBlockSearched
1140
- )) as unknown as FillWithBlock;
1141
- if (canRefundPrefills && !isSlowFill(prefill)) {
1244
+ allChainIds
1245
+ );
1246
+ if (!isDefined(verifiedFill)) {
1247
+ bundleUnrepayableFillsV3.push(prefill!);
1248
+ } else if (!isSlowFill(verifiedFill)) {
1142
1249
  validatedBundleV3Fills.push({
1143
- ...prefill,
1250
+ ...verifiedFill!,
1144
1251
  quoteTimestamp: deposit.quoteTimestamp,
1145
1252
  });
1253
+ } else {
1254
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1146
1255
  }
1147
1256
  }
1148
- // If deposit is not filled and its newly expired, we can create a deposit refund for it.
1149
- // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because
1150
- // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0
1151
- // for example. Those should be included in this bundle of refunded deposits.
1152
- else if (_depositIsExpired(deposit)) {
1153
- updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1154
- }
1155
- // If slow fill requested, then issue a slow fill leaf for the deposit.
1156
- else if (fillStatus === FillStatus.RequestedSlowFill) {
1157
- // Input and Output tokens must be equivalent on the deposit for this to be slow filled.
1158
- // Slow fill requests for deposits from or to lite chains are considered invalid
1159
- if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) {
1160
- // If deposit newly expired, then we can't create a slow fill leaf for it but we can
1161
- // create a deposit refund for it.
1162
- validatedBundleSlowFills.push(deposit);
1163
- }
1257
+ } else if (_depositIsExpired(deposit)) {
1258
+ updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
1259
+ } else if (
1260
+ fillStatus === FillStatus.RequestedSlowFill &&
1261
+ // Don't create duplicate slow fill requests for the same deposit.
1262
+ validatedBundleSlowFills.every((d) => this.getRelayHashFromEvent(d) !== relayDataHash)
1263
+ ) {
1264
+ if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) {
1265
+ validatedBundleSlowFills.push(deposit);
1164
1266
  }
1165
1267
  }
1166
- );
1268
+ });
1167
1269
 
1168
1270
  // For all fills that came after a slow fill request, we can now check if the slow fill request
1169
1271
  // was a valid one and whether it was created in a previous bundle. If so, then it created a slow fill
1170
1272
  // leaf that is now unexecutable.
1171
1273
  fastFillsReplacingSlowFills.forEach((relayDataHash) => {
1172
- const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1274
+ const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1173
1275
  assert(
1174
1276
  fill?.relayExecutionInfo.fillType === FillType.ReplacedSlowFill,
1175
1277
  "Fill type should be ReplacedSlowFill."
1176
1278
  );
1177
1279
  // Needed for TSC - are implicitely checking that deposit exists by making it to this point.
1178
- if (!deposit) {
1280
+ if (!deposits || deposits.length < 1) {
1179
1281
  throw new Error("Deposit should exist in relay hash dictionary.");
1180
1282
  }
1181
1283
  // We should never push fast fills involving lite chains here because slow fill requests for them are invalid:
1182
1284
  assert(
1183
- _canCreateSlowFillLeaf(deposit),
1285
+ _canCreateSlowFillLeaf(deposits[0]),
1184
1286
  "fastFillsReplacingSlowFills should contain only deposits that can be slow filled"
1185
1287
  );
1186
1288
  const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
@@ -1190,7 +1292,7 @@ export class BundleDataClient {
1190
1292
  !slowFillRequest ||
1191
1293
  slowFillRequest.blockNumber < destinationBlockRange[0]
1192
1294
  ) {
1193
- validatedBundleUnexecutableSlowFills.push(deposit);
1295
+ validatedBundleUnexecutableSlowFills.push(deposits[0]);
1194
1296
  }
1195
1297
  });
1196
1298
  }
@@ -1204,15 +1306,19 @@ export class BundleDataClient {
1204
1306
  // For all deposits older than this bundle, we need to check if they expired in this bundle and if they did,
1205
1307
  // whether there was a slow fill created for it in a previous bundle that is now unexecutable and replaced
1206
1308
  // by a new expired deposit refund.
1207
- await forEachAsync(olderDepositHashes, async (relayDataHash) => {
1208
- const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1209
- assert(isDefined(deposit), "Deposit should exist in relay hash dictionary.");
1210
- const { destinationChainId } = deposit!;
1309
+ await forEachAsync(olderDepositHashes, async (depositHash) => {
1310
+ const { relayDataHash, index } = decodeBundleDepositHash(depositHash);
1311
+ const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
1312
+ if (!deposits || deposits.length < 1) {
1313
+ throw new Error("Deposit should exist in relay hash dictionary.");
1314
+ }
1315
+ const deposit = deposits[index];
1316
+ const { destinationChainId } = deposit;
1211
1317
  const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds);
1212
1318
 
1213
1319
  // Only look for deposits that were mined before this bundle and that are newly expired.
1214
1320
  // If the fill deadline is lower than the bundle start block on the destination chain, then
1215
- // we should assume it was marked "newly expired" and refunded in a previous bundle.
1321
+ // we should assume it was refunded in a previous bundle.
1216
1322
  if (
1217
1323
  // If there is a valid fill that we saw matching this deposit, then it does not need a refund.
1218
1324
  !fill &&
@@ -1256,7 +1362,7 @@ export class BundleDataClient {
1256
1362
  validatedBundleV3Fills.length > 0
1257
1363
  ? this.clients.hubPoolClient.batchComputeRealizedLpFeePct(
1258
1364
  validatedBundleV3Fills.map((fill) => {
1259
- const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit;
1365
+ const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0];
1260
1366
  assert(isDefined(matchedDeposit), "Deposit should exist in relay hash dictionary.");
1261
1367
  const { chainToSendRefundTo: paymentChainId } = getRefundInformationFromFill(
1262
1368
  fill,
@@ -1300,7 +1406,7 @@ export class BundleDataClient {
1300
1406
  });
1301
1407
  v3FillLpFees.forEach(({ realizedLpFeePct }, idx) => {
1302
1408
  const fill = validatedBundleV3Fills[idx];
1303
- const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit;
1409
+ const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0];
1304
1410
  assert(isDefined(associatedDeposit), "Deposit should exist in relay hash dictionary.");
1305
1411
  const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill(
1306
1412
  fill,
@@ -1309,10 +1415,19 @@ export class BundleDataClient {
1309
1415
  chainIds,
1310
1416
  associatedDeposit!.fromLiteChain
1311
1417
  );
1312
- updateBundleFillsV3(bundleFillsV3, fill, realizedLpFeePct, chainToSendRefundTo, repaymentToken);
1418
+ updateBundleFillsV3(bundleFillsV3, fill, realizedLpFeePct, chainToSendRefundTo, repaymentToken, fill.relayer);
1313
1419
  });
1314
1420
  v3SlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => {
1315
1421
  const deposit = validatedBundleSlowFills[idx];
1422
+ // We should not create slow fill leaves for duplicate deposit hashes and we should only create a slow
1423
+ // fill leaf for the first deposit (the quote timestamp of the deposit determines the LP fee, so its
1424
+ // important we pick out the correct deposit). Deposits are pushed into validatedBundleSlowFills in ascending
1425
+ // order so the following slice will only match the first deposit.
1426
+ const relayDataHash = this.getRelayHashFromEvent(deposit);
1427
+ if (validatedBundleSlowFills.slice(0, idx).some((d) => this.getRelayHashFromEvent(d) === relayDataHash)) {
1428
+ return;
1429
+ }
1430
+ assert(!_depositIsExpired(deposit), "Cannot create slow fill leaf for expired deposit.");
1316
1431
  updateBundleSlowFills(bundleSlowFillsV3, { ...deposit, lpFeePct });
1317
1432
  });
1318
1433
  v3UnexecutableSlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => {
@@ -1323,7 +1438,6 @@ export class BundleDataClient {
1323
1438
  const v3SpokeEventsReadable = prettyPrintV3SpokePoolEvents(
1324
1439
  bundleDepositsV3,
1325
1440
  bundleFillsV3,
1326
- bundleInvalidFillsV3,
1327
1441
  bundleSlowFillsV3,
1328
1442
  expiredDepositsToRefundV3,
1329
1443
  unexecutableSlowFills
@@ -1332,12 +1446,30 @@ export class BundleDataClient {
1332
1446
  if (bundleInvalidFillsV3.length > 0) {
1333
1447
  this.logger.debug({
1334
1448
  at: "BundleDataClient#loadData",
1335
- message: "Finished loading V3 spoke pool data and found some invalid V3 fills in range",
1449
+ message: "Finished loading V3 spoke pool data and found some invalid fills in range",
1336
1450
  blockRangesForChains,
1337
1451
  bundleInvalidFillsV3,
1338
1452
  });
1339
1453
  }
1340
1454
 
1455
+ if (bundleUnrepayableFillsV3.length > 0) {
1456
+ this.logger.debug({
1457
+ at: "BundleDataClient#loadData",
1458
+ message: "Finished loading V3 spoke pool data and found some unrepayable fills in range",
1459
+ blockRangesForChains,
1460
+ bundleUnrepayableFillsV3,
1461
+ });
1462
+ }
1463
+
1464
+ if (bundleInvalidSlowFillRequests.length > 0) {
1465
+ this.logger.debug({
1466
+ at: "BundleDataClient#loadData",
1467
+ message: "Finished loading V3 spoke pool data and found some invalid slow fill requests in range",
1468
+ blockRangesForChains,
1469
+ bundleInvalidSlowFillRequests,
1470
+ });
1471
+ }
1472
+
1341
1473
  this.logger.debug({
1342
1474
  at: "BundleDataClient#loadDataFromScratch",
1343
1475
  message: `Computed bundle data in ${Math.round(performance.now() - start) / 1000}s.`,
@@ -1357,7 +1489,7 @@ export class BundleDataClient {
1357
1489
  // keccak256 hash of the relay data, which can be used as input into the on-chain `fillStatuses()` function in the
1358
1490
  // spoke pool contract. However, this internal function is used to uniquely identify a bridging event
1359
1491
  // for speed since its easier to build a string from the event data than to hash it.
1360
- private getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string {
1492
+ protected getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string {
1361
1493
  return `${event.depositor}-${event.recipient}-${event.exclusiveRelayer}-${event.inputToken}-${event.outputToken}-${
1362
1494
  event.inputAmount
1363
1495
  }-${event.outputAmount}-${event.originChainId}-${event.depositId.toString()}-${event.fillDeadline}-${
@@ -1365,6 +1497,18 @@ export class BundleDataClient {
1365
1497
  }-${event.message}-${event.destinationChainId}`;
1366
1498
  }
1367
1499
 
1500
+ protected async findMatchingFillEvent(
1501
+ deposit: DepositWithBlock,
1502
+ spokePoolClient: SpokePoolClient
1503
+ ): Promise<FillWithBlock | undefined> {
1504
+ return await findFillEvent(
1505
+ spokePoolClient.spokePool,
1506
+ deposit,
1507
+ spokePoolClient.deploymentBlock,
1508
+ spokePoolClient.latestBlockSearched
1509
+ );
1510
+ }
1511
+
1368
1512
  async getBundleBlockTimestamps(
1369
1513
  chainIds: number[],
1370
1514
  blockRangesForChains: number[][],
@@ -1390,13 +1534,26 @@ export class BundleDataClient {
1390
1534
  // will usually be called in production with block ranges that were validated by
1391
1535
  // DataworkerUtils.blockRangesAreInvalidForSpokeClients.
1392
1536
  const startBlockForChain = Math.min(_startBlockForChain, spokePoolClient.latestBlockSearched);
1393
- const endBlockForChain = Math.min(_endBlockForChain, spokePoolClient.latestBlockSearched);
1394
- const [startTime, endTime] = [
1537
+ // @dev Add 1 to the bundle end block. The thinking here is that there can be a gap between
1538
+ // block timestamps in subsequent blocks. The bundle data client assumes that fill deadlines expire
1539
+ // in exactly one bundle, therefore we must make sure that the bundle block timestamp for one bundle's
1540
+ // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way
1541
+ // there are no gaps in block timestamps between bundles.
1542
+ const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched);
1543
+ const [startTime, _endTime] = [
1395
1544
  await spokePoolClient.getTimestampForBlock(startBlockForChain),
1396
1545
  await spokePoolClient.getTimestampForBlock(endBlockForChain),
1397
1546
  ];
1547
+ // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also
1548
+ // no overlap, subtract 1 from the end time.
1549
+ const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0;
1550
+ const endTime = Math.max(0, _endTime - endBlockDelta);
1551
+
1398
1552
  // Sanity checks:
1399
- assert(endTime >= startTime, "End time should be greater than start time.");
1553
+ assert(
1554
+ endTime >= startTime,
1555
+ `End time for block ${endBlockForChain} should be greater than start time for block ${startBlockForChain}: ${endTime} >= ${startTime}.`
1556
+ );
1400
1557
  assert(
1401
1558
  startBlockForChain === 0 || startTime > 0,
1402
1559
  "Start timestamp must be greater than 0 if the start block is greater than 0."