Skip to content

Commit 6d31ae9

Browse files
committed
Merge pxrl/evmRelocate
1 parent d02955c commit 6d31ae9

File tree

9 files changed

+365
-340
lines changed

9 files changed

+365
-340
lines changed

src/arch/evm/SpokeUtils.ts

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import assert from "assert";
2+
import { BytesLike, Contract, PopulatedTransaction, providers } from "ethers";
3+
import { CHAIN_IDs } from "../../constants";
4+
import { Deposit, FillStatus, FillWithBlock, RelayData } from "../../interfaces";
5+
import {
6+
bnUint32Max,
7+
BigNumber,
8+
toBN,
9+
bnZero,
10+
chunk,
11+
getMessageHash,
12+
getRelayDataHash,
13+
isDefined,
14+
isUnsafeDepositId,
15+
isZeroAddress,
16+
getDepositRelayData,
17+
getNetworkName,
18+
paginatedEventQuery,
19+
spreadEventWithBlockNumber,
20+
toBytes32,
21+
} from "../../utils";
22+
23+
type BlockTag = providers.BlockTag;
24+
25+
/**
26+
* @param spokePool SpokePool Contract instance.
27+
* @param deposit V3Deopsit instance.
28+
* @param repaymentChainId Optional repaymentChainId (defaults to destinationChainId).
29+
* @returns An Ethers UnsignedTransaction instance.
30+
*/
31+
export function populateV3Relay(
32+
spokePool: Contract,
33+
deposit: Omit<Deposit, "messageHash">,
34+
relayer: string,
35+
repaymentChainId = deposit.destinationChainId
36+
): Promise<PopulatedTransaction> {
37+
const relayData = getDepositRelayData(deposit);
38+
39+
if (isDefined(deposit.speedUpSignature)) {
40+
assert(isDefined(deposit.updatedRecipient) && !isZeroAddress(deposit.updatedRecipient));
41+
assert(isDefined(deposit.updatedOutputAmount));
42+
assert(isDefined(deposit.updatedMessage));
43+
return spokePool.populateTransaction.fillRelayWithUpdatedDeposit(
44+
relayData,
45+
repaymentChainId,
46+
toBytes32(relayer),
47+
deposit.updatedOutputAmount,
48+
toBytes32(deposit.updatedRecipient),
49+
deposit.updatedMessage,
50+
deposit.speedUpSignature,
51+
{ from: relayer }
52+
);
53+
}
54+
55+
return spokePool.populateTransaction.fillRelay(relayData, repaymentChainId, toBytes32(relayer), { from: relayer });
56+
}
57+
58+
/**
59+
* Retrieves the time from the SpokePool contract at a particular block.
60+
* @returns The time at the specified block tag.
61+
*/
62+
export async function getTimeAt(spokePool: Contract, blockNumber: number): Promise<number> {
63+
const currentTime = await spokePool.getCurrentTime({ blockTag: blockNumber });
64+
assert(BigNumber.isBigNumber(currentTime) && currentTime.lt(bnUint32Max));
65+
return currentTime.toNumber();
66+
}
67+
68+
/**
69+
* Retrieves the chain time at a particular block.
70+
* @note This should be the same as getTimeAt() but can differ in test. These two functions should be consolidated.
71+
* @returns The chain time at the specified block tag.
72+
*/
73+
export async function getTimestampForBlock(provider: providers.Provider, blockNumber: number): Promise<number> {
74+
const block = await provider.getBlock(blockNumber);
75+
return block.timestamp;
76+
}
77+
78+
/**
79+
* Return maximum of fill deadline buffer at start and end of block range.
80+
* @param spokePool SpokePool contract instance
81+
* @param startBlock start block
82+
* @param endBlock end block
83+
* @returns maximum of fill deadline buffer at start and end block
84+
*/
85+
export async function getMaxFillDeadlineInRange(
86+
spokePool: Contract,
87+
startBlock: number,
88+
endBlock: number
89+
): Promise<number> {
90+
const fillDeadlineBuffers = await Promise.all([
91+
spokePool.fillDeadlineBuffer({ blockTag: startBlock }),
92+
spokePool.fillDeadlineBuffer({ blockTag: endBlock }),
93+
]);
94+
return Math.max(fillDeadlineBuffers[0], fillDeadlineBuffers[1]);
95+
}
96+
97+
/**
98+
* Finds the deposit id at a specific block number.
99+
* @param blockTag The block number to search for the deposit ID at.
100+
* @returns The deposit ID.
101+
*/
102+
export async function getDepositIdAtBlock(contract: Contract, blockTag: number): Promise<BigNumber> {
103+
const _depositIdAtBlock = await contract.numberOfDeposits({ blockTag });
104+
const depositIdAtBlock = toBN(_depositIdAtBlock);
105+
// Sanity check to ensure that the deposit ID is greater than or equal to zero.
106+
if (depositIdAtBlock.lt(bnZero)) {
107+
throw new Error("Invalid deposit count");
108+
}
109+
return depositIdAtBlock;
110+
}
111+
112+
export async function findDepositBlock(
113+
spokePool: Contract,
114+
depositId: BigNumber,
115+
lowBlock: number,
116+
highBlock?: number
117+
): Promise<number | undefined> {
118+
// We can only perform this search when we have a safe deposit ID.
119+
if (isUnsafeDepositId(depositId)) {
120+
throw new Error(`Cannot binary search for depositId ${depositId}`);
121+
}
122+
123+
highBlock ??= await spokePool.provider.getBlockNumber();
124+
assert(highBlock > lowBlock, `Block numbers out of range (${lowBlock} >= ${highBlock})`);
125+
126+
// Make sure the deposit occurred within the block range supplied by the caller.
127+
const [nDepositsLow, nDepositsHigh] = (
128+
await Promise.all([
129+
spokePool.numberOfDeposits({ blockTag: lowBlock }),
130+
spokePool.numberOfDeposits({ blockTag: highBlock }),
131+
])
132+
).map((n) => toBN(n));
133+
134+
if (nDepositsLow.gt(depositId) || nDepositsHigh.lte(depositId)) {
135+
return undefined; // Deposit did not occur within the specified block range.
136+
}
137+
138+
// Find the lowest block number where numberOfDeposits is greater than the requested depositId.
139+
do {
140+
const midBlock = Math.floor((highBlock + lowBlock) / 2);
141+
const nDeposits = toBN(await spokePool.numberOfDeposits({ blockTag: midBlock }));
142+
143+
if (nDeposits.gt(depositId)) {
144+
highBlock = midBlock; // depositId occurred at or earlier than midBlock.
145+
} else {
146+
lowBlock = midBlock + 1; // depositId occurred later than midBlock.
147+
}
148+
} while (lowBlock < highBlock);
149+
150+
return lowBlock;
151+
}
152+
153+
/**
154+
* Find the amount filled for a deposit at a particular block.
155+
* @param spokePool SpokePool contract instance.
156+
* @param relayData Deposit information that is used to complete a fill.
157+
* @param blockTag Block tag (numeric or "latest") to query at.
158+
* @returns The amount filled for the specified deposit at the requested block (or latest).
159+
*/
160+
export async function relayFillStatus(
161+
spokePool: Contract,
162+
relayData: RelayData,
163+
blockTag?: number | "latest",
164+
destinationChainId?: number
165+
): Promise<FillStatus> {
166+
destinationChainId ??= await spokePool.chainId();
167+
assert(isDefined(destinationChainId));
168+
169+
const hash = getRelayDataHash(relayData, destinationChainId);
170+
const _fillStatus = await spokePool.fillStatuses(hash, { blockTag });
171+
const fillStatus = Number(_fillStatus);
172+
173+
if (![FillStatus.Unfilled, FillStatus.RequestedSlowFill, FillStatus.Filled].includes(fillStatus)) {
174+
const { originChainId, depositId } = relayData;
175+
throw new Error(
176+
`relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId.toString()} (${fillStatus})`
177+
);
178+
}
179+
180+
return fillStatus;
181+
}
182+
183+
export async function fillStatusArray(
184+
spokePool: Contract,
185+
relayData: RelayData[],
186+
blockTag: BlockTag = "latest"
187+
): Promise<(FillStatus | undefined)[]> {
188+
const fillStatuses = "fillStatuses";
189+
const destinationChainId = await spokePool.chainId();
190+
191+
const queries = relayData.map((relayData) => {
192+
const hash = getRelayDataHash(relayData, destinationChainId);
193+
return spokePool.interface.encodeFunctionData(fillStatuses, [hash]);
194+
});
195+
196+
// Chunk the hashes into appropriate sizes to avoid death by rpc.
197+
const chunkSize = 250;
198+
const chunkedQueries = chunk(queries, chunkSize);
199+
200+
const multicalls = await Promise.all(
201+
chunkedQueries.map((queries) => spokePool.callStatic.multicall(queries, { blockTag }))
202+
);
203+
const status = multicalls
204+
.map((multicall: BytesLike[]) =>
205+
multicall.map((result) => spokePool.interface.decodeFunctionResult(fillStatuses, result)[0])
206+
)
207+
.flat();
208+
209+
const bnUnfilled = toBN(FillStatus.Unfilled);
210+
const bnFilled = toBN(FillStatus.Filled);
211+
212+
return status.map((status: unknown) => {
213+
return BigNumber.isBigNumber(status) && status.gte(bnUnfilled) && status.lte(bnFilled)
214+
? status.toNumber()
215+
: undefined;
216+
});
217+
}
218+
219+
/**
220+
* Find the block at which a fill was completed.
221+
* @todo After SpokePool upgrade, this function can be simplified to use the FillStatus enum.
222+
* @param spokePool SpokePool contract instance.
223+
* @param relayData Deposit information that is used to complete a fill.
224+
* @param lowBlockNumber The lower bound of the search. Must be bounded by SpokePool deployment.
225+
* @param highBlocknumber Optional upper bound for the search.
226+
* @returns The block number at which the relay was completed, or undefined.
227+
*/
228+
export async function findFillBlock(
229+
spokePool: Contract,
230+
relayData: RelayData,
231+
lowBlockNumber: number,
232+
highBlockNumber?: number
233+
): Promise<number | undefined> {
234+
const { provider } = spokePool;
235+
highBlockNumber ??= await provider.getBlockNumber();
236+
assert(highBlockNumber > lowBlockNumber, `Block numbers out of range (${lowBlockNumber} >= ${highBlockNumber})`);
237+
238+
// In production the chainId returned from the provider matches 1:1 with the actual chainId. Querying the provider
239+
// object saves an RPC query because the chainId is cached by StaticJsonRpcProvider instances. In hre, the SpokePool
240+
// may be configured with a different chainId than what is returned by the provider.
241+
const destinationChainId = Object.values(CHAIN_IDs).includes(relayData.originChainId)
242+
? (await provider.getNetwork()).chainId
243+
: Number(await spokePool.chainId());
244+
assert(
245+
relayData.originChainId !== destinationChainId,
246+
`Origin & destination chain IDs must not be equal (${destinationChainId})`
247+
);
248+
249+
// Make sure the relay was completed within the block range supplied by the caller.
250+
const [initialFillStatus, finalFillStatus] = (
251+
await Promise.all([
252+
relayFillStatus(spokePool, relayData, lowBlockNumber, destinationChainId),
253+
relayFillStatus(spokePool, relayData, highBlockNumber, destinationChainId),
254+
])
255+
).map(Number);
256+
257+
if (finalFillStatus !== FillStatus.Filled) {
258+
return undefined; // Wasn't filled within the specified block range.
259+
}
260+
261+
// Was filled earlier than the specified lowBlock. This is an error by the caller.
262+
if (initialFillStatus === FillStatus.Filled) {
263+
const { depositId, originChainId } = relayData;
264+
const [srcChain, dstChain] = [getNetworkName(originChainId), getNetworkName(destinationChainId)];
265+
throw new Error(`${srcChain} deposit ${depositId.toString()} filled on ${dstChain} before block ${lowBlockNumber}`);
266+
}
267+
268+
// Find the leftmost block where filledAmount equals the deposit amount.
269+
do {
270+
const midBlockNumber = Math.floor((highBlockNumber + lowBlockNumber) / 2);
271+
const fillStatus = await relayFillStatus(spokePool, relayData, midBlockNumber, destinationChainId);
272+
273+
if (fillStatus === FillStatus.Filled) {
274+
highBlockNumber = midBlockNumber;
275+
} else {
276+
lowBlockNumber = midBlockNumber + 1;
277+
}
278+
} while (lowBlockNumber < highBlockNumber);
279+
280+
return lowBlockNumber;
281+
}
282+
283+
export async function findFillEvent(
284+
spokePool: Contract,
285+
relayData: RelayData,
286+
lowBlockNumber: number,
287+
highBlockNumber?: number
288+
): Promise<FillWithBlock | undefined> {
289+
const blockNumber = await findFillBlock(spokePool, relayData, lowBlockNumber, highBlockNumber);
290+
if (!blockNumber) return undefined;
291+
292+
// We can hardcode this to 0 to instruct paginatedEventQuery to make a single request for the same block number.
293+
const maxBlockLookBack = 0;
294+
const [fromBlock, toBlock] = [blockNumber, blockNumber];
295+
296+
const query = (
297+
await Promise.all([
298+
paginatedEventQuery(
299+
spokePool,
300+
spokePool.filters.FilledRelay(null, null, null, null, null, relayData.originChainId, relayData.depositId),
301+
{ fromBlock, toBlock, maxBlockLookBack }
302+
),
303+
paginatedEventQuery(
304+
spokePool,
305+
spokePool.filters.FilledV3Relay(null, null, null, null, null, relayData.originChainId, relayData.depositId),
306+
{ fromBlock, toBlock, maxBlockLookBack }
307+
),
308+
])
309+
).flat();
310+
if (query.length === 0) throw new Error(`Failed to find fill event at block ${blockNumber}`);
311+
const event = query[0];
312+
// In production the chainId returned from the provider matches 1:1 with the actual chainId. Querying the provider
313+
// object saves an RPC query because the chainId is cached by StaticJsonRpcProvider instances. In hre, the SpokePool
314+
// may be configured with a different chainId than what is returned by the provider.
315+
const destinationChainId = Object.values(CHAIN_IDs).includes(relayData.originChainId)
316+
? (await spokePool.provider.getNetwork()).chainId
317+
: Number(await spokePool.chainId());
318+
const fill = {
319+
...spreadEventWithBlockNumber(event),
320+
destinationChainId,
321+
messageHash: getMessageHash(event.args.message),
322+
} as FillWithBlock;
323+
return fill;
324+
}

