|
|
@@ -0,0 +1,311 @@
|
|
|
+import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock";
|
|
|
+import dotenv from "dotenv";
|
|
|
+
|
|
|
+import {
|
|
|
+ RawSigner,
|
|
|
+ SUI_CLOCK_OBJECT_ID,
|
|
|
+ TransactionBlock,
|
|
|
+ fromB64,
|
|
|
+ normalizeSuiObjectId,
|
|
|
+ JsonRpcProvider,
|
|
|
+ Ed25519Keypair,
|
|
|
+ testnetConnection,
|
|
|
+ Connection,
|
|
|
+} from "@mysten/sui.js";
|
|
|
+import { execSync } from "child_process";
|
|
|
+import { resolve } from "path";
|
|
|
+import * as fs from "fs";
|
|
|
+
|
|
|
+import { REGISTRY, NETWORK } from "../registry";
|
|
|
+import { modifySignTransaction } from "@certusone/wormhole-sdk/lib/cjs/solana";
|
|
|
+
|
|
|
+dotenv.config({ path: "~/.env" });
|
|
|
+
|
|
|
+// Network dependent settings.
|
|
|
+let network = NETWORK.TESTNET; // <= NOTE: Update this when changing network
|
|
|
+const walletPrivateKey = process.env.SUI_TESTNET_ALT_KEY_BASE_64; // <= NOTE: Update this when changing network
|
|
|
+
|
|
|
+const guardianPrivateKey = process.env.WH_TESTNET_GUARDIAN_PRIVATE_KEY;
|
|
|
+
|
|
|
+const registry = REGISTRY[network];
|
|
|
+const provider = new JsonRpcProvider(
|
|
|
+ new Connection({ fullnode: registry["RPC_URL"] })
|
|
|
+);
|
|
|
+
|
|
|
+const PYTH_STATE_ID = registry["PYTH_STATE_ID"];
|
|
|
+const PYTH_PACKAGE_ID = registry["PYTH_PACKAGE_ID"];
|
|
|
+const WORMHOLE_STATE_ID = registry["WORMHOLE_STATE_ID"];
|
|
|
+const WORMHOLE_PACKAGE_ID = registry["WORMHOLE_PACKAGE_ID"];
|
|
|
+console.log("WORMHOLE_STATE_ID: ", WORMHOLE_STATE_ID);
|
|
|
+console.log("PYTH_STATE_ID: ", WORMHOLE_STATE_ID);
|
|
|
+
|
|
|
+const GOVERNANCE_EMITTER =
|
|
|
+ //"0000000000000000000000000000000000000000000000000000000000000004";
|
|
|
+ "63278d271099bfd491951b3e648f08b1c71631e4a53674ad43e8f9f98068c385";
|
|
|
+
|
|
|
+// To upgrade Pyth, take the following steps.
|
|
|
+// 0. Make contract changes in the "contracts" folder. These updated contracts will be posted on chain as an
|
|
|
+// entirely new package. The old package will still be valid unless we "brick" its call-sites explicitly
|
|
|
+// (this is done for you via the version control logic built into the Pyth contracts).
|
|
|
+// 1. Make sure that in version_control.move, you create a new struct for the new version and update the
|
|
|
+// current_version() and previous_version() functions accordingly. The former should point to the new version,
|
|
|
+// and the latter should point to the old version.
|
|
|
+// 2. Update the Move.toml file so that it points to a wormhole dependency whose Move.toml file has a "published-at" field
|
|
|
+// specified at the top with the correct address.
|
|
|
+// 3. Execute this script!
|
|
|
+//
|
|
|
+async function main() {
|
|
|
+ if (guardianPrivateKey === undefined) {
|
|
|
+ throw new Error("TESTNET_GUARDIAN_PRIVATE_KEY unset in environment");
|
|
|
+ }
|
|
|
+ if (walletPrivateKey === undefined) {
|
|
|
+ throw new Error("TESTNET_WALLET_PRIVATE_KEY unset in environment");
|
|
|
+ }
|
|
|
+ console.log("priv key: ", walletPrivateKey);
|
|
|
+
|
|
|
+ const wallet = new RawSigner(
|
|
|
+ Ed25519Keypair.fromSecretKey(
|
|
|
+ network == "MAINNET"
|
|
|
+ ? Buffer.from(walletPrivateKey, "hex")
|
|
|
+ : Buffer.from(walletPrivateKey, "base64")
|
|
|
+ ),
|
|
|
+ provider
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log("wallet address: ", wallet.getAddress());
|
|
|
+
|
|
|
+ const pythContractsPath = resolve(`${__dirname}/../../contracts`);
|
|
|
+
|
|
|
+ // Build for digest.
|
|
|
+ const { modules, dependencies, digest } =
|
|
|
+ buildForBytecodeAndDigest(pythContractsPath);
|
|
|
+ console.log("dependencies", dependencies);
|
|
|
+ console.log("digest", digest.toString("hex"));
|
|
|
+
|
|
|
+ // ===========================================================================================
|
|
|
+ // Construct VAA. We will use the signed VAA when we execute the upgrade.
|
|
|
+ // For a mainnet contract upgrade, we would not construct AND sign the VAA here. Instead, all
|
|
|
+ // the guardians would have to sign the upgrade VAA.
|
|
|
+ const guardians = new mock.MockGuardians(0, [guardianPrivateKey]);
|
|
|
+ const timestamp = 12345678;
|
|
|
+ const governance = new mock.GovernanceEmitter(GOVERNANCE_EMITTER);
|
|
|
+
|
|
|
+ const action = 0;
|
|
|
+ const chain = 21;
|
|
|
+
|
|
|
+ // construct VAA inner payload
|
|
|
+
|
|
|
+ const magic = Buffer.alloc(4);
|
|
|
+ magic.write("PTGM", 0); // magic
|
|
|
+ console.log("magic buffer: ", magic);
|
|
|
+
|
|
|
+ let inner_payload = Buffer.alloc(8); // 4 (magic) + 1 (module name) + 1 (action) + 2 (target chain) = 8
|
|
|
+ inner_payload.write(magic.toString(), 0); // magic = "PTGM"
|
|
|
+ inner_payload.writeUInt8(1, 4); // moduleName = 1
|
|
|
+ inner_payload.writeUInt8(0, 5); // action = 0
|
|
|
+ inner_payload.writeUInt16BE(21, 6); // target chain = 21
|
|
|
+ inner_payload = Buffer.concat([inner_payload, digest]);
|
|
|
+
|
|
|
+ console.log("digest: ", digest.toString("hex"));
|
|
|
+ console.log("inner payload: ", inner_payload.toString("hex"));
|
|
|
+
|
|
|
+ // create governance message
|
|
|
+ let msg = governance.publishGovernanceMessage(
|
|
|
+ timestamp,
|
|
|
+ "",
|
|
|
+ inner_payload,
|
|
|
+ action,
|
|
|
+ chain
|
|
|
+ );
|
|
|
+ msg.writeUInt8(0x1, 84 - 33 + 31); // here we insert an 0x1 in the right place to make the module name "0x00000000000000000000000000000001"
|
|
|
+
|
|
|
+ console.log("governance msg: ", msg.toString("hex"));
|
|
|
+
|
|
|
+ // sign governance message
|
|
|
+ const signedVaa = guardians.addSignatures(msg, [0]);
|
|
|
+ console.log("Upgrade VAA:", signedVaa.toString("hex"));
|
|
|
+ // ===========================================================================================
|
|
|
+
|
|
|
+ //Execute upgrade with signed governance VAA.
|
|
|
+ const upgradeResults = await upgradePyth(
|
|
|
+ wallet,
|
|
|
+ PYTH_STATE_ID,
|
|
|
+ WORMHOLE_STATE_ID,
|
|
|
+ modules,
|
|
|
+ dependencies,
|
|
|
+ signedVaa
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log("tx digest", upgradeResults.digest);
|
|
|
+ console.log("tx effects", JSON.stringify(upgradeResults.effects!));
|
|
|
+ console.log("tx events", JSON.stringify(upgradeResults.events!));
|
|
|
+
|
|
|
+ const migrateResults = await migratePyth(
|
|
|
+ wallet,
|
|
|
+ PYTH_STATE_ID,
|
|
|
+ WORMHOLE_STATE_ID,
|
|
|
+ signedVaa
|
|
|
+ );
|
|
|
+ console.log("tx digest", migrateResults.digest);
|
|
|
+ console.log("tx effects", JSON.stringify(migrateResults.effects!));
|
|
|
+ console.log("tx events", JSON.stringify(migrateResults.events!));
|
|
|
+}
|
|
|
+
|
|
|
+main();
|
|
|
+
|
|
|
+function buildForBytecodeAndDigest(packagePath: string) {
|
|
|
+ const buildOutput: {
|
|
|
+ modules: string[];
|
|
|
+ dependencies: string[];
|
|
|
+ digest: number[];
|
|
|
+ } = JSON.parse(
|
|
|
+ execSync(
|
|
|
+ `sui move build --dump-bytecode-as-base64 -p ${packagePath} 2> /dev/null`,
|
|
|
+ { encoding: "utf-8" }
|
|
|
+ )
|
|
|
+ );
|
|
|
+ return {
|
|
|
+ modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
|
|
|
+ dependencies: buildOutput.dependencies.map((d: string) =>
|
|
|
+ normalizeSuiObjectId(d)
|
|
|
+ ),
|
|
|
+ digest: Buffer.from(buildOutput.digest),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function getPackageId(
|
|
|
+ provider: JsonRpcProvider,
|
|
|
+ stateId: string
|
|
|
+): Promise<string> {
|
|
|
+ const state = await provider
|
|
|
+ .getObject({
|
|
|
+ id: stateId,
|
|
|
+ options: {
|
|
|
+ showContent: true,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .then((result) => {
|
|
|
+ if (result.data?.content?.dataType == "moveObject") {
|
|
|
+ return result.data.content.fields;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error("not move object");
|
|
|
+ });
|
|
|
+
|
|
|
+ if ("upgrade_cap" in state) {
|
|
|
+ return state.upgrade_cap.fields.package;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error("upgrade_cap not found");
|
|
|
+}
|
|
|
+
|
|
|
+async function upgradePyth(
|
|
|
+ signer: RawSigner,
|
|
|
+ pythStateId: string,
|
|
|
+ wormholeStateId: string,
|
|
|
+ modules: number[][],
|
|
|
+ dependencies: string[],
|
|
|
+ signedVaa: Buffer
|
|
|
+) {
|
|
|
+ const pythPackage = await getPackageId(signer.provider, pythStateId);
|
|
|
+ const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
|
|
|
+
|
|
|
+ console.log("pythPackage: ", pythPackage);
|
|
|
+ console.log("wormholePackage: ", wormholePackage);
|
|
|
+
|
|
|
+ const tx = new TransactionBlock();
|
|
|
+
|
|
|
+ const [verifiedVaa] = tx.moveCall({
|
|
|
+ target: `${wormholePackage}::vaa::parse_and_verify`,
|
|
|
+ arguments: [
|
|
|
+ tx.object(wormholeStateId),
|
|
|
+ tx.pure(Array.from(signedVaa)),
|
|
|
+ tx.object(SUI_CLOCK_OBJECT_ID),
|
|
|
+ ],
|
|
|
+ });
|
|
|
+
|
|
|
+ const [decreeTicket] = tx.moveCall({
|
|
|
+ target: `${pythPackage}::contract_upgrade::authorize_governance`,
|
|
|
+ arguments: [tx.object(pythStateId)],
|
|
|
+ });
|
|
|
+
|
|
|
+ const [decreeReceipt] = tx.moveCall({
|
|
|
+ target: `${wormholePackage}::governance_message::verify_vaa`,
|
|
|
+ arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
|
|
|
+ typeArguments: [`${pythPackage}::governance_witness::GovernanceWitness`],
|
|
|
+ });
|
|
|
+
|
|
|
+ // Authorize upgrade.
|
|
|
+ const [upgradeTicket] = tx.moveCall({
|
|
|
+ target: `${pythPackage}::contract_upgrade::authorize_upgrade`,
|
|
|
+ arguments: [tx.object(pythStateId), decreeReceipt],
|
|
|
+ });
|
|
|
+
|
|
|
+ // Build and generate modules and dependencies for upgrade.
|
|
|
+ const [upgradeReceipt] = tx.upgrade({
|
|
|
+ modules,
|
|
|
+ dependencies,
|
|
|
+ packageId: pythPackage,
|
|
|
+ ticket: upgradeTicket,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Commit upgrade.
|
|
|
+ tx.moveCall({
|
|
|
+ target: `${pythPackage}::contract_upgrade::commit_upgrade`,
|
|
|
+ arguments: [tx.object(pythStateId), upgradeReceipt],
|
|
|
+ });
|
|
|
+
|
|
|
+ tx.setGasBudget(2_000_000_000n);
|
|
|
+
|
|
|
+ return signer.signAndExecuteTransactionBlock({
|
|
|
+ transactionBlock: tx,
|
|
|
+ options: {
|
|
|
+ showEffects: true,
|
|
|
+ showEvents: true,
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function migratePyth(
|
|
|
+ signer: RawSigner,
|
|
|
+ pythStateId: string,
|
|
|
+ wormholeStateId: string,
|
|
|
+ signedUpgradeVaa: Buffer
|
|
|
+) {
|
|
|
+ const pythPackage = await getPackageId(signer.provider, pythStateId);
|
|
|
+ const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
|
|
|
+
|
|
|
+ const tx = new TransactionBlock();
|
|
|
+
|
|
|
+ const [verifiedVaa] = tx.moveCall({
|
|
|
+ target: `${wormholePackage}::vaa::parse_and_verify`,
|
|
|
+ arguments: [
|
|
|
+ tx.object(wormholeStateId),
|
|
|
+ tx.pure(Array.from(signedUpgradeVaa)),
|
|
|
+ tx.object(SUI_CLOCK_OBJECT_ID),
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ const [decreeTicket] = tx.moveCall({
|
|
|
+ target: `${pythPackage}::contract_upgrade::authorize_governance`,
|
|
|
+ arguments: [tx.object(pythStateId)],
|
|
|
+ });
|
|
|
+ const [decreeReceipt] = tx.moveCall({
|
|
|
+ target: `${wormholePackage}::governance_message::verify_vaa`,
|
|
|
+ arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
|
|
|
+ typeArguments: [`${pythPackage}::governance_witness::GovernanceWitness`],
|
|
|
+ });
|
|
|
+ tx.moveCall({
|
|
|
+ target: `${pythPackage}::migrate::migrate`,
|
|
|
+ arguments: [tx.object(pythStateId), decreeReceipt],
|
|
|
+ });
|
|
|
+
|
|
|
+ tx.setGasBudget(2_000_000_000n);
|
|
|
+
|
|
|
+ return signer.signAndExecuteTransactionBlock({
|
|
|
+ transactionBlock: tx,
|
|
|
+ options: {
|
|
|
+ showEffects: true,
|
|
|
+ showEvents: true,
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|