Skip to content

Commit

Permalink
feat: Helper to resolve the block number for a fill (#488)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pxrl authored Jan 5, 2024
1 parent 1cd06de commit 043690f
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 2 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
60 changes: 60 additions & 0 deletions src/utils/SpokeUtils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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<number | undefined> {
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;
}
86 changes: 85 additions & 1 deletion test/SpokePoolClient.fills.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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"
);
});
});

0 comments on commit 043690f

Please sign in to comment.