Kaynağa Gözat

[evm] Wormhole receiver deploy all (#1009)

* Add verification logic for evm set wormhole address instruction

* Minor improvements and cleanup on contract manager evm

* Batch deploy script

* Better docs on verification

* Fix zkSync deployment script and update documentation
Mohammad Amin Khashkhashi Moghaddam 2 yıl önce
ebeveyn
işleme
e422fb9321

+ 39 - 1
contract_manager/scripts/check_proposal.ts

@@ -5,6 +5,7 @@ import { createHash } from "crypto";
 import { DefaultStore } from "../src/store";
 import {
   CosmosUpgradeContract,
+  EvmSetWormholeAddress,
   EvmUpgradeContract,
   getProposalInstructions,
   MultisigParser,
@@ -17,7 +18,8 @@ import {
 } from "@pythnetwork/client/lib/cluster";
 import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
 import { AccountMeta, Keypair, PublicKey } from "@solana/web3.js";
-import { EvmContract } from "../src/contracts/evm";
+import { EvmContract, WormholeEvmContract } from "../src/contracts/evm";
+import Web3 from "web3";
 
 const parser = yargs(hideBin(process.argv))
   .scriptName("check_proposal.ts")
@@ -55,6 +57,42 @@ async function main() {
 
   for (const instruction of parsedInstructions) {
     if (instruction instanceof WormholeMultisigInstruction) {
+      if (instruction.governanceAction instanceof EvmSetWormholeAddress) {
+        console.log(
+          `Verifying EVM set wormhole address on ${instruction.governanceAction.targetChainId}`
+        );
+        for (const chain of Object.values(DefaultStore.chains)) {
+          if (
+            chain instanceof EvmChain &&
+            chain.isMainnet() === (cluster === "mainnet-beta") &&
+            chain.wormholeChainName ===
+              instruction.governanceAction.targetChainId
+          ) {
+            const address = instruction.governanceAction.address;
+            const contract = new WormholeEvmContract(chain, address);
+            const currentIndex = await contract.getCurrentGuardianSetIndex();
+            const guardianSet = await contract.getGuardianSet();
+
+            const proxyContract = new EvmContract(chain, address);
+            const proxyCode = await proxyContract.getCode();
+            const receiverImplementation =
+              await proxyContract.getImplementationAddress();
+            const implementationCode = await new EvmContract(
+              chain,
+              receiverImplementation
+            ).getCode();
+            const proxyDigest = Web3.utils.keccak256(proxyCode);
+            const implementationDigest =
+              Web3.utils.keccak256(implementationCode);
+            const guardianSetDigest = Web3.utils.keccak256(
+              JSON.stringify(guardianSet)
+            );
+            console.log(
+              `Address:\t\t${address}\nproxy digest:\t\t${proxyDigest}\nimplementation digest:\t${implementationDigest} \nguardian set index:\t${currentIndex} \nguardian set:\t\t${guardianSetDigest}`
+            );
+          }
+        }
+      }
       if (instruction.governanceAction instanceof EvmUpgradeContract) {
         console.log(
           `Verifying EVM Upgrade Contract on ${instruction.governanceAction.targetChainId}`

+ 49 - 1
contract_manager/src/chains.ts

@@ -11,6 +11,7 @@ import {
   SetDataSources,
   SetValidPeriod,
   DataSource,
+  EvmSetWormholeAddress,
 } from "xc_admin_common";
 import { AptosClient } from "aptos";
 import Web3 from "web3";
@@ -40,7 +41,9 @@ export abstract class Chain extends Storable {
     super();
     this.wormholeChainName = wormholeChainName as ChainName;
     if (toChainId(this.wormholeChainName) === undefined)
-      throw new Error(`Invalid chain name ${wormholeChainName}`);
+      throw new Error(
+        `Invalid chain name ${wormholeChainName}. Try rebuilding xc_admin_common package`
+      );
   }
 
   getId(): string {
@@ -270,6 +273,10 @@ export class EvmChain extends Chain {
     return new EvmUpgradeContract(this.wormholeChainName, address).encode();
   }
 
+  generateGovernanceSetWormholeAddressPayload(address: string): Buffer {
+    return new EvmSetWormholeAddress(this.wormholeChainName, address).encode();
+  }
+
   toJson(): any {
     return {
       id: this.id,
@@ -294,6 +301,47 @@ export class EvmChain extends Chain {
     }
     return gasPrice;
   }
+
+  /**
+   * Deploys a contract on this chain
+   * @param privateKey hex string of the 32 byte private key without the 0x prefix
+   * @param abi the abi of the contract, can be obtained from the compiled contract json file
+   * @param bytecode bytecode of the contract, can be obtained from the compiled contract json file
+   * @param deployArgs arguments to pass to the constructor
+   * @returns the address of the deployed contract
+   */
+  async deploy(
+    privateKey: string,
+    abi: any,
+    bytecode: string,
+    deployArgs: any[]
+  ): Promise<string> {
+    const web3 = new Web3(this.getRpcUrl());
+    const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
+    web3.eth.accounts.wallet.add(signer);
+    const contract = new web3.eth.Contract(abi);
+    const deployTx = contract.deploy({ data: bytecode, arguments: deployArgs });
+    const gas = await deployTx.estimateGas();
+    const gasPrice = await this.getGasPrice();
+    const deployerBalance = await web3.eth.getBalance(signer.address);
+    const gasDiff = BigInt(gas) * BigInt(gasPrice) - BigInt(deployerBalance);
+    if (gasDiff > 0n) {
+      throw new Error(
+        `Insufficient funds to deploy contract. Need ${gas} (gas) * ${gasPrice} (gasPrice)= ${
+          BigInt(gas) * BigInt(gasPrice)
+        } wei, but only have ${deployerBalance} wei. We need ${
+          Number(gasDiff) / 10 ** 18
+        } ETH more.`
+      );
+    }
+
+    const deployedContract = await deployTx.send({
+      from: signer.address,
+      gas,
+      gasPrice,
+    });
+    return deployedContract.options.address;
+  }
 }
 
 export class AptosChain extends Chain {

+ 0 - 33
contract_manager/src/contracts/evm.ts

@@ -303,39 +303,6 @@ export class EvmContract extends Contract {
     return result;
   }
 
-  static async deploy(
-    chain: EvmChain,
-    privateKey: string,
-    abi: any,
-    bytecode: string
-  ): Promise<EvmContract> {
-    const web3 = new Web3(chain.getRpcUrl());
-    const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
-    web3.eth.accounts.wallet.add(signer);
-    const contract = new web3.eth.Contract(abi);
-    const deployTx = contract.deploy({ data: bytecode });
-    const gas = await deployTx.estimateGas();
-    const gasPrice = await chain.getGasPrice();
-    const deployerBalance = await web3.eth.getBalance(signer.address);
-    const gasDiff = BigInt(gas) * BigInt(gasPrice) - BigInt(deployerBalance);
-    if (gasDiff > 0n) {
-      throw new Error(
-        `Insufficient funds to deploy contract. Need ${gas} (gas) * ${gasPrice} (gasPrice)= ${
-          BigInt(gas) * BigInt(gasPrice)
-        } wei, but only have ${deployerBalance} wei. We need ${
-          Number(gasDiff) / 10 ** 18
-        } ETH more.`
-      );
-    }
-
-    const deployedContract = await deployTx.send({
-      from: signer.address,
-      gas,
-      gasPrice,
-    });
-    return new EvmContract(chain, deployedContract.options.address);
-  }
-
   getContract() {
     const web3 = new Web3(this.chain.getRpcUrl());
     const pythContract = new web3.eth.Contract(EXTENDED_PYTH_ABI, this.address);

+ 4 - 0
contract_manager/src/contracts/wormhole.ts

@@ -30,6 +30,10 @@ export abstract class WormholeContract {
     for (let i = currentIndex; i < MAINNET_UPGRADE_VAAS.length; i++) {
       const vaa = MAINNET_UPGRADE_VAAS[i];
       await this.upgradeGuardianSets(senderPrivateKey, Buffer.from(vaa, "hex"));
+      // make sure the upgrade is complete before continuing
+      while ((await this.getCurrentGuardianSetIndex()) <= i) {
+        await new Promise((resolve) => setTimeout(resolve, 5000));
+      }
     }
   }
 }

+ 2 - 13
contract_manager/src/shell.ts

@@ -8,20 +8,9 @@ repl.evalCode(
   "import { loadHotWallet, Vault } from './src/governance';" +
     "import { SuiChain, CosmWasmChain, AptosChain, EvmChain } from './src/chains';" +
     "import { SuiContract } from './src/contracts/sui';" +
-    "import { CosmWasmContract } from './src/contracts/cosmwasm';" +
-    "import { EvmContract } from './src/contracts/evm';" +
+    "import { WormholeCosmWasmContract, CosmWasmContract } from './src/contracts/cosmwasm';" +
+    "import { WormholeEvmContract, EvmContract } from './src/contracts/evm';" +
     "import { AptosContract } from './src/contracts/aptos';" +
     "import { DefaultStore } from './src/store';" +
     "DefaultStore"
 );
-
-// import * as repl from 'node:repl';
-// import { CosmWasmChain, Chains, ChainContracts } from './entities';
-// // import { CHAINS_NETWORK_CONFIG } from './chains-manager/chains';
-
-// const replServer = repl.start('Pyth shell> ')
-// // const mnemonic = "salon myth guide analyst umbrella load arm first roast pelican stuff satoshi";
-
-// replServer.context.CosmWasmChain = CosmWasmChain;
-// replServer.context.Chains = Chains;
-// replServer.context.ChainContracts = ChainContracts;

+ 7 - 6
target_chains/ethereum/contracts/Deploying.md

@@ -137,13 +137,15 @@ We include artifacts required for verifying the contract in each release. To cre
 
 ```
 npx sol-merger contracts/pyth/PythUpgradable.sol
+npx sol-merger contracts/pyth/ReceiverImplementation.sol
 npx truffle run stdjsonin PythUpgradable
+npx truffle run stdjsonin ReceiverImplementation
 ```
 
-These commands create the files `contracts/pyth/PythUpgradable_merged.sol` and `PythUpgradable-input.json` respectively.
-The first file is a flattened version of the contract, and the second file is the standard json input of the contract.
+These commands create the files `contracts/pyth/PythUpgradable_merged.sol`, `contracts/pyth/ReceiverImplementation_merged.sol`, `PythUpgradable-input.json`, and `ReceiverImplementation-input.json` respectively.
+The `.sol` files are the flattened version of the contracts, and the `.json` files are the standard json input of the contracts.
 
-Please include both of these in the verification folder of the release.
+Please include all of these in the release.
 
 ## Verifying the contract
 
@@ -168,14 +170,13 @@ compile it to their binary format (zk-solc) and deploy it. As of this writing th
 contract or a new contract do the following steps in addition to the steps described above:
 
 1. Update the [`hardhad.config.ts`](./hardhat.config.ts) file.
-2. Add the configuration files to `truffle-config.js` and `.env.prod.<network>` file as described above. Truffle
-   config is required as the above deployment script still works in changing the contract (except upgrades).
+2. Add the required chain configuration in the contract manager files as described above.
 3. Run `npx hardhat clean && npx hardhat compile` to have a clean compile the contracts.
 4. Prepare the enviornment:
 
 - Export the secret recovery phrase for the deployment account. Please store it in a file and read
   the file into `MNEMONIC` environment variable like so: `export MNEMONIC=$(cat path/to/mnemonic)`.
-- Copy the correct env file (e.g: `.env.production.zksync`) to `.env`.
+- Create the env settings by running `node create-env.js zksync` and verifying the `.env` file.
 
 5. If you wish to deploy the contract run `npx hardhat deploy-zksync --network <network> --script deploy/zkSyncDeploy` to deploy it to the new network. Otherwise
    run `npx hardhat deploy-zksync --network <network> --script deploy/zkSyncDeployNewPythImpl.ts` to get a new implementation address. Then put it in

+ 14 - 18
target_chains/ethereum/contracts/deploy/zkSyncDeploy.ts

@@ -6,6 +6,7 @@ import { CHAINS } from "xc_admin_common";
 import { assert } from "chai";
 import { writeFileSync } from "fs";
 
+const { getDefaultConfig } = require("../scripts/contractManagerConfig");
 loadEnv("./");
 
 function envOrErr(name: string): string {
@@ -36,9 +37,16 @@ export default async function (hre: HardhatRuntimeEnvironment) {
   // await depositHandle.wait();
 
   // Deploy WormholeReceiver contract.
-  const initialSigners = JSON.parse(envOrErr("INIT_SIGNERS"));
-  const whGovernanceChainId = envOrErr("INIT_GOV_CHAIN_ID");
-  const whGovernanceContract = envOrErr("INIT_GOV_CONTRACT"); // bytes32
+
+  const {
+    wormholeGovernanceChainId,
+    wormholeGovernanceContract,
+    wormholeInitialSigners,
+    governanceEmitter,
+    governanceChainId,
+    emitterAddresses,
+    emitterChainIds,
+  } = getDefaultConfig(envOrErr("MIGRATIONS_NETWORK"));
 
   const chainName = envOrErr("WORMHOLE_CHAIN_NAME");
   const wormholeReceiverChainId = CHAINS[chainName];
@@ -62,10 +70,10 @@ export default async function (hre: HardhatRuntimeEnvironment) {
     "setup",
     [
       receiverImplContract.address,
-      initialSigners,
+      wormholeInitialSigners,
       wormholeReceiverChainId,
-      whGovernanceChainId,
-      whGovernanceContract,
+      wormholeGovernanceChainId,
+      wormholeGovernanceContract,
     ]
   );
 
@@ -79,18 +87,6 @@ export default async function (hre: HardhatRuntimeEnvironment) {
     `Deployed WormholeReceiver on ${wormholeReceiverContract.address}`
   );
 
-  // Deploy Pyth contract.
-  const emitterChainIds = [
-    envOrErr("SOLANA_CHAIN_ID"),
-    envOrErr("PYTHNET_CHAIN_ID"),
-  ];
-  const emitterAddresses = [
-    envOrErr("SOLANA_EMITTER"),
-    envOrErr("PYTHNET_EMITTER"),
-  ];
-  const governanceChainId = envOrErr("GOVERNANCE_CHAIN_ID");
-  const governanceEmitter = envOrErr("GOVERNANCE_EMITTER");
-  // Default value for this field is 0
   const governanceInitialSequence = Number(
     process.env.GOVERNANCE_INITIAL_SEQUENCE ?? "0"
   );

+ 137 - 0
target_chains/ethereum/contracts/scripts/batchDeployReceivers.ts

@@ -0,0 +1,137 @@
+/**
+ * This script deploys the receiver contracts on all the chains and creates a governance proposal to update the
+ * wormhole addresses to the deployed receiver contracts.
+ */
+
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import {
+  DefaultStore,
+  EvmChain,
+  loadHotWallet,
+  WormholeEvmContract,
+} from "contract_manager";
+import Web3 from "web3";
+import { CHAINS } from "xc_admin_common";
+import * as fs from "fs";
+
+const { getDefaultConfig } = require("./contractManagerConfig");
+
+const parser = yargs(hideBin(process.argv))
+  .usage(
+    "Usage: $0 --contracts <path-to-contract-json-folder> --network <contract_id> --private-key <private-key> --ops-wallet <ops-wallet>"
+  )
+  .options({
+    contract: {
+      type: "string",
+      demandOption: true,
+      desc: "Path to the contract json file containing abi and bytecode",
+    },
+    network: {
+      type: "string",
+      demandOption: true,
+      choices: ["testnet", "mainnet"],
+      desc: "The network to deploy the contract on",
+    },
+    "private-key": {
+      type: "string",
+      demandOption: true,
+      desc: "Private key to sign the transactions. Hex format, without 0x prefix.",
+    },
+    "ops-wallet": {
+      type: "string",
+      demandOption: true,
+      desc: "Path to operations wallet json file",
+    },
+  });
+
+async function memoize(key: string, fn: () => Promise<any>) {
+  const path = `./cache/${key}.json`;
+  if (fs.existsSync(path)) {
+    return JSON.parse(fs.readFileSync(path).toString());
+  }
+  const result = await fn();
+  fs.writeFileSync(path, JSON.stringify(result));
+  return result;
+}
+
+async function main() {
+  const argv = await parser.argv;
+  const privateKey = argv["private-key"];
+  const network = argv["network"];
+  const setupInfo = require(argv["contract"] + "/ReceiverSetup.json");
+  const implementationInfo = require(argv["contract"] +
+    "/ReceiverImplementation.json");
+  const receiverInfo = require(argv["contract"] + "/WormholeReceiver.json");
+
+  const payloads: Buffer[] = [];
+  for (const chain of Object.values(DefaultStore.chains)) {
+    if (
+      chain instanceof EvmChain &&
+      chain.isMainnet() === (network === "mainnet")
+    ) {
+      if (chain.wormholeChainName === "zksync") continue; // deploy zksync receiver separately
+      const {
+        wormholeGovernanceChainId,
+        wormholeGovernanceContract,
+        wormholeInitialSigners,
+      } = getDefaultConfig(chain.getId());
+      console.log(chain.getId());
+      const address = await memoize(chain.getId(), async () => {
+        const setupAddress = await chain.deploy(
+          privateKey,
+          setupInfo.abi,
+          setupInfo.bytecode,
+          []
+        );
+        console.log("setupAddress", setupAddress);
+        const implementationAddress = await chain.deploy(
+          privateKey,
+          implementationInfo.abi,
+          implementationInfo.bytecode,
+          []
+        );
+        console.log("implementationAddress", implementationAddress);
+        const web3 = new Web3();
+        const setup = new web3.eth.Contract(setupInfo.abi, setupAddress);
+        const initData = setup.methods
+          .setup(
+            implementationAddress,
+            wormholeInitialSigners,
+            CHAINS[chain.wormholeChainName],
+            wormholeGovernanceChainId,
+            wormholeGovernanceContract
+          )
+          .encodeABI();
+
+        // deploy proxy
+        const receiverAddress = await chain.deploy(
+          privateKey,
+          receiverInfo.abi,
+          receiverInfo.bytecode,
+          [setupAddress, initData]
+        );
+        const contract = new WormholeEvmContract(chain, receiverAddress);
+        console.log("receiverAddress", receiverAddress);
+        await contract.syncMainnetGuardianSets(privateKey);
+        console.log("synced");
+        return contract.address;
+      });
+      const payload = chain.generateGovernanceSetWormholeAddressPayload(
+        address.replace("0x", "")
+      );
+      payloads.push(payload);
+    }
+  }
+  let vaultName;
+  if (network === "mainnet") {
+    vaultName = "mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj";
+  } else {
+    vaultName = "devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3";
+  }
+  const vault = DefaultStore.vaults[vaultName];
+  vault.connect(await loadHotWallet(argv["ops-wallet"]));
+  await vault.proposeWormholeMessage(payloads);
+}
+
+main();