|
| 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 | + }); |
0 commit comments