src/arch/evm/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const platform = "evm"; // Placeholder for actual exports.
1+
export * from "./SpokeUtils";

src/clients/BundleDataClient/BundleDataClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
DepositWithBlock,
2121
} from "../../interfaces";
2222
import { SpokePoolClient } from "..";
23+
import { findFillEvent } from "../../arch/evm";
2324
import {
2425
BigNumber,
2526
bnZero,
@@ -37,7 +38,6 @@ import {
3738
mapAsync,
3839
bnUint32Max,
3940
isZeroValueDeposit,
40-
findFillEvent,
4141
isZeroValueFillOrSlowFillRequest,
4242
chainIsEvm,
4343
isValidEvmAddress,

src/clients/SpokePoolClient/EVMSpokePoolClient.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import { Contract, EventFilter } from "ethers";
2-
import { BigNumber, DepositSearchResult, getNetworkName, InvalidFill, MakeOptional, toBN } from "../../utils";
2+
import {
3+
findDepositBlock,
4+
getMaxFillDeadlineInRange as getMaxFillDeadline,
5+
getTimeAt as _getTimeAt,
6+
relayFillStatus,
7+
getTimestampForBlock as _getTimestampForBlock,
8+
} from "../../arch/evm";
9+
import { DepositWithBlock, FillStatus, RelayData } from "../../interfaces";
10+
import {
11+
BigNumber,
12+
DepositSearchResult,
13+
getNetworkName,
14+
InvalidFill,
15+
isZeroAddress,
16+
MakeOptional,
17+
toBN,
18+
} from "../../utils";
319
import {
420
EventSearchConfig,
521
paginatedEventQuery,
@@ -10,15 +26,6 @@ import { isUpdateFailureReason } from "../BaseAbstractClient";
1026
import { knownEventNames, SpokePoolClient, SpokePoolUpdate } from "./SpokePoolClient";
1127
import winston from "winston";
1228
import { HubPoolClient } from "../HubPoolClient";
13-
import {
14-
findDepositBlock,
15-
getMaxFillDeadlineInRange as getMaxFillDeadline,
16-
getTimeAt as _getTimeAt,
17-
relayFillStatus,
18-
isZeroAddress,
19-
getTimestampForBlock as _getTimestampForBlock,
20-
} from "../../utils/SpokeUtils";
21-
import { DepositWithBlock, FillStatus, RelayData } from "../../interfaces";
2229

2330
/**
2431
* An EVM-specific SpokePoolClient.

src/relayFeeCalculator/chain-queries/baseQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { Coingecko } from "../../coingecko";
66
import { CHAIN_IDs, DEFAULT_SIMULATED_RELAYER_ADDRESS } from "../../constants";
77
import { Deposit } from "../../interfaces";
88
import { SpokePool, SpokePool__factory } from "../../typechain";
9+
import { populateV3Relay } from "../../arch/evm";
910
import {
1011
BigNumberish,
1112
TransactionCostEstimate,
12-
populateV3Relay,
1313
BigNumber,
1414
toBNWei,
1515
bnZero,

0 commit comments

Comments
 (0)