Skip to content

Commit

Permalink
feat: Helper to resolve the block number for a fill
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 committed Jan 4, 2024
1 parent b62c83c commit 8003ea3
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 1 deletion.
43 changes: 43 additions & 0 deletions src/utils/SpokeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 @@ -177,3 +178,45 @@ export function relayFilledAmount(

return spokePool.relayFills(hash, { blockTag });
}

export async function findFillBlock(
spokePool: Contract,
relayData: RelayData,
lowBlockNumber: number,
highBlockNumber?: number
): Promise<number | undefined> {
const { provider } = spokePool;
highBlockNumber ??= await provider.getBlockNumber();

// 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;
}
81 changes: 80 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,80 @@ 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");
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 + 1),
`${srcChain} deposit ${deposit.depositId} filled on ${dstChain} before block`
);
});
});

0 comments on commit 8003ea3

Please sign in to comment.