From 1b8fee9022daa810cc3f0989babaf2756a231970 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 5 Jun 2024 10:57:08 -0700 Subject: [PATCH] [contract_manager] Add some nice-to-have features (#1650) * gr * some stuff * comment * gr * cleanup --- contract_manager/scripts/check_proposal.ts | 15 +++- .../scripts/upgrade_evm_entropy_contracts.ts | 73 +++++++++++++------ contract_manager/src/chains.ts | 19 +++-- contract_manager/src/governance.ts | 8 ++ 4 files changed, 83 insertions(+), 32 deletions(-) diff --git a/contract_manager/scripts/check_proposal.ts b/contract_manager/scripts/check_proposal.ts index 3beff23284..0ca63af516 100644 --- a/contract_manager/scripts/check_proposal.ts +++ b/contract_manager/scripts/check_proposal.ts @@ -140,7 +140,7 @@ async function main() { if (instruction.governanceAction instanceof EvmExecute) { // Note: it only checks for upgrade entropy contracts right now console.log( - `Verifying EVMExecute Contract on ${instruction.governanceAction.targetChainId}` + `Verifying EVMExecute on ${instruction.governanceAction.targetChainId}` ); for (const chain of Object.values(DefaultStore.chains)) { if ( @@ -153,7 +153,8 @@ async function main() { const callAddress = instruction.governanceAction.callAddress; const calldata = instruction.governanceAction.calldata; - // currently executor is only being used by the entropy contract + // TODO: If we add additional EVM contracts using the executor, we need to + // add some logic here to identify what kind of contract is at the call address. const contract = new EvmEntropyContract(chain, callAddress); const owner = await contract.getOwner(); @@ -169,10 +170,13 @@ async function main() { continue; } + // TODO: This logic assumes we are calling upgradeTo on the contract at callAddress. + // In the future, this logic may need to be generalized to support calling other functions. + const invokedMethod = "upgradeTo(address)"; const calldataHex = calldata.toString("hex"); const web3 = new Web3(); const methodSignature = web3.eth.abi - .encodeFunctionSignature("upgradeTo(address)") + .encodeFunctionSignature(invokedMethod) .replace("0x", ""); let newImplementationAddress: string | undefined = undefined; @@ -196,7 +200,10 @@ async function main() { ); // this should be the same keccak256 of the deployedCode property generated by truffle console.log( - `${chain.getId()} new implementation address:${newImplementationAddress} digest:${newImplementationCode}` + `${chain.getId()} call ${invokedMethod} with arguments (${newImplementationAddress}) on ${contract.getType()} at address:${callAddress} from executor:${executorAddress}.` + ); + console.log( + `${chain.getId()} new implementation address:${newImplementationAddress} has code digest:${newImplementationCode}` ); } } diff --git a/contract_manager/scripts/upgrade_evm_entropy_contracts.ts b/contract_manager/scripts/upgrade_evm_entropy_contracts.ts index c6a167ba06..8797d5b866 100644 --- a/contract_manager/scripts/upgrade_evm_entropy_contracts.ts +++ b/contract_manager/scripts/upgrade_evm_entropy_contracts.ts @@ -2,6 +2,7 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { DefaultStore, loadHotWallet, toPrivateKey } from "../src"; import { readFileSync } from "fs"; +import { PythCluster } from "@pythnetwork/client/lib/cluster"; import { COMMON_UPGRADE_OPTIONS, @@ -27,6 +28,18 @@ const parser = yargs(hideBin(process.argv)) }, }); +// Override these URLs to use a different RPC node for mainnet / testnet. +// TODO: extract these RPCs to a config file (?) +const RPCS = { + "mainnet-beta": "https://api.mainnet-beta.solana.com", + testnet: "https://api.testnet.solana.com", + devnet: "https://api.devnet.solana.com", +} as Record; + +function registry(cluster: PythCluster): string { + return RPCS[cluster]; +} + async function main() { const argv = await parser.argv; const cacheFile = @@ -45,40 +58,56 @@ async function main() { console.log("Using cache file", cacheFile); + // Try to deploy on every chain, then collect any failures at the end. This logic makes it simpler to + // identify deployment problems (e.g., not enough gas) on every chain where they occur. const payloads: Buffer[] = []; + const failures: string[] = []; for (const contract of Object.values(DefaultStore.entropy_contracts)) { if (selectedChains.includes(contract.chain)) { const artifact = JSON.parse(readFileSync(argv["std-output"], "utf8")); console.log("Deploying contract to", contract.chain.getId()); - const address = await runIfNotCached( - `deploy-${contract.chain.getId()}`, - () => { - return contract.chain.deploy( - toPrivateKey(argv["private-key"]), - artifact["abi"], - artifact["bytecode"], - [], - 2 - ); - } - ); - console.log( - `Deployed contract at ${address} on ${contract.chain.getId()}` - ); - const payload = - argv["contract-type"] === "executor" - ? await contract.generateUpgradeExecutorContractsPayload(address) - : await contract.generateUpgradeEntropyContractPayload(address); + try { + const address = await runIfNotCached( + `deploy-${contract.chain.getId()}`, + () => { + return contract.chain.deploy( + toPrivateKey(argv["private-key"]), + artifact["abi"], + artifact["bytecode"], + [], + 2 + ); + } + ); + console.log( + `Deployed contract at ${address} on ${contract.chain.getId()}` + ); + const payload = + argv["contract-type"] === "executor" + ? await contract.generateUpgradeExecutorContractsPayload(address) + : await contract.generateUpgradeEntropyContractPayload(address); - console.log(payload.toString("hex")); - payloads.push(payload); + console.log(payload.toString("hex")); + payloads.push(payload); + } catch (e) { + console.log(`error deploying: ${e}`); + failures.push(contract.chain.getId()); + } } } + if (failures.length > 0) { + throw new Error( + `Some chains could not be deployed: ${failures.join( + ", " + )}. Scroll up to see the errors from each chain.` + ); + } + console.log("Using vault at for proposal", vault.getId()); const wallet = await loadHotWallet(argv["ops-key-path"]); console.log("Using wallet ", wallet.publicKey.toBase58()); - await vault.connect(wallet); + vault.connect(wallet, registry); const proposal = await vault.proposeWormholeMessage(payloads); console.log("Proposal address", proposal.address.toBase58()); } diff --git a/contract_manager/src/chains.ts b/contract_manager/src/chains.ts index 92513cc1c9..351c165222 100644 --- a/contract_manager/src/chains.ts +++ b/contract_manager/src/chains.ts @@ -447,12 +447,19 @@ export class EvmChain extends Chain { ); } - const deployedContract = await deployTx.send({ - from: signer.address, - gas, - gasPrice: gasPrice.toString(), - }); - return deployedContract.options.address; + try { + const deployedContract = await deployTx.send({ + from: signer.address, + gas, + gasPrice: gasPrice.toString(), + }); + return deployedContract.options.address; + } catch (e) { + // RPC errors often have useful information in the non-primary message field. Log the whole error + // to simplify identifying the problem. + console.log(`Error deploying contract: ${JSON.stringify(e)}`); + throw e; + } } async getAccountAddress(privateKey: PrivateKey): Promise { diff --git a/contract_manager/src/governance.ts b/contract_manager/src/governance.ts index 606290f662..cececea168 100644 --- a/contract_manager/src/governance.ts +++ b/contract_manager/src/governance.ts @@ -58,6 +58,7 @@ export class SubmittedWormholeMessage { * Inspects the transaction logs to find the sequence number * @param signature signature of the transaction to inspect * @param cluster the cluster the transaction was submitted to + * @param registry registry of RPC nodes to use for each solana network. Defaults to the Solana public RPCs if not provided. */ static async fromTransactionSignature( signature: string, @@ -152,6 +153,11 @@ export class WormholeEmitter { return this.wallet.publicKey; } + /** + * Send a wormhole message containing payload through wormhole. + * @param payload the contents of the message + * @param registry registry of RPC nodes to use for each solana network. Defaults to the Solana public RPCs if not provided. + */ async sendMessage( payload: Buffer, registry: SolanaRpcRegistry = getPythClusterApiUrl @@ -299,6 +305,7 @@ export class Vault extends Storable { * Connects the vault to a wallet that can be used to submit proposals * The wallet should be a multisig signer of the vault * @param wallet + * @param registry registry of RPC nodes to use for each solana network. Defaults to the Solana public RPCs if not provided. */ public connect( wallet: Wallet, @@ -314,6 +321,7 @@ export class Vault extends Storable { /** * Gets the emitter address of the vault + * @param registry registry of RPC nodes to use for each solana network. Defaults to the Solana public RPCs if not provided. */ public async getEmitter(registry: SolanaRpcRegistry = getPythClusterApiUrl) { const squad = SquadsMesh.endpoint(