|
|
@@ -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) {
|