From 9638ac90e9cdbdb43734422ff7cc866ed3de75b5 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 20 Jan 2025 12:19:56 -0600 Subject: [PATCH 01/23] improve: interpret depositIds as BigNumbers and add utility funcs Signed-off-by: bennett --- .../utils/SuperstructUtils.ts | 2 +- src/clients/SpokePoolClient.ts | 51 ++++++++------- src/clients/mocks/MockSpokePoolClient.ts | 25 ++++---- src/constants.ts | 2 +- src/interfaces/SpokePool.ts | 4 +- src/utils/AddressUtils.ts | 17 +++++ src/utils/DepositUtils.ts | 8 +-- src/utils/EventUtils.ts | 8 +++ src/utils/SpokeUtils.ts | 62 +++++++++++++++---- src/utils/ValidatorUtils.ts | 2 +- test/SpokePoolClient.ValidateFill.ts | 20 +++--- test/utils/utils.ts | 8 +-- test/validatorUtils.test.ts | 6 +- 13 files changed, 142 insertions(+), 73 deletions(-) diff --git a/src/clients/BundleDataClient/utils/SuperstructUtils.ts b/src/clients/BundleDataClient/utils/SuperstructUtils.ts index dd3462fef..c33e30f5f 100644 --- a/src/clients/BundleDataClient/utils/SuperstructUtils.ts +++ b/src/clients/BundleDataClient/utils/SuperstructUtils.ts @@ -41,7 +41,7 @@ const V3RelayDataSS = { originChainId: number(), depositor: string(), recipient: string(), - depositId: number(), + depositId: BigNumberType, message: string(), }; diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index ef1b3173f..5cbfb82e8 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -12,6 +12,7 @@ import { getRelayDataHash, isDefined, toBN, + bnOne, } from "../utils"; import { paginatedEventQuery, @@ -46,8 +47,8 @@ type SpokePoolUpdateSuccess = { success: true; currentTime: number; oldestTime: number; - firstDepositId: number; - latestDepositId: number; + firstDepositId: BigNumber; + latestDepositId: BigNumber; events: Log[][]; searchEndBlock: number; }; @@ -66,7 +67,7 @@ export class SpokePoolClient extends BaseAbstractClient { protected oldestTime = 0; protected depositHashes: { [depositHash: string]: DepositWithBlock } = {}; protected depositHashesToFills: { [depositHash: string]: FillWithBlock[] } = {}; - protected speedUps: { [depositorAddress: string]: { [depositId: number]: SpeedUpWithBlock[] } } = {}; + protected speedUps: { [depositorAddress: string]: { [depositId: string]: SpeedUpWithBlock[] } } = {}; protected slowFillRequests: { [relayDataHash: string]: SlowFillRequestWithBlock } = {}; protected depositRoutes: { [originToken: string]: { [DestinationChainId: number]: boolean } } = {}; protected tokensBridged: TokensBridged[] = []; @@ -74,10 +75,10 @@ export class SpokePoolClient extends BaseAbstractClient { protected relayerRefundExecutions: RelayerRefundExecutionWithBlock[] = []; protected queryableEventNames: string[] = []; protected configStoreClient: AcrossConfigStoreClient | undefined; - public earliestDepositIdQueried = Number.MAX_SAFE_INTEGER; - public latestDepositIdQueried = 0; - public firstDepositIdForSpokePool = Number.MAX_SAFE_INTEGER; - public lastDepositIdForSpokePool = Number.MAX_SAFE_INTEGER; + public earliestDepositIdQueried = MAX_BIG_INT; + public latestDepositIdQueried = bnZero; + public firstDepositIdForSpokePool = MAX_BIG_INT; + public lastDepositIdForSpokePool = MAX_BIG_INT; public fills: { [OriginChainId: number]: FillWithBlock[] } = {}; /** @@ -232,7 +233,7 @@ export class SpokePoolClient extends BaseAbstractClient { */ public appendMaxSpeedUpSignatureToDeposit(deposit: DepositWithBlock): DepositWithBlock { const { depositId, depositor } = deposit; - const speedups = this.speedUps[depositor]?.[depositId]; + const speedups = this.speedUps[depositor]?.[depositId.toString()]; if (!isDefined(speedups) || speedups.length === 0) { return deposit; } @@ -265,7 +266,7 @@ export class SpokePoolClient extends BaseAbstractClient { * @param depositId The unique ID of the deposit being queried. * @returns The corresponding deposit if found, undefined otherwise. */ - public getDeposit(depositId: number): DepositWithBlock | undefined { + public getDeposit(depositId: BigNumber): DepositWithBlock | undefined { const depositHash = this.getDepositHash({ depositId, originChainId: this.chainId }); return this.depositHashes[depositHash]; } @@ -295,7 +296,7 @@ export class SpokePoolClient extends BaseAbstractClient { * Retrieves speed up requests grouped by depositor and depositId. * @returns A mapping of depositor addresses to deposit ids with their corresponding speed up requests. */ - public getSpeedUps(): { [depositorAddress: string]: { [depositId: number]: SpeedUpWithBlock[] } } { + public getSpeedUps(): { [depositorAddress: string]: { [depositId: string]: SpeedUpWithBlock[] } } { return this.speedUps; } @@ -365,7 +366,7 @@ export class SpokePoolClient extends BaseAbstractClient { ); // Log any invalid deposits with same deposit id but different params. - const invalidFillsForDeposit = invalidFills.filter((x) => x.depositId === deposit.depositId); + const invalidFillsForDeposit = invalidFills.filter((x) => x.depositId.eq(deposit.depositId)); if (invalidFillsForDeposit.length > 0) { this.logger.warn({ at: "SpokePoolClient", @@ -396,8 +397,8 @@ export class SpokePoolClient extends BaseAbstractClient { * @note This hash is used to match deposits and fills together. * @note This hash takes the form of: `${depositId}-${originChainId}`. */ - public getDepositHash(event: { depositId: number; originChainId: number }): string { - return `${event.depositId}-${event.originChainId}`; + public getDepositHash(event: { depositId: BigNumber; originChainId: number }): string { + return `${event.depositId.toString()}-${event.originChainId}`; } /** @@ -405,7 +406,7 @@ export class SpokePoolClient extends BaseAbstractClient { * @param blockTag The block number to search for the deposit ID at. * @returns The deposit ID. */ - public _getDepositIdAtBlock(blockTag: number): Promise { + public _getDepositIdAtBlock(blockTag: number): Promise { return getDepositIdAtBlock(this.spokePool as SpokePool, blockTag); } @@ -438,10 +439,11 @@ export class SpokePoolClient extends BaseAbstractClient { */ protected async _update(eventsToQuery: string[]): Promise { // Find the earliest known depositId. This assumes no deposits were placed in the deployment block. - let firstDepositId: number = this.firstDepositIdForSpokePool; - if (firstDepositId === Number.MAX_SAFE_INTEGER) { + let firstDepositId = this.firstDepositIdForSpokePool; + if (firstDepositId.eq(MAX_BIG_INT)) { firstDepositId = await this.spokePool.numberOfDeposits({ blockTag: this.deploymentBlock }); - if (isNaN(firstDepositId) || firstDepositId < 0) { + firstDepositId = BigNumber.from(firstDepositId); // Cast input to a big number. + if (!BigNumber.isBigNumber(firstDepositId) || firstDepositId.lt(bnZero)) { throw new Error(`SpokePoolClient::update: Invalid first deposit id (${firstDepositId})`); } } @@ -491,9 +493,10 @@ export class SpokePoolClient extends BaseAbstractClient { ]); this.log("debug", `Time to query new events from RPC for ${this.chainId}: ${Date.now() - timerStart} ms`); - const [currentTime, numberOfDeposits] = multicallFunctions.map( + const [currentTime, _numberOfDeposits] = multicallFunctions.map( (fn, idx) => spokePool.interface.decodeFunctionResult(fn, multicallOutput[idx])[0] ); + const _latestDepositId = BigNumber.from(_numberOfDeposits).sub(bnOne); if (!BigNumber.isBigNumber(currentTime) || currentTime.lt(this.currentTime)) { const errMsg = BigNumber.isBigNumber(currentTime) @@ -510,7 +513,7 @@ export class SpokePoolClient extends BaseAbstractClient { currentTime: currentTime.toNumber(), // uint32 oldestTime: oldestTime.toNumber(), firstDepositId, - latestDepositId: Math.max(numberOfDeposits - 1, 0), + latestDepositId: _latestDepositId > bnZero ? _latestDepositId : bnZero, searchEndBlock: searchConfig.toBlock, events, }; @@ -579,10 +582,10 @@ export class SpokePoolClient extends BaseAbstractClient { } assign(this.depositHashes, [this.getDepositHash(deposit)], deposit); - if (deposit.depositId < this.earliestDepositIdQueried) { + if (deposit.depositId.lt(this.earliestDepositIdQueried)) { this.earliestDepositIdQueried = deposit.depositId; } - if (deposit.depositId > this.latestDepositIdQueried) { + if (deposit.depositId.gt(this.latestDepositIdQueried)) { this.latestDepositIdQueried = deposit.depositId; } } @@ -594,7 +597,7 @@ export class SpokePoolClient extends BaseAbstractClient { for (const event of speedUpEvents) { const speedUp = { ...spreadEventWithBlockNumber(event), originChainId: this.chainId } as SpeedUpWithBlock; - assign(this.speedUps, [speedUp.depositor, speedUp.depositId], [speedUp]); + assign(this.speedUps, [speedUp.depositor, speedUp.depositId.toString()], [speedUp]); // Find deposit hash matching this speed up event and update the deposit data associated with the hash, // if the hash+data exists. @@ -778,7 +781,7 @@ export class SpokePoolClient extends BaseAbstractClient { return this.oldestTime; } - async findDeposit(depositId: number, destinationChainId: number): Promise { + async findDeposit(depositId: BigNumber, destinationChainId: number): Promise { // Binary search for event search bounds. This way we can get the blocks before and after the deposit with // deposit ID = fill.depositId and use those blocks to optimize the search for that deposit. // Stop searches after a maximum # of searches to limit number of eth_call requests. Make an @@ -807,7 +810,7 @@ export class SpokePoolClient extends BaseAbstractClient { ); 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/clients/mocks/MockSpokePoolClient.ts b/src/clients/mocks/MockSpokePoolClient.ts index 7e9f2ed90..e9293fdcc 100644 --- a/src/clients/mocks/MockSpokePoolClient.ts +++ b/src/clients/mocks/MockSpokePoolClient.ts @@ -14,7 +14,7 @@ import { SlowFillLeaf, SpeedUp, } from "../../interfaces"; -import { toBN, toBNWei, getCurrentTime, randomAddress } from "../../utils"; +import { toBN, toBNWei, getCurrentTime, randomAddress, BigNumber, bnZero, bnOne } from "../../utils"; import { SpokePoolClient, SpokePoolUpdate } from "../SpokePoolClient"; import { HubPoolClient } from "../HubPoolClient"; import { EventManager, EventOverrides, getEventManager } from "./MockEvents"; @@ -26,8 +26,8 @@ export class MockSpokePoolClient extends SpokePoolClient { public eventManager: EventManager; private destinationTokenForChainOverride: Record = {}; // Allow tester to set the numberOfDeposits() returned by SpokePool at a block height. - public depositIdAtBlock: number[] = []; - public numberOfDeposits = 0; + public depositIdAtBlock: BigNumber[] = []; + public numberOfDeposits = bnZero; constructor( logger: winston.Logger, @@ -57,21 +57,21 @@ export class MockSpokePoolClient extends SpokePoolClient { this.latestBlockSearched = blockNumber; } - setDepositIds(_depositIds: number[]): void { + setDepositIds(_depositIds: BigNumber[]): void { this.depositIdAtBlock = []; if (_depositIds.length === 0) { return; } let lastDepositId = _depositIds[0]; for (let i = 0; i < _depositIds.length; i++) { - if (_depositIds[i] < lastDepositId) { + if (_depositIds[i].lt(lastDepositId)) { throw new Error("deposit ID must be equal to or greater than previous"); } this.depositIdAtBlock[i] = _depositIds[i]; lastDepositId = _depositIds[i]; } } - _getDepositIdAtBlock(blockTag: number): Promise { + _getDepositIdAtBlock(blockTag: number): Promise { return Promise.resolve(this.depositIdAtBlock[blockTag]); } @@ -92,17 +92,20 @@ export class MockSpokePoolClient extends SpokePoolClient { events[idx].push(event); } }); + const bnMax = (val: BigNumber, cmp: BigNumber) => { + return val.gt(cmp) ? val : cmp; + }; // Update latestDepositIdQueried. const idx = eventsToQuery.indexOf("V3FundsDeposited"); const latestDepositId = (events[idx] ?? []).reduce( - (depositId, event) => Math.max(depositId, (event.args["depositId"] ?? 0) as number), + (depositId, event) => bnMax(depositId, event.args["depositId"] ?? bnZero), this.latestDepositIdQueried ); return Promise.resolve({ success: true, - firstDepositId: 0, + firstDepositId: bnZero, latestDepositId, currentTime, oldestTime: 0, @@ -122,8 +125,8 @@ export class MockSpokePoolClient extends SpokePoolClient { const { blockNumber, transactionIndex } = deposit; let { depositId, depositor, destinationChainId, inputToken, inputAmount, outputToken, outputAmount } = deposit; depositId ??= this.numberOfDeposits; - assert(depositId >= this.numberOfDeposits, `${depositId} < ${this.numberOfDeposits}`); - this.numberOfDeposits = depositId + 1; + assert(depositId.gte(this.numberOfDeposits), `${depositId} < ${this.numberOfDeposits}`); + this.numberOfDeposits = depositId.add(bnOne); destinationChainId ??= random(1, 42161, false); depositor ??= randomAddress(); @@ -168,7 +171,7 @@ export class MockSpokePoolClient extends SpokePoolClient { const { blockNumber, transactionIndex } = fill; let { originChainId, depositId, inputToken, inputAmount, outputAmount, fillDeadline, relayer } = fill; originChainId ??= random(1, 42161, false); - depositId ??= random(1, 100_000, false); + depositId ??= BigNumber.from(random(1, 100_000, false)); inputToken ??= randomAddress(); inputAmount ??= toBNWei(random(1, 1000, false)); outputAmount ??= inputAmount; diff --git a/src/constants.ts b/src/constants.ts index 825791963..3bd2d6b7d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,7 +10,7 @@ export { TOKEN_SYMBOLS_MAP, } from "@across-protocol/constants"; -export const { AddressZero: ZERO_ADDRESS } = ethersConstants; +export const { AddressZero: ZERO_ADDRESS, HashZero: ZERO_BYTES } = ethersConstants; // 2^96 - 1 is a conservative erc20 max allowance. export const MAX_SAFE_ALLOWANCE = "79228162514264337593543950335"; diff --git a/src/interfaces/SpokePool.ts b/src/interfaces/SpokePool.ts index 9bccb8947..83ea6297a 100644 --- a/src/interfaces/SpokePool.ts +++ b/src/interfaces/SpokePool.ts @@ -10,7 +10,7 @@ export interface RelayData { originChainId: number; depositor: string; recipient: string; - depositId: number; + depositId: BigNumber; inputToken: string; inputAmount: BigNumber; outputToken: string; @@ -67,7 +67,7 @@ export interface FillWithBlock extends Fill, SortableEvent {} export interface SpeedUp { depositor: string; depositorSignature: string; - depositId: number; + depositId: BigNumber; originChainId: number; updatedRecipient: string; updatedOutputAmount: BigNumber; diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 383aaf76d..0aab3eec0 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 5dd97aec2..2f12ba2ba 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"; @@ -52,7 +52,7 @@ export async function queryHistoricalDepositForFill( const { depositId } = fill; let { firstDepositIdForSpokePool: lowId, lastDepositIdForSpokePool: highId } = spokePoolClient; - if (depositId < lowId || depositId > highId) { + if (depositId.lt(lowId) || depositId.gt(highId)) { return { found: false, code: InvalidFill.DepositIdInvalid, @@ -61,7 +61,7 @@ export async function queryHistoricalDepositForFill( } ({ earliestDepositIdQueried: lowId, latestDepositIdQueried: highId } = spokePoolClient); - if (depositId >= lowId && depositId <= highId) { + if (depositId.gte(lowId) && depositId.lte(highId)) { const originChain = getNetworkName(fill.originChainId); const deposit = spokePoolClient.getDeposit(depositId); if (isDefined(deposit)) { @@ -143,7 +143,7 @@ export function isZeroValueDeposit(deposit: Pick): { [key: str if (returnedObject.rootBundleId) { returnedObject.rootBundleId = Number(returnedObject.rootBundleId); } + // If depositId is included in the event, cast it to a BigNumber. Need to check if it is defined since the deposit ID can + // be 0, which would still make this evaluate as false. + if (isDefined(returnedObject.depositId)) { + // Assuming a numeric output, we can safely cast the unknown to BigNumberish since the depositId will either be a uint32 (and therefore a TypeScript `number`), + // or a uint256 (and therefore an ethers BigNumber). + returnedObject.depositId = toBN(returnedObject.depositId as BigNumberish); + } return returnedObject; } diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 23962bd08..63c631893 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,10 +1,12 @@ 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 { toBytes32 } from "./AddressUtils"; import { chunk } from "./ArrayUtils"; -import { BigNumber, toBN } from "./BigNumberUtils"; +import { BigNumber, toBN, bnOne, bnZero } from "./BigNumberUtils"; +import { isMessageEmpty } from "./DepositUtils"; import { isDefined } from "./TypeGuards"; import { getNetworkName } from "./NetworkUtils"; @@ -70,7 +72,7 @@ export function populateV3Relay( * // where the deposit with deposit ID = targetDepositId was created. */ export async function getBlockRangeForDepositId( - targetDepositId: number, + targetDepositId: BigNumber, initLow: number, initHigh: number, maxSearches: number, @@ -109,13 +111,13 @@ export async function getBlockRangeForDepositId( }); // Define a mapping of block numbers to number of deposits at that block. This saves repeated lookups. - const queriedIds: Record = {}; + const queriedIds: Record = {}; // Define a llambda function to get the deposit ID at a block number. This function will first check the // queriedIds cache to see if the deposit ID at the block number has already been queried. If not, it will // make an eth_call request to get the deposit ID at the block number. It will then cache the deposit ID // in the queriedIds cache. - const _getDepositIdAtBlock = async (blockNumber: number): Promise => { + const _getDepositIdAtBlock = async (blockNumber: number): Promise => { queriedIds[blockNumber] ??= await spokePool._getDepositIdAtBlock(blockNumber); return queriedIds[blockNumber]; }; @@ -128,7 +130,7 @@ export async function getBlockRangeForDepositId( // If the deposit ID at the initial high block is less than the target deposit ID, then we know that // the target deposit ID must be greater than the initial high block, so we can throw an error. - if (highestDepositIdInRange <= targetDepositId) { + if (highestDepositIdInRange.lte(targetDepositId)) { // initLow = 5: Deposits Num: 10 // // targetId = 11 <- fail (triggers this error) // 10 <= 11 // // targetId = 10 <- fail (triggers this error) // 10 <= 10 @@ -140,7 +142,7 @@ export async function getBlockRangeForDepositId( // If the deposit ID at the initial low block is greater than the target deposit ID, then we know that // the target deposit ID must be less than the initial low block, so we can throw an error. - if (lowestDepositIdInRange > targetDepositId) { + if (lowestDepositIdInRange.gt(targetDepositId)) { // initLow = 5: Deposits Num: 10 // initLow-1 = 4: Deposits Num: 2 // // targetId = 1 <- fail (triggers this error) @@ -154,7 +156,6 @@ export async function getBlockRangeForDepositId( // Define the low and high block numbers for the binary search. let low = initLow; let high = initHigh; - // Define the number of searches performed so far. let searches = 0; @@ -166,12 +167,12 @@ export async function getBlockRangeForDepositId( const midDepositId = await _getDepositIdAtBlock(mid); // Let's define the latest ID of the current midpoint block. - const accountedIdByMidBlock = midDepositId - 1; + const accountedIdByMidBlock = midDepositId.sub(bnOne); // If our target deposit ID is less than the smallest range of our // midpoint deposit ID range, then we know that the target deposit ID // must be in the lower half of the block range. - if (targetDepositId <= accountedIdByMidBlock) { + if (targetDepositId.lte(accountedIdByMidBlock)) { high = mid; } // If our target deposit ID is greater than the largest range of our @@ -200,10 +201,11 @@ export async function getBlockRangeForDepositId( * @param blockTag The block number to search for the deposit ID at. * @returns The deposit ID. */ -export async function getDepositIdAtBlock(contract: Contract, blockTag: number): Promise { - const depositIdAtBlock = await contract.numberOfDeposits({ blockTag }); +export async function getDepositIdAtBlock(contract: Contract, blockTag: number): Promise { + const _depositIdAtBlock = await contract.numberOfDeposits({ blockTag }); + const depositIdAtBlock = toBN(_depositIdAtBlock); // Sanity check to ensure that the deposit ID is an integer and is greater than or equal to zero. - if (!Number.isInteger(depositIdAtBlock) || depositIdAtBlock < 0) { + if (!BigNumber.isBigNumber(depositIdAtBlock) || depositIdAtBlock.lt(bnZero)) { throw new Error("Invalid deposit count"); } return depositIdAtBlock; @@ -380,3 +382,37 @@ export async function findFillBlock( return lowBlockNumber; } + +/* + * 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 convertToUpdatedRelayData(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), + }; +} + +// Determines if the input address (either a bytes32 or bytes20) is the zero address. +export function isZeroAddress(address: string): boolean { + return address === ZERO_ADDRESS || address === ZERO_BYTES; +} diff --git a/src/utils/ValidatorUtils.ts b/src/utils/ValidatorUtils.ts index c1a56e6d9..9806bab46 100644 --- a/src/utils/ValidatorUtils.ts +++ b/src/utils/ValidatorUtils.ts @@ -8,7 +8,7 @@ const HexValidator = define("HexValidator", (v) => ethersUtils.isHexStri const BigNumberValidator = define("BigNumberValidator", (v) => BigNumber.isBigNumber(v)); const V3DepositSchema = object({ - depositId: Min(integer(), 0), + depositId: BigNumberValidator, depositor: AddressValidator, recipient: AddressValidator, inputToken: AddressValidator, diff --git a/test/SpokePoolClient.ValidateFill.ts b/test/SpokePoolClient.ValidateFill.ts index f9c91a5d0..b342dacc0 100644 --- a/test/SpokePoolClient.ValidateFill.ts +++ b/test/SpokePoolClient.ValidateFill.ts @@ -2,6 +2,8 @@ import { DepositWithBlock, FillStatus, FillType } from "../src/interfaces"; import { SpokePoolClient } from "../src/clients"; import { bnOne, + bnZero, + toBN, InvalidFill, fillStatusArray, relayFillStatus, @@ -299,7 +301,7 @@ describe("SpokePoolClient: Fill Validation", function () { // Throws when low < high await assertPromiseError( - getBlockRangeForDepositId(0, 1, 0, 10, spokePoolClient1), + getBlockRangeForDepositId(bnZero, 1, 0, 10, spokePoolClient1), "Binary search failed because low > high" ); @@ -309,7 +311,7 @@ describe("SpokePoolClient: Fill Validation", function () { // Searching for deposit ID 0 with 10 max searches should return the block range that deposit ID 0 was mined in. // Note: the search range is inclusive, so the range should include the block that deposit ID 0 was mined in. const searchRange0 = await getBlockRangeForDepositId( - 0, + bnZero, spokePool1DeploymentBlock, spokePoolClient1.latestBlockSearched, 10, @@ -326,7 +328,7 @@ describe("SpokePoolClient: Fill Validation", function () { // Where correct block is the block that the deposit ID incremented to the target. // So the correct block for deposit ID 1 is the block that deposit ID 0 was mined in. const searchRange1 = await getBlockRangeForDepositId( - 1, + bnOne, spokePool1DeploymentBlock, spokePoolClient1.latestBlockSearched, 10, @@ -339,7 +341,7 @@ describe("SpokePoolClient: Fill Validation", function () { // Searching for deposit ID 2 that doesn't exist yet should throw. await assertPromiseError( getBlockRangeForDepositId( - 2, + toBN(2), spokePool1DeploymentBlock, spokePoolClient1.latestBlockSearched, 10, @@ -381,14 +383,14 @@ describe("SpokePoolClient: Fill Validation", function () { // will never equal any of the target IDs (e.g. 3,4,5) because multiple deposits were mined in the same block, // incrementing numberOfDeposits() atomically from 2 to 6. const searchRange3 = await getBlockRangeForDepositId( - 3, + toBN(3), spokePool1DeploymentBlock, spokePoolClient1.latestBlockSearched, 10, spokePoolClient1 ); const searchRange4 = await getBlockRangeForDepositId( - 4, + toBN(4), spokePool1DeploymentBlock, spokePoolClient1.latestBlockSearched, 10, @@ -397,7 +399,7 @@ describe("SpokePoolClient: Fill Validation", function () { await assertPromiseError( getBlockRangeForDepositId( - 5, + toBN(5), spokePool1DeploymentBlock, spokePoolClient1.latestBlockSearched, 10, @@ -436,7 +438,7 @@ describe("SpokePoolClient: Fill Validation", function () { const increment = Math.max(0, Math.floor((Math.random() - 0.5) * 10)); depositIds[i] = depositIds[i - 1] + increment; } - fuzzClient.setDepositIds(depositIds); + fuzzClient.setDepositIds(depositIds.map(toBN)); fuzzClient.setLatestBlockNumber(initHigh + 1); for (let i = 0; i < testIterations; i++) { @@ -445,7 +447,7 @@ describe("SpokePoolClient: Fill Validation", function () { // Randomize max # of searches. const maxSearches = Math.floor(Math.random() * 19) + 1; - const results = await getBlockRangeForDepositId(target, initLow, initHigh, maxSearches, fuzzClient); + const results = await getBlockRangeForDepositId(toBN(target), initLow, initHigh, maxSearches, fuzzClient); // The "correct" block is the first block whose previous block's deposit ID is greater than // or equal to the target and whose deposit ID count is greater than the target. diff --git a/test/utils/utils.ts b/test/utils/utils.ts index 569b37a37..08e4782e3 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -362,7 +362,7 @@ export async function depositV3( const { blockNumber, transactionHash, transactionIndex, logIndex } = lastEvent!; return { - depositId: args!.depositId, + depositId: toBN(args!.depositId), originChainId: Number(originChainId), destinationChainId: Number(args!.destinationChainId), depositor: args!.depositor, @@ -403,7 +403,7 @@ export async function requestV3SlowFill( const { blockNumber, transactionHash, transactionIndex, logIndex } = lastEvent!; return { - depositId: args.depositId, + depositId: toBN(args.depositId), originChainId: Number(args.originChainId), destinationChainId, depositor: args.depositor, @@ -443,7 +443,7 @@ export async function fillV3Relay( const { blockNumber, transactionHash, transactionIndex, logIndex } = lastEvent!; return { - depositId: args.depositId, + depositId: toBN(args.depositId), originChainId: Number(args.originChainId), destinationChainId, depositor: args.depositor, @@ -539,7 +539,7 @@ export function buildDepositForRelayerFeeTest( const currentTime = getCurrentTime(); return { - depositId: bnUint32Max.toNumber(), + depositId: bnUint32Max, originChainId: 1, destinationChainId: 10, depositor: randomAddress(), diff --git a/test/validatorUtils.test.ts b/test/validatorUtils.test.ts index 8d77225d6..05eff465c 100644 --- a/test/validatorUtils.test.ts +++ b/test/validatorUtils.test.ts @@ -3,7 +3,7 @@ import { utils, interfaces } from "../src"; import { ZERO_ADDRESS } from "../src/constants"; import { randomAddress, toBN } from "./utils"; import { cloneDeep } from "lodash"; -import { objectWithBigNumberReviver } from "../src/utils"; +import { objectWithBigNumberReviver, bnOne } from "../src/utils"; import { Deposit } from "../src/interfaces"; describe("validatorUtils", () => { @@ -11,7 +11,7 @@ describe("validatorUtils", () => { let deposit: interfaces.DepositWithBlock; beforeEach(() => { deposit = { - depositId: 1, + depositId: bnOne, depositor: ZERO_ADDRESS, destinationChainId: 1, originChainId: 1, @@ -90,7 +90,7 @@ describe("validatorUtils", () => { }); it("should successfully rehydrate real deposits", () => { const deposits: string[] = [ - '{"inputAmount":{"type":"BigNumber","hex":"0x038d7ea4c68000"},"outputAmount":{"type":"BigNumber","hex":"0x038d7ea4c68000"},"originChainId":42161,"destinationChainId":10,"exclusivityDeadline":1697088000,"fillDeadline":1697088000,"depositId":1160366,"quoteTimestamp":1697088000,"inputToken":"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1","recipient":"0x525D59654479cFaED622C1Ca06f237ce1072c2AB","depositor":"0x269727F088F16E1Aea52Cf5a97B1CD41DAA3f02D","exclusiveRelayer":"0x269727F088F16E1Aea52Cf5a97B1CD41DAA3f02D","message":"0x","blockNumber":139874261,"transactionIndex":1,"logIndex":1,"transactionHash":"0x4c4273f4cceb288a76aa7d6c057a8e3ab571a19711a59a965726e06b04e6b821","realizedLpFeePct":{"type":"BigNumber","hex":"0x016b90ac8ef5b9"},"outputToken":"0x4200000000000000000000000000000000000006","quoteBlockNumber":18332204}', + '{"inputAmount":{"type":"BigNumber","hex":"0x038d7ea4c68000"},"outputAmount":{"type":"BigNumber","hex":"0x038d7ea4c68000"},"originChainId":42161,"destinationChainId":10,"exclusivityDeadline":1697088000,"fillDeadline":1697088000,"depositId":{"type":"BigNumber","hex":"0x11b4ae"},"quoteTimestamp":1697088000,"inputToken":"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1","recipient":"0x525D59654479cFaED622C1Ca06f237ce1072c2AB","depositor":"0x269727F088F16E1Aea52Cf5a97B1CD41DAA3f02D","exclusiveRelayer":"0x269727F088F16E1Aea52Cf5a97B1CD41DAA3f02D","message":"0x","blockNumber":139874261,"transactionIndex":1,"logIndex":1,"transactionHash":"0x4c4273f4cceb288a76aa7d6c057a8e3ab571a19711a59a965726e06b04e6b821","realizedLpFeePct":{"type":"BigNumber","hex":"0x016b90ac8ef5b9"},"outputToken":"0x4200000000000000000000000000000000000006","quoteBlockNumber":18332204}', ]; const rehydratedDeposits = deposits.map((d) => JSON.parse(d, objectWithBigNumberReviver) as Deposit); expect(rehydratedDeposits.every(utils.isDepositFormedCorrectly)).to.be.true; From 7dcd6b8f3b3a8b291b979d11a1e465462263386a Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 20 Jan 2025 12:46:30 -0600 Subject: [PATCH 02/23] stringify logs Signed-off-by: bennett --- src/clients/SpokePoolClient.ts | 4 ++-- src/utils/DepositUtils.ts | 6 +++--- src/utils/SpokeUtils.ts | 12 +++++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 5cbfb82e8..c06f3487c 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -314,7 +314,7 @@ export class SpokePoolClient extends BaseAbstractClient { this.logger.debug({ at: "SpokePoolClient::getDepositForFill", - message: `Rejected fill for ${getNetworkName(fill.originChainId)} deposit ${fill.depositId}.`, + message: `Rejected fill for ${getNetworkName(fill.originChainId)} deposit ${fill.depositId.toString()}.`, reason: match.reason, deposit, fill, @@ -815,7 +815,7 @@ export class SpokePoolClient extends BaseAbstractClient { const srcChain = getNetworkName(this.chainId); const dstChain = getNetworkName(destinationChainId); throw new Error( - `Could not find deposit ${depositId} for ${dstChain} fill` + + `Could not find deposit ${depositId.toString()} for ${dstChain} fill` + ` between ${srcChain} blocks [${searchBounds.low}, ${searchBounds.high}]` ); } diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 2f12ba2ba..1e8a6142a 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -56,7 +56,7 @@ export async function queryHistoricalDepositForFill( return { found: false, code: InvalidFill.DepositIdInvalid, - reason: `Deposit ID ${depositId} is outside of SpokePool bounds [${lowId},${highId}].`, + reason: `Deposit ID ${depositId.toString()} is outside of SpokePool bounds [${lowId},${highId}].`, }; } @@ -73,14 +73,14 @@ export async function queryHistoricalDepositForFill( return { found: false, code: InvalidFill.FillMismatch, - reason: `Fill for ${originChain} deposit ID ${depositId} is invalid (${match.reason}).`, + reason: `Fill for ${originChain} deposit ID ${depositId.toString()} is invalid (${match.reason}).`, }; } return { found: false, code: InvalidFill.DepositIdNotFound, - reason: `${originChain} deposit ID ${depositId} not found in SpokePoolClient event buffer.`, + reason: `${originChain} deposit ID ${depositId.toString()} not found in SpokePoolClient event buffer.`, }; } diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 63c631893..c97676494 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -136,7 +136,7 @@ export async function getBlockRangeForDepositId( // // targetId = 10 <- fail (triggers this error) // 10 <= 10 // // targetId = 09 <- pass (does not trigger this error) // 10 <= 09 throw new Error( - `Target depositId is greater than the initial high block (${targetDepositId} > ${highestDepositIdInRange})` + `Target depositId is greater than the initial high block (${targetDepositId.toString()} > ${highestDepositIdInRange.toString()})` ); } @@ -149,7 +149,7 @@ export async function getBlockRangeForDepositId( // // targetId = 2 <- pass (does not trigger this error) // // targetId = 3 <- pass (does not trigger this error) throw new Error( - `Target depositId is less than the initial low block (${targetDepositId} > ${lowestDepositIdInRange})` + `Target depositId is less than the initial low block (${targetDepositId.toString()} > ${lowestDepositIdInRange.toString()})` ); } @@ -246,7 +246,7 @@ export function getRelayHashFromEvent(e: Deposit | Fill | SlowFillRequest): stri return getRelayDataHash(e, e.destinationChainId); } -export function isUnsafeDepositId(depositId: number): boolean { +export function isUnsafeDepositId(depositId: BigNumber): boolean { // SpokePool.unsafeDepositV3() produces a uint256 depositId by hashing the msg.sender, depositor and input // uint256 depositNonce. There is a possibility that this resultant uint256 is less than the maxSafeDepositId (i.e. // the maximum uint32 value) which makes it possible that an unsafeDepositV3's depositId can collide with a safe @@ -276,7 +276,9 @@ export async function relayFillStatus( if (![FillStatus.Unfilled, FillStatus.RequestedSlowFill, FillStatus.Filled].includes(fillStatus)) { const { originChainId, depositId } = relayData; - throw new Error(`relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId} (${fillStatus})`); + throw new Error( + `relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId.toString()} (${fillStatus})` + ); } return fillStatus; @@ -365,7 +367,7 @@ export async function findFillBlock( if (initialFillStatus === FillStatus.Filled) { const { depositId, originChainId } = relayData; const [srcChain, dstChain] = [getNetworkName(originChainId), getNetworkName(destinationChainId)]; - throw new Error(`${srcChain} deposit ${depositId} filled on ${dstChain} before block ${lowBlockNumber}`); + throw new Error(`${srcChain} deposit ${depositId.toString()} filled on ${dstChain} before block ${lowBlockNumber}`); } // Find the leftmost block where filledAmount equals the deposit amount. From cc922c05b344bbc7e5a64f9cfb01b3e717d25a0e Mon Sep 17 00:00:00 2001 From: bmzig <57361391+bmzig@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:07:48 -0600 Subject: [PATCH 03/23] Update src/clients/SpokePoolClient.ts Co-authored-by: Dong-Ha Kim --- src/clients/SpokePoolClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index c06f3487c..8a6cb425d 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -513,7 +513,7 @@ export class SpokePoolClient extends BaseAbstractClient { currentTime: currentTime.toNumber(), // uint32 oldestTime: oldestTime.toNumber(), firstDepositId, - latestDepositId: _latestDepositId > bnZero ? _latestDepositId : bnZero, + latestDepositId: _latestDepositId.gt(bnZero) ? _latestDepositId : bnZero, searchEndBlock: searchConfig.toBlock, events, }; From 8194b366af4878fccb549e2afd9919bdf76a61b1 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 20 Jan 2025 16:10:44 -0600 Subject: [PATCH 04/23] Revert "stringify logs" This reverts commit 7dcd6b8f3b3a8b291b979d11a1e465462263386a. --- src/clients/SpokePoolClient.ts | 4 ++-- src/utils/DepositUtils.ts | 6 +++--- src/utils/SpokeUtils.ts | 12 +++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 8a6cb425d..5815ffbd0 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -314,7 +314,7 @@ export class SpokePoolClient extends BaseAbstractClient { this.logger.debug({ at: "SpokePoolClient::getDepositForFill", - message: `Rejected fill for ${getNetworkName(fill.originChainId)} deposit ${fill.depositId.toString()}.`, + message: `Rejected fill for ${getNetworkName(fill.originChainId)} deposit ${fill.depositId}.`, reason: match.reason, deposit, fill, @@ -815,7 +815,7 @@ export class SpokePoolClient extends BaseAbstractClient { const srcChain = getNetworkName(this.chainId); const dstChain = getNetworkName(destinationChainId); throw new Error( - `Could not find deposit ${depositId.toString()} for ${dstChain} fill` + + `Could not find deposit ${depositId} for ${dstChain} fill` + ` between ${srcChain} blocks [${searchBounds.low}, ${searchBounds.high}]` ); } diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 1e8a6142a..2f12ba2ba 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -56,7 +56,7 @@ export async function queryHistoricalDepositForFill( return { found: false, code: InvalidFill.DepositIdInvalid, - reason: `Deposit ID ${depositId.toString()} is outside of SpokePool bounds [${lowId},${highId}].`, + reason: `Deposit ID ${depositId} is outside of SpokePool bounds [${lowId},${highId}].`, }; } @@ -73,14 +73,14 @@ export async function queryHistoricalDepositForFill( return { found: false, code: InvalidFill.FillMismatch, - reason: `Fill for ${originChain} deposit ID ${depositId.toString()} is invalid (${match.reason}).`, + reason: `Fill for ${originChain} deposit ID ${depositId} is invalid (${match.reason}).`, }; } return { found: false, code: InvalidFill.DepositIdNotFound, - reason: `${originChain} deposit ID ${depositId.toString()} not found in SpokePoolClient event buffer.`, + reason: `${originChain} deposit ID ${depositId} not found in SpokePoolClient event buffer.`, }; } diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index c97676494..63c631893 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -136,7 +136,7 @@ export async function getBlockRangeForDepositId( // // targetId = 10 <- fail (triggers this error) // 10 <= 10 // // targetId = 09 <- pass (does not trigger this error) // 10 <= 09 throw new Error( - `Target depositId is greater than the initial high block (${targetDepositId.toString()} > ${highestDepositIdInRange.toString()})` + `Target depositId is greater than the initial high block (${targetDepositId} > ${highestDepositIdInRange})` ); } @@ -149,7 +149,7 @@ export async function getBlockRangeForDepositId( // // targetId = 2 <- pass (does not trigger this error) // // targetId = 3 <- pass (does not trigger this error) throw new Error( - `Target depositId is less than the initial low block (${targetDepositId.toString()} > ${lowestDepositIdInRange.toString()})` + `Target depositId is less than the initial low block (${targetDepositId} > ${lowestDepositIdInRange})` ); } @@ -246,7 +246,7 @@ export function getRelayHashFromEvent(e: Deposit | Fill | SlowFillRequest): stri return getRelayDataHash(e, e.destinationChainId); } -export function isUnsafeDepositId(depositId: BigNumber): boolean { +export function isUnsafeDepositId(depositId: number): boolean { // SpokePool.unsafeDepositV3() produces a uint256 depositId by hashing the msg.sender, depositor and input // uint256 depositNonce. There is a possibility that this resultant uint256 is less than the maxSafeDepositId (i.e. // the maximum uint32 value) which makes it possible that an unsafeDepositV3's depositId can collide with a safe @@ -276,9 +276,7 @@ export async function relayFillStatus( if (![FillStatus.Unfilled, FillStatus.RequestedSlowFill, FillStatus.Filled].includes(fillStatus)) { const { originChainId, depositId } = relayData; - throw new Error( - `relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId.toString()} (${fillStatus})` - ); + throw new Error(`relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId} (${fillStatus})`); } return fillStatus; @@ -367,7 +365,7 @@ export async function findFillBlock( if (initialFillStatus === FillStatus.Filled) { const { depositId, originChainId } = relayData; const [srcChain, dstChain] = [getNetworkName(originChainId), getNetworkName(destinationChainId)]; - throw new Error(`${srcChain} deposit ${depositId.toString()} filled on ${dstChain} before block ${lowBlockNumber}`); + throw new Error(`${srcChain} deposit ${depositId} filled on ${dstChain} before block ${lowBlockNumber}`); } // Find the leftmost block where filledAmount equals the deposit amount. From 27108576642c72324fc72db58c64ec905b147cb4 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 20 Jan 2025 16:11:14 -0600 Subject: [PATCH 05/23] update isUnsafeDepositId Signed-off-by: bennett --- src/utils/SpokeUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 63c631893..b4254de76 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -246,7 +246,7 @@ export function getRelayHashFromEvent(e: Deposit | Fill | SlowFillRequest): stri return getRelayDataHash(e, e.destinationChainId); } -export function isUnsafeDepositId(depositId: number): boolean { +export function isUnsafeDepositId(depositId: BigNumber): boolean { // SpokePool.unsafeDepositV3() produces a uint256 depositId by hashing the msg.sender, depositor and input // uint256 depositNonce. There is a possibility that this resultant uint256 is less than the maxSafeDepositId (i.e. // the maximum uint32 value) which makes it possible that an unsafeDepositV3's depositId can collide with a safe From 83c4c935391c7bb21fb403b5a9ceb1cef55ba23a Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 20 Jan 2025 16:12:37 -0600 Subject: [PATCH 06/23] fix isMessageEmpty Signed-off-by: bennett --- src/utils/DepositUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 2f12ba2ba..7409a2190 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -143,7 +143,7 @@ export function isZeroValueDeposit(deposit: Pick Date: Mon, 20 Jan 2025 16:33:11 -0600 Subject: [PATCH 07/23] add bounds for binary searches Signed-off-by: bennett --- src/clients/SpokePoolClient.ts | 5 +++-- src/utils/DepositUtils.ts | 2 +- src/utils/SpokeUtils.ts | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/clients/SpokePoolClient.ts b/src/clients/SpokePoolClient.ts index 5815ffbd0..12d70b669 100644 --- a/src/clients/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient.ts @@ -13,6 +13,7 @@ import { isDefined, toBN, bnOne, + isUnsafeDepositId, } from "../utils"; import { paginatedEventQuery, @@ -582,10 +583,10 @@ export class SpokePoolClient extends BaseAbstractClient { } assign(this.depositHashes, [this.getDepositHash(deposit)], deposit); - if (deposit.depositId.lt(this.earliestDepositIdQueried)) { + if (deposit.depositId.lt(this.earliestDepositIdQueried) && !isUnsafeDepositId(deposit.depositId)) { this.earliestDepositIdQueried = deposit.depositId; } - if (deposit.depositId.gt(this.latestDepositIdQueried)) { + if (deposit.depositId.gt(this.latestDepositIdQueried) && !isUnsafeDepositId(deposit.depositId)) { this.latestDepositIdQueried = deposit.depositId; } } diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 7409a2190..06d61e81b 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, ZERO_BYTES } from "../constants"; +import { DEFAULT_CACHING_TTL, EMPTY_MESSAGE } from "../constants"; import { CachingMechanismInterface, Deposit, DepositWithBlock, Fill, SlowFillRequest } from "../interfaces"; import { getNetworkName } from "./NetworkUtils"; import { getDepositInCache, getDepositKey, setDepositInCache } from "./CachingUtils"; diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index b4254de76..abe5aa094 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -81,6 +81,12 @@ export async function getBlockRangeForDepositId( low: number; high: number; }> { + // We can only perform this search when we have a safe deposit ID. + if (isUnsafeDepositId(targetDepositId)) + throw new Error( + `Target deposit ID ${targetDepositId} is deterministic and therefore unresolvable via a binary search.` + ); + // Resolve the deployment block number. const deploymentBlock = spokePool.deploymentBlock; From 58d24d66b2874b34b24a2dfd675c563f6ecb3899 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 21 Jan 2025 09:27:01 -0600 Subject: [PATCH 08/23] bump arweave version Signed-off-by: bennett --- src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index 3bd2d6b7d..a424d3fd8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,7 +33,7 @@ export const TRANSFER_THRESHOLD_MAX_CONFIG_STORE_VERSION = 1; export const ARWEAVE_TAG_APP_NAME = "across-protocol"; // A hardcoded version number used, by default, to tag all Arweave records. -export const ARWEAVE_TAG_APP_VERSION = 2; +export const ARWEAVE_TAG_APP_VERSION = 3; /** * A default list of chain Ids that the protocol supports. This is outlined From a75bf3204ad08188c8fd2cdd8f950074b29db635 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 21 Jan 2025 15:09:10 -0600 Subject: [PATCH 09/23] feat: add updated UMIP invalidity cases Signed-off-by: bennett --- .../BundleDataClient/BundleDataClient.ts | 48 ++++++++++++++++-- src/utils/AddressUtils.ts | 9 ++++ src/utils/DepositUtils.ts | 4 ++ src/utils/NetworkUtils.ts | 50 +++++++++++++++++++ 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index fd992c95d..2bfffe599 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -14,6 +14,7 @@ import { ExpiredDepositsToRefundV3, Clients, CombinedRefunds, + DepositWithBlock, } from "../../interfaces"; import { AcrossConfigStoreClient, SpokePoolClient } from ".."; import { @@ -32,6 +33,8 @@ import { mapAsync, bnUint32Max, isZeroValueDeposit, + chainIsEvm, + isAddressBytes32, } from "../../utils"; import winston from "winston"; import { @@ -805,8 +808,17 @@ export class BundleDataClient { return; } + // A refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. + const refundIsValid = (deposit: DepositWithBlock) => { + return !(isAddressBytes32(deposit.depositor) && chainIsEvm(deposit.originChainId)); + }; + // If deposit block is within origin chain bundle block range, then save as bundle deposit. + // // If deposit is in bundle and it has expired, additionally save it as an expired deposit. + // Note: If the depositor has an invalid address on the origin chain, then it cannot be refunded, so it is instead + // ignored. + // // If deposit is not in the bundle block range, then save it as an older deposit that // may have expired. if (deposit.blockNumber >= originChainBlockRange[0] && deposit.blockNumber <= originChainBlockRange[1]) { @@ -816,7 +828,7 @@ export class BundleDataClient { // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 // for example. Those should be impossible to create but technically should be included in this // bundle of refunded deposits. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && refundIsValid(deposit)) { expiredBundleDepositHashes.add(relayDataHash); } } else if (deposit.blockNumber < originChainBlockRange[0]) { @@ -914,11 +926,37 @@ export class BundleDataClient { bundleInvalidFillsV3.push(fill); return; } + // If the fill's repayment address is not a valid EVM address and the repayment chain is an EVM chain, the fill is invalid. + if (chainIsEvm(fill.repaymentChainId) && isAddressBytes32(fill.relayer)) { + const transactionReceipt = await originClient.spokePool.provider.getTransaction(fill.transactionHash); + const originRelayer = transactionReceipt.from; + this.logger.debug({ + at: "BundleDataClient#loadData", + message: `${fill.relayer} is not a valid address on chain ${fill.repaymentChainId}. Falling back to ${originRelayer}.`, + }); + // Repayment chain is still an EVM chain, but the msg.sender is a bytes32 address, so the fill is invalid. + if (isAddressBytes32(originRelayer)) { + bundleInvalidFillsV3.push(fill); + return; + } + // Otherwise, assume the relayer to be repaid is the msg.sender. + fill.relayer = originRelayer; + } // If deposit is using the deterministic relay hash feature, then the following binary search-based - // algorithm will not work. However, it is impossible to emit an infinite fill deadline using - // the unsafeDepositV3 function so there is no need to catch the special case. - const historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); - if (!historicalDeposit.found) { + // algorithm will not work. + let historicalDeposit; + try { + historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); + } catch (e) { + // We will likely enter the exception block if we tried to query a historical deposit for a deterministic deposit ID. + this.logger.debug({ + at: "BundleDataClient#loadData", + message: "Could not binary search a historical deposit for fill.", + fill, + error: e, + }); + } + if (!isDefined(historicalDeposit) || !historicalDeposit.found) { bundleInvalidFillsV3.push(fill); } else { const matchedDeposit = historicalDeposit.deposit; diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 0aab3eec0..13aa4fe8f 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -55,3 +55,12 @@ export function toAddress(bytes32: string): string { const rawAddress = utils.hexZeroPad(utils.hexStripZeros(bytes32), 20); return utils.getAddress(rawAddress); } + +// Checks if an input address is a 32-byte address or not. +export function isAddressBytes32(address: string): boolean { + // If the address is not 32 bytes, then don't check. + if (utils.hexDataLength(address) !== 32) return false; + + const strippedAddress = utils.hexStripZeros(address); + return utils.isBytes(strippedAddress) && utils.hexDataLength(strippedAddress) > 20; +} diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 06d61e81b..b7a6f5e43 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -5,6 +5,7 @@ import { CachingMechanismInterface, Deposit, DepositWithBlock, Fill, SlowFillReq import { getNetworkName } from "./NetworkUtils"; import { getDepositInCache, getDepositKey, setDepositInCache } from "./CachingUtils"; import { validateFillForDeposit } from "./FlowUtils"; +import { isUnsafeDepositId } from "./SpokeUtils"; import { getCurrentTime } from "./TimeUtils"; import { isDefined } from "./TypeGuards"; import { isDepositFormedCorrectly } from "./ValidatorUtils"; @@ -40,6 +41,9 @@ export async function queryHistoricalDepositForFill( fill: Fill | SlowFillRequest, cache?: CachingMechanismInterface ): Promise { + if (isUnsafeDepositId(fill.depositId)) { + throw new Error(`Cannot find historical deposit for fill with unsafe deposit ID ${fill.depositId}`); + } if (fill.originChainId !== spokePoolClient.chainId) { throw new Error(`OriginChainId mismatch (${fill.originChainId} != ${spokePoolClient.chainId})`); } diff --git a/src/utils/NetworkUtils.ts b/src/utils/NetworkUtils.ts index 56b21ae8a..952772504 100644 --- a/src/utils/NetworkUtils.ts +++ b/src/utils/NetworkUtils.ts @@ -123,6 +123,56 @@ export function chainIsL1(chainId: number): boolean { return [CHAIN_IDs.MAINNET, CHAIN_IDs.SEPOLIA].includes(chainId); } +/** + * Determines whether a chain ID runs on an EVM-like execution layer. + * @param chainId Chain ID to evaluate. + * @returns True if chain corresponding to chainId has an EVM-like execution layer. + */ +export function chainIsEvm(chainId: number): boolean { + const { + ALEPH_ZERO, + ARBITRUM, + BASE, + BLAST, + BOBA, + INK, + LINEA, + LISK, + MAINNET, + MODE, + OPTIMISM, + POLYGON, + REDSTONE, + SCROLL, + SONEIUM, + SUPERSEED, + WORLD_CHAIN, + ZK_SYNC, + ZORA, + } = CHAIN_IDs; + return [ + ALEPH_ZERO, + ARBITRUM, + BASE, + BLAST, + BOBA, + INK, + LINEA, + LISK, + MAINNET, + MODE, + OPTIMISM, + POLYGON, + REDSTONE, + SCROLL, + SONEIUM, + SUPERSEED, + WORLD_CHAIN, + ZK_SYNC, + ZORA, + ].includes(chainId); +} + /** * Determines whether a chain ID has the capacity for having its USDC bridged via CCTP. * @param chainId Chain ID to evaluate. From 846bc86b3a1ed7be6f1813695f36e80332570ca4 Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 22 Jan 2025 12:38:52 -0600 Subject: [PATCH 10/23] union types Signed-off-by: bennett --- src/clients/BundleDataClient/utils/SuperstructUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clients/BundleDataClient/utils/SuperstructUtils.ts b/src/clients/BundleDataClient/utils/SuperstructUtils.ts index c33e30f5f..6ebe8c047 100644 --- a/src/clients/BundleDataClient/utils/SuperstructUtils.ts +++ b/src/clients/BundleDataClient/utils/SuperstructUtils.ts @@ -10,6 +10,7 @@ import { pattern, boolean, defaulted, + union, type, } from "superstruct"; import { BigNumber } from "../../../utils"; @@ -17,7 +18,7 @@ import { BigNumber } from "../../../utils"; const PositiveIntegerStringSS = pattern(string(), /\d+/); const Web3AddressSS = pattern(string(), /^0x[a-fA-F0-9]{40}$/); -const BigNumberType = coerce(instance(BigNumber), string(), (value) => { +const BigNumberType = coerce(instance(BigNumber), union([string(), number()]), (value) => { try { // Attempt to convert the string to a BigNumber return BigNumber.from(value); From 59582b35d542cf00c4fb60c71c0025b0e5e4e3fd Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 22 Jan 2025 13:42:16 -0600 Subject: [PATCH 11/23] remove unused function Signed-off-by: bennett --- src/utils/SpokeUtils.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index abe5aa094..d06353da9 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -401,23 +401,6 @@ export function isUpdatedRelayData(relayData: RelayData) { return isValidBytes32(relayData.depositor); } -/* - * Converts an input relay data to to the version with 32-byte address representations. - */ -export function convertToUpdatedRelayData(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), - }; -} - // Determines if the input address (either a bytes32 or bytes20) is the zero address. export function isZeroAddress(address: string): boolean { return address === ZERO_ADDRESS || address === ZERO_BYTES; From 9166127928272c912bc90675d24f617e6672ead7 Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 22 Jan 2025 13:46:58 -0600 Subject: [PATCH 12/23] lint Signed-off-by: bennett --- src/utils/SpokeUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index d06353da9..b6da9be4a 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -3,10 +3,8 @@ import { BytesLike, Contract, PopulatedTransaction, providers, utils as ethersUt 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 { toBytes32 } from "./AddressUtils"; import { chunk } from "./ArrayUtils"; import { BigNumber, toBN, bnOne, bnZero } from "./BigNumberUtils"; -import { isMessageEmpty } from "./DepositUtils"; import { isDefined } from "./TypeGuards"; import { getNetworkName } from "./NetworkUtils"; From aeb9a95db71a669e9d2fe3e77a17e174979c0c6c Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 22 Jan 2025 15:39:25 -0600 Subject: [PATCH 13/23] remove invalid deposit case and simplify checks Signed-off-by: bennett --- .../BundleDataClient/BundleDataClient.ts | 27 +++++-------------- src/utils/DepositUtils.ts | 7 ++++- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 2bfffe599..c7b63c6ba 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -14,7 +14,6 @@ import { ExpiredDepositsToRefundV3, Clients, CombinedRefunds, - DepositWithBlock, } from "../../interfaces"; import { AcrossConfigStoreClient, SpokePoolClient } from ".."; import { @@ -808,17 +807,8 @@ export class BundleDataClient { return; } - // A refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. - const refundIsValid = (deposit: DepositWithBlock) => { - return !(isAddressBytes32(deposit.depositor) && chainIsEvm(deposit.originChainId)); - }; - // If deposit block is within origin chain bundle block range, then save as bundle deposit. - // // If deposit is in bundle and it has expired, additionally save it as an expired deposit. - // Note: If the depositor has an invalid address on the origin chain, then it cannot be refunded, so it is instead - // ignored. - // // If deposit is not in the bundle block range, then save it as an older deposit that // may have expired. if (deposit.blockNumber >= originChainBlockRange[0] && deposit.blockNumber <= originChainBlockRange[1]) { @@ -828,7 +818,7 @@ export class BundleDataClient { // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 // for example. Those should be impossible to create but technically should be included in this // bundle of refunded deposits. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && refundIsValid(deposit)) { + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { expiredBundleDepositHashes.add(relayDataHash); } } else if (deposit.blockNumber < originChainBlockRange[0]) { @@ -928,8 +918,8 @@ export class BundleDataClient { } // If the fill's repayment address is not a valid EVM address and the repayment chain is an EVM chain, the fill is invalid. if (chainIsEvm(fill.repaymentChainId) && isAddressBytes32(fill.relayer)) { - const transactionReceipt = await originClient.spokePool.provider.getTransaction(fill.transactionHash); - const originRelayer = transactionReceipt.from; + const fillTransaction = await originClient.spokePool.provider.getTransaction(fill.transactionHash); + const originRelayer = fillTransaction.from; this.logger.debug({ at: "BundleDataClient#loadData", message: `${fill.relayer} is not a valid address on chain ${fill.repaymentChainId}. Falling back to ${originRelayer}.`, @@ -944,19 +934,14 @@ export class BundleDataClient { } // If deposit is using the deterministic relay hash feature, then the following binary search-based // algorithm will not work. - let historicalDeposit; - try { - historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); - } catch (e) { - // We will likely enter the exception block if we tried to query a historical deposit for a deterministic deposit ID. + const historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); + if (!historicalDeposit.found) { this.logger.debug({ at: "BundleDataClient#loadData", message: "Could not binary search a historical deposit for fill.", fill, - error: e, + reason: historicalDeposit.reason, }); - } - if (!isDefined(historicalDeposit) || !historicalDeposit.found) { bundleInvalidFillsV3.push(fill); } else { const matchedDeposit = historicalDeposit.deposit; diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index b7a6f5e43..96178bbf8 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -18,6 +18,7 @@ export enum InvalidFill { DepositIdInvalid = 0, // Deposit ID seems invalid for origin SpokePool DepositIdNotFound, // Deposit ID not found (bad RPC data?) FillMismatch, // Fill does not match deposit parameters for deposit ID. + DepositIdOutOfRange, // Fill is for a deterministic deposit. } export type DepositSearchResult = @@ -42,7 +43,11 @@ export async function queryHistoricalDepositForFill( cache?: CachingMechanismInterface ): Promise { if (isUnsafeDepositId(fill.depositId)) { - throw new Error(`Cannot find historical deposit for fill with unsafe deposit ID ${fill.depositId}`); + return { + found: false, + code: InvalidFill.DepositIdOutOfRange, + reason: `Cannot find historical deposit for fill with unsafe deposit ID ${fill.depositId}.`, + }; } if (fill.originChainId !== spokePoolClient.chainId) { throw new Error(`OriginChainId mismatch (${fill.originChainId} != ${spokePoolClient.chainId})`); From 03a7f28b859abb0b1935fb6b20d3d48806b34b26 Mon Sep 17 00:00:00 2001 From: bennett Date: Thu, 23 Jan 2025 10:39:57 -0600 Subject: [PATCH 14/23] remove unused function Signed-off-by: bennett --- src/utils/SpokeUtils.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index b6da9be4a..38fce434a 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -387,18 +387,6 @@ export async function findFillBlock( return lowBlockNumber; } -/* - * 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); -} - // Determines if the input address (either a bytes32 or bytes20) is the zero address. export function isZeroAddress(address: string): boolean { return address === ZERO_ADDRESS || address === ZERO_BYTES; From 5deb2f5b3215cab9c1fd330c383556fe0b6d9429 Mon Sep 17 00:00:00 2001 From: bennett Date: Thu, 23 Jan 2025 12:07:28 -0600 Subject: [PATCH 15/23] add back invalid deposits Signed-off-by: bennett --- .../BundleDataClient/BundleDataClient.ts | 10 ++++- src/utils/NetworkUtils.ts | 44 +------------------ 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index c7b63c6ba..11f55dd57 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -14,6 +14,7 @@ import { ExpiredDepositsToRefundV3, Clients, CombinedRefunds, + DepositWithBlock, } from "../../interfaces"; import { AcrossConfigStoreClient, SpokePoolClient } from ".."; import { @@ -807,8 +808,15 @@ export class BundleDataClient { return; } + // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. + const refundIsValid = (deposit: DepositWithBlock) => { + return !(isAddressBytes32(deposit.depositor) && chainIsEvm(deposit.originChainId)); + }; + // If deposit block is within origin chain bundle block range, then save as bundle deposit. // If deposit is in bundle and it has expired, additionally save it as an expired deposit. + // Note: if the `depositor` field in the expired deposit is an invalid address, e.g. a bytes32 address on an EVM + // chain, then the deposit refund is invalid and ignored. // If deposit is not in the bundle block range, then save it as an older deposit that // may have expired. if (deposit.blockNumber >= originChainBlockRange[0] && deposit.blockNumber <= originChainBlockRange[1]) { @@ -818,7 +826,7 @@ export class BundleDataClient { // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 // for example. Those should be impossible to create but technically should be included in this // bundle of refunded deposits. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && refundIsValid(deposit)) { expiredBundleDepositHashes.add(relayDataHash); } } else if (deposit.blockNumber < originChainBlockRange[0]) { diff --git a/src/utils/NetworkUtils.ts b/src/utils/NetworkUtils.ts index 952772504..685eddaec 100644 --- a/src/utils/NetworkUtils.ts +++ b/src/utils/NetworkUtils.ts @@ -129,48 +129,8 @@ export function chainIsL1(chainId: number): boolean { * @returns True if chain corresponding to chainId has an EVM-like execution layer. */ export function chainIsEvm(chainId: number): boolean { - const { - ALEPH_ZERO, - ARBITRUM, - BASE, - BLAST, - BOBA, - INK, - LINEA, - LISK, - MAINNET, - MODE, - OPTIMISM, - POLYGON, - REDSTONE, - SCROLL, - SONEIUM, - SUPERSEED, - WORLD_CHAIN, - ZK_SYNC, - ZORA, - } = CHAIN_IDs; - return [ - ALEPH_ZERO, - ARBITRUM, - BASE, - BLAST, - BOBA, - INK, - LINEA, - LISK, - MAINNET, - MODE, - OPTIMISM, - POLYGON, - REDSTONE, - SCROLL, - SONEIUM, - SUPERSEED, - WORLD_CHAIN, - ZK_SYNC, - ZORA, - ].includes(chainId); + const chainFamily = PUBLIC_NETWORKS[chainId].family; + return chainFamily === ChainFamily.NONE || chainFamily === ChainFamily.OP_STACK || chainFamily === ChainFamily.ORBIT || chainFamily === ChainFamily.ZK_STACK; } /** From bdaa603fb53c95641090caaf99d9172a03f3c616 Mon Sep 17 00:00:00 2001 From: bennett Date: Thu, 23 Jan 2025 12:18:33 -0600 Subject: [PATCH 16/23] update chainIsEvm Signed-off-by: bennett --- src/utils/NetworkUtils.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/utils/NetworkUtils.ts b/src/utils/NetworkUtils.ts index 685eddaec..ded7d43af 100644 --- a/src/utils/NetworkUtils.ts +++ b/src/utils/NetworkUtils.ts @@ -130,7 +130,40 @@ export function chainIsL1(chainId: number): boolean { */ export function chainIsEvm(chainId: number): boolean { const chainFamily = PUBLIC_NETWORKS[chainId].family; - return chainFamily === ChainFamily.NONE || chainFamily === ChainFamily.OP_STACK || chainFamily === ChainFamily.ORBIT || chainFamily === ChainFamily.ZK_STACK; + const checkEvmExceptionChains = (_chainId: number) => { + const { + MAINNET, + SEPOLIA, + ARBITRUM, + ARBITRUM_SEPOLIA, + LINEA, + POLYGON, + POLYGON_AMOY, + SCROLL, + SCROLL_SEPOLIA, + ZK_SYNC, + ZK_SYNC_SEPOLIA, + } = CHAIN_IDs; + return [ + MAINNET, + SEPOLIA, + ARBITRUM, + ARBITRUM_SEPOLIA, + LINEA, + POLYGON, + POLYGON_AMOY, + SCROLL, + SCROLL_SEPOLIA, + ZK_SYNC, + ZK_SYNC_SEPOLIA, + ].includes(_chainId); + }; + return ( + chainFamily === ChainFamily.OP_STACK || + chainFamily === ChainFamily.ORBIT || + chainFamily === ChainFamily.ZK_STACK || + checkEvmExceptionChains(chainId) + ); } /** From 59315b8a3694f2eb6f2f6c1de6b88d9bd47b9005 Mon Sep 17 00:00:00 2001 From: bennett Date: Thu, 23 Jan 2025 13:19:26 -0600 Subject: [PATCH 17/23] refactor deposit checks Signed-off-by: bennett --- .../BundleDataClient/BundleDataClient.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 11f55dd57..dbf65b8f4 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1,4 +1,5 @@ import _ from "lodash"; +import { utils } from "ethers"; import { ProposedRootBundle, SlowFillRequestWithBlock, @@ -14,7 +15,6 @@ import { ExpiredDepositsToRefundV3, Clients, CombinedRefunds, - DepositWithBlock, } from "../../interfaces"; import { AcrossConfigStoreClient, SpokePoolClient } from ".."; import { @@ -58,6 +58,16 @@ type DataCache = Record>; // V3 dictionary helper functions function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void { + // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. + const refundIsValid = (deposit: V3DepositWithBlock) => { + return ( + (utils.isAddress(deposit.depositor) && chainIsEvm(deposit.originChainId)) || !chainIsEvm(deposit.originChainId) + ); + }; + + if (!refundIsValid(deposit)) { + return; + } const { originChainId, inputToken } = deposit; if (!dict?.[originChainId]?.[inputToken]) { assign(dict, [originChainId, inputToken], []); @@ -808,11 +818,6 @@ export class BundleDataClient { return; } - // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. - const refundIsValid = (deposit: DepositWithBlock) => { - return !(isAddressBytes32(deposit.depositor) && chainIsEvm(deposit.originChainId)); - }; - // If deposit block is within origin chain bundle block range, then save as bundle deposit. // If deposit is in bundle and it has expired, additionally save it as an expired deposit. // Note: if the `depositor` field in the expired deposit is an invalid address, e.g. a bytes32 address on an EVM @@ -826,7 +831,7 @@ export class BundleDataClient { // that would eliminate any deposits in this bundle with a very low fillDeadline like equal to 0 // for example. Those should be impossible to create but technically should be included in this // bundle of refunded deposits. - if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1] && refundIsValid(deposit)) { + if (deposit.fillDeadline < bundleBlockTimestamps[destinationChainId][1]) { expiredBundleDepositHashes.add(relayDataHash); } } else if (deposit.blockNumber < originChainBlockRange[0]) { From 76de69a24a709b48b3b8efa71acb3bdf2704d898 Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 24 Jan 2025 12:43:34 -0600 Subject: [PATCH 18/23] add checks to dictionary updates Signed-off-by: bennett --- .../BundleDataClient/BundleDataClient.ts | 19 +++++--------- src/utils/AddressUtils.ts | 26 ------------------- 2 files changed, 6 insertions(+), 39 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index dbf65b8f4..508d8e9f9 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -34,7 +34,6 @@ import { bnUint32Max, isZeroValueDeposit, chainIsEvm, - isAddressBytes32, } from "../../utils"; import winston from "winston"; import { @@ -90,6 +89,10 @@ function updateBundleFillsV3( repaymentChainId: number, repaymentToken: string ): void { + // It is impossible to refund a deposit if the repayment chain is EVM and the relayer is a non-evm address. + if (chainIsEvm(fill.repaymentChainId) && !utils.isAddress(fill.relayer)) { + return; + } if (!dict?.[repaymentChainId]?.[repaymentToken]) { assign(dict, [repaymentChainId, repaymentToken], { fills: [], @@ -930,15 +933,11 @@ export class BundleDataClient { return; } // If the fill's repayment address is not a valid EVM address and the repayment chain is an EVM chain, the fill is invalid. - if (chainIsEvm(fill.repaymentChainId) && isAddressBytes32(fill.relayer)) { + if (chainIsEvm(fill.repaymentChainId) && !utils.isAddress(fill.relayer)) { const fillTransaction = await originClient.spokePool.provider.getTransaction(fill.transactionHash); const originRelayer = fillTransaction.from; - this.logger.debug({ - at: "BundleDataClient#loadData", - message: `${fill.relayer} is not a valid address on chain ${fill.repaymentChainId}. Falling back to ${originRelayer}.`, - }); // Repayment chain is still an EVM chain, but the msg.sender is a bytes32 address, so the fill is invalid. - if (isAddressBytes32(originRelayer)) { + if (!utils.isAddress(originRelayer)) { bundleInvalidFillsV3.push(fill); return; } @@ -949,12 +948,6 @@ export class BundleDataClient { // algorithm will not work. const historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); if (!historicalDeposit.found) { - this.logger.debug({ - at: "BundleDataClient#loadData", - message: "Could not binary search a historical deposit for fill.", - fill, - reason: historicalDeposit.reason, - }); bundleInvalidFillsV3.push(fill); } else { const matchedDeposit = historicalDeposit.deposit; diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 13aa4fe8f..383aaf76d 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -38,29 +38,3 @@ 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); -} - -// Checks if an input address is a 32-byte address or not. -export function isAddressBytes32(address: string): boolean { - // If the address is not 32 bytes, then don't check. - if (utils.hexDataLength(address) !== 32) return false; - - const strippedAddress = utils.hexStripZeros(address); - return utils.isBytes(strippedAddress) && utils.hexDataLength(strippedAddress) > 20; -} From 966c60beb9b7032bc26f2576a8c7ab6df5804d88 Mon Sep 17 00:00:00 2001 From: bennett Date: Fri, 24 Jan 2025 12:47:03 -0600 Subject: [PATCH 19/23] refactor Signed-off-by: bennett --- src/clients/BundleDataClient/BundleDataClient.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index 508d8e9f9..f291e3dd4 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -58,13 +58,7 @@ type DataCache = Record>; // V3 dictionary helper functions function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void { // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. - const refundIsValid = (deposit: V3DepositWithBlock) => { - return ( - (utils.isAddress(deposit.depositor) && chainIsEvm(deposit.originChainId)) || !chainIsEvm(deposit.originChainId) - ); - }; - - if (!refundIsValid(deposit)) { + if (chainIsEvm(deposit.originChainId) && !utils.isAddress(deposit.depositor)) { return; } const { originChainId, inputToken } = deposit; From 3bd6c6c64945263613355528022b8ba76a1124b4 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 27 Jan 2025 13:21:01 -0600 Subject: [PATCH 20/23] add back custom evm address check Signed-off-by: bennett --- .../BundleDataClient/BundleDataClient.ts | 13 ++++--- src/utils/AddressUtils.ts | 15 ++++++++ src/utils/NetworkUtils.ts | 38 ++----------------- 3 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/clients/BundleDataClient/BundleDataClient.ts b/src/clients/BundleDataClient/BundleDataClient.ts index f291e3dd4..07dcfa266 100644 --- a/src/clients/BundleDataClient/BundleDataClient.ts +++ b/src/clients/BundleDataClient/BundleDataClient.ts @@ -1,5 +1,4 @@ import _ from "lodash"; -import { utils } from "ethers"; import { ProposedRootBundle, SlowFillRequestWithBlock, @@ -34,6 +33,7 @@ import { bnUint32Max, isZeroValueDeposit, chainIsEvm, + isValidEvmAddress, } from "../../utils"; import winston from "winston"; import { @@ -58,7 +58,7 @@ type DataCache = Record>; // V3 dictionary helper functions function updateExpiredDepositsV3(dict: ExpiredDepositsToRefundV3, deposit: V3DepositWithBlock): void { // A deposit refund for a deposit is invalid if the depositor has a bytes32 address input for an EVM chain. It is valid otherwise. - if (chainIsEvm(deposit.originChainId) && !utils.isAddress(deposit.depositor)) { + if (chainIsEvm(deposit.originChainId) && !isValidEvmAddress(deposit.depositor)) { return; } const { originChainId, inputToken } = deposit; @@ -84,7 +84,7 @@ function updateBundleFillsV3( repaymentToken: string ): void { // It is impossible to refund a deposit if the repayment chain is EVM and the relayer is a non-evm address. - if (chainIsEvm(fill.repaymentChainId) && !utils.isAddress(fill.relayer)) { + if (chainIsEvm(fill.repaymentChainId) && !isValidEvmAddress(fill.relayer)) { return; } if (!dict?.[repaymentChainId]?.[repaymentToken]) { @@ -927,11 +927,11 @@ export class BundleDataClient { return; } // If the fill's repayment address is not a valid EVM address and the repayment chain is an EVM chain, the fill is invalid. - if (chainIsEvm(fill.repaymentChainId) && !utils.isAddress(fill.relayer)) { + if (chainIsEvm(fill.repaymentChainId) && !isValidEvmAddress(fill.relayer)) { const fillTransaction = await originClient.spokePool.provider.getTransaction(fill.transactionHash); const originRelayer = fillTransaction.from; // Repayment chain is still an EVM chain, but the msg.sender is a bytes32 address, so the fill is invalid. - if (!utils.isAddress(originRelayer)) { + if (!isValidEvmAddress(originRelayer)) { bundleInvalidFillsV3.push(fill); return; } @@ -939,7 +939,8 @@ export class BundleDataClient { fill.relayer = originRelayer; } // If deposit is using the deterministic relay hash feature, then the following binary search-based - // algorithm will not work. + // algorithm will not work. However, it is impossible to emit an infinite fill deadline using + // the unsafeDepositV3 function so there is no need to catch the special case. const historicalDeposit = await queryHistoricalDepositForFill(originClient, fill); if (!historicalDeposit.found) { bundleInvalidFillsV3.push(fill); diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 383aaf76d..b90613c52 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -38,3 +38,18 @@ export function compareAddressesSimple(addressA?: string, addressB?: string): bo } return addressA.toLowerCase() === addressB.toLowerCase(); } + +export function isValidEvmAddress(address: string): boolean { + if (utils.isAddress(address)) { + return true; + } + // We may throw an error here if zero pad fails. This will happen if the address is not a bytes20 address. + // We may also throw at getAddress if the input address is not possibly an evm address (i.e. incorrect hex characters). + // For both cases, return false. + try { + const evmAddress = utils.hexZeroPad(utils.hexStripZeros(address), 20); + return utils.isAddress(utils.getAddress(evmAddress)); + } catch (_e) { + return false; + } +} diff --git a/src/utils/NetworkUtils.ts b/src/utils/NetworkUtils.ts index ded7d43af..98d60b2b7 100644 --- a/src/utils/NetworkUtils.ts +++ b/src/utils/NetworkUtils.ts @@ -129,41 +129,9 @@ export function chainIsL1(chainId: number): boolean { * @returns True if chain corresponding to chainId has an EVM-like execution layer. */ export function chainIsEvm(chainId: number): boolean { - const chainFamily = PUBLIC_NETWORKS[chainId].family; - const checkEvmExceptionChains = (_chainId: number) => { - const { - MAINNET, - SEPOLIA, - ARBITRUM, - ARBITRUM_SEPOLIA, - LINEA, - POLYGON, - POLYGON_AMOY, - SCROLL, - SCROLL_SEPOLIA, - ZK_SYNC, - ZK_SYNC_SEPOLIA, - } = CHAIN_IDs; - return [ - MAINNET, - SEPOLIA, - ARBITRUM, - ARBITRUM_SEPOLIA, - LINEA, - POLYGON, - POLYGON_AMOY, - SCROLL, - SCROLL_SEPOLIA, - ZK_SYNC, - ZK_SYNC_SEPOLIA, - ].includes(_chainId); - }; - return ( - chainFamily === ChainFamily.OP_STACK || - chainFamily === ChainFamily.ORBIT || - chainFamily === ChainFamily.ZK_STACK || - checkEvmExceptionChains(chainId) - ); + chainId; + // TODO: Fix when we support non-EVM chains. + return true; } /** From 390592ffafe5408804422863ac904fea4893a028 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 27 Jan 2025 13:27:32 -0600 Subject: [PATCH 21/23] better comments Signed-off-by: bennett --- src/utils/AddressUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index b90613c52..b56ccefe5 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -43,9 +43,10 @@ export function isValidEvmAddress(address: string): boolean { if (utils.isAddress(address)) { return true; } - // We may throw an error here if zero pad fails. This will happen if the address is not a bytes20 address. - // We may also throw at getAddress if the input address is not possibly an evm address (i.e. incorrect hex characters). - // For both cases, return false. + // We may throw an error here if hexZeroPadFails. This will happen if the address to pad is greater than 20 bytes long, indicating + // that the address had less than 12 leading zero bytes. + // We may also throw at getAddress if the input cannot be converted into a checksummed EVM address for some reason. + // For both cases, this indicates that the address cannot be casted as a bytes20 EVM address, so we should return false. try { const evmAddress = utils.hexZeroPad(utils.hexStripZeros(address), 20); return utils.isAddress(utils.getAddress(evmAddress)); From 3c3c92cf638b3b184c0644ca2b91ffaaadd5ce07 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 27 Jan 2025 13:28:15 -0600 Subject: [PATCH 22/23] bump package Signed-off-by: bennett --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 481bde319..30f533a22 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "3.4.13", + "version": "3.4.14", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From a409497ebdbe3b95f53557e469f74d0534f09f71 Mon Sep 17 00:00:00 2001 From: bennett Date: Mon, 27 Jan 2025 13:37:34 -0600 Subject: [PATCH 23/23] bump package again Signed-off-by: bennett --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30f533a22..6c50604c1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "3.4.14", + "version": "3.4.15", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [