diff --git a/package.json b/package.json index 12f2c66e33..5bc3634f8a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@across-protocol/constants": "^3.1.68", "@across-protocol/contracts": "^4.1.0", - "@across-protocol/sdk": "4.3.40", + "@across-protocol/sdk": "4.3.44", "@arbitrum/sdk": "^4.0.2", "@consensys/linea-sdk": "^0.2.1", "@coral-xyz/anchor": "^0.31.1", @@ -70,6 +70,7 @@ "run-disputer": "DISPUTER_ENABLED=true node ./dist/index.js --dataworker", "run-executor": "EXECUTOR_ENABLED=true node ./dist/index.js --dataworker", "run-proposer": "PROPOSER_ENABLED=true node ./dist/index.js --dataworker", + "run-monitor": "node ./dist/index.js --monitor", "run-finalizer": "node ./dist/index.js --finalizer", "relay": "node ./dist/index.js --relayer", "deposit": "yarn ts-node ./scripts/spokepool.ts deposit", diff --git a/src/clients/TokenTransferClient.ts b/src/clients/TokenTransferClient.ts index aaa488821b..41de51ad33 100644 --- a/src/clients/TokenTransferClient.ts +++ b/src/clients/TokenTransferClient.ts @@ -48,7 +48,8 @@ export class TokenTransferClient { const chainIds = Object.keys(this.providerByChainIds).map(Number); for (const chainId of chainIds) { const tokenContracts = tokenContractsByChainId[chainId]; - for (const monitoredAddress of this.monitoredAddresses) { + const evmMonitoredAddresses = this.monitoredAddresses.filter((address) => address.isEVM()); + for (const monitoredAddress of evmMonitoredAddresses) { const transferEventsList = await Promise.all( tokenContracts.map((tokenContract) => this.querySendAndReceiveEvents(tokenContract, monitoredAddress, searchConfigByChainIds[chainId]) diff --git a/src/dataworker/Dataworker.ts b/src/dataworker/Dataworker.ts index 67dbcab26f..76a9bb79ad 100644 --- a/src/dataworker/Dataworker.ts +++ b/src/dataworker/Dataworker.ts @@ -3185,7 +3185,8 @@ export class Dataworker { }); // Get the slow fill information. - const relayDataHash = getRelayDataHash(leaf.relayData, leaf.chainId); + const messageHash = getMessageHash(leaf.relayData.message); + const relayDataHash = getRelayDataHash({ ...leaf.relayData, messageHash }, leaf.chainId); // Construct the slow fill instruction. const executeSlowFillIx = SvmSpokeClient.getExecuteSlowRelayLeafInstruction({ diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 8d0163045f..5a8ced126d 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -70,5 +70,6 @@ export type Refund = interfaces.Refund; export type RunningBalances = interfaces.RunningBalances; export type TokensBridged = interfaces.TokensBridged; export const { FillType, FillStatus } = interfaces; +export type FillStatus = interfaces.FillStatus; export type CachingMechanismInterface = interfaces.CachingMechanismInterface; diff --git a/src/libexec/RelayerSpokePoolListenerSVM.ts b/src/libexec/RelayerSpokePoolListenerSVM.ts index 1a3316095b..7163b5294d 100644 --- a/src/libexec/RelayerSpokePoolListenerSVM.ts +++ b/src/libexec/RelayerSpokePoolListenerSVM.ts @@ -83,7 +83,7 @@ async function scrapeEvents( ): Promise { const provider = eventsClient.getRpc(); const [{ timestamp: currentTime }, ...events] = await Promise.all([ - arch.svm.getNearestSlotTime(provider, logger), + arch.svm.getNearestSlotTime(provider, { commitment: "confirmed" }, logger), ...eventNames.map((eventName) => _scrapeEvents(chain, eventsClient, eventName, { ...opts, to: opts.to }, logger)), ]); @@ -173,7 +173,11 @@ async function run(argv: string[]): Promise { const provider = getSvmProvider(await getRedisCache()); const blockFinder = undefined; - const { slot: latestSlot, timestamp: now } = await arch.svm.getNearestSlotTime(provider, logger); + const { slot: latestSlot, timestamp: now } = await arch.svm.getNearestSlotTime( + provider, + { commitment: "confirmed" }, + logger + ); const deploymentBlock = getDeploymentBlockNumber("SvmSpoke", chainId); let startSlot = latestSlot; diff --git a/src/monitor/Monitor.ts b/src/monitor/Monitor.ts index 38d56a1250..126eb01a35 100644 --- a/src/monitor/Monitor.ts +++ b/src/monitor/Monitor.ts @@ -5,6 +5,7 @@ import { BundleAction, DepositWithBlock, FillStatus, + FillWithBlock, L1Token, RelayerBalanceReport, RelayerBalanceTable, @@ -55,12 +56,21 @@ import { getBinanceApiClient, getBinanceWithdrawalLimits, chainIsEvm, + getFillStatusPda, + getKitKeypairFromEvmSigner, + getRelayDataFromFill, } from "../utils"; import { MonitorClients, updateMonitorClients } from "./MonitorClientHelper"; import { MonitorConfig } from "./MonitorConfig"; import { CombinedRefunds, getImpliedBundleBlockRanges } from "../dataworker/DataworkerUtils"; import { PUBLIC_NETWORKS, TOKEN_EQUIVALENCE_REMAPPING } from "@across-protocol/constants"; import { utils as sdkUtils, arch } from "@across-protocol/sdk"; +import { + address, + fetchEncodedAccount, + getBase64EncodedWireTransaction, + signTransactionMessageWithSigners, +} from "@solana/kit"; // 60 minutes, which is the length of the challenge window, so if a rebalance takes longer than this to finalize, // then its finalizing after the subsequent challenge period has started, which is sub-optimal. @@ -481,6 +491,11 @@ export class Monitor { const l1Tokens = this.getL1TokensForRelayerBalancesReport(); for (const relayer of this.monitorConfig.monitoredRelayers) { for (const chainId of this.monitorChains) { + // If the monitored relayer address is invalid on the monitored chain (e.g. the monitored relayer is a base58 address while the chain ID is mainnet), + // then there is no balance to update in this loop. + if (!relayer.isValidOn(chainId)) { + continue; + } const l2ToL1Tokens = this.getL2ToL1TokenMap(l1Tokens, chainId); const l2TokenAddresses = Object.keys(l2ToL1Tokens); const tokenBalances = await this._getBalances( @@ -1183,6 +1198,90 @@ export class Monitor { } } + async closePDAs(): Promise { + const simulate = process.env["SEND_TRANSACTIONS"] !== "true"; + const svmSpokePoolClient = this.clients.spokePoolClients[CHAIN_IDs.SOLANA]; + if (!isSVMSpokePoolClient(svmSpokePoolClient)) { + return; + } + const fills: FillWithBlock[] = []; + for (const relayers of this.monitorConfig.monitoredRelayers) { + if (relayers.isSVM()) { + const relayerFills = svmSpokePoolClient.getFillsForRelayer(relayers); + fills.push(...relayerFills); + } + } + const spokePoolProgramId = address(svmSpokePoolClient.spokePoolAddress.toBase58()); + const signer = await getKitKeypairFromEvmSigner(this.clients.hubPoolClient.hubPool.signer); + const svmRpc = svmSpokePoolClient.svmEventsClient.getRpc(); + + for (const fill of fills) { + const relayData = getRelayDataFromFill(fill); + const relayDataWithMessageHash = { + ...relayData, + messageHash: fill.messageHash, + }; + const fillStatus = await svmSpokePoolClient.relayFillStatus(relayDataWithMessageHash, fill.destinationChainId); + // If fill PDA should not be closed, skip. + if (!this._shouldCloseFillPDA(fillStatus, fill.fillDeadline, svmSpokePoolClient.getCurrentTime())) { + this.logger.info({ + at: "Monitor#closePDAs", + message: `Not ready to close PDA for fill ${fill.txnRef}`, + fill, + }); + continue; + } + + const fillStatusPda = await getFillStatusPda( + spokePoolProgramId, + relayDataWithMessageHash, + fill.destinationChainId + ); + // Check if PDA is already closed + const fillStatusPdaAccount = await fetchEncodedAccount(svmRpc, fillStatusPda); + if (!fillStatusPdaAccount.exists) { + continue; + } + + const closePdaInstruction = await arch.svm.createCloseFillPdaInstruction(signer, svmRpc, fillStatusPda); + const signedTransaction = await signTransactionMessageWithSigners(closePdaInstruction); + const encodedTransaction = getBase64EncodedWireTransaction(signedTransaction); + + if (simulate) { + const result = await svmRpc + .simulateTransaction(encodedTransaction, { + encoding: "base64", + }) + .send(); + if (result.value.err) { + this.logger.error({ + at: "Monitor#closePDAs", + message: `Failed to close PDA for fill ${fill.txnRef}`, + error: result.value.err, + }); + } + continue; + } + + try { + await svmRpc + .sendTransaction(encodedTransaction, { preflightCommitment: "confirmed", encoding: "base64" }) + .send(); + + this.logger.info({ + at: "Monitor#closePDAs", + message: `Closed PDA ${fillStatusPda} for fill ${fill.txnRef}`, + }); + } catch (err) { + this.logger.error({ + at: "Monitor#closePDAs", + message: `Failed to close PDA for fill ${fill.txnRef}`, + error: err, + }); + } + } + } + async updateLatestAndFutureRelayerRefunds(relayerBalanceReport: RelayerBalanceReport): Promise { const validatedBundleRefunds: CombinedRefunds[] = await this.clients.bundleDataClient.getPendingRefundsFromValidBundles(); @@ -1384,7 +1483,11 @@ export class Monitor { this.spokePoolsBlocks[chainId].endingBlock = endingBlock; } else if (isSVMSpokePoolClient(spokePoolClient)) { const svmProvider = await spokePoolClient.svmEventsClient.getRpc(); - const { slot: latestSlot } = await arch.svm.getNearestSlotTime(svmProvider, spokePoolClient.logger); + const { slot: latestSlot } = await arch.svm.getNearestSlotTime( + svmProvider, + { commitment: "confirmed" }, + spokePoolClient.logger + ); const endingBlock = this.monitorConfig.spokePoolsBlocks[chainId]?.endingBlock; this.monitorConfig.spokePoolsBlocks[chainId] ??= { startingBlock: undefined, endingBlock: undefined }; if (this.monitorConfig.pollingDelay === 0) { @@ -1497,4 +1600,8 @@ export class Monitor { } return false; } + + private _shouldCloseFillPDA(fillStatus: FillStatus, fillDeadline: number, currentTime: number): boolean { + return fillStatus === FillStatus.Filled && currentTime > fillDeadline; + } } diff --git a/src/monitor/MonitorConfig.ts b/src/monitor/MonitorConfig.ts index 80778537f2..ec502c866b 100644 --- a/src/monitor/MonitorConfig.ts +++ b/src/monitor/MonitorConfig.ts @@ -21,6 +21,7 @@ export interface BotModes { unknownRootBundleCallersEnabled: boolean; // Monitors relay related events triggered by non-whitelisted addresses spokePoolBalanceReportEnabled: boolean; binanceWithdrawalLimitsEnabled: boolean; + closePDAsEnabled: boolean; } export class MonitorConfig extends CommonConfig { @@ -82,6 +83,7 @@ export class MonitorConfig extends CommonConfig { BUNDLES_COUNT, BINANCE_WITHDRAW_WARN_THRESHOLD, BINANCE_WITHDRAW_ALERT_THRESHOLD, + CLOSE_PDAS_ENABLED, } = env; this.botModes = { @@ -92,6 +94,7 @@ export class MonitorConfig extends CommonConfig { unknownRootBundleCallersEnabled: UNKNOWN_ROOT_BUNDLE_CALLERS_ENABLED === "true", stuckRebalancesEnabled: STUCK_REBALANCES_ENABLED === "true", spokePoolBalanceReportEnabled: REPORT_SPOKE_POOL_BALANCES === "true", + closePDAsEnabled: CLOSE_PDAS_ENABLED === "true", binanceWithdrawalLimitsEnabled: isDefined(BINANCE_WITHDRAW_WARN_THRESHOLD) || isDefined(BINANCE_WITHDRAW_ALERT_THRESHOLD), }; @@ -99,8 +102,6 @@ export class MonitorConfig extends CommonConfig { // Used to monitor activities not from whitelisted data workers or relayers. this.whitelistedDataworkers = parseAddressesOptional(WHITELISTED_DATA_WORKERS); this.whitelistedRelayers = parseAddressesOptional(WHITELISTED_RELAYERS); - - // Used to monitor balances, activities, etc. from the specified relayers. this.monitoredRelayers = parseAddressesOptional(MONITORED_RELAYERS); this.knownV1Addresses = parseAddressesOptional(KNOWN_V1_ADDRESSES); this.monitoredSpokePoolChains = JSON.parse(MONITORED_SPOKE_POOL_CHAINS ?? "[]"); @@ -211,5 +212,8 @@ export class MonitorConfig extends CommonConfig { const parseAddressesOptional = (addressJson?: string): Address[] => { const rawAddresses: string[] = addressJson ? JSON.parse(addressJson) : []; - return rawAddresses.map((address) => toAddressType(ethers.utils.getAddress(address), CHAIN_IDs.MAINNET)); + return rawAddresses.map((address) => { + const chainId = address.startsWith("0x") ? CHAIN_IDs.MAINNET : CHAIN_IDs.SOLANA; + return toAddressType(address, chainId); + }); }; diff --git a/src/monitor/index.ts b/src/monitor/index.ts index 5a0259b5bc..3d8c1e1344 100644 --- a/src/monitor/index.ts +++ b/src/monitor/index.ts @@ -68,6 +68,12 @@ export async function runMonitor(_logger: winston.Logger, baseSigner: Signer): P logger.debug({ at: "Monitor#index", message: "Binance withdrawal limits check disabled" }); } + if (config.botModes.closePDAsEnabled) { + await acrossMonitor.closePDAs(); + } else { + logger.debug({ at: "Monitor#index", message: "Close PDAs disabled" }); + } + await clients.multiCallerClient.executeTxnQueues(); logger.debug({ at: "Monitor#index", message: `Time to loop: ${(Date.now() - loopStart) / 1000}s` }); diff --git a/src/utils/BlockUtils.ts b/src/utils/BlockUtils.ts index 87db109956..01c61c7489 100644 --- a/src/utils/BlockUtils.ts +++ b/src/utils/BlockUtils.ts @@ -28,7 +28,7 @@ export async function getBlockFinder(logger: winston.Logger, chainId: number): P return evmBlockFinders[chainId]; } const provider = getSvmProvider(await getRedisCache()); - svmBlockFinder ??= new SVMBlockFinder(logger, provider); + svmBlockFinder ??= new SVMBlockFinder(provider, [], logger); return svmBlockFinder; } @@ -79,9 +79,13 @@ export async function getTimestampsForBundleStartBlocks( return [chainId, (await spokePoolClient.spokePool.getCurrentTime({ blockTag: startAt })).toNumber()]; } else if (isSVMSpokePoolClient(spokePoolClient)) { const provider = spokePoolClient.svmEventsClient.getRpc(); - const { timestamp } = await arch.svm.getNearestSlotTime(provider, spokePoolClient.logger, { - slot: BigInt(startAt), - }); + const { timestamp } = await arch.svm.getNearestSlotTime( + provider, + { + slot: BigInt(startAt), + }, + spokePoolClient.logger + ); return [chainId, timestamp]; } }) diff --git a/src/utils/FillUtils.ts b/src/utils/FillUtils.ts index 15825892b1..cab7875f13 100644 --- a/src/utils/FillUtils.ts +++ b/src/utils/FillUtils.ts @@ -1,6 +1,6 @@ import { HubPoolClient, SpokePoolClient } from "../clients"; -import { FillStatus, FillWithBlock, SpokePoolClientsByChain, DepositWithBlock } from "../interfaces"; -import { bnZero, CHAIN_IDs } from "../utils"; +import { FillStatus, FillWithBlock, SpokePoolClientsByChain, DepositWithBlock, RelayData } from "../interfaces"; +import { bnZero, CHAIN_IDs, EMPTY_MESSAGE } from "../utils"; export type RelayerUnfilledDeposit = { deposit: DepositWithBlock; @@ -8,6 +8,26 @@ export type RelayerUnfilledDeposit = { invalidFills: FillWithBlock[]; }; +// @description Returns RelayData object with empty message. +// @param fill FillWithBlock object. +// @returns RelayData object. +export function getRelayDataFromFill(fill: FillWithBlock): RelayData { + return { + originChainId: fill.originChainId, + depositor: fill.depositor, + recipient: fill.recipient, + depositId: fill.depositId, + inputToken: fill.inputToken, + inputAmount: fill.inputAmount, + outputToken: fill.outputToken, + outputAmount: fill.outputAmount, + message: EMPTY_MESSAGE, + fillDeadline: fill.fillDeadline, + exclusiveRelayer: fill.exclusiveRelayer, + exclusivityDeadline: fill.exclusivityDeadline, + }; +} + // @description Returns all unfilled deposits, indexed by destination chain. // @param destinationChainId Chain ID to query outstanding deposits on. // @param spokePoolClients Mapping of chainIds to SpokePoolClient objects. diff --git a/src/utils/index.ts b/src/utils/index.ts index 77ed4c20bd..c495ea4ac1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,7 +6,7 @@ import winston from "winston"; import assert from "assert"; export { winston, assert }; -export const { MAX_SAFE_ALLOWANCE, ZERO_BYTES, DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM } = sdkConstants; +export const { MAX_SAFE_ALLOWANCE, ZERO_BYTES, DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM, EMPTY_MESSAGE } = sdkConstants; export const { AddressZero: ZERO_ADDRESS, MaxUint256: MAX_UINT_VAL } = ethersConstants; export { diff --git a/yarn.lock b/yarn.lock index 83c9a8d9d1..af232863da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -92,10 +92,10 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@4.3.40": - version "4.3.40" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-4.3.40.tgz#926f06399d5955694676dd4e24d8d0b044aa5afc" - integrity sha512-4mDiEhEi4sK+BzJURcvhTlsKWnNuRp7kSiaL5y2MzyF3sYe/avMcmio3Cm3sluJq3xZtrhNBg1VyN0bWWQnjrA== +"@across-protocol/sdk@4.3.44": + version "4.3.44" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-4.3.44.tgz#48babd26d5f4f1b8b462bef8200a0f159375eacb" + integrity sha512-eHjqjzZAU6KAQIPyuWUR9Ur+LwHiz5xDMl2NzXDh+Xr2Fm2p7+4oFHLWnzNFuq/b9lHYKG2lgWwVIKokXRC/Ow== dependencies: "@across-protocol/across-token" "^1.0.0" "@across-protocol/constants" "^3.1.71"