pyth.js 20 KB

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