Ver Fonte

[eth] Complete syncPythState.js (#425)

Ali Behjati há 2 anos atrás
pai
commit
09f8af74ed

+ 0 - 1
ethereum/.env.cluster.mainnet

@@ -6,7 +6,6 @@ PYTHNET_EMITTER=0xf8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bb
 GOVERNANCE_CHAIN_ID=0x1
 GOVERNANCE_EMITTER=0x5635979a221c34931e32620b9293a463065555ea71fe97cd6237ade875b12e9e
 
-MIGRATION_12_SET_FEE_VAA=0x01000000020d00c0749e12c5a921d38934aea2025e2748b589bc887b071649ad7e55c9356c942f11f0a731b57b4540136d3fdf0fa76a79b2c270cfb56457544ebcdc2bfa1de1bf00034855f23564ec5d9540c0a45d38ddbfdafe5ad7864e5e539f8233b9d1c35e78654fbb48bfde2ba995437bfb38b92be272224b34d8df2df54d8e478eba53f637e00106af3e7ba2f6891b79564270b549ac93f81d02cca85d80ea3bdf2fa0b5fc9831f04088cc644b65e37853d84150f5e7577946c1bf5b58fdce779c5890506a4a639800083b2fed7f4ae9ebe1be7d952be53ffeebc42f03ab2ebbb6d10fcbee34530857083537ec32154cf4c6c298401828145a095eca8d901c6bd34fd3dcbe51a95d9bf40109bd66527f6553a8fd3429305d4a18822d53104b299c9df15ec9437c75bdab9c3c7f55f7c8993283972e5c98a837f5c79e1ccd234330235829f745e97aa1ede88e010afa65862b5b74eab87b72d91cc026629bd82928424a7b85191fcc430cd5cd2ceb25438aa8a346c26585e2c74d00f460e9225e764969e218203fd9f9c41b3cdb2b010b95a4707053581ba586f4463bb6592420942beb1ef5060d7ff87f8980c0cc33526499643f25eb61939b6245a10cd1deb998ff3cf2a30586d336ab7b57424c5419000c12547c3b942944106d126151036f58d7382cdaea2c5bdd12fa5cb359ff0bf56c470ef714ff33313e3d76b36e12f76378d747417ad40cc73b1ad9050d40e50633010dbc7b13e8175715273342e7577018a5d2d22ee83c4916eac8996886568145244e2be027c0739593d10104f55661d4589ff1d45deb80918324e7888db7e0419f03000e12e244ada40eb8d3c1bdce80d4abd5efa9c57cd5bafc1b123f0539c2106aa8474cd250b1d7fffaeb922e6c54a3ce050fc221177d1c56cc784a4e8c76925a3804000f476f02d40ceaa24ee311f50f0486b047ec3ba4d985e771f08941ab81e184bb275e50a0cc2d738b178ef2c982db28e1eac5eaf87658716a5c3959689240ffd7670010f0a374448b3abc05f1cb765f7d8c790d1c34044a8145380557d42077f9bb027d6e29e013360809702e7a8a407b6abd63036d269d1a1abe66f69a7f73d0bb9209001210be861c7f88caa19c8afcdbd2c9fda6c35c68933e7d3a470b916fa4d47fa62868cf38c022764d542ed9a4ddba81ae093aa1e1ecad88f79c9d879e5484bb9b5b01634878210000000000015635979a221c34931e32620b9293a463065555ea71fe97cd6237ade875b12e9e000000000000000c015054474d0103000000000000000000010000000000000000
 SINGLE_UPDATE_FEE_IN_WEI=1
 
 # Only used for networks with Wormhole receiver

+ 0 - 1
ethereum/.env.cluster.testnet

@@ -6,7 +6,6 @@ PYTHNET_EMITTER=0xa27839d641b07743c0cb5f68c51f8cd31d2c0762bec00dc6fcd25433ef1ab5
 GOVERNANCE_CHAIN_ID=0x1
 GOVERNANCE_EMITTER=0x63278d271099bfd491951b3e648f08b1c71631e4a53674ad43e8f9f98068c385
 
-MIGRATION_12_SET_FEE_VAA=0x010000000001006c844a6f378ddc46842e61552db124bf384d7fb2410584cdc8f3be8cc864b2d169cd9640f23c72e80ac119f10614bb22570731ce9cd8999501cb9178ad7b27e80063471aea00000000000163278d271099bfd491951b3e648f08b1c71631e4a53674ad43e8f9f98068c3850000000000000006015054474d0103000000000000000000010000000000000000
 SINGLE_UPDATE_FEE_IN_WEI=1
 
 # Only used for networks with Wormhole receiver

+ 12 - 33
ethereum/Deploying.md

@@ -5,10 +5,16 @@ Running the Truffle migrations in [`migrations/prod`](migrations/prod) or [`migr
 This is the deployment process:
 
 ```bash
-# The Secret Recovery Phrase for our deployment account.
-export MNEMONIC=...
+# 1. Follow the installation instructions on README.md
 
-# Deploy the changes
+# 2. Export the secret recovery phrase for the deployment account.
+export MNEMONIC=$(cat path/to/mnemonic)
+
+# 3. Make sure that third_party/pyth/multisig-wh-message-builder/keys/key.json
+# has the proper operational key for interacting with the multisig. Please follow
+# the corresponding notion doc for more information about the keys.
+
+# 4. Deploy the changes
 # You might need to repeat this script because of busy RPCs. Repeating would not cause any problem even
 # if the changes are already made. Also, sometimes the gases are not adjusted and it will cause the tx to
 # remain on the mempool for a long time (so there is no progress until timeout). Please update them with
@@ -50,37 +56,10 @@ Changes to the files in this directory should be commited as well.
 
 # Upgrading the contract
 
-To upgrade the contract you should add a new migration file in the `migrations/*` directories increasing the migration number.
-
-It looks like so:
-
-```javascript
-require("dotenv").config({ path: "../.env" });
-
-const PythUpgradable = artifacts.require("PythUpgradable");
-
-const { upgradeProxy } = require("@openzeppelin/truffle-upgrades");
-
-/**
- * Version <x.y.z>.
- *
- * Briefly describe the changelog here.
- */
-module.exports = async function (deployer) {
-  const proxy = await PythUpgradable.deployed();
-  await upgradeProxy(proxy.address, PythUpgradable, { deployer });
-};
-```
-
-**When changing the storage, you might need to disable the storage checks because Open Zeppelin is very conservative,
-and appending to the Pyth State struct is considered illegal.** Pyth `_state` variable is a Pyth State
-struct that contains all Pyth variables inside it. It is the last variable in the contract
-and is safe to append fields inside it. However, Open Zeppelin only allows appending variables
-in the contract surface and does not allow appending in the nested structs.
+To upgrade the contract you should bump the version of the contract and the npm package to the new version and run the deployment
+process described above. Please bump the version properly as described in [the section below](#versioning).
 
-To disable security checks, you can add
-`unsafeSkipStorageCheck: true` option in `upgradeProxy` call. **If you do such a thing,
-make sure that your change to the contract won't cause any collision**. For example:
+**When you are making changes to the storage, please make sure that your change to the contract won't cause any collision**. For example:
 
 - Renaming a variable is fine.
 - Changing a variable type to another type with the same size is ok.

+ 2 - 1
ethereum/package-lock.json

@@ -6,7 +6,7 @@
   "packages": {
     "": {
       "name": "@pythnetwork/pyth-evm-contract",
-      "version": "1.1.0",
+      "version": "1.2.0",
       "license": "ISC",
       "dependencies": {
         "@certusone/wormhole-sdk": "^0.8.0",
@@ -20,6 +20,7 @@
         "ethers": "^5.6.8",
         "ganache-cli": "^6.12.1",
         "jsonfile": "^4.0.0",
+        "lodash": "^4.17.21",
         "solc": "^0.8.4",
         "truffle-contract-size": "^2.0.1",
         "web3": "^1.2.2",

+ 1 - 0
ethereum/package.json

@@ -39,6 +39,7 @@
     "ethers": "^5.6.8",
     "ganache-cli": "^6.12.1",
     "jsonfile": "^4.0.0",
+    "lodash": "^4.17.21",
     "solc": "^0.8.4",
     "truffle-contract-size": "^2.0.1",
     "web3": "^1.2.2",

+ 249 - 31
ethereum/scripts/syncPythState.js

@@ -1,9 +1,24 @@
+/**
+ * This is a truffle script that syncs the on-chain contract with
+ * the reference implementation and state in this repo. Please execute
+ * this script using `deploy.sh` as described in `Deploying.md` file.
+ *
+ * This script is a statefull and fully automated. It will invoke the multisig
+ * cli in the `../third_party/pyth/multisig-wh-message-builder`
+ * to create governed instructions to change on-chain contracts.
+ * As multisig instructions require multiple people approval, this script
+ * will create some cache files to store the last step and continues from
+ * the previous step in the next run.
+ */
+
 const governance = require("@pythnetwork/xc-governance-sdk");
+const wormhole = require("@certusone/wormhole-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 lodash = require("lodash");
 
 const loadEnv = require("./loadEnv");
 loadEnv("../");
@@ -76,27 +91,37 @@ async function executeMultisigTxAndGetVaa(txKey) {
 
 /**
  *
- * @param {string} payload
+ * @param {Buffer} payload
+ */
+function cleanUpVaaCache(payload) {
+  fs.rmSync(`.${network}.ms_vaa_${payload.toString("hex")}`);
+}
+
+/**
+ *
+ * @param {Buffer} payload
  * @returns {Promise<string>} VAA for the tx as hex (without leading 0x).
  */
-async function createVaaFromPayload(payload) {
-  const msVaaCachePath = `.${network}.ms_vaa_${payload}`;
+async function createVaaFromPayloadThroughMultiSig(payload) {
+  const payloadHex = payload.toString("hex");
+
+  const msVaaCachePath = `.${network}.ms_vaa_${payloadHex}`;
   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}`;
+    const msTxCachePath = `.${network}.ms_tx_${payloadHex}`;
 
     let txKey;
     if (fs.existsSync(msTxCachePath)) {
       txKey = fs.readFileSync(msTxCachePath).toString();
     } else {
       console.log(
-        `Creating multisig to send VAA with this payload: ${payload} ...`
+        `Creating multisig to send VAA with this payload: ${payloadHex} ...`
       );
-      txKey = await createMultisigTx(payload);
+      txKey = await createMultisigTx(payloadHex);
       fs.writeFileSync(msTxCachePath, txKey);
       throw new Error(
         "Contract not sync yet. Run the script again once the multisig transaction is ready to be executed."
@@ -104,29 +129,83 @@ async function createVaaFromPayload(payload) {
     }
 
     try {
-      vaa = await executeMultisigTxAndGetVaa(txKey, payload);
+      vaa = await executeMultisigTxAndGetVaa(txKey, payloadHex);
     } 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.`
+          `and put it on .${network}.ms_vaa_${payloadHex}. Then execute the script again.`
       );
     }
 
     fs.writeFileSync(msVaaCachePath, vaa);
-    fs.rmSync(`.${network}.ms_tx_${payload}`);
+    fs.rmSync(`.${network}.ms_tx_${payloadHex}`);
   }
 
   return vaa;
 }
 
-function cleanUpVaaCache(payload) {
-  fs.rmSync(`.${network}.ms_vaa_${payload}`);
+/**
+ * Create a VAA from Payload through multisig.
+ *
+ * @param {} proxy
+ * @param {Buffer} payload
+ * @param {boolean|undefined} keepVaaCache
+ * @returns {Promise<void>}
+ */
+async function createAndExecuteVaaFromPayloadThroughMultiSig(
+  proxy,
+  payload,
+  keepVaaCache
+) {
+  const vaa = await createVaaFromPayloadThroughMultiSig(payload);
+
+  assertVaaPayloadEquals(vaa, payload);
+
+  console.log(`Executing the VAA...`);
+  await proxy.executeGovernanceInstruction("0x" + vaa);
+
+  if (keepVaaCache !== true) {
+    cleanUpVaaCache(payload);
+  }
+}
+
+async function ensureWormholeAddrAndChainIdIsCorrect(proxy) {
+  let desiredWormholeAddr;
+  if (governance.RECEIVER_CHAINS[chainName] !== undefined) {
+    const WormholeReceiver = artifacts.require("WormholeReceiver");
+    desiredWormholeAddr = (await WormholeReceiver.deployed()).address;
+  } else {
+    desiredWormholeAddr =
+      wormhole.CONTRACTS[cluster.toUpperCase()][chainName].core;
+  }
+
+  assert(desiredWormholeAddr !== undefined);
+
+  const onchainWormholeAddr = await proxy.wormhole();
+  assert(desiredWormholeAddr == onchainWormholeAddr);
+
+  const desiredChainId = governance.CHAINS[chainName];
+  const onchainChainId = await proxy.chainId();
+  assert(desiredChainId == onchainChainId);
+
+  console.log(
+    `✅ Wormhole address and chain id is correct: ${desiredWormholeAddr} chainId: ${desiredChainId}`
+  );
 }
 
-async function upgradeContract(proxy) {
-  console.log("Upgrading the contract...");
+async function ensureThereIsNoOwner(proxy) {
+  const onchainOwner = await proxy.owner();
+  assert(onchainOwner == "0x0000000000000000000000000000000000000000");
+  console.log("✅ There is no owner");
+}
 
+/**
+ *
+ * @param {} proxy
+ * @param {string} desiredVersion
+ */
+async function upgradeContract(proxy, desiredVersion) {
   const implCachePath = `.${network}.new_impl`;
   let newImplementationAddress;
   if (fs.existsSync(implCachePath)) {
@@ -150,35 +229,166 @@ async function upgradeContract(proxy) {
 
   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");
+  await createAndExecuteVaaFromPayloadThroughMultiSig(proxy, upgradePayload);
 
   fs.rmSync(implCachePath);
   cleanUpVaaCache(upgradePayloadHex);
 
-  console.log(`Contract upgraded successfully`);
+  const newVersion = await proxy.version();
+  assert(desiredVersion == newVersion, "New contract version is not a match");
+
+  console.log(`✅ Upgraded the contract successfully.`);
 }
 
 async function syncContractCode(proxy) {
-  let deployedVersion = await proxy.version();
-  const { version: targetVersion } = require("../package.json");
+  const onchainVersion = await proxy.version();
+  const { version: desiredVersion } = require("../package.json");
 
-  if (deployedVersion === targetVersion) {
-    console.log("Contract version up to date");
-    return;
+  if (onchainVersion === desiredVersion) {
+    console.log(`✅ Contract version is up to date: ${desiredVersion}`);
   } else {
     console.log(
-      `Deployed version: ${deployedVersion}, target version: ${targetVersion}. On-chain contract is outdated.`
+      `❌ On-chain contract is outdated. Deployed version: ${onchainVersion}, desired version: ${desiredVersion}. Upgrading...`
     );
-    await upgradeContract(proxy);
+    await upgradeContract(proxy, desiredVersion);
+  }
+}
+
+async function syncUpdateFee(proxy) {
+  const desiredUpdateFee = process.env.SINGLE_UPDATE_FEE_IN_WEI;
+  const onchainUpdateFee = (await proxy.singleUpdateFeeInWei()).toString();
+
+  if (onchainUpdateFee == desiredUpdateFee) {
+    console.log(`✅ Contract update fee is in sync: ${desiredUpdateFee}`);
+  } else {
+    console.log(
+      `❌ Update fee is not in sync. on-chain update fee: ${onchainUpdateFee}, ` +
+        `desired update fee: ${desiredUpdateFee}. Updating...`
+    );
+
+    const setFeePayload = new governance.SetFeeInstruction(
+      governance.CHAINS[chainName],
+      BigInt(desiredUpdateFee),
+      BigInt(0)
+    ).serialize();
+
+    await createAndExecuteVaaFromPayloadThroughMultiSig(proxy, setFeePayload);
+
+    const newOnchainUpdateFee = (await proxy.singleUpdateFeeInWei()).toString();
+    assert(newOnchainUpdateFee == desiredUpdateFee);
+
+    console.log(`✅ Set the new update fee successfully.`);
+  }
+}
+
+async function syncValidTimePeriod(proxy) {
+  const desiredValidTimePeriod = process.env.VALID_TIME_PERIOD_SECONDS;
+  const onchainValidTimePeriod = (
+    await proxy.validTimePeriodSeconds()
+  ).toString();
+
+  if (onchainValidTimePeriod == desiredValidTimePeriod) {
+    console.log(
+      `✅ Contract valid time period is in sync: ${desiredValidTimePeriod}s`
+    );
+  } else {
+    console.log(
+      `❌ Valid time period is not in sync. on-chain valid time period: ${onchainValidTimePeriod}s, ` +
+        `desired valid time period: ${desiredValidTimePeriod}s. Updating...`
+    );
+
+    const setValidPeriodPayload = new governance.SetValidPeriodInstruction(
+      governance.CHAINS[chainName],
+      BigInt(desiredValidTimePeriod)
+    ).serialize();
+
+    await createAndExecuteVaaFromPayloadThroughMultiSig(
+      proxy,
+      setValidPeriodPayload
+    );
+
+    const newOnchainValidTimePeriod = (
+      await proxy.validTimePeriodSeconds()
+    ).toString();
+    assert(newOnchainValidTimePeriod == desiredValidTimePeriod);
+
+    console.log(`✅ Set the new valid time period successfully.`);
+  }
+}
+
+async function syncDataSources(proxy) {
+  const desiredDataSources = new Set([
+    [
+      Number(process.env.SOLANA_CHAIN_ID).toString(),
+      process.env.SOLANA_EMITTER,
+    ],
+    [
+      Number(process.env.PYTHNET_CHAIN_ID).toString(),
+      process.env.PYTHNET_EMITTER,
+    ],
+  ]);
+
+  const onchainDataSources = new Set(await proxy.validDataSources());
+
+  if (lodash.isEqual(desiredDataSources, onchainDataSources)) {
+    console.log(
+      `✅ Contract data sources are in sync:\n` +
+        `${JSON.stringify([...desiredDataSources])}`
+    );
+  } else {
+    console.log(
+      `❌ Data sources are not in sync. on-chain data sources:\n` +
+        `${JSON.stringify([...onchainDataSources])}\n` +
+        `desired data sources:\n` +
+        `${JSON.stringify([...desiredDataSources])}\n` +
+        `Updating...`
+    );
+
+    // Usually this change is universal, so the Payload is generated for all
+    // the chains.
+    const setDataSourcesPayload = new governance.SetDataSourcesInstruction(
+      governance.CHAINS[chainName],
+      Array.from(desiredDataSources).map(
+        (ds) =>
+          new governance.DataSource(
+            Number(ds[0]),
+            new governance.HexString32Bytes(ds[1])
+          )
+      )
+    ).serialize();
+    await createAndExecuteVaaFromPayloadThroughMultiSig(
+      proxy,
+      setDataSourcesPayload
+    );
+
+    const newOnchainDataSources = new Set(await proxy.validDataSources());
+    assert(lodash.isEqual(desiredDataSources, newOnchainDataSources));
+
+    console.log(`✅ Set the new data sources successfully.`);
+  }
+}
+
+async function syncGovernanceDataSource(proxy) {
+  const desiredGovDataSource = [
+    Number(process.env.GOVERNANCE_CHAIN_ID).toString(),
+    process.env.GOVERNANCE_EMITTER,
+  ];
+
+  const onchainGovDataSource = Array.from(await proxy.governanceDataSource());
+
+  if (lodash.isEqual(desiredGovDataSource, onchainGovDataSource)) {
+    console.log(
+      `✅ Contract data sources are in sync:\n` + `${desiredGovDataSource}`
+    );
+  } else {
+    console.log(
+      `❌ Governance data source is not in sync. on-chain governance data source:\n` +
+        `${onchainGovDataSource}\n` +
+        `desired governance data source:\n` +
+        `${desiredGovDataSource}\n` +
+        `Cannot upgrade governance data source automatically. Please upgrade it manually`
+    );
+    throw new Error("Governance data source is not in sync.");
   }
 }
 
@@ -186,7 +396,15 @@ module.exports = async function (callback) {
   try {
     const proxy = await PythUpgradable.deployed();
     console.log(`Syncing Pyth contract deployed on ${proxy.address}...`);
+
+    await ensureThereIsNoOwner(proxy);
+    await ensureWormholeAddrAndChainIdIsCorrect(proxy);
+
     await syncContractCode(proxy);
+    await syncUpdateFee(proxy);
+    await syncValidTimePeriod(proxy);
+    await syncDataSources(proxy);
+    await syncGovernanceDataSource(proxy);
 
     callback();
   } catch (e) {

+ 2 - 1
third_party/pyth/xc-governance-sdk-js/src/chains.ts

@@ -1,6 +1,7 @@
 import { CHAINS as WORMHOLE_CHAINS } from "@certusone/wormhole-sdk";
 
-const RECEIVER_CHAINS = {
+export { CHAINS as WORMHOLE_CHAINS } from "@certusone/wormhole-sdk";
+export const RECEIVER_CHAINS = {
   cronos: 60001,
   kcc: 60002,
   zksync: 60003,

+ 7 - 1
third_party/pyth/xc-governance-sdk-js/src/index.ts

@@ -12,4 +12,10 @@ export {
   Instruction,
 } from "./instructions";
 
-export { CHAINS, ChainId, ChainName } from "./chains";
+export {
+  WORMHOLE_CHAINS,
+  RECEIVER_CHAINS,
+  CHAINS,
+  ChainId,
+  ChainName,
+} from "./chains";