Skip to content

Commit

Permalink
update with breaking changes to pass tests
Browse files Browse the repository at this point in the history
Signed-off-by: bennett <[email protected]>
  • Loading branch information
bmzig committed Jan 20, 2025
1 parent f3e975a commit d41d0e0
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 48 deletions.
35 changes: 25 additions & 10 deletions src/clients/SpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,18 +843,33 @@ export class SpokePoolClient extends BaseAbstractClient {
);

const tStart = Date.now();
const query = await paginatedEventQuery(
this.spokePool,
this.spokePool.filters.V3FundsDeposited(null, null, null, null, null, depositId),
{
fromBlock: searchBounds.low,
toBlock: searchBounds.high,
maxBlockLookBack: this.eventSearchConfig.maxBlockLookBack,
}
);
// Check both V3FundsDeposited and FundsDeposited events to look for a specified depositId.
const query = (
await Promise.all([
paginatedEventQuery(
this.spokePool,
this.spokePool.filters.V3FundsDeposited(null, null, null, null, null, depositId),
{
fromBlock: searchBounds.low,
toBlock: searchBounds.high,
maxBlockLookBack: this.eventSearchConfig.maxBlockLookBack,
}
),

paginatedEventQuery(
this.spokePool,
this.spokePool.filters.FundsDeposited(null, null, null, null, null, depositId),
{
fromBlock: searchBounds.low,
toBlock: searchBounds.high,
maxBlockLookBack: this.eventSearchConfig.maxBlockLookBack,
}
),
])
).flat();
const tStop = Date.now();

const event = query.find(({ args }) => args["depositId"] === depositId);
const event = query.find(({ args }) => args["depositId"].eq(depositId));
if (event === undefined) {
const srcChain = getNetworkName(this.chainId);
const dstChain = getNetworkName(destinationChainId);
Expand Down
17 changes: 17 additions & 0 deletions src/utils/AddressUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,20 @@ export function compareAddressesSimple(addressA?: string, addressB?: string): bo
}
return addressA.toLowerCase() === addressB.toLowerCase();
}

// Converts an input hex data string into a bytes32 string. Note that the output bytes will be lowercase
// so that it naturally matches with ethers event data.
// Throws an error if the input string is already greater than 32 bytes.
export function toBytes32(address: string): string {
return utils.hexZeroPad(address, 32).toLowerCase();
}

