diff --git a/contract_manager/package.json b/contract_manager/package.json index b1901abb5..948541010 100644 --- a/contract_manager/package.json +++ b/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", diff --git a/contract_manager/scripts/common.ts b/contract_manager/scripts/common.ts index b92bbf169..f191dd3da 100644 --- a/contract_manager/scripts/common.ts +++ b/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 { - 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) => Promise { + async function runIfNotCached( + cacheKey: string, + fn: () => Promise + ): Promise { + 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; + testnet: InferredOptionType; + allChains: InferredOptionType; +}) { + 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; +} diff --git a/contract_manager/scripts/deploy_cosmwasm.ts b/contract_manager/scripts/deploy_cosmwasm.ts index 8212f109b..b5852fddb 100644 --- a/contract_manager/scripts/deploy_cosmwasm.ts +++ b/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, diff --git a/contract_manager/scripts/deploy_evm_contract.ts b/contract_manager/scripts/deploy_evm_contract.ts index dfc18e771..552b0811a 100644 --- a/contract_manager/scripts/deploy_evm_contract.ts +++ b/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", diff --git a/contract_manager/scripts/deploy_evm_entropy_contracts.ts b/contract_manager/scripts/deploy_evm_entropy_contracts.ts index c9f6cfff7..79c7906d1 100644 --- a/contract_manager/scripts/deploy_evm_entropy_contracts.ts +++ b/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 --private-key --chain --chain " + "Usage: $0 --std-output-dir --private-key --chain --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 { + 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); diff --git a/contract_manager/scripts/deploy_evm_pricefeed_contracts.ts b/contract_manager/scripts/deploy_evm_pricefeed_contracts.ts index 8a1d78411..de9057b82 100644 --- a/contract_manager/scripts/deploy_evm_pricefeed_contracts.ts +++ b/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 --private-key --chain --chain " ) .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( diff --git a/contract_manager/scripts/execute_vaas.ts b/contract_manager/scripts/execute_vaas.ts index 504b4ae6f..2c43fa24a 100644 --- a/contract_manager/scripts/execute_vaas.ts +++ b/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, diff --git a/contract_manager/scripts/list_entropy_contracts.ts b/contract_manager/scripts/list_entropy_contracts.ts new file mode 100644 index 000000000..37a69729f --- /dev/null +++ b/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(); diff --git a/contract_manager/scripts/list_evm_contracts.ts b/contract_manager/scripts/list_evm_contracts.ts index 7c36eea33..2c154dfd4 100644 --- a/contract_manager/scripts/list_evm_contracts.ts +++ b/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({ diff --git a/contract_manager/scripts/sync_governance_vaas.ts b/contract_manager/scripts/sync_governance_vaas.ts index dc87c9028..1fa2ba383 100644 --- a/contract_manager/scripts/sync_governance_vaas.ts +++ b/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(), diff --git a/contract_manager/scripts/upgrade_evm_executor_contracts.ts b/contract_manager/scripts/upgrade_evm_executor_contracts.ts index ecfc87fda..c9622a1f3 100644 --- a/contract_manager/scripts/upgrade_evm_executor_contracts.ts +++ b/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 --private-key --ops-key-path --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 -): Promise { - 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[ diff --git a/contract_manager/scripts/upgrade_evm_pricefeed_contracts.ts b/contract_manager/scripts/upgrade_evm_pricefeed_contracts.ts index 58cdce540..aee6547c6 100644 --- a/contract_manager/scripts/upgrade_evm_pricefeed_contracts.ts +++ b/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 --private-key --ops-key-path --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 -): Promise { - 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[ diff --git a/contract_manager/src/contracts/evm.ts b/contract_manager/src/contracts/evm.ts index 75939c897..3b08315bd 100644 --- a/contract_manager/src/contracts/evm.ts +++ b/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 { + 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 { const contract = this.getContract(); return contract.methods.owner().call(); } - getPendingOwner(): string { + async getExecutorContract(): Promise { + const owner = await this.getOwner(); + return new EvmExecutorContract(this.chain, owner); + } + + async getPendingOwner(): Promise { const contract = this.getContract(); return contract.methods.pendingOwner().call(); } @@ -495,7 +535,13 @@ export class EvmEntropyContract extends Storable { async getProviderInfo(address: string): Promise { 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 { + 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 { + const contract = this.getContract(); + return contract.methods.owner().call(); + } + async executeGovernanceInstruction( senderPrivateKey: PrivateKey, vaa: Buffer diff --git a/package-lock.json b/package-lock.json index 4c8d68b57..345095371 100644 --- a/package-lock.json +++ b/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",