pyth.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. const jsonfile = require('jsonfile');
  2. const elliptic = require('elliptic');
  3. const BigNumber = require('bignumber.js');
  4. const PythStructs = artifacts.require("PythStructs");
  5. const { deployProxy, upgradeProxy } = require('@openzeppelin/truffle-upgrades');
  6. const {expectRevert} = require('@openzeppelin/test-helpers');
  7. const Wormhole = artifacts.require("Wormhole");
  8. const PythUpgradable = artifacts.require("PythUpgradable");
  9. const MockPythUpgrade = artifacts.require("MockPythUpgrade");
  10. const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
  11. const testSigner2PK = "892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe";
  12. contract("Pyth", function () {
  13. const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK);
  14. const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK);
  15. const testGovernanceChainId = "3";
  16. const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004";
  17. const testPyth2WormholeChainId = "1";
  18. const testPyth2WormholeEmitter = "0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
  19. const notOwnerError = "Ownable: caller is not the owner -- Reason given: Ownable: caller is not the owner.";
  20. beforeEach(async function () {
  21. this.pythProxy = await deployProxy(
  22. PythUpgradable,
  23. [
  24. (await Wormhole.deployed()).address,
  25. testPyth2WormholeChainId,
  26. testPyth2WormholeEmitter,
  27. ]
  28. );
  29. });
  30. it("should be initialized with the correct signers and values", async function(){
  31. // pyth2wormhole
  32. const pyth2wormChain = await this.pythProxy.pyth2WormholeChainId();
  33. assert.equal(pyth2wormChain, testPyth2WormholeChainId);
  34. const pyth2wormEmitter = await this.pythProxy.pyth2WormholeEmitter();
  35. assert.equal(pyth2wormEmitter, testPyth2WormholeEmitter);
  36. })
  37. it("should allow upgrades from the owner", async function(){
  38. // Check that the owner is the default account Truffle
  39. // has configured for the network. upgradeProxy will send
  40. // transactions from the default account.
  41. const accounts = await web3.eth.getAccounts();
  42. const defaultAccount = accounts[0];
  43. const owner = await this.pythProxy.owner();
  44. assert.equal(owner, defaultAccount);
  45. // Try and upgrade the proxy
  46. const newImplementation = await upgradeProxy(
  47. this.pythProxy.address, MockPythUpgrade);
  48. // Check that the new upgrade is successful
  49. assert.equal(await newImplementation.isUpgradeActive(), true);
  50. assert.equal(this.pythProxy.address, newImplementation.address);
  51. })
  52. it("should allow ownership transfer", async function(){
  53. // Check that the owner is the default account Truffle
  54. // has configured for the network.
  55. const accounts = await web3.eth.getAccounts();
  56. const defaultAccount = accounts[0];
  57. assert.equal(await this.pythProxy.owner(), defaultAccount);
  58. // Check that another account can't transfer the ownership
  59. await expectRevert(this.pythProxy.transferOwnership(accounts[1], {from: accounts[1]}), notOwnerError);
  60. // Transfer the ownership to another account
  61. await this.pythProxy.transferOwnership(accounts[2], {from: defaultAccount});
  62. assert.equal(await this.pythProxy.owner(), accounts[2]);
  63. // Check that the original account can't transfer the ownership back to itself
  64. await expectRevert(this.pythProxy.transferOwnership(defaultAccount, {from: defaultAccount}), notOwnerError);
  65. // Check that the new owner can transfer the ownership back to the original account
  66. await this.pythProxy.transferOwnership(defaultAccount, {from: accounts[2]});
  67. assert.equal(await this.pythProxy.owner(), defaultAccount);
  68. })
  69. it("should not allow upgrades from the another account", async function(){
  70. // This test is slightly convoluted as, due to a limitation of Truffle,
  71. // we cannot specify which account upgradeProxy send transactions from:
  72. // it will always use the default account.
  73. //
  74. // Therefore, we transfer the ownership to another account first,
  75. // and then attempt an upgrade using the default account.
  76. // Check that the owner is the default account Truffle
  77. // has configured for the network.
  78. const accounts = await web3.eth.getAccounts();
  79. const defaultAccount = accounts[0];
  80. assert.equal(await this.pythProxy.owner(), defaultAccount);
  81. // Transfer the ownership to another account
  82. const newOwnerAccount = accounts[1];
  83. await this.pythProxy.transferOwnership(newOwnerAccount, {from: defaultAccount});
  84. assert.equal(await this.pythProxy.owner(), newOwnerAccount);
  85. // Try and upgrade using the default account, which will fail
  86. // because we are no longer the owner.
  87. await expectRevert(upgradeProxy(this.pythProxy.address, MockPythUpgrade), notOwnerError);
  88. })
  89. // NOTE(2022-04-11): Raw hex payload obtained from format serialization unit tests in `p2w-sdk/rust`
  90. // Latest known addition: num_publishers, max_num_publishers
  91. //
  92. // Tests rely on a p2w-sdk mock price/prod ID generation rule:
  93. // nthProdByte(n) = n % 256, starting with n=1
  94. // nthPriceByte(n) = 255 - (n % 256), starting with n=1
  95. //
  96. // Examples:
  97. // 1st prod = "0x010101[...]"
  98. // 1st price = "0xFEFEFE[...]"
  99. // 2nd prod = "0x020202[...]"
  100. // 2nd price = "0xFDFDFD[...]"
  101. // 3rd prod = "0x030303[...]"
  102. // 3rd price = "0xFCFCFC[...]"
  103. const RAW_BATCH_TIMESTAMP_REGEX = /DEADBEEFFADEDEED/g;
  104. const RAW_BATCH_PRICE_REGEX = /0000002BAD2FEED7/g;
  105. const RAW_PRICE_ATTESTATION_SIZE = 190;
  106. const RAW_BATCH_ATTESTATION_COUNT = 10;
  107. const rawBatchPriceAttestation = "0x" + "50325748000202000A00BE503257480002010101010101010101010101010101010101010101010101010101010101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010202020202020202020202020202020202020202020202020202020202020202FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010303030303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010404040404040404040404040404040404040404040404040404040404040404FBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFB010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010505050505050505050505050505050505050505050505050505050505050505FAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFA010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010606060606060606060606060606060606060606060606060606060606060606F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010707070707070707070707070707070707070707070707070707070707070707F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010808080808080808080808080808080808080808080808080808080808080808F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010909090909090909090909090909090909090909090909090909090909090909F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0AF5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF";
  108. // Takes an unsigned 64-bit integer, converts it to hex with 0-padding
  109. function u64ToHex(timestamp) {
  110. // u64 -> 8 bytes -> 16 hex bytes
  111. return timestamp.toString(16).padStart(16, "0");
  112. }
  113. function generateRawBatchAttestation(timestamp, priceVal) {
  114. const ts = u64ToHex(timestamp);
  115. const price = u64ToHex(priceVal);
  116. const replaced = rawBatchPriceAttestation
  117. .replace(RAW_BATCH_TIMESTAMP_REGEX, ts)
  118. .replace(RAW_BATCH_PRICE_REGEX, price);
  119. return replaced;
  120. }
  121. it("should parse batch price attestation correctly", async function() {
  122. const magic = 0x50325748;
  123. const version = 2;
  124. let timestamp = 1647273460;
  125. let priceVal = 1337;
  126. let rawBatch = generateRawBatchAttestation(timestamp, priceVal);
  127. let parsed = await this.pythProxy.parseBatchPriceAttestation(rawBatch);
  128. // Check the header
  129. assert.equal(parsed.header.magic, magic);
  130. assert.equal(parsed.header.version, version);
  131. assert.equal(parsed.header.payloadId, 2);
  132. assert.equal(parsed.nAttestations, RAW_BATCH_ATTESTATION_COUNT);
  133. assert.equal(parsed.attestationSize, RAW_PRICE_ATTESTATION_SIZE);
  134. assert.equal(parsed.attestations.length, parsed.nAttestations);
  135. for (var i = 0; i < parsed.attestations.length; ++i) {
  136. const prodId = "0x" + (i+1).toString(16).padStart(2, "0").repeat(32);
  137. const priceByte = 255 - ((i+1) % 256);
  138. const priceId = "0x" + priceByte.toString(16).padStart(2, "0").repeat(32);
  139. assert.equal(parsed.attestations[i].header.magic, magic);
  140. assert.equal(parsed.attestations[i].header.version, version);
  141. assert.equal(parsed.attestations[i].header.payloadId, 1);
  142. assert.equal(parsed.attestations[i].productId, prodId);
  143. assert.equal(parsed.attestations[i].priceId, priceId);
  144. assert.equal(parsed.attestations[i].priceType, 1);
  145. assert.equal(parsed.attestations[i].price, priceVal);
  146. assert.equal(parsed.attestations[i].exponent, -3);
  147. assert.equal(parsed.attestations[i].emaPrice.value, -42);
  148. assert.equal(parsed.attestations[i].emaPrice.numerator, 15);
  149. assert.equal(parsed.attestations[i].emaPrice.denominator, 37);
  150. assert.equal(parsed.attestations[i].emaConf.value, 42);
  151. assert.equal(parsed.attestations[i].emaConf.numerator, 1111);
  152. assert.equal(parsed.attestations[i].emaConf.denominator, 2222);
  153. assert.equal(parsed.attestations[i].confidenceInterval, 101);
  154. assert.equal(parsed.attestations[i].status, 1);
  155. assert.equal(parsed.attestations[i].corpAct, 0);
  156. assert.equal(parsed.attestations[i].timestamp, timestamp);
  157. assert.equal(parsed.attestations[i].num_publishers, 123212);
  158. assert.equal(parsed.attestations[i].max_num_publishers, 321232);
  159. assert.equal(parsed.attestations[i].publish_time, 0xdeadbeef);
  160. assert.equal(parsed.attestations[i].prev_publish_time, 0xdeadbabe);
  161. assert.equal(parsed.attestations[i].prev_price, 0xdeadfacebeef);
  162. assert.equal(parsed.attestations[i].prev_conf, 0xbadbadbeef);
  163. console.debug(`attestation ${i + 1}/${parsed.attestations.length} parsed OK`);
  164. }
  165. })
  166. async function attest(contract, data) {
  167. const vm = await signAndEncodeVM(
  168. 1,
  169. 1,
  170. testPyth2WormholeChainId,
  171. testPyth2WormholeEmitter,
  172. 0,
  173. data,
  174. [
  175. testSigner1PK
  176. ],
  177. 0,
  178. 0
  179. );
  180. await contract.updatePriceBatchFromVm("0x"+vm);
  181. }
  182. it("should attest price updates over wormhole", async function() {
  183. let rawBatch = generateRawBatchAttestation(1647273460, 1337);
  184. await attest(this.pythProxy, rawBatch);
  185. })
  186. it("should cache price updates", async function() {
  187. let currentTimestamp = (await web3.eth.getBlock("latest")).timestamp;
  188. let priceVal = 521;
  189. let rawBatch = generateRawBatchAttestation(currentTimestamp, priceVal);
  190. await attest(this.pythProxy, rawBatch);
  191. let first_prod_id = "0x" + "01".repeat(32)
  192. let first_price_id = "0x" + "fe".repeat(32)
  193. let second_prod_id = "0x" + "02".repeat(32)
  194. let second_price_id = "0x" + "fd".repeat(32)
  195. // Confirm that previously non-existent feeds are created
  196. let first = await this.pythProxy.queryPriceFeed(first_price_id);
  197. console.debug(`first is ${JSON.stringify(first)}`);
  198. assert.equal(first.price, priceVal);
  199. let second = await this.pythProxy.queryPriceFeed(second_price_id);
  200. assert.equal(second.price, priceVal);
  201. // Confirm the price is bumped after a new attestation updates each record
  202. let nextTimestamp = currentTimestamp + 1;
  203. let rawBatch2 = generateRawBatchAttestation(nextTimestamp, priceVal + 5);
  204. await attest(this.pythProxy, rawBatch2);
  205. first = await this.pythProxy.queryPriceFeed(first_price_id);
  206. assert.equal(first.price, priceVal + 5);
  207. second = await this.pythProxy.queryPriceFeed(second_price_id);
  208. assert.equal(second.price, priceVal + 5);
  209. // Confirm that only strictly larger timestamps trigger updates
  210. let rawBatch3 = generateRawBatchAttestation(nextTimestamp, priceVal + 10);
  211. await attest(this.pythProxy, rawBatch3);
  212. first = await this.pythProxy.queryPriceFeed(first_price_id);
  213. assert.equal(first.price, priceVal + 5);
  214. assert.notEqual(first.price, priceVal + 10);
  215. second = await this.pythProxy.queryPriceFeed(second_price_id);
  216. assert.equal(second.price, priceVal + 5);
  217. assert.notEqual(second.price, priceVal + 10);
  218. })
  219. it("should fail transaction if a price is not found", async function() {
  220. await expectRevert(
  221. this.pythProxy.queryPriceFeed(
  222. "0xdeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeed"),
  223. "no price feed found for the given price id");
  224. })
  225. it("should show stale cached prices as unknown", async function() {
  226. let smallestTimestamp = 1;
  227. let rawBatch = generateRawBatchAttestation(smallestTimestamp, 1337);
  228. await attest(this.pythProxy, rawBatch);
  229. for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) {
  230. const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32);
  231. let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id);
  232. assert.equal(priceFeedResult.status.toString(), PythStructs.PriceStatus.UNKNOWN.toString());
  233. }
  234. })
  235. it("should show cached prices too far into the future as unknown", async function() {
  236. let largestTimestamp = 4294967295;
  237. let rawBatch = generateRawBatchAttestation(largestTimestamp, 1337);
  238. await attest(this.pythProxy, rawBatch);
  239. for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) {
  240. const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32);
  241. let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id);
  242. assert.equal(priceFeedResult.status.toString(), PythStructs.PriceStatus.UNKNOWN.toString());
  243. }
  244. })
  245. });
  246. const signAndEncodeVM = async function (
  247. timestamp,
  248. nonce,
  249. emitterChainId,
  250. emitterAddress,
  251. sequence,
  252. data,
  253. signers,
  254. guardianSetIndex,
  255. consistencyLevel
  256. ) {
  257. const body = [
  258. web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)),
  259. web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)),
  260. web3.eth.abi.encodeParameter("uint16", emitterChainId).substring(2 + (64 - 4)),
  261. web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2),
  262. web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)),
  263. web3.eth.abi.encodeParameter("uint8", consistencyLevel).substring(2 + (64 - 2)),
  264. data.substr(2)
  265. ]
  266. const hash = web3.utils.soliditySha3(web3.utils.soliditySha3("0x" + body.join("")))
  267. let signatures = "";
  268. for (let i in signers) {
  269. const ec = new elliptic.ec("secp256k1");
  270. const key = ec.keyFromPrivate(signers[i]);
  271. const signature = key.sign(hash.substr(2), {canonical: true});
  272. const packSig = [
  273. web3.eth.abi.encodeParameter("uint8", i).substring(2 + (64 - 2)),
  274. zeroPadBytes(signature.r.toString(16), 32),
  275. zeroPadBytes(signature.s.toString(16), 32),
  276. web3.eth.abi.encodeParameter("uint8", signature.recoveryParam).substr(2 + (64 - 2)),
  277. ]
  278. signatures += packSig.join("")
  279. }
  280. const vm = [
  281. web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)),
  282. web3.eth.abi.encodeParameter("uint32", guardianSetIndex).substring(2 + (64 - 8)),
  283. web3.eth.abi.encodeParameter("uint8", signers.length).substring(2 + (64 - 2)),
  284. signatures,
  285. body.join("")
  286. ].join("");
  287. return vm
  288. }
  289. function zeroPadBytes(value, length) {
  290. while (value.length < 2 * length) {
  291. value = "0" + value;
  292. }
  293. return value;
  294. }