diff --git a/package.json b/package.json index 38970f33..586c5499 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk-v2", "author": "UMA Team", - "version": "0.21.0", + "version": "0.21.1", "license": "AGPL-3.0", "homepage": "https://docs.across.to/v/developer-docs/developers/across-sdk", "files": [ diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index fcd8c2ce..e49439a2 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -43,7 +43,9 @@ import { SlowFillRequestWithBlock, SpeedUp, TokensBridged, + V2Deposit, V2DepositWithBlock, + V2Fill, V2FillWithBlock, V2SpeedUp, V3DepositWithBlock, @@ -380,6 +382,17 @@ export class SpokePoolClient extends BaseAbstractClient { return this.slowFillRequests[hash]; } + /** + * Retrieves a list of slow fill requests for deposits from a specific origin chain ID. + * @param originChainId The origin chain ID. + * @returns A list of slow fill requests. + */ + public getSlowFillRequestsForOriginChain(originChainId: number): SlowFillRequestWithBlock[] { + return Object.values(this.slowFillRequests).filter( + (e: SlowFillRequestWithBlock) => e.originChainId === originChainId + ); + } + /** * Find a corresponding deposit for a given fill. * @param fill The fill to find a corresponding deposit for. @@ -526,7 +539,11 @@ export class SpokePoolClient extends BaseAbstractClient { * @param toBlock The block number to search up to. * @returns A list of fills that match the given fill and deposit. */ - public async queryHistoricalMatchingFills(fill: Fill, deposit: Deposit, toBlock: number): Promise { + public async queryHistoricalMatchingFills( + fill: V2Fill, + deposit: V2Deposit, + toBlock: number + ): Promise { const searchConfig = { fromBlock: this.deploymentBlock, toBlock, @@ -543,7 +560,10 @@ export class SpokePoolClient extends BaseAbstractClient { * @param searchConfig The search configuration. * @returns A Promise that resolves to a list of fills that match the given fill. */ - public async queryFillsInBlockRange(matchingFill: Fill, searchConfig: EventSearchConfig): Promise { + public async queryFillsInBlockRange( + matchingFill: V2Fill, + searchConfig: EventSearchConfig + ): Promise { // Filtering on the fill's depositor address, the only indexed deposit field in the FilledRelay event, // should speed up this search a bit. // TODO: Once depositId is indexed in FilledRelay event, filter on that as well. @@ -568,7 +588,7 @@ export class SpokePoolClient extends BaseAbstractClient { ), searchConfig ); - const fills = query.map((event) => spreadEventWithBlockNumber(event) as FillWithBlock); + const fills = query.map((event) => spreadEventWithBlockNumber(event) as V2FillWithBlock); return sortEventsAscending(fills.filter((_fill) => filledSameDeposit(_fill, matchingFill))); } @@ -769,6 +789,9 @@ export class SpokePoolClient extends BaseAbstractClient { deposit.realizedLpFeePct = realizedLpFeePct; deposit.quoteBlockNumber = quoteBlockNumber; + if (this.depositHashes[this.getDepositHash(deposit)] !== undefined) { + continue; + } assign(this.depositHashes, [this.getDepositHash(deposit)], deposit); if (deposit.depositId < this.earliestDepositIdQueried) { @@ -815,6 +838,9 @@ export class SpokePoolClient extends BaseAbstractClient { destinationChainId: this.chainId, }; const relayDataHash = getRelayDataHash(slowFillRequest, this.chainId); + if (this.slowFillRequests[relayDataHash] !== undefined) { + continue; + } this.slowFillRequests[relayDataHash] = slowFillRequest; } } @@ -863,9 +889,6 @@ export class SpokePoolClient extends BaseAbstractClient { } } - // Exact sequencing of relayer refund executions doesn't seem to be important. There are very few consumers of - // these objects, and they are typically used to search for a specific rootBundleId & leafId pair. Therefore, - // relayerRefundExecutions don't need exact sequencing and parsing of v2/v3 events can occur without sorting. if (eventsToQuery.includes("ExecutedRelayerRefundRoot")) { const refundEvents = queryResults[eventsToQuery.indexOf("ExecutedRelayerRefundRoot")]; for (const event of refundEvents) { @@ -1018,7 +1041,7 @@ export class SpokePoolClient extends BaseAbstractClient { * @returns The deposit if found. * @note This method is used to find deposits that are outside of the search range of this client. */ - async findDeposit(depositId: number, destinationChainId: number, depositor: string): Promise { + async findDeposit(depositId: number, destinationChainId: number, depositor: string): Promise { // Binary search for block. This way we can get the blocks before and after the deposit with // deposit ID = fill.depositId and use those blocks to optimize the search for that deposit. // Stop searches after a maximum # of searches to limit number of eth_call requests. Make an @@ -1066,11 +1089,11 @@ export class SpokePoolClient extends BaseAbstractClient { ` between ${srcChain} blocks [${searchBounds.low}, ${searchBounds.high}]` ); } - const partialDeposit = spreadEventWithBlockNumber(event) as DepositWithBlock; + const partialDeposit = spreadEventWithBlockNumber(event) as V2DepositWithBlock; const { realizedLpFeePct, quoteBlock: quoteBlockNumber } = (await this.batchComputeRealizedLpFeePct([event]))[0]; // Append the realizedLpFeePct. // Append destination token and realized lp fee to deposit. - const deposit: DepositWithBlock = { + const deposit: V2DepositWithBlock = { ...partialDeposit, realizedLpFeePct, destinationToken: this.getDestinationTokenForDeposit(partialDeposit), @@ -1086,4 +1109,80 @@ export class SpokePoolClient extends BaseAbstractClient { return deposit; } + + async findDepositV3(depositId: number, destinationChainId: number, depositor: string): Promise { + // Binary search for event search bounds. This way we can get the blocks before and after the deposit with + // deposit ID = fill.depositId and use those blocks to optimize the search for that deposit. + // Stop searches after a maximum # of searches to limit number of eth_call requests. Make an + // eth_getLogs call on the remaining block range (i.e. the [low, high] remaining from the binary + // search) to find the target deposit ID. + // + // @dev Limiting between 5-10 searches empirically performs best when there are ~300,000 deposits + // for a spoke pool and we're looking for a deposit <5 days older than HEAD. + const searchBounds = await this._getBlockRangeForDepositId( + depositId, + this.deploymentBlock, + this.latestBlockSearched, + 7 + ); + + const tStart = Date.now(); + const query = await paginatedEventQuery( + this.spokePool, + this.spokePool.filters.V3FundsDeposited( + null, + null, + null, + null, + destinationChainId, + depositId, + null, + null, + null, + depositor, + null, + null, + null + ), + { + fromBlock: searchBounds.low, + toBlock: searchBounds.high, + maxBlockLookBack: this.eventSearchConfig.maxBlockLookBack, + } + ); + const tStop = Date.now(); + + const event = (query as V3FundsDepositedEvent[]).find((deposit) => deposit.args.depositId === depositId); + if (event === undefined) { + const srcChain = getNetworkName(this.chainId); + const dstChain = getNetworkName(destinationChainId); + throw new Error( + `Could not find deposit ${depositId} for ${dstChain} fill` + + ` between ${srcChain} blocks [${searchBounds.low}, ${searchBounds.high}]` + ); + } + const partialDeposit = spreadEventWithBlockNumber(event) as V3DepositWithBlock; + const { realizedLpFeePct, quoteBlock: quoteBlockNumber } = (await this.batchComputeRealizedLpFeePct([event]))[0]; // Append the realizedLpFeePct. + + // Append destination token and realized lp fee to deposit. + const deposit: V3DepositWithBlock = { + ...partialDeposit, + originChainId: this.chainId, + realizedLpFeePct, + quoteBlockNumber, + outputToken: + partialDeposit.outputToken === ZERO_ADDRESS + ? this.getDestinationTokenForDeposit(partialDeposit) + : partialDeposit.outputToken, + }; + + this.logger.debug({ + at: "SpokePoolClient#findDepositV3", + message: "Located V3 deposit outside of SpokePoolClient's search range", + deposit, + elapsedMs: tStop - tStart, + }); + + return deposit; + } } diff --git a/src/interfaces/SpokePool.ts b/src/interfaces/SpokePool.ts index 198a1676..d78aa722 100644 --- a/src/interfaces/SpokePool.ts +++ b/src/interfaces/SpokePool.ts @@ -166,7 +166,7 @@ export interface RelayerRefundExecution extends RelayerRefundLeaf { export interface RelayerRefundExecutionWithBlock extends RelayerRefundExecution, SortableEvent {} export interface UnfilledDeposit { - deposit: Deposit; + deposit: V2Deposit; unfilledAmount: BigNumber; hasFirstPartialFill?: boolean; relayerBalancingFee?: BigNumber; @@ -182,7 +182,7 @@ export interface Refund { export type FillsToRefund = { [repaymentChainId: number]: { [l2TokenAddress: string]: { - fills: Fill[]; + fills: V2Fill[]; refunds?: Refund; totalRefundAmount: BigNumber; realizedLpFees: BigNumber; diff --git a/src/utils/CachingUtils.ts b/src/utils/CachingUtils.ts index c1be13f2..d2a78efb 100644 --- a/src/utils/CachingUtils.ts +++ b/src/utils/CachingUtils.ts @@ -1,9 +1,11 @@ import { DEFAULT_CACHING_SAFE_LAG, DEFAULT_CACHING_TTL } from "../constants"; -import { CachingMechanismInterface, Deposit, Fill } from "../interfaces"; +import { CachingMechanismInterface, Deposit, Fill, SlowFillRequest } from "../interfaces"; import { assert } from "./LogUtils"; import { composeRevivers, objectWithBigNumberReviver } from "./ReviverUtils"; +import { getV3RelayHashFromEvent } from "./SpokeUtils"; import { getCurrentTime } from "./TimeUtils"; import { isDefined } from "./TypeGuards"; +import { isV2Deposit, isV2Fill } from "./V3Utils"; export function shouldCache(eventTimestamp: number, latestTime: number, cachingMaxAge: number): boolean { assert(eventTimestamp.toString().length === 10, "eventTimestamp must be in seconds"); @@ -44,10 +46,16 @@ export async function setDepositInCache( } /** - * Resolves the key for caching either a deposit or a fill. - * @param depositOrFill Either a deposit or a fill. In either case, the depositId and originChainId are used to generate the key. - * @returns The key for caching the deposit or fill. + * Resolves the key for caching a bridge event. + * @param bridgeEvent The depositId, and originChainId are used to generate the key for v2, and the + * full V3 relay hash is used for v3 events.. + * @returns The key for caching the event. */ -export function getDepositKey(depositOrFill: Deposit | Fill): string { - return `deposit_${depositOrFill.originChainId}_${depositOrFill.depositId}`; +export function getDepositKey(bridgeEvent: Deposit | Fill | SlowFillRequest): string { + if (isV2Deposit(bridgeEvent as Deposit) || isV2Fill(bridgeEvent)) { + return `deposit_${bridgeEvent.originChainId}_${bridgeEvent.depositId}`; + } else { + const relayHash = getV3RelayHashFromEvent(bridgeEvent); + return `deposit_${bridgeEvent.originChainId}_${bridgeEvent.depositId}_${relayHash}`; + } } diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 92b5f425..1f3da958 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -1,14 +1,14 @@ import assert from "assert"; import { SpokePoolClient } from "../clients"; import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE } from "../constants"; -import { CachingMechanismInterface, Deposit, DepositWithBlock, Fill } from "../interfaces"; +import { CachingMechanismInterface, Deposit, DepositWithBlock, Fill, SlowFillRequest } from "../interfaces"; import { getNetworkName } from "./NetworkUtils"; import { getDepositInCache, getDepositKey, setDepositInCache } from "./CachingUtils"; import { validateFillForDeposit } from "./FlowUtils"; import { getCurrentTime } from "./TimeUtils"; import { isDefined } from "./TypeGuards"; import { isDepositFormedCorrectly } from "./ValidatorUtils"; -import { isV2Deposit, isV3Deposit } from "./V3Utils"; +import { isV2Deposit, isV2Fill, isV3Deposit } from "./V3Utils"; // Load a deposit for a fill if the fill's deposit ID is outside this client's search range. // This can be used by the Dataworker to determine whether to give a relayer a refund for a fill @@ -38,7 +38,7 @@ export type DepositSearchResult = */ export async function queryHistoricalDepositForFill( spokePoolClient: SpokePoolClient, - fill: Fill, + fill: Fill | SlowFillRequest, cache?: CachingMechanismInterface ): Promise { if (fill.originChainId !== spokePoolClient.chainId) { @@ -98,7 +98,11 @@ export async function queryHistoricalDepositForFill( if (isDefined(cachedDeposit)) { deposit = cachedDeposit as DepositWithBlock; } else { - deposit = await spokePoolClient.findDeposit(fill.depositId, fill.destinationChainId, fill.depositor); + if (isV2Fill(fill)) { + deposit = await spokePoolClient.findDeposit(fill.depositId, fill.destinationChainId, fill.depositor); + } else { + deposit = await spokePoolClient.findDepositV3(fill.depositId, fill.destinationChainId, fill.depositor); + } if (cache) { await setDepositInCache(deposit, getCurrentTime(), cache, DEFAULT_CACHING_TTL); } diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 6aa2239d..74663305 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,7 +1,7 @@ import assert from "assert"; import { Contract, utils as ethersUtils } from "ethers"; import { CHAIN_IDs } from "../constants"; -import { FillStatus, RelayData, V2RelayData, V3RelayData } from "../interfaces"; +import { FillStatus, RelayData, SlowFillRequest, V2RelayData, V3Deposit, V3Fill, V3RelayData } from "../interfaces"; import { SpokePoolClient } from "../clients"; import { isDefined } from "./TypeGuards"; import { isV2RelayData } from "./V3Utils"; @@ -240,6 +240,9 @@ export function getV3RelayHash(relayData: V3RelayData, destinationChainId: numbe ); } +export function getV3RelayHashFromEvent(e: V3Deposit | V3Fill | SlowFillRequest): string { + return getV3RelayHash(e, e.destinationChainId); +} /** * Find the amount filled for a deposit at a particular block. * @param spokePool SpokePool contract instance. diff --git a/src/utils/V3Utils.ts b/src/utils/V3Utils.ts index ef2e9fa5..6f0af986 100644 --- a/src/utils/V3Utils.ts +++ b/src/utils/V3Utils.ts @@ -13,6 +13,7 @@ import { V3SpeedUp, } from "../interfaces"; import { BN } from "./BigNumberUtils"; +import { fixedPointAdjustment } from "./common"; // Lowest ConfigStore version where the V3 model is in effect. The version update to the following value should take // place atomically with the SpokePool upgrade to V3 so that the dataworker knows what kind of MerkleLeaves to propose @@ -168,3 +169,12 @@ export function getSlowFillLeafChainId< >(leaf: T | U): number { return unsafeIsType(leaf, "chainId") ? leaf.chainId : leaf.relayData.destinationChainId; } + +export function getSlowFillLeafLpFeePct< + T extends { relayData: { realizedLpFeePct: V2SlowFillLeaf["relayData"]["realizedLpFeePct"] } }, + U extends Pick, +>(leaf: T | U): BN { + return unsafeIsType(leaf, "updatedOutputAmount") + ? leaf.relayData.inputAmount.sub(leaf.updatedOutputAmount).mul(fixedPointAdjustment).div(leaf.relayData.inputAmount) + : leaf.relayData.realizedLpFeePct; +} diff --git a/src/utils/ValidatorUtils.ts b/src/utils/ValidatorUtils.ts index b9f7ad4a..416a3b95 100644 --- a/src/utils/ValidatorUtils.ts +++ b/src/utils/ValidatorUtils.ts @@ -6,7 +6,7 @@ const AddressValidator = define("AddressValidator", (v) => ethers.utils. const HexValidator = define("HexValidator", (v) => ethers.utils.isHexString(String(v))); const BigNumberValidator = define("BigNumberValidator", (v) => ethers.BigNumber.isBigNumber(v)); -const DepositSchema = object({ +const V2DepositSchema = object({ depositId: Min(integer(), 0), depositor: AddressValidator, recipient: AddressValidator, @@ -28,9 +28,35 @@ const DepositSchema = object({ logIndex: Min(integer(), 0), quoteBlockNumber: Min(integer(), 0), transactionHash: HexValidator, - blockTimestamp: optional(Min(integer(), 0)), +}); + +const V3DepositSchema = object({ + depositId: Min(integer(), 0), + depositor: AddressValidator, + recipient: AddressValidator, + inputToken: AddressValidator, + inputAmount: BigNumberValidator, + originChainId: Min(integer(), 0), + destinationChainId: Min(integer(), 0), + quoteTimestamp: Min(integer(), 0), + fillDeadline: Min(integer(), 0), + exclusivityDeadline: Min(integer(), 0), + exclusiveRelayer: AddressValidator, + realizedLpFeePct: optional(BigNumberValidator), + outputToken: AddressValidator, + outputAmount: BigNumberValidator, + message: string(), + speedUpSignature: optional(HexValidator), + updatedOutputAmount: optional(BigNumberValidator), + updatedRecipient: optional(string()), + updatedMessage: optional(string()), + blockNumber: Min(integer(), 0), + transactionIndex: Min(integer(), 0), + logIndex: Min(integer(), 0), + quoteBlockNumber: Min(integer(), 0), + transactionHash: HexValidator, }); export function isDepositFormedCorrectly(deposit: unknown): deposit is DepositWithBlock { - return DepositSchema.is(deposit); + return V2DepositSchema.is(deposit) || V3DepositSchema.is(deposit); } diff --git a/test/utils/SpokePoolUtils.ts b/test/utils/SpokePoolUtils.ts index 05722d08..75a3bc5a 100644 --- a/test/utils/SpokePoolUtils.ts +++ b/test/utils/SpokePoolUtils.ts @@ -58,9 +58,9 @@ export function v3FillFromDeposit(deposit: V3DepositWithBlock, relayer: string): exclusiveRelayer: relayer, repaymentChainId: deposit.destinationChainId, relayExecutionInfo: { - recipient: deposit.updatedRecipient ?? recipient, - message: deposit.updatedMessage ?? message, - outputAmount: deposit.updatedOutputAmount ?? deposit.outputAmount, + updatedRecipient: deposit.updatedRecipient ?? recipient, + updatedMessage: deposit.updatedMessage ?? message, + updatedOutputAmount: deposit.updatedOutputAmount ?? deposit.outputAmount, fillType: FillType.FastFill, }, }; diff --git a/test/utils/utils.ts b/test/utils/utils.ts index cf68dcb3..d9391b04 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -444,10 +444,10 @@ export async function fillV3Relay( exclusiveRelayer: args.exclusiveRelayer, relayer: args.relayer, repaymentChainId: Number(args.repaymentChainId), - updatableRelayData: { - recipient: args.relayExecutionInfo.recipient, - message: args.relayExecutionInfo.message, - outputAmount: args.relayExecutionInfo.outputAmount, + relayExecutionInfo: { + updatedRecipient: args.relayExecutionInfo.recipient, + updatedMessage: args.relayExecutionInfo.message, + updatedOutputAmount: args.relayExecutionInfo.outputAmount, fillType: args.relayExecutionInfo.fillType, }, blockNumber, diff --git a/test/validatorUtils.test.ts b/test/validatorUtils.test.ts index b56b5ff5..a313f1fd 100644 --- a/test/validatorUtils.test.ts +++ b/test/validatorUtils.test.ts @@ -8,7 +8,7 @@ import { Deposit } from "../src/interfaces"; describe("validatorUtils", () => { describe("isDeposit", () => { - let deposit: interfaces.DepositWithBlock; + let deposit: interfaces.V2DepositWithBlock, depositV3: interfaces.V3DepositWithBlock; beforeEach(() => { deposit = { depositId: 1, @@ -22,13 +22,36 @@ describe("validatorUtils", () => { updatedRecipient: ZERO_ADDRESS, originToken: ZERO_ADDRESS, relayerFeePct: toBN(0), + realizedLpFeePct: toBN(0), destinationToken: ZERO_ADDRESS, transactionHash: "0xa", blockNumber: 0, transactionIndex: 0, logIndex: 0, quoteBlockNumber: 0, - blockTimestamp: 0, + }; + depositV3 = { + depositId: 1, + depositor: ZERO_ADDRESS, + destinationChainId: 1, + originChainId: 1, + inputAmount: toBN(100), + inputToken: ZERO_ADDRESS, + outputAmount: toBN(100), + outputToken: ZERO_ADDRESS, + message: "", + quoteTimestamp: 0, + recipient: ZERO_ADDRESS, + updatedRecipient: ZERO_ADDRESS, + fillDeadline: 100, + exclusiveRelayer: ZERO_ADDRESS, + exclusivityDeadline: 100, + realizedLpFeePct: toBN(0), + transactionHash: "0xa", + blockNumber: 0, + transactionIndex: 0, + logIndex: 0, + quoteBlockNumber: 0, }; }); @@ -38,6 +61,7 @@ describe("validatorUtils", () => { it("should return true on positive conditions", () => { // We should be able to return true for the default deposit expect(utils.isDepositFormedCorrectly(deposit)).to.be.true; + expect(utils.isDepositFormedCorrectly(depositV3)).to.be.true; // Let's change the recipient to a valid address deposit.recipient = randomAddress(); expect(utils.isDepositFormedCorrectly(deposit)).to.be.true; @@ -87,6 +111,7 @@ describe("validatorUtils", () => { it("should successfully rehydrate real deposits", () => { const deposits: string[] = [ '{"amount":{"type":"BigNumber","hex":"0x038d7ea4c68000"},"originChainId":42161,"destinationChainId":10,"relayerFeePct":{"type":"BigNumber","hex":"0xee042a4c72e9a8"},"depositId":1160366,"quoteTimestamp":1697088000,"originToken":"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1","recipient":"0x525D59654479cFaED622C1Ca06f237ce1072c2AB","depositor":"0x269727F088F16E1Aea52Cf5a97B1CD41DAA3f02D","message":"0x","blockNumber":139874261,"transactionIndex":1,"logIndex":1,"transactionHash":"0x4c4273f4cceb288a76aa7d6c057a8e3ab571a19711a59a965726e06b04e6b821","realizedLpFeePct":{"type":"BigNumber","hex":"0x016b90ac8ef5b9"},"destinationToken":"0x4200000000000000000000000000000000000006","quoteBlockNumber":18332204}', + '{"inputAmount":{"type":"BigNumber","hex":"0x038d7ea4c68000"},"outputAmount":{"type":"BigNumber","hex":"0x038d7ea4c68000"},"originChainId":42161,"destinationChainId":10,"exclusivityDeadline":1697088000,"fillDeadline":1697088000,"depositId":1160366,"quoteTimestamp":1697088000,"inputToken":"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1","recipient":"0x525D59654479cFaED622C1Ca06f237ce1072c2AB","depositor":"0x269727F088F16E1Aea52Cf5a97B1CD41DAA3f02D","exclusiveRelayer":"0x269727F088F16E1Aea52Cf5a97B1CD41DAA3f02D","message":"0x","blockNumber":139874261,"transactionIndex":1,"logIndex":1,"transactionHash":"0x4c4273f4cceb288a76aa7d6c057a8e3ab571a19711a59a965726e06b04e6b821","realizedLpFeePct":{"type":"BigNumber","hex":"0x016b90ac8ef5b9"},"outputToken":"0x4200000000000000000000000000000000000006","quoteBlockNumber":18332204}', ]; const rehydratedDeposits = deposits.map((d) => JSON.parse(d, objectWithBigNumberReviver) as Deposit); expect(rehydratedDeposits.every(utils.isDepositFormedCorrectly)).to.be.true;