[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
This commit is contained in:
Ali Behjati 2022-12-06 17:56:21 +01:00 committed by GitHub
parent 0d2f60cf28
commit 4821b877e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 112 deletions

View File

@ -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";
}
}

View File

@ -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 ==========="

View File

@ -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,
});
};

View File

@ -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");
};

View File

@ -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");
};

View File

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

View File

@ -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",

View File

@ -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"
);
};

View File

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

View File

@ -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);
}
};

View File

@ -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,

View File

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