Skip to content

Commit 3dcc325

Browse files
authored
feat: remote hub pool set deposit route script (#780)
* feat: remote hub pool set deposit route script Signed-off-by: Pablo Maldonado <[email protected]> * feat: fixes Signed-off-by: Pablo Maldonado <[email protected]> * fix: cleanup Signed-off-by: Pablo Maldonado <[email protected]> * fix: cleanup Signed-off-by: Pablo Maldonado <[email protected]> * feat: add pause deposits Signed-off-by: Pablo Maldonado <[email protected]> * feat: remove pause deposits Signed-off-by: Pablo Maldonado <[email protected]> --------- Signed-off-by: Pablo Maldonado <[email protected]>
1 parent bd47385 commit 3dcc325

File tree

4 files changed

+320
-0
lines changed

4 files changed

+320
-0
lines changed

Anchor.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ simpleFakeRelayerRepayment = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/
3434
closeRelayerPdas = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/closeRelayerPdas.ts"
3535
closeDataWorkerLookUpTables = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/closeDataWorkerLookUpTables.ts"
3636
remotePauseDeposits = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/remotePauseDeposits.ts"
37+
remoteHubPoolSetDepositRoute = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/remoteHubPoolSetDepositRoute.ts"
3738
generateExternalTypes = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/generateExternalTypes.ts"
3839
fakeFillWithRandomDistribution = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/fakeFillWithRandomDistribution.ts"
3940

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// This script bridges remote call to pause deposits on Solana Spoke Pool. Required environment:
2+
// - ETHERS_PROVIDER_URL: Ethereum RPC provider URL.
3+
// - ETHERS_MNEMONIC: Mnemonic of the wallet that will sign the sending transaction on Ethereum
4+
// - HUB_POOL_ADDRESS: Hub Pool address
5+
6+
import "dotenv/config";
7+
import * as anchor from "@coral-xyz/anchor";
8+
import { BN, Program, AnchorProvider, web3 } from "@coral-xyz/anchor";
9+
import { AccountMeta, PublicKey, SystemProgram } from "@solana/web3.js";
10+
import { SvmSpoke } from "../../target/types/svm_spoke";
11+
import yargs from "yargs";
12+
import { hideBin } from "yargs/helpers";
13+
import { ethers } from "ethers";
14+
import { MessageTransmitter } from "../../target/types/message_transmitter";
15+
import { decodeMessageHeader, getMessages } from "../../test/svm/cctpHelpers";
16+
import { HubPool__factory } from "../../typechain";
17+
import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token";
18+
import {
19+
CIRCLE_IRIS_API_URL_DEVNET,
20+
CIRCLE_IRIS_API_URL_MAINNET,
21+
SOLANA_USDC_DEVNET,
22+
SOLANA_USDC_MAINNET,
23+
} from "./utils/constants";
24+
import { fromBase58ToBytes32, fromBytes32ToAddress } from "./utils/helpers";
25+
26+
// Set up Solana provider.
27+
const provider = AnchorProvider.env();
28+
anchor.setProvider(provider);
29+
30+
// Parse arguments
31+
const argv = yargs(hideBin(process.argv))
32+
.option("originChainId", { type: "string", demandOption: true, describe: "Origin chain ID" })
33+
.option("destinationChainId", { type: "string", demandOption: true, describe: "Destination chain ID" })
34+
.option("depositsEnabled", { type: "boolean", demandOption: true, describe: "Deposits enabled" })
35+
.option("resumeRemoteTx", { type: "string", demandOption: false, describe: "Resume receiving remote tx" }).argv;
36+
37+
async function remoteHubPoolSetDepositRoute(): Promise<void> {
38+
const resolvedArgv = await argv;
39+
40+
const originChainId = resolvedArgv.originChainId;
41+
const destinationChainId = resolvedArgv.destinationChainId;
42+
const depositsEnabled = resolvedArgv.depositsEnabled;
43+
const seed = new BN(0);
44+
const resumeRemoteTx = resolvedArgv.resumeRemoteTx;
45+
46+
// Set up Ethereum provider.
47+
if (!process.env.ETHERS_PROVIDER_URL) {
48+
throw new Error("Environment variable ETHERS_PROVIDER_URL is not set");
49+
}
50+
const ethersProvider = new ethers.providers.JsonRpcProvider(process.env.ETHERS_PROVIDER_URL);
51+
if (!process.env.ETHERS_MNEMONIC) {
52+
throw new Error("Environment variable ETHERS_MNEMONIC is not set");
53+
}
54+
const ethersSigner = ethers.Wallet.fromMnemonic(process.env.ETHERS_MNEMONIC).connect(ethersProvider);
55+
56+
if (!process.env.HUB_POOL_ADDRESS) {
57+
throw new Error("Environment variable HUB_POOL_ADDRESS is not set");
58+
}
59+
const hubPoolAddress = process.env.HUB_POOL_ADDRESS;
60+
61+
let cluster: "devnet" | "mainnet";
62+
const rpcEndpoint = provider.connection.rpcEndpoint;
63+
if (rpcEndpoint.includes("devnet")) cluster = "devnet";
64+
else if (rpcEndpoint.includes("mainnet")) cluster = "mainnet";
65+
else throw new Error(`Unsupported cluster endpoint: ${rpcEndpoint}`);
66+
const isDevnet = cluster == "devnet";
67+
68+
const usdcProgramId = isDevnet ? SOLANA_USDC_DEVNET : SOLANA_USDC_MAINNET;
69+
const originToken = new PublicKey(usdcProgramId);
70+
const originTokenAddress = fromBytes32ToAddress(fromBase58ToBytes32(originToken.toBase58()));
71+
72+
// CCTP domains.
73+
const remoteDomain = 0; // Ethereum
74+
75+
// Get Solana programs and accounts.
76+
const svmSpokeIdl = require("../../target/idl/svm_spoke.json");
77+
const svmSpokeProgram = new Program<SvmSpoke>(svmSpokeIdl, provider);
78+
const [statePda, _] = PublicKey.findProgramAddressSync(
79+
[Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)],
80+
svmSpokeProgram.programId
81+
);
82+
const [routePda] = PublicKey.findProgramAddressSync(
83+
[
84+
Buffer.from("route"),
85+
originToken.toBytes(),
86+
seed.toArrayLike(Buffer, "le", 8),
87+
new BN(destinationChainId).toArrayLike(Buffer, "le", 8),
88+
],
89+
svmSpokeProgram.programId
90+
);
91+
92+
const vault = getAssociatedTokenAddressSync(
93+
originToken,
94+
statePda,
95+
true,
96+
TOKEN_PROGRAM_ID,
97+
ASSOCIATED_TOKEN_PROGRAM_ID
98+
);
99+
100+
const messageTransmitterIdl = require("../../target/idl/message_transmitter.json");
101+
const messageTransmitterProgram = new Program<MessageTransmitter>(messageTransmitterIdl, provider);
102+
const [messageTransmitterState] = PublicKey.findProgramAddressSync(
103+
[Buffer.from("message_transmitter")],
104+
messageTransmitterProgram.programId
105+
);
106+
const [authorityPda] = PublicKey.findProgramAddressSync(
107+
[Buffer.from("message_transmitter_authority"), svmSpokeProgram.programId.toBuffer()],
108+
messageTransmitterProgram.programId
109+
);
110+
const [selfAuthority] = PublicKey.findProgramAddressSync([Buffer.from("self_authority")], svmSpokeProgram.programId);
111+
const [eventAuthority] = PublicKey.findProgramAddressSync(
112+
[Buffer.from("__event_authority")],
113+
svmSpokeProgram.programId
114+
);
115+
116+
const irisApiUrl = isDevnet ? CIRCLE_IRIS_API_URL_DEVNET : CIRCLE_IRIS_API_URL_MAINNET;
117+
118+
const supportedChainId = isDevnet ? 11155111 : 1; // Sepolia is bridged to devnet, Ethereum to mainnet in CCTP.
119+
const chainId = (await ethersProvider.getNetwork()).chainId;
120+
if (chainId !== supportedChainId) {
121+
throw new Error(`Chain ID ${chainId} does not match expected Solana cluster ${cluster}`);
122+
}
123+
124+
const hubPool = HubPool__factory.connect(hubPoolAddress, ethersProvider);
125+
126+
console.log("Remotely configuring deposit route...");
127+
console.table([
128+
{ Property: "seed", Value: seed.toString() },
129+
{ Property: "chainId", Value: (chainId as any).toString() },
130+
{ Property: "originChainId", Value: originChainId },
131+
{ Property: "destinationChainId", Value: destinationChainId },
132+
{ Property: "depositsEnabled", Value: depositsEnabled },
133+
{ Property: "svmSpokeProgramProgramId", Value: svmSpokeProgram.programId.toString() },
134+
{ Property: "providerPublicKey", Value: provider.wallet.publicKey.toString() },
135+
{ Property: "statePda", Value: statePda.toString() },
136+
{ Property: "messageTransmitterProgramId", Value: messageTransmitterProgram.programId.toString() },
137+
{ Property: "messageTransmitterState", Value: messageTransmitterState.toString() },
138+
{ Property: "authorityPda", Value: authorityPda.toString() },
139+
{ Property: "selfAuthority", Value: selfAuthority.toString() },
140+
{ Property: "eventAuthority", Value: eventAuthority.toString() },
141+
{ Property: "remoteSender", Value: ethersSigner.address },
142+
]);
143+
144+
// Send setDepositRoute call from Ethereum, unless resuming a remote transaction.
145+
let remoteTxHash: string;
146+
if (!resumeRemoteTx) {
147+
console.log("Sending setDepositRoute message from HubPool...");
148+
const tx = await hubPool
149+
.connect(ethersSigner)
150+
.setDepositRoute(originChainId, destinationChainId, originTokenAddress, depositsEnabled);
151+
await tx.wait();
152+
remoteTxHash = tx.hash;
153+
console.log("Message sent on remote chain, tx", remoteTxHash);
154+
} else remoteTxHash = resumeRemoteTx;
155+
156+
// Fetch attestation from CCTP attestation service.
157+
const attestationResponse = await getMessages(remoteTxHash, remoteDomain, irisApiUrl);
158+
const { attestation, message } = attestationResponse.messages[0];
159+
console.log("CCTP attestation response:", attestationResponse.messages[0]);
160+
161+
// Accounts in CCTP message_transmitter receive_message instruction.
162+
const nonce = decodeMessageHeader(Buffer.from(message.replace("0x", ""), "hex")).nonce;
163+
const usedNonces = (await messageTransmitterProgram.methods
164+
.getNoncePda({
165+
nonce: new BN(nonce.toString()),
166+
sourceDomain: remoteDomain,
167+
})
168+
.accounts({
169+
messageTransmitter: messageTransmitterState,
170+
})
171+
.view()) as PublicKey;
172+
173+
const receiveMessageAccounts = {
174+
payer: provider.wallet.publicKey,
175+
caller: provider.wallet.publicKey,
176+
authorityPda,
177+
messageTransmitter: messageTransmitterState,
178+
usedNonces,
179+
receiver: svmSpokeProgram.programId,
180+
systemProgram: web3.SystemProgram.programId,
181+
};
182+
183+
// accountMetas list to pass to remaining accounts when receiving message via CCTP.
184+
const remainingAccounts: AccountMeta[] = [];
185+
186+
// state in HandleReceiveMessage accounts (used for remote domain and sender authentication).
187+
remainingAccounts.push({
188+
isSigner: false,
189+
isWritable: false,
190+
pubkey: statePda,
191+
});
192+
// self_authority in HandleReceiveMessage accounts, also signer in self-invoked CPIs.
193+
remainingAccounts.push({
194+
isSigner: false,
195+
isWritable: false,
196+
pubkey: selfAuthority,
197+
});
198+
// program in HandleReceiveMessage accounts.
199+
remainingAccounts.push({
200+
isSigner: false,
201+
isWritable: false,
202+
pubkey: svmSpokeProgram.programId,
203+
});
204+
205+
// payer
206+
remainingAccounts.push({
207+
isSigner: true,
208+
isWritable: true,
209+
pubkey: provider.wallet.publicKey,
210+
});
211+
212+
// state in self-invoked CPIs (state can change as a result of remote call).
213+
remainingAccounts.push({
214+
isSigner: false,
215+
isWritable: true,
216+
pubkey: statePda,
217+
});
218+
219+
// route
220+
remainingAccounts.push({
221+
isSigner: false,
222+
isWritable: true,
223+
pubkey: routePda,
224+
});
225+
// vault
226+
remainingAccounts.push({
227+
isSigner: false,
228+
isWritable: true,
229+
pubkey: vault,
230+
});
231+
232+
// origin token mint
233+
remainingAccounts.push({
234+
isSigner: false,
235+
isWritable: true,
236+
pubkey: originToken,
237+
});
238+
239+
// token_program
240+
remainingAccounts.push({
241+
isSigner: false,
242+
isWritable: true,
243+
pubkey: TOKEN_PROGRAM_ID,
244+
});
245+
// associated_token_program
246+
remainingAccounts.push({
247+
isSigner: false,
248+
isWritable: true,
249+
pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
250+
});
251+
// system_program
252+
remainingAccounts.push({
253+
isSigner: false,
254+
isWritable: true,
255+
pubkey: SystemProgram.programId,
256+
});
257+
// event_authority in self-invoked CPIs (appended by Anchor with event_cpi macro).
258+
remainingAccounts.push({
259+
isSigner: false,
260+
isWritable: true,
261+
pubkey: eventAuthority,
262+
});
263+
// program
264+
remainingAccounts.push({
265+
isSigner: false,
266+
isWritable: true,
267+
pubkey: svmSpokeProgram.programId,
268+
});
269+
270+
// Receive remote message on Solana.
271+
console.log("Receiving message on Solana...");
272+
const receiveMessageTx = await messageTransmitterProgram.methods
273+
.receiveMessage({
274+
message: Buffer.from(message.replace("0x", ""), "hex"),
275+
attestation: Buffer.from(attestation.replace("0x", ""), "hex"),
276+
})
277+
.accounts(receiveMessageAccounts as any)
278+
.remainingAccounts(remainingAccounts)
279+
.rpc();
280+
console.log("\nReceived remote message");
281+
console.log("Your transaction signature", receiveMessageTx);
282+
283+
let routeAccount = await svmSpokeProgram.account.route.fetch(routePda);
284+
console.log("Updated deposit route state to: enabled =", routeAccount.enabled);
285+
}
286+
287+
remoteHubPoolSetDepositRoute()
288+
.then(() => {
289+
process.exit(0);
290+
})
291+
.catch((err) => {
292+
console.error(err);
293+
process.exit(1);
294+
});

scripts/svm/utils/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const CIRCLE_IRIS_API_URL_DEVNET = "https://iris-api-sandbox.circle.com";
2+
export const CIRCLE_IRIS_API_URL_MAINNET = "https://iris-api.circle.com";
3+
export const SOLANA_USDC_MAINNET = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
4+
export const SOLANA_USDC_DEVNET = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";

scripts/svm/utils/helpers.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { utils as anchorUtils } from "@coral-xyz/anchor";
2+
3+
export const fromBase58ToBytes32 = (input: string): string => {
4+
const decodedBytes = anchorUtils.bytes.bs58.decode(input);
5+
return "0x" + Buffer.from(decodedBytes).toString("hex");
6+
};
7+
8+
export const fromBytes32ToAddress = (input: string): string => {
9+
// Remove the '0x' prefix if present
10+
const hexString = input.startsWith("0x") ? input.slice(2) : input;
11+
12+
// Ensure the input is 64 characters long (32 bytes)
13+
if (hexString.length !== 64) {
14+
throw new Error("Invalid bytes32 string");
15+
}
16+
17+
// Get the last 40 characters (20 bytes) for the address
18+
const address = hexString.slice(-40);
19+
20+
return "0x" + address;
21+
};

0 commit comments

Comments
 (0)