@across-protocol/contracts 3.0.17 → 3.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/scripts/svm/addressToPublicKey.d.ts +1 -0
  2. package/dist/scripts/svm/addressToPublicKey.js +20 -0
  3. package/dist/scripts/svm/bridgeLiabilityToHubPool.d.ts +28 -0
  4. package/dist/scripts/svm/bridgeLiabilityToHubPool.js +231 -0
  5. package/dist/scripts/svm/closeRelayerPdas.js +6 -7
  6. package/dist/scripts/svm/enableRoute.js +7 -1
  7. package/dist/scripts/svm/executeRebalanceToHubPool.d.ts +32 -0
  8. package/dist/scripts/svm/executeRebalanceToHubPool.js +355 -0
  9. package/dist/scripts/svm/executeRebalanceToSpokePool.d.ts +1 -0
  10. package/dist/scripts/svm/executeRebalanceToSpokePool.js +253 -0
  11. package/dist/scripts/svm/proposeRebalanceToHubPool.d.ts +27 -0
  12. package/dist/scripts/svm/proposeRebalanceToHubPool.js +117 -0
  13. package/dist/scripts/svm/proposeRebalanceToSpokePool.d.ts +1 -0
  14. package/dist/scripts/svm/proposeRebalanceToSpokePool.js +101 -0
  15. package/dist/scripts/svm/publicKeyToAddress.d.ts +1 -0
  16. package/dist/scripts/svm/publicKeyToAddress.js +20 -0
  17. package/dist/scripts/svm/queryDeposits.js +1 -0
  18. package/dist/scripts/svm/queryFills.js +11 -12
  19. package/dist/scripts/svm/queryRoute.js +7 -1
  20. package/dist/scripts/svm/queryState.js +1 -0
  21. package/dist/scripts/svm/remoteHubPoolPauseDeposits.d.ts +1 -0
  22. package/dist/scripts/svm/remoteHubPoolPauseDeposits.js +205 -0
  23. package/dist/scripts/svm/simpleDeposit.js +7 -1
  24. package/dist/scripts/svm/simpleFill.js +5 -4
  25. package/dist/scripts/svm/utils/constants.d.ts +4 -0
  26. package/dist/scripts/svm/utils/constants.js +8 -1
  27. package/dist/scripts/svm/utils/helpers.d.ts +31 -0
  28. package/dist/scripts/svm/utils/helpers.js +50 -1
  29. package/dist/scripts/svm/utils/poolRebalanceTree.d.ts +22 -0
  30. package/dist/scripts/svm/utils/poolRebalanceTree.js +20 -0
  31. package/dist/src/SvmUtils.d.ts +24 -2
  32. package/dist/src/SvmUtils.js +107 -26
  33. package/dist/target/types/svm_spoke.d.ts +47 -42
  34. package/dist/tasks/enableL1TokenAcrossEcosystem.js +3 -33
  35. package/dist/tasks/types.d.ts +2 -0
  36. package/dist/tasks/types.js +2 -0
  37. package/dist/tasks/utils.d.ts +12 -0
  38. package/dist/tasks/utils.js +34 -0
  39. package/dist/test/svm/SvmSpoke.Bundle.js +15 -16
  40. package/dist/test/svm/SvmSpoke.Deposit.js +18 -26
  41. package/dist/test/svm/SvmSpoke.Fill.AcrossPlus.js +1 -1
  42. package/dist/test/svm/SvmSpoke.Fill.js +13 -14
  43. package/dist/test/svm/SvmSpoke.Ownership.js +10 -16
  44. package/dist/test/svm/SvmSpoke.RefundClaims.js +9 -8
  45. package/dist/test/svm/SvmSpoke.Routes.js +10 -6
  46. package/dist/test/svm/SvmSpoke.SlowFill.AcrossPlus.js +59 -2
  47. package/dist/test/svm/SvmSpoke.SlowFill.js +23 -18
  48. package/dist/test/svm/SvmSpoke.TokenBridge.js +3 -5
  49. package/dist/test/svm/SvmSpoke.common.js +2 -1
  50. package/dist/test/svm/utils.d.ts +4 -5
  51. package/dist/test/svm/utils.js +24 -18
  52. package/package.json +2 -2
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ /**
3
+ * Script: Execute USDC Rebalance to Hub Pool
4
+ *
5
+ * This script executes a previously proposed root bundle on the Hub Pool to rebalance USDC from the Solana Spoke Pool
6
+ * to the Ethereum Hub Pool. It handles CCTP attestations, relayer refund leaf execution, and prepares pending
7
+ * liabilities for bridging back to the Hub Pool.
8
+ *
9
+ * Required Environment Variables:
10
+ * - TESTNET: (Optional) Set to "true" to use Sepolia; defaults to mainnet.
11
+ * - MNEMONIC: Wallet mnemonic to sign the Ethereum transaction.
12
+ * - HUB_POOL_ADDRESS: Ethereum address of the Hub Pool.
13
+ * - NODE_URL_1: Ethereum RPC URL for mainnet (ignored if TESTNET=true).
14
+ * - NODE_URL_11155111: Ethereum RPC URL for Sepolia (ignored if TESTNET=false).
15
+ *
16
+ * Required Arguments:
17
+ * - `--netSendAmount`: The unscaled amount of USDC to rebalance (e.g., for USDC with 6 decimals, 1 = 0.000001 USDC).
18
+ * - `--resumeRemoteTx`: (Optional) Hash of a previously submitted remote transaction to resume.
19
+ *
20
+ * Example Usage:
21
+ * TESTNET=true \
22
+ * NODE_URL_11155111=$NODE_URL_11155111 \
23
+ * MNEMONIC=$MNEMONIC \
24
+ * HUB_POOL_ADDRESS=$HUB_POOL_ADDRESS \
25
+ * anchor run executeRebalanceToHubPool \
26
+ * --provider.cluster "devnet" \
27
+ * --provider.wallet $SOLANA_PKEY_PATH \
28
+ * -- --netSendAmount 7
29
+ *
30
+ * Note:
31
+ * - Ensure all required environment variables are properly configured.
32
+ */
33
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ var desc = Object.getOwnPropertyDescriptor(m, k);
36
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
37
+ desc = { enumerable: true, get: function() { return m[k]; } };
38
+ }
39
+ Object.defineProperty(o, k2, desc);
40
+ }) : (function(o, m, k, k2) {
41
+ if (k2 === undefined) k2 = k;
42
+ o[k2] = m[k];
43
+ }));
44
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
45
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
46
+ }) : function(o, v) {
47
+ o["default"] = v;
48
+ });
49
+ var __importStar = (this && this.__importStar) || function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ var __importDefault = (this && this.__importDefault) || function (mod) {
57
+ return (mod && mod.__esModule) ? mod : { "default": mod };
58
+ };
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ const anchor = __importStar(require("@coral-xyz/anchor"));
61
+ const anchor_1 = require("@coral-xyz/anchor");
62
+ const spl_token_1 = require("@solana/spl-token");
63
+ const web3_js_1 = require("@solana/web3.js");
64
+ // eslint-disable-next-line camelcase
65
+ const ethers_1 = require("ethers");
66
+ const yargs_1 = __importDefault(require("yargs"));
67
+ const helpers_1 = require("yargs/helpers");
68
+ const constants_1 = require("../../utils/constants");
69
+ // eslint-disable-next-line camelcase
70
+ const typechain_1 = require("../../typechain");
71
+ const constants_2 = require("./utils/constants");
72
+ const helpers_2 = require("./utils/helpers");
73
+ const common_1 = require("@uma/common");
74
+ const cctpHelpers_1 = require("../../test/svm/cctpHelpers");
75
+ const utils_1 = require("../../test/svm/utils");
76
+ // Set up Solana provider.
77
+ const provider = anchor_1.AnchorProvider.env();
78
+ anchor.setProvider(provider);
79
+ // Get Solana programs.
80
+ const svmSpokeIdl = require("../../target/idl/svm_spoke.json");
81
+ const svmSpokeProgram = new anchor_1.Program(svmSpokeIdl, provider);
82
+ const messageTransmitterIdl = require("../../target/idl/message_transmitter.json");
83
+ const messageTransmitterProgram = new anchor_1.Program(messageTransmitterIdl, provider);
84
+ const [messageTransmitterState] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("message_transmitter")], messageTransmitterProgram.programId);
85
+ const [authorityPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("message_transmitter_authority"), svmSpokeProgram.programId.toBuffer()], messageTransmitterProgram.programId);
86
+ const [selfAuthority] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("self_authority")], svmSpokeProgram.programId);
87
+ const [eventAuthority] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], svmSpokeProgram.programId);
88
+ // Set up Ethereum provider and signer.
89
+ const nodeURL = process.env.TESTNET === "true" ? (0, common_1.getNodeUrl)("sepolia", true) : (0, common_1.getNodeUrl)("mainnet", true);
90
+ const ethersProvider = new ethers_1.ethers.providers.JsonRpcProvider(nodeURL);
91
+ const ethersSigner = ethers_1.ethers.Wallet.fromMnemonic((0, helpers_2.requireEnv)("MNEMONIC")).connect(ethersProvider);
92
+ // Get the HubPool contract instance.
93
+ const hubPoolAddress = ethers_1.ethers.utils.getAddress((0, helpers_2.requireEnv)("HUB_POOL_ADDRESS"));
94
+ const hubPool = typechain_1.HubPool__factory.connect(hubPoolAddress, ethersProvider);
95
+ // CCTP domains.
96
+ const ethereumDomain = 0; // Ethereum
97
+ // Parse arguments
98
+ const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
99
+ // Net send amount is always required as we need it to calculate the relayer refund leaf.
100
+ .option("netSendAmount", { type: "string", demandOption: true, describe: "Net send amount to Hub Pool" })
101
+ .option("resumeRemoteTx", { type: "string", demandOption: false, describe: "Resume receiving remote tx" }).argv;
102
+ async function executeRebalanceToHubPool() {
103
+ const resolvedArgv = await argv;
104
+ const seed = constants_2.SOLANA_SPOKE_STATE_SEED; // Seed is always 0 for the state account PDA in public networks.
105
+ const netSendAmount = ethers_1.BigNumber.from(resolvedArgv.netSendAmount);
106
+ const resumeRemoteTx = resolvedArgv.resumeRemoteTx;
107
+ // Resolve Solana cluster, EVM chain ID, Iris API URL and USDC addresses.
108
+ let isDevnet;
109
+ const solanaRpcEndpoint = provider.connection.rpcEndpoint;
110
+ if (solanaRpcEndpoint.includes("devnet"))
111
+ isDevnet = true;
112
+ else if (solanaRpcEndpoint.includes("mainnet"))
113
+ isDevnet = false;
114
+ else
115
+ throw new Error(`Unsupported solanaCluster endpoint: ${solanaRpcEndpoint}`);
116
+ const solanaCluster = isDevnet ? "devnet" : "mainnet";
117
+ const solanaChainId = (0, helpers_2.getSolanaChainId)(solanaCluster);
118
+ const irisApiUrl = isDevnet ? constants_2.CIRCLE_IRIS_API_URL_DEVNET : constants_2.CIRCLE_IRIS_API_URL_MAINNET;
119
+ const supportedEvmChainId = isDevnet ? constants_1.CHAIN_IDs.SEPOLIA : constants_1.CHAIN_IDs.MAINNET; // Sepolia is bridged to devnet, Ethereum to mainnet in CCTP.
120
+ const evmChainId = (await ethersProvider.getNetwork()).chainId;
121
+ if (evmChainId !== supportedEvmChainId) {
122
+ throw new Error(`Chain ID ${evmChainId} does not match expected Solana cluster ${solanaCluster}`);
123
+ }
124
+ const svmUsdc = isDevnet ? constants_2.SOLANA_USDC_DEVNET : constants_2.SOLANA_USDC_MAINNET;
125
+ const [statePda, _] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)], svmSpokeProgram.programId);
126
+ const state = await svmSpokeProgram.account.state.fetch(statePda);
127
+ const [rootBundlePda] = getRootBundlePda(state.rootBundleId, seed);
128
+ console.log("Executing rebalance to hub pool...");
129
+ console.table([
130
+ { Property: "isTestnet", Value: process.env.TESTNET === "true" },
131
+ { Property: "originChainId", Value: evmChainId.toString() },
132
+ { Property: "targetChainId", Value: solanaChainId.toString() },
133
+ { Property: "hubPoolAddress", Value: hubPool.address },
134
+ { Property: "svmSpokeProgramProgramId", Value: svmSpokeProgram.programId.toString() },
135
+ { Property: "svmProviderPublicKey", Value: provider.wallet.publicKey.toString() },
136
+ { Property: "netSendAmount (formatted)", Value: (0, helpers_2.formatUsdc)(netSendAmount) },
137
+ ]);
138
+ // Send executeRootBundle call from Ethereum, unless resuming a remote transaction.
139
+ let remoteTxHash;
140
+ if (!resumeRemoteTx) {
141
+ remoteTxHash = await executeRootBalanceOnHubPool(solanaChainId);
142
+ }
143
+ else
144
+ remoteTxHash = resumeRemoteTx;
145
+ // Fetch attestation from CCTP attestation service.
146
+ const attestationResponse = await (0, cctpHelpers_1.getMessages)(remoteTxHash, ethereumDomain, irisApiUrl);
147
+ const { attestation, message } = attestationResponse.messages[0];
148
+ console.log("CCTP attestation response:", attestationResponse.messages[0]);
149
+ // Accounts in CCTP message_transmitter receive_message instruction.
150
+ const nonce = (0, cctpHelpers_1.decodeMessageHeader)(Buffer.from(message.replace("0x", ""), "hex")).nonce;
151
+ const usedNonces = (await messageTransmitterProgram.methods
152
+ .getNoncePda({
153
+ nonce: new anchor_1.BN(nonce.toString()),
154
+ sourceDomain: ethereumDomain,
155
+ })
156
+ .accounts({
157
+ messageTransmitter: messageTransmitterState,
158
+ })
159
+ .view());
160
+ const receiveMessageAccounts = {
161
+ payer: provider.wallet.publicKey,
162
+ caller: provider.wallet.publicKey,
163
+ authorityPda,
164
+ messageTransmitter: messageTransmitterState,
165
+ usedNonces,
166
+ receiver: svmSpokeProgram.programId,
167
+ systemProgram: web3_js_1.SystemProgram.programId,
168
+ };
169
+ // accountMetas list to pass to remaining accounts when receiving message via CCTP.
170
+ const remainingAccounts = [];
171
+ // state in HandleReceiveMessage accounts (used for remote domain and sender authentication).
172
+ remainingAccounts.push({
173
+ isSigner: false,
174
+ isWritable: false,
175
+ pubkey: statePda,
176
+ });
177
+ // self_authority in HandleReceiveMessage accounts, also signer in self-invoked CPIs.
178
+ remainingAccounts.push({
179
+ isSigner: false,
180
+ isWritable: false,
181
+ pubkey: selfAuthority,
182
+ });
183
+ // program in HandleReceiveMessage accounts.
184
+ remainingAccounts.push({
185
+ isSigner: false,
186
+ isWritable: false,
187
+ pubkey: svmSpokeProgram.programId,
188
+ });
189
+ // payer
190
+ remainingAccounts.push({
191
+ isSigner: true,
192
+ isWritable: true,
193
+ pubkey: provider.wallet.publicKey,
194
+ });
195
+ // state in self-invoked CPIs (state can change as a result of remote call).
196
+ remainingAccounts.push({
197
+ isSigner: false,
198
+ isWritable: true,
199
+ pubkey: statePda,
200
+ });
201
+ // root_bundle
202
+ remainingAccounts.push({
203
+ isSigner: false,
204
+ isWritable: true,
205
+ pubkey: rootBundlePda,
206
+ });
207
+ // system_program
208
+ remainingAccounts.push({
209
+ isSigner: false,
210
+ isWritable: false,
211
+ pubkey: web3_js_1.SystemProgram.programId,
212
+ });
213
+ // event_authority in self-invoked CPIs (appended by Anchor with event_cpi macro).
214
+ remainingAccounts.push({
215
+ isSigner: false,
216
+ isWritable: true,
217
+ pubkey: eventAuthority,
218
+ });
219
+ // program
220
+ remainingAccounts.push({
221
+ isSigner: false,
222
+ isWritable: true,
223
+ pubkey: svmSpokeProgram.programId,
224
+ });
225
+ // Receive remote message on Solana.
226
+ console.log("Receiving message on Solana...");
227
+ const receiveMessageTx = await messageTransmitterProgram.methods
228
+ .receiveMessage({
229
+ message: Buffer.from(message.replace("0x", ""), "hex"),
230
+ attestation: Buffer.from(attestation.replace("0x", ""), "hex"),
231
+ })
232
+ .accounts(receiveMessageAccounts)
233
+ .remainingAccounts(remainingAccounts)
234
+ .rpc();
235
+ console.log("\nReceived remote message");
236
+ console.log("Your transaction signature", receiveMessageTx);
237
+ const finalState = await svmSpokeProgram.account.state.fetch(statePda);
238
+ // The state stores the next root bundle ID, so we need to subtract one to get the last executed root bundle ID.
239
+ const lastRootBundleId = finalState.rootBundleId - 1;
240
+ // Reconstruct the merkle tree for the relayer refund leaf.
241
+ const { merkleTree, leaves } = (0, helpers_2.constructSimpleRebalanceTreeToHubPool)(netSendAmount, solanaChainId, new web3_js_1.PublicKey(svmUsdc));
242
+ const [rootBundlePdaNew] = getRootBundlePda(lastRootBundleId, seed);
243
+ console.log(`Executing relayer refund leaf for root bundle ID: ${lastRootBundleId}`);
244
+ await executeRelayerRefundLeaf(provider.wallet, svmSpokeProgram, statePda, rootBundlePdaNew, leaves[0], merkleTree, new web3_js_1.PublicKey(svmUsdc), lastRootBundleId);
245
+ console.log("✅ executed relayer refund leaf");
246
+ const [transferLiability] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("transfer_liability"), new web3_js_1.PublicKey(svmUsdc).toBuffer()], svmSpokeProgram.programId);
247
+ const liability = await svmSpokeProgram.account.transferLiability.fetch(transferLiability);
248
+ console.log(`Pending transfer liability: ${(0, helpers_2.formatUsdc)(ethers_1.BigNumber.from(liability.pendingToHubPool.toString()))} USDC`);
249
+ console.log("You can now send the pending liability to hub pool with bridgeLiabilityToHubPool.ts");
250
+ }
251
+ function getRootBundlePda(rootBundleId, seed) {
252
+ const rootBundleIdBuffer = Buffer.alloc(4);
253
+ rootBundleIdBuffer.writeUInt32LE(rootBundleId);
254
+ return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("root_bundle"), seed.toArrayLike(Buffer, "le", 8), rootBundleIdBuffer], svmSpokeProgram.programId);
255
+ }
256
+ async function executeRootBalanceOnHubPool(solanaChainId) {
257
+ // Reconstruct the merkle tree for the pool rebalance.
258
+ const { poolRebalanceLeaf, poolRebalanceTree } = (0, helpers_2.constructEmptyPoolRebalanceTree)(solanaChainId, 0);
259
+ // Make sure the proposal liveness has passed, it has not been executed and rebalance root matches.
260
+ const currentRootBundleProposal = await hubPool.connect(ethersSigner).callStatic.rootBundleProposal();
261
+ if (currentRootBundleProposal.challengePeriodEndTimestamp > (await hubPool.callStatic.getCurrentTime()).toNumber())
262
+ throw new Error("Not passed liveness");
263
+ if (!currentRootBundleProposal.claimedBitMap.isZero())
264
+ throw new Error("Already claimed");
265
+ if (currentRootBundleProposal.poolRebalanceRoot !== poolRebalanceTree.getHexRoot())
266
+ throw new Error("Rebalance root mismatch");
267
+ // Execute the rebalance bundle on the HubPool.
268
+ const tx = await hubPool.connect(ethersSigner).executeRootBundle(solanaChainId, 0, // groupIndex
269
+ poolRebalanceLeaf.bundleLpFees, poolRebalanceLeaf.netSendAmounts, poolRebalanceLeaf.runningBalances, poolRebalanceLeaf.leafId, poolRebalanceLeaf.l1Tokens, poolRebalanceTree.getHexProof(poolRebalanceLeaf));
270
+ console.log(`✅ submitted tx hash: ${tx.hash}`);
271
+ await tx.wait();
272
+ console.log("✅ tx confirmed");
273
+ return tx.hash;
274
+ }
275
+ async function executeRelayerRefundLeaf(signer, program, statePda, rootBundle, relayerRefundLeaf, merkleTree, inputToken, rootBundleId) {
276
+ // Execute the single leaf
277
+ const proof = merkleTree.getProof(relayerRefundLeaf).map((p) => Array.from(p));
278
+ const leaf = relayerRefundLeaf;
279
+ const vault = (0, spl_token_1.getAssociatedTokenAddressSync)(inputToken, statePda, true, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
280
+ // Derive the transferLiability PDA
281
+ const [transferLiability] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("transfer_liability"), inputToken.toBuffer()], program.programId);
282
+ // Load the instruction parameters
283
+ const proofAsNumbers = proof.map((p) => Array.from(p));
284
+ console.log("loading execute relayer refund leaf params...");
285
+ const [instructionParams] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("instruction_params"), signer.publicKey.toBuffer()], program.programId);
286
+ const staticAccounts = {
287
+ instructionParams,
288
+ state: statePda,
289
+ rootBundle: rootBundle,
290
+ signer: signer.publicKey,
291
+ vault: vault,
292
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
293
+ mint: inputToken,
294
+ transferLiability,
295
+ systemProgram: anchor.web3.SystemProgram.programId,
296
+ // Appended by Acnhor `event_cpi` macro:
297
+ eventAuthority: web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], program.programId)[0],
298
+ program: program.programId,
299
+ };
300
+ const refundAccounts = [];
301
+ // Consolidate all above addresses into a single array for the Address Lookup Table (ALT).
302
+ const [lookupTableInstruction, lookupTableAddress] = await web3_js_1.AddressLookupTableProgram.createLookupTable({
303
+ authority: signer.publicKey,
304
+ payer: signer.publicKey,
305
+ recentSlot: await provider.connection.getSlot(),
306
+ });
307
+ // Submit the ALT creation transaction
308
+ await anchor.web3.sendAndConfirmTransaction(provider.connection, new anchor.web3.Transaction().add(lookupTableInstruction), [anchor.AnchorProvider.env().wallet.payer], { skipPreflight: true });
309
+ const lookupAddresses = [...Object.values(staticAccounts), ...refundAccounts];
310
+ // Create the transaction with the compute budget expansion instruction & use extended ALT account.
311
+ // Extend the ALT with all accounts
312
+ const maxExtendedAccounts = 30; // Maximum number of accounts that can be added to ALT in a single transaction.
313
+ for (let i = 0; i < lookupAddresses.length; i += maxExtendedAccounts) {
314
+ const extendInstruction = web3_js_1.AddressLookupTableProgram.extendLookupTable({
315
+ lookupTable: lookupTableAddress,
316
+ authority: signer.publicKey,
317
+ payer: signer.publicKey,
318
+ addresses: lookupAddresses.slice(i, i + maxExtendedAccounts),
319
+ });
320
+ await anchor.web3.sendAndConfirmTransaction(provider.connection, new anchor.web3.Transaction().add(extendInstruction), [anchor.AnchorProvider.env().wallet.payer], { skipPreflight: true });
321
+ }
322
+ // Fetch the AddressLookupTableAccount
323
+ const lookupTableAccount = (await provider.connection.getAddressLookupTable(lookupTableAddress)).value;
324
+ if (!lookupTableAccount) {
325
+ throw new Error("AddressLookupTableAccount not fetched");
326
+ }
327
+ await (0, utils_1.loadExecuteRelayerRefundLeafParams)(program, signer.publicKey, rootBundleId, leaf, proofAsNumbers);
328
+ console.log(`loaded execute relayer refund leaf params ${instructionParams}. \nExecuting relayer refund leaf...`);
329
+ const executeInstruction = await program.methods
330
+ .executeRelayerRefundLeaf()
331
+ .accounts(staticAccounts)
332
+ .remainingAccounts([])
333
+ .instruction();
334
+ // Create the versioned transaction
335
+ const computeBudgetInstruction = web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 });
336
+ const versionedTx = new web3_js_1.VersionedTransaction(new web3_js_1.TransactionMessage({
337
+ payerKey: signer.publicKey,
338
+ recentBlockhash: (await provider.connection.getLatestBlockhash()).blockhash,
339
+ instructions: [computeBudgetInstruction, executeInstruction],
340
+ }).compileToV0Message([lookupTableAccount]));
341
+ // Sign and submit the versioned transaction
342
+ versionedTx.sign([anchor.AnchorProvider.env().wallet.payer]);
343
+ const tx = await provider.connection.sendTransaction(versionedTx);
344
+ console.log(`Execute relayer refund leaf transaction sent: ${tx}`);
345
+ // Close the instruction parameters account
346
+ console.log("Closing instruction params...");
347
+ await new Promise((resolve) => setTimeout(resolve, 15000)); // Wait for the previous transaction to be processed.
348
+ const closeInstructionParamsTx = await program.methods.closeInstructionParams()
349
+ .accounts({ signer: signer.publicKey, instructionParams: instructionParams })
350
+ .rpc();
351
+ console.log(`Close instruction params transaction sent: ${closeInstructionParamsTx}`);
352
+ // Note we cant close the lookup table account as it needs to be both deactivated and expired at to do this.
353
+ }
354
+ // Run the executeRebalanceToHubPool function
355
+ executeRebalanceToHubPool();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ // This script executes root bundle on HubPool that rebalances tokens to Solana Spoke Pool. Required environment:
3
+ // - ETHERS_PROVIDER_URL: Ethereum RPC provider URL.
4
+ // - ETHERS_MNEMONIC: Mnemonic of the wallet that will sign the sending transaction on Ethereum
5
+ // - HUB_POOL_ADDRESS: Hub Pool address
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || function (mod) {
23
+ if (mod && mod.__esModule) return mod;
24
+ var result = {};
25
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
26
+ __setModuleDefault(result, mod);
27
+ return result;
28
+ };
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ const anchor = __importStar(require("@coral-xyz/anchor"));
34
+ const anchor_1 = require("@coral-xyz/anchor");
35
+ const web3_js_1 = require("@solana/web3.js");
36
+ const spl_token_1 = require("@solana/spl-token");
37
+ // eslint-disable-next-line camelcase
38
+ const constants_1 = require("../../utils/constants");
39
+ const yargs_1 = __importDefault(require("yargs"));
40
+ const helpers_1 = require("yargs/helpers");
41
+ const SvmUtils_1 = require("../../src/SvmUtils");
42
+ const ethers_1 = require("ethers");
43
+ // eslint-disable-next-line camelcase
44
+ const typechain_1 = require("../../typechain");
45
+ const constants_2 = require("./utils/constants");
46
+ const poolRebalanceTree_1 = require("./utils/poolRebalanceTree");
47
+ const cctpHelpers_1 = require("../../test/svm/cctpHelpers");
48
+ // Set up Solana provider.
49
+ const provider = anchor_1.AnchorProvider.env();
50
+ anchor.setProvider(provider);
51
+ // Get Solana programs.
52
+ const svmSpokeIdl = require("../../target/idl/svm_spoke.json");
53
+ const svmSpokeProgram = new anchor_1.Program(svmSpokeIdl, provider);
54
+ const messageTransmitterIdl = require("../../target/idl/message_transmitter.json");
55
+ const messageTransmitterProgram = new anchor_1.Program(messageTransmitterIdl, provider);
56
+ const tokenMessengerMinterIdl = require("../../target/idl/token_messenger_minter.json");
57
+ const tokenMessengerMinterProgram = new anchor_1.Program(tokenMessengerMinterIdl, provider);
58
+ // Set up Ethereum provider.
59
+ if (!process.env.ETHERS_PROVIDER_URL) {
60
+ throw new Error("Environment variable ETHERS_PROVIDER_URL is not set");
61
+ }
62
+ const ethersProvider = new ethers_1.ethers.providers.JsonRpcProvider(process.env.ETHERS_PROVIDER_URL);
63
+ if (!process.env.ETHERS_MNEMONIC) {
64
+ throw new Error("Environment variable ETHERS_MNEMONIC is not set");
65
+ }
66
+ const ethersSigner = ethers_1.ethers.Wallet.fromMnemonic(process.env.ETHERS_MNEMONIC).connect(ethersProvider);
67
+ // Get the HubPool contract instance.
68
+ if (!process.env.HUB_POOL_ADDRESS) {
69
+ throw new Error("Environment variable HUB_POOL_ADDRESS is not set");
70
+ }
71
+ const hubPoolAddress = ethers_1.ethers.utils.getAddress(process.env.HUB_POOL_ADDRESS);
72
+ const hubPool = typechain_1.HubPool__factory.connect(hubPoolAddress, ethersProvider);
73
+ // CCTP domains.
74
+ const remoteDomain = 0; // Ethereum
75
+ // Parse arguments
76
+ const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
77
+ .option("netSendAmount", { type: "string", demandOption: false, describe: "Net send amount to spoke" })
78
+ .option("resumeRemoteTx", { type: "string", demandOption: false, describe: "Resume receiving remote tx" })
79
+ .check((argv) => {
80
+ if (argv.netSendAmount !== undefined && argv.resumeRemoteTx !== undefined) {
81
+ throw new Error("Options --netSendAmount and --resumeRemoteTx are mutually exclusive");
82
+ }
83
+ if (argv.netSendAmount === undefined && argv.resumeRemoteTx === undefined) {
84
+ throw new Error("One of the options --netSendAmount or --resumeRemoteTx is required");
85
+ }
86
+ return true;
87
+ }).argv;
88
+ async function executeRebalanceToSpokePool() {
89
+ const resolvedArgv = await argv;
90
+ const seed = new anchor_1.BN(0); // Seed is always 0 for the state account PDA in public networks.
91
+ const netSendAmount = resolvedArgv.netSendAmount ? ethers_1.BigNumber.from(resolvedArgv.netSendAmount) : ethers_1.BigNumber.from(0);
92
+ const resumeRemoteTx = resolvedArgv.resumeRemoteTx;
93
+ // Resolve Solana cluster, EVM chain ID, Iris API URL and USDC addresses.
94
+ let isDevnet;
95
+ const solanaRpcEndpoint = provider.connection.rpcEndpoint;
96
+ if (solanaRpcEndpoint.includes("devnet"))
97
+ isDevnet = true;
98
+ else if (solanaRpcEndpoint.includes("mainnet"))
99
+ isDevnet = false;
100
+ else
101
+ throw new Error(`Unsupported solanaCluster endpoint: ${solanaRpcEndpoint}`);
102
+ const solanaCluster = isDevnet ? "devnet" : "mainnet";
103
+ const solanaChainId = ethers_1.BigNumber.from(BigInt(ethers_1.ethers.utils.keccak256(ethers_1.ethers.utils.toUtf8Bytes(`solana-${solanaCluster}`))) & BigInt("0xFFFFFFFFFFFFFFFF"));
104
+ const irisApiUrl = isDevnet ? constants_2.CIRCLE_IRIS_API_URL_DEVNET : constants_2.CIRCLE_IRIS_API_URL_MAINNET;
105
+ const supportedEvmChainId = isDevnet ? constants_1.CHAIN_IDs.SEPOLIA : constants_1.CHAIN_IDs.MAINNET; // Sepolia is bridged to devnet, Ethereum to mainnet in CCTP.
106
+ const evmChainId = (await ethersProvider.getNetwork()).chainId;
107
+ if (evmChainId !== supportedEvmChainId) {
108
+ throw new Error(`Chain ID ${evmChainId} does not match expected Solana cluster ${solanaCluster}`);
109
+ }
110
+ const l1TokenAddress = constants_1.TOKEN_SYMBOLS_MAP.USDC.addresses[evmChainId];
111
+ const solanaTokenKey = isDevnet ? new web3_js_1.PublicKey(constants_2.SOLANA_USDC_DEVNET) : new web3_js_1.PublicKey(constants_2.SOLANA_USDC_MAINNET);
112
+ console.log("Executing rebalance pool bundle to spoke...");
113
+ console.table([
114
+ { Property: "originChainId", Value: evmChainId.toString() },
115
+ { Property: "targetChainId", Value: solanaChainId.toString() },
116
+ { Property: "hubPoolAddress", Value: hubPool.address },
117
+ { Property: "l1TokenAddress", Value: l1TokenAddress },
118
+ { Property: "solanaTokenKey", Value: solanaTokenKey.toString() },
119
+ { Property: "svmSpokeProgramProgramId", Value: svmSpokeProgram.programId.toString() },
120
+ { Property: "providerPublicKey", Value: provider.wallet.publicKey.toString() },
121
+ { Property: "netSendAmount", Value: netSendAmount.toString() },
122
+ ]);
123
+ // Send executeRootBundle call from Ethereum, unless resuming a remote transaction.
124
+ let remoteTxHash;
125
+ if (!resumeRemoteTx) {
126
+ remoteTxHash = await executeRebalanceOnHubPool(l1TokenAddress, netSendAmount, solanaChainId);
127
+ }
128
+ else
129
+ remoteTxHash = resumeRemoteTx;
130
+ // Get Solana accounts required to receive tokens over CCTP.
131
+ const [statePda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)], svmSpokeProgram.programId);
132
+ const vault = (0, spl_token_1.getAssociatedTokenAddressSync)(solanaTokenKey, statePda, true);
133
+ const [messageTransmitterState] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("message_transmitter")], messageTransmitterProgram.programId);
134
+ const [authorityPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("message_transmitter_authority"), tokenMessengerMinterProgram.programId.toBuffer()], messageTransmitterProgram.programId);
135
+ const [tokenMessengerAccount] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("token_messenger")], tokenMessengerMinterProgram.programId);
136
+ const [remoteTokenMessengerKey] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("remote_token_messenger"), Buffer.from(remoteDomain.toString())], tokenMessengerMinterProgram.programId);
137
+ const [tokenMinterAccount] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("token_minter")], tokenMessengerMinterProgram.programId);
138
+ const [localToken] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("local_token"), solanaTokenKey.toBuffer()], tokenMessengerMinterProgram.programId);
139
+ const [tokenPair] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("token_pair"), Buffer.from(remoteDomain.toString()), (0, SvmUtils_1.evmAddressToPublicKey)(l1TokenAddress).toBuffer()], tokenMessengerMinterProgram.programId);
140
+ const [custodyTokenAccount] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("custody"), solanaTokenKey.toBuffer()], tokenMessengerMinterProgram.programId);
141
+ const [tokenMessengerEventAuthority] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], tokenMessengerMinterProgram.programId);
142
+ // Fetch attestation from CCTP attestation service.
143
+ const attestationResponse = await (0, cctpHelpers_1.getMessages)(remoteTxHash, remoteDomain, irisApiUrl);
144
+ const { attestation, message } = attestationResponse.messages[0];
145
+ console.log("CCTP attestation response:", attestationResponse.messages[0]);
146
+ // Accounts in CCTP message_transmitter receive_message instruction.
147
+ const nonce = (0, cctpHelpers_1.decodeMessageHeader)(Buffer.from(message.replace("0x", ""), "hex")).nonce;
148
+ const usedNonces = (await messageTransmitterProgram.methods
149
+ .getNoncePda({
150
+ nonce: new anchor_1.BN(nonce.toString()),
151
+ sourceDomain: remoteDomain,
152
+ })
153
+ .accounts({
154
+ messageTransmitter: messageTransmitterState,
155
+ })
156
+ .view());
157
+ const receiveMessageAccounts = {
158
+ payer: provider.wallet.publicKey,
159
+ caller: provider.wallet.publicKey,
160
+ authorityPda,
161
+ messageTransmitter: messageTransmitterState,
162
+ usedNonces,
163
+ receiver: tokenMessengerMinterProgram.programId,
164
+ systemProgram: web3_js_1.SystemProgram.programId,
165
+ };
166
+ // accountMetas list to pass to remaining accounts when receiving token bridge message via CCTP.
167
+ const remainingAccounts = [];
168
+ remainingAccounts.push({
169
+ isSigner: false,
170
+ isWritable: false,
171
+ pubkey: tokenMessengerAccount,
172
+ });
173
+ remainingAccounts.push({
174
+ isSigner: false,
175
+ isWritable: false,
176
+ pubkey: remoteTokenMessengerKey,
177
+ });
178
+ remainingAccounts.push({
179
+ isSigner: false,
180
+ isWritable: true,
181
+ pubkey: tokenMinterAccount,
182
+ });
183
+ remainingAccounts.push({
184
+ isSigner: false,
185
+ isWritable: true,
186
+ pubkey: localToken,
187
+ });
188
+ remainingAccounts.push({
189
+ isSigner: false,
190
+ isWritable: false,
191
+ pubkey: tokenPair,
192
+ });
193
+ remainingAccounts.push({
194
+ isSigner: false,
195
+ isWritable: true,
196
+ pubkey: vault,
197
+ });
198
+ remainingAccounts.push({
199
+ isSigner: false,
200
+ isWritable: true,
201
+ pubkey: custodyTokenAccount,
202
+ });
203
+ remainingAccounts.push({
204
+ isSigner: false,
205
+ isWritable: false,
206
+ pubkey: spl_token_1.TOKEN_PROGRAM_ID,
207
+ });
208
+ remainingAccounts.push({
209
+ isSigner: false,
210
+ isWritable: false,
211
+ pubkey: tokenMessengerEventAuthority,
212
+ });
213
+ remainingAccounts.push({
214
+ isSigner: false,
215
+ isWritable: false,
216
+ pubkey: tokenMessengerMinterProgram.programId,
217
+ });
218
+ // Receive tokens on Solana.
219
+ console.log(`Receiving ${netSendAmount.toString()} tokens on Solana...`);
220
+ const receiveMessageTx = await messageTransmitterProgram.methods
221
+ .receiveMessage({
222
+ message: Buffer.from(message.replace("0x", ""), "hex"),
223
+ attestation: Buffer.from(attestation.replace("0x", ""), "hex"),
224
+ })
225
+ .accounts(receiveMessageAccounts)
226
+ .remainingAccounts(remainingAccounts)
227
+ .rpc();
228
+ console.log("\nReceived remote message");
229
+ console.log("Your transaction signature", receiveMessageTx);
230
+ }
231
+ async function executeRebalanceOnHubPool(l1TokenAddress, netSendAmount, solanaChainId) {
232
+ // Reconstruct the merkle tree for the pool rebalance.
233
+ const { poolRebalanceTree, poolRebalanceLeaf } = (0, poolRebalanceTree_1.constructSimpleRebalanceTree)(l1TokenAddress, netSendAmount, solanaChainId);
234
+ // Make sure the proposal liveness has passed, it has not been executed and rebalance root matches.
235
+ const currentRootBundleProposal = await hubPool.connect(ethersSigner).callStatic.rootBundleProposal();
236
+ if (currentRootBundleProposal.challengePeriodEndTimestamp > (await hubPool.callStatic.getCurrentTime()).toNumber())
237
+ throw new Error("Not passed liveness");
238
+ if (!currentRootBundleProposal.claimedBitMap.isZero())
239
+ throw new Error("Already claimed");
240
+ if (currentRootBundleProposal.poolRebalanceRoot !== poolRebalanceTree.getHexRoot())
241
+ throw new Error("Rebalance root mismatch");
242
+ // Execute the rebalance bundle on the HubPool.
243
+ console.log(`Executing ${netSendAmount.toString()} rebalance to spoke pool:`);
244
+ const tx = await hubPool
245
+ .connect(ethersSigner)
246
+ .executeRootBundle(poolRebalanceLeaf.chainId, poolRebalanceLeaf.groupIndex, poolRebalanceLeaf.bundleLpFees, poolRebalanceLeaf.netSendAmounts, poolRebalanceLeaf.runningBalances, poolRebalanceLeaf.leafId, poolRebalanceLeaf.l1Tokens, poolRebalanceTree.getHexProof(poolRebalanceLeaf));
247
+ console.log(`✔️ submitted tx hash: ${tx.hash}`);
248
+ await tx.wait();
249
+ console.log(`✔️ tx confirmed`);
250
+ return tx.hash;
251
+ }
252
+ // Run the executeRebalanceToSpokePool function
253
+ executeRebalanceToSpokePool();
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Script: Propose Root Bundle for USDC Rebalance to Hub Pool
3
+ *
4
+ * Submits a root bundle proposal on the Hub Pool to rebalance USDC from the Solana Spoke Pool to the Ethereum Hub Pool.
5
+ * After submission and the liveness period, the rebalance can be executed with `executeRebalanceToHubPool.ts`.
6
+ *
7
+ * Required Environment Variables:
8
+ * - TESTNET: (Optional) Set to "true" to use Sepolia; defaults to mainnet.
9
+ * - MNEMONIC: Wallet mnemonic to sign the Ethereum transaction.
10
+ * - HUB_POOL_ADDRESS: Ethereum address of the Hub Pool.
11
+ * - NODE_URL_1: Ethereum RPC URL for mainnet (ignored if TESTNET=true).
12
+ * - NODE_URL_11155111: Ethereum RPC URL for Sepolia (ignored if TESTNET=false).
13
+ *
14
+ * Required Argument:
15
+ * - `--netSendAmount`: The unscaled amount of USDC to rebalance. (e.g., for USDC with 6 decimals, 1 = 0.000001 USDC).
16
+ *
17
+ * Example Usage:
18
+ * TESTNET=true \
19
+ * NODE_URL_11155111=$NODE_URL_11155111 \
20
+ * MNEMONIC=$MNEMONIC \
21
+ * HUB_POOL_ADDRESS=$HUB_POOL_ADDRESS \
22
+ * anchor run proposeRebalanceToHubPool -- --netSendAmount 7
23
+ *
24
+ * Note:
25
+ * Ensure the required environment variables are set before running this script.
26
+ */
27
+ export {};