Selaa lähdekoodia

[eth] Improve and automate deployment process (#412)

* Bump contract version

* Some refactoring to add types with JSDoc

* Use better RPCs for some networks

* Remove unneeded migration files

* Add initial syncPythState script that does upgrade

* Update truffle-config for new gas values
Ali Behjati 3 vuotta sitten
vanhempi
sitoutus
4821b877e3

+ 1 - 1
ethereum/contracts/pyth/Pyth.sol

@@ -536,6 +536,6 @@ abstract contract Pyth is PythGetters, PythSetters, AbstractPyth {
     }
 
     function version() public pure returns (string memory) {
-        return "1.1.0";
+        return "1.2.0";
     }
 }

+ 4 - 1
ethereum/deploy.sh

@@ -25,7 +25,7 @@ while [[ $# -ne 0 ]]; do
     NETWORK=$1
     shift
 
-    echo "=========== Deploying to ${NETWORK} ==========="
+    echo "=========== Deploying to ${NETWORK} (if not deployed) ==========="
 
     # Load the configuration environment variables for deploying your network. make sure to use right env file.
     # If it is a new chain you are deploying to, create a new env file and commit it to the repo.
@@ -35,6 +35,9 @@ while [[ $# -ne 0 ]]; do
     npx truffle migrate --network $MIGRATIONS_NETWORK
 
     echo "Deployment to $NETWORK finished successfully"
+
+    echo "=========== Syncing contract state ==========="
+    npx truffle exec scripts/syncPythState.js --network $MIGRATIONS_NETWORK || echo "Syncing failed/incomplete.. skipping"
 done
 
 echo "=========== Finished ==========="

+ 0 - 25
ethereum/migrations/prod-receiver/11_pyth_make_interface_simpler.js

@@ -1,25 +0,0 @@
-const loadEnv = require("../../scripts/loadEnv");
-loadEnv("../../");
-
-const PythUpgradable = artifacts.require("PythUpgradable");
-const governanceChainId = process.env.GOVERNANCE_CHAIN_ID;
-const governanceEmitter = process.env.GOVERNANCE_EMITTER;
-
-console.log("governanceEmitter: " + governanceEmitter);
-console.log("governanceChainId: " + governanceChainId);
-
-const { upgradeProxy } = require("@openzeppelin/truffle-upgrades");
-
-/**
- * Version 1.1.0
- *
- * This change:
- * - Use pyth-sdk-solidity 1.0.0 which simplifies the PriceFeed interface
- */
-module.exports = async function (deployer) {
-  const proxy = await PythUpgradable.deployed();
-  await upgradeProxy(proxy.address, PythUpgradable, {
-    deployer,
-    unsafeSkipStorageCheck: true,
-  });
-};

+ 0 - 34
ethereum/migrations/prod/12_pyth_set_fee_1_wei.js

@@ -1,34 +0,0 @@
-const governance = require("@pythnetwork/xc-governance-sdk");
-const assertVaaPayloadEquals = require("../../scripts/assertVaaPayloadEquals");
-const { assert } = require("chai");
-
-const loadEnv = require("../../scripts/loadEnv");
-loadEnv("../../");
-
-const setFeeVaa = process.env.MIGRATION_12_SET_FEE_VAA;
-console.log("Set fee vaa: ", setFeeVaa);
-
-const PythUpgradable = artifacts.require("PythUpgradable");
-
-/**
- *
- * This change:
- * - Executes the VAA to set the fee to 1 wei
- */
-module.exports = async function (_deployer) {
-  const proxy = await PythUpgradable.deployed();
-
-  const setFeeInstruction = new governance.SetFeeInstruction(
-    governance.CHAINS.unset, // All the chains
-    BigInt(1),
-    BigInt(0)
-  ).serialize();
-
-  console.log("SetFeeInstruction: 0x", setFeeInstruction.toString("hex"));
-
-  assertVaaPayloadEquals(setFeeVaa, setFeeInstruction);
-
-  await proxy.executeGovernanceInstruction(setFeeVaa);
-
-  assert.equal((await proxy.singleUpdateFeeInWei()).toString(), "1");
-};

+ 0 - 33
ethereum/migrations/test/12_pyth_set_fee_1_wei.js

@@ -1,33 +0,0 @@
-const createLocalnetGovernanceVaa = require("../../scripts/createLocalnetGovernanceVaa");
-const assertVaaPayloadEquals = require("../../scripts/assertVaaPayloadEquals");
-const governance = require("@pythnetwork/xc-governance-sdk");
-const { assert } = require("chai");
-
-const loadEnv = require("../../scripts/loadEnv");
-loadEnv("../../");
-
-const PythUpgradable = artifacts.require("PythUpgradable");
-
-/**
- *
- * This change:
- * - Executes the VAA to set the fee to 1 wei
- */
-module.exports = async function (_deployer) {
-  const setFeeInstruction = new governance.SetFeeInstruction(
-    governance.CHAINS.unset, // All the chains
-    BigInt(1),
-    BigInt(0)
-  ).serialize();
-
-  console.log("SetFeeInstruction: 0x", setFeeInstruction.toString("hex"));
-
-  const setFeeVaa = createLocalnetGovernanceVaa(setFeeInstruction, 2);
-
-  assertVaaPayloadEquals(setFeeVaa, setFeeInstruction);
-
-  const proxy = await PythUpgradable.deployed();
-  await proxy.executeGovernanceInstruction(setFeeVaa);
-
-  assert.equal((await proxy.singleUpdateFeeInWei()).toString(), "1");
-};

+ 1 - 1
ethereum/package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-evm-contract",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {

+ 1 - 1
ethereum/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-evm-contract",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "description": "",
   "devDependencies": {
     "@chainsafe/truffle-plugin-abigen": "0.0.1",

+ 11 - 8
ethereum/scripts/assertVaaPayloadEquals.js

@@ -5,22 +5,25 @@ const {
 setDefaultWasm("node");
 const { assert } = require("chai");
 
+/**
+ * Assert the VAA has payload equal to `expectedPayload`
+ * @param {string} vaaHex
+ * @param {Buffer} expectedPayload
+ */
 module.exports = async function assertVaaPayloadEquals(
-  vaaHexString,
-  expectedPayloadBuffer
+  vaaHex,
+  expectedPayload
 ) {
   const { parse_vaa } = await importCoreWasm();
 
-  if (vaaHexString.startsWith("0x")) {
-    vaaHexString = vaaHexString.substring(2);
+  if (vaaHex.startsWith("0x")) {
+    vaaHex = vaaHex.substring(2);
   }
 
-  const vaaPayload = Buffer.from(
-    parse_vaa(Buffer.from(vaaHexString, "hex")).payload
-  );
+  const vaaPayload = Buffer.from(parse_vaa(Buffer.from(vaaHex, "hex")).payload);
 
   assert(
-    expectedPayloadBuffer.equals(vaaPayload),
+    expectedPayload.equals(vaaPayload),
     "The VAA payload is not equal to the expected payload"
   );
 };

+ 6 - 0
ethereum/scripts/loadEnv.js

@@ -1,6 +1,12 @@
 const dotenv = require("dotenv");
 var path = require("path");
 
+/**
+ * Load environment variables for truffle. This method will load some
+ * cluster-wide environment variables if `CLUSTER` is set in
+ * `{rootPath}/.env`.
+ * @param {string} rootPath
+ */
 module.exports = function loadEnv(rootPath) {
   dotenv.config({ path: path.join(rootPath, ".env") });
   if (process.env.CLUSTER !== undefined) {

+ 195 - 0
ethereum/scripts/syncPythState.js

@@ -0,0 +1,195 @@
+const governance = require("@pythnetwork/xc-governance-sdk");
+const assertVaaPayloadEquals = require("./assertVaaPayloadEquals");
+const { assert } = require("chai");
+const util = require("node:util");
+const exec = util.promisify(require("node:child_process").exec);
+const fs = require("fs");
+
+const loadEnv = require("./loadEnv");
+loadEnv("../");
+
+const network = process.env.MIGRATIONS_NETWORK;
+const chainName = process.env.WORMHOLE_CHAIN_NAME;
+const cluster = process.env.CLUSTER;
+const PythUpgradable = artifacts.require("PythUpgradable");
+
+/**
+ *
+ * @param {string} cmd
+ * @returns {Promise<string>} output of the multisig command
+ */
+async function execMultisigCommand(cmd) {
+  const multisigCluster = cluster === "mainnet" ? "mainnet" : "devnet";
+  const fullCmd = `npm start -- ${cmd} -c ${multisigCluster}`;
+  console.log(`Executing "${fullCmd}"`);
+
+  const { stdout, stderr } = await exec(fullCmd, {
+    cwd: "../third_party/pyth/multisig-wh-message-builder",
+  });
+
+  console.log("stdout:");
+  console.log(stdout);
+  console.log("stderr");
+  console.log(stderr);
+
+  return stdout;
+}
+
+/**
+ *
+ * @param {string} payload Payload in hex string without leading 0x
+ * @returns {Promise<string>}
+ */
+async function createMultisigTx(payload) {
+  console.log("Creating a multisig transaction for this transaction");
+  const stdout = await execMultisigCommand(`create -p ${payload}`);
+
+  const txKey = stdout.match(/Tx key: (.*)\n/)[1];
+  assert(txKey !== undefined && txKey.length > 10);
+  console.log(`Created a multisig tx with key: ${txKey}`);
+
+  return txKey;
+}
+
+/**
+ *
+ * @param {string} txKey
+ * @param {string} payload
+ * @returns {Promise<string>} VAA for the tx as hex (without leading 0x).
+ */
+async function executeMultisigTxAndGetVaa(txKey) {
+  console.log("Executing a multisig transaction for this transaction");
+  const stdout = await execMultisigCommand(`execute -t ${txKey}`);
+
+  let /** @type {string} */ vaa;
+  try {
+    vaa = stdout.match(/VAA \(Hex\): (.*)\n/)[1];
+    assert(vaa !== undefined && vaa.length > 10);
+  } catch (err) {
+    throw new Error("Couldn't find VAA from the logs.");
+  }
+
+  console.log(`Executed multisig tx and got VAA: ${vaa}`);
+
+  return vaa;
+}
+
+/**
+ *
+ * @param {string} payload
+ * @returns {Promise<string>} VAA for the tx as hex (without leading 0x).
+ */
+async function createVaaFromPayload(payload) {
+  const msVaaCachePath = `.${network}.ms_vaa_${payload}`;
+  let vaa;
+  if (fs.existsSync(msVaaCachePath)) {
+    vaa = fs.readFileSync(msVaaCachePath).toString().trim();
+    console.log(`VAA already exists: ${vaa}`);
+    return vaa;
+  } else {
+    const msTxCachePath = `.${network}.ms_tx_${payload}`;
+
+    let txKey;
+    if (fs.existsSync(msTxCachePath)) {
+      txKey = fs.readFileSync(msTxCachePath).toString();
+    } else {
+      console.log(
+        `Creating multisig to send VAA with this payload: ${payload} ...`
+      );
+      txKey = await createMultisigTx(payload);
+      fs.writeFileSync(msTxCachePath, txKey);
+      throw new Error(
+        "Contract not sync yet. Run the script again once the multisig transaction is ready to be executed."
+      );
+    }
+
+    try {
+      vaa = await executeMultisigTxAndGetVaa(txKey, payload);
+    } catch (e) {
+      console.error(e);
+      throw new Error(
+        "Could not execute multisig tx. If the transaction is executed please get the VAA manually " +
+          `and put it on .${network}.ms_vaa_${payload}. Then execute the script again.`
+      );
+    }
+
+    fs.writeFileSync(msVaaCachePath, vaa);
+    fs.rmSync(`.${network}.ms_tx_${payload}`);
+  }
+
+  return vaa;
+}
+
+function cleanUpVaaCache(payload) {
+  fs.rmSync(`.${network}.ms_vaa_${payload}`);
+}
+
+async function upgradeContract(proxy) {
+  console.log("Upgrading the contract...");
+
+  const implCachePath = `.${network}.new_impl`;
+  let newImplementationAddress;
+  if (fs.existsSync(implCachePath)) {
+    newImplementationAddress = fs.readFileSync(implCachePath).toString();
+    console.log(
+      `A new implementation has already been deployed at address ${newImplementationAddress}`
+    );
+  } else {
+    console.log("Deploying a new implementation...");
+    const newImplementation = await PythUpgradable.new();
+    console.log(`Tx hash:  ${newImplementation.transactionHash}`);
+    console.log(`New implementation address: ${newImplementation.address}`);
+    fs.writeFileSync(implCachePath, newImplementation.address);
+    newImplementationAddress = newImplementation.address;
+  }
+
+  const upgradePayload = new governance.EthereumUpgradeContractInstruction(
+    governance.CHAINS[chainName],
+    new governance.HexString20Bytes(newImplementationAddress)
+  ).serialize();
+
+  const upgradePayloadHex = upgradePayload.toString("hex");
+
+  const vaa = await createVaaFromPayload(upgradePayloadHex);
+  assertVaaPayloadEquals(vaa, upgradePayload);
+
+  console.log(`Executing the VAA...`);
+
+  await proxy.executeGovernanceInstruction("0x" + vaa);
+
+  const newVersion = await proxy.version();
+  const { version: targetVersion } = require("../package.json");
+  assert(targetVersion == newVersion, "New contract version is not a match");
+
+  fs.rmSync(implCachePath);
+  cleanUpVaaCache(upgradePayloadHex);
+
+  console.log(`Contract upgraded successfully`);
+}
+
+async function syncContractCode(proxy) {
+  let deployedVersion = await proxy.version();
+  const { version: targetVersion } = require("../package.json");
+
+  if (deployedVersion === targetVersion) {
+    console.log("Contract version up to date");
+    return;
+  } else {
+    console.log(
+      `Deployed version: ${deployedVersion}, target version: ${targetVersion}. On-chain contract is outdated.`
+    );
+    await upgradeContract(proxy);
+  }
+}
+
+module.exports = async function (callback) {
+  try {
+    const proxy = await PythUpgradable.deployed();
+    console.log(`Syncing Pyth contract deployed on ${proxy.address}...`);
+    await syncContractCode(proxy);
+
+    callback();
+  } catch (e) {
+    callback(e);
+  }
+};

+ 11 - 8
ethereum/truffle-config.js

@@ -17,7 +17,7 @@ module.exports = {
         ),
       network_id: 1,
       gas: 10000000,
-      gasPrice: 20000000000,
+      gasPrice: 17000000000,
       confirmations: 1,
       timeoutBlocks: 200,
       skipDryRun: false,
@@ -62,7 +62,7 @@ module.exports = {
       provider: () => {
         return new HDWalletProvider(
           process.env.MNEMONIC,
-          "https://bsc-dataseed3.defibit.io/"
+          "https://rpc.ankr.com/bsc"
         );
       },
       network_id: "56",
@@ -73,7 +73,7 @@ module.exports = {
       provider: () =>
         new HDWalletProvider(
           process.env.MNEMONIC,
-          "https://data-seed-prebsc-1-s1.binance.org:8545"
+          "https://rpc.ankr.com/bsc_testnet_chapel"
         ),
       network_id: "97",
       confirmations: 10,
@@ -168,7 +168,10 @@ module.exports = {
     },
     optimism: {
       provider: () => {
-        return new HDWalletProvider(process.env.MNEMONIC, "https://1rpc.io/op");
+        return new HDWalletProvider(
+          process.env.MNEMONIC,
+          "https://rpc.ankr.com/optimism"
+        );
       },
       network_id: 10,
     },
@@ -176,7 +179,7 @@ module.exports = {
       provider: () => {
         return new HDWalletProvider(
           process.env.MNEMONIC,
-          "https://opt-goerli.g.alchemy.com/v2/demo"
+          "https://rpc.ankr.com/optimism_testnet"
         );
       },
       network_id: 420,
@@ -185,19 +188,19 @@ module.exports = {
       provider: () => {
         return new HDWalletProvider(
           process.env.MNEMONIC,
-          "https://rpc.ftm.tools/"
+          "https://rpc.ankr.com/fantom"
         );
       },
       network_id: 250,
       gas: 8000000,
-      gasPrice: 3000000000,
+      gasPrice: 50000000000,
       timeoutBlocks: 15000,
     },
     fantom_testnet: {
       provider: () => {
         return new HDWalletProvider(
           process.env.MNEMONIC,
-          "https://rpc.testnet.fantom.network/"
+          "https://rpc.ankr.com/fantom_testnet"
         );
       },
       network_id: 0xfa2,

+ 11 - 0
third_party/pyth/multisig-wh-message-builder/src/index.ts

@@ -425,6 +425,7 @@ async function addInstructionsToTx(
   console.log("Approving transaction...");
   await squad.approveTransaction(txKey);
   console.log("Transaction approved.");
+  console.log(`Tx key: ${txKey}`);
   console.log(
     `Tx URL: https://mesh${
       cluster === "devnet" ? "-devnet" : ""
@@ -681,6 +682,16 @@ async function executeMultisigTx(
     msAccount.authorityIndex
   );
 
+  const tx = await squad.getTransaction(txPDA);
+  if ((tx.status as any).executeReady === undefined) {
+    console.log(
+      `Transaction is either executed or not ready yet. Status: ${JSON.stringify(
+        tx.status
+      )}`
+    );
+    return;
+  }
+
   const executeIx = await squad.buildExecuteTransaction(
     txPDA,
     squad.wallet.publicKey