Skip to content

Commit

Permalink
fix: scripts to rebalance funds from hub to spoke (#783)
Browse files Browse the repository at this point in the history
* fix: script to propose rebalance to spoke

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: use zero hash

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: remove unused import

Signed-off-by: Reinis Martinsons <[email protected]>

* fix

Signed-off-by: Reinis Martinsons <[email protected]>

* fix

Signed-off-by: Reinis Martinsons <[email protected]>

* fix

Signed-off-by: Reinis Martinsons <[email protected]>

* fix: execute rebalance script

Signed-off-by: Reinis Martinsons <[email protected]>

---------

Signed-off-by: Reinis Martinsons <[email protected]>
  • Loading branch information
Reinis-FRP authored Dec 4, 2024
1 parent 1cdef0d commit 0785cf2
Show file tree
Hide file tree
Showing 4 changed files with 429 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ simpleFill = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/simpleFill.ts"
simpleFakeRelayerRepayment = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/simpleFakeRelayerRepayment.ts"
closeRelayerPdas = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/closeRelayerPdas.ts"
closeDataWorkerLookUpTables = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/closeDataWorkerLookUpTables.ts"
proposeRebalanceToSpokePool = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/proposeRebalanceToSpokePool.ts"
executeRebalanceToSpokePool = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/executeRebalanceToSpokePool.ts"
remotePauseDeposits = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/remotePauseDeposits.ts"
remoteHubPoolSetDepositRoute = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/remoteHubPoolSetDepositRoute.ts"
remoteHubPoolPauseDeposits = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/remoteHubPoolPauseDeposits.ts"
Expand Down
293 changes: 293 additions & 0 deletions scripts/svm/executeRebalanceToSpokePool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
// This script executes root bundle on HubPool that rebalances tokens to Solana Spoke Pool. Required environment:
// - ETHERS_PROVIDER_URL: Ethereum RPC provider URL.
// - ETHERS_MNEMONIC: Mnemonic of the wallet that will sign the sending transaction on Ethereum
// - HUB_POOL_ADDRESS: Hub Pool address

import * as anchor from "@coral-xyz/anchor";
import { BN, Program, AnchorProvider } from "@coral-xyz/anchor";
import { AccountMeta, PublicKey, SystemProgram } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from "@solana/spl-token";
// eslint-disable-next-line camelcase
import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../utils/constants";
import { SvmSpoke } from "../../target/types/svm_spoke";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { evmAddressToPublicKey } from "../../src/SvmUtils";
import { MessageTransmitter } from "../../target/types/message_transmitter";
import { TokenMessengerMinter } from "../../target/types/token_messenger_minter";
import { ethers, BigNumber } from "ethers";
// eslint-disable-next-line camelcase
import { HubPool__factory } from "../../typechain";
import {
CIRCLE_IRIS_API_URL_DEVNET,
CIRCLE_IRIS_API_URL_MAINNET,
SOLANA_USDC_DEVNET,
SOLANA_USDC_MAINNET,
} from "./utils/constants";
import { constructSimpleRebalanceTree } from "./utils/poolRebalanceTree";
import { decodeMessageHeader, getMessages } from "../../test/svm/cctpHelpers";

// Set up Solana provider.
const provider = AnchorProvider.env();
anchor.setProvider(provider);

// Get Solana programs.
const svmSpokeIdl = require("../../target/idl/svm_spoke.json");
const svmSpokeProgram = new Program<SvmSpoke>(svmSpokeIdl, provider);
const messageTransmitterIdl = require("../../target/idl/message_transmitter.json");
const messageTransmitterProgram = new Program<MessageTransmitter>(messageTransmitterIdl, provider);
const tokenMessengerMinterIdl = require("../../target/idl/token_messenger_minter.json");
const tokenMessengerMinterProgram = new Program<TokenMessengerMinter>(tokenMessengerMinterIdl, provider);

// Set up Ethereum provider.
if (!process.env.ETHERS_PROVIDER_URL) {
throw new Error("Environment variable ETHERS_PROVIDER_URL is not set");
}
const ethersProvider = new ethers.providers.JsonRpcProvider(process.env.ETHERS_PROVIDER_URL);
if (!process.env.ETHERS_MNEMONIC) {
throw new Error("Environment variable ETHERS_MNEMONIC is not set");
}
const ethersSigner = ethers.Wallet.fromMnemonic(process.env.ETHERS_MNEMONIC).connect(ethersProvider);

// Get the HubPool contract instance.
if (!process.env.HUB_POOL_ADDRESS) {
throw new Error("Environment variable HUB_POOL_ADDRESS is not set");
}
const hubPoolAddress = ethers.utils.getAddress(process.env.HUB_POOL_ADDRESS);
const hubPool = HubPool__factory.connect(hubPoolAddress, ethersProvider);

// CCTP domains.
const remoteDomain = 0; // Ethereum

// Parse arguments
const argv = yargs(hideBin(process.argv))
.option("netSendAmount", { type: "string", demandOption: false, describe: "Net send amount to spoke" })
.option("resumeRemoteTx", { type: "string", demandOption: false, describe: "Resume receiving remote tx" })
.check((argv) => {
if (argv.netSendAmount !== undefined && argv.resumeRemoteTx !== undefined) {
throw new Error("Options --netSendAmount and --resumeRemoteTx are mutually exclusive");
}
if (argv.netSendAmount === undefined && argv.resumeRemoteTx === undefined) {
throw new Error("One of the options --netSendAmount or --resumeRemoteTx is required");
}
return true;
}).argv;

async function executeRebalanceToSpokePool(): Promise<void> {
const resolvedArgv = await argv;
const seed = new BN(0); // Seed is always 0 for the state account PDA in public networks.
const netSendAmount = resolvedArgv.netSendAmount ? BigNumber.from(resolvedArgv.netSendAmount) : BigNumber.from(0);
const resumeRemoteTx = resolvedArgv.resumeRemoteTx;

// Resolve Solana cluster, EVM chain ID, Iris API URL and USDC addresses.
let isDevnet: boolean;
const solanaRpcEndpoint = provider.connection.rpcEndpoint;
if (solanaRpcEndpoint.includes("devnet")) isDevnet = true;
else if (solanaRpcEndpoint.includes("mainnet")) isDevnet = false;
else throw new Error(`Unsupported solanaCluster endpoint: ${solanaRpcEndpoint}`);
const solanaCluster = isDevnet ? "devnet" : "mainnet";
const solanaChainId = BigNumber.from(
BigInt(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`solana-${solanaCluster}`))) & BigInt("0xFFFFFFFFFFFFFFFF")
);
const irisApiUrl = isDevnet ? CIRCLE_IRIS_API_URL_DEVNET : CIRCLE_IRIS_API_URL_MAINNET;
const supportedEvmChainId = isDevnet ? CHAIN_IDs.SEPOLIA : CHAIN_IDs.MAINNET; // Sepolia is bridged to devnet, Ethereum to mainnet in CCTP.
const evmChainId = (await ethersProvider.getNetwork()).chainId;
if (evmChainId !== supportedEvmChainId) {
throw new Error(`Chain ID ${evmChainId} does not match expected Solana cluster ${solanaCluster}`);
}
const l1TokenAddress = TOKEN_SYMBOLS_MAP.USDC.addresses[evmChainId];
const solanaTokenKey = isDevnet ? new PublicKey(SOLANA_USDC_DEVNET) : new PublicKey(SOLANA_USDC_MAINNET);

