diff --git a/ethereum/Deploying.md b/ethereum/Deploying.md index ceceb83d..cb4f8842 100644 --- a/ethereum/Deploying.md +++ b/ethereum/Deploying.md @@ -18,6 +18,9 @@ rm -rf build && npx truffle compile --all # Merge the network addresses into the artifacts, if some contracts are already deployed. npx apply-registry +# Set the deploy commit hash in the contract binary (used for debugging purposes) +sed -i "s/dead0beaf0deb10700c0331700da5d00deadbead/$(git rev-parse HEAD)/g" build/contracts/* + # Perform the migration npx truffle migrate --network $MIGRATIONS_NETWORK @@ -60,6 +63,8 @@ const PythUpgradable = artifacts.require("PythUpgradable"); const { upgradeProxy } = require("@openzeppelin/truffle-upgrades"); /** + * Version . + * * Briefly describe the changelog here. */ module.exports = async function (deployer) { @@ -86,6 +91,12 @@ make sure that your change to the contract won't cause any collision**. For exam Anything other than the operations above will probably cause a collision. Please refer to Open Zeppelin Upgradeable (documentations)[https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable] for more information. +## Versioning + +We use [Semantic Versioning](https://semver.org/) for our releases. When upgrading the contract, update the npm package version using +`npm version --no-git-tag-version`. Also, modify the hard-coded value in `version()` method in +[the `Pyth.sol` contract](./contracts/pyth/Pyth.sol) to the new version. Then, after your PR is merged in main, create a release like with tag `pyth-evm-contract-v`. This will help developers to be able to track code changes easier. + # Testing The [pyth-js][] repository contains an example with documentation and a code sample showing how to relay your own prices to a diff --git a/ethereum/contracts/pyth/Pyth.sol b/ethereum/contracts/pyth/Pyth.sol index 1d52cd03..b70f71f1 100644 --- a/ethereum/contracts/pyth/Pyth.sol +++ b/ethereum/contracts/pyth/Pyth.sol @@ -24,7 +24,7 @@ abstract contract Pyth is PythGetters, PythSetters, AbstractPyth { setPyth2WormholeEmitter(pyth2WormholeEmitter); } - function updatePriceBatchFromVm(bytes memory encodedVm) private returns (PythInternalStructs.BatchPriceAttestation memory bpa) { + function updatePriceBatchFromVm(bytes calldata encodedVm) private returns (PythInternalStructs.BatchPriceAttestation memory bpa) { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm); require(valid, reason); @@ -211,34 +211,29 @@ abstract contract Pyth is PythGetters, PythSetters, AbstractPyth { } function queryPriceFeed(bytes32 id) public view override returns (PythStructs.PriceFeed memory priceFeed){ - // Look up the latest price info for the given ID PythInternalStructs.PriceInfo memory info = latestPriceInfo(id); require(info.priceFeed.id != 0, "no price feed found for the given price id"); - // Check that there is not a significant difference between this chain's time - // and the price publish time. - if (info.priceFeed.status == PythStructs.PriceStatus.TRADING && - absDiff(block.timestamp, info.priceFeed.publishTime) > validTimePeriodSeconds()) { - info.priceFeed.status = PythStructs.PriceStatus.UNKNOWN; - // getLatestAvailablePrice* gets prevPrice when status is - // unknown. So, now that status is being set to unknown, - // we should move the current price to the previous - // price to ensure getLatestAvailablePrice* works - // as intended. - info.priceFeed.prevPrice = info.priceFeed.price; - info.priceFeed.prevConf = info.priceFeed.conf; - info.priceFeed.prevPublishTime = info.priceFeed.publishTime; - } - return info.priceFeed; } - function absDiff(uint x, uint y) private pure returns (uint) { - if (x > y) { - return x - y; - } else { - return y - x; - } + function priceFeedExists(bytes32 id) public override view returns (bool) { + PythInternalStructs.PriceInfo memory info = latestPriceInfo(id); + return (info.priceFeed.id != 0); + } + + function getValidTimePeriod() public override view returns (uint) { + return validTimePeriodSeconds(); + } + + function version() public pure returns (string memory) { + return "0.1.0"; + } + + function deployCommitHash() public pure returns (bytes20) { + // This is a place holder for the commit hash and will be replaced + // with the commit hash upon deployment. + return hex"dead0beaf0deb10700c0331700da5d00deadbead"; } } diff --git a/ethereum/migrations/prod-receiver/8_pyth_update_interface_add_update_if_necessary.js b/ethereum/migrations/prod-receiver/8_pyth_update_interface_add_update_if_necessary.js index fe1e3089..08c950cc 100644 --- a/ethereum/migrations/prod-receiver/8_pyth_update_interface_add_update_if_necessary.js +++ b/ethereum/migrations/prod-receiver/8_pyth_update_interface_add_update_if_necessary.js @@ -5,9 +5,15 @@ const PythUpgradable = artifacts.require("PythUpgradable"); const { upgradeProxy } = require("@openzeppelin/truffle-upgrades"); /** + * Version 0.1.0 + * * This change: * - Updates the interface, adds `updatePriceFeedsIfNecessary` that wraps * `updatePriceFeeds` and rejects if the price update is not necessary. + * - Changes some memory modifiers to improve gas efficiency. + * - Changes staleness logic to be included in the sdk and bring + * more clarity to the existing code. + * - Adds version to the contract (which is hard coded) */ module.exports = async function (deployer) { const proxy = await PythUpgradable.deployed(); diff --git a/ethereum/migrations/prod/7_pyth_update_interface_add_update_if_necessary.js b/ethereum/migrations/prod/7_pyth_update_interface_add_update_if_necessary.js index fe1e3089..08c950cc 100644 --- a/ethereum/migrations/prod/7_pyth_update_interface_add_update_if_necessary.js +++ b/ethereum/migrations/prod/7_pyth_update_interface_add_update_if_necessary.js @@ -5,9 +5,15 @@ const PythUpgradable = artifacts.require("PythUpgradable"); const { upgradeProxy } = require("@openzeppelin/truffle-upgrades"); /** + * Version 0.1.0 + * * This change: * - Updates the interface, adds `updatePriceFeedsIfNecessary` that wraps * `updatePriceFeeds` and rejects if the price update is not necessary. + * - Changes some memory modifiers to improve gas efficiency. + * - Changes staleness logic to be included in the sdk and bring + * more clarity to the existing code. + * - Adds version to the contract (which is hard coded) */ module.exports = async function (deployer) { const proxy = await PythUpgradable.deployed(); diff --git a/ethereum/migrations/test/8_pyth_update_interface_add_update_if_necessary.js b/ethereum/migrations/test/8_pyth_update_interface_add_update_if_necessary.js index fe1e3089..08c950cc 100644 --- a/ethereum/migrations/test/8_pyth_update_interface_add_update_if_necessary.js +++ b/ethereum/migrations/test/8_pyth_update_interface_add_update_if_necessary.js @@ -5,9 +5,15 @@ const PythUpgradable = artifacts.require("PythUpgradable"); const { upgradeProxy } = require("@openzeppelin/truffle-upgrades"); /** + * Version 0.1.0 + * * This change: * - Updates the interface, adds `updatePriceFeedsIfNecessary` that wraps * `updatePriceFeeds` and rejects if the price update is not necessary. + * - Changes some memory modifiers to improve gas efficiency. + * - Changes staleness logic to be included in the sdk and bring + * more clarity to the existing code. + * - Adds version to the contract (which is hard coded) */ module.exports = async function (deployer) { const proxy = await PythUpgradable.deployed(); diff --git a/ethereum/package-lock.json b/ethereum/package-lock.json index a04aaf26..d3e853ee 100644 --- a/ethereum/package-lock.json +++ b/ethereum/package-lock.json @@ -1,17 +1,17 @@ { - "name": "wormhole", - "version": "1.0.0", + "name": "@pythnetwork/pyth-evm-contract", + "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "wormhole", - "version": "1.0.0", + "name": "@pythnetwork/pyth-evm-contract", + "version": "0.1.0", "license": "ISC", "dependencies": { "@openzeppelin/contracts": "^4.5.0", "@openzeppelin/contracts-upgradeable": "^4.5.2", - "@pythnetwork/pyth-sdk-solidity": "^0.5.1", + "@pythnetwork/pyth-sdk-solidity": "^0.5.2", "dotenv": "^10.0.0", "elliptic": "^6.5.2", "ganache-cli": "^6.12.1", @@ -3674,9 +3674,9 @@ "dev": true }, "node_modules/@pythnetwork/pyth-sdk-solidity": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-solidity/-/pyth-sdk-solidity-0.5.1.tgz", - "integrity": "sha512-uvV48IPzkU4+q6PQZNbLnjT+xVaiLmnMEalMvLFK3+QRBpd8gTXm8izdO0ZPbKHdytOueTHxlFK5imo7hRabBA==" + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-solidity/-/pyth-sdk-solidity-0.5.2.tgz", + "integrity": "sha512-On0hyZcTuMsonSzLc+HHA/5npjwB9WlFZlLrXW8TUV+7WEKH4o73OYJy/b9197E7snTviDIASlVT+rmcC9b7rg==" }, "node_modules/@redux-saga/core": { "version": "1.1.3", @@ -39998,9 +39998,9 @@ "dev": true }, "@pythnetwork/pyth-sdk-solidity": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-solidity/-/pyth-sdk-solidity-0.5.1.tgz", - "integrity": "sha512-uvV48IPzkU4+q6PQZNbLnjT+xVaiLmnMEalMvLFK3+QRBpd8gTXm8izdO0ZPbKHdytOueTHxlFK5imo7hRabBA==" + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-solidity/-/pyth-sdk-solidity-0.5.2.tgz", + "integrity": "sha512-On0hyZcTuMsonSzLc+HHA/5npjwB9WlFZlLrXW8TUV+7WEKH4o73OYJy/b9197E7snTviDIASlVT+rmcC9b7rg==" }, "@redux-saga/core": { "version": "1.1.3", diff --git a/ethereum/package.json b/ethereum/package.json index 30d793da..dc814b7e 100644 --- a/ethereum/package.json +++ b/ethereum/package.json @@ -1,8 +1,7 @@ { - "name": "wormhole", - "version": "1.0.0", + "name": "@pythnetwork/pyth-evm-contract", + "version": "0.1.0", "description": "", - "main": "networks.js", "devDependencies": { "@chainsafe/truffle-plugin-abigen": "0.0.1", "@openzeppelin/cli": "^2.8.2", @@ -30,7 +29,7 @@ "dependencies": { "@openzeppelin/contracts": "^4.5.0", "@openzeppelin/contracts-upgradeable": "^4.5.2", - "@pythnetwork/pyth-sdk-solidity": "^0.5.1", + "@pythnetwork/pyth-sdk-solidity": "^0.5.2", "dotenv": "^10.0.0", "elliptic": "^6.5.2", "ganache-cli": "^6.12.1", diff --git a/ethereum/test/pyth.js b/ethereum/test/pyth.js index e9b862bc..0b0ad91d 100644 --- a/ethereum/test/pyth.js +++ b/ethereum/test/pyth.js @@ -6,7 +6,7 @@ const PythStructs = artifacts.require("PythStructs"); const { deployProxy, upgradeProxy } = require("@openzeppelin/truffle-upgrades"); const { expectRevert, expectEvent, time } = require("@openzeppelin/test-helpers"); -const { assert } = require("chai"); +const { assert, expect } = require("chai"); // Use "WormholeReceiver" if you are testing with Wormhole Receiver const Wormhole = artifacts.require("Wormhole"); @@ -562,7 +562,7 @@ contract("Pyth", function () { ); }); - it("should show stale cached prices as unknown", async function () { + it("should revert on getting stale current prices", async function () { let smallestTimestamp = 1; let rawBatch = generateRawBatchAttestation( smallestTimestamp, @@ -575,15 +575,14 @@ contract("Pyth", function () { const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id); - assert.equal( - priceFeedResult.status.toString(), - PythStructs.PriceStatus.UNKNOWN.toString() + expectRevert( + this.pythProxy.getCurrentPrice(price_id), + "current price unavailable" ); } }); - it("should show cached prices too far into the future as unknown", async function () { + it("should revert on getting current prices too far into the future as they are considered unknown", async function () { let largestTimestamp = 4294967295; let rawBatch = generateRawBatchAttestation( largestTimestamp - 5, @@ -596,12 +595,11 @@ contract("Pyth", function () { const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id); - assert.equal( - priceFeedResult.status.toString(), - PythStructs.PriceStatus.UNKNOWN.toString() - ); - } + expectRevert( + this.pythProxy.getCurrentPrice(price_id), + "current price unavailable" + ); + } }); it("changing validity time works", async function() { @@ -622,11 +620,9 @@ contract("Pyth", function () { const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id); - assert.equal( - priceFeedResult.status.toString(), - PythStructs.PriceStatus.TRADING.toString() - ); + + // Expect getCurrentPrice to work (not revert) + await this.pythProxy.getCurrentPrice(price_id); } // One minute passes @@ -637,10 +633,10 @@ contract("Pyth", function () { const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id); - assert.equal( - priceFeedResult.status.toString(), - PythStructs.PriceStatus.UNKNOWN.toString() + + expectRevert( + this.pythProxy.getCurrentPrice(price_id), + "current price unavailable" ); } @@ -653,10 +649,9 @@ contract("Pyth", function () { "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id); - assert.equal( - priceFeedResult.status.toString(), - PythStructs.PriceStatus.TRADING.toString() - ); + + // Expect getCurrentPrice to work (not revert) + await this.pythProxy.getCurrentPrice(price_id); } }); @@ -724,6 +719,13 @@ contract("Pyth", function () { "invalid data source chain/emitter ID" ); }); + + it("Make sure version is the npm package version", async function () { + const contractVersion = await this.pythProxy.version(); + const { version } = require('../package.json'); + + expect(contractVersion).equal(version); + }); }); const signAndEncodeVM = async function (