Skip to content

Commit d13c916

Browse files
dijanin-bratsameersubudhi
authored andcommitted
feat: Close PDAs for fills from our relayer (across-protocol#2381)
* feat: Close PDAs for fills from our relayer * Log invalidFills without deposit * Check if PDA is already closed and enable SVM addresses to be monitored * lint fix * Use one array of Addresses for both EVM and SVM Addresses for monitored relayers * Fix MonitorClientHelper * Remove unused interface * Add simulate flag to closePDAs bot * Lint fix * Bump SDK * Lint fix * Exit early if monitoredAddress is not EVM * Log invalidFills only if there are any * Remove unnecessary if statement * Remove unused invalidFills reporting for closing PDAs * Remove unnecessary comment * Lint fix * reportRelayerBalances fix
1 parent 471d511 commit d13c916

12 files changed

Lines changed: 169 additions & 20 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"dependencies": {
1313
"@across-protocol/constants": "^3.1.68",
1414
"@across-protocol/contracts": "^4.1.0",
15-
"@across-protocol/sdk": "4.3.40",
15+
"@across-protocol/sdk": "4.3.44",
1616
"@arbitrum/sdk": "^4.0.2",
1717
"@aws-sdk/client-kms": "^3.592.0",
1818
"@aws-sdk/client-s3": "^3.592.0",
@@ -74,6 +74,7 @@
7474
"run-disputer": "DISPUTER_ENABLED=true node ./dist/index.js --dataworker",
7575
"run-executor": "EXECUTOR_ENABLED=true node ./dist/index.js --dataworker",
7676
"run-proposer": "PROPOSER_ENABLED=true node ./dist/index.js --dataworker",
77+
"run-monitor": "node ./dist/index.js --monitor",
7778
"run-finalizer": "node ./dist/index.js --finalizer",
7879
"relay": "node ./dist/index.js --relayer",
7980
"deposit": "yarn ts-node ./scripts/spokepool.ts deposit",

src/clients/TokenTransferClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export class TokenTransferClient {
4848
const chainIds = Object.keys(this.providerByChainIds).map(Number);
4949
for (const chainId of chainIds) {
5050
const tokenContracts = tokenContractsByChainId[chainId];
51-
for (const monitoredAddress of this.monitoredAddresses) {
51+
const evmMonitoredAddresses = this.monitoredAddresses.filter((address) => address.isEVM());
52+
for (const monitoredAddress of evmMonitoredAddresses) {
5253
const transferEventsList = await Promise.all(
5354
tokenContracts.map((tokenContract) =>
5455
this.querySendAndReceiveEvents(tokenContract, monitoredAddress, searchConfigByChainIds[chainId])

src/dataworker/Dataworker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3185,7 +3185,8 @@ export class Dataworker {
31853185
});
31863186

31873187
// Get the slow fill information.
3188-
const relayDataHash = getRelayDataHash(leaf.relayData, leaf.chainId);
3188+
const messageHash = getMessageHash(leaf.relayData.message);
3189+
const relayDataHash = getRelayDataHash({ ...leaf.relayData, messageHash }, leaf.chainId);
31893190

31903191
// Construct the slow fill instruction.
31913192
const executeSlowFillIx = SvmSpokeClient.getExecuteSlowRelayLeafInstruction({

src/interfaces/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,6 @@ export type Refund = interfaces.Refund;
7070
export type RunningBalances = interfaces.RunningBalances;
7171
export type TokensBridged = interfaces.TokensBridged;
7272
export const { FillType, FillStatus } = interfaces;
73+
export type FillStatus = interfaces.FillStatus;
7374

7475
export type CachingMechanismInterface = interfaces.CachingMechanismInterface;

src/libexec/RelayerSpokePoolListenerSVM.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async function scrapeEvents(
8383
): Promise<void> {
8484
const provider = eventsClient.getRpc();
8585
const [{ timestamp: currentTime }, ...events] = await Promise.all([
86-
arch.svm.getNearestSlotTime(provider, logger),
86+
arch.svm.getNearestSlotTime(provider, { commitment: "confirmed" }, logger),
8787
...eventNames.map((eventName) => _scrapeEvents(chain, eventsClient, eventName, { ...opts, to: opts.to }, logger)),
8888
]);
8989

@@ -173,7 +173,11 @@ async function run(argv: string[]): Promise<void> {
173173

174174
const provider = getSvmProvider(await getRedisCache());
175175
const blockFinder = undefined;
176-
const { slot: latestSlot, timestamp: now } = await arch.svm.getNearestSlotTime(provider, logger);
176+
const { slot: latestSlot, timestamp: now } = await arch.svm.getNearestSlotTime(
177+
provider,
178+
{ commitment: "confirmed" },
179+
logger
180+
);
177181

178182
const deploymentBlock = getDeploymentBlockNumber("SvmSpoke", chainId);
179183
let startSlot = latestSlot;

src/monitor/Monitor.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BundleAction,
66
DepositWithBlock,
77
FillStatus,
8+
FillWithBlock,
89
L1Token,
910
RelayerBalanceReport,
1011
RelayerBalanceTable,
@@ -55,12 +56,21 @@ import {
5556
getBinanceApiClient,
5657
getBinanceWithdrawalLimits,
5758
chainIsEvm,
59+
getFillStatusPda,
60+
getKitKeypairFromEvmSigner,
61+
getRelayDataFromFill,
5862
} from "../utils";
5963
import { MonitorClients, updateMonitorClients } from "./MonitorClientHelper";
6064
import { MonitorConfig } from "./MonitorConfig";
6165
import { CombinedRefunds, getImpliedBundleBlockRanges } from "../dataworker/DataworkerUtils";
6266
import { PUBLIC_NETWORKS, TOKEN_EQUIVALENCE_REMAPPING } from "@across-protocol/constants";
6367
import { utils as sdkUtils, arch } from "@across-protocol/sdk";
68+
import {
69+
address,
70+
fetchEncodedAccount,
71+
getBase64EncodedWireTransaction,
72+
signTransactionMessageWithSigners,
73+
} from "@solana/kit";
6474

6575
// 60 minutes, which is the length of the challenge window, so if a rebalance takes longer than this to finalize,
6676
// then its finalizing after the subsequent challenge period has started, which is sub-optimal.
@@ -481,6 +491,11 @@ export class Monitor {
481491
const l1Tokens = this.getL1TokensForRelayerBalancesReport();
482492
for (const relayer of this.monitorConfig.monitoredRelayers) {
483493
for (const chainId of this.monitorChains) {
494+
// 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),
495+
// then there is no balance to update in this loop.
496+
if (!relayer.isValidOn(chainId)) {
497+
continue;
498+
}
484499
const l2ToL1Tokens = this.getL2ToL1TokenMap(l1Tokens, chainId);
485500
const l2TokenAddresses = Object.keys(l2ToL1Tokens);
486501
const tokenBalances = await this._getBalances(
@@ -1183,6 +1198,90 @@ export class Monitor {
11831198
}
11841199
}
11851200

1201+
async closePDAs(): Promise<void> {
1202+
const simulate = process.env["SEND_TRANSACTIONS"] !== "true";
1203+
const svmSpokePoolClient = this.clients.spokePoolClients[CHAIN_IDs.SOLANA];
1204+
if (!isSVMSpokePoolClient(svmSpokePoolClient)) {
1205+
return;
1206+
}
1207+
const fills: FillWithBlock[] = [];
1208+
for (const relayers of this.monitorConfig.monitoredRelayers) {
1209+
if (relayers.isSVM()) {
1210+
const relayerFills = svmSpokePoolClient.getFillsForRelayer(relayers);
1211+
fills.push(...relayerFills);
1212+
}
1213+
}
1214+
const spokePoolProgramId = address(svmSpokePoolClient.spokePoolAddress.toBase58());
1215+
const signer = await getKitKeypairFromEvmSigner(this.clients.hubPoolClient.hubPool.signer);
1216+
const svmRpc = svmSpokePoolClient.svmEventsClient.getRpc();
1217+
1218+
for (const fill of fills) {
1219+
const relayData = getRelayDataFromFill(fill);
1220+
const relayDataWithMessageHash = {
1221+
...relayData,
1222+
messageHash: fill.messageHash,
1223+
};
1224+
const fillStatus = await svmSpokePoolClient.relayFillStatus(relayDataWithMessageHash, fill.destinationChainId);
1225+
// If fill PDA should not be closed, skip.
1226+
if (!this._shouldCloseFillPDA(fillStatus, fill.fillDeadline, svmSpokePoolClient.getCurrentTime())) {
1227+
this.logger.info({
1228+
at: "Monitor#closePDAs",
1229+
message: `Not ready to close PDA for fill ${fill.txnRef}`,
1230+
fill,
1231+
});
1232+
continue;
1233+
}
1234+
1235+
const fillStatusPda = await getFillStatusPda(
1236+
spokePoolProgramId,
1237+
relayDataWithMessageHash,
1238+
fill.destinationChainId
1239+
);
1240+
// Check if PDA is already closed
1241+
const fillStatusPdaAccount = await fetchEncodedAccount(svmRpc, fillStatusPda);
1242+
if (!fillStatusPdaAccount.exists) {
1243+
continue;
1244+
}
1245+
1246+
const closePdaInstruction = await arch.svm.createCloseFillPdaInstruction(signer, svmRpc, fillStatusPda);
1247+
const signedTransaction = await signTransactionMessageWithSigners(closePdaInstruction);
1248+
const encodedTransaction = getBase64EncodedWireTransaction(signedTransaction);
1249+
1250+
if (simulate) {
1251+
const result = await svmRpc
1252+
.simulateTransaction(encodedTransaction, {
1253+
encoding: "base64",
1254+
})
1255+
.send();
1256+
if (result.value.err) {
1257+
this.logger.error({
1258+
at: "Monitor#closePDAs",
1259+
message: `Failed to close PDA for fill ${fill.txnRef}`,
1260+
error: result.value.err,
1261+
});
1262+
}
1263+
continue;
1264+
}
1265+
1266+
try {
1267+
await svmRpc
1268+
.sendTransaction(encodedTransaction, { preflightCommitment: "confirmed", encoding: "base64" })
1269+
.send();
1270+
1271+
this.logger.info({
1272+
at: "Monitor#closePDAs",
1273+
message: `Closed PDA ${fillStatusPda} for fill ${fill.txnRef}`,
1274+
});
1275+
} catch (err) {
1276+
this.logger.error({
1277+
at: "Monitor#closePDAs",
1278+
message: `Failed to close PDA for fill ${fill.txnRef}`,
1279+
error: err,
1280+
});
1281+
}
1282+
}
1283+
}
1284+
11861285
async updateLatestAndFutureRelayerRefunds(relayerBalanceReport: RelayerBalanceReport): Promise<void> {
11871286
const validatedBundleRefunds: CombinedRefunds[] =
11881287
await this.clients.bundleDataClient.getPendingRefundsFromValidBundles();
@@ -1384,7 +1483,11 @@ export class Monitor {
13841483
this.spokePoolsBlocks[chainId].endingBlock = endingBlock;
13851484
} else if (isSVMSpokePoolClient(spokePoolClient)) {
13861485
const svmProvider = await spokePoolClient.svmEventsClient.getRpc();
1387-
const { slot: latestSlot } = await arch.svm.getNearestSlotTime(svmProvider, spokePoolClient.logger);
1486+
const { slot: latestSlot } = await arch.svm.getNearestSlotTime(
1487+
svmProvider,
1488+
{ commitment: "confirmed" },
1489+
spokePoolClient.logger
1490+
);
13881491
const endingBlock = this.monitorConfig.spokePoolsBlocks[chainId]?.endingBlock;
13891492
this.monitorConfig.spokePoolsBlocks[chainId] ??= { startingBlock: undefined, endingBlock: undefined };
13901493
if (this.monitorConfig.pollingDelay === 0) {
@@ -1497,4 +1600,8 @@ export class Monitor {
14971600
}
14981601
return false;
14991602
}
1603+
1604+
private _shouldCloseFillPDA(fillStatus: FillStatus, fillDeadline: number, currentTime: number): boolean {
1605+
return fillStatus === FillStatus.Filled && currentTime > fillDeadline;
1606+
}
15001607
}

src/monitor/MonitorConfig.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface BotModes {
2121
unknownRootBundleCallersEnabled: boolean; // Monitors relay related events triggered by non-whitelisted addresses
2222
spokePoolBalanceReportEnabled: boolean;
2323
binanceWithdrawalLimitsEnabled: boolean;
24+
closePDAsEnabled: boolean;
2425
}
2526

2627
export class MonitorConfig extends CommonConfig {
@@ -82,6 +83,7 @@ export class MonitorConfig extends CommonConfig {
8283
BUNDLES_COUNT,
8384
BINANCE_WITHDRAW_WARN_THRESHOLD,
8485
BINANCE_WITHDRAW_ALERT_THRESHOLD,
86+
CLOSE_PDAS_ENABLED,
8587
} = env;
8688

8789
this.botModes = {
@@ -92,15 +94,14 @@ export class MonitorConfig extends CommonConfig {
9294
unknownRootBundleCallersEnabled: UNKNOWN_ROOT_BUNDLE_CALLERS_ENABLED === "true",
9395
stuckRebalancesEnabled: STUCK_REBALANCES_ENABLED === "true",
9496
spokePoolBalanceReportEnabled: REPORT_SPOKE_POOL_BALANCES === "true",
97+
closePDAsEnabled: CLOSE_PDAS_ENABLED === "true",
9598
binanceWithdrawalLimitsEnabled:
9699
isDefined(BINANCE_WITHDRAW_WARN_THRESHOLD) || isDefined(BINANCE_WITHDRAW_ALERT_THRESHOLD),
97100
};
98101

99102
// Used to monitor activities not from whitelisted data workers or relayers.
100103
this.whitelistedDataworkers = parseAddressesOptional(WHITELISTED_DATA_WORKERS);
101104
this.whitelistedRelayers = parseAddressesOptional(WHITELISTED_RELAYERS);
102-
103-
// Used to monitor balances, activities, etc. from the specified relayers.
104105
this.monitoredRelayers = parseAddressesOptional(MONITORED_RELAYERS);
105106
this.knownV1Addresses = parseAddressesOptional(KNOWN_V1_ADDRESSES);
106107
this.monitoredSpokePoolChains = JSON.parse(MONITORED_SPOKE_POOL_CHAINS ?? "[]");
@@ -211,5 +212,8 @@ export class MonitorConfig extends CommonConfig {
211212

212213
const parseAddressesOptional = (addressJson?: string): Address[] => {
213214
const rawAddresses: string[] = addressJson ? JSON.parse(addressJson) : [];
214-
return rawAddresses.map((address) => toAddressType(ethers.utils.getAddress(address), CHAIN_IDs.MAINNET));
215+
return rawAddresses.map((address) => {
216+
const chainId = address.startsWith("0x") ? CHAIN_IDs.MAINNET : CHAIN_IDs.SOLANA;
217+
return toAddressType(address, chainId);
218+
});
215219
};

src/monitor/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export async function runMonitor(_logger: winston.Logger, baseSigner: Signer): P
6868
logger.debug({ at: "Monitor#index", message: "Binance withdrawal limits check disabled" });
6969
}
7070

71+
if (config.botModes.closePDAsEnabled) {
72+
await acrossMonitor.closePDAs();
73+
} else {
74+
logger.debug({ at: "Monitor#index", message: "Close PDAs disabled" });
75+
}
76+
7177
await clients.multiCallerClient.executeTxnQueues();
7278

7379
logger.debug({ at: "Monitor#index", message: `Time to loop: ${(Date.now() - loopStart) / 1000}s` });

src/utils/BlockUtils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export async function getBlockFinder(logger: winston.Logger, chainId: number): P
2828
return evmBlockFinders[chainId];
2929
}
3030
const provider = getSvmProvider(await getRedisCache());
31-
svmBlockFinder ??= new SVMBlockFinder(logger, provider);
31+
svmBlockFinder ??= new SVMBlockFinder(provider, [], logger);
3232
return svmBlockFinder;
3333
}
3434

@@ -79,9 +79,13 @@ export async function getTimestampsForBundleStartBlocks(
7979
return [chainId, (await spokePoolClient.spokePool.getCurrentTime({ blockTag: startAt })).toNumber()];
8080
} else if (isSVMSpokePoolClient(spokePoolClient)) {
8181
const provider = spokePoolClient.svmEventsClient.getRpc();
82-
const { timestamp } = await arch.svm.getNearestSlotTime(provider, spokePoolClient.logger, {
83-
slot: BigInt(startAt),
84-
});
82+
const { timestamp } = await arch.svm.getNearestSlotTime(
83+
provider,
84+
{
85+
slot: BigInt(startAt),
86+
},
87+
spokePoolClient.logger
88+
);
8589
return [chainId, timestamp];
8690
}
8791
})

src/utils/FillUtils.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
import { HubPoolClient, SpokePoolClient } from "../clients";
2-
import { FillStatus, FillWithBlock, SpokePoolClientsByChain, DepositWithBlock } from "../interfaces";
3-
import { bnZero, CHAIN_IDs } from "../utils";
2+
import { FillStatus, FillWithBlock, SpokePoolClientsByChain, DepositWithBlock, RelayData } from "../interfaces";
3+
import { bnZero, CHAIN_IDs, EMPTY_MESSAGE } from "../utils";
44

55
export type RelayerUnfilledDeposit = {
66
deposit: DepositWithBlock;
77
version: number;
88
invalidFills: FillWithBlock[];
99
};
1010

11+
// @description Returns RelayData object with empty message.
12+
// @param fill FillWithBlock object.
13+
// @returns RelayData object.
14+
export function getRelayDataFromFill(fill: FillWithBlock): RelayData {
15+
return {
16+
originChainId: fill.originChainId,
17+
depositor: fill.depositor,
18+
recipient: fill.recipient,
19+
depositId: fill.depositId,
20+
inputToken: fill.inputToken,
21+
inputAmount: fill.inputAmount,
22+
outputToken: fill.outputToken,
23+
outputAmount: fill.outputAmount,
24+
message: EMPTY_MESSAGE,
25+
fillDeadline: fill.fillDeadline,
26+
exclusiveRelayer: fill.exclusiveRelayer,
27+
exclusivityDeadline: fill.exclusivityDeadline,
28+
};
29+
}
30+
1131
// @description Returns all unfilled deposits, indexed by destination chain.
1232
// @param destinationChainId Chain ID to query outstanding deposits on.
1333
// @param spokePoolClients Mapping of chainIds to SpokePoolClient objects.

0 commit comments

Comments
 (0)