From 043690f4864168110e3c3e3692be3ab8a115904d Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:55:18 +0100 Subject: [PATCH] feat: Helper to resolve the block number for a fill (#488) Given the valid relayData that can be constructed from a Deposit, resolve whether the deposit was ever filled on the destination chain without relying on events. This can be used as a sanity check on RPC responses that sometimes drop logs. The "left-most" variant of a binary search is implemented. The logic is very simple and has been repeatedly tested to resolve in ~20 iterations with a fill made randomly within a range of 1M blocks. This can be verified locally by adjusting the nBlocks variable within the accompanying test case (remember also to bump the test timeout via this.timeout(ms) in the relevant test case). The criteria for determining when the fill was made is the block at which is was considered complete. This effectively ignores partial fills entirely, but that's fortunately forwards-compatible with the direction of Across. --- package.json | 2 +- src/utils/SpokeUtils.ts | 60 ++++++++++++++++++++++++ test/SpokePoolClient.fills.ts | 86 ++++++++++++++++++++++++++++++++++- 3 files changed, 146 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 14c046822..77dc28669 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk-v2", "author": "UMA Team", - "version": "0.20.0", + "version": "0.20.1", "license": "AGPL-3.0", "homepage": "https://docs.across.to/v/developer-docs/developers/across-sdk", "files": [ diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 0e14781d5..41f7e75f2 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,7 +1,9 @@ +import assert from "assert"; import { getRelayHash } from "@across-protocol/contracts-v2/dist/test-utils"; import { BigNumber, Contract } from "ethers"; import { RelayData } from "../interfaces"; import { SpokePoolClient } from "../clients"; +import { getNetworkName } from "./NetworkUtils"; /** * Find the block range that contains the deposit ID. This is a binary search that searches for the block range @@ -158,6 +160,13 @@ export async function getDepositIdAtBlock(contract: Contract, blockTag: number): return depositIdAtBlock; } +/** + * Find the amount filled for a deposit at a particular block. + * @param spokePool SpokePool contract instance. + * @param relayData Deposit information that is used to complete a fill. + * @param blockTag Block tag (numeric or "latest") to query at. + * @returns The amount filled for the specified deposit at the requested block (or latest). + */ export function relayFilledAmount( spokePool: Contract, relayData: RelayData, @@ -178,3 +187,54 @@ export function relayFilledAmount( return spokePool.relayFills(hash, { blockTag }); } + +/** + * Find the block at which a fill was completed. + * @param spokePool SpokePool contract instance. + * @param relayData Deposit information that is used to complete a fill. + * @param lowBlockNumber The lower bound of the search. Must be bounded by SpokePool deployment. + * @param highBlocknumber Optional upper bound for the search. + * @returns The block number at which the relay was completed, or undefined. + */ +export async function findFillBlock( + spokePool: Contract, + relayData: RelayData, + lowBlockNumber: number, + highBlockNumber?: number +): Promise { + const { provider } = spokePool; + highBlockNumber ??= await provider.getBlockNumber(); + assert(highBlockNumber > lowBlockNumber, `Block numbers out of range (${lowBlockNumber} > ${highBlockNumber})`); + + // Make sure the relay is 100% completed within the block range supplied by the caller. + const [initialFillAmount, finalFillAmount] = await Promise.all([ + relayFilledAmount(spokePool, relayData, lowBlockNumber), + relayFilledAmount(spokePool, relayData, highBlockNumber), + ]); + + // Wasn't filled within the specified block range. + if (finalFillAmount.lt(relayData.amount)) { + return undefined; + } + + // Was filled earlier than the specified lowBlock.. This is an error by the caller. + if (initialFillAmount.eq(relayData.amount)) { + const { depositId, originChainId, destinationChainId } = relayData; + const [srcChain, dstChain] = [getNetworkName(originChainId), getNetworkName(destinationChainId)]; + throw new Error(`${srcChain} deposit ${depositId} filled on ${dstChain} before block ${lowBlockNumber}`); + } + + // Find the leftmost block where filledAmount equals the deposit amount. + do { + const midBlockNumber = Math.floor((highBlockNumber + lowBlockNumber) / 2); + const filledAmount = await relayFilledAmount(spokePool, relayData, midBlockNumber); + + if (filledAmount.eq(relayData.amount)) { + highBlockNumber = midBlockNumber; + } else { + lowBlockNumber = midBlockNumber + 1; + } + } while (lowBlockNumber < highBlockNumber); + + return lowBlockNumber; +} diff --git a/test/SpokePoolClient.fills.ts b/test/SpokePoolClient.fills.ts index 732ca0edc..8d3d4d9c3 100644 --- a/test/SpokePoolClient.fills.ts +++ b/test/SpokePoolClient.fills.ts @@ -1,6 +1,9 @@ +import hre from "hardhat"; import { SpokePoolClient } from "../src/clients"; -import { Deposit } from "../src/interfaces"; +import { Deposit, RelayData } from "../src/interfaces"; +import { findFillBlock, getNetworkName } from "../src/utils"; import { + assertPromiseError, Contract, SignerWithAddress, buildFill, @@ -102,4 +105,85 @@ describe("SpokePoolClient: Fills", function () { expect(spokePoolClient.getFillsForRelayer(relayer1.address).length).to.equal(3); expect(spokePoolClient.getFillsForRelayer(relayer2.address).length).to.equal(3); }); + + it("Correctly locates the block number for a FilledRelay event", async function () { + const nBlocks = 1_000; + + const deposit: Deposit = { + depositId: 0, + depositor: depositor.address, + recipient: depositor.address, + originToken: erc20.address, + amount: toBNWei("1"), + originChainId, + destinationChainId, + relayerFeePct: toBNWei("0.01"), + quoteTimestamp: Date.now(), + realizedLpFeePct: toBNWei("0.01"), + destinationToken: destErc20.address, + message: "0x", + }; + + // Submit the fill randomly within the next `nBlocks` blocks. + const startBlock = await spokePool.provider.getBlockNumber(); + const targetFillBlock = startBlock + Math.floor(Math.random() * nBlocks); + + for (let i = 0; i < nBlocks; ++i) { + const blockNumber = await spokePool.provider.getBlockNumber(); + if (blockNumber === targetFillBlock - 1) { + await buildFill(spokePool, destErc20, depositor, relayer1, deposit, 1); + continue; + } + + await hre.network.provider.send("evm_mine"); + } + + const fillBlock = await findFillBlock(spokePool, deposit as RelayData, startBlock); + expect(fillBlock).to.equal(targetFillBlock); + }); + + it("FilledRelay block search: bounds checking", async function () { + const nBlocks = 100; + + const deposit: Deposit = { + depositId: 0, + depositor: depositor.address, + recipient: depositor.address, + originToken: erc20.address, + amount: toBNWei("1"), + originChainId, + destinationChainId, + relayerFeePct: toBNWei("0.01"), + quoteTimestamp: Date.now(), + realizedLpFeePct: toBNWei("0.01"), + destinationToken: destErc20.address, + message: "0x", + }; + + const startBlock = await spokePool.provider.getBlockNumber(); + for (let i = 0; i < nBlocks; ++i) { + await hre.network.provider.send("evm_mine"); + } + + // No fill has been made, so expect an undefined fillBlock. + const fillBlock = await findFillBlock(spokePool, deposit as RelayData, startBlock); + expect(fillBlock).to.be.undefined; + + await buildFill(spokePool, destErc20, depositor, relayer1, deposit, 1); + const lateBlockNumber = await spokePool.provider.getBlockNumber(); + await hre.network.provider.send("evm_mine"); + + // Now search for the fill _after_ it was filled and expect an exception. + const [srcChain, dstChain] = [getNetworkName(deposit.originChainId), getNetworkName(deposit.destinationChainId)]; + await assertPromiseError( + findFillBlock(spokePool, deposit as RelayData, lateBlockNumber), + `${srcChain} deposit ${deposit.depositId} filled on ${dstChain} before block` + ); + + // Should assert if highBlock <= lowBlock. + await assertPromiseError( + findFillBlock(spokePool, deposit as RelayData, await spokePool.provider.getBlockNumber()), + "Block numbers out of range" + ); + }); });