Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Helper to resolve the block number for a fill #488

Merged
merged 7 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
);
});
});