diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 0aecc429..a31fedab 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -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); diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 383aaf76..0aab3eec 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -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); +} diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 5dd97aec..0f18aaa7 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -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"; @@ -143,7 +143,7 @@ export function isZeroValueDeposit(deposit: Pick 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 }; diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 23962bd0..a701d03a 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -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; @@ -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( @@ -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); @@ -268,7 +305,9 @@ export async function relayFillStatus( destinationChainId?: number ): Promise { 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); @@ -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. diff --git a/test/SpokePoolClient.SpeedUp.ts b/test/SpokePoolClient.SpeedUp.ts index 3a5f9cf8..1b6beba3 100644 --- a/test/SpokePoolClient.SpeedUp.ts +++ b/test/SpokePoolClient.SpeedUp.ts @@ -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, @@ -84,8 +84,8 @@ describe("SpokePoolClient: SpeedUp", function () { await spokePool .connect(depositor) - .speedUpV3Deposit( - depositor.address, + .speedUpDeposit( + toBytes32(depositor.address), deposit.depositId, updatedOutputAmount, updatedRecipient, @@ -146,8 +146,8 @@ describe("SpokePoolClient: SpeedUp", function () { await spokePool .connect(depositor) - .speedUpV3Deposit( - depositor.address, + .speedUpDeposit( + toBytes32(depositor.address), depositId, updatedOutputAmount, updatedRecipient, diff --git a/test/SpokePoolClient.ValidateFill.ts b/test/SpokePoolClient.ValidateFill.ts index f9c91a5d..73574a6b 100644 --- a/test/SpokePoolClient.ValidateFill.ts +++ b/test/SpokePoolClient.ValidateFill.ts @@ -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, @@ -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; @@ -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 @@ -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(); @@ -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 ); @@ -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; diff --git a/test/SpokePoolClient.fills.ts b/test/SpokePoolClient.fills.ts index 742dddf9..f3df6f13 100644 --- a/test/SpokePoolClient.fills.ts +++ b/test/SpokePoolClient.fills.ts @@ -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 { @@ -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 () { diff --git a/test/relayFeeCalculator.test.ts b/test/relayFeeCalculator.test.ts index 55543d9e..322737da 100644 --- a/test/relayFeeCalculator.test.ts +++ b/test/relayFeeCalculator.test.ts @@ -11,6 +11,7 @@ import { spreadEvent, isMessageEmpty, fixedPointAdjustment, + toBytes32, } from "../src/utils"; import { BigNumber, @@ -440,9 +441,9 @@ describe("RelayFeeCalculator: Composable Bridging", function () { }, 10 ); - const fillData = await spokePool.queryFilter(spokePool.filters.FilledV3Relay()); + const fillData = await spokePool.queryFilter(spokePool.filters.FilledRelay()); expect(fillData.length).to.eq(1); - const onlyMessages = fillData.filter((fill) => !isMessageEmpty(fill.args.message)); + const onlyMessages = fillData.filter((fill) => !isMessageEmpty(fill.args.messageHash)); expect(onlyMessages.length).to.eq(1); const relevantFill = onlyMessages[0]; const spreadFill = spreadEvent(relevantFill.args); @@ -451,8 +452,8 @@ describe("RelayFeeCalculator: Composable Bridging", function () { ...spreadFill.relayExecutionInfo, updatedOutputAmount: spreadFill.relayExecutionInfo.updatedOutputAmount.toString(), }).to.deep.eq({ - updatedRecipient: testContract.address, - updatedMessage: "0xabcdef", + updatedRecipient: toBytes32(testContract.address).toLowerCase(), + updatedMessageHash: ethers.utils.keccak256("0xabcdef"), updatedOutputAmount: "1", fillType: 0, }); diff --git a/test/utils/utils.ts b/test/utils/utils.ts index 569b37a3..2e917dd9 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -26,6 +26,7 @@ import { toBNWei, toWei, utf8ToHex, + toBytes32, } from "../../src/utils"; import { MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF, @@ -349,9 +350,8 @@ export async function depositV3( exclusivityDeadline, message ); - const [events, originChainId] = await Promise.all([ - spokePool.queryFilter(spokePool.filters.V3FundsDeposited()), + spokePool.queryFilter(spokePool.filters.FundsDeposited()), spokePool.chainId(), ]); @@ -392,9 +392,9 @@ export async function requestV3SlowFill( const destinationChainId = Number(await spokePool.chainId()); assert.notEqual(relayData.originChainId, destinationChainId); - await spokePool.connect(signer).requestV3SlowFill(relayData); + await spokePool.connect(signer).requestSlowFill(relayData); - const events = await spokePool.queryFilter(spokePool.filters.RequestedV3SlowFill()); + const events = await spokePool.queryFilter(spokePool.filters.RequestedSlowFill()); const lastEvent = events.at(-1); let args = lastEvent!.args; assert.exists(args); @@ -432,9 +432,17 @@ export async function fillV3Relay( const destinationChainId = Number(await spokePool.chainId()); assert.notEqual(deposit.originChainId, destinationChainId); - await spokePool.connect(signer).fillV3Relay(deposit, repaymentChainId ?? destinationChainId); - - const events = await spokePool.queryFilter(spokePool.filters.FilledV3Relay()); + // If the input deposit token has a bytes32 on any field, assume it is going to the new fillRelay + // spoke pool method. + // Should be 0x + 32 bytes, so a 2 + 64 = 66 length string. + const useFillRelayMethod = deposit.depositor.length === 66; + if (useFillRelayMethod) + await spokePool + .connect(signer) + .fillRelay(deposit, repaymentChainId ?? destinationChainId, toBytes32(signer.address)); + else await spokePool.connect(signer).fillV3Relay(deposit, repaymentChainId ?? destinationChainId); + + const events = await spokePool.queryFilter(spokePool.filters.FilledRelay()); const lastEvent = events.at(-1); let args = lastEvent!.args; assert.exists(args); @@ -452,7 +460,7 @@ export async function fillV3Relay( inputAmount: args.inputAmount, outputToken: args.outputToken, outputAmount: args.outputAmount, - message: args.message, + messageHash: args.messageHash, fillDeadline: args.fillDeadline, exclusivityDeadline: args.exclusivityDeadline, exclusiveRelayer: args.exclusiveRelayer, @@ -460,7 +468,7 @@ export async function fillV3Relay( repaymentChainId: Number(args.repaymentChainId), relayExecutionInfo: { updatedRecipient: args.relayExecutionInfo.updatedRecipient, - updatedMessage: args.relayExecutionInfo.updatedMessage, + updatedMessageHash: args.relayExecutionInfo.updatedMessageHash, updatedOutputAmount: args.relayExecutionInfo.updatedOutputAmount, fillType: args.relayExecutionInfo.fillType, },