diff --git a/ethereum/contracts/pyth/Pyth.sol b/ethereum/contracts/pyth/Pyth.sol index 95a39504..cb3753b3 100644 --- a/ethereum/contracts/pyth/Pyth.sol +++ b/ethereum/contracts/pyth/Pyth.sol @@ -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"; } } diff --git a/ethereum/deploy.sh b/ethereum/deploy.sh index 4b79f31a..5e79657a 100755 --- a/ethereum/deploy.sh +++ b/ethereum/deploy.sh @@ -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 ===========" diff --git a/ethereum/migrations/prod-receiver/11_pyth_make_interface_simpler.js b/ethereum/migrations/prod-receiver/11_pyth_make_interface_simpler.js deleted file mode 100644 index 30022ba1..00000000 --- a/ethereum/migrations/prod-receiver/11_pyth_make_interface_simpler.js +++ /dev/null @@ -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, - }); -}; diff --git a/ethereum/migrations/prod/12_pyth_set_fee_1_wei.js b/ethereum/migrations/prod/12_pyth_set_fee_1_wei.js deleted file mode 100644 index 950d3ae7..00000000 --- a/ethereum/migrations/prod/12_pyth_set_fee_1_wei.js +++ /dev/null @@ -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"); -}; diff --git a/ethereum/migrations/test/12_pyth_set_fee_1_wei.js b/ethereum/migrations/test/12_pyth_set_fee_1_wei.js deleted file mode 100644 index 1b3ba51d..00000000 --- a/ethereum/migrations/test/12_pyth_set_fee_1_wei.js +++ /dev/null @@ -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"); -}; diff --git a/ethereum/package-lock.json b/ethereum/package-lock.json index 4f81e47d..bd6695d7 100644 --- a/ethereum/package-lock.json +++ b/ethereum/package-lock.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-evm-contract", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/ethereum/package.json b/ethereum/package.json index a79d66c2..b808bc65 100644 --- a/ethereum/package.json +++ b/ethereum/package.json @@ -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", diff --git a/ethereum/scripts/assertVaaPayloadEquals.js b/ethereum/scripts/assertVaaPayloadEquals.js index 30971816..c69a27e1 100644 --- a/ethereum/scripts/assertVaaPayloadEquals.js +++ b/ethereum/scripts/assertVaaPayloadEquals.js @@ -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" ); }; diff --git a/ethereum/scripts/loadEnv.js b/ethereum/scripts/loadEnv.js index 27f85018..46f8c49b 100644 --- a/ethereum/scripts/loadEnv.js +++ b/ethereum/scripts/loadEnv.js @@ -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) { diff --git a/ethereum/scripts/syncPythState.js b/ethereum/scripts/syncPythState.js new file mode 100644 index 00000000..a240cbec --- /dev/null +++ b/ethereum/scripts/syncPythState.js @@ -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} 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} + */ +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} 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} 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); + } +}; diff --git a/ethereum/truffle-config.js b/ethereum/truffle-config.js index 14e6dc3f..c8342417 100644 --- a/ethereum/truffle-config.js +++ b/ethereum/truffle-config.js @@ -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, diff --git a/third_party/pyth/multisig-wh-message-builder/src/index.ts b/third_party/pyth/multisig-wh-message-builder/src/index.ts index d87e25b5..96b951e4 100644 --- a/third_party/pyth/multisig-wh-message-builder/src/index.ts +++ b/third_party/pyth/multisig-wh-message-builder/src/index.ts @@ -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