// Converts an input (assumed to be) bytes32 string into a bytes20 string.
// If the input is not a bytes32 but is less than type(uint160).max, then this function
// will still succeed.
// Throws an error if the string as an unsigned integer is greater than type(uint160).max.
export function toAddress(bytes32: string): string {
// rawAddress is the address which is not properly checksummed.
const rawAddress = utils.hexZeroPad(utils.hexStripZeros(bytes32), 20);
return utils.getAddress(rawAddress);
}
4 changes: 2 additions & 2 deletions src/utils/DepositUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "assert";
import { SpokePoolClient } from "../clients";
import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE } from "../constants";
import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE, ZERO_BYTES } from "../constants";
import { CachingMechanismInterface, Deposit, DepositWithBlock, Fill, SlowFillRequest } from "../interfaces";
import { getNetworkName } from "./NetworkUtils";
import { getDepositInCache, getDepositKey, setDepositInCache } from "./CachingUtils";
Expand Down Expand Up @@ -143,7 +143,7 @@ export function isZeroValueDeposit(deposit: Pick<Deposit, "inputAmount" | "messa
* @returns True if the message is empty, false otherwise.
*/
export function isMessageEmpty(message = EMPTY_MESSAGE): boolean {
return message === "" || message === "0x";
return message === "" || message === "0x" || message === ZERO_BYTES;
}

/**
Expand Down
20 changes: 17 additions & 3 deletions src/utils/FlowUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { isDefined } from "../utils";
import { isDefined, isMessageEmpty } from "../utils";
import { ZERO_BYTES } from "../constants";
import { Deposit, RelayData } from "../interfaces";
import { utils } from "ethers";

export const RELAYDATA_KEYS = [
"depositId",
Expand All @@ -14,13 +16,12 @@ export const RELAYDATA_KEYS = [
"fillDeadline",
"exclusivityDeadline",
"exclusiveRelayer",
"message",
] as const;

// Ensure that each deposit element is included with the same value in the fill. This includes all elements defined
// by the depositor as well as destinationToken, which are pulled from other clients.
export function validateFillForDeposit(
relayData: RelayData & { destinationChainId: number },
relayData: RelayData & { destinationChainId: number; messageHash?: string },
deposit?: Deposit
): { valid: true } | { valid: false; reason: string } {
if (deposit === undefined) {
Expand All @@ -32,6 +33,19 @@ export function validateFillForDeposit(
// validated against the fields in Fill and Deposit, generating an error if there is a discrepency.
const invalidKey = RELAYDATA_KEYS.find((key) => relayData[key].toString() !== deposit[key].toString());

const calculateMessageHash = (message: string) => {
return isMessageEmpty(message) ? ZERO_BYTES : utils.keccak256(deposit.message);
};
// Manually check if the message hash does not match.
// This is done separately since the deposit event emits the full message, while the relay event only emits the message hash.
const messageHash = calculateMessageHash(deposit.message);
// We may be checking a FilledV3Relay event or a FilledRelay event here. If we are checking a FilledV3Relay event, then relayData.message will be defined,
// and relayData.messageHash will be undefined. If we are checking a FilledRelay event, the opposite will be true.
const relayHash = isDefined(relayData.message) ? calculateMessageHash(relayData.message) : relayData.messageHash;
if (messageHash !== relayHash) {
return { valid: false, reason: `message mismatch (${messageHash} != ${relayHash})` };
}

return isDefined(invalidKey)
? { valid: false, reason: `${invalidKey} mismatch (${relayData[invalidKey]} != ${deposit[invalidKey]})` }
: { valid: true };
Expand Down
72 changes: 70 additions & 2 deletions src/utils/SpokeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import assert from "assert";
import { BytesLike, Contract, PopulatedTransaction, providers, utils as ethersUtils } from "ethers";
import { CHAIN_IDs, MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS } from "../constants";
import { CHAIN_IDs, MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS, ZERO_BYTES } from "../constants";
import { Deposit, Fill, FillStatus, RelayData, SlowFillRequest } from "../interfaces";
import { SpokePoolClient } from "../clients";
import { chunk } from "./ArrayUtils";
import { BigNumber, toBN } from "./BigNumberUtils";
import { isDefined } from "./TypeGuards";
import { getNetworkName } from "./NetworkUtils";
import { toBytes32 } from "./AddressUtils";
import { isMessageEmpty } from "./DepositUtils";

type BlockTag = providers.BlockTag;

Expand Down Expand Up @@ -215,6 +217,7 @@ export async function getDepositIdAtBlock(contract: Contract, blockTag: number):
* @param destinationChainId Supplementary destination chain ID required by V3 hashes.
* @returns The corresponding RelayData hash.
*/
/*
export function getRelayDataHash(relayData: RelayData, destinationChainId: number): string {
return ethersUtils.keccak256(
ethersUtils.defaultAbiCoder.encode(
Expand All @@ -239,6 +242,40 @@ export function getRelayDataHash(relayData: RelayData, destinationChainId: numbe
)
);
}
*/

/**
* Compute the RelayData hash for a fill assuming with new bytes32 spoke pool events.
* This can be used to determine the fill status.
* @param relayData RelayData information that is used to complete a fill.
* @param destinationChainId Supplementary destination chain ID required by V3 hashes.
* @returns The corresponding RelayData hash.
*/
export function getRelayDataHash(relayData: RelayData, destinationChainId: number): string {
const updatedRelayData = translateToUpdatedRelayData(relayData);
return ethersUtils.keccak256(
ethersUtils.defaultAbiCoder.encode(
[
"tuple(" +
"bytes32 depositor," +
"bytes32 recipient," +
"bytes32 exclusiveRelayer," +
"bytes32 inputToken," +
"bytes32 outputToken," +
"uint256 inputAmount," +
"uint256 outputAmount," +
"uint256 originChainId," +
"uint256 depositId," +
"uint32 fillDeadline," +
"uint32 exclusivityDeadline," +
"bytes32 message" +
")",
"uint256 destinationChainId",
],
[updatedRelayData, destinationChainId]
)
);
}

export function getRelayHashFromEvent(e: Deposit | Fill | SlowFillRequest): string {
return getRelayDataHash(e, e.destinationChainId);
Expand Down Expand Up @@ -268,7 +305,9 @@ export async function relayFillStatus(
destinationChainId?: number
): Promise<FillStatus> {
destinationChainId ??= await spokePool.chainId();
const hash = getRelayDataHash(relayData, destinationChainId!);
assert(isDefined(destinationChainId));

const hash = getRelayDataHash(relayData, destinationChainId);
const _fillStatus = await spokePool.fillStatuses(hash, { blockTag });
const fillStatus = Number(_fillStatus);

Expand Down Expand Up @@ -316,6 +355,35 @@ export async function fillStatusArray(
});
}

/*
* Determines if the relay data provided contains bytes32 for addresses or standard evm 20-byte addresses.
* Returns true if the relay data has bytes32 address representations.
*/
export function isUpdatedRelayData(relayData: RelayData) {
const isValidBytes32 = (maybeBytes32: string) => {
return ethersUtils.isBytes(maybeBytes32) && maybeBytes32.length === 66;
};
// Return false if the depositor is not a bytes32. Assume that if any field is a bytes32 in relayData, then all fields will be bytes32 representations.
return isValidBytes32(relayData.depositor);
}

/*
* Converts an input relay data to to the version with 32-byte address representations.
*/
export function translateToUpdatedRelayData(relayData: RelayData): RelayData {
return isUpdatedRelayData(relayData)
? relayData
: {
...relayData,
depositor: toBytes32(relayData.depositor),
recipient: toBytes32(relayData.recipient),
exclusiveRelayer: toBytes32(relayData.exclusiveRelayer),
inputToken: toBytes32(relayData.inputToken),
outputToken: toBytes32(relayData.outputToken),
message: isMessageEmpty(relayData.message) ? ZERO_BYTES : ethersUtils.keccak256(relayData.message),
};
}

/**
* Find the block at which a fill was completed.
* @todo After SpokePool upgrade, this function can be simplified to use the FillStatus enum.
Expand Down
10 changes: 5 additions & 5 deletions test/SpokePoolClient.SpeedUp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SpokePoolClient } from "../src/clients";
import { Deposit, SpeedUp } from "../src/interfaces";
import { bnOne } from "../src/utils";
import { bnOne, toBytes32 } from "../src/utils";
import { destinationChainId, originChainId } from "./constants";
import {
assert,
Expand Down Expand Up @@ -84,8 +84,8 @@ describe("SpokePoolClient: SpeedUp", function () {

await spokePool
.connect(depositor)
.speedUpV3Deposit(
depositor.address,
.speedUpDeposit(
toBytes32(depositor.address),
deposit.depositId,
updatedOutputAmount,
updatedRecipient,
Expand Down Expand Up @@ -146,8 +146,8 @@ describe("SpokePoolClient: SpeedUp", function () {

await spokePool
.connect(depositor)
.speedUpV3Deposit(
depositor.address,
.speedUpDeposit(
toBytes32(depositor.address),
depositId,
updatedOutputAmount,
updatedRecipient,
Expand Down
29 changes: 19 additions & 10 deletions test/SpokePoolClient.ValidateFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
queryHistoricalDepositForFill,
DepositSearchResult,
getBlockRangeForDepositId,
toBytes32,
} from "../src/utils";
import { ZERO_BYTES } from "../src/constants";
import { CHAIN_ID_TEST_LIST, originChainId, destinationChainId, repaymentChainId } from "./constants";
import {
assert,
Expand Down Expand Up @@ -146,16 +148,18 @@ describe("SpokePoolClient: Fill Validation", function () {

// For each RelayData field, toggle the value to produce an invalid fill. Verify that it's rejected.
const fields = Object.keys(fill).filter((field) => !ignoredFields.includes(field));
for (const field of fields) {
for (let field of fields) {
let val: BigNumber | string | number;
if (BigNumber.isBigNumber(fill[field])) {
val = fill[field].add(bnOne);
} else if (typeof fill[field] === "string") {
val = fill[field] + "xxx";
val = fill[field] + "1234";
} else {
expect(typeof fill[field]).to.equal("number");
val = fill[field] + 1;
}
// Remap the messageHash field to message for the deposit.
field = field === "messageHash" ? "message" : field;

const result = validateFillForDeposit(fill, { ...deposit_2, [field]: val });
expect(result.valid).to.be.false;
Expand Down Expand Up @@ -294,7 +298,7 @@ describe("SpokePoolClient: Fill Validation", function () {
await depositV3(spokePool_1, destinationChainId, depositor, inputToken, inputAmount, outputToken, outputAmount);
await mineRandomBlocks();

const [, deposit1Event] = await spokePool_1.queryFilter("V3FundsDeposited");
const [, deposit1Event] = await spokePool_1.queryFilter("FundsDeposited");
const deposit1Block = deposit1Event.blockNumber;

// Throws when low < high
Expand Down Expand Up @@ -369,10 +373,10 @@ describe("SpokePoolClient: Fill Validation", function () {
relayerFeePct: toBNWei("0.01"),
quoteTimestamp: await spokePool_1.getCurrentTime(),
});
const depositData = await spokePool_1.populateTransaction.deposit(...depositParams);
const depositData = await spokePool_1.populateTransaction.depositDeprecated_5947912356(...depositParams);
await spokePool_1.connect(depositor).multicall(Array(3).fill(depositData.data));
expect(await spokePool_1.numberOfDeposits()).to.equal(5);
const depositEvents = await spokePool_1.queryFilter("V3FundsDeposited");
const depositEvents = await spokePool_1.queryFilter("FundsDeposited");

// Set fromBlock to block later than deposits.
spokePoolClient1.latestBlockSearched = await spokePool_1.provider.getBlockNumber();
Expand Down Expand Up @@ -683,7 +687,12 @@ describe("SpokePoolClient: Fill Validation", function () {
const fill_1 = await fillV3Relay(spokePool_2, deposit_1, relayer);
const fill_2 = await fillV3Relay(
spokePool_2,
{ ...deposit_1, recipient: relayer.address, outputAmount: deposit_1.outputAmount.div(2), message: "0x12" },
{
...deposit_1,
recipient: toBytes32(relayer.address),
outputAmount: deposit_1.outputAmount.div(2),
message: "0x12",
},
relayer
);

Expand All @@ -693,10 +702,10 @@ describe("SpokePoolClient: Fill Validation", function () {
throw new Error("fill_2 is undefined");
}

expect(fill_1.relayExecutionInfo.updatedRecipient === depositor.address).to.be.true;
expect(fill_2.relayExecutionInfo.updatedRecipient === relayer.address).to.be.true;
expect(fill_2.relayExecutionInfo.updatedMessage === "0x12").to.be.true;
expect(fill_1.relayExecutionInfo.updatedMessage === "0x").to.be.true;
expect(fill_1.relayExecutionInfo.updatedRecipient === toBytes32(depositor.address)).to.be.true;
expect(fill_2.relayExecutionInfo.updatedRecipient === toBytes32(relayer.address)).to.be.true;
expect(fill_2.relayExecutionInfo.updatedMessageHash === ethers.utils.keccak256("0x12")).to.be.true;
expect(fill_1.relayExecutionInfo.updatedMessageHash === ZERO_BYTES).to.be.true;
expect(fill_1.relayExecutionInfo.updatedOutputAmount.eq(fill_2.relayExecutionInfo.updatedOutputAmount)).to.be.false;
expect(fill_1.relayExecutionInfo.fillType === FillType.FastFill).to.be.true;
expect(fill_2.relayExecutionInfo.fillType === FillType.FastFill).to.be.true;
Expand Down
6 changes: 3 additions & 3 deletions test/SpokePoolClient.fills.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import hre from "hardhat";
import { SpokePoolClient } from "../src/clients";
import { Deposit } from "../src/interfaces";
import { bnOne, findFillBlock, getNetworkName } from "../src/utils";
import { bnOne, findFillBlock, getNetworkName, toBytes32 } from "../src/utils";
import { EMPTY_MESSAGE, ZERO_ADDRESS } from "../src/constants";
import { originChainId, destinationChainId } from "./constants";
import {
Expand Down Expand Up @@ -88,8 +88,8 @@ describe("SpokePoolClient: Fills", function () {

expect(spokePoolClient.getFillsForOriginChain(originChainId).length).to.equal(3);
expect(spokePoolClient.getFillsForOriginChain(originChainId2).length).to.equal(1);
expect(spokePoolClient.getFillsForRelayer(relayer1.address).length).to.equal(3);
expect(spokePoolClient.getFillsForRelayer(relayer2.address).length).to.equal(1);
expect(spokePoolClient.getFillsForRelayer(toBytes32(relayer1.address)).length).to.equal(3);
expect(spokePoolClient.getFillsForRelayer(toBytes32(relayer2.address)).length).to.equal(1);
});

it("Correctly locates the block number for a FilledV3Relay event", async function () {
Expand Down
Loading

0 comments on commit d41d0e0

Please sign in to comment.