From 2b647e5acf21a4a7753bf5b86562b195962fb93e Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 22 Jan 2025 19:51:34 -0500 Subject: [PATCH 001/103] feat(BundleDataClient): Support refunds for pre-fills and fills for pre-slowfill-requests ### Definitions - "pre-fill": any fill that is in a bundle that precedes the bundle containing the matched deposit - "pre-slow-fill-request": a mouthful, and any slow fill request in a bundle that precedes the bundle containing the matched deposit ## Core logic for refunding pre-fills: Evaluate all deposits in the current bundle that were not filled in the current bundle. Figure out whether the deposit was filled, by either: - Finding the fill in the spoke pool client's memory, or - Querying the fill status on-chain If there is a fill for this deposit then we need to issue a refund for this fill because the fill occurred in a previous bundle where it might not have been refunded. We need to use a similar algorithm for creating slow fill leaves for pre-slow-fill-requests ## Avoiding duplicate refunds for pre-fills We don't deterministically know whether this pre-fill was refunded in the prior bundle because we don't know for certain what kind of event search window the proposer of the prior bundle used when constructing the bundle. To illustrate this problem, imagine if a fill and a deposit are sent 10 minutes apart such that the fill is at the end of the current bundle and the deposit is at the beginning of the next. The prior bundle proposer probably would have issued a refund for this fill, but we don't know for certain. So, one way around this non-determinism is to introduce a new rule to the UMIP: ### Do not issue refunds for fills or slow fill leaves where the matched deposit is later than the current bundle block range This rule makes it always apparent which bundle should include a refund or slow fill leaf for a pre-fill or pre-slow-fill-request. This will require a UMIP change to [this PR](https://github.com/UMAprotocol/UMIPs/pull/611) ## TODO I still need to update the `findFillBlock()` function to return a historical fill for a deposit, because we'll need the FilledRelay's `relayerAddress` or `msg.sender` to credit the refund to (the latter in the case where the relayer address isn't valid on the repayment chain). --- .../BundleDataClient/BundleDataClient.ts | 191 +++++++++++++----- 1 file changed, 135 insertions(+), 56 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index fd992c95d..2250f3041 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -14,6 +14,9 @@ import { ExpiredDepositsToRefundV3, Clients, CombinedRefunds, + FillWithBlock, + Deposit, + DepositWithBlock, } from "../../interfaces"; import { AcrossConfigStoreClient, SpokePoolClient } from ".."; import { @@ -32,6 +35,7 @@ import { mapAsync, bnUint32Max, isZeroValueDeposit, + findFillBlock, } from "../../utils"; import winston from "winston"; import { @@ -665,6 +669,37 @@ export class BundleDataClient { return isChainDisabled(blockRangeForChain); }; + const _canCreateSlowFillLeaf = (deposit: DepositWithBlock): boolean => { + return ( + // Cannot slow fill when input and output tokens are not equivalent. + this.clients.hubPoolClient.areTokensEquivalent( + deposit.inputToken, + deposit.originChainId, + deposit.outputToken, + deposit.destinationChainId, + deposit.quoteBlockNumber + ) && + // Cannot slow fill from or to a lite chain. + !deposit.fromLiteChain && + !deposit.toLiteChain && + // Deposit must not expire during this bundle. + deposit.fillDeadline >= bundleBlockTimestamps[deposit.destinationChainId][1] + ); + }; + + const _getFillStatusForDeposit = (deposit: Deposit, queryBlock: number): Promise => { + return spokePoolClients[deposit.destinationChainId].relayFillStatus( + deposit, + // We can assume that in production + // the block to query is not one that the spoke pool client + // hasn't queried. This is because this function will usually be called + // in production with block ranges that were validated by + // DataworkerUtils.blockRangesAreInvalidForSpokeClients. + Math.min(queryBlock, spokePoolClients[deposit.destinationChainId].latestBlockSearched), + deposit.destinationChainId + ); + }; + // Infer chain ID's to load from number of block ranges passed in. const allChainIds = blockRangesForChains .map((_blockRange, index) => chainIds[index]) @@ -779,7 +814,10 @@ export class BundleDataClient { continue; } originClient.getDepositsForDestinationChain(destinationChainId).forEach((deposit) => { - if (isZeroValueDeposit(deposit)) { + // Only evaluate deposits that are in this bundle or in previous bundles. This means we cannot issue fill + // refunds or slow fills here for deposits that are in future bundles (i.e. "pre-fills"). Instead, we'll + // evaluate these pre-fills once the deposit is inside the "current" bundle block range. + if (isZeroValueDeposit(deposit) || deposit.blockNumber > originChainBlockRange[1]) { return; } depositCounter++; @@ -809,7 +847,7 @@ export class BundleDataClient { // If deposit is in bundle and it has expired, additionally save it as an expired deposit. // If deposit is not in the bundle block range, then save it as an older deposit that // may have expired. - if (deposit.blockNumber >= originChainBlockRange[0] && deposit.blockNumber <= originChainBlockRange[1]) { + if (deposit.blockNumber >= originChainBlockRange[0]) { // Deposit is a V3 deposit in this origin chain's bundle block range and is not a duplicate. updateBundleDepositsV3(bundleDepositsV3, deposit); // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because @@ -968,36 +1006,14 @@ export class BundleDataClient { ); // The ! is safe here because we've already checked that the deposit exists in the relay hash dictionary. const matchedDeposit = v3RelayHashes[relayDataHash].deposit!; - - // Input and Output tokens must be equivalent on the deposit for this to be slow filled. - if ( - !this.clients.hubPoolClient.areTokensEquivalent( - matchedDeposit.inputToken, - matchedDeposit.originChainId, - matchedDeposit.outputToken, - matchedDeposit.destinationChainId, - matchedDeposit.quoteBlockNumber - ) - ) { - return; - } - - // slow fill requests for deposits from or to lite chains are considered invalid - if ( - v3RelayHashes[relayDataHash].deposit?.fromLiteChain || - v3RelayHashes[relayDataHash].deposit?.toLiteChain - ) { + if (!_canCreateSlowFillLeaf(matchedDeposit)) { return; } // If there is no fill matching the relay hash, then this might be a valid slow fill request // that we should produce a slow fill leaf for. Check if the slow fill request is in the - // destination chain block range and that the underlying deposit has not expired yet. - if ( - slowFillRequest.blockNumber >= destinationChainBlockRange[0] && - // Deposit must not have expired in this bundle. - slowFillRequest.fillDeadline >= bundleBlockTimestamps[destinationChainId][1] - ) { + // destination chain block range. + if (slowFillRequest.blockNumber >= destinationChainBlockRange[0]) { // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit, // so this slow fill request relay data is correct. validatedBundleSlowFills.push(matchedDeposit); @@ -1043,37 +1059,109 @@ export class BundleDataClient { this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Deposit relay hashes should match." ); + v3RelayHashes[relayDataHash].deposit = matchedDeposit; - // slow fill requests for deposits from or to lite chains are considered invalid - if (matchedDeposit.fromLiteChain || matchedDeposit.toLiteChain) { + if (!_canCreateSlowFillLeaf(matchedDeposit)) { return; } - v3RelayHashes[relayDataHash].deposit = matchedDeposit; - // Note: we don't need to query for a historical fill at this point because a fill // cannot precede a slow fill request and if the fill came after the slow fill request, // we would have seen it already because we would have processed it in the loop above. - if ( - // Input and Output tokens must be equivalent on the deposit for this to be slow filled. - !this.clients.hubPoolClient.areTokensEquivalent( - matchedDeposit.inputToken, - matchedDeposit.originChainId, - matchedDeposit.outputToken, - matchedDeposit.destinationChainId, - matchedDeposit.quoteBlockNumber - ) || - // Deposit must not have expired in this bundle. - slowFillRequest.fillDeadline < bundleBlockTimestamps[destinationChainId][1] - ) { - // TODO: Invalid slow fill request. Maybe worth logging. - return; - } validatedBundleSlowFills.push(matchedDeposit); } } ); + // The above loops for adding deposits, fills, and then slow fill requests to the relay hash dictionary + // ignore any events that are after the bundle block range. Because of that, we should consider that there + // are deposits in this bundle that correspond to fills that were sent in a prior bundle that have not + // yet been refunded. These fills are also known as "pre-fills" from here on. + const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); + await mapAsync( + originClient + .getDepositsForDestinationChain(destinationChainId) + // We don't need to check if the deposit is after the bundle block range because it wouldn't have been + // added to v3RelayHashes if it was. Ignore zero value deposits since there is no value to refund. + .filter( + (deposit) => + deposit.blockNumber <= originBlockRange[0] && + deposit.blockNumber >= originBlockRange[1] && + !isZeroValueDeposit(deposit) + ), + async (deposit) => { + // We don't check the deposit's fillDeadline here because we are ok if the deposit expires in this bundle + // and we issue an expiry refund for it. This expired deposit could also have been pre-filled and we just + // want to make sure in this code block that all valid pre-fills get refunded once the deposit appears. + // If a pre-fill gets refunded and its deposit expired and gets refunded as well, then there is no net loss + // to the protocol. + + const relayDataHash = this.getRelayHashFromEvent(deposit); + const fill = v3RelayHashes[relayDataHash].fill; + + // If fill exists in memory, then the only case in which we need to create a refund is if the + // the fill occurred in a previous bundle. + if (fill) { + if (!isSlowFill(fill) && fill.blockNumber < destinationChainBlockRange[0]) { + // If fill is in the current bundle then we can assume there is already a refund for it, so only + // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then + // we won't consider it, following the previous treatment of fills after the bundle block range. + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: deposit.quoteTimestamp, + }); + } + return; + } + + // If fill does not exist in memory but there is a slow fill request in memory, then we need to issue a + // slow fill leaf for the deposit. We can assume there was no fill preceding the slow fill request because + // slow fill requests cannot follow fills. If there were a fill following this request, we would have + // entered the above case. + const slowFillRequest = v3RelayHashes[relayDataHash].slowFillRequest; + if (slowFillRequest) { + if (!_canCreateSlowFillLeaf(deposit)) { + validatedBundleSlowFills.push(deposit); + } + return; + } + + // So at this point in the code, there is no fill or slow fill request in memory for this deposit. + // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. + // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles + // because the spoke pool client lookback would have returned this entire bundle of events and stored + // them into the relay hash dictionary. + const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); + + // If deposit was filled, then we need to issue a refund for it. + if (fillStatus === FillStatus.Filled) { + // We need to find the fill event to issue a refund to the right relayer and repayment chain, + // or msg.sender if relayer address is invalid for the repayment chain. + // TODO: Update findFillBlock to return the full fill event. + const prefill = (await findFillBlock( + destinationClient.spokePool, + deposit, + destinationClient.deploymentBlock, + destinationClient.firstBlockToSearch + )) as unknown as FillWithBlock; + if (!isSlowFill(prefill)) { + validatedBundleV3Fills.push({ + ...prefill, + quoteTimestamp: deposit.quoteTimestamp, + }); + } + } + // If slow fill requested, then issue a slow fill leaf for the deposit. + else if (fillStatus === FillStatus.RequestedSlowFill) { + // Input and Output tokens must be equivalent on the deposit for this to be slow filled. + // Slow fill requests for deposits from or to lite chains are considered invalid + if (_canCreateSlowFillLeaf(deposit)) { + validatedBundleSlowFills.push(deposit); + } + } + } + ); + // For all fills that came after a slow fill request, we can now check if the slow fill request // was a valid one and whether it was created in a previous bundle. If so, then it created a slow fill // leaf that is now unexecutable. @@ -1154,16 +1242,7 @@ export class BundleDataClient { ) { // If we haven't seen a fill matching this deposit, then we need to rule out that it was filled a long time ago // by checkings its on-chain fill status. - const fillStatus = await spokePoolClients[destinationChainId].relayFillStatus( - deposit, - // We can assume that in production - // the block ranges passed into this function would never contain blocks where the spoke pool client - // hasn't queried. This is because this function will usually be called - // in production with block ranges that were validated by - // DataworkerUtils.blockRangesAreInvalidForSpokeClients - Math.min(destinationBlockRange[1], spokePoolClients[destinationChainId].latestBlockSearched), - destinationChainId - ); + const fillStatus = await _getFillStatusForDeposit(deposit, destinationBlockRange[1]); // If there is no matching fill and the deposit expired in this bundle and the fill status on-chain is not // Filled, then we can to refund it as an expired deposit. From 5d817196647533b3be054b336e9c444d243ec0f3 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 08:56:07 -0500 Subject: [PATCH 002/103] re-use v3Relayhashes to get all deposits --- .../BundleDataClient/BundleDataClient.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 2250f3041..e1380f362 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1078,27 +1078,27 @@ export class BundleDataClient { // are deposits in this bundle that correspond to fills that were sent in a prior bundle that have not // yet been refunded. These fills are also known as "pre-fills" from here on. const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); + + // We don't need to check if the deposit is after the bundle block range because it wouldn't have been + // added to v3RelayHashes if it was. Ignore zero value deposits since there is no value to refund. + await mapAsync( - originClient - .getDepositsForDestinationChain(destinationChainId) - // We don't need to check if the deposit is after the bundle block range because it wouldn't have been - // added to v3RelayHashes if it was. Ignore zero value deposits since there is no value to refund. - .filter( - (deposit) => - deposit.blockNumber <= originBlockRange[0] && - deposit.blockNumber >= originBlockRange[1] && - !isZeroValueDeposit(deposit) - ), - async (deposit) => { + Object.values(v3RelayHashes).filter( + ({ deposit }) => + deposit && + deposit.originChainId === originChainId && + deposit.destinationChainId === destinationChainId && + deposit.blockNumber <= originBlockRange[0] && + !isZeroValueDeposit(deposit) + ), + async ({ deposit, fill, slowFillRequest }) => { + if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); // We don't check the deposit's fillDeadline here because we are ok if the deposit expires in this bundle // and we issue an expiry refund for it. This expired deposit could also have been pre-filled and we just // want to make sure in this code block that all valid pre-fills get refunded once the deposit appears. // If a pre-fill gets refunded and its deposit expired and gets refunded as well, then there is no net loss // to the protocol. - const relayDataHash = this.getRelayHashFromEvent(deposit); - const fill = v3RelayHashes[relayDataHash].fill; - // If fill exists in memory, then the only case in which we need to create a refund is if the // the fill occurred in a previous bundle. if (fill) { @@ -1118,7 +1118,6 @@ export class BundleDataClient { // slow fill leaf for the deposit. We can assume there was no fill preceding the slow fill request because // slow fill requests cannot follow fills. If there were a fill following this request, we would have // entered the above case. - const slowFillRequest = v3RelayHashes[relayDataHash].slowFillRequest; if (slowFillRequest) { if (!_canCreateSlowFillLeaf(deposit)) { validatedBundleSlowFills.push(deposit); From b6053c7a7e7e488254bcca41e64129e50ac677c5 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:17:50 -0500 Subject: [PATCH 003/103] Update src/clients/BundleDataClient/BundleDataClient.ts Co-authored-by: bmzig <57361391+bmzig@users.noreply.github.com> --- src/clients/BundleDataClient/BundleDataClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index e1380f362..6fb0c697a 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1119,7 +1119,7 @@ export class BundleDataClient { // slow fill requests cannot follow fills. If there were a fill following this request, we would have // entered the above case. if (slowFillRequest) { - if (!_canCreateSlowFillLeaf(deposit)) { + if (_canCreateSlowFillLeaf(deposit)) { validatedBundleSlowFills.push(deposit); } return; From f91d4dd4c2432c7611f5dbed314f65dbea5debf9 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 10:31:45 -0500 Subject: [PATCH 004/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 6fb0c697a..79aafd95d 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1088,7 +1088,7 @@ export class BundleDataClient { deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId && - deposit.blockNumber <= originBlockRange[0] && + deposit.blockNumber >= originBlockRange[0] && !isZeroValueDeposit(deposit) ), async ({ deposit, fill, slowFillRequest }) => { From 20325f14d480601f16ba40f1f74170eaa568535e Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 11:38:57 -0500 Subject: [PATCH 005/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 79aafd95d..319e62250 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1117,9 +1117,10 @@ export class BundleDataClient { // If fill does not exist in memory but there is a slow fill request in memory, then we need to issue a // slow fill leaf for the deposit. We can assume there was no fill preceding the slow fill request because // slow fill requests cannot follow fills. If there were a fill following this request, we would have - // entered the above case. + // entered the above case. Again as with pre-fills, we should only consider slow fill requests that were + // in previous bundles. if (slowFillRequest) { - if (_canCreateSlowFillLeaf(deposit)) { + if (_canCreateSlowFillLeaf(deposit) && slowFillRequest.blockNumber < destinationChainBlockRange[0]) { validatedBundleSlowFills.push(deposit); } return; From 8ed54b8b4fdf3351c33435bf737a876bc789ad4f Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 13:53:27 -0500 Subject: [PATCH 006/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 319e62250..e0b519f51 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1079,9 +1079,6 @@ export class BundleDataClient { // yet been refunded. These fills are also known as "pre-fills" from here on. const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); - // We don't need to check if the deposit is after the bundle block range because it wouldn't have been - // added to v3RelayHashes if it was. Ignore zero value deposits since there is no value to refund. - await mapAsync( Object.values(v3RelayHashes).filter( ({ deposit }) => @@ -1089,6 +1086,7 @@ export class BundleDataClient { deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId && deposit.blockNumber >= originBlockRange[0] && + deposit.blockNumber <= originBlockRange[1] && !isZeroValueDeposit(deposit) ), async ({ deposit, fill, slowFillRequest }) => { From d3be3126f1c77cec7d8c81d232cc31075e3a7061 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:31:35 -0500 Subject: [PATCH 007/103] feat: Add findFillEvent utility function (#836) --- src/utils/SpokeUtils.ts | 37 ++++++++++++++++++++++++++++++++++- test/SpokePoolClient.fills.ts | 20 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 23962bd08..18725d56f 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,12 +1,13 @@ import assert from "assert"; import { BytesLike, Contract, PopulatedTransaction, providers, utils as ethersUtils } from "ethers"; import { CHAIN_IDs, MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS } from "../constants"; -import { Deposit, Fill, FillStatus, RelayData, SlowFillRequest } from "../interfaces"; +import { Deposit, Fill, FillStatus, FillWithBlock, RelayData, SlowFillRequest } from "../interfaces"; import { SpokePoolClient } from "../clients"; import { chunk } from "./ArrayUtils"; import { BigNumber, toBN } from "./BigNumberUtils"; import { isDefined } from "./TypeGuards"; import { getNetworkName } from "./NetworkUtils"; +import { paginatedEventQuery, spreadEventWithBlockNumber } from "./EventUtils"; type BlockTag = providers.BlockTag; @@ -380,3 +381,37 @@ export async function findFillBlock( return lowBlockNumber; } + +export async function findFillEvent( + spokePool: Contract, + relayData: RelayData, + lowBlockNumber: number, + highBlockNumber?: number +): Promise { + const blockNumber = await findFillBlock(spokePool, relayData, lowBlockNumber, highBlockNumber); + if (!blockNumber) return undefined; + const query = await paginatedEventQuery( + spokePool, + spokePool.filters.FilledV3Relay(null, null, null, null, null, relayData.originChainId, relayData.depositId), + { + fromBlock: blockNumber, + toBlock: blockNumber, + maxBlockLookBack: 0, // We can hardcode this to 0 to instruct paginatedEventQuery to make a single request + // for the same block number. + } + ); + if (query.length === 0) throw new Error(`Failed to find fill event at block ${blockNumber}`); + const event = query[0]; + // In production the chainId returned from the provider matches 1:1 with the actual chainId. Querying the provider + // object saves an RPC query becasue the chainId is cached by StaticJsonRpcProvider instances. In hre, the SpokePool + // may be configured with a different chainId than what is returned by the provider. + // @todo Sub out actual chain IDs w/ CHAIN_IDs constants + const destinationChainId = Object.values(CHAIN_IDs).includes(relayData.originChainId) + ? (await spokePool.provider.getNetwork()).chainId + : Number(await spokePool.chainId()); + const fill = { + ...spreadEventWithBlockNumber(event), + destinationChainId, + } as FillWithBlock; + return fill; +} diff --git a/test/SpokePoolClient.fills.ts b/test/SpokePoolClient.fills.ts index 742dddf92..222617fa2 100644 --- a/test/SpokePoolClient.fills.ts +++ b/test/SpokePoolClient.fills.ts @@ -1,7 +1,7 @@ import hre from "hardhat"; import { SpokePoolClient } from "../src/clients"; import { Deposit } from "../src/interfaces"; -import { bnOne, findFillBlock, getNetworkName } from "../src/utils"; +import { bnOne, findFillBlock, findFillEvent, getNetworkName } from "../src/utils"; import { EMPTY_MESSAGE, ZERO_ADDRESS } from "../src/constants"; import { originChainId, destinationChainId } from "./constants"; import { @@ -114,6 +114,24 @@ describe("SpokePoolClient: Fills", function () { expect(fillBlock).to.equal(targetFillBlock); }); + it("Correctly returns the FilledV3Relay event using the relay data", async function () { + const targetDeposit = { ...deposit, depositId: deposit.depositId + 1 }; + // Submit multiple fills at the same block: + const startBlock = await spokePool.provider.getBlockNumber(); + await fillV3Relay(spokePool, deposit, relayer1); + await fillV3Relay(spokePool, targetDeposit, relayer1); + await fillV3Relay(spokePool, { ...deposit, depositId: deposit.depositId + 2 }, relayer1); + await hre.network.provider.send("evm_mine"); + + const fill = await findFillEvent(spokePool, targetDeposit, startBlock); + expect(fill).to.not.be.undefined; + expect(fill!.depositId).to.equal(targetDeposit.depositId); + + // Looking for a fill can return undefined: + const missingFill = await findFillEvent(spokePool, { ...deposit, depositId: deposit.depositId + 3 }, startBlock); + expect(missingFill).to.be.undefined; + }); + it("FilledV3Relay block search: bounds checking", async function () { const nBlocks = 100; const startBlock = await spokePool.provider.getBlockNumber(); From 23d9a6eb1cce655208825b74a77b0869650d183d Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 15:31:55 -0500 Subject: [PATCH 008/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index e0b519f51..d1aeae98c 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -35,7 +35,7 @@ import { mapAsync, bnUint32Max, isZeroValueDeposit, - findFillBlock, + findFillEvent, } from "../../utils"; import winston from "winston"; import { @@ -1135,12 +1135,11 @@ export class BundleDataClient { if (fillStatus === FillStatus.Filled) { // We need to find the fill event to issue a refund to the right relayer and repayment chain, // or msg.sender if relayer address is invalid for the repayment chain. - // TODO: Update findFillBlock to return the full fill event. - const prefill = (await findFillBlock( + const prefill = (await findFillEvent( destinationClient.spokePool, deposit, destinationClient.deploymentBlock, - destinationClient.firstBlockToSearch + destinationClient.latestBlockSearched )) as unknown as FillWithBlock; if (!isSlowFill(prefill)) { validatedBundleV3Fills.push({ From 80e5b1cdc5c9ac19c691a66adbd940b42287aa2c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 15:56:37 -0500 Subject: [PATCH 009/103] feat(BundleDataClient): Support duplicate expired deposit refunds I think these are the changes needed but I need to consider more the impact if two deposits expire and they both create slow fill excesses, for example, or other weird edge cases. --- .../BundleDataClient/BundleDataClient.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index fd992c95d..7800ad768 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -765,9 +765,9 @@ export class BundleDataClient { // Process all deposits first and keep track of deposits that may be refunded as an expired deposit: // - expiredBundleDepositHashes: Deposits sent in this bundle that expired. - const expiredBundleDepositHashes: Set = new Set(); + const expiredBundleDepositHashes: string[] = []; // - olderDepositHashes: Deposits sent in a prior bundle that newly expired in this bundle - const olderDepositHashes: Set = new Set(); + const olderDepositHashes: string[] = []; let depositCounter = 0; for (const originChainId of allChainIds) { @@ -784,18 +784,15 @@ export class BundleDataClient { } depositCounter++; const relayDataHash = this.getRelayHashFromEvent(deposit); - if (v3RelayHashes[relayDataHash]) { - // If we've seen this deposit before, then skip this deposit. This can happen if our RPC provider - // gives us bad data. - return; + if (!v3RelayHashes[relayDataHash]) { + // Even if deposit is not in bundle block range, store all deposits we can see in memory in this + // convenient dictionary. + v3RelayHashes[relayDataHash] = { + deposit: deposit, + fill: undefined, + slowFillRequest: undefined, + }; } - // Even if deposit is not in bundle block range, store all deposits we can see in memory in this - // convenient dictionary. - v3RelayHashes[relayDataHash] = { - deposit: deposit, - fill: undefined, - slowFillRequest: undefined, - }; // Once we've saved the deposit hash into v3RelayHashes, then we can exit early here if the inputAmount // is 0 because there can be no expired amount to refund and no unexecutable slow fill amount to return @@ -817,10 +814,10 @@ export class BundleDataClient { // for example. Those should be impossible to create but technically should be included in this // bundle of refunded deposits. if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { - expiredBundleDepositHashes.add(relayDataHash); + expiredBundleDepositHashes.push(relayDataHash); } } else if (deposit.blockNumber < originChainBlockRange[0]) { - olderDepositHashes.add(relayDataHash); + olderDepositHashes.push(relayDataHash); } }); } @@ -1120,7 +1117,8 @@ export class BundleDataClient { start = performance.now(); // Go through expired deposits in this bundle and now prune those that we have seen a fill for to construct - // the list of expired deposits we need to refund in this bundle. + // the list of expired deposits we need to refund in this bundle. This handles the case where a deposit is + // duplicated and one refund will be sent for each duplicated deposit. expiredBundleDepositHashes.forEach((relayDataHash) => { const { deposit, fill } = v3RelayHashes[relayDataHash]; assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); From c60849979753b32e7edaedd94e984f1f0a9c8e1c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 18:06:15 -0500 Subject: [PATCH 010/103] Handle duplicate deposits --- src/clients/BundleDataClient/BundleDataClient.ts | 2 +- src/clients/SpokePoolClient.ts | 16 +++++++++++++++- src/clients/mocks/MockSpokePoolClient.ts | 1 - 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 7800ad768..513481b9b 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -778,7 +778,7 @@ export class BundleDataClient { if (originChainId === destinationChainId) { continue; } - originClient.getDepositsForDestinationChain(destinationChainId).forEach((deposit) => { + originClient.getDepositsForDestinationChainWithDuplicates(destinationChainId).forEach((deposit) => { if (isZeroValueDeposit(deposit)) { return; } diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index ef1b3173f..8d86cd4b2 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -65,6 +65,7 @@ export class SpokePoolClient extends BaseAbstractClient { protected currentTime = 0; protected oldestTime = 0; protected depositHashes: { [depositHash: string]: DepositWithBlock } = {}; + protected duplicateDepositHashes: { [depositHash: string]: DepositWithBlock[] } = {}; protected depositHashesToFills: { [depositHash: string]: FillWithBlock[] } = {}; protected speedUps: { [depositorAddress: string]: { [depositId: number]: SpeedUpWithBlock[] } } = {}; protected slowFillRequests: { [relayDataHash: string]: SlowFillRequestWithBlock } = {}; @@ -124,7 +125,7 @@ export class SpokePoolClient extends BaseAbstractClient { } /** - * Retrieves a list of deposits from the SpokePool contract destined for the given destination chain ID. + * Retrieves a list of unique deposits from the SpokePool contract destined for the given destination chain ID. * @param destinationChainId The destination chain ID. * @returns A list of deposits. */ @@ -132,6 +133,18 @@ export class SpokePoolClient extends BaseAbstractClient { return Object.values(this.depositHashes).filter((deposit) => deposit.destinationChainId === destinationChainId); } + /** + * Returns a list of all deposits including any duplicate ones. Designed only to be used in use cases where + * all deposits are required, regardless of duplicates. For example, the Dataworker can use this to refund + * expired deposits including for duplicates. + * @param destinationChainId + * @returns A list of deposits + */ + public getDepositsForDestinationChainWithDuplicates(destinationChainId: number): DepositWithBlock[] { + const deposits = this.getDepositsForDestinationChain(destinationChainId); + return sortEventsAscendingInPlace(deposits.concat(Object.values(this.duplicateDepositHashes).flat())); + } + /** * Retrieves a list of deposits from the SpokePool contract that are associated with this spoke pool. * @returns A list of deposits. @@ -575,6 +588,7 @@ export class SpokePoolClient extends BaseAbstractClient { } if (this.depositHashes[this.getDepositHash(deposit)] !== undefined) { + assign(this.duplicateDepositHashes, [this.getDepositHash(deposit)], deposit); continue; } assign(this.depositHashes, [this.getDepositHash(deposit)], deposit); diff --git a/src/clients/mocks/MockSpokePoolClient.ts b/src/clients/mocks/MockSpokePoolClient.ts index 7e9f2ed90..ec37ad44c 100644 --- a/src/clients/mocks/MockSpokePoolClient.ts +++ b/src/clients/mocks/MockSpokePoolClient.ts @@ -122,7 +122,6 @@ export class MockSpokePoolClient extends SpokePoolClient { const { blockNumber, transactionIndex } = deposit; let { depositId, depositor, destinationChainId, inputToken, inputAmount, outputToken, outputAmount } = deposit; depositId ??= this.numberOfDeposits; - assert(depositId >= this.numberOfDeposits, `${depositId} < ${this.numberOfDeposits}`); this.numberOfDeposits = depositId + 1; destinationChainId ??= random(1, 42161, false); From 9cbded75c66dace1356bb118a81c9222b332b711 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 18:12:28 -0500 Subject: [PATCH 011/103] Update SpokePoolClient.ts --- src/clients/SpokePoolClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 8d86cd4b2..3f74340f9 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -137,7 +137,7 @@ export class SpokePoolClient extends BaseAbstractClient { * Returns a list of all deposits including any duplicate ones. Designed only to be used in use cases where * all deposits are required, regardless of duplicates. For example, the Dataworker can use this to refund * expired deposits including for duplicates. - * @param destinationChainId + * @param destinationChainId * @returns A list of deposits */ public getDepositsForDestinationChainWithDuplicates(destinationChainId: number): DepositWithBlock[] { From 828c55eedd6335c8f2c4525553cab237a03fa54c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 22:12:16 -0500 Subject: [PATCH 012/103] Fix duplicate deposit logic --- src/clients/BundleDataClient/BundleDataClient.ts | 2 ++ src/clients/SpokePoolClient.ts | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index c5082b9b6..0d8b90603 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1076,6 +1076,8 @@ export class BundleDataClient { // yet been refunded. These fills are also known as "pre-fills" from here on. const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); + // We don't iterate through deposits again here because we only need to consider unique deposit hashes + // to look for pre-fills. Duplicate deposits could only have been pre-filled once. await mapAsync( Object.values(v3RelayHashes).filter( ({ deposit }) => diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 3f74340f9..8130e19c6 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -142,7 +142,10 @@ export class SpokePoolClient extends BaseAbstractClient { */ public getDepositsForDestinationChainWithDuplicates(destinationChainId: number): DepositWithBlock[] { const deposits = this.getDepositsForDestinationChain(destinationChainId); - return sortEventsAscendingInPlace(deposits.concat(Object.values(this.duplicateDepositHashes).flat())); + const duplicateDeposits = Object.values(this.duplicateDepositHashes).filter( + (deposits) => deposits.length > 0 && deposits[0].destinationChainId === destinationChainId + ); + return sortEventsAscendingInPlace(deposits.concat(duplicateDeposits.flat())); } /** @@ -588,7 +591,7 @@ export class SpokePoolClient extends BaseAbstractClient { } if (this.depositHashes[this.getDepositHash(deposit)] !== undefined) { - assign(this.duplicateDepositHashes, [this.getDepositHash(deposit)], deposit); + assign(this.duplicateDepositHashes, [this.getDepositHash(deposit)], [deposit]); continue; } assign(this.depositHashes, [this.getDepositHash(deposit)], deposit); From 45e75eba6fea35ddb9484cc1526b0f5699897c11 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 22:26:30 -0500 Subject: [PATCH 013/103] Add caveats about duplicate deposits --- src/clients/BundleDataClient/BundleDataClient.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index d1aeae98c..6170c6d77 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1101,6 +1101,12 @@ export class BundleDataClient { // the fill occurred in a previous bundle. if (fill) { if (!isSlowFill(fill) && fill.blockNumber < destinationChainBlockRange[0]) { + // TODO: Make sure this is the first time we have seen this deposit and that this is not a + // duplicate deposit. Otherwise duplicate deposits can be used to refund fillers multiple times. + // This check wouldn't be necessary if pre-fills cannot precede deposits by more than the + // fillDeadlineBuffer. In this case, we'd always have duplicate deposits and the original deposit + // in-memory and therefore we can check if this is the first deposit. + // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. @@ -1119,6 +1125,10 @@ export class BundleDataClient { // in previous bundles. if (slowFillRequest) { if (_canCreateSlowFillLeaf(deposit) && slowFillRequest.blockNumber < destinationChainBlockRange[0]) { + // TODO: Make sure this is the first time we have seen this deposit and that this is not a + // duplicate deposit. This isn't as critical as with pre-fills because a slow fill leaf can only be + // executed once for a unique relay data hash. + validatedBundleSlowFills.push(deposit); } return; From c9184e4d96c4e0e3cf637d282c030aa8c22812a2 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 22:30:38 -0500 Subject: [PATCH 014/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 0d8b90603..ac96832a0 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -849,8 +849,7 @@ export class BundleDataClient { updateBundleDepositsV3(bundleDepositsV3, deposit); // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 - // for example. Those should be impossible to create but technically should be included in this - // bundle of refunded deposits. + // for example. Those should be included in this bundle of refunded deposits. if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { expiredBundleDepositHashes.push(relayDataHash); } From 886a5e08a219274e4b860a67320e4da0fde36fa0 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 22:40:07 -0500 Subject: [PATCH 015/103] Handle expired deposits better --- .../BundleDataClient/BundleDataClient.ts | 140 ++++++++---------- 1 file changed, 60 insertions(+), 80 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 6170c6d77..ce14a171c 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1101,12 +1101,6 @@ export class BundleDataClient { // the fill occurred in a previous bundle. if (fill) { if (!isSlowFill(fill) && fill.blockNumber < destinationChainBlockRange[0]) { - // TODO: Make sure this is the first time we have seen this deposit and that this is not a - // duplicate deposit. Otherwise duplicate deposits can be used to refund fillers multiple times. - // This check wouldn't be necessary if pre-fills cannot precede deposits by more than the - // fillDeadlineBuffer. In this case, we'd always have duplicate deposits and the original deposit - // in-memory and therefore we can check if this is the first deposit. - // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. @@ -1125,10 +1119,6 @@ export class BundleDataClient { // in previous bundles. if (slowFillRequest) { if (_canCreateSlowFillLeaf(deposit) && slowFillRequest.blockNumber < destinationChainBlockRange[0]) { - // TODO: Make sure this is the first time we have seen this deposit and that this is not a - // duplicate deposit. This isn't as critical as with pre-fills because a slow fill leaf can only be - // executed once for a unique relay data hash. - validatedBundleSlowFills.push(deposit); } return; @@ -1214,80 +1204,70 @@ export class BundleDataClient { }); start = performance.now(); - // Go through expired deposits in this bundle and now prune those that we have seen a fill for to construct - // the list of expired deposits we need to refund in this bundle. - expiredBundleDepositHashes.forEach((relayDataHash) => { - const { deposit, fill } = v3RelayHashes[relayDataHash]; - assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); - if ( - !fill && - isDefined(deposit) // Needed for TSC - we check this above. - ) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } - }); - - // For all deposits older than this bundle, we need to check if they expired in this bundle and if they did, - // whether there was a slow fill created for it in a previous bundle that is now unexecutable and replaced - // by a new expired deposit refund. - await forEachAsync(Array.from(olderDepositHashes), async (relayDataHash) => { - const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; - assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); - const { destinationChainId } = deposit!; - const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); - - // Only look for deposits that were mined before this bundle and that are newly expired. - // If the fill deadline is lower than the bundle start block on the destination chain, then - // we should assume it was marked "newly expired" and refunded in a previous bundle. - if ( - // If there is a valid fill that we saw matching this deposit, then it does not need a refund. - !fill && - isDefined(deposit) && // Needed for TSC - we check this above. - deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && - deposit.fillDeadline >= bundleBlockTimestamps[destinationChainId][0] && - spokePoolClients[destinationChainId] !== undefined - ) { - // If we haven't seen a fill matching this deposit, then we need to rule out that it was filled a long time ago - // by checkings its on-chain fill status. - const fillStatus = await _getFillStatusForDeposit(deposit, destinationBlockRange[1]); - - // If there is no matching fill and the deposit expired in this bundle and the fill status on-chain is not - // Filled, then we can to refund it as an expired deposit. - if (fillStatus !== FillStatus.Filled) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } - // If fill status is RequestedSlowFill, then we might need to mark down an unexecutable - // slow fill that we're going to replace with an expired deposit refund. - // If deposit cannot be slow filled, then exit early. - // slow fill requests for deposits from or to lite chains are considered invalid - if (fillStatus !== FillStatus.RequestedSlowFill || deposit.fromLiteChain || deposit.toLiteChain) { - return; - } - // Now, check if there was a slow fill created for this deposit in a previous bundle which would now be - // unexecutable. Mark this deposit as having created an unexecutable slow fill if there is no matching - // slow fill request or the matching slow fill request took place in a previous bundle. - - // If there is a slow fill request in this bundle, then the expired deposit refund will supercede - // the slow fill request. If there is no slow fill request seen or its older than this bundle, then we can - // assume a slow fill leaf was created for it because its tokens are equivalent. The slow fill request was - // also sent before the fill deadline expired since we checked that above. + // Add any newly expired deposits to the list of expired deposits to refund. + // For these refunds, we need to check whether there was a slow fill created for it in a previous bundle + // that is now unexecutable and replaced by a new expired deposit refund. + await forEachAsync( + Array.from(olderDepositHashes).concat(Array.from(expiredBundleDepositHashes)), + async (relayDataHash) => { + const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; + assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); + const { destinationChainId } = deposit!; + const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); + + // Only look for deposits that were mined before this bundle and that are newly expired. + // If the fill deadline is lower than the bundle start block on the destination chain, then + // we should assume it was marked "newly expired" and refunded in a previous bundle. if ( - // Since this deposit was requested for a slow fill in an older bundle at this point, we don't - // technically need to check if the slow fill request was valid since we can assume all bundles in the past - // were validated. However, we might as well double check. - this.clients.hubPoolClient.areTokensEquivalent( - deposit.inputToken, - deposit.originChainId, - deposit.outputToken, - deposit.destinationChainId, - deposit.quoteBlockNumber - ) && - (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) + // If there is a valid fill that we saw matching this deposit, then it does not need a refund. + !fill && + isDefined(deposit) && // Needed for TSC - we check this above. + deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && + deposit.fillDeadline >= bundleBlockTimestamps[destinationChainId][0] && + spokePoolClients[destinationChainId] !== undefined ) { - validatedBundleUnexecutableSlowFills.push(deposit); + // If we haven't seen a fill matching this deposit, then we need to rule out that it was filled a long time ago + // by checkings its on-chain fill status. + const fillStatus = await _getFillStatusForDeposit(deposit, destinationBlockRange[1]); + + // If there is no matching fill and the deposit expired in this bundle and the fill status on-chain is not + // Filled, then we can to refund it as an expired deposit. + if (fillStatus !== FillStatus.Filled) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } + // If fill status is RequestedSlowFill, then we might need to mark down an unexecutable + // slow fill that we're going to replace with an expired deposit refund. + // If deposit cannot be slow filled, then exit early. + // slow fill requests for deposits from or to lite chains are considered invalid + if (fillStatus !== FillStatus.RequestedSlowFill || deposit.fromLiteChain || deposit.toLiteChain) { + return; + } + // Now, check if there was a slow fill created for this deposit in a previous bundle which would now be + // unexecutable. Mark this deposit as having created an unexecutable slow fill if there is no matching + // slow fill request or the matching slow fill request took place in a previous bundle. + + // If there is a slow fill request in this bundle, then the expired deposit refund will supercede + // the slow fill request. If there is no slow fill request seen or its older than this bundle, then we can + // assume a slow fill leaf was created for it because its tokens are equivalent. The slow fill request was + // also sent before the fill deadline expired since we checked that above. + if ( + // Since this deposit was requested for a slow fill in an older bundle at this point, we don't + // technically need to check if the slow fill request was valid since we can assume all bundles in the past + // were validated. However, we might as well double check. + this.clients.hubPoolClient.areTokensEquivalent( + deposit.inputToken, + deposit.originChainId, + deposit.outputToken, + deposit.destinationChainId, + deposit.quoteBlockNumber + ) && + (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) + ) { + validatedBundleUnexecutableSlowFills.push(deposit); + } } } - }); + ); // Batch compute V3 lp fees. start = performance.now(); From 1fc293a48b2fe770f8025e00ef59599771bc8adc Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 22:42:29 -0500 Subject: [PATCH 016/103] Update BundleDataClient.ts --- .../BundleDataClient/BundleDataClient.ts | 112 +++++++++--------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index ce14a171c..cb479f3ed 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1207,67 +1207,65 @@ export class BundleDataClient { // Add any newly expired deposits to the list of expired deposits to refund. // For these refunds, we need to check whether there was a slow fill created for it in a previous bundle // that is now unexecutable and replaced by a new expired deposit refund. - await forEachAsync( - Array.from(olderDepositHashes).concat(Array.from(expiredBundleDepositHashes)), - async (relayDataHash) => { - const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; - assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); - const { destinationChainId } = deposit!; - const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); - - // Only look for deposits that were mined before this bundle and that are newly expired. - // If the fill deadline is lower than the bundle start block on the destination chain, then - // we should assume it was marked "newly expired" and refunded in a previous bundle. + const possibleExpiredDeposits = Array.from(olderDepositHashes).concat(Array.from(expiredBundleDepositHashes)); + await forEachAsync(possibleExpiredDeposits, async (relayDataHash) => { + const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; + assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); + const { destinationChainId } = deposit!; + const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); + + // Only look for deposits that were mined before this bundle and that are newly expired. + // If the fill deadline is lower than the bundle start block on the destination chain, then + // we should assume it was marked "newly expired" and refunded in a previous bundle. + if ( + // If there is a valid fill that we saw matching this deposit, then it does not need a refund. + !fill && + isDefined(deposit) && // Needed for TSC - we check this above. + deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && + deposit.fillDeadline >= bundleBlockTimestamps[destinationChainId][0] && + spokePoolClients[destinationChainId] !== undefined + ) { + // If we haven't seen a fill matching this deposit, then we need to rule out that it was filled a long time ago + // by checkings its on-chain fill status. + const fillStatus = await _getFillStatusForDeposit(deposit, destinationBlockRange[1]); + + // If there is no matching fill and the deposit expired in this bundle and the fill status on-chain is not + // Filled, then we can to refund it as an expired deposit. + if (fillStatus !== FillStatus.Filled) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } + // If fill status is RequestedSlowFill, then we might need to mark down an unexecutable + // slow fill that we're going to replace with an expired deposit refund. + // If deposit cannot be slow filled, then exit early. + // slow fill requests for deposits from or to lite chains are considered invalid + if (fillStatus !== FillStatus.RequestedSlowFill || deposit.fromLiteChain || deposit.toLiteChain) { + return; + } + // Now, check if there was a slow fill created for this deposit in a previous bundle which would now be + // unexecutable. Mark this deposit as having created an unexecutable slow fill if there is no matching + // slow fill request or the matching slow fill request took place in a previous bundle. + + // If there is a slow fill request in this bundle, then the expired deposit refund will supercede + // the slow fill request. If there is no slow fill request seen or its older than this bundle, then we can + // assume a slow fill leaf was created for it because its tokens are equivalent. The slow fill request was + // also sent before the fill deadline expired since we checked that above. if ( - // If there is a valid fill that we saw matching this deposit, then it does not need a refund. - !fill && - isDefined(deposit) && // Needed for TSC - we check this above. - deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && - deposit.fillDeadline >= bundleBlockTimestamps[destinationChainId][0] && - spokePoolClients[destinationChainId] !== undefined + // Since this deposit was requested for a slow fill in an older bundle at this point, we don't + // technically need to check if the slow fill request was valid since we can assume all bundles in the past + // were validated. However, we might as well double check. + this.clients.hubPoolClient.areTokensEquivalent( + deposit.inputToken, + deposit.originChainId, + deposit.outputToken, + deposit.destinationChainId, + deposit.quoteBlockNumber + ) && + (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) ) { - // If we haven't seen a fill matching this deposit, then we need to rule out that it was filled a long time ago - // by checkings its on-chain fill status. - const fillStatus = await _getFillStatusForDeposit(deposit, destinationBlockRange[1]); - - // If there is no matching fill and the deposit expired in this bundle and the fill status on-chain is not - // Filled, then we can to refund it as an expired deposit. - if (fillStatus !== FillStatus.Filled) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } - // If fill status is RequestedSlowFill, then we might need to mark down an unexecutable - // slow fill that we're going to replace with an expired deposit refund. - // If deposit cannot be slow filled, then exit early. - // slow fill requests for deposits from or to lite chains are considered invalid - if (fillStatus !== FillStatus.RequestedSlowFill || deposit.fromLiteChain || deposit.toLiteChain) { - return; - } - // Now, check if there was a slow fill created for this deposit in a previous bundle which would now be - // unexecutable. Mark this deposit as having created an unexecutable slow fill if there is no matching - // slow fill request or the matching slow fill request took place in a previous bundle. - - // If there is a slow fill request in this bundle, then the expired deposit refund will supercede - // the slow fill request. If there is no slow fill request seen or its older than this bundle, then we can - // assume a slow fill leaf was created for it because its tokens are equivalent. The slow fill request was - // also sent before the fill deadline expired since we checked that above. - if ( - // Since this deposit was requested for a slow fill in an older bundle at this point, we don't - // technically need to check if the slow fill request was valid since we can assume all bundles in the past - // were validated. However, we might as well double check. - this.clients.hubPoolClient.areTokensEquivalent( - deposit.inputToken, - deposit.originChainId, - deposit.outputToken, - deposit.destinationChainId, - deposit.quoteBlockNumber - ) && - (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) - ) { - validatedBundleUnexecutableSlowFills.push(deposit); - } + validatedBundleUnexecutableSlowFills.push(deposit); } } - ); + }); // Batch compute V3 lp fees. start = performance.now(); From 6569e60e4ac1171ed60feea7fcf4d773080a6388 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 22:44:59 -0500 Subject: [PATCH 017/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 4c4682c02..9c378a4fd 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1205,7 +1205,8 @@ export class BundleDataClient { // Add any newly expired deposits to the list of expired deposits to refund. // For these refunds, we need to check whether there was a slow fill created for it in a previous bundle // that is now unexecutable and replaced by a new expired deposit refund. - await forEachAsync(olderDepositHashes.concat(expiredBundleDepositHashes), async (relayDataHash) => { + const possibleExpiredDeposits =olderDepositHashes.concat(expiredBundleDepositHashes); + await forEachAsync(possibleExpiredDeposits, async (relayDataHash) => { const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); const { destinationChainId } = deposit!; From 09d55f88271ff2306cdab90d336c272ebcccc9eb Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 22:48:06 -0500 Subject: [PATCH 018/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 0d7fa4cb1..1b9a650ac 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1095,6 +1095,12 @@ export class BundleDataClient { // If a pre-fill gets refunded and its deposit expired and gets refunded as well, then there is no net loss // to the protocol. + // We don't check here that this deposit is a duplicate because we are willing to refund a pre-fill + // even if this deposit is a duplicate. This is because a duplicate deposit for a pre-fill cannot get + // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore + // any duplicate deposits result in a net loss of funds for the depositor and effectively payout + // the pre-filler. + // If fill exists in memory, then the only case in which we need to create a refund is if the // the fill occurred in a previous bundle. if (fill) { From 29be5485828c8a34e21ce246b8742b0d329fbe98 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 23 Jan 2025 23:11:19 -0500 Subject: [PATCH 019/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index cb479f3ed..49a298bcd 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1214,7 +1214,6 @@ export class BundleDataClient { const { destinationChainId } = deposit!; const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); - // Only look for deposits that were mined before this bundle and that are newly expired. // If the fill deadline is lower than the bundle start block on the destination chain, then // we should assume it was marked "newly expired" and refunded in a previous bundle. if ( From 75ba3841af9591cb15e0fd27e489732ef0764653 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 00:19:25 -0500 Subject: [PATCH 020/103] Fix expired deposit loigc --- .../BundleDataClient/BundleDataClient.ts | 135 +++++++----------- 1 file changed, 51 insertions(+), 84 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 49a298bcd..57dc21005 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -681,9 +681,7 @@ export class BundleDataClient { ) && // Cannot slow fill from or to a lite chain. !deposit.fromLiteChain && - !deposit.toLiteChain && - // Deposit must not expire during this bundle. - deposit.fillDeadline >= bundleBlockTimestamps[deposit.destinationChainId][1] + !deposit.toLiteChain ); }; @@ -730,59 +728,6 @@ export class BundleDataClient { bundleBlockTimestamps = _cachedBundleTimestamps; } - /** ***************************** - * - * Handle V3 events - * - * *****************************/ - - // The methodology here is roughly as follows - // - Query all deposits from SpokePoolClients - // - If deposit is in origin chain block range, add it to bundleDepositsV3 - // - If deposit is expired or from an older bundle, stash it away as a deposit that may require an expired - // deposit refund. - // - Query fills from SpokePoolClients - // - If fill is in destination chain block range, then validate fill - // - Fill is valid if its RelayData hash is identical to a deposit's relay data hash that we've already seen. - // If we haven't seen a deposit with a matching hash, then we need to query for an older deposit earlier than - // the SpokePoolClient's lookback window via queryHistoricalDepositForFill(). - // - If fill is valid, then add it to bundleFillsV3. If it's a slow fill execution, we won't - // add a relayer refund for it, but all fills accumulate realized LP fees. - // - If fill replaced a slow fill request, then stash it away as one that potentially created an - // unexecutable slow fill. - // - Query slow fills from SpokePoolClients - // - If slow fill is in destination chain block range, then validate slow fill - // - Slow fill is valid if its RelayData hash is identical to a deposit's relay data hash that we've already seen, - // and it does not match with a Fill that we've seen, and its input and output tokens are equivalent, - // and the deposit that is being slow filled has not expired. - // - Note that if we haven't can't match the slow fill with a deposit, then we need to query for an older - // deposit earlier than the SpokePoolClient's lookback window via queryHistoricalDepositForFill(). - // - input and output tokens are considered equivalent if they map to the same L1 token via a PoolRebalanceRoute - // at the deposit.quoteBlockNumber. - // - To validate fills that replaced slow fills, we should check that there is no slow fill request in the - // current destination chain bundle block range with a matching relay hash. Additionally, the - // fast fill replacing a slow fill must have filled a slow-fill eligible deposit meaning that - // its input and output tokens are equivalent. We don't need to check that the slow fill was created - // before the deposit expired by definition because the deposit was fast-filled, meaning that it did not - // expire. - // - To validate deposits in the current bundle block range that expired newly in this destination - // chain's current bundle block range, we only have to check that the deposit was not filled in the current - // destination chain block range. - // - To validate deposits from a prior bundle that expired newly, we need to make sure that the deposit - // was not filled. If we can't find a fill, then we should check its FillStatus on-chain via eth_call. - // This will return either Unfilled, RequestedSlowFill, or Filled. If the deposit is Filled, then - // then the fill happened a long time ago and we should do nothing. If the deposit is Unfilled, then - // we should refund it as an expired deposit. If the deposit is RequestedSlowFill then we need to validate - // that the deposit is eligible for a slow fill (its input and output tokens are equivalent) and that - // the slow fill request was not sent in the current destination chain's bundle block range. - - // Using the above rules, we will create a list of: - // - deposits in the current bundle - // - fast fills to refund in the current bundle - // - fills creating bundle LP fees in the current bundle - // - slow fills to create for the current bundle - // - deposits that expired in the current bundle - // Use this dictionary to conveniently unite all events with the same relay data hash which will make // secondary lookups faster. The goal is to lazily fill up this dictionary with all events in the SpokePool // client's in-memory event cache. @@ -798,9 +743,7 @@ export class BundleDataClient { }; } = {}; - // Process all deposits first and keep track of deposits that may be refunded as an expired deposit: - // - expiredBundleDepositHashes: Deposits sent in this bundle that expired. - const expiredBundleDepositHashes: Set = new Set(); + // Process all deposits first and populate the v3RelayHashes dictionary. // - olderDepositHashes: Deposits sent in a prior bundle that newly expired in this bundle const olderDepositHashes: Set = new Set(); @@ -844,19 +787,10 @@ export class BundleDataClient { } // If deposit block is within origin chain bundle block range, then save as bundle deposit. - // If deposit is in bundle and it has expired, additionally save it as an expired deposit. // If deposit is not in the bundle block range, then save it as an older deposit that // may have expired. if (deposit.blockNumber >= originChainBlockRange[0]) { - // Deposit is a V3 deposit in this origin chain's bundle block range and is not a duplicate. updateBundleDepositsV3(bundleDepositsV3, deposit); - // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because - // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 - // for example. Those should be impossible to create but technically should be included in this - // bundle of refunded deposits. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { - expiredBundleDepositHashes.add(relayDataHash); - } } else if (deposit.blockNumber < originChainBlockRange[0]) { olderDepositHashes.add(relayDataHash); } @@ -1013,7 +947,11 @@ export class BundleDataClient { // If there is no fill matching the relay hash, then this might be a valid slow fill request // that we should produce a slow fill leaf for. Check if the slow fill request is in the // destination chain block range. - if (slowFillRequest.blockNumber >= destinationChainBlockRange[0]) { + if ( + slowFillRequest.blockNumber >= destinationChainBlockRange[0] && + // Deposit must not have expired in this bundle. + slowFillRequest.fillDeadline >= bundleBlockTimestamps[destinationChainId][1] + ) { // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit, // so this slow fill request relay data is correct. validatedBundleSlowFills.push(matchedDeposit); @@ -1061,7 +999,11 @@ export class BundleDataClient { ); v3RelayHashes[relayDataHash].deposit = matchedDeposit; - if (!_canCreateSlowFillLeaf(matchedDeposit)) { + if ( + !_canCreateSlowFillLeaf(matchedDeposit) || + // Deposit must not have expired in this bundle. + slowFillRequest.fillDeadline < bundleBlockTimestamps[destinationChainId][1] + ) { return; } @@ -1073,10 +1015,15 @@ export class BundleDataClient { } ); - // The above loops for adding deposits, fills, and then slow fill requests to the relay hash dictionary - // ignore any events that are after the bundle block range. Because of that, we should consider that there - // are deposits in this bundle that correspond to fills that were sent in a prior bundle that have not - // yet been refunded. These fills are also known as "pre-fills" from here on. + // Deposits can be submitted an arbitrary amount of time after matching fills and slow fill requests. + // Therefore, let's go through each deposit in this bundle again and check a few things: + // - Has the deposit been filled in a previous bundle? If so, then we need to issue a relayer refund for + // this "pre-fill". + // - Has the deposit been slow filled in a previous bundle? If so, then we need to issue a slow fill leaf + // for this "pre-slow-fill-request". + // - Has the deposit expired in this bundle and not been filled? If so, then we need to issue an expiry refund. + // - Additionally, has the deposit been slow filled in a previous bundle? If so, then we need to issue an + // unexecutable slow fill refund becuase that slow fill is no longer executable. const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); await mapAsync( @@ -1091,14 +1038,14 @@ export class BundleDataClient { ), async ({ deposit, fill, slowFillRequest }) => { if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); - // We don't check the deposit's fillDeadline here because we are ok if the deposit expires in this bundle - // and we issue an expiry refund for it. This expired deposit could also have been pre-filled and we just - // want to make sure in this code block that all valid pre-fills get refunded once the deposit appears. - // If a pre-fill gets refunded and its deposit expired and gets refunded as well, then there is no net loss - // to the protocol. + // We don't check here that this deposit is a duplicate because we are willing to refund a pre-fill + // even if this deposit is a duplicate. This is because a duplicate deposit for a pre-fill cannot get + // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore + // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out + // the pre-filler. // If fill exists in memory, then the only case in which we need to create a refund is if the - // the fill occurred in a previous bundle. + // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. if (fill) { if (!isSlowFill(fill) && fill.blockNumber < destinationChainBlockRange[0]) { // If fill is in the current bundle then we can assume there is already a refund for it, so only @@ -1119,7 +1066,13 @@ export class BundleDataClient { // in previous bundles. if (slowFillRequest) { if (_canCreateSlowFillLeaf(deposit) && slowFillRequest.blockNumber < destinationChainBlockRange[0]) { - validatedBundleSlowFills.push(deposit); + // If deposit newly expired, then we can't create a slow fill leaf for it but we can + // create a deposit refund for it. + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } else { + validatedBundleSlowFills.push(deposit); + } } return; } @@ -1153,7 +1106,21 @@ export class BundleDataClient { // Input and Output tokens must be equivalent on the deposit for this to be slow filled. // Slow fill requests for deposits from or to lite chains are considered invalid if (_canCreateSlowFillLeaf(deposit)) { - validatedBundleSlowFills.push(deposit); + // If deposit newly expired, then we can't create a slow fill leaf for it but we can + // create a deposit refund for it. + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } else { + validatedBundleSlowFills.push(deposit); + } + } + } else { + // If deposit is Unfilled and its newly expired, we can create a deposit refund for it. + // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because + // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 + // for example. Those should be included in this bundle of refunded deposits. + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } } } @@ -1207,13 +1174,13 @@ export class BundleDataClient { // Add any newly expired deposits to the list of expired deposits to refund. // For these refunds, we need to check whether there was a slow fill created for it in a previous bundle // that is now unexecutable and replaced by a new expired deposit refund. - const possibleExpiredDeposits = Array.from(olderDepositHashes).concat(Array.from(expiredBundleDepositHashes)); - await forEachAsync(possibleExpiredDeposits, async (relayDataHash) => { + await forEachAsync(Array.from(olderDepositHashes), async (relayDataHash) => { const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); const { destinationChainId } = deposit!; const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); + // Only look for deposits that were mined before this bundle and that are newly expired. // If the fill deadline is lower than the bundle start block on the destination chain, then // we should assume it was marked "newly expired" and refunded in a previous bundle. if ( From 130992e6f87cf48fac7548f8bd576c8f682c0265 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 00:20:48 -0500 Subject: [PATCH 021/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index fa7795ee0..8f32a8866 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1043,12 +1043,6 @@ export class BundleDataClient { // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out // the pre-filler. - // We don't check here that this deposit is a duplicate because we are willing to refund a pre-fill - // even if this deposit is a duplicate. This is because a duplicate deposit for a pre-fill cannot get - // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore - // any duplicate deposits result in a net loss of funds for the depositor and effectively payout - // the pre-filler. - // If fill exists in memory, then the only case in which we need to create a refund is if the // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. if (fill) { From 15ebfead98d68aa27b3e8e29616a8fa4b461aae4 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 00:51:38 -0500 Subject: [PATCH 022/103] Update BundleDataClient.ts --- .../BundleDataClient/BundleDataClient.ts | 56 +++++++------------ 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 8f32a8866..5c2496d20 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -734,8 +734,7 @@ export class BundleDataClient { const v3RelayHashes: { [relayHash: string]: { // Note: Since there are no partial fills in v3, there should only be one fill per relay hash. - // There should also only be one deposit per relay hash since deposit ID's can't be re-used on the - // same spoke pool. Moreover, the SpokePool blocks multiple slow fill requests, so + // Moreover, the SpokePool blocks multiple slow fill requests, so // there should also only be one slow fill request per relay hash. deposit?: V3DepositWithBlock; fill?: V3FillWithBlock; @@ -1023,22 +1022,22 @@ export class BundleDataClient { // unexecutable slow fill refund becuase that slow fill is no longer executable. const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); - // We don't iterate through deposits again here because we only need to consider unique deposit hashes - // to look for pre-fills. Duplicate deposits could only have been pre-filled once. await mapAsync( - Object.values(v3RelayHashes).filter( - ({ deposit }) => - deposit && - deposit.originChainId === originChainId && - deposit.destinationChainId === destinationChainId && - deposit.blockNumber >= originBlockRange[0] && - deposit.blockNumber <= originBlockRange[1] && - !isZeroValueDeposit(deposit) - ), - async ({ deposit, fill, slowFillRequest }) => { - if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); - // We don't check here that this deposit is a duplicate because we are willing to refund a pre-fill - // even if this deposit is a duplicate. This is because a duplicate deposit for a pre-fill cannot get + originClient + .getDepositsForDestinationChainWithDuplicates(destinationChainId) + .filter( + (deposit) => + deposit.blockNumber >= originBlockRange[0] && + deposit.blockNumber <= originBlockRange[1] && + !isZeroValueDeposit(deposit) + ), + async (deposit) => { + const relayDataHash = this.getRelayHashFromEvent(deposit); + if (!v3RelayHashes[relayDataHash]) throw new Error("Deposit should exist in relay hash dictionary."); + const { fill, slowFillRequest } = v3RelayHashes[relayDataHash]; + + // We are willing to refund a pre-fill multiple times for each duplicate deposit. + // This is because a duplicate deposit for a pre-fill cannot get // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out // the pre-filler. @@ -1058,24 +1057,6 @@ export class BundleDataClient { return; } - // If fill does not exist in memory but there is a slow fill request in memory, then we need to issue a - // slow fill leaf for the deposit. We can assume there was no fill preceding the slow fill request because - // slow fill requests cannot follow fills. If there were a fill following this request, we would have - // entered the above case. Again as with pre-fills, we should only consider slow fill requests that were - // in previous bundles. - if (slowFillRequest) { - if (_canCreateSlowFillLeaf(deposit) && slowFillRequest.blockNumber < destinationChainBlockRange[0]) { - // If deposit newly expired, then we can't create a slow fill leaf for it but we can - // create a deposit refund for it. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } else { - validatedBundleSlowFills.push(deposit); - } - } - return; - } - // So at this point in the code, there is no fill or slow fill request in memory for this deposit. // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles @@ -1109,7 +1090,10 @@ export class BundleDataClient { // create a deposit refund for it. if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } else { + } + // If slow fill request exists in memory then make sure it wasn't in this bundle otherwise we + // would have already created a slow fill leaf for it. + else if (!slowFillRequest || slowFillRequest.blockNumber < destinationChainBlockRange[0]) { validatedBundleSlowFills.push(deposit); } } From a79840593f32c7de9f44957dd03919ae0fca496f Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 00:59:22 -0500 Subject: [PATCH 023/103] Update BundleDataClient.ts --- .../BundleDataClient/BundleDataClient.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 5c2496d20..082edf979 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1081,6 +1081,13 @@ export class BundleDataClient { }); } } + // If deposit is not filled and its newly expired, we can create a deposit refund for it. + // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because + // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 + // for example. Those should be included in this bundle of refunded deposits. + else if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } // If slow fill requested, then issue a slow fill leaf for the deposit. else if (fillStatus === FillStatus.RequestedSlowFill) { // Input and Output tokens must be equivalent on the deposit for this to be slow filled. @@ -1088,23 +1095,12 @@ export class BundleDataClient { if (_canCreateSlowFillLeaf(deposit)) { // If deposit newly expired, then we can't create a slow fill leaf for it but we can // create a deposit refund for it. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } // If slow fill request exists in memory then make sure it wasn't in this bundle otherwise we // would have already created a slow fill leaf for it. - else if (!slowFillRequest || slowFillRequest.blockNumber < destinationChainBlockRange[0]) { + if (!slowFillRequest || slowFillRequest.blockNumber < destinationChainBlockRange[0]) { validatedBundleSlowFills.push(deposit); } } - } else { - // If deposit is Unfilled and its newly expired, we can create a deposit refund for it. - // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because - // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 - // for example. Those should be included in this bundle of refunded deposits. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } } } ); From 768b5d6ddc2b6b286d89d296b7d631c96bd1b054 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 10:34:25 -0500 Subject: [PATCH 024/103] fix pre slow fill request handling --- .../BundleDataClient/BundleDataClient.ts | 76 +++++++++++-------- src/utils/SpokeUtils.ts | 2 +- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 082edf979..b526ec448 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -742,8 +742,9 @@ export class BundleDataClient { }; } = {}; - // Process all deposits first and populate the v3RelayHashes dictionary. - // - olderDepositHashes: Deposits sent in a prior bundle that newly expired in this bundle + // Process all deposits first and populate the v3RelayHashes dictionary. Sort deposits into whether they are + // in this bundle block range or in a previous bundle block range. + const bundleDepositHashes: string[] = []; const olderDepositHashes: string[] = []; let depositCounter = 0; @@ -765,8 +766,6 @@ export class BundleDataClient { depositCounter++; const relayDataHash = this.getRelayHashFromEvent(deposit); if (!v3RelayHashes[relayDataHash]) { - // Even if deposit is not in bundle block range, store all deposits we can see in memory in this - // convenient dictionary. v3RelayHashes[relayDataHash] = { deposit: deposit, fill: undefined, @@ -782,10 +781,8 @@ export class BundleDataClient { return; } - // If deposit block is within origin chain bundle block range, then save as bundle deposit. - // If deposit is not in the bundle block range, then save it as an older deposit that - // may have expired. if (deposit.blockNumber >= originChainBlockRange[0]) { + bundleDepositHashes.push(relayDataHash); updateBundleDepositsV3(bundleDepositsV3, deposit); } else if (deposit.blockNumber < originChainBlockRange[0]) { olderDepositHashes.push(relayDataHash); @@ -913,6 +910,8 @@ export class BundleDataClient { } ); + // Process slow fill requests. One invariant we need to maintain is that we cannot create slow fill requests + // for deposits that would expire in this bundle. await forEachAsync( destinationClient .getSlowFillRequestsForOriginChain(originChainId) @@ -1012,29 +1011,29 @@ export class BundleDataClient { ); // Deposits can be submitted an arbitrary amount of time after matching fills and slow fill requests. - // Therefore, let's go through each deposit in this bundle again and check a few things: - // - Has the deposit been filled in a previous bundle? If so, then we need to issue a relayer refund for - // this "pre-fill". - // - Has the deposit been slow filled in a previous bundle? If so, then we need to issue a slow fill leaf - // for this "pre-slow-fill-request". - // - Has the deposit expired in this bundle and not been filled? If so, then we need to issue an expiry refund. - // - Additionally, has the deposit been slow filled in a previous bundle? If so, then we need to issue an - // unexecutable slow fill refund becuase that slow fill is no longer executable. + // Therefore, let's go through each deposit in this bundle again and check a few things in order: + // - Has the deposit been filled ? If so, then we need to issue a relayer refund for + // this "pre-fill" if the fill took place in a previous bundle. + // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund. + // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf + // for this "pre-slow-fill-request" if this request took place in a previous bundle. const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); await mapAsync( - originClient - .getDepositsForDestinationChainWithDuplicates(destinationChainId) - .filter( - (deposit) => - deposit.blockNumber >= originBlockRange[0] && - deposit.blockNumber <= originBlockRange[1] && - !isZeroValueDeposit(deposit) - ), - async (deposit) => { - const relayDataHash = this.getRelayHashFromEvent(deposit); - if (!v3RelayHashes[relayDataHash]) throw new Error("Deposit should exist in relay hash dictionary."); - const { fill, slowFillRequest } = v3RelayHashes[relayDataHash]; + bundleDepositHashes.filter((depositHash) => { + const { deposit } = v3RelayHashes[depositHash]; + return ( + deposit && + deposit.originChainId === originChainId && + deposit.destinationChainId === destinationChainId && + deposit.blockNumber >= originBlockRange[0] && + deposit.blockNumber <= originBlockRange[1] && + !isZeroValueDeposit(deposit) + ); + }), + async (depositHash) => { + const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash]; + if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); // We are willing to refund a pre-fill multiple times for each duplicate deposit. // This is because a duplicate deposit for a pre-fill cannot get @@ -1057,6 +1056,23 @@ export class BundleDataClient { return; } + // If a slow fill request exists in memory, then we know the deposit has not been filled because fills + // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, + // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or + // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request + // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. + if (slowFillRequest) { + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } else if ( + _canCreateSlowFillLeaf(deposit) && + slowFillRequest.blockNumber < destinationChainBlockRange[0] + ) { + validatedBundleSlowFills.push(deposit); + } + return; + } + // So at this point in the code, there is no fill or slow fill request in memory for this deposit. // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles @@ -1095,11 +1111,7 @@ export class BundleDataClient { if (_canCreateSlowFillLeaf(deposit)) { // If deposit newly expired, then we can't create a slow fill leaf for it but we can // create a deposit refund for it. - // If slow fill request exists in memory then make sure it wasn't in this bundle otherwise we - // would have already created a slow fill leaf for it. - if (!slowFillRequest || slowFillRequest.blockNumber < destinationChainBlockRange[0]) { - validatedBundleSlowFills.push(deposit); - } + validatedBundleSlowFills.push(deposit); } } } diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 18725d56f..a66717c2d 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -334,7 +334,7 @@ export async function findFillBlock( ): Promise { const { provider } = spokePool; highBlockNumber ??= await provider.getBlockNumber(); - assert(highBlockNumber > lowBlockNumber, `Block numbers out of range (${lowBlockNumber} > ${highBlockNumber})`); + assert(highBlockNumber > lowBlockNumber, `Block numbers out of range (${lowBlockNumber} >= ${highBlockNumber})`); // In production the chainId returned from the provider matches 1:1 with the actual chainId. Querying the provider // object saves an RPC query becasue the chainId is cached by StaticJsonRpcProvider instances. In hre, the SpokePool From 945defd6221d16a80a0ede29bd30c1dce03507b5 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 10:37:50 -0500 Subject: [PATCH 025/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index b526ec448..8c2e61845 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1001,10 +1001,6 @@ export class BundleDataClient { ) { return; } - - // Note: we don't need to query for a historical fill at this point because a fill - // cannot precede a slow fill request and if the fill came after the slow fill request, - // we would have seen it already because we would have processed it in the loop above. validatedBundleSlowFills.push(matchedDeposit); } } From 4d34e0d87d9e88ac6dc07fe059fac009db0afd77 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 10:40:00 -0500 Subject: [PATCH 026/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 8c2e61845..4c08a8519 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1158,9 +1158,9 @@ export class BundleDataClient { }); start = performance.now(); - // Add any newly expired deposits to the list of expired deposits to refund. - // For these refunds, we need to check whether there was a slow fill created for it in a previous bundle - // that is now unexecutable and replaced by a new expired deposit refund. + // For all deposits older than this bundle, we need to check if they expired in this bundle and if they did, + // whether there was a slow fill created for it in a previous bundle that is now unexecutable and replaced + // by a new expired deposit refund. await forEachAsync(olderDepositHashes, async (relayDataHash) => { const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); From d8a1881440930536850f16ffd28844837cd56828 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 10:55:17 -0500 Subject: [PATCH 027/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2881e61bd..52ac93655 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "3.4.12", + "version": "4.0.0-beta.0", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 719c29c67591a3f0d043f5e27a3389afcda27a40 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 12:32:27 -0500 Subject: [PATCH 028/103] adjust for empty message hash --- src/constants.ts | 2 ++ src/utils/DepositUtils.ts | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 825791963..0b458b869 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -55,6 +55,8 @@ export const DEFAULT_ARWEAVE_STORAGE_ADDRESS = "Z6hjBM8FHu90lYWB8o5jR1dfX92FlV2W export const EMPTY_MESSAGE = "0x"; +export const EMPTY_MESSAGE_HASH = ethersConstants.HashZero; + export const BRIDGED_USDC_SYMBOLS = [ TOKEN_SYMBOLS_MAP["USDC.e"].symbol, TOKEN_SYMBOLS_MAP.USDbC.symbol, diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 5dd97aec2..864f900d8 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -1,6 +1,6 @@ import assert from "assert"; import { SpokePoolClient } from "../clients"; -import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE } from "../constants"; +import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE, EMPTY_MESSAGE_HASH } from "../constants"; import { CachingMechanismInterface, Deposit, DepositWithBlock, Fill, SlowFillRequest } from "../interfaces"; import { getNetworkName } from "./NetworkUtils"; import { getDepositInCache, getDepositKey, setDepositInCache } from "./CachingUtils"; @@ -143,7 +143,9 @@ export function isZeroValueDeposit(deposit: Pick Date: Fri, 24 Jan 2025 12:52:18 -0500 Subject: [PATCH 029/103] add isZeroValueFillOrSlowFillRequest --- src/clients/BundleDataClient/BundleDataClient.ts | 10 ++++++++-- src/utils/DepositUtils.ts | 12 +++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 4c08a8519..267cfc2e8 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -36,6 +36,7 @@ import { bnUint32Max, isZeroValueDeposit, findFillEvent, + isZeroValueFillOrSlowFillRequest, } from "../../utils"; import winston from "winston"; import { @@ -820,7 +821,9 @@ export class BundleDataClient { // We can remove fills for deposits with input amount equal to zero because these will result in 0 refunded // tokens to the filler. We can't remove non-empty message deposit here in case there is a slow fill // request for the deposit, we'd want to see the fill took place. - .filter((fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueDeposit(fill)), + .filter( + (fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueFillOrSlowFillRequest(fill) + ), async (fill) => { const relayDataHash = this.getRelayHashFromEvent(fill); fillCounter++; @@ -915,7 +918,10 @@ export class BundleDataClient { await forEachAsync( destinationClient .getSlowFillRequestsForOriginChain(originChainId) - .filter((request) => request.blockNumber <= destinationChainBlockRange[1] && !isZeroValueDeposit(request)), + .filter( + (request) => + request.blockNumber <= destinationChainBlockRange[1] && !isZeroValueFillOrSlowFillRequest(request) + ), async (slowFillRequest: SlowFillRequestWithBlock) => { const relayDataHash = this.getRelayHashFromEvent(slowFillRequest); diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 864f900d8..efc4eccaa 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -137,15 +137,21 @@ export function isZeroValueDeposit(deposit: Pick): boolean { + return e.inputAmount.eq(0) && isFillOrSlowFillRequestMessageEmpty(e.message); +} + /** * Determines if a message is empty or not. * @param message The message to check. * @returns True if the message is empty, false otherwise. */ export function isMessageEmpty(message = EMPTY_MESSAGE): boolean { - // An empty message on a deposit should be "" or "0x" while message hashes are emitted in Fills and SlowFillRequests - // so an empty message hash will be 32 bytes of 0. - return message === "" || message === "0x" || message === EMPTY_MESSAGE_HASH; + return message === "" || message === "0x"; +} + +export function isFillOrSlowFillRequestMessageEmpty(message: string): boolean { + return message === EMPTY_MESSAGE_HASH; } /** From 70bf908ac768b94be9330cb93c4f220f85003808 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 13:10:04 -0500 Subject: [PATCH 030/103] fix --- test/SpokePoolClient.fills.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/SpokePoolClient.fills.ts b/test/SpokePoolClient.fills.ts index 222617fa2..c6f39c877 100644 --- a/test/SpokePoolClient.fills.ts +++ b/test/SpokePoolClient.fills.ts @@ -1,7 +1,7 @@ import hre from "hardhat"; import { SpokePoolClient } from "../src/clients"; import { Deposit } from "../src/interfaces"; -import { bnOne, findFillBlock, findFillEvent, getNetworkName } from "../src/utils"; +import { bnOne, bnZero, findFillBlock, findFillEvent, getNetworkName } from "../src/utils"; import { EMPTY_MESSAGE, ZERO_ADDRESS } from "../src/constants"; import { originChainId, destinationChainId } from "./constants"; import { @@ -48,7 +48,7 @@ describe("SpokePoolClient: Fills", function () { const spokePoolTime = Number(await spokePool.getCurrentTime()); const outputAmount = toBNWei(1); deposit = { - depositId: 0, + depositId: bnZero, originChainId, destinationChainId, depositor: depositor.address, @@ -69,7 +69,7 @@ describe("SpokePoolClient: Fills", function () { it("Correctly fetches fill data single fill, single chain", async function () { await fillV3Relay(spokePool, deposit, relayer1); - await fillV3Relay(spokePool, { ...deposit, depositId: deposit.depositId + 1 }, relayer1); + await fillV3Relay(spokePool, { ...deposit, depositId: deposit.depositId.add(1) }, relayer1); await spokePoolClient.update(); expect(spokePoolClient.getFills().length).to.equal(2); }); @@ -115,12 +115,12 @@ describe("SpokePoolClient: Fills", function () { }); it("Correctly returns the FilledV3Relay event using the relay data", async function () { - const targetDeposit = { ...deposit, depositId: deposit.depositId + 1 }; + const targetDeposit = { ...deposit, depositId: deposit.depositId.add(1) }; // Submit multiple fills at the same block: const startBlock = await spokePool.provider.getBlockNumber(); await fillV3Relay(spokePool, deposit, relayer1); await fillV3Relay(spokePool, targetDeposit, relayer1); - await fillV3Relay(spokePool, { ...deposit, depositId: deposit.depositId + 2 }, relayer1); + await fillV3Relay(spokePool, { ...deposit, depositId: deposit.depositId.add(2) }, relayer1); await hre.network.provider.send("evm_mine"); const fill = await findFillEvent(spokePool, targetDeposit, startBlock); @@ -128,7 +128,7 @@ describe("SpokePoolClient: Fills", function () { expect(fill!.depositId).to.equal(targetDeposit.depositId); // Looking for a fill can return undefined: - const missingFill = await findFillEvent(spokePool, { ...deposit, depositId: deposit.depositId + 3 }, startBlock); + const missingFill = await findFillEvent(spokePool, { ...deposit, depositId: deposit.depositId.add(3) }, startBlock); expect(missingFill).to.be.undefined; }); From 23667694754dfe7786ee48c60b37853b325d35ab Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 13:33:38 -0500 Subject: [PATCH 031/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c250e6d5..de0422a32 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.0", + "version": "4.0.0-beta.1", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From e6f499615b2301f6f34a008a385bf2e21736fb4c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 13:37:58 -0500 Subject: [PATCH 032/103] use isMessageEmpty in isFillOrSlowFillRequestMessageEmpty --- package.json | 2 +- src/utils/DepositUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index de0422a32..8e4e11c1f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 154674904..af1a3e893 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -151,7 +151,7 @@ export function isMessageEmpty(message = EMPTY_MESSAGE): boolean { } export function isFillOrSlowFillRequestMessageEmpty(message: string): boolean { - return message === EMPTY_MESSAGE_HASH; + return isMessageEmpty(message) || message === EMPTY_MESSAGE_HASH; } /** From 2d83142c5faa3e4da4de0f07217597e9ce2ffb41 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 24 Jan 2025 17:17:30 -0500 Subject: [PATCH 033/103] Update SpokePoolClient.ValidateFill.ts --- test/SpokePoolClient.ValidateFill.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/SpokePoolClient.ValidateFill.ts b/test/SpokePoolClient.ValidateFill.ts index b342dacc0..02300d6e9 100644 --- a/test/SpokePoolClient.ValidateFill.ts +++ b/test/SpokePoolClient.ValidateFill.ts @@ -636,7 +636,7 @@ describe("SpokePoolClient: Fill Validation", function () { ); // Override the deposit ID that we are "filling" to be > 1, the latest deposit ID in spoke pool 1. - await fillV3Relay(spokePool_2, { ...deposit, depositId: deposit.depositId + 1 }, relayer); + await fillV3Relay(spokePool_2, { ...deposit, depositId: deposit.depositId.add(1) }, relayer); await spokePoolClient2.update(); const [fill] = spokePoolClient2.getFills(); From dde1130efa8cdb1759c0126c275396add6026f0a Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sun, 26 Jan 2025 00:38:22 -0500 Subject: [PATCH 034/103] Refactor and change up conditionals for marginal speedups Put more likely to be used filters in conditions first --- package.json | 2 +- .../BundleDataClient/BundleDataClient.ts | 74 +++++++------------ 2 files changed, 27 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 8e4e11c1f..a6e8c6130 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 267cfc2e8..d4cd21c7f 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -686,6 +686,10 @@ export class BundleDataClient { ); }; + const _depositIsExpired = (deposit: DepositWithBlock): boolean => { + return deposit.fillDeadline < bundleBlockTimestamps[deposit.destinationChainId][1]; + }; + const _getFillStatusForDeposit = (deposit: Deposit, queryBlock: number): Promise => { return spokePoolClients[deposit.destinationChainId].relayFillStatus( deposit, @@ -761,7 +765,7 @@ export class BundleDataClient { // Only evaluate deposits that are in this bundle or in previous bundles. This means we cannot issue fill // refunds or slow fills here for deposits that are in future bundles (i.e. "pre-fills"). Instead, we'll // evaluate these pre-fills once the deposit is inside the "current" bundle block range. - if (isZeroValueDeposit(deposit) || deposit.blockNumber > originChainBlockRange[1]) { + if (deposit.blockNumber > originChainBlockRange[1] || isZeroValueDeposit(deposit)) { return; } depositCounter++; @@ -848,8 +852,7 @@ export class BundleDataClient { // slow fill requests for deposits from or to lite chains are considered invalid if ( fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill && - !v3RelayHashes[relayDataHash].deposit!.fromLiteChain && - !v3RelayHashes[relayDataHash].deposit!.toLiteChain + _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposit!) ) { fastFillsReplacingSlowFills.push(relayDataHash); } @@ -903,8 +906,7 @@ export class BundleDataClient { // slow fill requests for deposits from or to lite chains are considered invalid if ( fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill && - !matchedDeposit.fromLiteChain && - !matchedDeposit.toLiteChain + _canCreateSlowFillLeaf(matchedDeposit) ) { fastFillsReplacingSlowFills.push(relayDataHash); } @@ -941,17 +943,15 @@ export class BundleDataClient { ); // The ! is safe here because we've already checked that the deposit exists in the relay hash dictionary. const matchedDeposit = v3RelayHashes[relayDataHash].deposit!; - if (!_canCreateSlowFillLeaf(matchedDeposit)) { - return; - } // If there is no fill matching the relay hash, then this might be a valid slow fill request // that we should produce a slow fill leaf for. Check if the slow fill request is in the // destination chain block range. if ( slowFillRequest.blockNumber >= destinationChainBlockRange[0] && + _canCreateSlowFillLeaf(matchedDeposit) && // Deposit must not have expired in this bundle. - slowFillRequest.fillDeadline >= bundleBlockTimestamps[destinationChainId][1] + !_depositIsExpired(matchedDeposit) ) { // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit, // so this slow fill request relay data is correct. @@ -981,8 +981,8 @@ export class BundleDataClient { // found using such a method) because infinite fill deadlines cannot be produced from the unsafeDepositV3() // function. if ( - INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline) && - slowFillRequest.blockNumber >= destinationChainBlockRange[0] + slowFillRequest.blockNumber >= destinationChainBlockRange[0] && + INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline) ) { const historicalDeposit = await queryHistoricalDepositForFill(originClient, slowFillRequest); if (!historicalDeposit.found) { @@ -1003,7 +1003,7 @@ export class BundleDataClient { if ( !_canCreateSlowFillLeaf(matchedDeposit) || // Deposit must not have expired in this bundle. - slowFillRequest.fillDeadline < bundleBlockTimestamps[destinationChainId][1] + _depositIsExpired(matchedDeposit) ) { return; } @@ -1046,7 +1046,7 @@ export class BundleDataClient { // If fill exists in memory, then the only case in which we need to create a refund is if the // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. if (fill) { - if (!isSlowFill(fill) && fill.blockNumber < destinationChainBlockRange[0]) { + if (fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) { // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. @@ -1064,11 +1064,11 @@ export class BundleDataClient { // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. if (slowFillRequest) { - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + if (_depositIsExpired(deposit)) { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } else if ( - _canCreateSlowFillLeaf(deposit) && - slowFillRequest.blockNumber < destinationChainBlockRange[0] + slowFillRequest.blockNumber < destinationChainBlockRange[0] && + _canCreateSlowFillLeaf(deposit) ) { validatedBundleSlowFills.push(deposit); } @@ -1103,7 +1103,7 @@ export class BundleDataClient { // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 // for example. Those should be included in this bundle of refunded deposits. - else if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + else if (_depositIsExpired(deposit)) { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } // If slow fill requested, then issue a slow fill leaf for the deposit. @@ -1134,24 +1134,15 @@ export class BundleDataClient { } // We should never push fast fills involving lite chains here because slow fill requests for them are invalid: assert( - !deposit.fromLiteChain && !deposit.toLiteChain, - "fastFillsReplacingSlowFills should not contain lite chain deposits" + _canCreateSlowFillLeaf(deposit), + "fastFillsReplacingSlowFills should contain only deposits that can be slow filled" ); const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); if ( - // If the slow fill request that was replaced by this fill was in an older bundle, then we don't - // need to check if the slow fill request was valid since we can assume all bundles in the past - // were validated. However, we might as well double check. - this.clients.hubPoolClient.areTokensEquivalent( - deposit.inputToken, - deposit.originChainId, - deposit.outputToken, - deposit.destinationChainId, - deposit.quoteBlockNumber - ) && // If there is a slow fill request in this bundle that matches the relay hash, then there was no slow fill // created that would be considered excess. - (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) + !slowFillRequest || + slowFillRequest.blockNumber < destinationBlockRange[0] ) { validatedBundleUnexecutableSlowFills.push(deposit); } @@ -1180,7 +1171,7 @@ export class BundleDataClient { // If there is a valid fill that we saw matching this deposit, then it does not need a refund. !fill && isDefined(deposit) && // Needed for TSC - we check this above. - deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && + _depositIsExpired(deposit) && deposit.fillDeadline >= bundleBlockTimestamps[destinationChainId][0] && spokePoolClients[destinationChainId] !== undefined ) { @@ -1196,8 +1187,7 @@ export class BundleDataClient { // If fill status is RequestedSlowFill, then we might need to mark down an unexecutable // slow fill that we're going to replace with an expired deposit refund. // If deposit cannot be slow filled, then exit early. - // slow fill requests for deposits from or to lite chains are considered invalid - if (fillStatus !== FillStatus.RequestedSlowFill || deposit.fromLiteChain || deposit.toLiteChain) { + if (fillStatus !== FillStatus.RequestedSlowFill || !_canCreateSlowFillLeaf(deposit)) { return; } // Now, check if there was a slow fill created for this deposit in a previous bundle which would now be @@ -1206,21 +1196,9 @@ export class BundleDataClient { // If there is a slow fill request in this bundle, then the expired deposit refund will supercede // the slow fill request. If there is no slow fill request seen or its older than this bundle, then we can - // assume a slow fill leaf was created for it because its tokens are equivalent. The slow fill request was - // also sent before the fill deadline expired since we checked that above. - if ( - // Since this deposit was requested for a slow fill in an older bundle at this point, we don't - // technically need to check if the slow fill request was valid since we can assume all bundles in the past - // were validated. However, we might as well double check. - this.clients.hubPoolClient.areTokensEquivalent( - deposit.inputToken, - deposit.originChainId, - deposit.outputToken, - deposit.destinationChainId, - deposit.quoteBlockNumber - ) && - (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) - ) { + // assume a slow fill leaf was created for it because of the previous _canCreateSlowFillLeaf check. + // The slow fill request was also sent before the fill deadline expired since we checked that above. + if (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) { validatedBundleUnexecutableSlowFills.push(deposit); } } From d18e6a60266bc9bc54b5de57a80f9494ef9dec42 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sun, 26 Jan 2025 13:37:26 -0500 Subject: [PATCH 035/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index d4cd21c7f..83bdee802 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -770,6 +770,8 @@ export class BundleDataClient { } depositCounter++; const relayDataHash = this.getRelayHashFromEvent(deposit); + + // Duplicate deposits are treated like normal deposits. if (!v3RelayHashes[relayDataHash]) { v3RelayHashes[relayDataHash] = { deposit: deposit, @@ -786,6 +788,8 @@ export class BundleDataClient { return; } + // Evaluate all expired deposits after fetching fill statuses, + // since we can't know for certain whether an expired deposit was filled a long time ago. if (deposit.blockNumber >= originChainBlockRange[0]) { bundleDepositHashes.push(relayDataHash); updateBundleDepositsV3(bundleDepositsV3, deposit); From 45441a16600ff5c8e91a776f4c5375f1e2eaeae9 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 27 Jan 2025 11:00:42 -0500 Subject: [PATCH 036/103] Update SpokePoolClient.fills.ts --- test/SpokePoolClient.fills.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/SpokePoolClient.fills.ts b/test/SpokePoolClient.fills.ts index 45f48c499..a51cda825 100644 --- a/test/SpokePoolClient.fills.ts +++ b/test/SpokePoolClient.fills.ts @@ -2,7 +2,6 @@ import hre from "hardhat"; import { SpokePoolClient } from "../src/clients"; import { Deposit } from "../src/interfaces"; import { bnOne, bnZero, findFillBlock, findFillEvent, getNetworkName } from "../src/utils"; -import { bnOne, bnZero, findFillBlock, getNetworkName } from "../src/utils"; import { EMPTY_MESSAGE, ZERO_ADDRESS } from "../src/constants"; import { originChainId, destinationChainId } from "./constants"; import { From 461db1cccb4bcced3157678f2aa11eda7689c910 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 27 Jan 2025 14:37:24 -0500 Subject: [PATCH 037/103] refactor --- src/clients/BundleDataClient/BundleDataClient.ts | 9 +-------- src/utils/SpokeUtils.ts | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 3df8fc97c..d5dff76e9 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1023,18 +1023,11 @@ export class BundleDataClient { // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund. // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf // for this "pre-slow-fill-request" if this request took place in a previous bundle. - const originBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); - await mapAsync( bundleDepositHashes.filter((depositHash) => { const { deposit } = v3RelayHashes[depositHash]; return ( - deposit && - deposit.originChainId === originChainId && - deposit.destinationChainId === destinationChainId && - deposit.blockNumber >= originBlockRange[0] && - deposit.blockNumber <= originBlockRange[1] && - !isZeroValueDeposit(deposit) + deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId ); }), async (depositHash) => { diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 7af5c8538..f97f668da 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -347,7 +347,6 @@ export async function findFillBlock( // In production the chainId returned from the provider matches 1:1 with the actual chainId. Querying the provider // object saves an RPC query becasue the chainId is cached by StaticJsonRpcProvider instances. In hre, the SpokePool // may be configured with a different chainId than what is returned by the provider. - // @todo Sub out actual chain IDs w/ CHAIN_IDs constants const destinationChainId = Object.values(CHAIN_IDs).includes(relayData.originChainId) ? (await provider.getNetwork()).chainId : Number(await spokePool.chainId()); From ebcaf160a709c6f96bf4f37c3db99342e40ac54e Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 27 Jan 2025 14:38:43 -0500 Subject: [PATCH 038/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6e8c6130..52cd16e47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.3", + "version": "4.0.0-beta.4", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 1924d76ea544c8b3732f3a4d049822aea23fac2e Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 27 Jan 2025 14:52:16 -0500 Subject: [PATCH 039/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52cd16e47..34f5c6dff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.4", + "version": "4.0.0-beta.5", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From bd9bd0b6b55d7a815ae6fd1216152d10bb84241b Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 27 Jan 2025 15:35:11 -0500 Subject: [PATCH 040/103] Update MockSpokePoolClient.ts --- src/clients/mocks/MockSpokePoolClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clients/mocks/MockSpokePoolClient.ts b/src/clients/mocks/MockSpokePoolClient.ts index 60e129efb..e14be8ffd 100644 --- a/src/clients/mocks/MockSpokePoolClient.ts +++ b/src/clients/mocks/MockSpokePoolClient.ts @@ -122,7 +122,6 @@ export class MockSpokePoolClient extends SpokePoolClient { const { blockNumber, transactionIndex } = deposit; let { depositId, depositor, destinationChainId, inputToken, inputAmount, outputToken, outputAmount } = deposit; depositId ??= this.numberOfDeposits; - assert(depositId.gte(this.numberOfDeposits), `${depositId.toString()} < ${this.numberOfDeposits}`); this.numberOfDeposits = depositId.add(bnOne); destinationChainId ??= random(1, 42161, false); From 2e3f0be87a52f80dfd6f480825bd3ce0b16f5a5a Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 27 Jan 2025 15:35:43 -0500 Subject: [PATCH 041/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 34f5c6dff..bb84c25f8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.5", + "version": "4.0.0-beta.6", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 7b3170d39fa8d68c63ecd933024f6c455b3c7fbf Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Tue, 28 Jan 2025 13:55:45 -0500 Subject: [PATCH 042/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index f591eaff6..23a442c58 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -320,6 +320,12 @@ export class BundleDataClient { continue; } const chainIndex = chainIds.indexOf(chainId); + + // @todo This function does not account for pre-fill refunds as it is optimized for speed. The way to detect + // pre-fill refunds is to load all deposits that are unmatched by fills in the spoke pool client's memory + // and then query the FillStatus on-chain, but that might slow this function down too much. For now, we + // will live with this expected inaccuracy as it should be small. The pre-fill would have to precede the deposit + // by more than the caller's event lookback window which is expected to be unlikely. this.spokePoolClients[chainId] .getFills() .filter((fill) => { From 36f08367171e527faaab693f25010c264a465669 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Tue, 28 Jan 2025 16:36:51 -0500 Subject: [PATCH 043/103] Gatekeep behind version bump --- package.json | 2 +- .../BundleDataClient/BundleDataClient.ts | 186 ++++++++++-------- src/constants.ts | 4 +- 3 files changed, 105 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index bb84c25f8..e08871e38 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.6", + "version": "4.0.0-beta.7", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 23a442c58..81a00e0cf 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -54,6 +54,7 @@ import { V3DepositWithBlock, V3FillWithBlock, } from "./utils"; +import { PRE_FILL_MIN_CONFIG_STORE_VERSION } from "../../constants"; // max(uint256) - 1 export const INFINITE_FILL_DEADLINE = bnUint32Max; @@ -1051,98 +1052,113 @@ export class BundleDataClient { // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund. // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf // for this "pre-slow-fill-request" if this request took place in a previous bundle. - await mapAsync( - bundleDepositHashes.filter((depositHash) => { - const { deposit } = v3RelayHashes[depositHash]; - return ( - deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId - ); - }), - async (depositHash) => { - const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash]; - if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); - - // We are willing to refund a pre-fill multiple times for each duplicate deposit. - // This is because a duplicate deposit for a pre-fill cannot get - // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore - // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out - // the pre-filler. - - // If fill exists in memory, then the only case in which we need to create a refund is if the - // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. - if (fill) { - if (fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) { - // If fill is in the current bundle then we can assume there is already a refund for it, so only - // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then - // we won't consider it, following the previous treatment of fills after the bundle block range. - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: deposit.quoteTimestamp, - }); + const startBlockForMainnet = getBlockRangeForChain( + blockRangesForChains, + this.clients.hubPoolClient.chainId, + this.chainIdListForBundleEvaluationBlockNumbers + )[0]; + const versionAtProposalBlock = + this.clients.configStoreClient.getConfigStoreVersionForBlock(startBlockForMainnet); + + // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We + // should remove this check once we've advanced far beyond the version bump block. + if ( + versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION || + process.env.FORCE_PRE_FILL_REFUNDS === "true" + ) { + await mapAsync( + bundleDepositHashes.filter((depositHash) => { + const { deposit } = v3RelayHashes[depositHash]; + return ( + deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId + ); + }), + async (depositHash) => { + const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash]; + if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); + + // We are willing to refund a pre-fill multiple times for each duplicate deposit. + // This is because a duplicate deposit for a pre-fill cannot get + // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore + // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out + // the pre-filler. + + // If fill exists in memory, then the only case in which we need to create a refund is if the + // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. + if (fill) { + if (fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) { + // If fill is in the current bundle then we can assume there is already a refund for it, so only + // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then + // we won't consider it, following the previous treatment of fills after the bundle block range. + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: deposit.quoteTimestamp, + }); + } + return; } - return; - } - // If a slow fill request exists in memory, then we know the deposit has not been filled because fills - // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, - // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or - // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request - // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. - if (slowFillRequest) { - if (_depositIsExpired(deposit)) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } else if ( - slowFillRequest.blockNumber < destinationChainBlockRange[0] && - _canCreateSlowFillLeaf(deposit) - ) { - validatedBundleSlowFills.push(deposit); + // If a slow fill request exists in memory, then we know the deposit has not been filled because fills + // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, + // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or + // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request + // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. + if (slowFillRequest) { + if (_depositIsExpired(deposit)) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } else if ( + slowFillRequest.blockNumber < destinationChainBlockRange[0] && + _canCreateSlowFillLeaf(deposit) + ) { + validatedBundleSlowFills.push(deposit); + } + return; } - return; - } - // So at this point in the code, there is no fill or slow fill request in memory for this deposit. - // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. - // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles - // because the spoke pool client lookback would have returned this entire bundle of events and stored - // them into the relay hash dictionary. - const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); - - // If deposit was filled, then we need to issue a refund for it. - if (fillStatus === FillStatus.Filled) { - // We need to find the fill event to issue a refund to the right relayer and repayment chain, - // or msg.sender if relayer address is invalid for the repayment chain. - const prefill = (await findFillEvent( - destinationClient.spokePool, - deposit, - destinationClient.deploymentBlock, - destinationClient.latestBlockSearched - )) as unknown as FillWithBlock; - if (!isSlowFill(prefill)) { - validatedBundleV3Fills.push({ - ...prefill, - quoteTimestamp: deposit.quoteTimestamp, - }); + // So at this point in the code, there is no fill or slow fill request in memory for this deposit. + // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. + // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles + // because the spoke pool client lookback would have returned this entire bundle of events and stored + // them into the relay hash dictionary. + const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); + + // If deposit was filled, then we need to issue a refund for it. + if (fillStatus === FillStatus.Filled) { + // We need to find the fill event to issue a refund to the right relayer and repayment chain, + // or msg.sender if relayer address is invalid for the repayment chain. + const prefill = (await findFillEvent( + destinationClient.spokePool, + deposit, + destinationClient.deploymentBlock, + destinationClient.latestBlockSearched + )) as unknown as FillWithBlock; + if (!isSlowFill(prefill)) { + validatedBundleV3Fills.push({ + ...prefill, + quoteTimestamp: deposit.quoteTimestamp, + }); + } } - } - // If deposit is not filled and its newly expired, we can create a deposit refund for it. - // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because - // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 - // for example. Those should be included in this bundle of refunded deposits. - else if (_depositIsExpired(deposit)) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } - // If slow fill requested, then issue a slow fill leaf for the deposit. - else if (fillStatus === FillStatus.RequestedSlowFill) { - // Input and Output tokens must be equivalent on the deposit for this to be slow filled. - // Slow fill requests for deposits from or to lite chains are considered invalid - if (_canCreateSlowFillLeaf(deposit)) { - // If deposit newly expired, then we can't create a slow fill leaf for it but we can - // create a deposit refund for it. - validatedBundleSlowFills.push(deposit); + // If deposit is not filled and its newly expired, we can create a deposit refund for it. + // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because + // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 + // for example. Those should be included in this bundle of refunded deposits. + else if (_depositIsExpired(deposit)) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } + // If slow fill requested, then issue a slow fill leaf for the deposit. + else if (fillStatus === FillStatus.RequestedSlowFill) { + // Input and Output tokens must be equivalent on the deposit for this to be slow filled. + // Slow fill requests for deposits from or to lite chains are considered invalid + if (_canCreateSlowFillLeaf(deposit)) { + // If deposit newly expired, then we can't create a slow fill leaf for it but we can + // create a deposit refund for it. + validatedBundleSlowFills.push(deposit); + } } } - } - ); + ); + } // For all fills that came after a slow fill request, we can now check if the slow fill request // was a valid one and whether it was created in a previous bundle. If so, then it created a slow fill diff --git a/src/constants.ts b/src/constants.ts index 8c85bd623..008e9a929 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,9 +26,11 @@ export const SECONDS_PER_YEAR = 31557600; // 365.25 days per year. */ export const HUBPOOL_CHAIN_ID = 1; -// List of versions where certain UMIP features were deprecated +// List of versions where certain UMIP features were deprecated or activated export const TRANSFER_THRESHOLD_MAX_CONFIG_STORE_VERSION = 1; +export const PRE_FILL_MIN_CONFIG_STORE_VERSION = 5; + // A hardcoded identifier used, by default, to tag all Arweave records. export const ARWEAVE_TAG_APP_NAME = "across-protocol"; From de4f3f8e582b395a73bf3e74a23b121d19db0368 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Tue, 28 Jan 2025 16:44:04 -0500 Subject: [PATCH 044/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 81a00e0cf..f810a4d72 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1062,10 +1062,7 @@ export class BundleDataClient { // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We // should remove this check once we've advanced far beyond the version bump block. - if ( - versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION || - process.env.FORCE_PRE_FILL_REFUNDS === "true" - ) { + if (versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION) { await mapAsync( bundleDepositHashes.filter((depositHash) => { const { deposit } = v3RelayHashes[depositHash]; From 9488ecf2e889e581e18521e78179189114c17480 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Tue, 28 Jan 2025 19:19:01 -0500 Subject: [PATCH 045/103] fix --- package.json | 2 +- .../BundleDataClient/BundleDataClient.ts | 180 +++++++++--------- 2 files changed, 94 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index e08871e38..eb67d0e90 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.7", + "version": "4.0.0-beta.8", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index f810a4d72..c2bed0877 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1062,100 +1062,106 @@ export class BundleDataClient { // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We // should remove this check once we've advanced far beyond the version bump block. - if (versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION) { - await mapAsync( - bundleDepositHashes.filter((depositHash) => { - const { deposit } = v3RelayHashes[depositHash]; - return ( - deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId - ); - }), - async (depositHash) => { - const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash]; - if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); - - // We are willing to refund a pre-fill multiple times for each duplicate deposit. - // This is because a duplicate deposit for a pre-fill cannot get - // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore - // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out - // the pre-filler. - - // If fill exists in memory, then the only case in which we need to create a refund is if the - // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. - if (fill) { - if (fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) { - // If fill is in the current bundle then we can assume there is already a refund for it, so only - // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then - // we won't consider it, following the previous treatment of fills after the bundle block range. - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: deposit.quoteTimestamp, - }); - } - return; + await mapAsync( + bundleDepositHashes.filter((depositHash) => { + const { deposit } = v3RelayHashes[depositHash]; + return ( + deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId + ); + }), + async (depositHash) => { + const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash]; + if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); + + // We are willing to refund a pre-fill multiple times for each duplicate deposit. + // This is because a duplicate deposit for a pre-fill cannot get + // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore + // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out + // the pre-filler. + + // If fill exists in memory, then the only case in which we need to create a refund is if the + // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. + if (fill) { + if ( + versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION && + fill.blockNumber < destinationChainBlockRange[0] && + !isSlowFill(fill) + ) { + // If fill is in the current bundle then we can assume there is already a refund for it, so only + // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then + // we won't consider it, following the previous treatment of fills after the bundle block range. + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: deposit.quoteTimestamp, + }); } + return; + } - // If a slow fill request exists in memory, then we know the deposit has not been filled because fills - // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, - // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or - // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request - // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. - if (slowFillRequest) { - if (_depositIsExpired(deposit)) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } else if ( - slowFillRequest.blockNumber < destinationChainBlockRange[0] && - _canCreateSlowFillLeaf(deposit) - ) { - validatedBundleSlowFills.push(deposit); - } - return; + // If a slow fill request exists in memory, then we know the deposit has not been filled because fills + // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, + // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or + // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request + // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. + if (slowFillRequest) { + if (_depositIsExpired(deposit)) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } else if ( + versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION && + slowFillRequest.blockNumber < destinationChainBlockRange[0] && + _canCreateSlowFillLeaf(deposit) + ) { + validatedBundleSlowFills.push(deposit); } + return; + } - // So at this point in the code, there is no fill or slow fill request in memory for this deposit. - // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. - // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles - // because the spoke pool client lookback would have returned this entire bundle of events and stored - // them into the relay hash dictionary. - const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); - - // If deposit was filled, then we need to issue a refund for it. - if (fillStatus === FillStatus.Filled) { - // We need to find the fill event to issue a refund to the right relayer and repayment chain, - // or msg.sender if relayer address is invalid for the repayment chain. - const prefill = (await findFillEvent( - destinationClient.spokePool, - deposit, - destinationClient.deploymentBlock, - destinationClient.latestBlockSearched - )) as unknown as FillWithBlock; - if (!isSlowFill(prefill)) { - validatedBundleV3Fills.push({ - ...prefill, - quoteTimestamp: deposit.quoteTimestamp, - }); - } - } - // If deposit is not filled and its newly expired, we can create a deposit refund for it. - // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because - // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 - // for example. Those should be included in this bundle of refunded deposits. - else if (_depositIsExpired(deposit)) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + // So at this point in the code, there is no fill or slow fill request in memory for this deposit. + // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. + // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles + // because the spoke pool client lookback would have returned this entire bundle of events and stored + // them into the relay hash dictionary. + const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); + + // If deposit was filled, then we need to issue a refund for it. + if (fillStatus === FillStatus.Filled && versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION) { + // We need to find the fill event to issue a refund to the right relayer and repayment chain, + // or msg.sender if relayer address is invalid for the repayment chain. + const prefill = (await findFillEvent( + destinationClient.spokePool, + deposit, + destinationClient.deploymentBlock, + destinationClient.latestBlockSearched + )) as unknown as FillWithBlock; + if (!isSlowFill(prefill)) { + validatedBundleV3Fills.push({ + ...prefill, + quoteTimestamp: deposit.quoteTimestamp, + }); } - // If slow fill requested, then issue a slow fill leaf for the deposit. - else if (fillStatus === FillStatus.RequestedSlowFill) { - // Input and Output tokens must be equivalent on the deposit for this to be slow filled. - // Slow fill requests for deposits from or to lite chains are considered invalid - if (_canCreateSlowFillLeaf(deposit)) { - // If deposit newly expired, then we can't create a slow fill leaf for it but we can - // create a deposit refund for it. - validatedBundleSlowFills.push(deposit); - } + } + // If deposit is not filled and its newly expired, we can create a deposit refund for it. + // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because + // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 + // for example. Those should be included in this bundle of refunded deposits. + else if (_depositIsExpired(deposit)) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } + // If slow fill requested, then issue a slow fill leaf for the deposit. + else if ( + fillStatus === FillStatus.RequestedSlowFill && + versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION + ) { + // Input and Output tokens must be equivalent on the deposit for this to be slow filled. + // Slow fill requests for deposits from or to lite chains are considered invalid + if (_canCreateSlowFillLeaf(deposit)) { + // If deposit newly expired, then we can't create a slow fill leaf for it but we can + // create a deposit refund for it. + validatedBundleSlowFills.push(deposit); } } - ); - } + } + ); // For all fills that came after a slow fill request, we can now check if the slow fill request // was a valid one and whether it was created in a previous bundle. If so, then it created a slow fill From a6b236d5a93d4b595e658356dfd209c2a3311bf6 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 09:29:34 -0500 Subject: [PATCH 046/103] fix toggle --- package.json | 2 +- .../BundleDataClient/BundleDataClient.ts | 42 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index eb67d0e90..b08af5172 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.8", + "version": "4.0.0-beta.9", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index c2bed0877..9fc50a94c 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -769,6 +769,22 @@ export class BundleDataClient { const bundleDepositHashes: string[] = []; const olderDepositHashes: string[] = []; + // We use the following toggle to aid with the migration to pre-fills. The first bundle proposed using this + // pre-fill logic can double refund pre-fills that have already been filled in the last bundle, because the + // last bundle did not recognize a fill as a pre-fill. Therefore the developer should ensure that the version + // is bumped to the PRE_FILL_MIN_CONFIG_STORE_VERSION version before the first pre-fill bundle is proposed. + // To test the following bundle after this, the developer can set the FORCE_REFUND_PREFILLS environment variable + // to "true" simulate the bundle with pre-fill refunds. + // @todo Remove this logic once we have advanced sufficiently past the pre-fill migration. + const startBlockForMainnet = getBlockRangeForChain( + blockRangesForChains, + this.clients.hubPoolClient.chainId, + this.chainIdListForBundleEvaluationBlockNumbers + )[0]; + const versionAtProposalBlock = this.clients.configStoreClient.getConfigStoreVersionForBlock(startBlockForMainnet); + const canRefundPrefills = + versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION || process.env.FORCE_REFUND_PREFILLS === "true"; + let depositCounter = 0; for (const originChainId of allChainIds) { const originClient = spokePoolClients[originChainId]; @@ -1052,13 +1068,6 @@ export class BundleDataClient { // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund. // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf // for this "pre-slow-fill-request" if this request took place in a previous bundle. - const startBlockForMainnet = getBlockRangeForChain( - blockRangesForChains, - this.clients.hubPoolClient.chainId, - this.chainIdListForBundleEvaluationBlockNumbers - )[0]; - const versionAtProposalBlock = - this.clients.configStoreClient.getConfigStoreVersionForBlock(startBlockForMainnet); // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We // should remove this check once we've advanced far beyond the version bump block. @@ -1082,11 +1091,7 @@ export class BundleDataClient { // If fill exists in memory, then the only case in which we need to create a refund is if the // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. if (fill) { - if ( - versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION && - fill.blockNumber < destinationChainBlockRange[0] && - !isSlowFill(fill) - ) { + if (canRefundPrefills && fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) { // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. @@ -1107,7 +1112,7 @@ export class BundleDataClient { if (_depositIsExpired(deposit)) { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } else if ( - versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION && + canRefundPrefills && slowFillRequest.blockNumber < destinationChainBlockRange[0] && _canCreateSlowFillLeaf(deposit) ) { @@ -1124,7 +1129,7 @@ export class BundleDataClient { const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); // If deposit was filled, then we need to issue a refund for it. - if (fillStatus === FillStatus.Filled && versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION) { + if (fillStatus === FillStatus.Filled) { // We need to find the fill event to issue a refund to the right relayer and repayment chain, // or msg.sender if relayer address is invalid for the repayment chain. const prefill = (await findFillEvent( @@ -1133,7 +1138,7 @@ export class BundleDataClient { destinationClient.deploymentBlock, destinationClient.latestBlockSearched )) as unknown as FillWithBlock; - if (!isSlowFill(prefill)) { + if (canRefundPrefills && !isSlowFill(prefill)) { validatedBundleV3Fills.push({ ...prefill, quoteTimestamp: deposit.quoteTimestamp, @@ -1148,13 +1153,10 @@ export class BundleDataClient { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } // If slow fill requested, then issue a slow fill leaf for the deposit. - else if ( - fillStatus === FillStatus.RequestedSlowFill && - versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION - ) { + else if (fillStatus === FillStatus.RequestedSlowFill) { // Input and Output tokens must be equivalent on the deposit for this to be slow filled. // Slow fill requests for deposits from or to lite chains are considered invalid - if (_canCreateSlowFillLeaf(deposit)) { + if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) { // If deposit newly expired, then we can't create a slow fill leaf for it but we can // create a deposit refund for it. validatedBundleSlowFills.push(deposit); From 5a32e81b115802bf524c99c067866f4fd2d18033 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 15:12:01 -0500 Subject: [PATCH 047/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index b9578e9ef..fbbea3a85 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1055,8 +1055,8 @@ export class BundleDataClient { // found using such a method) because infinite fill deadlines cannot be produced from the unsafeDepositV3() // function. if ( - slowFillRequest.blockNumber >= destinationChainBlockRange[0] && - INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline) + INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline) && + slowFillRequest.blockNumber >= destinationChainBlockRange[0] ) { const historicalDeposit = await queryHistoricalDepositForFill(originClient, slowFillRequest); if (!historicalDeposit.found) { From 1ec42b5d22bc078b0b6efdc0c683b886e4ed7582 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 15:22:48 -0500 Subject: [PATCH 048/103] Add verifyFill check to pre-fillr efund --- src/clients/BundleDataClient/BundleDataClient.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index fbbea3a85..1287bde2c 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1163,7 +1163,13 @@ export class BundleDataClient { destinationClient.deploymentBlock, destinationClient.latestBlockSearched )) as unknown as FillWithBlock; - if (canRefundPrefills && !isSlowFill(prefill)) { + const verifiedFill = await verifyFillRepayment( + prefill, + destinationClient.spokePool.provider, + deposit, + allChainIds + ); + if (canRefundPrefills && isDefined(verifiedFill) && !isSlowFill(prefill)) { validatedBundleV3Fills.push({ ...prefill, quoteTimestamp: deposit.quoteTimestamp, From 7bdc4a1540fcbd6d4778170f4363b418c1d431a7 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:25:18 -0500 Subject: [PATCH 049/103] Update src/clients/BundleDataClient/BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 1287bde2c..5aa9791d2 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1104,6 +1104,8 @@ export class BundleDataClient { ); }), async (depositHash) => { + // We don't need to call verifyFillRepayment() here to replace the fill.relayer because this value should already + // be overwritten because the deposit and fill both exist. const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash]; if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); From 5bf0ad29ad21be5a7244b2bface80c399e730c23 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 17:25:10 -0500 Subject: [PATCH 050/103] Update BundleDataClient.ts --- .../BundleDataClient/BundleDataClient.ts | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 5aa9791d2..b4c289ff2 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -330,7 +330,11 @@ export class BundleDataClient { // will live with this expected inaccuracy as it should be small. The pre-fill would have to precede the deposit // by more than the caller's event lookback window which is expected to be unlikely. const fillsToCount = await filterAsync(this.spokePoolClients[chainId].getFills(), async (fill) => { - if (fill.blockNumber < blockRanges[chainIndex][0] || fill.blockNumber > blockRanges[chainIndex][1]) { + if ( + fill.blockNumber < blockRanges[chainIndex][0] || + fill.blockNumber > blockRanges[chainIndex][1] || + isZeroValueFillOrSlowFillRequest(fill) + ) { return false; } @@ -877,7 +881,9 @@ export class BundleDataClient { // We can remove fills for deposits with input amount equal to zero because these will result in 0 refunded // tokens to the filler. We can't remove non-empty message deposit here in case there is a slow fill // request for the deposit, we'd want to see the fill took place. - .filter((fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueDeposit(fill)), + .filter( + (fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueFillOrSlowFillRequest(fill) + ), async (_fill) => { fillCounter++; const relayDataHash = this.getRelayHashFromEvent(_fill); @@ -1159,21 +1165,18 @@ export class BundleDataClient { if (fillStatus === FillStatus.Filled) { // We need to find the fill event to issue a refund to the right relayer and repayment chain, // or msg.sender if relayer address is invalid for the repayment chain. - const prefill = (await findFillEvent( - destinationClient.spokePool, - deposit, - destinationClient.deploymentBlock, - destinationClient.latestBlockSearched - )) as unknown as FillWithBlock; + const prefill = await this.findMatchingFillEvent(deposit, destinationClient); + assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${depositHash}`); + assert(this.getRelayHashFromEvent(prefill!) === depositHash, "Relay hashes should match."); const verifiedFill = await verifyFillRepayment( - prefill, + prefill!, destinationClient.spokePool.provider, deposit, allChainIds ); - if (canRefundPrefills && isDefined(verifiedFill) && !isSlowFill(prefill)) { + if (canRefundPrefills && isDefined(verifiedFill) && !isSlowFill(verifiedFill)) { validatedBundleV3Fills.push({ - ...prefill, + ...verifiedFill!, quoteTimestamp: deposit.quoteTimestamp, }); } @@ -1390,7 +1393,7 @@ export class BundleDataClient { // keccak256 hash of the relay data, which can be used as input into the on-chain `fillStatuses()` function in the // spoke pool contract. However, this internal function is used to uniquely identify a bridging event // for speed since its easier to build a string from the event data than to hash it. - private getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string { + protected getRelayHashFromEvent(event: V3DepositWithBlock | V3FillWithBlock | SlowFillRequestWithBlock): string { return `${event.depositor}-${event.recipient}-${event.exclusiveRelayer}-${event.inputToken}-${event.outputToken}-${ event.inputAmount }-${event.outputAmount}-${event.originChainId}-${event.depositId.toString()}-${event.fillDeadline}-${ @@ -1398,6 +1401,18 @@ export class BundleDataClient { }-${event.message}-${event.destinationChainId}`; } + protected async findMatchingFillEvent( + deposit: DepositWithBlock, + spokePoolClient: SpokePoolClient + ): Promise { + return await findFillEvent( + spokePoolClient.spokePool, + deposit, + spokePoolClient.deploymentBlock, + spokePoolClient.latestBlockSearched + ); + } + async getBundleBlockTimestamps( chainIds: number[], blockRangesForChains: number[][], From 68f50a447308a600602590f83242a7d2bb928624 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 17:26:36 -0500 Subject: [PATCH 051/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d0c50a8a..d1b3ca912 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.10", + "version": "4.0.0-beta.11", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 57a2a9ff9f05055334a1223f19bfedc118062d39 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 17:47:15 -0500 Subject: [PATCH 052/103] fix(BundleDataClient): Make sure bundle block timestamps have no gaps Currently there is a small chance that a fill deadline can fall between block ranges inspired by @bmzig 's comment here https://github.com/across-protocol/sdk/pull/835#discussion_r1934742099 --- src/clients/BundleDataClient/BundleDataClient.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index d1400c6c9..7b9f4b4f8 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1395,7 +1395,12 @@ export class BundleDataClient { // will usually be called in production with block ranges that were validated by // DataworkerUtils.blockRangesAreInvalidForSpokeClients. const startBlockForChain = Math.min(_startBlockForChain, spokePoolClient.latestBlockSearched); - const endBlockForChain = Math.min(_endBlockForChain, spokePoolClient.latestBlockSearched); + // @dev Add 1 to the bundle end block. The thinking here is that there can be a gap between + // block timestamps in subsequent blocks. The bundle data client assumes that fill deadlines expire + // in exactly one bundle, therefore we must make sure that the bundle block timestamp for one bundle's + // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way + // there are no gaps in block timestamps between bundles. + const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched); const [startTime, endTime] = [ await spokePoolClient.getTimestampForBlock(startBlockForChain), await spokePoolClient.getTimestampForBlock(endBlockForChain), From 01343f444194f1755b7eaee02a8afa1882778afc Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 17:47:33 -0500 Subject: [PATCH 053/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 615d018eb..c5ae8c1ea 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "3.4.16", + "version": "3.4.17", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 6ad712193bda14fc2792c32771d063342b1d07af Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:09:22 -0500 Subject: [PATCH 054/103] Revert "Merge branch 'bundle-block-timestamps' into pre-fills" This reverts commit b98d74addb679499bafa48f4b0603b94d780046c, reversing changes made to 68f50a447308a600602590f83242a7d2bb928624. --- package.json | 2 +- src/clients/BundleDataClient/BundleDataClient.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d8245d76a..d1b3ca912 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.12", + "version": "4.0.0-beta.11", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 3807f0da5..b4c289ff2 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1438,12 +1438,7 @@ export class BundleDataClient { // will usually be called in production with block ranges that were validated by // DataworkerUtils.blockRangesAreInvalidForSpokeClients. const startBlockForChain = Math.min(_startBlockForChain, spokePoolClient.latestBlockSearched); - // @dev Add 1 to the bundle end block. The thinking here is that there can be a gap between - // block timestamps in subsequent blocks. The bundle data client assumes that fill deadlines expire - // in exactly one bundle, therefore we must make sure that the bundle block timestamp for one bundle's - // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way - // there are no gaps in block timestamps between bundles. - const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched); + const endBlockForChain = Math.min(_endBlockForChain, spokePoolClient.latestBlockSearched); const [startTime, endTime] = [ await spokePoolClient.getTimestampForBlock(startBlockForChain), await spokePoolClient.getTimestampForBlock(endBlockForChain), From 6001a8cae0a4156848f77a3e16bb23b970772c53 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:11:07 -0500 Subject: [PATCH 055/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1b3ca912..d8245d76a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.11", + "version": "4.0.0-beta.12", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 6e90289edb73ac4ce62c3fe26006171417527a36 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:12:58 -0500 Subject: [PATCH 056/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 7b9f4b4f8..a54410d89 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1403,7 +1403,9 @@ export class BundleDataClient { const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched); const [startTime, endTime] = [ await spokePoolClient.getTimestampForBlock(startBlockForChain), - await spokePoolClient.getTimestampForBlock(endBlockForChain), + // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also + // no overlap, subtract 1 from the end time. + (await spokePoolClient.getTimestampForBlock(endBlockForChain)) - 1, ]; // Sanity checks: assert(endTime >= startTime, "End time should be greater than start time."); From 3058a4c9c24fdc46fda73f6e614e6845289803ca Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:27:12 -0500 Subject: [PATCH 057/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index a54410d89..de47557a6 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1401,11 +1401,12 @@ export class BundleDataClient { // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way // there are no gaps in block timestamps between bundles. const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched); + // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also + // no overlap, subtract 1 from the end time. + const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0; const [startTime, endTime] = [ await spokePoolClient.getTimestampForBlock(startBlockForChain), - // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also - // no overlap, subtract 1 from the end time. - (await spokePoolClient.getTimestampForBlock(endBlockForChain)) - 1, + (await spokePoolClient.getTimestampForBlock(endBlockForChain)) - endBlockDelta, ]; // Sanity checks: assert(endTime >= startTime, "End time should be greater than start time."); From bc5e5a6c04795e94fe4f650d27aa531c78a37165 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:12:58 -0500 Subject: [PATCH 058/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index b4c289ff2..d0ba97d60 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1441,7 +1441,9 @@ export class BundleDataClient { const endBlockForChain = Math.min(_endBlockForChain, spokePoolClient.latestBlockSearched); const [startTime, endTime] = [ await spokePoolClient.getTimestampForBlock(startBlockForChain), - await spokePoolClient.getTimestampForBlock(endBlockForChain), + // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also + // no overlap, subtract 1 from the end time. + (await spokePoolClient.getTimestampForBlock(endBlockForChain)) - 1, ]; // Sanity checks: assert(endTime >= startTime, "End time should be greater than start time."); From 89595e94b1d6b4c8d758895f962daa28b90a4b5d Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:25:45 -0500 Subject: [PATCH 059/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8245d76a..21f13d519 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.12", + "version": "4.0.0-beta.13", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 5aae5fae1cefa4ba2a7568714951fecd10f42a7c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:27:12 -0500 Subject: [PATCH 060/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index d0ba97d60..9ca9c16eb 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1438,12 +1438,18 @@ export class BundleDataClient { // will usually be called in production with block ranges that were validated by // DataworkerUtils.blockRangesAreInvalidForSpokeClients. const startBlockForChain = Math.min(_startBlockForChain, spokePoolClient.latestBlockSearched); - const endBlockForChain = Math.min(_endBlockForChain, spokePoolClient.latestBlockSearched); + // @dev Add 1 to the bundle end block. The thinking here is that there can be a gap between + // block timestamps in subsequent blocks. The bundle data client assumes that fill deadlines expire + // in exactly one bundle, therefore we must make sure that the bundle block timestamp for one bundle's + // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way + // there are no gaps in block timestamps between bundles. + const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched); + // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also + // no overlap, subtract 1 from the end time. + const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0; const [startTime, endTime] = [ await spokePoolClient.getTimestampForBlock(startBlockForChain), - // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also - // no overlap, subtract 1 from the end time. - (await spokePoolClient.getTimestampForBlock(endBlockForChain)) - 1, + (await spokePoolClient.getTimestampForBlock(endBlockForChain)) - endBlockDelta, ]; // Sanity checks: assert(endTime >= startTime, "End time should be greater than start time."); From 7ab59e35b434a307db67dcd3573e28de64c5b625 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:28:49 -0500 Subject: [PATCH 061/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21f13d519..472591820 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.13", + "version": "4.0.0-beta.14", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From d251e09e2c761352624e5d9847ec5e994e49c6be Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 18:47:51 -0500 Subject: [PATCH 062/103] fix --- src/clients/BundleDataClient/BundleDataClient.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index de47557a6..86a3ad792 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1401,15 +1401,20 @@ export class BundleDataClient { // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way // there are no gaps in block timestamps between bundles. const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched); + const [startTime, _endTime] = await Promise.all([ + spokePoolClient.getTimestampForBlock(startBlockForChain), + spokePoolClient.getTimestampForBlock(endBlockForChain), + ]); // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also // no overlap, subtract 1 from the end time. const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0; - const [startTime, endTime] = [ - await spokePoolClient.getTimestampForBlock(startBlockForChain), - (await spokePoolClient.getTimestampForBlock(endBlockForChain)) - endBlockDelta, - ]; + const endTime = Math.max(0, _endTime - endBlockDelta); + // Sanity checks: - assert(endTime >= startTime, "End time should be greater than start time."); + assert( + endTime >= startTime, + `End time for block ${endBlockForChain} should be greater than start time for block ${startBlockForChain}: ${endTime} >= ${startTime}.` + ); assert( startBlockForChain === 0 || startTime > 0, "Start timestamp must be greater than 0 if the start block is greater than 0." From 929eac8189b07e571d2afec3cff12c73587c6593 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 19:26:38 -0500 Subject: [PATCH 063/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 86a3ad792..d0a1f6d7d 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1401,10 +1401,10 @@ export class BundleDataClient { // end block is exactly equal to the bundle block timestamp for the next bundle's start block. This way // there are no gaps in block timestamps between bundles. const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestBlockSearched); - const [startTime, _endTime] = await Promise.all([ - spokePoolClient.getTimestampForBlock(startBlockForChain), - spokePoolClient.getTimestampForBlock(endBlockForChain), - ]); + const [startTime, _endTime] = [ + await spokePoolClient.getTimestampForBlock(startBlockForChain), + await spokePoolClient.getTimestampForBlock(endBlockForChain), + ]; // @dev similar to reasoning above to ensure no gaps between bundle block range timestamps and also // no overlap, subtract 1 from the end time. const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0; From b19482a4af56c27219e9f0b77a030390c1c9a767 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 19:27:59 -0500 Subject: [PATCH 064/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2b0a900d..bd0d84f77 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.15", + "version": "4.0.0-beta.16", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 2349b73a03d9ba62a8c52328255da6bf7ab48f78 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 29 Jan 2025 19:36:45 -0500 Subject: [PATCH 065/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd0d84f77..db9aca158 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.16", + "version": "4.0.0-beta.17", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 79f13f29988d732d1d719522509b5e806bde20ef Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 13:30:52 -0500 Subject: [PATCH 066/103] Add case work --- .../BundleDataClient/BundleDataClient.ts | 116 +++++++++++------- src/clients/SpokePoolClient.ts | 19 ++- 2 files changed, 89 insertions(+), 46 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 92dae07c8..abbcc1fb1 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -884,36 +884,39 @@ export class BundleDataClient { .filter( (fill) => fill.blockNumber <= destinationChainBlockRange[1] && !isZeroValueFillOrSlowFillRequest(fill) ), - async (_fill) => { + async (fill) => { fillCounter++; - const relayDataHash = this.getRelayHashFromEvent(_fill); + const relayDataHash = this.getRelayHashFromEvent(fill); if (v3RelayHashes[relayDataHash]) { if (!v3RelayHashes[relayDataHash].fill) { assert( isDefined(v3RelayHashes[relayDataHash].deposit), "Deposit should exist in relay hash dictionary." ); - // `fill` will only possibly differ from `_fill` in the `relayer` field, which does not affect the - // relay hash, so it is safe to modify. - const fill = await verifyFillRepayment( - _fill, - destinationClient.spokePool.provider, - v3RelayHashes[relayDataHash].deposit!, - allChainIds - ); - if (!isDefined(fill)) { - bundleInvalidFillsV3.push(_fill); - return; - } - // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit, - // so this fill is validated. + // so this fill can no longer be filled on-chain. v3RelayHashes[relayDataHash].fill = fill; if (fill.blockNumber >= destinationChainBlockRange[0]) { - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: v3RelayHashes[relayDataHash].deposit!.quoteTimestamp, // ! due to assert above - }); + // `fill` will only possibly differ from `_fill` in the `relayer` field, which does not affect the + // relay hash, so it is safe to modify. + const fillToRefund = await verifyFillRepayment( + fill, + destinationClient.spokePool.provider, + v3RelayHashes[relayDataHash].deposit!, + allChainIds + ); + if (!isDefined(fillToRefund)) { + bundleInvalidFillsV3.push(fill); + // We don't return here yet because we still need to mark unexecutable slow fill leaves + // or duplicate deposits. However, we won't issue a fast fill refund. + } else { + v3RelayHashes[relayDataHash].fill = fillToRefund; + validatedBundleV3Fills.push({ + ...fillToRefund, + quoteTimestamp: v3RelayHashes[relayDataHash].deposit!.quoteTimestamp, // ! due to assert above + }); + } + // If fill replaced a slow fill request, then mark it as one that might have created an // unexecutable slow fill. We can't know for sure until we check the slow fill request // events. @@ -924,7 +927,18 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } + // Now that know this deposit has been filled on-chain, identify any duplicate deposits sent for this fill and refund + // them, because they would not be refunded otherwise. These deposits can no longer expire and get + // refunded as an expired deposit, and they won't trigger a pre-fill refund because the fill is + // in this bundle. Pre-fill refunds only happen when deposits are sent in this bundle and the + // fill is from a prior bundle. + const duplicateDeposits = originClient.getDuplicateDeposits(v3RelayHashes[relayDataHash].deposit!); + duplicateDeposits.forEach((duplicateDeposit) => { + updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); + }); } + } else { + throw new Error("Duplicate fill detected."); } return; } @@ -933,7 +947,7 @@ export class BundleDataClient { // instantiate the entry. We won't modify the fill.relayer until we match it with a deposit. v3RelayHashes[relayDataHash] = { deposit: undefined, - fill: _fill, + fill, slowFillRequest: undefined, }; @@ -945,44 +959,47 @@ export class BundleDataClient { // older deposit in case the spoke pool client's lookback isn't old enough to find the matching deposit. // We can skip this step if the fill's fill deadline is not infinite, because we can assume that the // spoke pool clients have loaded deposits old enough to cover all fills with a non-infinite fill deadline. - if (_fill.blockNumber >= destinationChainBlockRange[0]) { + if (fill.blockNumber >= destinationChainBlockRange[0]) { // Fill has a non-infinite expiry, and we can assume our spoke pool clients have old enough deposits // to conclude that this fill is invalid if we haven't found a matching deposit in memory, so // skip the historical query. - if (!INFINITE_FILL_DEADLINE.eq(_fill.fillDeadline)) { - bundleInvalidFillsV3.push(_fill); + if (!INFINITE_FILL_DEADLINE.eq(fill.fillDeadline)) { + bundleInvalidFillsV3.push(fill); return; } // If deposit is using the deterministic relay hash feature, then the following binary search-based // algorithm will not work. However, it is impossible to emit an infinite fill deadline using // the unsafeDepositV3 function so there is no need to catch the special case. - const historicalDeposit = await queryHistoricalDepositForFill(originClient, _fill); + const historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); if (!historicalDeposit.found) { - bundleInvalidFillsV3.push(_fill); + bundleInvalidFillsV3.push(fill); } else { const matchedDeposit = historicalDeposit.deposit; - const fill = await verifyFillRepayment( - _fill, + v3RelayHashes[relayDataHash].deposit = matchedDeposit; + + const fillToRefund = await verifyFillRepayment( + fill, destinationClient.spokePool.provider, matchedDeposit, allChainIds ); - if (!isDefined(fill)) { - bundleInvalidFillsV3.push(_fill); - return; + if (!isDefined(fillToRefund)) { + bundleInvalidFillsV3.push(fill); + // Don't return yet as we still need to mark down any unexecutable slow fill leaves + // in case this fast fill replaced a slow fill request. + } else { + // @dev Since queryHistoricalDepositForFill validates the fill by checking individual + // object property values against the deposit's, we + // sanity check it here by comparing the full relay hashes. If there's an error here then the + // historical deposit query is not working as expected. + assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match."); + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: matchedDeposit.quoteTimestamp, + }); + v3RelayHashes[relayDataHash].fill = fillToRefund; } - v3RelayHashes[relayDataHash].fill = fill; - // @dev Since queryHistoricalDepositForFill validates the fill by checking individual - // object property values against the deposit's, we - // sanity check it here by comparing the full relay hashes. If there's an error here then the - // historical deposit query is not working as expected. - assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match."); - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: matchedDeposit.quoteTimestamp, - }); - v3RelayHashes[relayDataHash].deposit = matchedDeposit; // slow fill requests for deposits from or to lite chains are considered invalid if ( fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill && @@ -990,6 +1007,10 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } + + // No need to check for duplicate deposits here since we would have seen them in memory if they + // had a non-infinite fill deadline, and duplicate deposits with infinite deadlines are impossible + // to send. } } } @@ -1014,7 +1035,9 @@ export class BundleDataClient { v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest; if (v3RelayHashes[relayDataHash].fill) { // If there is a fill matching the relay hash, then this slow fill request can't be used - // to create a slow fill for a filled deposit. + // to create a slow fill for a filled deposit. This takes advantage of the fact that + // slow fill requests must precede fills, so if there is a matching fill for this request's + // relay data, then this slow fill will be unexecutable. return; } assert( @@ -1037,6 +1060,8 @@ export class BundleDataClient { // so this slow fill request relay data is correct. validatedBundleSlowFills.push(matchedDeposit); } + } else { + throw new Error("Duplicate slow fill request detected."); } return; } @@ -1132,6 +1157,11 @@ export class BundleDataClient { ...fill, quoteTimestamp: deposit.quoteTimestamp, }); + + // We don't refund duplicate deposits for pre-fill refunds because we are refunding the pre-fill instead + // using the duplicate deposited funds. We make an assumption that duplicate deposits for pre-fills + // are highly unlikely because deposits for pre-fills are designed to be sent by the pre-filler, + // so the depositor's approval should protect them from the pre-filler sending multiple deposits. } return; } diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index edcec21a7..07f9f20ac 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -135,6 +135,18 @@ export class SpokePoolClient extends BaseAbstractClient { return Object.values(this.depositHashes).filter((deposit) => deposit.destinationChainId === destinationChainId); } + /** + * Retrieves a list of duplicate deposits matching the given deposit's deposit hash. + * @notice A duplicate is considered any deposit sent after the original deposit with the same deposit hash. + * @param deposit The deposit to find duplicates for. + * @returns A list of duplicate deposits. Does NOT include the original deposit + * unless the original deposit is a duplicate. + */ + public getDuplicateDeposits(deposit: DepositWithBlock): DepositWithBlock[] { + const depositHash = this.getDepositHash(deposit); + return this.duplicateDepositHashes[depositHash] ?? []; + } + /** * Returns a list of all deposits including any duplicate ones. Designed only to be used in use cases where * all deposits are required, regardless of duplicates. For example, the Dataworker can use this to refund @@ -144,9 +156,10 @@ export class SpokePoolClient extends BaseAbstractClient { */ public getDepositsForDestinationChainWithDuplicates(destinationChainId: number): DepositWithBlock[] { const deposits = this.getDepositsForDestinationChain(destinationChainId); - const duplicateDeposits = Object.values(this.duplicateDepositHashes).filter( - (deposits) => deposits.length > 0 && deposits[0].destinationChainId === destinationChainId - ); + const duplicateDeposits = deposits.reduce((acc, deposit) => { + const duplicates = this.getDuplicateDeposits(deposit); + return acc.concat(duplicates); + }, [] as DepositWithBlock[]); return sortEventsAscendingInPlace(deposits.concat(duplicateDeposits.flat())); } From d8b439e03af9753f0aa47922b528d2845cd6aaf0 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 13:45:45 -0500 Subject: [PATCH 067/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db9aca158..dc3d29916 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.17", + "version": "4.0.0-beta.18", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 52090fb7f33903962d2c25b8dd057703f7253ab8 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 13:57:06 -0500 Subject: [PATCH 068/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index abbcc1fb1..16c3e3509 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -994,7 +994,7 @@ export class BundleDataClient { // historical deposit query is not working as expected. assert(this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Relay hashes should match."); validatedBundleV3Fills.push({ - ...fill, + ...fillToRefund, quoteTimestamp: matchedDeposit.quoteTimestamp, }); v3RelayHashes[relayDataHash].fill = fillToRefund; From 709952b1a4b48d44c555b0d897ca7383f046f415 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 17:24:35 -0500 Subject: [PATCH 069/103] Refund duplicate deposits --- .../BundleDataClient/BundleDataClient.ts | 253 ++++++++++-------- 1 file changed, 147 insertions(+), 106 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 16c3e3509..576de1c61 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -777,7 +777,7 @@ export class BundleDataClient { // Note: Since there are no partial fills in v3, there should only be one fill per relay hash. // Moreover, the SpokePool blocks multiple slow fill requests, so // there should also only be one slow fill request per relay hash. - deposit?: V3DepositWithBlock; + deposits?: V3DepositWithBlock[]; fill?: V3FillWithBlock; slowFillRequest?: SlowFillRequestWithBlock; }; @@ -788,6 +788,11 @@ export class BundleDataClient { const bundleDepositHashes: string[] = []; const olderDepositHashes: string[] = []; + const decodeBundleDepositHash = (depositHash: string): { relayDataHash: string; index: number } => { + const [relayDataHash, i] = depositHash.split("@"); + return { relayDataHash, index: Number(i) }; + }; + // We use the following toggle to aid with the migration to pre-fills. The first bundle proposed using this // pre-fill logic can double refund pre-fills that have already been filled in the last bundle, because the // last bundle did not recognize a fill as a pre-fill. Therefore the developer should ensure that the version @@ -823,13 +828,14 @@ export class BundleDataClient { depositCounter++; const relayDataHash = this.getRelayHashFromEvent(deposit); - // Duplicate deposits are treated like normal deposits. if (!v3RelayHashes[relayDataHash]) { v3RelayHashes[relayDataHash] = { - deposit: deposit, + deposits: [deposit], fill: undefined, slowFillRequest: undefined, }; + } else { + v3RelayHashes[relayDataHash].deposits!.push(deposit); } // Once we've saved the deposit hash into v3RelayHashes, then we can exit early here if the inputAmount @@ -842,11 +848,18 @@ export class BundleDataClient { // Evaluate all expired deposits after fetching fill statuses, // since we can't know for certain whether an expired deposit was filled a long time ago. + const newBundleDepositHash = `${relayDataHash}@${v3RelayHashes[relayDataHash].deposits!.length - 1}`; + const decodedBundleDepositHash = decodeBundleDepositHash(newBundleDepositHash); + assert( + decodedBundleDepositHash.relayDataHash === relayDataHash && + decodedBundleDepositHash.index === v3RelayHashes[relayDataHash].deposits!.length - 1, + "Not using correct bundle deposit hash key" + ); if (deposit.blockNumber >= originChainBlockRange[0]) { - bundleDepositHashes.push(relayDataHash); + bundleDepositHashes.push(newBundleDepositHash); updateBundleDepositsV3(bundleDepositsV3, deposit); } else if (deposit.blockNumber < originChainBlockRange[0]) { - olderDepositHashes.push(relayDataHash); + olderDepositHashes.push(newBundleDepositHash); } }); } @@ -890,7 +903,7 @@ export class BundleDataClient { if (v3RelayHashes[relayDataHash]) { if (!v3RelayHashes[relayDataHash].fill) { assert( - isDefined(v3RelayHashes[relayDataHash].deposit), + isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0, "Deposit should exist in relay hash dictionary." ); // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit, @@ -902,7 +915,7 @@ export class BundleDataClient { const fillToRefund = await verifyFillRepayment( fill, destinationClient.spokePool.provider, - v3RelayHashes[relayDataHash].deposit!, + v3RelayHashes[relayDataHash].deposits![0], allChainIds ); if (!isDefined(fillToRefund)) { @@ -913,7 +926,7 @@ export class BundleDataClient { v3RelayHashes[relayDataHash].fill = fillToRefund; validatedBundleV3Fills.push({ ...fillToRefund, - quoteTimestamp: v3RelayHashes[relayDataHash].deposit!.quoteTimestamp, // ! due to assert above + quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp, // ! due to assert above }); } @@ -923,7 +936,7 @@ export class BundleDataClient { // slow fill requests for deposits from or to lite chains are considered invalid if ( fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill && - _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposit!) + _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposits![0]) ) { fastFillsReplacingSlowFills.push(relayDataHash); } @@ -932,7 +945,7 @@ export class BundleDataClient { // refunded as an expired deposit, and they won't trigger a pre-fill refund because the fill is // in this bundle. Pre-fill refunds only happen when deposits are sent in this bundle and the // fill is from a prior bundle. - const duplicateDeposits = originClient.getDuplicateDeposits(v3RelayHashes[relayDataHash].deposit!); + const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1); duplicateDeposits.forEach((duplicateDeposit) => { updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); }); @@ -946,7 +959,7 @@ export class BundleDataClient { // At this point, there is no relay hash dictionary entry for this fill, so we need to // instantiate the entry. We won't modify the fill.relayer until we match it with a deposit. v3RelayHashes[relayDataHash] = { - deposit: undefined, + deposits: undefined, fill, slowFillRequest: undefined, }; @@ -975,7 +988,7 @@ export class BundleDataClient { bundleInvalidFillsV3.push(fill); } else { const matchedDeposit = historicalDeposit.deposit; - v3RelayHashes[relayDataHash].deposit = matchedDeposit; + v3RelayHashes[relayDataHash].deposits = [matchedDeposit]; const fillToRefund = await verifyFillRepayment( fill, @@ -1041,11 +1054,11 @@ export class BundleDataClient { return; } assert( - isDefined(v3RelayHashes[relayDataHash].deposit), + isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0, "Deposit should exist in relay hash dictionary." ); // The ! is safe here because we've already checked that the deposit exists in the relay hash dictionary. - const matchedDeposit = v3RelayHashes[relayDataHash].deposit!; + const matchedDeposit = v3RelayHashes[relayDataHash].deposits![0]; // If there is no fill matching the relay hash, then this might be a valid slow fill request // that we should produce a slow fill leaf for. Check if the slow fill request is in the @@ -1068,7 +1081,7 @@ export class BundleDataClient { // Instantiate dictionary if there is neither a deposit nor fill matching it. v3RelayHashes[relayDataHash] = { - deposit: undefined, + deposits: undefined, fill: undefined, slowFillRequest: slowFillRequest, }; @@ -1103,7 +1116,7 @@ export class BundleDataClient { this.getRelayHashFromEvent(matchedDeposit) === relayDataHash, "Deposit relay hashes should match." ); - v3RelayHashes[relayDataHash].deposit = matchedDeposit; + v3RelayHashes[relayDataHash].deposits = [matchedDeposit]; if ( !_canCreateSlowFillLeaf(matchedDeposit) || @@ -1127,77 +1140,101 @@ export class BundleDataClient { // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We // should remove this check once we've advanced far beyond the version bump block. - await mapAsync( - bundleDepositHashes.filter((depositHash) => { - const { deposit } = v3RelayHashes[depositHash]; - return ( - deposit && deposit.originChainId === originChainId && deposit.destinationChainId === destinationChainId - ); - }), - async (depositHash) => { - // We don't need to call verifyFillRepayment() here to replace the fill.relayer because this value should already - // be overwritten because the deposit and fill both exist. - const { deposit, fill, slowFillRequest } = v3RelayHashes[depositHash]; - if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); - - // We are willing to refund a pre-fill multiple times for each duplicate deposit. - // This is because a duplicate deposit for a pre-fill cannot get - // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore - // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out - // the pre-filler. - - // If fill exists in memory, then the only case in which we need to create a refund is if the - // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. - if (fill) { - if (canRefundPrefills && fill.blockNumber < destinationChainBlockRange[0] && !isSlowFill(fill)) { + await mapAsync(bundleDepositHashes, async (depositHash, currentBundleDepositHashIndex) => { + // We don't need to call verifyFillRepayment() here to replace the fill.relayer because this value should already + // be overwritten because the deposit and fill both exist. + const { relayDataHash, index } = decodeBundleDepositHash(depositHash); + const { deposits, fill, slowFillRequest } = v3RelayHashes[relayDataHash]; + const deposit = deposits![index]; + if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); + if (deposit.originChainId !== originChainId || deposit.destinationChainId !== destinationChainId) { + return; + } + const isDuplicateDepositInBundle = bundleDepositHashes + .slice(0, currentBundleDepositHashIndex) + .some((_depositHash) => { + const { relayDataHash: _relayDataHash } = decodeBundleDepositHash(_depositHash); + return _relayDataHash === relayDataHash; + }); + // Don't refund duplicate deposits from a prior bundle, as they should have been refunded already + // if they coincided with another deposit in the same bundle. If they didn't, then its input + // amount was used to refund a pre-fill. + // We will refund any duplicate deposits the first time that we see a deposit hash in this bundle. + // If this is the first time we are seeing this deposit hash, then refund any duplicate deposits since + // a fill exists for it and these duplicate deposits can no longer be refunded for expiry. + // This means unfortunately that every duplicate deposit that is sent that + // does not accompany another deposit in the same bundle will not be refunded. This should be unlikely. + // This rule also allows us to protect honest depositors who accidentally send duplicate deposits + // in rapid succession in most cases, unless they are unlucky enough to send duplicate deposits + // in different bundle block ranges. + const duplicateDepositsInBundle = deposits!.slice(index + 1); + + // We are willing to refund a pre-fill multiple times for each duplicate deposit. + // This is because a duplicate deposit for a pre-fill cannot get + // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore + // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out + // the pre-filler. + + // If fill exists in memory, then the only case in which we need to create a refund is if the + // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. + if (fill) { + if (!isDuplicateDepositInBundle && fill.blockNumber < destinationChainBlockRange[0]) { + duplicateDepositsInBundle.forEach((duplicateDeposit) => { + updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); + }); + if (canRefundPrefills) { // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: deposit.quoteTimestamp, - }); - - // We don't refund duplicate deposits for pre-fill refunds because we are refunding the pre-fill instead - // using the duplicate deposited funds. We make an assumption that duplicate deposits for pre-fills - // are highly unlikely because deposits for pre-fills are designed to be sent by the pre-filler, - // so the depositor's approval should protect them from the pre-filler sending multiple deposits. + if (!isSlowFill(fill)) { + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: deposit.quoteTimestamp, + }); + } } - return; } + return; + } - // If a slow fill request exists in memory, then we know the deposit has not been filled because fills - // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, - // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or - // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request - // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. - if (slowFillRequest) { - if (_depositIsExpired(deposit)) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } else if ( - canRefundPrefills && - slowFillRequest.blockNumber < destinationChainBlockRange[0] && - _canCreateSlowFillLeaf(deposit) - ) { - validatedBundleSlowFills.push(deposit); - } - return; + // If a slow fill request exists in memory, then we know the deposit has not been filled because fills + // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, + // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or + // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request + // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. + if (slowFillRequest) { + if (_depositIsExpired(deposit)) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } else if ( + !isDuplicateDepositInBundle && + canRefundPrefills && + slowFillRequest.blockNumber < destinationChainBlockRange[0] && + _canCreateSlowFillLeaf(deposit) + ) { + validatedBundleSlowFills.push(deposit); } + return; + } - // So at this point in the code, there is no fill or slow fill request in memory for this deposit. - // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. - // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles - // because the spoke pool client lookback would have returned this entire bundle of events and stored - // them into the relay hash dictionary. - const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); - - // If deposit was filled, then we need to issue a refund for it. - if (fillStatus === FillStatus.Filled) { - // We need to find the fill event to issue a refund to the right relayer and repayment chain, - // or msg.sender if relayer address is invalid for the repayment chain. - const prefill = await this.findMatchingFillEvent(deposit, destinationClient); - assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${depositHash}`); - assert(this.getRelayHashFromEvent(prefill!) === depositHash, "Relay hashes should match."); + // So at this point in the code, there is no fill or slow fill request in memory for this deposit. + // We need to check its fill status on-chain to figure out whether to issue a refund or a slow fill leaf. + // We can assume at this point that all fills or slow fill requests, if found, were in previous bundles + // because the spoke pool client lookback would have returned this entire bundle of events and stored + // them into the relay hash dictionary. + const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); + + // If deposit was filled, then we need to issue a refund for the fill and also any duplicate deposits + // in the same bundle. + if (fillStatus === FillStatus.Filled) { + // We need to find the fill event to issue a refund to the right relayer and repayment chain, + // or msg.sender if relayer address is invalid for the repayment chain. + const prefill = await this.findMatchingFillEvent(deposit, destinationClient); + assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`); + assert(this.getRelayHashFromEvent(prefill!) === relayDataHash, "Relay hashes should match."); + if (!isDuplicateDepositInBundle) { + duplicateDepositsInBundle.forEach((duplicateDeposit) => { + updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); + }); const verifiedFill = await verifyFillRepayment( prefill!, destinationClient.spokePool.provider, @@ -1211,42 +1248,42 @@ export class BundleDataClient { }); } } - // If deposit is not filled and its newly expired, we can create a deposit refund for it. - // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because - // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 - // for example. Those should be included in this bundle of refunded deposits. - else if (_depositIsExpired(deposit)) { - updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } - // If slow fill requested, then issue a slow fill leaf for the deposit. - else if (fillStatus === FillStatus.RequestedSlowFill) { - // Input and Output tokens must be equivalent on the deposit for this to be slow filled. - // Slow fill requests for deposits from or to lite chains are considered invalid - if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) { - // If deposit newly expired, then we can't create a slow fill leaf for it but we can - // create a deposit refund for it. - validatedBundleSlowFills.push(deposit); - } + } + // If deposit is not filled and its newly expired, we can create a deposit refund for it. + // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because + // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 + // for example. Those should be included in this bundle of refunded deposits. + else if (_depositIsExpired(deposit)) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); + } + // If slow fill requested, then issue a slow fill leaf for the deposit. + else if (fillStatus === FillStatus.RequestedSlowFill) { + // Input and Output tokens must be equivalent on the deposit for this to be slow filled. + // Slow fill requests for deposits from or to lite chains are considered invalid + if (!isDuplicateDepositInBundle && canRefundPrefills && _canCreateSlowFillLeaf(deposit)) { + // If deposit newly expired, then we can't create a slow fill leaf for it but we can + // create a deposit refund for it. + validatedBundleSlowFills.push(deposit); } } - ); + }); // For all fills that came after a slow fill request, we can now check if the slow fill request // was a valid one and whether it was created in a previous bundle. If so, then it created a slow fill // leaf that is now unexecutable. fastFillsReplacingSlowFills.forEach((relayDataHash) => { - const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; + const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; assert( fill?.relayExecutionInfo.fillType === FillType.ReplacedSlowFill, "Fill type should be ReplacedSlowFill." ); // Needed for TSC - are implicitely checking that deposit exists by making it to this point. - if (!deposit) { + if (!deposits || deposits.length < 1) { throw new Error("Deposit should exist in relay hash dictionary."); } // We should never push fast fills involving lite chains here because slow fill requests for them are invalid: assert( - _canCreateSlowFillLeaf(deposit), + _canCreateSlowFillLeaf(deposits[0]), "fastFillsReplacingSlowFills should contain only deposits that can be slow filled" ); const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); @@ -1256,7 +1293,7 @@ export class BundleDataClient { !slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0] ) { - validatedBundleUnexecutableSlowFills.push(deposit); + validatedBundleUnexecutableSlowFills.push(deposits[0]); } }); } @@ -1270,10 +1307,14 @@ export class BundleDataClient { // For all deposits older than this bundle, we need to check if they expired in this bundle and if they did, // whether there was a slow fill created for it in a previous bundle that is now unexecutable and replaced // by a new expired deposit refund. - await forEachAsync(olderDepositHashes, async (relayDataHash) => { - const { deposit, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; - assert(isDefined(deposit), "Deposit should exist in relay hash dictionary."); - const { destinationChainId } = deposit!; + await forEachAsync(olderDepositHashes, async (depositHash) => { + const { relayDataHash, index } = decodeBundleDepositHash(depositHash); + const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash]; + if (!deposits || deposits.length < 1) { + throw new Error("Deposit should exist in relay hash dictionary."); + } + const deposit = deposits[index]; + const { destinationChainId } = deposit; const destinationBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); // Only look for deposits that were mined before this bundle and that are newly expired. @@ -1322,7 +1363,7 @@ export class BundleDataClient { validatedBundleV3Fills.length > 0 ? this.clients.hubPoolClient.batchComputeRealizedLpFeePct( validatedBundleV3Fills.map((fill) => { - const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit; + const matchedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0]; assert(isDefined(matchedDeposit), "Deposit should exist in relay hash dictionary."); const { chainToSendRefundTo: paymentChainId } = getRefundInformationFromFill( fill, @@ -1366,7 +1407,7 @@ export class BundleDataClient { }); v3FillLpFees.forEach(({ realizedLpFeePct }, idx) => { const fill = validatedBundleV3Fills[idx]; - const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposit; + const associatedDeposit = v3RelayHashes[this.getRelayHashFromEvent(fill)].deposits![0]; assert(isDefined(associatedDeposit), "Deposit should exist in relay hash dictionary."); const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill( fill, From 4a520a68902402c7f97315d48a67f7dbe159a8fb Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 18:10:54 -0500 Subject: [PATCH 070/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc3d29916..c8b04ca80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.18", + "version": "4.0.0-beta.19", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 0b5a43dbe52a849e9c26474edb2aa4b2a7333bcb Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 19:08:02 -0500 Subject: [PATCH 071/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8b04ca80..442edec26 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.19", + "version": "4.0.0-beta.20", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 02604897b6756117c9007c4fd7aa457b5e44f305 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 19:18:13 -0500 Subject: [PATCH 072/103] fix --- package.json | 2 +- src/clients/BundleDataClient/BundleDataClient.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 442edec26..9cbd8e826 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.20", + "version": "4.0.0-beta.21", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 061eb63b7..7dbb2b349 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1239,7 +1239,9 @@ export class BundleDataClient { deposit, allChainIds ); - if (canRefundPrefills && isDefined(verifiedFill) && !isSlowFill(verifiedFill)) { + if (!isDefined(verifiedFill)) { + bundleUnrepayableFillsV3.push(prefill!); + } else if (canRefundPrefills && !isSlowFill(verifiedFill)) { validatedBundleV3Fills.push({ ...verifiedFill!, quoteTimestamp: deposit.quoteTimestamp, From 8aa383573c7e1b2f7ba45c6fe18aad814314c47d Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 19:28:39 -0500 Subject: [PATCH 073/103] use ZERO_BYTES --- src/constants.ts | 2 -- src/utils/DepositUtils.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 008e9a929..51ebd10d3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -57,8 +57,6 @@ export const DEFAULT_ARWEAVE_STORAGE_ADDRESS = "Z6hjBM8FHu90lYWB8o5jR1dfX92FlV2W export const EMPTY_MESSAGE = "0x"; -export const EMPTY_MESSAGE_HASH = ethersConstants.HashZero; - export const BRIDGED_USDC_SYMBOLS = [ TOKEN_SYMBOLS_MAP["USDC.e"].symbol, TOKEN_SYMBOLS_MAP.USDbC.symbol, diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index e32922c70..b5faef0bc 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -1,6 +1,6 @@ import assert from "assert"; import { SpokePoolClient } from "../clients"; -import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE, EMPTY_MESSAGE_HASH } from "../constants"; +import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE, ZERO_BYTES } from "../constants"; import { CachingMechanismInterface, Deposit, DepositWithBlock, Fill, SlowFillRequest } from "../interfaces"; import { getNetworkName } from "./NetworkUtils"; import { getDepositInCache, getDepositKey, setDepositInCache } from "./CachingUtils"; @@ -160,7 +160,7 @@ export function isMessageEmpty(message = EMPTY_MESSAGE): boolean { } export function isFillOrSlowFillRequestMessageEmpty(message: string): boolean { - return isMessageEmpty(message) || message === EMPTY_MESSAGE_HASH; + return isMessageEmpty(message) || message === ZERO_BYTES; } /** From dc6d4826d0fa465501113d022b73ba2727f5e9d8 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 19:52:23 -0500 Subject: [PATCH 074/103] Remove isSlowFill check --- src/clients/BundleDataClient/BundleDataClient.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 7dbb2b349..35668a330 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1184,12 +1184,10 @@ export class BundleDataClient { // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. - if (!isSlowFill(fill)) { - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: deposit.quoteTimestamp, - }); - } + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: deposit.quoteTimestamp, + }); } } return; @@ -1241,7 +1239,7 @@ export class BundleDataClient { ); if (!isDefined(verifiedFill)) { bundleUnrepayableFillsV3.push(prefill!); - } else if (canRefundPrefills && !isSlowFill(verifiedFill)) { + } else if (canRefundPrefills) { validatedBundleV3Fills.push({ ...verifiedFill!, quoteTimestamp: deposit.quoteTimestamp, From 9ebd0f287dccbb483595a4036c53d453dab23c7b Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 30 Jan 2025 19:52:52 -0500 Subject: [PATCH 075/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9cbd8e826..0c3b401b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.21", + "version": "4.0.0-beta.22", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From d1eafe59914bef3abefc6afbd21cc14e63ca8a1c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 07:33:48 -0500 Subject: [PATCH 076/103] Revert "Remove isSlowFill check" This reverts commit dc6d4826d0fa465501113d022b73ba2727f5e9d8. --- src/clients/BundleDataClient/BundleDataClient.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 35668a330..7dbb2b349 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1184,10 +1184,12 @@ export class BundleDataClient { // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: deposit.quoteTimestamp, - }); + if (!isSlowFill(fill)) { + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: deposit.quoteTimestamp, + }); + } } } return; @@ -1239,7 +1241,7 @@ export class BundleDataClient { ); if (!isDefined(verifiedFill)) { bundleUnrepayableFillsV3.push(prefill!); - } else if (canRefundPrefills) { + } else if (canRefundPrefills && !isSlowFill(verifiedFill)) { validatedBundleV3Fills.push({ ...verifiedFill!, quoteTimestamp: deposit.quoteTimestamp, From 3479dae7cfd78279f58eb619e71a8c839501dc14 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 07:45:58 -0500 Subject: [PATCH 077/103] Re-add slow fill check and refund to depositor --- .../BundleDataClient/BundleDataClient.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 7dbb2b349..d2ea4f520 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1176,20 +1176,22 @@ export class BundleDataClient { // If fill exists in memory, then the only case in which we need to create a refund is if the // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. if (fill) { - if (!isDuplicateDepositInBundle && fill.blockNumber < destinationChainBlockRange[0]) { + if (canRefundPrefills && !isDuplicateDepositInBundle && fill.blockNumber < destinationChainBlockRange[0]) { duplicateDepositsInBundle.forEach((duplicateDeposit) => { updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); }); - if (canRefundPrefills) { // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. - if (!isSlowFill(fill)) { - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: deposit.quoteTimestamp, - }); - } + if (!isSlowFill(fill)) { + validatedBundleV3Fills.push({ + ...fill, + quoteTimestamp: deposit.quoteTimestamp, + }); + } else { + // Slow fills cannot result in refunds to a relayer to refund the deposit. Slow fills also + // were created after the deposit was sent, so we can assume this deposit is a duplicate. + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } } return; @@ -1229,7 +1231,7 @@ export class BundleDataClient { const prefill = await this.findMatchingFillEvent(deposit, destinationClient); assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`); assert(this.getRelayHashFromEvent(prefill!) === relayDataHash, "Relay hashes should match."); - if (!isDuplicateDepositInBundle) { + if (canRefundPrefills && !isDuplicateDepositInBundle) { duplicateDepositsInBundle.forEach((duplicateDeposit) => { updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); }); @@ -1241,11 +1243,15 @@ export class BundleDataClient { ); if (!isDefined(verifiedFill)) { bundleUnrepayableFillsV3.push(prefill!); - } else if (canRefundPrefills && !isSlowFill(verifiedFill)) { + } else if (!isSlowFill(verifiedFill)) { validatedBundleV3Fills.push({ ...verifiedFill!, quoteTimestamp: deposit.quoteTimestamp, }); + } else { + // Slow fills cannot result in refunds to a relayer to refund the deposit. Slow fills also + // were created after the deposit was sent, so we can assume this deposit is a duplicate. + updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } } } From 6ffd1a0a7c656025a12565d897860a03648063ac Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 11:16:46 -0500 Subject: [PATCH 078/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index d2ea4f520..f1e1e5976 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1180,9 +1180,9 @@ export class BundleDataClient { duplicateDepositsInBundle.forEach((duplicateDeposit) => { updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); }); - // If fill is in the current bundle then we can assume there is already a refund for it, so only - // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then - // we won't consider it, following the previous treatment of fills after the bundle block range. + // If fill is in the current bundle then we can assume there is already a refund for it, so only + // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then + // we won't consider it, following the previous treatment of fills after the bundle block range. if (!isSlowFill(fill)) { validatedBundleV3Fills.push({ ...fill, From 5ada1fbb186b1e9795a8aed90173656c95f4e932 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 12:14:27 -0500 Subject: [PATCH 079/103] Remove duplicate deposit refunds and revert back to refunding pre-fills --- .../BundleDataClient/BundleDataClient.ts | 56 +++++-------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index f1e1e5976..9e19e3d6d 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -938,15 +938,6 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } - // Now that know this deposit has been filled on-chain, identify any duplicate deposits sent for this fill and refund - // them, because they would not be refunded otherwise. These deposits can no longer expire and get - // refunded as an expired deposit, and they won't trigger a pre-fill refund because the fill is - // in this bundle. Pre-fill refunds only happen when deposits are sent in this bundle and the - // fill is from a prior bundle. - const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1); - duplicateDeposits.forEach((duplicateDeposit) => { - updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); - }); } } else { throw new Error("Duplicate fill detected"); @@ -1018,10 +1009,6 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } - - // No need to check for duplicate deposits here since we would have seen them in memory if they - // had a non-infinite fill deadline, and duplicate deposits with infinite deadlines are impossible - // to send. } } } @@ -1138,34 +1125,19 @@ export class BundleDataClient { // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We // should remove this check once we've advanced far beyond the version bump block. - await mapAsync(bundleDepositHashes, async (depositHash, currentBundleDepositHashIndex) => { + await mapAsync(bundleDepositHashes, async (depositHash) => { // We don't need to call verifyFillRepayment() here to replace the fill.relayer because this value should already // be overwritten because the deposit and fill both exist. const { relayDataHash, index } = decodeBundleDepositHash(depositHash); const { deposits, fill, slowFillRequest } = v3RelayHashes[relayDataHash]; - const deposit = deposits![index]; + if (!deposits || deposits.length === 0) { + throw new Error("Deposits should exist in relay hash dictionary."); + } + const deposit = deposits[index]; if (!deposit) throw new Error("Deposit should exist in relay hash dictionary."); if (deposit.originChainId !== originChainId || deposit.destinationChainId !== destinationChainId) { return; } - const isDuplicateDepositInBundle = bundleDepositHashes - .slice(0, currentBundleDepositHashIndex) - .some((_depositHash) => { - const { relayDataHash: _relayDataHash } = decodeBundleDepositHash(_depositHash); - return _relayDataHash === relayDataHash; - }); - // Don't refund duplicate deposits from a prior bundle, as they should have been refunded already - // if they coincided with another deposit in the same bundle. If they didn't, then its input - // amount was used to refund a pre-fill. - // We will refund any duplicate deposits the first time that we see a deposit hash in this bundle. - // If this is the first time we are seeing this deposit hash, then refund any duplicate deposits since - // a fill exists for it and these duplicate deposits can no longer be refunded for expiry. - // This means unfortunately that every duplicate deposit that is sent that - // does not accompany another deposit in the same bundle will not be refunded. This should be unlikely. - // This rule also allows us to protect honest depositors who accidentally send duplicate deposits - // in rapid succession in most cases, unless they are unlucky enough to send duplicate deposits - // in different bundle block ranges. - const duplicateDepositsInBundle = deposits!.slice(index + 1); // We are willing to refund a pre-fill multiple times for each duplicate deposit. // This is because a duplicate deposit for a pre-fill cannot get @@ -1176,10 +1148,7 @@ export class BundleDataClient { // If fill exists in memory, then the only case in which we need to create a refund is if the // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. if (fill) { - if (canRefundPrefills && !isDuplicateDepositInBundle && fill.blockNumber < destinationChainBlockRange[0]) { - duplicateDepositsInBundle.forEach((duplicateDeposit) => { - updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); - }); + if (canRefundPrefills && fill.blockNumber < destinationChainBlockRange[0]) { // If fill is in the current bundle then we can assume there is already a refund for it, so only // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. @@ -1206,7 +1175,6 @@ export class BundleDataClient { if (_depositIsExpired(deposit)) { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } else if ( - !isDuplicateDepositInBundle && canRefundPrefills && slowFillRequest.blockNumber < destinationChainBlockRange[0] && _canCreateSlowFillLeaf(deposit) @@ -1231,10 +1199,7 @@ export class BundleDataClient { const prefill = await this.findMatchingFillEvent(deposit, destinationClient); assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`); assert(this.getRelayHashFromEvent(prefill!) === relayDataHash, "Relay hashes should match."); - if (canRefundPrefills && !isDuplicateDepositInBundle) { - duplicateDepositsInBundle.forEach((duplicateDeposit) => { - updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); - }); + if (canRefundPrefills) { const verifiedFill = await verifyFillRepayment( prefill!, destinationClient.spokePool.provider, @@ -1266,7 +1231,7 @@ export class BundleDataClient { else if (fillStatus === FillStatus.RequestedSlowFill) { // Input and Output tokens must be equivalent on the deposit for this to be slow filled. // Slow fill requests for deposits from or to lite chains are considered invalid - if (!isDuplicateDepositInBundle && canRefundPrefills && _canCreateSlowFillLeaf(deposit)) { + if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) { // If deposit newly expired, then we can't create a slow fill leaf for it but we can // create a deposit refund for it. validatedBundleSlowFills.push(deposit); @@ -1426,6 +1391,11 @@ export class BundleDataClient { }); v3SlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => { const deposit = validatedBundleSlowFills[idx]; + // We should not create slow fill leaves for duplicate deposit hashes: + const relayDataHash = this.getRelayHashFromEvent(deposit); + if (validatedBundleSlowFills.slice(0, idx).some((d) => this.getRelayHashFromEvent(d) === relayDataHash)) { + return; + } updateBundleSlowFills(bundleSlowFillsV3, { ...deposit, lpFeePct }); }); v3UnexecutableSlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => { From e1d60a642adc072d36c837808c60f935f70ea166 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 12:21:48 -0500 Subject: [PATCH 080/103] wip --- src/clients/BundleDataClient/BundleDataClient.ts | 5 +---- src/clients/SpokePoolClient.ts | 4 ++-- src/utils/SpokeUtils.ts | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 9e19e3d6d..cedebeb12 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -323,7 +323,7 @@ export class BundleDataClient { continue; } const chainIndex = chainIds.indexOf(chainId); - // @todo This function does not account for pre-fill refunds as it is optimized for speed. The way to detect + // @dev This function does not account for pre-fill refunds as it is optimized for speed. The way to detect // pre-fill refunds is to load all deposits that are unmatched by fills in the spoke pool client's memory // and then query the FillStatus on-chain, but that might slow this function down too much. For now, we // will live with this expected inaccuracy as it should be small. The pre-fill would have to precede the deposit @@ -1122,9 +1122,6 @@ export class BundleDataClient { // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund. // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf // for this "pre-slow-fill-request" if this request took place in a previous bundle. - - // @todo Only start refunding pre-fills and slow fill requests after a config store version is activated. We - // should remove this check once we've advanced far beyond the version bump block. await mapAsync(bundleDepositHashes, async (depositHash) => { // We don't need to call verifyFillRepayment() here to replace the fill.relayer because this value should already // be overwritten because the deposit and fill both exist. diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 07f9f20ac..9e18fb907 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -142,7 +142,7 @@ export class SpokePoolClient extends BaseAbstractClient { * @returns A list of duplicate deposits. Does NOT include the original deposit * unless the original deposit is a duplicate. */ - public getDuplicateDeposits(deposit: DepositWithBlock): DepositWithBlock[] { + private _getDuplicateDeposits(deposit: DepositWithBlock): DepositWithBlock[] { const depositHash = this.getDepositHash(deposit); return this.duplicateDepositHashes[depositHash] ?? []; } @@ -157,7 +157,7 @@ export class SpokePoolClient extends BaseAbstractClient { public getDepositsForDestinationChainWithDuplicates(destinationChainId: number): DepositWithBlock[] { const deposits = this.getDepositsForDestinationChain(destinationChainId); const duplicateDeposits = deposits.reduce((acc, deposit) => { - const duplicates = this.getDuplicateDeposits(deposit); + const duplicates = this._getDuplicateDeposits(deposit); return acc.concat(duplicates); }, [] as DepositWithBlock[]); return sortEventsAscendingInPlace(deposits.concat(duplicateDeposits.flat())); diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index dda8cccb7..f32c6d2ee 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -412,7 +412,6 @@ export async function findFillEvent( // In production the chainId returned from the provider matches 1:1 with the actual chainId. Querying the provider // object saves an RPC query becasue the chainId is cached by StaticJsonRpcProvider instances. In hre, the SpokePool // may be configured with a different chainId than what is returned by the provider. - // @todo Sub out actual chain IDs w/ CHAIN_IDs constants const destinationChainId = Object.values(CHAIN_IDs).includes(relayData.originChainId) ? (await spokePool.provider.getNetwork()).chainId : Number(await spokePool.chainId()); From f0544d4dcb79d64dc07150738c808cd15d066e7d Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 12:23:08 -0500 Subject: [PATCH 081/103] fix --- test/SpokePoolClient.SpeedUp.ts | 4 ++-- test/SpokePoolClient.ValidateFill.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/SpokePoolClient.SpeedUp.ts b/test/SpokePoolClient.SpeedUp.ts index 3a5f9cf8a..e0fd5f977 100644 --- a/test/SpokePoolClient.SpeedUp.ts +++ b/test/SpokePoolClient.SpeedUp.ts @@ -215,13 +215,13 @@ describe("SpokePoolClient: SpeedUp", function () { // attributed to the existing deposit. for (const field of ["originChainId", "depositId", "depositor"]) { const testOriginChainId = field !== "originChainId" ? originChainId : originChainId + 1; - const testDepositId = field !== "depositId" ? depositId : depositId + 1; + const testDepositId = field !== "depositId" ? depositId : depositId.add(1); const testDepositor = field !== "depositor" ? depositor : (await ethers.getSigners())[0]; assert.isTrue(field !== "depositor" || testDepositor.address !== depositor.address); // Sanity check const signature = await getUpdatedV3DepositSignature( testDepositor, - testDepositId, + testDepositId.toNumber(), testOriginChainId, updatedOutputAmount, updatedRecipient, diff --git a/test/SpokePoolClient.ValidateFill.ts b/test/SpokePoolClient.ValidateFill.ts index 02300d6e9..0878224f8 100644 --- a/test/SpokePoolClient.ValidateFill.ts +++ b/test/SpokePoolClient.ValidateFill.ts @@ -615,7 +615,7 @@ describe("SpokePoolClient: Fill Validation", function () { // Override the first spoke pool deposit ID that the client thinks is available in the contract. await spokePoolClient1.update(); - spokePoolClient1.firstDepositIdForSpokePool = deposit.depositId + 1; + spokePoolClient1.firstDepositIdForSpokePool = deposit.depositId.add(1); expect(fill.depositId < spokePoolClient1.firstDepositIdForSpokePool).is.true; const search = await queryHistoricalDepositForFill(spokePoolClient1, fill); From 3fc8262c3db674a4bf90093fa406b2d6165e44c5 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 12:28:01 -0500 Subject: [PATCH 082/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c3b401b9..3677e5b04 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.22", + "version": "4.0.0-beta.23", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 56ea58878a89f7fb43ac0600b3391499541eed58 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 14:10:05 -0500 Subject: [PATCH 083/103] Make sure any time we queryHistoricalDepositForFill we also check the matching deposit's block number --- package.json | 2 +- src/clients/BundleDataClient/BundleDataClient.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3677e5b04..81eccce40 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.23", + "version": "4.0.0-beta.24", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index cedebeb12..a51de4f28 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -884,6 +884,7 @@ export class BundleDataClient { const destinationClient = spokePoolClients[destinationChainId]; const destinationChainBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); + const originChainBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); // Keep track of fast fills that replaced slow fills, which we'll use to create "unexecutable" slow fills // if the slow fill request was sent in a prior bundle. @@ -977,6 +978,12 @@ export class BundleDataClient { bundleInvalidFillsV3.push(fill); } else { const matchedDeposit = historicalDeposit.deposit; + // If deposit is in a following bundle, then this fill will have to refunded once that deposit + // is in the current bundle. + if (matchedDeposit.blockNumber > originChainBlockRange[1]) { + bundleInvalidFillsV3.push(fill); + return; + } v3RelayHashes[relayDataHash].deposits = [matchedDeposit]; const fillToRefund = await verifyFillRepayment( @@ -1093,6 +1100,11 @@ export class BundleDataClient { return; } const matchedDeposit: V3DepositWithBlock = historicalDeposit.deposit; + // If deposit is in a following bundle, then this slow fill request will have to be created + // once that deposit is in the current bundle. + if (matchedDeposit.blockNumber > originChainBlockRange[1]) { + return; + } // @dev Since queryHistoricalDepositForFill validates the slow fill request by checking individual // object property values against the deposit's, we // sanity check it here by comparing the full relay hashes. If there's an error here then the From b63cf9a98d9b0e4a80bddf04f6b2115ed77a308e Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 14:11:11 -0500 Subject: [PATCH 084/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81eccce40..7c2acea67 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.24", + "version": "4.0.0-beta.25", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From e5cd4189c7172309b36cf608489ef981f9be4283 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 14:14:26 -0500 Subject: [PATCH 085/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index a51de4f28..4c20dbe1b 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -939,6 +939,16 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } + + // Now that know this deposit has been filled on-chain, identify any duplicate deposits sent for this fill and refund + // them, because they would not be refunded otherwise. These deposits can no longer expire and get + // refunded as an expired deposit, and they won't trigger a pre-fill refund because the fill is + // in this bundle. Pre-fill refunds only happen when deposits are sent in this bundle and the + // fill is from a prior bundle. + const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1); + duplicateDeposits.forEach((duplicateDeposit) => { + updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); + }); } } else { throw new Error("Duplicate fill detected"); @@ -1016,6 +1026,10 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } + + // No need to check for duplicate deposits here since we would have seen them in memory if they + // had a non-infinite fill deadline, and duplicate deposits with infinite deadlines are impossible + // to send. } } } From 5b6eeeb27e9ab4e34194001f865a227d4e06525e Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 14:17:58 -0500 Subject: [PATCH 086/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7c2acea67..62b52e77a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.25", + "version": "4.0.0-beta.26", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 6def4c02577cb1d2580e0955b516bcb993b1ff37 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 15:51:13 -0500 Subject: [PATCH 087/103] Pay duplicate deposits to filler or depositor --- package.json | 2 +- .../BundleDataClient/BundleDataClient.ts | 44 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 62b52e77a..532f776cb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.26", + "version": "4.0.0-beta.27", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 4c20dbe1b..0edd3c6ab 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -927,6 +927,27 @@ export class BundleDataClient { ...fillToRefund, quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp, // ! due to assert above }); + + // Now that we know this deposit has been filled on-chain, identify any duplicate deposits + // sent for this fill and refund them to the filler, because this value would not be paid out + // otherwise. These deposits can no longer expire and get refunded as an expired deposit, + // and they won't trigger a pre-fill refund because the fill is in this bundle. + // Pre-fill refunds only happen when deposits are sent in this bundle and the + // fill is from a prior bundle. Paying out the filler keeps the behavior consistent for how + // we deal with duplicate deposits regardless if the deposit is matched with a pre-fill or + // a current bundle fill. If the fill is a slow fill, + const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1); + duplicateDeposits.forEach((duplicateDeposit) => { + // If fill is a slow fill, refund deposit to depositor, otherwise refund to filler. + if (isSlowFill(fill)) { + updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); + } else { + validatedBundleV3Fills.push({ + ...fillToRefund, + quoteTimestamp: duplicateDeposit.quoteTimestamp, + }); + } + }); } // If fill replaced a slow fill request, then mark it as one that might have created an @@ -939,16 +960,6 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } - - // Now that know this deposit has been filled on-chain, identify any duplicate deposits sent for this fill and refund - // them, because they would not be refunded otherwise. These deposits can no longer expire and get - // refunded as an expired deposit, and they won't trigger a pre-fill refund because the fill is - // in this bundle. Pre-fill refunds only happen when deposits are sent in this bundle and the - // fill is from a prior bundle. - const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1); - duplicateDeposits.forEach((duplicateDeposit) => { - updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); - }); } } else { throw new Error("Duplicate fill detected"); @@ -1017,6 +1028,10 @@ export class BundleDataClient { quoteTimestamp: matchedDeposit.quoteTimestamp, }); v3RelayHashes[relayDataHash].fill = fillToRefund; + + // No need to check for duplicate deposits here since we would have seen them in memory if they + // had a non-infinite fill deadline, and duplicate deposits with infinite deadlines are impossible + // to send. } // slow fill requests for deposits from or to lite chains are considered invalid @@ -1026,10 +1041,6 @@ export class BundleDataClient { ) { fastFillsReplacingSlowFills.push(relayDataHash); } - - // No need to check for duplicate deposits here since we would have seen them in memory if they - // had a non-infinite fill deadline, and duplicate deposits with infinite deadlines are impossible - // to send. } } } @@ -1218,7 +1229,10 @@ export class BundleDataClient { // in the same bundle. if (fillStatus === FillStatus.Filled) { // We need to find the fill event to issue a refund to the right relayer and repayment chain, - // or msg.sender if relayer address is invalid for the repayment chain. + // or msg.sender if relayer address is invalid for the repayment chain. We don't need to + // verify the fill block is before the bundle end block on the destination chain because + // we queried the fillStatus at the end block. Therefore, if the fill took place after the end block, + // then we wouldn't be in this branch of the code. const prefill = await this.findMatchingFillEvent(deposit, destinationClient); assert(isDefined(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`); assert(this.getRelayHashFromEvent(prefill!) === relayDataHash, "Relay hashes should match."); From de176c707e6d1f118f1aac8aca3ac589272a6d20 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 15:57:17 -0500 Subject: [PATCH 088/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 0edd3c6ab..a646e9691 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -918,6 +918,8 @@ export class BundleDataClient { allChainIds ); if (!isDefined(fillToRefund)) { + // We won't repay the fill but the depositor has received funds so we don't need to make a + // payment. bundleUnrepayableFillsV3.push(fill); // We don't return here yet because we still need to mark unexecutable slow fill leaves // or duplicate deposits. However, we won't issue a fast fill refund. From 283a8e95183ad7fe91b024dabcfdf1d54c96ddbb Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 16:04:52 -0500 Subject: [PATCH 089/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 532f776cb..c3e8d55aa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.27", + "version": "4.0.0", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From 49560fbb7fd334d8f6689ca69cdf7f8ba8022445 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 16:09:00 -0500 Subject: [PATCH 090/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index a646e9691..89d694c11 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -937,7 +937,7 @@ export class BundleDataClient { // Pre-fill refunds only happen when deposits are sent in this bundle and the // fill is from a prior bundle. Paying out the filler keeps the behavior consistent for how // we deal with duplicate deposits regardless if the deposit is matched with a pre-fill or - // a current bundle fill. If the fill is a slow fill, + // a current bundle fill. const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1); duplicateDeposits.forEach((duplicateDeposit) => { // If fill is a slow fill, refund deposit to depositor, otherwise refund to filler. @@ -1001,7 +1001,7 @@ export class BundleDataClient { bundleInvalidFillsV3.push(fill); } else { const matchedDeposit = historicalDeposit.deposit; - // If deposit is in a following bundle, then this fill will have to refunded once that deposit + // If deposit is in a following bundle, then this fill will have to be refunded once that deposit // is in the current bundle. if (matchedDeposit.blockNumber > originChainBlockRange[1]) { bundleInvalidFillsV3.push(fill); @@ -1031,9 +1031,8 @@ export class BundleDataClient { }); v3RelayHashes[relayDataHash].fill = fillToRefund; - // No need to check for duplicate deposits here since we would have seen them in memory if they - // had a non-infinite fill deadline, and duplicate deposits with infinite deadlines are impossible - // to send. + // No need to check for duplicate deposits here since duplicate deposits with + // infinite deadlines are impossible to send via unsafeDeposit(). } // slow fill requests for deposits from or to lite chains are considered invalid From 84f61ad7255d4c1790da48b2bec21c8960701d9a Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 16:47:13 -0500 Subject: [PATCH 091/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 89d694c11..5db9f92af 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -145,6 +145,9 @@ function updateBundleExcessSlowFills( } function updateBundleSlowFills(dict: BundleSlowFills, deposit: V3DepositWithBlock & { lpFeePct: BigNumber }): void { + if (chainIsEvm(deposit.destinationChainId) && !isValidEvmAddress(deposit.recipient)) { + return; + } const { destinationChainId, outputToken } = deposit; if (!dict?.[destinationChainId]?.[outputToken]) { assign(dict, [destinationChainId, outputToken], []); From 573d00f51ac6bd13d416629b6423b6c53254cf97 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 18:09:25 -0500 Subject: [PATCH 092/103] add comments about matching first slow fill leaf --- src/clients/BundleDataClient/BundleDataClient.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 5db9f92af..781b7a62c 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -812,6 +812,8 @@ export class BundleDataClient { const canRefundPrefills = versionAtProposalBlock >= PRE_FILL_MIN_CONFIG_STORE_VERSION || process.env.FORCE_REFUND_PREFILLS === "true"; + // Prerequisite step: Load all deposit events from the current or older bundles into the v3RelayHashes dictionary + // for convenient matching with fills. let depositCounter = 0; for (const originChainId of allChainIds) { const originClient = spokePoolClients[originChainId]; @@ -1432,7 +1434,10 @@ export class BundleDataClient { }); v3SlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => { const deposit = validatedBundleSlowFills[idx]; - // We should not create slow fill leaves for duplicate deposit hashes: + // We should not create slow fill leaves for duplicate deposit hashes and we should only create a slow + // fill leaf for the first deposit (the quote timestamp of the deposit determines the LP fee, so its + // important we pick out the correct deposit). Deposits are pushed into validatedBundleSlowFills in ascending + // order so the following slice will only match the first deposit. const relayDataHash = this.getRelayHashFromEvent(deposit); if (validatedBundleSlowFills.slice(0, idx).some((d) => this.getRelayHashFromEvent(d) === relayDataHash)) { return; From 54f28e47d3b0524eee7d6dcb821da2235d5d629d Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 31 Jan 2025 18:14:58 -0500 Subject: [PATCH 093/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 781b7a62c..79a0d9b0d 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1217,7 +1217,8 @@ export class BundleDataClient { } else if ( canRefundPrefills && slowFillRequest.blockNumber < destinationChainBlockRange[0] && - _canCreateSlowFillLeaf(deposit) + _canCreateSlowFillLeaf(deposit) && + validatedBundleSlowFills.every((d) => this.getRelayHashFromEvent(d) !== relayDataHash) ) { validatedBundleSlowFills.push(deposit); } @@ -1271,7 +1272,10 @@ export class BundleDataClient { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } // If slow fill requested, then issue a slow fill leaf for the deposit. - else if (fillStatus === FillStatus.RequestedSlowFill) { + else if ( + fillStatus === FillStatus.RequestedSlowFill && + validatedBundleSlowFills.every((d) => this.getRelayHashFromEvent(d) !== relayDataHash) + ) { // Input and Output tokens must be equivalent on the deposit for this to be slow filled. // Slow fill requests for deposits from or to lite chains are considered invalid if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) { From 87cb051462af1f0ed36d04f5f6338ccbb7bc8c41 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 1 Feb 2025 15:52:56 -0500 Subject: [PATCH 094/103] Add verifyFillRepayment check to prefill loop --- .../BundleDataClient/BundleDataClient.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index ea05b9d50..7cf86d7af 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1166,8 +1166,6 @@ export class BundleDataClient { // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf // for this "pre-slow-fill-request" if this request took place in a previous bundle. await mapAsync(bundleDepositHashes, async (depositHash) => { - // We don't need to call verifyFillRepayment() here to replace the fill.relayer because this value should already - // be overwritten because the deposit and fill both exist. const { relayDataHash, index } = decodeBundleDepositHash(depositHash); const { deposits, fill, slowFillRequest } = v3RelayHashes[relayDataHash]; if (!deposits || deposits.length === 0) { @@ -1193,10 +1191,23 @@ export class BundleDataClient { // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then // we won't consider it, following the previous treatment of fills after the bundle block range. if (!isSlowFill(fill)) { - validatedBundleV3Fills.push({ - ...fill, - quoteTimestamp: deposit.quoteTimestamp, - }); + const fillToRefund = await verifyFillRepayment( + fill, + destinationClient.spokePool.provider, + v3RelayHashes[relayDataHash].deposits![0], + allChainIds + ); + if (!isDefined(fillToRefund)) { + // We won't repay the fill but the depositor has received funds so we don't need to make a + // payment. + bundleUnrepayableFillsV3.push(fill); + } else { + v3RelayHashes[relayDataHash].fill = fillToRefund; + validatedBundleV3Fills.push({ + ...fillToRefund, + quoteTimestamp: deposit.quoteTimestamp, + }); + } } else { // Slow fills cannot result in refunds to a relayer to refund the deposit. Slow fills also // were created after the deposit was sent, so we can assume this deposit is a duplicate. From 6e6488cc10e86b43f25707058d13966a43742897 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 1 Feb 2025 16:20:37 -0500 Subject: [PATCH 095/103] Add extra verifyFillRepayment checks Try to consolidate calls to verifyFillRepayment and refactor it into some utility functions we can use as additional asserts in the BundleDataClient --- .../BundleDataClient/BundleDataClient.ts | 10 +-- .../BundleDataClient/utils/FillUtils.ts | 67 ++++++++++++------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 7cf86d7af..a7a136ca9 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -50,6 +50,7 @@ import { getRefundsFromBundle, getWidestPossibleExpectedBlockRange, isChainDisabled, + isEvmRepaymentValid, PoolRebalanceRoot, prettyPrintV3SpokePoolEvents, V3DepositWithBlock, @@ -92,10 +93,11 @@ function updateBundleFillsV3( repaymentToken: string, repaymentAddress: string ): void { - // It is impossible to refund a deposit if the repayment chain is EVM and the relayer is a non-evm address. - if (chainIsEvm(repaymentChainId) && !isValidEvmAddress(repaymentAddress)) { - return; - } + // We shouldn't pass any unrepayable fills into this function, so we perform an extra safety check. + assert( + chainIsEvm(repaymentChainId) && isEvmRepaymentValid(fill, repaymentChainId), + "validatedBundleV3Fills dictionary should only contain fills with valid repayment information" + ); if (!dict?.[repaymentChainId]?.[repaymentToken]) { assign(dict, [repaymentChainId, repaymentToken], { fills: [], diff --git a/src/clients/BundleDataClient/utils/FillUtils.ts b/src/clients/BundleDataClient/utils/FillUtils.ts index 10197ec32..f5712422f 100644 --- a/src/clients/BundleDataClient/utils/FillUtils.ts +++ b/src/clients/BundleDataClient/utils/FillUtils.ts @@ -1,6 +1,6 @@ -import _ from "lodash"; +import _, { get } from "lodash"; import { providers } from "ethers"; -import { DepositWithBlock, Fill, FillWithBlock } from "../../../interfaces"; +import { Deposit, DepositWithBlock, Fill, FillWithBlock } from "../../../interfaces"; import { getBlockRangeForChain, isSlowFill, chainIsEvm, isValidEvmAddress, isDefined } from "../../../utils"; import { HubPoolClient } from "../../HubPoolClient"; @@ -47,34 +47,51 @@ export function getRefundInformationFromFill( }; } +export function getRepaymentChainId(fill: Fill, matchedDeposit: Deposit): number { + // Lite chain deposits force repayment on origin chain. + return matchedDeposit.fromLiteChain ? fill.originChainId : fill.repaymentChainId; +} + +export function isEvmRepaymentValid( + fill: Fill, + repaymentChainId: number, + possibleRepaymentChainIds: number[] = [] +): boolean { + // Slow fills don't result in repayments so they're always valid. + if (isSlowFill(fill)) { + return true; + } + // Return undefined if the requested repayment chain ID is not in a passed in set of eligible chains. This can + // be used by the caller to narrow the chains to those that are not disabled in the config store. + if (possibleRepaymentChainIds.length > 0 && !possibleRepaymentChainIds.includes(repaymentChainId)) { + return false; + } + return chainIsEvm(repaymentChainId) && isValidEvmAddress(fill.relayer); +} + // Verify that a fill sent to an EVM chain has a 20 byte address. If the fill does not, then attempt // to repay the `msg.sender` of the relay transaction. Otherwise, return undefined. export async function verifyFillRepayment( - fill: FillWithBlock, + _fill: FillWithBlock, destinationChainProvider: providers.Provider, matchedDeposit: DepositWithBlock, - possibleRepaymentChainIds: number[] + possibleRepaymentChainIds: number[] = [] ): Promise { - // Slow fills don't result in repayments so they're always valid. - if (isSlowFill(fill)) { - return fill; - } - // Lite chain deposits force repayment on origin chain. - const repaymentChainId = matchedDeposit.fromLiteChain ? fill.originChainId : fill.repaymentChainId; - // Return undefined if the requested repayment chain ID is not recognized by the hub pool. - if (!possibleRepaymentChainIds.includes(repaymentChainId)) { - return undefined; - } - const updatedFill = _.cloneDeep(fill); + const fill = _.cloneDeep(_fill); - // If the fill requests repayment on a chain where the repayment address is not valid, attempt to find a valid - // repayment address, otherwise return undefined. + const repaymentChainId = getRepaymentChainId(fill, matchedDeposit); + const validEvmRepayment = isEvmRepaymentValid(fill, repaymentChainId, possibleRepaymentChainIds); - // Case 1: repayment chain is an EVM chain but repayment address is not a valid EVM address. - if (chainIsEvm(repaymentChainId) && !isValidEvmAddress(updatedFill.relayer)) { + // Case 1: Repayment chain is EVM and repayment address is valid EVM address. + if (validEvmRepayment) { + return fill; + } + // Case 2: Repayment chain is EVM but repayment address is not a valid EVM address. Attempt to switch repayment + // address to msg.sender of relay transaction. + else if (chainIsEvm(repaymentChainId) && !isValidEvmAddress(fill.relayer)) { // TODO: Handle case where fill was sent on non-EVM chain, in which case the following call would fail // or return something unexpected. We'd want to return undefined here. - const fillTransaction = await destinationChainProvider.getTransaction(updatedFill.transactionHash); + const fillTransaction = await destinationChainProvider.getTransaction(fill.transactionHash); const destinationRelayer = fillTransaction?.from; // Repayment chain is still an EVM chain, but the msg.sender is a bytes32 address, so the fill is invalid. if (!isDefined(destinationRelayer) || !isValidEvmAddress(destinationRelayer)) { @@ -83,9 +100,11 @@ export async function verifyFillRepayment( // Otherwise, assume the relayer to be repaid is the msg.sender. We don't need to modify the repayment chain since // the getTransaction() call would only succeed if the fill was sent on an EVM chain and therefore the msg.sender // is a valid EVM address and the repayment chain is an EVM chain. - updatedFill.relayer = destinationRelayer; + fill.relayer = destinationRelayer; + return fill; + } + // Case 3: Repayment chain is not an EVM chain, must be invalid. + else { + return undefined; } - - // Case 2: TODO repayment chain is an SVM chain and repayment address is not a valid SVM address. - return updatedFill; } From 7bcc8550ecef20d29ec98765433dce2eec9a470a Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 1 Feb 2025 16:23:09 -0500 Subject: [PATCH 096/103] lint --- package.json | 2 +- src/clients/BundleDataClient/utils/FillUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c3e8d55aa..10f8771d8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0", + "version": "4.0.0-beta.30", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/utils/FillUtils.ts b/src/clients/BundleDataClient/utils/FillUtils.ts index f5712422f..01d856233 100644 --- a/src/clients/BundleDataClient/utils/FillUtils.ts +++ b/src/clients/BundleDataClient/utils/FillUtils.ts @@ -1,4 +1,4 @@ -import _, { get } from "lodash"; +import _ from "lodash"; import { providers } from "ethers"; import { Deposit, DepositWithBlock, Fill, FillWithBlock } from "../../../interfaces"; import { getBlockRangeForChain, isSlowFill, chainIsEvm, isValidEvmAddress, isDefined } from "../../../utils"; From 1bcd9c57f19faca354e72497c2dabc8ec0d87be1 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 1 Feb 2025 23:59:46 -0500 Subject: [PATCH 097/103] Add asserts --- package.json | 2 +- src/clients/BundleDataClient/BundleDataClient.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 10f8771d8..9449d442b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.30", + "version": "4.0.0-beta.31", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index a7a136ca9..eaced21fc 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -66,10 +66,10 @@ type DataCache = Record>; // V3 dictionary helper functions function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void { - // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. - if (chainIsEvm(deposit.originChainId) && !isValidEvmAddress(deposit.depositor)) { - return; - } + assert( + chainIsEvm(deposit.originChainId) && isValidEvmAddress(deposit.depositor), + "expired depositor cannot be refunded" + ); const { originChainId, inputToken } = deposit; if (!dict?.[originChainId]?.[inputToken]) { assign(dict, [originChainId, inputToken], []); @@ -147,9 +147,10 @@ function updateBundleExcessSlowFills( } function updateBundleSlowFills(dict: BundleSlowFills, deposit: V3DepositWithBlock & { lpFeePct: BigNumber }): void { - if (chainIsEvm(deposit.destinationChainId) && !isValidEvmAddress(deposit.recipient)) { - return; - } + assert( + chainIsEvm(deposit.destinationChainId) && isValidEvmAddress(deposit.recipient), + "slow fill recipient cannot be paid" + ); const { destinationChainId, outputToken } = deposit; if (!dict?.[destinationChainId]?.[outputToken]) { assign(dict, [destinationChainId, outputToken], []); From 5092f14a6c5ed70b1c78bbf5efaff4f4d642b6af Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sun, 2 Feb 2025 21:38:34 -0500 Subject: [PATCH 098/103] Simplify code by removing out of date comments and other things --- package.json | 2 +- .../BundleDataClient/BundleDataClient.ts | 192 +++++++----------- 2 files changed, 79 insertions(+), 115 deletions(-) diff --git a/package.json b/package.json index 9449d442b..f43238d44 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.31", + "version": "4.0.0-beta.32", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index eaced21fc..cc6e4a76d 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -817,7 +817,6 @@ export class BundleDataClient { // Prerequisite step: Load all deposit events from the current or older bundles into the v3RelayHashes dictionary // for convenient matching with fills. - let depositCounter = 0; for (const originChainId of allChainIds) { const originClient = spokePoolClients[originChainId]; const originChainBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); @@ -827,13 +826,9 @@ export class BundleDataClient { continue; } originClient.getDepositsForDestinationChainWithDuplicates(destinationChainId).forEach((deposit) => { - // Only evaluate deposits that are in this bundle or in previous bundles. This means we cannot issue fill - // refunds or slow fills here for deposits that are in future bundles (i.e. "pre-fills"). Instead, we'll - // evaluate these pre-fills once the deposit is inside the "current" bundle block range. if (deposit.blockNumber > originChainBlockRange[1] || isZeroValueDeposit(deposit)) { return; } - depositCounter++; const relayDataHash = this.getRelayHashFromEvent(deposit); if (!v3RelayHashes[relayDataHash]) { @@ -846,16 +841,8 @@ export class BundleDataClient { v3RelayHashes[relayDataHash].deposits!.push(deposit); } - // Once we've saved the deposit hash into v3RelayHashes, then we can exit early here if the inputAmount - // is 0 because there can be no expired amount to refund and no unexecutable slow fill amount to return - // if this deposit did expire. Input amount can only be zero at this point if the message is non-empty, - // but the message doesn't matter for expired deposits and unexecutable slow fills. - if (deposit.inputAmount.eq(0)) { - return; - } - - // Evaluate all expired deposits after fetching fill statuses, - // since we can't know for certain whether an expired deposit was filled a long time ago. + // Account for duplicate deposits by concatenating the relayDataHash with the count of the number of times + // we have seen it so far. const newBundleDepositHash = `${relayDataHash}@${v3RelayHashes[relayDataHash].deposits!.length - 1}`; const decodedBundleDepositHash = decodeBundleDepositHash(newBundleDepositHash); assert( @@ -874,11 +861,24 @@ export class BundleDataClient { } this.logger.debug({ at: "BundleDataClient#loadData", - message: `Processed ${depositCounter} deposits in ${performance.now() - start}ms.`, + message: `Processed ${bundleDepositHashes.length + olderDepositHashes.length} deposits in ${ + performance.now() - start + }ms.`, }); start = performance.now(); - // Process fills now that we've populated relay hash dictionary with deposits: + // Process fills and maintain the following the invariants: + // - Every single fill whose type is not SlowFill in the bundle block range whose relay data matches + // with a deposit in the same or an older range produces a refund to the filler, + // unless the specified filler address cannot be repaid on the repayment chain. + // - Fills can match with duplicate deposits, so for every matched fill whose type is not SlowFill + // in the bundle block range, produce a refund to the filler for each matched deposit. + // - For every SlowFill in the block range that matches with multiple deposits, produce a refund to the depositor + // for every deposit except except the first. + + // Assumptions about fills: + // - Duplicate fills for the same relay data hash are impossible to send. + // - Fills can only be sent before the deposit's fillDeadline. const validatedBundleV3Fills: (V3FillWithBlock & { quoteTimestamp: number })[] = []; const validatedBundleSlowFills: V3DepositWithBlock[] = []; const validatedBundleUnexecutableSlowFills: V3DepositWithBlock[] = []; @@ -894,8 +894,6 @@ export class BundleDataClient { const destinationChainBlockRange = getBlockRangeForChain(blockRangesForChains, destinationChainId, chainIds); const originChainBlockRange = getBlockRangeForChain(blockRangesForChains, originChainId, chainIds); - // Keep track of fast fills that replaced slow fills, which we'll use to create "unexecutable" slow fills - // if the slow fill request was sent in a prior bundle. const fastFillsReplacingSlowFills: string[] = []; await forEachAsync( destinationClient @@ -915,8 +913,6 @@ export class BundleDataClient { isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0, "Deposit should exist in relay hash dictionary." ); - // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit, - // so this fill can no longer be filled on-chain. v3RelayHashes[relayDataHash].fill = fill; if (fill.blockNumber >= destinationChainBlockRange[0]) { const fillToRefund = await verifyFillRepayment( @@ -926,8 +922,6 @@ export class BundleDataClient { allChainIds ); if (!isDefined(fillToRefund)) { - // We won't repay the fill but the depositor has received funds so we don't need to make a - // payment. bundleUnrepayableFillsV3.push(fill); // We don't return here yet because we still need to mark unexecutable slow fill leaves // or duplicate deposits. However, we won't issue a fast fill refund. @@ -935,7 +929,7 @@ export class BundleDataClient { v3RelayHashes[relayDataHash].fill = fillToRefund; validatedBundleV3Fills.push({ ...fillToRefund, - quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp, // ! due to assert above + quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp, }); // Now that we know this deposit has been filled on-chain, identify any duplicate deposits @@ -948,7 +942,6 @@ export class BundleDataClient { // a current bundle fill. const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1); duplicateDeposits.forEach((duplicateDeposit) => { - // If fill is a slow fill, refund deposit to depositor, otherwise refund to filler. if (isSlowFill(fill)) { updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit); } else { @@ -963,7 +956,6 @@ export class BundleDataClient { // If fill replaced a slow fill request, then mark it as one that might have created an // unexecutable slow fill. We can't know for sure until we check the slow fill request // events. - // slow fill requests for deposits from or to lite chains are considered invalid if ( fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill && _canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposits![0]) @@ -985,7 +977,7 @@ export class BundleDataClient { slowFillRequest: undefined, }; - // TODO: We might be able to remove the following historical query once we deprecate the deposit() + // TODO: We can remove the following historical query once we deprecate the deposit() // function since there won't be any old, unexpired deposits anymore assuming the spoke pool client // lookbacks have been validated, which they should be before we run this function. @@ -1043,7 +1035,6 @@ export class BundleDataClient { // infinite deadlines are impossible to send via unsafeDeposit(). } - // slow fill requests for deposits from or to lite chains are considered invalid if ( fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill && _canCreateSlowFillLeaf(matchedDeposit) @@ -1055,8 +1046,14 @@ export class BundleDataClient { } ); - // Process slow fill requests. One invariant we need to maintain is that we cannot create slow fill requests - // for deposits that would expire in this bundle. + // Process slow fill requests and produce slow fill leaves while maintaining the following the invariants: + // - Slow fill leaves cannot be produced for deposits that have expired in this bundle. + // - Slow fill leaves cannot be produced for deposits that have been filled. + + // Assumptions about fills: + // - Duplicate slow fill requests for the same relay data hash are impossible to send. + // - Slow fill requests can only be sent before the deposit's fillDeadline. + // - Slow fill requests for a deposit that has been filled. await forEachAsync( destinationClient .getSlowFillRequestsForOriginChain(originChainId) @@ -1069,34 +1066,24 @@ export class BundleDataClient { if (v3RelayHashes[relayDataHash]) { if (!v3RelayHashes[relayDataHash].slowFillRequest) { - // At this point, the v3RelayHashes entry already existed meaning that there is either a matching - // fill or deposit. v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest; if (v3RelayHashes[relayDataHash].fill) { - // If there is a fill matching the relay hash, then this slow fill request can't be used - // to create a slow fill for a filled deposit. This takes advantage of the fact that - // slow fill requests must precede fills, so if there is a matching fill for this request's - // relay data, then this slow fill will be unexecutable. + // Exiting here assumes that slow fill requests must precede fills, so if there was a fill + // following this slow fill request, then we would have already seen it. We don't need to check + // for a fill older than this slow fill request. return; } assert( isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0, "Deposit should exist in relay hash dictionary." ); - // The ! is safe here because we've already checked that the deposit exists in the relay hash dictionary. const matchedDeposit = v3RelayHashes[relayDataHash].deposits![0]; - // If there is no fill matching the relay hash, then this might be a valid slow fill request - // that we should produce a slow fill leaf for. Check if the slow fill request is in the - // destination chain block range. if ( slowFillRequest.blockNumber >= destinationChainBlockRange[0] && _canCreateSlowFillLeaf(matchedDeposit) && - // Deposit must not have expired in this bundle. !_depositIsExpired(matchedDeposit) ) { - // At this point, the v3RelayHashes entry already existed meaning that there is a matching deposit, - // so this slow fill request relay data is correct. validatedBundleSlowFills.push(matchedDeposit); } } else { @@ -1112,7 +1099,7 @@ export class BundleDataClient { slowFillRequest: slowFillRequest, }; - // TODO: We might be able to remove the following historical query once we deprecate the deposit() + // TODO: We can remove the following historical query once we deprecate the deposit() // function since there won't be any old, unexpired deposits anymore assuming the spoke pool client // lookbacks have been validated, which they should be before we run this function. @@ -1130,7 +1117,6 @@ export class BundleDataClient { ) { const historicalDeposit = await queryHistoricalDepositForFill(originClient, slowFillRequest); if (!historicalDeposit.found) { - // TODO: Invalid slow fill request. Maybe worth logging. return; } const matchedDeposit: V3DepositWithBlock = historicalDeposit.deposit; @@ -1149,11 +1135,7 @@ export class BundleDataClient { ); v3RelayHashes[relayDataHash].deposits = [matchedDeposit]; - if ( - !_canCreateSlowFillLeaf(matchedDeposit) || - // Deposit must not have expired in this bundle. - _depositIsExpired(matchedDeposit) - ) { + if (!_canCreateSlowFillLeaf(matchedDeposit) || _depositIsExpired(matchedDeposit)) { return; } validatedBundleSlowFills.push(matchedDeposit); @@ -1161,13 +1143,27 @@ export class BundleDataClient { } ); - // Deposits can be submitted an arbitrary amount of time after matching fills and slow fill requests. - // Therefore, let's go through each deposit in this bundle again and check a few things in order: - // - Has the deposit been filled ? If so, then we need to issue a relayer refund for - // this "pre-fill" if the fill took place in a previous bundle. - // - Or, has the deposit expired in this bundle? If so, then we need to issue an expiry refund. - // - And finally, has the deposit been slow filled? If so, then we need to issue a slow fill leaf - // for this "pre-slow-fill-request" if this request took place in a previous bundle. + // Process deposits and maintain the following invariants: + // - Deposits matching fills that are not type SlowFill from previous bundle block ranges should produce + // refunds for those fills. + // - Deposits matching fills that are type SlowFill from previous bundle block ranges should be refunded to the + // depositor. + // - All deposits expiring in this bundle, even those sent in prior bundle block ranges, should be refunded + // to the depositor. + // - An expired deposit cannot be refunded if the deposit was filled. + // - If a deposit from a prior bundle expired in this bundle, had a slow fill request created for it + // in a prior bundle, and has not been filled yet, then an unexecutable slow fill leaf has been created + // and needs to be refunded to the HubPool. + // - Deposits matching slow fill requests from previous bundle block ranges should produce slow fills + // if the deposit has not been filled. + + // Assumptions: + // - If the deposit has a matching fill or slow fill request in the bundle then we have already stored + // it in the relay hashes dictionary. + // - We've created refunds for all fills in this bundle matching a deposit. + // - We've created slow fill leaves for all slow fill requests in this bundle matching an unfilled deposit. + // - Deposits for the same relay data hash can be sent an arbitrary amount of times. + // - Deposits can be sent an arbitrary amount of time after a fill has been sent for the matching relay data. await mapAsync(bundleDepositHashes, async (depositHash) => { const { relayDataHash, index } = decodeBundleDepositHash(depositHash); const { deposits, fill, slowFillRequest } = v3RelayHashes[relayDataHash]; @@ -1180,40 +1176,25 @@ export class BundleDataClient { return; } - // We are willing to refund a pre-fill multiple times for each duplicate deposit. - // This is because a duplicate deposit for a pre-fill cannot get - // refunded to the depositor anymore because its fill status on-chain has changed to Filled. Therefore - // any duplicate deposits result in a net loss of funds for the depositor and effectively pay out - // the pre-filler. - - // If fill exists in memory, then the only case in which we need to create a refund is if the - // the fill occurred in a previous bundle. There are no expiry refunds for filled deposits. + // If fill is in the current bundle then we can assume there is already a refund for it, so only + // include this pre fill if the fill is in an older bundle. if (fill) { if (canRefundPrefills && fill.blockNumber < destinationChainBlockRange[0]) { - // If fill is in the current bundle then we can assume there is already a refund for it, so only - // include this pre fill if the fill is in an older bundle. If fill is after this current bundle, then - // we won't consider it, following the previous treatment of fills after the bundle block range. - if (!isSlowFill(fill)) { - const fillToRefund = await verifyFillRepayment( - fill, - destinationClient.spokePool.provider, - v3RelayHashes[relayDataHash].deposits![0], - allChainIds - ); - if (!isDefined(fillToRefund)) { - // We won't repay the fill but the depositor has received funds so we don't need to make a - // payment. - bundleUnrepayableFillsV3.push(fill); - } else { - v3RelayHashes[relayDataHash].fill = fillToRefund; - validatedBundleV3Fills.push({ - ...fillToRefund, - quoteTimestamp: deposit.quoteTimestamp, - }); - } + const fillToRefund = await verifyFillRepayment( + fill, + destinationClient.spokePool.provider, + v3RelayHashes[relayDataHash].deposits![0], + allChainIds + ); + if (!isDefined(fillToRefund)) { + bundleUnrepayableFillsV3.push(fill); + } else if (!isSlowFill(fill)) { + v3RelayHashes[relayDataHash].fill = fillToRefund; + validatedBundleV3Fills.push({ + ...fillToRefund, + quoteTimestamp: deposit.quoteTimestamp, + }); } else { - // Slow fills cannot result in refunds to a relayer to refund the deposit. Slow fills also - // were created after the deposit was sent, so we can assume this deposit is a duplicate. updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } } @@ -1221,10 +1202,10 @@ export class BundleDataClient { } // If a slow fill request exists in memory, then we know the deposit has not been filled because fills - // must follow slow fill requests and we would have seen the fill already if it existed. Therefore, - // we can conclude that either the deposit has expired and we need to create a deposit expiry refund, or - // we need to create a slow fill leaf for the deposit. The latter should only happen if the slow fill request - // took place in a prior bundle otherwise we would have already created a slow fill leaf for it. + // must follow slow fill requests and we would have seen the fill already if it existed., + // We can conclude that either the deposit has expired or we need to create a slow fill leaf for the + // deposit because it has not been filled. Slow fill leaves were already created for requests sent + // in the current bundle so only create new slow fill leaves for prior bundle deposits. if (slowFillRequest) { if (_depositIsExpired(deposit)) { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); @@ -1245,13 +1226,8 @@ export class BundleDataClient { // because the spoke pool client lookback would have returned this entire bundle of events and stored // them into the relay hash dictionary. const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]); - - // If deposit was filled, then we need to issue a refund for the fill and also any duplicate deposits - // in the same bundle. if (fillStatus === FillStatus.Filled) { - // We need to find the fill event to issue a refund to the right relayer and repayment chain, - // or msg.sender if relayer address is invalid for the repayment chain. We don't need to - // verify the fill block is before the bundle end block on the destination chain because + // We don't need to verify the fill block is before the bundle end block on the destination chain because // we queried the fillStatus at the end block. Therefore, if the fill took place after the end block, // then we wouldn't be in this branch of the code. const prefill = await this.findMatchingFillEvent(deposit, destinationClient); @@ -1272,29 +1248,17 @@ export class BundleDataClient { quoteTimestamp: deposit.quoteTimestamp, }); } else { - // Slow fills cannot result in refunds to a relayer to refund the deposit. Slow fills also - // were created after the deposit was sent, so we can assume this deposit is a duplicate. updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); } } - } - // If deposit is not filled and its newly expired, we can create a deposit refund for it. - // We don't check that fillDeadline >= bundleBlockTimestamps[destinationChainId][0] because - // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 - // for example. Those should be included in this bundle of refunded deposits. - else if (_depositIsExpired(deposit)) { + } else if (_depositIsExpired(deposit)) { updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit); - } - // If slow fill requested, then issue a slow fill leaf for the deposit. - else if ( + } else if ( fillStatus === FillStatus.RequestedSlowFill && + // Don't create duplicate slow fill requests for the same deposit. validatedBundleSlowFills.every((d) => this.getRelayHashFromEvent(d) !== relayDataHash) ) { - // Input and Output tokens must be equivalent on the deposit for this to be slow filled. - // Slow fill requests for deposits from or to lite chains are considered invalid if (canRefundPrefills && _canCreateSlowFillLeaf(deposit)) { - // If deposit newly expired, then we can't create a slow fill leaf for it but we can - // create a deposit refund for it. validatedBundleSlowFills.push(deposit); } } @@ -1351,7 +1315,7 @@ export class BundleDataClient { // Only look for deposits that were mined before this bundle and that are newly expired. // If the fill deadline is lower than the bundle start block on the destination chain, then - // we should assume it was marked "newly expired" and refunded in a previous bundle. + // we should assume it was refunded in a previous bundle. if ( // If there is a valid fill that we saw matching this deposit, then it does not need a refund. !fill && From a2251a0548d758f10dee11fe522d91ba822e8244 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sun, 2 Feb 2025 21:42:17 -0500 Subject: [PATCH 099/103] Add assert --- package.json | 2 +- src/clients/BundleDataClient/BundleDataClient.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f43238d44..40f0d9022 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.32", + "version": "4.0.0-beta.33", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index cc6e4a76d..3e8f223e3 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1424,6 +1424,7 @@ export class BundleDataClient { if (validatedBundleSlowFills.slice(0, idx).some((d) => this.getRelayHashFromEvent(d) === relayDataHash)) { return; } + assert(!_depositIsExpired(deposit), "Cannot create slow fill leaf for expired deposit."); updateBundleSlowFills(bundleSlowFillsV3, { ...deposit, lpFeePct }); }); v3UnexecutableSlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => { From cc9ebfcfc70b3bc221ee3c0b59c62f17aa1372a2 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 3 Feb 2025 11:58:45 -0500 Subject: [PATCH 100/103] Update BundleDataClient.ts --- .../BundleDataClient/BundleDataClient.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 3e8f223e3..9f47fc820 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -697,6 +697,7 @@ export class BundleDataClient { const bundleFillsV3: BundleFillsV3 = {}; // Fills to refund in bundle block range. const bundleInvalidFillsV3: V3FillWithBlock[] = []; // Fills that are not valid in this bundle. const bundleUnrepayableFillsV3: V3FillWithBlock[] = []; // Fills that are not repayable in this bundle. + const bundleInvalidSlowFillRequests: SlowFillRequestWithBlock[] = []; // Slow fill requests that are not valid in this bundle. const bundleSlowFillsV3: BundleSlowFills = {}; // Deposits that we need to send slow fills // for in this bundle. const expiredDepositsToRefundV3: ExpiredDepositsToRefundV3 = {}; @@ -1111,18 +1112,21 @@ export class BundleDataClient { // want to perform a binary search lookup for it because the deposit ID is "unsafe" and cannot be // found using such a method) because infinite fill deadlines cannot be produced from the unsafeDepositV3() // function. - if ( - INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline) && - slowFillRequest.blockNumber >= destinationChainBlockRange[0] - ) { + if (slowFillRequest.blockNumber >= destinationChainBlockRange[0]) { + if (!INFINITE_FILL_DEADLINE.eq(slowFillRequest.fillDeadline)) { + bundleInvalidSlowFillRequests.push(slowFillRequest); + return; + } const historicalDeposit = await queryHistoricalDepositForFill(originClient, slowFillRequest); if (!historicalDeposit.found) { + bundleInvalidSlowFillRequests.push(slowFillRequest); return; } const matchedDeposit: V3DepositWithBlock = historicalDeposit.deposit; // If deposit is in a following bundle, then this slow fill request will have to be created // once that deposit is in the current bundle. if (matchedDeposit.blockNumber > originChainBlockRange[1]) { + bundleInvalidSlowFillRequests.push(slowFillRequest); return; } // @dev Since queryHistoricalDepositForFill validates the slow fill request by checking individual @@ -1443,7 +1447,7 @@ export class BundleDataClient { if (bundleInvalidFillsV3.length > 0) { this.logger.debug({ at: "BundleDataClient#loadData", - message: "Finished loading V3 spoke pool data and found some invalid V3 fills in range", + message: "Finished loading V3 spoke pool data and found some invalid fills in range", blockRangesForChains, bundleInvalidFillsV3, }); @@ -1452,12 +1456,21 @@ export class BundleDataClient { if (bundleUnrepayableFillsV3.length > 0) { this.logger.debug({ at: "BundleDataClient#loadData", - message: "Finished loading V3 spoke pool data and found some unrepayable V3 fills in range", + message: "Finished loading V3 spoke pool data and found some unrepayable fills in range", blockRangesForChains, bundleUnrepayableFillsV3, }); } + if (bundleInvalidSlowFillRequests.length > 0) { + this.logger.debug({ + at: "BundleDataClient#loadData", + message: "Finished loading V3 spoke pool data and found some invalid slow fill requests in range", + blockRangesForChains, + bundleInvalidSlowFillRequests, + }); + } + this.logger.debug({ at: "BundleDataClient#loadDataFromScratch", message: `Computed bundle data in ${Math.round(performance.now() - start) / 1000}s.`, From 5c32bd80eafa3faf05788d2718a09766dddbc7a9 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 3 Feb 2025 11:59:13 -0500 Subject: [PATCH 101/103] Update BundleDataClient.ts --- src/clients/BundleDataClient/BundleDataClient.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 9f47fc820..ef325fcb4 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -66,10 +66,10 @@ type DataCache = Record>; // V3 dictionary helper functions function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void { - assert( - chainIsEvm(deposit.originChainId) && isValidEvmAddress(deposit.depositor), - "expired depositor cannot be refunded" - ); + // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. + if (chainIsEvm(deposit.originChainId) && !isValidEvmAddress(deposit.depositor)) { + return; + } const { originChainId, inputToken } = deposit; if (!dict?.[originChainId]?.[inputToken]) { assign(dict, [originChainId, inputToken], []); @@ -147,10 +147,9 @@ function updateBundleExcessSlowFills( } function updateBundleSlowFills(dict: BundleSlowFills, deposit: V3DepositWithBlock & { lpFeePct: BigNumber }): void { - assert( - chainIsEvm(deposit.destinationChainId) && isValidEvmAddress(deposit.recipient), - "slow fill recipient cannot be paid" - ); + if (chainIsEvm(deposit.destinationChainId) && !isValidEvmAddress(deposit.recipient)) { + return; + } const { destinationChainId, outputToken } = deposit; if (!dict?.[destinationChainId]?.[outputToken]) { assign(dict, [destinationChainId, outputToken], []); From 7eb7ede06dc8e00d7311faf598ca35eeff63ae59 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 3 Feb 2025 11:59:40 -0500 Subject: [PATCH 102/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40f0d9022..9fbc8c70d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.33", + "version": "4.0.0-beta.34", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From d614976d2f30b33c52cfd8fa702b772e537be02c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 3 Feb 2025 12:48:16 -0500 Subject: [PATCH 103/103] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9fbc8c70d..c3e8d55aa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.0.0-beta.34", + "version": "4.0.0", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [