From 2782c6bf643ab115701bc8a0de1011f285a05377 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Khashkhashi Moghaddam Date: Mon, 31 Jul 2023 11:43:58 +0100 Subject: [PATCH] [aptos] Aptos cleanup (#994) * Better errors * Better variable typing * Upgrade cli with more straightforward flow * Remove unnecessary configs and distinguish between deployer contract and signer * Convert the named-addresses argument to 3 separate options with default values * Add README --- contract_manager/src/governance.ts | 9 +- target_chains/aptos/cli/README.md | 74 +++++++ target_chains/aptos/cli/package.json | 1 + target_chains/aptos/cli/src/cli.ts | 3 +- target_chains/aptos/cli/src/commands/aptos.ts | 199 +++++++++--------- 5 files changed, 178 insertions(+), 108 deletions(-) create mode 100644 target_chains/aptos/cli/README.md diff --git a/contract_manager/src/governance.ts b/contract_manager/src/governance.ts index 6a14207f..ec58700f 100644 --- a/contract_manager/src/governance.ts +++ b/contract_manager/src/governance.ts @@ -47,7 +47,7 @@ export class SubmittedWormholeMessage { constructor( public emitter: PublicKey, public sequenceNumber: number, - public cluster: string + public cluster: PythCluster ) {} /** @@ -107,8 +107,7 @@ export class SubmittedWormholeMessage { * @param waitingSeconds how long to wait before giving up */ async fetchVaa(waitingSeconds: number = 1): Promise { - let rpcUrl = - WORMHOLE_API_ENDPOINT[this.cluster as keyof typeof WORMHOLE_API_ENDPOINT]; + let rpcUrl = WORMHOLE_API_ENDPOINT[this.cluster]; let startTime = Date.now(); while (Date.now() - startTime < waitingSeconds * 1000) { @@ -176,6 +175,10 @@ export class WormholeEmitter { this.cluster = asPythCluster(cluster); } + public getEmitter() { + return this.wallet.publicKey; + } + async sendMessage(payload: Buffer) { const provider = new AnchorProvider( new Connection(getPythClusterApiUrl(this.cluster), "confirmed"), diff --git a/target_chains/aptos/cli/README.md b/target_chains/aptos/cli/README.md new file mode 100644 index 00000000..09d6ecb4 --- /dev/null +++ b/target_chains/aptos/cli/README.md @@ -0,0 +1,74 @@ +# Pre-requisites + +Install aptos cli with the same version specified in the ci workflows. + +All the commands which submit transactions require an environment variable for the private key to be set. +Depending on the network, this can be either `APTOS_LOCALNET_KEY`, `APTOS_TESTNET_KEY` or `APTOS_MAINNET_KEY`. + +# Deploying from scratch + +In addition to the wormhole dependency we depend on the deployer contract that facilitates the ownership of package upgrade +capability. You can read more about it [here](https://github.com/wormhole-foundation/wormhole/blob/5255e933d68629f0643207b0f9d3fa797af5cbf7/aptos/deployer/sources/deployer.move). + +Assuming the wormhole and deployer contracts are already deployed, we can deploy the pyth oracle with the following command: + +```bash +npm run cli deploy-pyth ../contracts -n testnet +``` + +`seed` can be any random string that is used for determining a specific contract address based on the seed value and the signer address. + +You can manually specify the address of wormhole and deployer contracts with `--wormhole` and `--deployer` flags. +This requires the addresses to be empty in the `Move.toml` file for the pyth package: + +```toml +[addresses] +pyth = "_" +deployer = "_" +wormhole = "_" +``` + +### Initializing pyth + +You can run the following to initialize the pyth contract, the following is a sample (testnet) config: + +```bash +npm run cli init-pyth -n testnet \ +--stale-price-threshold 60 \ +--update-fee 1 \ +--governance-emitter-chain-id 1 \ +--governance-emitter-address 63278d271099bfd491951b3e648f08b1c71631e4a53674ad43e8f9f98068c385 \ +--data-source-chain-ids 1 \ +--data-source-chain-ids 26 \ +--data-source-chain-ids 26 \ +--data-source-emitter-addresses f346195ac02f37d60d4db8ffa6ef74cb1be3550047543a4a9ee9acf4d78697b0 \ +--data-source-emitter-addresses a27839d641b07743c0cb5f68c51f8cd31d2c0762bec00dc6fcd25433ef1ab5b6 \ +--data-source-emitter-addresses e101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71 +``` + +Note that the `data-source-chain-ids` are paired with `data-source-emitter-addresses` and their order matters. + +# Upgrade process: + +The following steps are needed to upgrade our aptos contracts: + +- Generate the hash for the new contract build +- Create a governance proposal, proposing the aptos package to be upgraded to this specific hash +- Approve and execute the governance proposal +- Run the upgrade transaction and publish the new package + +## Generating the new contract hash: + +Run the following command to generate the new hash, this will assume the default deployed addresses of deployer, wormhole, and pyth, but you can override them if necessary. + +```bash +npm run cli hash-contracts ../contracts +``` + +## Upgrading the contract + +To upgrade the contract after the governance vaa was executed run: + +```bash +npm run cli upgrade ../contracts +``` diff --git a/target_chains/aptos/cli/package.json b/target_chains/aptos/cli/package.json index f1d98636..08e2547a 100644 --- a/target_chains/aptos/cli/package.json +++ b/target_chains/aptos/cli/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "cli": "ts-node src/cli.ts", "build": "npx tsc && pkg . --targets node14-linux-x64 --output build/pyth" }, "author": "", diff --git a/target_chains/aptos/cli/src/cli.ts b/target_chains/aptos/cli/src/cli.ts index c722abd3..5a72a018 100644 --- a/target_chains/aptos/cli/src/cli.ts +++ b/target_chains/aptos/cli/src/cli.ts @@ -2,5 +2,6 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; +import { builder } from "./commands/aptos"; -yargs(hideBin(process.argv)).commandDir("commands").strict().argv; +builder(yargs(hideBin(process.argv))).argv; diff --git a/target_chains/aptos/cli/src/commands/aptos.ts b/target_chains/aptos/cli/src/commands/aptos.ts index 9545c73d..498e5b4f 100644 --- a/target_chains/aptos/cli/src/commands/aptos.ts +++ b/target_chains/aptos/cli/src/commands/aptos.ts @@ -1,6 +1,6 @@ +import { Argv } from "yargs"; import { spawnSync } from "child_process"; -import { BCS, AptosClient, AptosAccount, TxnBuilderTypes } from "aptos"; -import type { CommandBuilder } from "yargs"; +import { AptosAccount, AptosClient, BCS, TxnBuilderTypes } from "aptos"; import fs from "fs"; import sha3 from "js-sha3"; import { ethers } from "ethers"; @@ -14,18 +14,29 @@ interface Network { endpoint: string; // Private key of the network key: string | undefined; - // Address of the Pyth deployer contract - deployer: string; - // The Pyth deployer contract seed used to generate the derived address of the Pyth contract - pythDeployerSeed: string; } -const network = { +const NETWORK_OPTION = { alias: "n", describe: "network", type: "string", choices: [LOCALNET, TESTNET, MAINNET], - required: true, + demandOption: true, +} as const; +const DEPLOYER_OPTION = { + describe: "deployer contract address deployed in the network", + type: "string", + default: "0xb31e712b26fd295357355f6845e77c888298636609e93bc9b05f0f604049f434", +} as const; +const WORMHOLE_OPTION = { + describe: "wormhole contract address deployed in the network", + type: "string", + default: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625", +} as const; +const PYTH_OPTION = { + describe: "pyth contract address deployed in the network", + type: "string", + default: "0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387", } as const; interface Package { @@ -45,9 +56,6 @@ const networks = new Map([ { key: process.env["APTOS_LOCALNET_KEY"], endpoint: "http://0.0.0.0:8080", - deployer: - "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b", - pythDeployerSeed: "pyth", }, ], [ @@ -55,10 +63,6 @@ const networks = new Map([ { key: process.env["APTOS_TESTNET_KEY"], endpoint: "https://fullnode.testnet.aptoslabs.com/v1", - deployer: - "0xb31e712b26fd295357355f6845e77c888298636609e93bc9b05f0f604049f434", - // A Wormhole redeploy meant we had to use different seeds for testnet and mainnet - pythDeployerSeed: "pyth-a8d0d", }, ], [ @@ -66,16 +70,10 @@ const networks = new Map([ { key: process.env["APTOS_MAINNET_KEY"], endpoint: "https://fullnode.mainnet.aptoslabs.com/v1", - deployer: - "0xb31e712b26fd295357355f6845e77c888298636609e93bc9b05f0f604049f434", - pythDeployerSeed: "pyth-a8d0d", }, ], ]); - -export const command: string = "aptos "; - -export const builder: CommandBuilder = (yargs) => +export const builder: (args: Argv) => Argv = (yargs) => yargs .command( "deploy ", @@ -85,7 +83,7 @@ export const builder: CommandBuilder = (yargs) => .positional("package-dir", { type: "string" }) .positional("account", { type: "string" }) .option("named-addresses", { type: "string" }) - .option("network", network); + .option("network", NETWORK_OPTION); }, async (argv) => { const artefact = serializePackage( @@ -104,27 +102,40 @@ export const builder: CommandBuilder = (yargs) => } ) .command( - "deploy-resource ", - "Deploy a package using a resource account", + "deploy-pyth ", + "Deploy the pyth package using a resource account.", (yargs) => { return yargs .positional("package-dir", { type: "string" }) .positional("seed", { type: "string" }) - .option("named-addresses", { type: "string" }) - .option("network", network); + .option("deployer", DEPLOYER_OPTION) + .option("wormhole", WORMHOLE_OPTION) + .option("network", NETWORK_OPTION); }, async (argv) => { - const artefact = serializePackage( - buildPackage(argv["package-dir"]!, argv["named-addresses"]) + const sender = getSender(argv.network); + const derivedAddress = generateDerivedAddress( + sender.address().toString(), + argv.seed! ); + + const namedAddresses = `wormhole=${argv.wormhole},deployer=${argv.deployer},pyth=0x${derivedAddress}`; + console.log("Building the package with the following named addresses:"); + console.log(`Wormhole=${argv.wormhole}`); + console.log(`Deployer=${argv.deployer}`); + console.log(`Pyth=${derivedAddress}`); + const artifact = serializePackage( + buildPackage(argv["package-dir"]!, namedAddresses) + ); + const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction( TxnBuilderTypes.EntryFunction.natural( - networks.get(argv.network)!.deployer + "::deployer", + argv.deployer + "::deployer", "deploy_derived", [], [ - artefact.meta, - artefact.bytecodes, + artifact.meta, + artifact.bytecodes, BCS.bcsSerializeBytes(Buffer.from(argv["seed"]!, "ascii")), ] ) @@ -134,20 +145,15 @@ export const builder: CommandBuilder = (yargs) => } ) .command( - "derived-address ", - "Generate the derived address for the given deployer seed", + "derived-address ", + "Generate the derived address for the given seed and sender address", (yargs) => { return yargs - .positional("seed", { type: "string" }) - .option("network", network); + .positional("seed", { type: "string", demandOption: true }) + .positional("signer", { type: "string", demandOption: true }); }, async (argv) => { - console.log( - generateDerivedAddress( - networks.get(argv.network)!.deployer, - argv.seed! - ) - ); + console.log(generateDerivedAddress(argv.signer, argv.seed)); } ) .command( @@ -155,29 +161,29 @@ export const builder: CommandBuilder = (yargs) => "Init Wormhole core contract", (yargs) => { return yargs - .option("network", network) + .option("network", NETWORK_OPTION) .option("chain-id", { describe: "Chain id", type: "number", default: 22, - required: false, + demandOption: false, }) .option("governance-chain-id", { describe: "Governance chain id", type: "number", default: 1, - required: false, + demandOption: false, }) .option("governance-address", { describe: "Governance address", type: "string", default: "0x0000000000000000000000000000000000000000000000000000000000000004", - required: false, + demandOption: false, }) .option("guardian-address", { alias: "g", - required: true, + demandOption: true, describe: "Initial guardian's address", type: "string", }); @@ -202,8 +208,9 @@ export const builder: CommandBuilder = (yargs) => BCS.bcsSerializeBytes(Buffer.from(governance_address, "hex")), guardian_addresses_serializer.getBytes(), ]; + const sender = getSender(argv.network); const wormholeAddress = generateDerivedAddress( - networks.get(argv.network)!.deployer!, + sender.address().hex(), "wormhole" ); const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction( @@ -219,40 +226,41 @@ export const builder: CommandBuilder = (yargs) => } ) .command( - "init-pyth", + "init-pyth ", "Init Pyth contract", (yargs) => { return yargs - .option("network", network) + .positional("seed", { type: "string", demandOption: true }) + .option("network", NETWORK_OPTION) .option("stale-price-threshold", { describe: "Stale price threshold", type: "number", - required: true, + demandOption: true, }) .option("governance-emitter-chain-id", { describe: "Governance emitter chain id", type: "number", - required: true, + demandOption: true, }) .option("governance-emitter-address", { describe: "Governance emitter address", type: "string", - required: true, + demandOption: true, }) .option("update-fee", { describe: "Update fee", type: "number", - required: true, + demandOption: true, }) .option("data-source-chain-ids", { describe: "Data source chain IDs", type: "array", - required: true, + demandOption: true, }) .option("data-source-emitter-addresses", { describe: "Data source emitter addresses", type: "array", - required: true, + demandOption: true, }); }, async (argv) => { @@ -266,8 +274,8 @@ export const builder: CommandBuilder = (yargs) => dataSourceChainIdsSerializer.serializeU32AsUleb128( argv["data-source-chain-ids"].length ); - argv["data-source-chain-ids"].forEach((chain_id) => - dataSourceChainIdsSerializer.serializeU64(chain_id as number) + argv["data-source-chain-ids"].forEach((chain_id: number) => + dataSourceChainIdsSerializer.serializeU64(chain_id) ); const dataSourceEmitterAddressesSerializer = new BCS.Serializer(); @@ -289,9 +297,10 @@ export const builder: CommandBuilder = (yargs) => dataSourceEmitterAddressesSerializer.getBytes(), BCS.bcsSerializeUint64(update_fee), ]; + const sender = getSender(argv.network); const pythAddress = generateDerivedAddress( - networks.get(argv.network)!.deployer!, - networks.get(argv.network)!.pythDeployerSeed! + sender.address().hex(), + argv.seed ); const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction( TxnBuilderTypes.EntryFunction.natural( @@ -307,17 +316,20 @@ export const builder: CommandBuilder = (yargs) => ) .command( "hash-contracts ", - "Hash contract bytecodes for upgrade", + "Hash contract bytecodes for upgrade, the named addresses should be the same as the currently deployed ones", (yargs) => { return yargs .positional("package-dir", { type: "string", required: true, }) - .option("named-addresses", { type: "string" }); + .option("deployer", DEPLOYER_OPTION) + .option("wormhole", WORMHOLE_OPTION) + .option("pyth", PYTH_OPTION); }, (argv) => { - const p = buildPackage(argv["package-dir"]!, argv["named-addresses"]); + const namedAddresses = `wormhole=${argv.wormhole},deployer=${argv.deployer},pyth=${argv.pyth}`; + const p = buildPackage(argv["package-dir"]!, namedAddresses); const b = serializePackage(p); console.log(Buffer.from(b.codeHash).toString("hex")); } @@ -331,18 +343,18 @@ export const builder: CommandBuilder = (yargs) => type: "string", required: true, }) - .option("network", network) - .option("named-addresses", { type: "string" }); + .option("network", NETWORK_OPTION) + .option("deployer", DEPLOYER_OPTION) + .option("wormhole", WORMHOLE_OPTION) + .option("pyth", PYTH_OPTION); }, async (argv) => { + const namedAddresses = `wormhole=${argv.wormhole},deployer=${argv.deployer},pyth=${argv.pyth}`; const artefact = serializePackage( - buildPackage(argv["package-dir"]!, argv["named-addresses"]) + buildPackage(argv["package-dir"]!, namedAddresses) ); - let pythAddress = generateDerivedAddress( - networks.get(argv.network)!.deployer!, - networks.get(argv.network)!.pythDeployerSeed! - ); + let pythAddress = argv.pyth; const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction( TxnBuilderTypes.EntryFunction.natural( `${pythAddress}::contract_upgrade`, @@ -355,42 +367,24 @@ export const builder: CommandBuilder = (yargs) => await executeTransaction(argv.network, txPayload); } ) - .command( - "execute-governance ", - "Execute a governance instruction on the Pyth contract", - (_yargs) => { - return yargs - .positional("vaa-bytes", { - type: "string", - }) - .option("network", network); - }, - async (argv) => { - let pythAddress = generateDerivedAddress( - networks.get(argv.network)!.deployer!, - networks.get(argv.network)!.pythDeployerSeed! - ); - const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction( - TxnBuilderTypes.EntryFunction.natural( - `${pythAddress}::governance`, - "execute_governance_instruction", - [], - [BCS.bcsSerializeBytes(Buffer.from(argv.vaaBytes!, "hex"))] - ) - ); + .demandCommand(); - await executeTransaction(argv.network, txPayload); - } +function getSender(network: string) { + if (networks.get(network)!.key === undefined) { + throw new Error( + `No key for network ${network}. Please set the APTOS_${network.toUpperCase()}_KEY environment variable.` ); - + } + return new AptosAccount( + new Uint8Array(Buffer.from(networks.get(network)!.key!, "hex")) + ); +} async function executeTransaction( network: string, txPayload: TxnBuilderTypes.TransactionPayloadEntryFunction ) { const client = new AptosClient(networks.get(network)!.endpoint); - const sender = new AptosAccount( - new Uint8Array(Buffer.from(networks.get(network)!.key!, "hex")) - ); + const sender = getSender(network); console.log( await client.generateSignSubmitWaitForTransaction(sender, txPayload, { maxGasAmount: BigInt(30000), @@ -413,15 +407,12 @@ function hexStringToByteArray(hexString: string) { return byteArray; } -function generateDerivedAddress( - deployer_address: string, - seed: string -): string { +function generateDerivedAddress(signer_address: string, seed: string): string { let derive_resource_account_scheme = Buffer.alloc(1); derive_resource_account_scheme.writeUInt8(255); return sha3.sha3_256( Buffer.concat([ - hexStringToByteArray(deployer_address), + hexStringToByteArray(signer_address), Buffer.from(seed, "ascii"), derive_resource_account_scheme, ])