console.log("Executing rebalance pool bundle to spoke...");
console.table([
{ Property: "originChainId", Value: evmChainId.toString() },
{ Property: "targetChainId", Value: solanaChainId.toString() },
{ Property: "hubPoolAddress", Value: hubPool.address },
{ Property: "l1TokenAddress", Value: l1TokenAddress },
{ Property: "solanaTokenKey", Value: solanaTokenKey.toString() },
{ Property: "svmSpokeProgramProgramId", Value: svmSpokeProgram.programId.toString() },
{ Property: "providerPublicKey", Value: provider.wallet.publicKey.toString() },
{ Property: "netSendAmount", Value: netSendAmount.toString() },
]);

// Send executeRootBundle call from Ethereum, unless resuming a remote transaction.
let remoteTxHash: string;
if (!resumeRemoteTx) {
remoteTxHash = await executeRebalanceOnHubPool(l1TokenAddress, netSendAmount, solanaChainId);
} else remoteTxHash = resumeRemoteTx;

// Get Solana accounts required to receive tokens over CCTP.
const [statePda] = PublicKey.findProgramAddressSync(
[Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)],
svmSpokeProgram.programId
);
const vault = getAssociatedTokenAddressSync(solanaTokenKey, statePda, true);
const [messageTransmitterState] = PublicKey.findProgramAddressSync(
[Buffer.from("message_transmitter")],
messageTransmitterProgram.programId
);
const [authorityPda] = PublicKey.findProgramAddressSync(
[Buffer.from("message_transmitter_authority"), tokenMessengerMinterProgram.programId.toBuffer()],
messageTransmitterProgram.programId
);
const [tokenMessengerAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("token_messenger")],
tokenMessengerMinterProgram.programId
);
const [remoteTokenMessengerKey] = PublicKey.findProgramAddressSync(
[Buffer.from("remote_token_messenger"), Buffer.from(remoteDomain.toString())],
tokenMessengerMinterProgram.programId
);
const [tokenMinterAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("token_minter")],
tokenMessengerMinterProgram.programId
);
const [localToken] = PublicKey.findProgramAddressSync(
[Buffer.from("local_token"), solanaTokenKey.toBuffer()],
tokenMessengerMinterProgram.programId
);
const [tokenPair] = PublicKey.findProgramAddressSync(
[Buffer.from("token_pair"), Buffer.from(remoteDomain.toString()), evmAddressToPublicKey(l1TokenAddress).toBuffer()],
tokenMessengerMinterProgram.programId
);
const [custodyTokenAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("custody"), solanaTokenKey.toBuffer()],
tokenMessengerMinterProgram.programId
);
const [tokenMessengerEventAuthority] = PublicKey.findProgramAddressSync(
[Buffer.from("__event_authority")],
tokenMessengerMinterProgram.programId
);

// Fetch attestation from CCTP attestation service.
const attestationResponse = await getMessages(remoteTxHash, remoteDomain, irisApiUrl);
const { attestation, message } = attestationResponse.messages[0];
console.log("CCTP attestation response:", attestationResponse.messages[0]);

// Accounts in CCTP message_transmitter receive_message instruction.
const nonce = decodeMessageHeader(Buffer.from(message.replace("0x", ""), "hex")).nonce;
const usedNonces = (await messageTransmitterProgram.methods
.getNoncePda({
nonce: new BN(nonce.toString()),
sourceDomain: remoteDomain,
})
.accounts({
messageTransmitter: messageTransmitterState,
})
.view()) as PublicKey;
const receiveMessageAccounts = {
payer: provider.wallet.publicKey,
caller: provider.wallet.publicKey,
authorityPda,
messageTransmitter: messageTransmitterState,
usedNonces,
receiver: tokenMessengerMinterProgram.programId,
systemProgram: SystemProgram.programId,
};

// accountMetas list to pass to remaining accounts when receiving token bridge message via CCTP.
const remainingAccounts: AccountMeta[] = [];
remainingAccounts.push({
isSigner: false,
isWritable: false,
pubkey: tokenMessengerAccount,
});
remainingAccounts.push({
isSigner: false,
isWritable: false,
pubkey: remoteTokenMessengerKey,
});
remainingAccounts.push({
isSigner: false,
isWritable: true,
pubkey: tokenMinterAccount,
});
remainingAccounts.push({
isSigner: false,
isWritable: true,
pubkey: localToken,
});
remainingAccounts.push({
isSigner: false,
isWritable: false,
pubkey: tokenPair,
});
remainingAccounts.push({
isSigner: false,
isWritable: true,
pubkey: vault,
});
remainingAccounts.push({
isSigner: false,
isWritable: true,
pubkey: custodyTokenAccount,
});
remainingAccounts.push({
isSigner: false,
isWritable: false,
pubkey: TOKEN_PROGRAM_ID,
});
remainingAccounts.push({
isSigner: false,
isWritable: false,
pubkey: tokenMessengerEventAuthority,
});
remainingAccounts.push({
isSigner: false,
isWritable: false,
pubkey: tokenMessengerMinterProgram.programId,
});

// Receive tokens on Solana.
console.log(`Receiving ${netSendAmount.toString()} tokens on Solana...`);
const receiveMessageTx = await messageTransmitterProgram.methods
.receiveMessage({
message: Buffer.from(message.replace("0x", ""), "hex"),
attestation: Buffer.from(attestation.replace("0x", ""), "hex"),
})
.accounts(receiveMessageAccounts as any)
.remainingAccounts(remainingAccounts)
.rpc();
console.log("\nReceived remote message");
console.log("Your transaction signature", receiveMessageTx);
}

async function executeRebalanceOnHubPool(l1TokenAddress: string, netSendAmount: BigNumber, solanaChainId: BigNumber) {
// Reconstruct the merkle tree for the pool rebalance.
const { poolRebalanceTree, poolRebalanceLeaf } = constructSimpleRebalanceTree(
l1TokenAddress,
netSendAmount,
solanaChainId
);

// Make sure the proposal liveness has passed, it has not been executed and rebalance root matches.
const currentRootBundleProposal = await hubPool.connect(ethersSigner).callStatic.rootBundleProposal();
if (currentRootBundleProposal.challengePeriodEndTimestamp > (await hubPool.callStatic.getCurrentTime()).toNumber())
throw new Error("Not passed liveness");
if (!currentRootBundleProposal.claimedBitMap.isZero()) throw new Error("Already claimed");
if (currentRootBundleProposal.poolRebalanceRoot !== poolRebalanceTree.getHexRoot())
throw new Error("Rebalance root mismatch");

// Execute the rebalance bundle on the HubPool.
console.log(`Executing ${netSendAmount.toString()} rebalance to spoke pool:`);
const tx = await hubPool
.connect(ethersSigner)
.executeRootBundle(
poolRebalanceLeaf.chainId,
poolRebalanceLeaf.groupIndex,
poolRebalanceLeaf.bundleLpFees,
poolRebalanceLeaf.netSendAmounts,
poolRebalanceLeaf.runningBalances,
poolRebalanceLeaf.leafId,
poolRebalanceLeaf.l1Tokens,
poolRebalanceTree.getHexProof(poolRebalanceLeaf)
);
console.log(`✔️ submitted tx hash: ${tx.hash}`);
await tx.wait();
console.log(`✔️ tx confirmed`);

return tx.hash;
}

// Run the executeRebalanceToSpokePool function
executeRebalanceToSpokePool();
Loading

0 comments on commit 0785cf2

Please sign in to comment.