From 09f8af74ed590e8931887477cf8dccd244b59b48 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 13 Dec 2022 17:54:15 +0100 Subject: [PATCH] [eth] Complete syncPythState.js (#425) --- ethereum/.env.cluster.mainnet | 1 - ethereum/.env.cluster.testnet | 1 - ethereum/Deploying.md | 45 +-- ethereum/package-lock.json | 3 +- ethereum/package.json | 1 + ethereum/scripts/syncPythState.js | 280 ++++++++++++++++-- .../pyth/xc-governance-sdk-js/src/chains.ts | 3 +- .../pyth/xc-governance-sdk-js/src/index.ts | 8 +- 8 files changed, 273 insertions(+), 69 deletions(-) diff --git a/ethereum/.env.cluster.mainnet b/ethereum/.env.cluster.mainnet index 6b902077..7db123ea 100644 --- a/ethereum/.env.cluster.mainnet +++ b/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 diff --git a/ethereum/.env.cluster.testnet b/ethereum/.env.cluster.testnet index 26a5cd77..0e944354 100644 --- a/ethereum/.env.cluster.testnet +++ b/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 diff --git a/ethereum/Deploying.md b/ethereum/Deploying.md index c3521841..dd12131c 100644 --- a/ethereum/Deploying.md +++ b/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. +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). -It looks like so: - -```javascript -require("dotenv").config({ path: "../.env" }); - -const PythUpgradable = artifacts.require("PythUpgradable"); - -const { upgradeProxy } = require("@openzeppelin/truffle-upgrades"); - -/** - * Version . - * - * 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 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. diff --git a/ethereum/package-lock.json b/ethereum/package-lock.json index bd6695d7..e931bed2 100644 --- a/ethereum/package-lock.json +++ b/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", diff --git a/ethereum/package.json b/ethereum/package.json index b808bc65..c591f159 100644 --- a/ethereum/package.json +++ b/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", diff --git a/ethereum/scripts/syncPythState.js b/ethereum/scripts/syncPythState.js index a240cbec..7c32e4ac 100644 --- a/ethereum/scripts/syncPythState.js +++ b/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} 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} + */ +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 upgradeContract(proxy) { - console.log("Upgrading the contract..."); +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 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) { diff --git a/third_party/pyth/xc-governance-sdk-js/src/chains.ts b/third_party/pyth/xc-governance-sdk-js/src/chains.ts index 65aa5bca..90fc1413 100644 --- a/third_party/pyth/xc-governance-sdk-js/src/chains.ts +++ b/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, diff --git a/third_party/pyth/xc-governance-sdk-js/src/index.ts b/third_party/pyth/xc-governance-sdk-js/src/index.ts index 07dc8f5a..2b09a357 100644 --- a/third_party/pyth/xc-governance-sdk-js/src/index.ts +++ b/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";