[eth] Complete syncPythState.js (#425)

This commit is contained in:
Ali Behjati 2022-12-13 17:54:15 +01:00 committed by GitHub
parent 024d73a8a8
commit 09f8af74ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 273 additions and 69 deletions

View File

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

View File

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

View File

@ -5,10 +5,16 @@ Running the Truffle migrations in [`migrations/prod`](migrations/prod) or [`migr
This is the deployment process: This is the deployment process:
```bash ```bash
# The Secret Recovery Phrase for our deployment account. # 1. Follow the installation instructions on README.md
export MNEMONIC=...
# 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 # 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 # 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 # 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 # 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: **When you are making changes to the storage, please make sure that your change to the contract won't cause any collision**. For example:
```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 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:
- Renaming a variable is fine. - Renaming a variable is fine.
- Changing a variable type to another type with the same size is ok. - Changing a variable type to another type with the same size is ok.

View File

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

View File

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

View File

@ -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 governance = require("@pythnetwork/xc-governance-sdk");
const wormhole = require("@certusone/wormhole-sdk");
const assertVaaPayloadEquals = require("./assertVaaPayloadEquals"); const assertVaaPayloadEquals = require("./assertVaaPayloadEquals");
const { assert } = require("chai"); const { assert } = require("chai");
const util = require("node:util"); const util = require("node:util");
const exec = util.promisify(require("node:child_process").exec); const exec = util.promisify(require("node:child_process").exec);
const fs = require("fs"); const fs = require("fs");
const lodash = require("lodash");
const loadEnv = require("./loadEnv"); const loadEnv = require("./loadEnv");
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). * @returns {Promise<string>} VAA for the tx as hex (without leading 0x).
*/ */
async function createVaaFromPayload(payload) { async function createVaaFromPayloadThroughMultiSig(payload) {
const msVaaCachePath = `.${network}.ms_vaa_${payload}`; const payloadHex = payload.toString("hex");
const msVaaCachePath = `.${network}.ms_vaa_${payloadHex}`;
let vaa; let vaa;
if (fs.existsSync(msVaaCachePath)) { if (fs.existsSync(msVaaCachePath)) {
vaa = fs.readFileSync(msVaaCachePath).toString().trim(); vaa = fs.readFileSync(msVaaCachePath).toString().trim();
console.log(`VAA already exists: ${vaa}`); console.log(`VAA already exists: ${vaa}`);
return vaa; return vaa;
} else { } else {
const msTxCachePath = `.${network}.ms_tx_${payload}`; const msTxCachePath = `.${network}.ms_tx_${payloadHex}`;
let txKey; let txKey;
if (fs.existsSync(msTxCachePath)) { if (fs.existsSync(msTxCachePath)) {
txKey = fs.readFileSync(msTxCachePath).toString(); txKey = fs.readFileSync(msTxCachePath).toString();
} else { } else {
console.log( 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); fs.writeFileSync(msTxCachePath, txKey);
throw new Error( throw new Error(
"Contract not sync yet. Run the script again once the multisig transaction is ready to be executed." "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 { try {
vaa = await executeMultisigTxAndGetVaa(txKey, payload); vaa = await executeMultisigTxAndGetVaa(txKey, payloadHex);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
throw new Error( throw new Error(
"Could not execute multisig tx. If the transaction is executed please get the VAA manually " + "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.writeFileSync(msVaaCachePath, vaa);
fs.rmSync(`.${network}.ms_tx_${payload}`); fs.rmSync(`.${network}.ms_tx_${payloadHex}`);
} }
return vaa; 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 upgradeContract(proxy) { async function ensureWormholeAddrAndChainIdIsCorrect(proxy) {
console.log("Upgrading the contract..."); 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`; const implCachePath = `.${network}.new_impl`;
let newImplementationAddress; let newImplementationAddress;
if (fs.existsSync(implCachePath)) { if (fs.existsSync(implCachePath)) {
@ -150,35 +229,166 @@ async function upgradeContract(proxy) {
const upgradePayloadHex = upgradePayload.toString("hex"); const upgradePayloadHex = upgradePayload.toString("hex");
const vaa = await createVaaFromPayload(upgradePayloadHex); await createAndExecuteVaaFromPayloadThroughMultiSig(proxy, upgradePayload);
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); fs.rmSync(implCachePath);
cleanUpVaaCache(upgradePayloadHex); 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) { async function syncContractCode(proxy) {
let deployedVersion = await proxy.version(); const onchainVersion = await proxy.version();
const { version: targetVersion } = require("../package.json"); const { version: desiredVersion } = require("../package.json");
if (deployedVersion === targetVersion) { if (onchainVersion === desiredVersion) {
console.log("Contract version up to date"); console.log(`✅ Contract version is up to date: ${desiredVersion}`);
return;
} else { } else {
console.log( 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 { try {
const proxy = await PythUpgradable.deployed(); const proxy = await PythUpgradable.deployed();
console.log(`Syncing Pyth contract deployed on ${proxy.address}...`); console.log(`Syncing Pyth contract deployed on ${proxy.address}...`);
await ensureThereIsNoOwner(proxy);
await ensureWormholeAddrAndChainIdIsCorrect(proxy);
await syncContractCode(proxy); await syncContractCode(proxy);
await syncUpdateFee(proxy);
await syncValidTimePeriod(proxy);
await syncDataSources(proxy);
await syncGovernanceDataSource(proxy);
callback(); callback();
} catch (e) { } catch (e) {

View File

@ -1,6 +1,7 @@
import { CHAINS as WORMHOLE_CHAINS } from "@certusone/wormhole-sdk"; 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, cronos: 60001,
kcc: 60002, kcc: 60002,
zksync: 60003, zksync: 60003,

View File

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