pyth.js 18 KB

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