浏览代码

refactor(contract_manager): reduce duplicate code (#1311)

* feat(contract_manager): Add check for wormhole address on entropy deployment

* feat(contract_manager): Add getter for wormhole in entropy contract

* feat(contract_manager): topup entropy provider if necessary

* feat(contract_manager): find out wormhole address automatically for entropy

* feat(contract_manager): parse uri

* feat(contract_manager): Add list script for entropy contracts
Amin Moghaddam 1 年之前
父节点
当前提交
0d49986eb1

+ 3 - 1
contract_manager/package.json

@@ -11,7 +11,8 @@
   "scripts": {
     "build": "tsc",
     "shell": "ts-node ./src/shell.ts",
-    "lint": "eslint src/"
+    "lint": "eslint src/ scripts/",
+    "format": "prettier --write \"src/**/*.ts\" \"scripts/**/*.ts\""
   },
   "author": "",
   "license": "Apache-2.0",
@@ -27,6 +28,7 @@
     "@pythnetwork/entropy-sdk-solidity": "*",
     "@pythnetwork/price-service-client": "*",
     "@pythnetwork/pyth-sui-js": "*",
+    "@types/yargs": "^17.0.32",
     "aptos": "^1.5.0",
     "bs58": "^5.0.0",
     "ts-node": "^10.9.1",

+ 142 - 30
contract_manager/scripts/common.ts

@@ -1,8 +1,9 @@
-import { EvmChain, PrivateKey } from "../src";
+import { DefaultStore, EvmChain, PrivateKey } from "../src";
 import { existsSync, readFileSync, writeFileSync } from "fs";
 import { join } from "path";
 import Web3 from "web3";
 import { Contract } from "web3-eth-contract";
+import { InferredOptionType } from "yargs";
 
 interface DeployConfig {
   gasMultiplier: number;
@@ -23,39 +24,26 @@ export async function deployIfNotCached(
   deployArgs: any[], // eslint-disable-line  @typescript-eslint/no-explicit-any
   cacheKey?: string
 ): Promise<string> {
-  const cache = existsSync(cacheFile)
-    ? JSON.parse(readFileSync(cacheFile, "utf8"))
-    : {};
-
+  const runIfNotCached = makeCacheFunction(cacheFile);
   const key = cacheKey ?? `${chain.getId()}-${artifactName}`;
-  if (cache[key]) {
-    const address = cache[key];
-    console.log(
-      `Using cached deployment of ${artifactName} on ${chain.getId()} at ${address}`
+  return runIfNotCached(key, async () => {
+    const artifact = JSON.parse(
+      readFileSync(join(config.jsonOutputDir, `${artifactName}.json`), "utf8")
     );
-    return address;
-  }
-
-  const artifact = JSON.parse(
-    readFileSync(join(config.jsonOutputDir, `${artifactName}.json`), "utf8")
-  );
 
-  console.log(`Deploying ${artifactName} on ${chain.getId()}...`);
-
-  const addr = await chain.deploy(
-    config.privateKey,
-    artifact["abi"],
-    artifact["bytecode"],
-    deployArgs,
-    config.gasMultiplier,
-    config.gasPriceMultiplier
-  );
-
-  console.log(`✅ Deployed ${artifactName} on ${chain.getId()} at ${addr}`);
+    console.log(`Deploying ${artifactName} on ${chain.getId()}...`);
+    const addr = await chain.deploy(
+      config.privateKey,
+      artifact["abi"],
+      artifact["bytecode"],
+      deployArgs,
+      config.gasMultiplier,
+      config.gasPriceMultiplier
+    );
+    console.log(`✅ Deployed ${artifactName} on ${chain.getId()} at ${addr}`);
 
-  cache[key] = addr;
-  writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
-  return addr;
+    return addr;
+  });
 }
 
 export function getWeb3Contract(
@@ -69,3 +57,127 @@ export function getWeb3Contract(
   const web3 = new Web3();
   return new web3.eth.Contract(artifact["abi"], address);
 }
+
+export const COMMON_DEPLOY_OPTIONS = {
+  "std-output-dir": {
+    type: "string",
+    demandOption: true,
+    desc: "Path to the standard JSON output of the contracts (build artifact) directory",
+  },
+  "private-key": {
+    type: "string",
+    demandOption: true,
+    desc: "Private key to sign the trnasactions with",
+  },
+  chain: {
+    type: "array",
+    demandOption: true,
+    desc: "Chain to upload the contract on. Can be one of the evm chains available in the store",
+  },
+  "deployment-type": {
+    type: "string",
+    demandOption: false,
+    default: "stable",
+    desc: "Deployment type to use. Can be 'stable' or 'beta'",
+  },
+  "gas-multiplier": {
+    type: "number",
+    demandOption: false,
+    // Proxy (ERC1967) contract gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
+    default: 2,
+    desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
+  },
+  "gas-price-multiplier": {
+    type: "number",
+    demandOption: false,
+    default: 1,
+    desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
+  },
+  "save-contract": {
+    type: "boolean",
+    demandOption: false,
+    default: true,
+    desc: "Save the contract to the store",
+  },
+} as const;
+export const COMMON_UPGRADE_OPTIONS = {
+  testnet: {
+    type: "boolean",
+    default: false,
+    desc: "Upgrade testnet contracts instead of mainnet",
+  },
+  "all-chains": {
+    type: "boolean",
+    default: false,
+    desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
+  },
+  chain: {
+    type: "array",
+    string: true,
+    desc: "Chains to upgrade the contract on",
+  },
+  "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
+  "ops-key-path": {
+    type: "string",
+    demandOption: true,
+    desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
+  },
+  "std-output": {
+    type: "string",
+    demandOption: true,
+    desc: "Path to the standard JSON output of the pyth contract (build artifact)",
+  },
+} as const;
+
+export function makeCacheFunction(
+  cacheFile: string
+): (cacheKey: string, fn: () => Promise<string>) => Promise<string> {
+  async function runIfNotCached(
+    cacheKey: string,
+    fn: () => Promise<string>
+  ): Promise<string> {
+    const cache = existsSync(cacheFile)
+      ? JSON.parse(readFileSync(cacheFile, "utf8"))
+      : {};
+    if (cache[cacheKey]) {
+      return cache[cacheKey];
+    }
+    const result = await fn();
+    cache[cacheKey] = result;
+    writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
+    return result;
+  }
+
+  return runIfNotCached;
+}
+
+export function getSelectedChains(argv: {
+  chain: InferredOptionType<typeof COMMON_UPGRADE_OPTIONS["chain"]>;
+  testnet: InferredOptionType<typeof COMMON_UPGRADE_OPTIONS["testnet"]>;
+  allChains: InferredOptionType<typeof COMMON_UPGRADE_OPTIONS["all-chains"]>;
+}) {
+  const selectedChains: EvmChain[] = [];
+  if (argv.allChains && argv.chain)
+    throw new Error("Cannot use both --all-chains and --chain");
+  if (!argv.allChains && !argv.chain)
+    throw new Error("Must use either --all-chains or --chain");
+  for (const chain of Object.values(DefaultStore.chains)) {
+    if (!(chain instanceof EvmChain)) continue;
+    if (
+      (argv.allChains && chain.isMainnet() !== argv.testnet) ||
+      argv.chain?.includes(chain.getId())
+    )
+      selectedChains.push(chain);
+  }
+  if (argv.chain && selectedChains.length !== argv.chain.length)
+    throw new Error(
+      `Some chains were not found ${selectedChains
+        .map((chain) => chain.getId())
+        .toString()}`
+    );
+  for (const chain of selectedChains) {
+    if (chain.isMainnet() != selectedChains[0].isMainnet())
+      throw new Error("All chains must be either mainnet or testnet");
+  }
+  return selectedChains;
+}

+ 3 - 5
contract_manager/scripts/deploy_cosmwasm.ts

@@ -4,6 +4,8 @@ import { CosmWasmChain } from "../src/chains";
 import { CosmWasmPriceFeedContract } from "../src/contracts/cosmwasm";
 import { DefaultStore } from "../src/store";
 
+import { COMMON_DEPLOY_OPTIONS } from "./common";
+
 const parser = yargs(hideBin(process.argv))
   .scriptName("deploy_cosmwasm.ts")
   .usage(
@@ -15,11 +17,7 @@ const parser = yargs(hideBin(process.argv))
       demandOption: true,
       desc: "Path to the artifact .wasm file",
     },
-    "private-key": {
-      type: "string",
-      demandOption: true,
-      desc: "Private key to use for the deployment",
-    },
+    "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
     chain: {
       type: "string",
       demandOption: true,

+ 4 - 10
contract_manager/scripts/deploy_evm_contract.ts

@@ -5,6 +5,8 @@ import { DefaultStore } from "../src/store";
 import { readFileSync } from "fs";
 import { toPrivateKey } from "../src";
 
+import { COMMON_DEPLOY_OPTIONS } from "./common";
+
 const parser = yargs(hideBin(process.argv))
   .scriptName("deploy_evm_contract.ts")
   .usage(
@@ -16,16 +18,8 @@ const parser = yargs(hideBin(process.argv))
       demandOption: true,
       desc: "Path to the standard JSON output of the contract (build artifact)",
     },
-    "private-key": {
-      type: "string",
-      demandOption: true,
-      desc: "Private key to use for the deployment",
-    },
-    chain: {
-      type: "string",
-      demandOption: true,
-      desc: "Chain to upload the contract on. Can be one of the evm chains available in the store",
-    },
+    "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
+    chain: COMMON_DEPLOY_OPTIONS["chain"],
     "deploy-args": {
       type: "array",
       desc: "Arguments to pass to the contract constructor. Each argument must begin with 0x if it's a hex string",

+ 80 - 52
contract_manager/scripts/deploy_evm_entropy_contracts.ts

@@ -5,12 +5,19 @@ import { DefaultStore } from "../src/store";
 import {
   DeploymentType,
   EvmEntropyContract,
+  EvmPriceFeedContract,
   getDefaultDeploymentConfig,
   PrivateKey,
   toDeploymentType,
   toPrivateKey,
+  WormholeEvmContract,
 } from "../src";
-import { deployIfNotCached, getWeb3Contract } from "./common";
+import {
+  COMMON_DEPLOY_OPTIONS,
+  deployIfNotCached,
+  getWeb3Contract,
+} from "./common";
+import Web3 from "web3";
 
 type DeploymentConfig = {
   type: DeploymentType;
@@ -31,55 +38,15 @@ const ENTROPY_DEFAULT_PROVIDER = {
 const parser = yargs(hideBin(process.argv))
   .scriptName("deploy_evm_entropy_contracts.ts")
   .usage(
-    "Usage: $0 --std-output-dir <path/to/std-output-dir/> --private-key <private-key> --chain <chain0> --chain <chain1>"
+    "Usage: $0 --std-output-dir <path/to/std-output-dir/> --private-key <private-key> --chain <chain> --wormhole-addr <wormhole-addr>"
   )
   .options({
-    "std-output-dir": {
-      type: "string",
-      demandOption: true,
-      desc: "Path to the standard JSON output of the contracts (build artifact) directory",
-    },
-    "private-key": {
-      type: "string",
-      demandOption: true,
-      desc: "Private key to use for the deployment",
-    },
+    ...COMMON_DEPLOY_OPTIONS,
     chain: {
       type: "string",
       demandOption: true,
       desc: "Chain to upload the contract on. Can be one of the evm chains available in the store",
     },
-    "deployment-type": {
-      type: "string",
-      demandOption: false,
-      default: "stable",
-      desc: "Deployment type to use. Can be 'stable' or 'beta'",
-    },
-    "gas-multiplier": {
-      type: "number",
-      demandOption: false,
-      // Proxy (ERC1967) contract gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
-      default: 2,
-      desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
-    },
-    "gas-price-multiplier": {
-      type: "number",
-      demandOption: false,
-      default: 1,
-      desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
-    },
-    "save-contract": {
-      type: "boolean",
-      demandOption: false,
-      default: true,
-      desc: "Save the contract to the store",
-    },
-    // TODO: maintain a wormhole store
-    "wormhole-addr": {
-      type: "string",
-      demandOption: true,
-      desc: "Wormhole address",
-    },
   });
 
 async function deployExecutorContracts(
@@ -163,9 +130,67 @@ async function deployEntropyContracts(
   );
 }
 
+async function topupProviderIfNecessary(
+  chain: EvmChain,
+  deploymentConfig: DeploymentConfig
+) {
+  const provider = chain.isMainnet()
+    ? ENTROPY_DEFAULT_PROVIDER.mainnet
+    : ENTROPY_DEFAULT_PROVIDER.testnet;
+  const web3 = new Web3(chain.getRpcUrl());
+  const balance = Number(
+    web3.utils.fromWei(await web3.eth.getBalance(provider), "ether")
+  );
+  const MIN_BALANCE = 0.01;
+  console.log(`Provider balance: ${balance} ETH`);
+  if (balance < MIN_BALANCE) {
+    console.log(
+      `Balance is less than ${MIN_BALANCE}. Topping up the provider address...`
+    );
+    const signer = web3.eth.accounts.privateKeyToAccount(
+      deploymentConfig.privateKey
+    );
+    web3.eth.accounts.wallet.add(signer);
+    const tx = await web3.eth.sendTransaction({
+      from: signer.address,
+      to: provider,
+      gas: 30000,
+      value: web3.utils.toWei(`${MIN_BALANCE}`, "ether"),
+    });
+    console.log("Topped up the provider address. Tx: ", tx.transactionHash);
+  }
+}
+
+async function findWormholeAddress(
+  chain: EvmChain
+): Promise<string | undefined> {
+  for (const contract of Object.values(DefaultStore.contracts)) {
+    if (
+      contract instanceof EvmPriceFeedContract &&
+      contract.getChain().getId() === chain.getId()
+    ) {
+      return (await contract.getWormholeContract()).address;
+    }
+  }
+}
+
 async function main() {
   const argv = await parser.argv;
 
+  const chainName = argv.chain;
+  const chain = DefaultStore.chains[chainName];
+  if (!chain) {
+    throw new Error(`Chain ${chainName} not found`);
+  } else if (!(chain instanceof EvmChain)) {
+    throw new Error(`Chain ${chainName} is not an EVM chain`);
+  }
+
+  const wormholeAddr = await findWormholeAddress(chain);
+  if (!wormholeAddr) {
+    // TODO: deploy wormhole if necessary and maintain a wormhole store
+    throw new Error(`Wormhole contract not found for chain ${chain.getId()}`);
+  }
+
   const deploymentConfig: DeploymentConfig = {
     type: toDeploymentType(argv.deploymentType),
     gasMultiplier: argv.gasMultiplier,
@@ -173,21 +198,24 @@ async function main() {
     privateKey: toPrivateKey(argv.privateKey),
     jsonOutputDir: argv.stdOutputDir,
     saveContract: argv.saveContract,
-    wormholeAddr: argv.wormholeAddr,
+    wormholeAddr,
   };
+  const wormholeContract = new WormholeEvmContract(
+    chain,
+    deploymentConfig.wormholeAddr
+  );
+  const wormholeChainId = await wormholeContract.getChainId();
+  if (chain.getWormholeChainId() != wormholeChainId) {
+    throw new Error(
+      `Wormhole chain id mismatch. Expected ${chain.getWormholeChainId()} but got ${wormholeChainId}`
+    );
+  }
+  await topupProviderIfNecessary(chain, deploymentConfig);
 
   console.log(
     `Deployment config: ${JSON.stringify(deploymentConfig, null, 2)}\n`
   );
 
-  const chainName = argv.chain;
-  const chain = DefaultStore.chains[chainName];
-  if (!chain) {
-    throw new Error(`Chain ${chainName} not found`);
-  } else if (!(chain instanceof EvmChain)) {
-    throw new Error(`Chain ${chainName} is not an EVM chain`);
-  }
-
   console.log(`Deploying entropy contracts on ${chain.getId()}...`);
 
   const executorAddr = await deployExecutorContracts(chain, deploymentConfig);

+ 6 - 41
contract_manager/scripts/deploy_evm_pricefeed_contracts.ts

@@ -11,7 +11,11 @@ import {
   toPrivateKey,
   WormholeEvmContract,
 } from "../src";
-import { deployIfNotCached, getWeb3Contract } from "./common";
+import {
+  COMMON_DEPLOY_OPTIONS,
+  deployIfNotCached,
+  getWeb3Contract,
+} from "./common";
 
 type DeploymentConfig = {
   type: DeploymentType;
@@ -32,27 +36,7 @@ const parser = yargs(hideBin(process.argv))
     "Usage: $0 --std-output-dir <path/to/std-output-dir/> --private-key <private-key> --chain <chain0> --chain <chain1>"
   )
   .options({
-    "std-output-dir": {
-      type: "string",
-      demandOption: true,
-      desc: "Path to the standard JSON output of the contracts (build artifact) directory",
-    },
-    "private-key": {
-      type: "string",
-      demandOption: true,
-      desc: "Private key to use for the deployment",
-    },
-    chain: {
-      type: "array",
-      demandOption: true,
-      desc: "Chain to upload the contract on. Can be one of the evm chains available in the store",
-    },
-    "deployment-type": {
-      type: "string",
-      demandOption: false,
-      default: "stable",
-      desc: "Deployment type to use. Can be 'stable' or 'beta'",
-    },
+    ...COMMON_DEPLOY_OPTIONS,
     "valid-time-period-seconds": {
       type: "number",
       demandOption: false,
@@ -65,25 +49,6 @@ const parser = yargs(hideBin(process.argv))
       default: 1,
       desc: "Single update fee in wei for the price feed",
     },
-    "gas-multiplier": {
-      type: "number",
-      demandOption: false,
-      // Pyth Proxy (ERC1967) gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
-      default: 2,
-      desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
-    },
-    "gas-price-multiplier": {
-      type: "number",
-      demandOption: false,
-      default: 1,
-      desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
-    },
-    "save-contract": {
-      type: "boolean",
-      demandOption: false,
-      default: true,
-      desc: "Save the contract to the store",
-    },
   });
 
 async function deployWormholeReceiverContracts(

+ 3 - 5
contract_manager/scripts/execute_vaas.ts

@@ -7,6 +7,8 @@ import { decodeGovernancePayload } from "xc_admin_common";
 import { executeVaa } from "../src/executor";
 import { toPrivateKey } from "../src";
 
+import { COMMON_DEPLOY_OPTIONS } from "./common";
+
 const parser = yargs(hideBin(process.argv))
   .usage(
     "Tries to execute all vaas on a vault.\n" +
@@ -20,11 +22,7 @@ const parser = yargs(hideBin(process.argv))
       choices: ["mainnet", "devnet"],
       desc: "Which vault to use for fetching VAAs",
     },
-    "private-key": {
-      type: "string",
-      demandOption: true,
-      desc: "Private key to sign the transactions executing the governance VAAs. Hex format, without 0x prefix.",
-    },
+    "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
     offset: {
       type: "number",
       demandOption: true,

+ 48 - 0
contract_manager/scripts/list_entropy_contracts.ts

@@ -0,0 +1,48 @@
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import { DefaultStore } from "../src";
+import Web3 from "web3";
+
+const parser = yargs(hideBin(process.argv))
+  .usage("Usage: $0")
+  .options({
+    testnet: {
+      type: "boolean",
+      default: false,
+      desc: "Fetch testnet contract fees instead of mainnet",
+    },
+  });
+
+async function main() {
+  const argv = await parser.argv;
+  const entries = [];
+  for (const contract of Object.values(DefaultStore.entropy_contracts)) {
+    if (contract.getChain().isMainnet() === argv.testnet) continue;
+    try {
+      const provider = await contract.getDefaultProvider();
+      const w3 = new Web3(contract.getChain().getRpcUrl());
+      const balance = await w3.eth.getBalance(provider);
+      let version = "unknown";
+      try {
+        version = await contract.getVersion();
+      } catch (e) {
+        /* old deployments did not have this method */
+      }
+      const providerInfo = await contract.getProviderInfo(provider);
+      entries.push({
+        chain: contract.getChain().getId(),
+        contract: contract.address,
+        provider: providerInfo.uri,
+        balance,
+        seq: providerInfo.sequenceNumber,
+        version,
+      });
+      console.log(`Fetched info for ${contract.getId()}`);
+    } catch (e) {
+      console.error(`Error fetching info for ${contract.getId()}`, e);
+    }
+  }
+  console.table(entries);
+}
+
+main();

+ 1 - 7
contract_manager/scripts/list_evm_contracts.ts

@@ -1,11 +1,6 @@
 import yargs from "yargs";
 import { hideBin } from "yargs/helpers";
-import {
-  AptosPriceFeedContract,
-  CosmWasmPriceFeedContract,
-  DefaultStore,
-  EvmPriceFeedContract,
-} from "../src";
+import { DefaultStore, EvmPriceFeedContract } from "../src";
 
 const parser = yargs(hideBin(process.argv))
   .usage("Usage: $0")
@@ -23,7 +18,6 @@ async function main() {
   for (const contract of Object.values(DefaultStore.contracts)) {
     if (contract.getChain().isMainnet() === argv.testnet) continue;
     if (contract instanceof EvmPriceFeedContract) {
-      let wormholeContract = await contract.getWormholeContract();
       try {
         const version = await contract.getVersion();
         entries.push({

+ 1 - 0
contract_manager/scripts/sync_governance_vaas.ts

@@ -67,6 +67,7 @@ async function main() {
     lastExecuted = argv.offset - 1;
   }
   console.log("Starting from sequence number", lastExecuted);
+  // eslint-disable-next-line no-constant-condition
   while (true) {
     const submittedWormholeMessage = new SubmittedWormholeMessage(
       await matchedVault.getEmitter(),

+ 11 - 74
contract_manager/scripts/upgrade_evm_executor_contracts.ts

@@ -1,9 +1,16 @@
 import yargs from "yargs";
 import { hideBin } from "yargs/helpers";
-import { DefaultStore, EvmChain, loadHotWallet, toPrivateKey } from "../src";
-import { existsSync, readFileSync, writeFileSync } from "fs";
+import { DefaultStore, loadHotWallet, toPrivateKey } from "../src";
+import { readFileSync } from "fs";
+
+import {
+  COMMON_UPGRADE_OPTIONS,
+  getSelectedChains,
+  makeCacheFunction,
+} from "./common";
 
 const CACHE_FILE = ".cache-upgrade-evm-executor-contract";
+const runIfNotCached = makeCacheFunction(CACHE_FILE);
 
 const parser = yargs(hideBin(process.argv))
   .usage(
@@ -11,81 +18,11 @@ const parser = yargs(hideBin(process.argv))
       `Uses a cache file (${CACHE_FILE}) to avoid deploying contracts twice\n` +
       "Usage: $0 --chain <chain_1> --chain <chain_2> --private-key <private_key> --ops-key-path <ops_key_path> --std-output <std_output>"
   )
-  .options({
-    testnet: {
-      type: "boolean",
-      default: false,
-      desc: "Upgrade testnet contracts instead of mainnet",
-    },
-    "all-chains": {
-      type: "boolean",
-      default: false,
-      desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
-    },
-    chain: {
-      type: "array",
-      string: true,
-      desc: "Chains to upgrade the contract on",
-    },
-    "private-key": {
-      type: "string",
-      demandOption: true,
-      desc: "Private key to use for the deployment",
-    },
-    "ops-key-path": {
-      type: "string",
-      demandOption: true,
-      desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
-    },
-    "std-output": {
-      type: "string",
-      demandOption: true,
-      desc: "Path to the standard JSON output of the pyth contract (build artifact)",
-    },
-  });
-
-async function runIfNotCached(
-  cacheKey: string,
-  fn: () => Promise<string>
-): Promise<string> {
-  const cache = existsSync(CACHE_FILE)
-    ? JSON.parse(readFileSync(CACHE_FILE, "utf8"))
-    : {};
-  if (cache[cacheKey]) {
-    return cache[cacheKey];
-  }
-  const result = await fn();
-  cache[cacheKey] = result;
-  writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
-  return result;
-}
+  .options(COMMON_UPGRADE_OPTIONS);
 
 async function main() {
   const argv = await parser.argv;
-  const selectedChains: EvmChain[] = [];
-
-  if (argv.allChains && argv.chain)
-    throw new Error("Cannot use both --all-chains and --chain");
-  if (!argv.allChains && !argv.chain)
-    throw new Error("Must use either --all-chains or --chain");
-  for (const chain of Object.values(DefaultStore.chains)) {
-    if (!(chain instanceof EvmChain)) continue;
-    if (
-      (argv.allChains && chain.isMainnet() !== argv.testnet) ||
-      argv.chain?.includes(chain.getId())
-    )
-      selectedChains.push(chain);
-  }
-  if (argv.chain && selectedChains.length !== argv.chain.length)
-    throw new Error(
-      `Some chains were not found ${selectedChains
-        .map((chain) => chain.getId())
-        .toString()}`
-    );
-  for (const chain of selectedChains) {
-    if (chain.isMainnet() != selectedChains[0].isMainnet())
-      throw new Error("All chains must be either mainnet or testnet");
-  }
+  const selectedChains = getSelectedChains(argv);
 
   const vault =
     DefaultStore.vaults[

+ 11 - 74
contract_manager/scripts/upgrade_evm_pricefeed_contracts.ts

@@ -1,9 +1,16 @@
 import yargs from "yargs";
 import { hideBin } from "yargs/helpers";
-import { DefaultStore, EvmChain, loadHotWallet, toPrivateKey } from "../src";
-import { existsSync, readFileSync, writeFileSync } from "fs";
+import { DefaultStore, loadHotWallet, toPrivateKey } from "../src";
+import { readFileSync } from "fs";
+
+import {
+  COMMON_UPGRADE_OPTIONS,
+  getSelectedChains,
+  makeCacheFunction,
+} from "./common";
 
 const CACHE_FILE = ".cache-upgrade-evm";
+const runIfNotCached = makeCacheFunction(CACHE_FILE);
 
 const parser = yargs(hideBin(process.argv))
   .usage(
@@ -11,81 +18,11 @@ const parser = yargs(hideBin(process.argv))
       `Uses a cache file (${CACHE_FILE}) to avoid deploying contracts twice\n` +
       "Usage: $0 --chain <chain_1> --chain <chain_2> --private-key <private_key> --ops-key-path <ops_key_path> --std-output <std_output>"
   )
-  .options({
-    testnet: {
-      type: "boolean",
-      default: false,
-      desc: "Upgrade testnet contracts instead of mainnet",
-    },
-    "all-chains": {
-      type: "boolean",
-      default: false,
-      desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
-    },
-    chain: {
-      type: "array",
-      string: true,
-      desc: "Chains to upgrade the contract on",
-    },
-    "private-key": {
-      type: "string",
-      demandOption: true,
-      desc: "Private key to use for the deployment",
-    },
-    "ops-key-path": {
-      type: "string",
-      demandOption: true,
-      desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
-    },
-    "std-output": {
-      type: "string",
-      demandOption: true,
-      desc: "Path to the standard JSON output of the pyth contract (build artifact)",
-    },
-  });
-
-async function runIfNotCached(
-  cacheKey: string,
-  fn: () => Promise<string>
-): Promise<string> {
-  const cache = existsSync(CACHE_FILE)
-    ? JSON.parse(readFileSync(CACHE_FILE, "utf8"))
-    : {};
-  if (cache[cacheKey]) {
-    return cache[cacheKey];
-  }
-  const result = await fn();
-  cache[cacheKey] = result;
-  writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
-  return result;
-}
+  .options(COMMON_UPGRADE_OPTIONS);
 
 async function main() {
   const argv = await parser.argv;
-  const selectedChains: EvmChain[] = [];
-
-  if (argv.allChains && argv.chain)
-    throw new Error("Cannot use both --all-chains and --chain");
-  if (!argv.allChains && !argv.chain)
-    throw new Error("Must use either --all-chains or --chain");
-  for (const chain of Object.values(DefaultStore.chains)) {
-    if (!(chain instanceof EvmChain)) continue;
-    if (
-      (argv.allChains && chain.isMainnet() !== argv.testnet) ||
-      argv.chain?.includes(chain.getId())
-    )
-      selectedChains.push(chain);
-  }
-  if (argv.chain && selectedChains.length !== argv.chain.length)
-    throw new Error(
-      `Some chains were not found ${selectedChains
-        .map((chain) => chain.getId())
-        .toString()}`
-    );
-  for (const chain of selectedChains) {
-    if (chain.isMainnet() != selectedChains[0].isMainnet())
-      throw new Error("All chains must be either mainnet or testnet");
-  }
+  const selectedChains = getSelectedChains(argv);
 
   const vault =
     DefaultStore.vaults[

+ 66 - 3
contract_manager/src/contracts/evm.ts

@@ -49,6 +49,19 @@ const EXTENDED_ENTROPY_ABI = [
     stateMutability: "view",
     type: "function",
   },
+  {
+    inputs: [],
+    name: "version",
+    outputs: [
+      {
+        internalType: "string",
+        name: "",
+        type: "string",
+      },
+    ],
+    stateMutability: "pure",
+    type: "function",
+  },
   ...EntropyAbi,
 ] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any
 const EXTENDED_PYTH_ABI = [
@@ -327,6 +340,19 @@ const EXECUTOR_ABI = [
     stateMutability: "view",
     type: "function",
   },
+  {
+    inputs: [],
+    name: "owner",
+    outputs: [
+      {
+        internalType: "address",
+        name: "",
+        type: "address",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
 ] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any
 export class WormholeEvmContract extends WormholeContract {
   constructor(public chain: EvmChain, public address: string) {
@@ -408,10 +434,19 @@ export class EvmEntropyContract extends Storable {
     return `${this.chain.getId()}_${this.address}`;
   }
 
+  getChain(): EvmChain {
+    return this.chain;
+  }
+
   getType(): string {
     return EvmEntropyContract.type;
   }
 
+  async getVersion(): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.version().call();
+  }
+
   static fromJson(
     chain: Chain,
     parsed: { type: string; address: string }
@@ -465,12 +500,17 @@ export class EvmEntropyContract extends Storable {
     return this.generateExecutorPayload(executorAddr, executorAddr, data);
   }
 
-  getOwner(): string {
+  async getOwner(): Promise<string> {
     const contract = this.getContract();
     return contract.methods.owner().call();
   }
 
-  getPendingOwner(): string {
+  async getExecutorContract(): Promise<EvmExecutorContract> {
+    const owner = await this.getOwner();
+    return new EvmExecutorContract(this.chain, owner);
+  }
+
+  async getPendingOwner(): Promise<string> {
     const contract = this.getContract();
     return contract.methods.pendingOwner().call();
   }
@@ -495,7 +535,13 @@ export class EvmEntropyContract extends Storable {
 
   async getProviderInfo(address: string): Promise<EntropyProviderInfo> {
     const contract = this.getContract();
-    return await contract.methods.getProviderInfo(address).call();
+    const info: EntropyProviderInfo = await contract.methods
+      .getProviderInfo(address)
+      .call();
+    return {
+      ...info,
+      uri: Web3.utils.toAscii(info.uri),
+    };
   }
 }
 
@@ -506,6 +552,15 @@ export class EvmExecutorContract {
     return `${this.chain.getId()}_${this.address}`;
   }
 
+  async getWormholeContract(): Promise<WormholeEvmContract> {
+    const web3 = new Web3(this.chain.getRpcUrl());
+    //Unfortunately, there is no public method to get the wormhole address
+    //Found 251 by using `forge build --extra-output storageLayout` and finding the slot for the wormhole variable.
+    let address = await web3.eth.getStorageAt(this.address, 251);
+    address = "0x" + address.slice(26);
+    return new WormholeEvmContract(this.chain, address);
+  }
+
   getContract() {
     const web3 = new Web3(this.chain.getRpcUrl());
     return new web3.eth.Contract(EXECUTOR_ABI, this.address);
@@ -529,6 +584,14 @@ export class EvmExecutorContract {
     };
   }
 
+  /**
+   * Returns the owner of the executor contract, this should always be the contract address itself
+   */
+  async getOwner(): Promise<string> {
+    const contract = this.getContract();
+    return contract.methods.owner().call();
+  }
+
   async executeGovernanceInstruction(
     senderPrivateKey: PrivateKey,
     vaa: Buffer

+ 8 - 6
package-lock.json

@@ -45,6 +45,7 @@
         "@pythnetwork/entropy-sdk-solidity": "*",
         "@pythnetwork/price-service-client": "*",
         "@pythnetwork/pyth-sui-js": "*",
+        "@types/yargs": "^17.0.32",
         "aptos": "^1.5.0",
         "bs58": "^5.0.0",
         "ts-node": "^10.9.1",
@@ -20713,9 +20714,9 @@
       }
     },
     "node_modules/@types/yargs": {
-      "version": "17.0.20",
-      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.20.tgz",
-      "integrity": "sha512-eknWrTHofQuPk2iuqDm1waA7V6xPlbgBoaaXEgYkClhLOnB0TtbW+srJaOToAgawPxPlHQzwypFA2bhZaUGP5A==",
+      "version": "17.0.32",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
+      "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
       "dependencies": {
         "@types/yargs-parser": "*"
       }
@@ -77587,9 +77588,9 @@
       }
     },
     "@types/yargs": {
-      "version": "17.0.20",
-      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.20.tgz",
-      "integrity": "sha512-eknWrTHofQuPk2iuqDm1waA7V6xPlbgBoaaXEgYkClhLOnB0TtbW+srJaOToAgawPxPlHQzwypFA2bhZaUGP5A==",
+      "version": "17.0.32",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
+      "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
       "requires": {
         "@types/yargs-parser": "*"
       }
@@ -81421,6 +81422,7 @@
         "@pythnetwork/entropy-sdk-solidity": "*",
         "@pythnetwork/price-service-client": "*",
         "@pythnetwork/pyth-sui-js": "*",
+        "@types/yargs": "^17.0.32",
         "aptos": "^1.5.0",
         "bs58": "^5.0.0",
         "prettier": "^2.6.2",