Pyth.t.sol 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
  4. import "forge-std/Test.sol";
  5. import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
  6. import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
  7. import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
  8. import "./utils/WormholeTestUtils.t.sol";
  9. import "./utils/PythTestUtils.t.sol";
  10. import "./utils/RandTestUtils.t.sol";
  11. import "forge-std/console.sol";
  12. contract PythTest is Test, WormholeTestUtils, PythTestUtils {
  13. IPyth public pyth;
  14. // -1 is equal to 0xffffff which is the biggest uint if converted back
  15. uint64 constant MAX_UINT64 = uint64(int64(-1));
  16. // 2/3 of the guardians should sign a message for a VAA which is 13 out of 19 guardians.
  17. // It is possible to have more signers but the median seems to be 13.
  18. uint8 constant NUM_GUARDIAN_SIGNERS = 13;
  19. // We will have less than 512 price for a foreseeable future.
  20. uint8 constant MERKLE_TREE_DEPTH = 9;
  21. // Base TWAP messages that will be used as templates for tests
  22. TwapPriceFeedMessage[2] baseTwapStartMessages;
  23. TwapPriceFeedMessage[2] baseTwapEndMessages;
  24. bytes32[2] basePriceIds;
  25. function setUp() public {
  26. pyth = IPyth(setUpPyth(setUpWormholeReceiver(NUM_GUARDIAN_SIGNERS)));
  27. // Initialize base TWAP messages for two price feeds
  28. basePriceIds[0] = bytes32(uint256(1));
  29. basePriceIds[1] = bytes32(uint256(2));
  30. // First price feed TWAP messages
  31. baseTwapStartMessages[0] = TwapPriceFeedMessage({
  32. priceId: basePriceIds[0],
  33. cumulativePrice: 100_000, // Base cumulative value
  34. cumulativeConf: 10_000, // Base cumulative conf
  35. numDownSlots: 0,
  36. publishSlot: 1000,
  37. publishTime: 1000,
  38. prevPublishTime: 900,
  39. expo: -8
  40. });
  41. baseTwapEndMessages[0] = TwapPriceFeedMessage({
  42. priceId: basePriceIds[0],
  43. cumulativePrice: 210_000, // Increased by 110_000
  44. cumulativeConf: 18_000, // Increased by 8_000
  45. numDownSlots: 0,
  46. publishSlot: 1100,
  47. publishTime: 1100,
  48. prevPublishTime: 1000,
  49. expo: -8
  50. });
  51. // Second price feed TWAP messages
  52. baseTwapStartMessages[1] = TwapPriceFeedMessage({
  53. priceId: basePriceIds[1],
  54. cumulativePrice: 500_000, // Different base cumulative value
  55. cumulativeConf: 20_000, // Different base cumulative conf
  56. numDownSlots: 0,
  57. publishSlot: 1000,
  58. publishTime: 1000,
  59. prevPublishTime: 900,
  60. expo: -8
  61. });
  62. baseTwapEndMessages[1] = TwapPriceFeedMessage({
  63. priceId: basePriceIds[1],
  64. cumulativePrice: 800_000, // Increased by 300_000
  65. cumulativeConf: 40_000, // Increased by 20_000
  66. numDownSlots: 0,
  67. publishSlot: 1100,
  68. publishTime: 1100,
  69. prevPublishTime: 1000,
  70. expo: -8
  71. });
  72. }
  73. function generateRandomPriceMessages(
  74. uint length
  75. )
  76. internal
  77. returns (bytes32[] memory priceIds, PriceFeedMessage[] memory messages)
  78. {
  79. messages = new PriceFeedMessage[](length);
  80. priceIds = new bytes32[](length);
  81. for (uint i = 0; i < length; i++) {
  82. messages[i].priceId = bytes32(i + 1); // price ids should be non-zero and unique
  83. messages[i].price = getRandInt64();
  84. messages[i].conf = getRandUint64();
  85. messages[i].expo = getRandInt32();
  86. messages[i].emaPrice = getRandInt64();
  87. messages[i].emaConf = getRandUint64();
  88. messages[i].publishTime = getRandUint64();
  89. messages[i].prevPublishTime = getRandUint64();
  90. priceIds[i] = messages[i].priceId;
  91. }
  92. }
  93. // This method divides messages into a couple of batches and creates
  94. // updateData for them. It returns the updateData and the updateFee
  95. function createBatchedUpdateDataFromMessagesWithConfig(
  96. PriceFeedMessage[] memory messages,
  97. MerkleUpdateConfig memory config
  98. ) internal returns (bytes[] memory updateData, uint updateFee) {
  99. uint batchSize = 1 + (getRandUint() % messages.length);
  100. uint numBatches = (messages.length + batchSize - 1) / batchSize;
  101. updateData = new bytes[](numBatches);
  102. for (uint i = 0; i < messages.length; i += batchSize) {
  103. uint len = batchSize;
  104. if (messages.length - i < len) {
  105. len = messages.length - i;
  106. }
  107. PriceFeedMessage[] memory batchMessages = new PriceFeedMessage[](
  108. len
  109. );
  110. for (uint j = i; j < i + len; j++) {
  111. batchMessages[j - i] = messages[j];
  112. }
  113. updateData[i / batchSize] = generateWhMerkleUpdateWithSource(
  114. batchMessages,
  115. config
  116. );
  117. }
  118. updateFee = pyth.getUpdateFee(updateData);
  119. }
  120. function createBatchedUpdateDataFromMessages(
  121. PriceFeedMessage[] memory messages
  122. ) internal returns (bytes[] memory updateData, uint updateFee) {
  123. (updateData, updateFee) = createBatchedUpdateDataFromMessagesWithConfig(
  124. messages,
  125. MerkleUpdateConfig(
  126. MERKLE_TREE_DEPTH,
  127. NUM_GUARDIAN_SIGNERS,
  128. SOURCE_EMITTER_CHAIN_ID,
  129. SOURCE_EMITTER_ADDRESS,
  130. false
  131. )
  132. );
  133. }
  134. // This method divides messages into a couple of batches and creates
  135. // twap updateData for them. It returns the updateData and the updateFee
  136. function createBatchedTwapUpdateDataFromMessagesWithConfig(
  137. PriceFeedMessage[] memory messages,
  138. MerkleUpdateConfig memory config
  139. ) public returns (bytes[] memory updateData, uint updateFee) {
  140. require(messages.length >= 2, "At least 2 messages required for TWAP");
  141. // Create arrays to hold the start and end TWAP messages for all price feeds
  142. TwapPriceFeedMessage[]
  143. memory startTwapMessages = new TwapPriceFeedMessage[](
  144. messages.length / 2
  145. );
  146. TwapPriceFeedMessage[]
  147. memory endTwapMessages = new TwapPriceFeedMessage[](
  148. messages.length / 2
  149. );
  150. // Fill the arrays with all price feeds' start and end points
  151. for (uint i = 0; i < messages.length / 2; i++) {
  152. // Create start message for this price feed
  153. startTwapMessages[i].priceId = messages[i * 2].priceId;
  154. startTwapMessages[i].cumulativePrice =
  155. int128(messages[i * 2].price) *
  156. 1000;
  157. startTwapMessages[i].cumulativeConf =
  158. uint128(messages[i * 2].conf) *
  159. 1000;
  160. startTwapMessages[i].numDownSlots = 0; // No down slots for testing
  161. startTwapMessages[i].expo = messages[i * 2].expo;
  162. startTwapMessages[i].publishTime = messages[i * 2].publishTime;
  163. startTwapMessages[i].prevPublishTime = messages[i * 2]
  164. .prevPublishTime;
  165. startTwapMessages[i].publishSlot = 1000; // Start slot
  166. // Create end message for this price feed
  167. endTwapMessages[i].priceId = messages[i * 2 + 1].priceId;
  168. endTwapMessages[i].cumulativePrice =
  169. int128(messages[i * 2 + 1].price) *
  170. 1000 +
  171. startTwapMessages[i].cumulativePrice;
  172. endTwapMessages[i].cumulativeConf =
  173. uint128(messages[i * 2 + 1].conf) *
  174. 1000 +
  175. startTwapMessages[i].cumulativeConf;
  176. endTwapMessages[i].numDownSlots = 0; // No down slots for testing
  177. endTwapMessages[i].expo = messages[i * 2 + 1].expo;
  178. endTwapMessages[i].publishTime = messages[i * 2 + 1].publishTime;
  179. endTwapMessages[i].prevPublishTime = messages[i * 2 + 1]
  180. .prevPublishTime;
  181. endTwapMessages[i].publishSlot = 1100; // End slot (100 slots after start)
  182. }
  183. // Create exactly 2 updateData entries as required by parseTwapPriceFeedUpdates
  184. updateData = new bytes[](2);
  185. // First update contains all start points
  186. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  187. startTwapMessages,
  188. config
  189. );
  190. // Second update contains all end points
  191. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  192. endTwapMessages,
  193. config
  194. );
  195. // Calculate the update fee
  196. updateFee = pyth.getUpdateFee(updateData);
  197. }
  198. function createBatchedTwapUpdateDataFromMessages(
  199. PriceFeedMessage[] memory messages
  200. ) internal returns (bytes[] memory updateData, uint updateFee) {
  201. (
  202. updateData,
  203. updateFee
  204. ) = createBatchedTwapUpdateDataFromMessagesWithConfig(
  205. messages,
  206. MerkleUpdateConfig(
  207. MERKLE_TREE_DEPTH,
  208. NUM_GUARDIAN_SIGNERS,
  209. SOURCE_EMITTER_CHAIN_ID,
  210. SOURCE_EMITTER_ADDRESS,
  211. false
  212. )
  213. );
  214. }
  215. function testParsePriceFeedUpdatesWorks(uint seed) public {
  216. setRandSeed(seed);
  217. uint numMessages = 1 + (getRandUint() % 10);
  218. (
  219. bytes32[] memory priceIds,
  220. PriceFeedMessage[] memory messages
  221. ) = generateRandomPriceMessages(numMessages);
  222. (
  223. bytes[] memory updateData,
  224. uint updateFee
  225. ) = createBatchedUpdateDataFromMessages(messages);
  226. PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
  227. value: updateFee
  228. }(updateData, priceIds, 0, MAX_UINT64);
  229. for (uint i = 0; i < numMessages; i++) {
  230. assertEq(priceFeeds[i].id, priceIds[i]);
  231. assertEq(priceFeeds[i].price.price, messages[i].price);
  232. assertEq(priceFeeds[i].price.conf, messages[i].conf);
  233. assertEq(priceFeeds[i].price.expo, messages[i].expo);
  234. assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime);
  235. assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice);
  236. assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf);
  237. assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo);
  238. assertEq(
  239. priceFeeds[i].emaPrice.publishTime,
  240. messages[i].publishTime
  241. );
  242. }
  243. }
  244. function testParsePriceFeedUpdatesWithConfigIfStorageTrue(
  245. uint seed
  246. ) public {
  247. setRandSeed(seed);
  248. uint numMessages = 1 + (getRandUint() % 10);
  249. (
  250. bytes32[] memory priceIds,
  251. PriceFeedMessage[] memory messages
  252. ) = generateRandomPriceMessages(numMessages);
  253. (
  254. bytes[] memory updateData,
  255. uint updateFee
  256. ) = createBatchedUpdateDataFromMessages(messages);
  257. (PythStructs.PriceFeed[] memory priceFeeds, ) = pyth
  258. .parsePriceFeedUpdatesWithConfig{value: updateFee}(
  259. updateData,
  260. priceIds,
  261. 0,
  262. MAX_UINT64,
  263. false,
  264. true,
  265. true
  266. );
  267. for (uint i = 0; i < numMessages; i++) {
  268. // Validating that returned priceIds are equal
  269. assertEq(priceFeeds[i].id, priceIds[i]);
  270. assertEq(priceFeeds[i].price.price, messages[i].price);
  271. assertEq(priceFeeds[i].price.conf, messages[i].conf);
  272. assertEq(priceFeeds[i].price.expo, messages[i].expo);
  273. assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime);
  274. assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice);
  275. assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf);
  276. assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo);
  277. assertEq(
  278. priceFeeds[i].emaPrice.publishTime,
  279. messages[i].publishTime
  280. );
  281. // Validating that prices are stored on chain
  282. PythStructs.Price memory curPrice = pyth.getPriceUnsafe(
  283. messages[i].priceId
  284. );
  285. assertEq(priceFeeds[i].price.price, curPrice.price);
  286. assertEq(priceFeeds[i].price.conf, curPrice.conf);
  287. assertEq(priceFeeds[i].price.expo, curPrice.expo);
  288. assertEq(priceFeeds[i].price.publishTime, curPrice.publishTime);
  289. }
  290. }
  291. function testParsePriceFeedUpdatesWithConfigIfStorageFalse(
  292. uint seed
  293. ) public {
  294. setRandSeed(seed);
  295. uint numMessages = 1 + (getRandUint() % 10);
  296. (
  297. bytes32[] memory priceIds,
  298. PriceFeedMessage[] memory messages
  299. ) = generateRandomPriceMessages(numMessages);
  300. (
  301. bytes[] memory updateData,
  302. uint updateFee
  303. ) = createBatchedUpdateDataFromMessages(messages);
  304. pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
  305. updateData,
  306. priceIds,
  307. 0,
  308. MAX_UINT64,
  309. false,
  310. true,
  311. false
  312. );
  313. // validate that stored prices of each priceId are still unpopulated
  314. for (uint i = 0; i < numMessages; i++) {
  315. vm.expectRevert(PythErrors.PriceFeedNotFound.selector);
  316. pyth.getPriceUnsafe(priceIds[i]);
  317. }
  318. }
  319. function testParsePriceFeedUpdatesWithConfigWorks(uint seed) public {
  320. setRandSeed(seed);
  321. uint numMessages = 1 + (getRandUint() % 10);
  322. (
  323. bytes32[] memory priceIds,
  324. PriceFeedMessage[] memory messages
  325. ) = generateRandomPriceMessages(numMessages);
  326. (
  327. bytes[] memory updateData,
  328. uint updateFee
  329. ) = createBatchedUpdateDataFromMessages(messages);
  330. (
  331. PythStructs.PriceFeed[] memory priceFeeds,
  332. uint64[] memory slots
  333. ) = pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
  334. updateData,
  335. priceIds,
  336. 0,
  337. MAX_UINT64,
  338. false,
  339. true,
  340. false
  341. );
  342. assertEq(priceFeeds.length, numMessages);
  343. assertEq(slots.length, numMessages);
  344. for (uint i = 0; i < numMessages; i++) {
  345. assertEq(priceFeeds[i].id, priceIds[i]);
  346. assertEq(priceFeeds[i].price.price, messages[i].price);
  347. assertEq(priceFeeds[i].price.conf, messages[i].conf);
  348. assertEq(priceFeeds[i].price.expo, messages[i].expo);
  349. assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime);
  350. assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice);
  351. assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf);
  352. assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo);
  353. assertEq(
  354. priceFeeds[i].emaPrice.publishTime,
  355. messages[i].publishTime
  356. );
  357. // Check that the slot returned is 1, as set in generateWhMerkleUpdateWithSource
  358. assertEq(slots[i], 1);
  359. }
  360. }
  361. function testParsePriceFeedUpdatesWorksWithOverlappingWithinTimeRangeUpdates()
  362. public
  363. {
  364. PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
  365. messages[0].priceId = bytes32(uint(1));
  366. messages[0].price = 1000;
  367. messages[0].publishTime = 10;
  368. messages[1].priceId = bytes32(uint(1));
  369. messages[1].price = 2000;
  370. messages[1].publishTime = 20;
  371. (
  372. bytes[] memory updateData,
  373. uint updateFee
  374. ) = createBatchedUpdateDataFromMessages(messages);
  375. bytes32[] memory priceIds = new bytes32[](1);
  376. priceIds[0] = bytes32(uint(1));
  377. PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
  378. value: updateFee
  379. }(updateData, priceIds, 0, 20);
  380. assertEq(priceFeeds.length, 1);
  381. assertEq(priceFeeds[0].id, bytes32(uint(1)));
  382. assertTrue(
  383. (priceFeeds[0].price.price == 1000 &&
  384. priceFeeds[0].price.publishTime == 10) ||
  385. (priceFeeds[0].price.price == 2000 &&
  386. priceFeeds[0].price.publishTime == 20)
  387. );
  388. }
  389. function testParsePriceFeedUpdatesWorksWithOverlappingMixedTimeRangeUpdates()
  390. public
  391. {
  392. PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
  393. messages[0].priceId = bytes32(uint(1));
  394. messages[0].price = 1000;
  395. messages[0].publishTime = 10;
  396. messages[1].priceId = bytes32(uint(1));
  397. messages[1].price = 2000;
  398. messages[1].publishTime = 20;
  399. (
  400. bytes[] memory updateData,
  401. uint updateFee
  402. ) = createBatchedUpdateDataFromMessages(messages);
  403. bytes32[] memory priceIds = new bytes32[](1);
  404. priceIds[0] = bytes32(uint(1));
  405. PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
  406. value: updateFee
  407. }(updateData, priceIds, 5, 15);
  408. assertEq(priceFeeds.length, 1);
  409. assertEq(priceFeeds[0].id, bytes32(uint(1)));
  410. assertEq(priceFeeds[0].price.price, 1000);
  411. assertEq(priceFeeds[0].price.publishTime, 10);
  412. priceFeeds = pyth.parsePriceFeedUpdates{value: updateFee}(
  413. updateData,
  414. priceIds,
  415. 15,
  416. 25
  417. );
  418. assertEq(priceFeeds.length, 1);
  419. assertEq(priceFeeds[0].id, bytes32(uint(1)));
  420. assertEq(priceFeeds[0].price.price, 2000);
  421. assertEq(priceFeeds[0].price.publishTime, 20);
  422. }
  423. function testParsePriceFeedUpdatesRevertsIfUpdateVAAIsInvalid(
  424. uint seed
  425. ) public {
  426. setRandSeed(seed);
  427. uint numMessages = 1 + (getRandUint() % 10);
  428. (
  429. bytes32[] memory priceIds,
  430. PriceFeedMessage[] memory messages
  431. ) = generateRandomPriceMessages(numMessages);
  432. (
  433. bytes[] memory updateData,
  434. uint updateFee
  435. ) = createBatchedUpdateDataFromMessagesWithConfig(
  436. messages,
  437. MerkleUpdateConfig(
  438. MERKLE_TREE_DEPTH,
  439. NUM_GUARDIAN_SIGNERS,
  440. SOURCE_EMITTER_CHAIN_ID,
  441. SOURCE_EMITTER_ADDRESS,
  442. true
  443. )
  444. );
  445. // It might revert due to different wormhole errors
  446. vm.expectRevert();
  447. pyth.parsePriceFeedUpdates{value: updateFee}(
  448. updateData,
  449. priceIds,
  450. 0,
  451. MAX_UINT64
  452. );
  453. }
  454. function testParsePriceFeedUpdatesWithConfigRevertsWithExcessUpdateData()
  455. public
  456. {
  457. // Create a price update with more price updates than requested price IDs
  458. uint numPriceIds = 2;
  459. uint numMessages = numPriceIds + 1; // One more than the number of price IDs
  460. (
  461. bytes32[] memory priceIds,
  462. PriceFeedMessage[] memory messages
  463. ) = generateRandomPriceMessages(numMessages);
  464. // Only use a subset of the price IDs to trigger the strict check
  465. bytes32[] memory requestedPriceIds = new bytes32[](numPriceIds);
  466. for (uint i = 0; i < numPriceIds; i++) {
  467. requestedPriceIds[i] = priceIds[i];
  468. }
  469. (
  470. bytes[] memory updateData,
  471. uint updateFee
  472. ) = createBatchedUpdateDataFromMessages(messages);
  473. // Should revert in strict mode
  474. vm.expectRevert(PythErrors.InvalidArgument.selector);
  475. pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
  476. updateData,
  477. requestedPriceIds,
  478. 0,
  479. MAX_UINT64,
  480. false,
  481. true,
  482. false
  483. );
  484. }
  485. function testParsePriceFeedUpdatesWithConfigRevertsWithFewerUpdateData()
  486. public
  487. {
  488. // Create a price update with fewer price updates than requested price IDs
  489. uint numPriceIds = 3;
  490. uint numMessages = numPriceIds - 1; // One less than the number of price IDs
  491. (
  492. bytes32[] memory priceIds,
  493. PriceFeedMessage[] memory messages
  494. ) = generateRandomPriceMessages(numMessages);
  495. // Create a larger array of requested price IDs to trigger the strict check
  496. bytes32[] memory requestedPriceIds = new bytes32[](numPriceIds);
  497. for (uint i = 0; i < numMessages; i++) {
  498. requestedPriceIds[i] = priceIds[i];
  499. }
  500. // Add an extra price ID that won't be in the updates
  501. requestedPriceIds[numMessages] = bytes32(
  502. uint256(keccak256(abi.encodePacked("extra_id")))
  503. );
  504. (
  505. bytes[] memory updateData,
  506. uint updateFee
  507. ) = createBatchedUpdateDataFromMessages(messages);
  508. // Should revert in strict mode because we have fewer updates than price IDs
  509. vm.expectRevert(PythErrors.InvalidArgument.selector);
  510. pyth.parsePriceFeedUpdatesWithConfig{value: updateFee}(
  511. updateData,
  512. requestedPriceIds,
  513. 0,
  514. MAX_UINT64,
  515. false,
  516. true,
  517. false
  518. );
  519. }
  520. function testParsePriceFeedUpdatesRevertsIfUpdateSourceChainIsInvalid()
  521. public
  522. {
  523. uint numMessages = 10;
  524. (
  525. bytes32[] memory priceIds,
  526. PriceFeedMessage[] memory messages
  527. ) = generateRandomPriceMessages(numMessages);
  528. (
  529. bytes[] memory updateData,
  530. uint updateFee
  531. ) = createBatchedUpdateDataFromMessagesWithConfig(
  532. messages,
  533. MerkleUpdateConfig(
  534. MERKLE_TREE_DEPTH,
  535. NUM_GUARDIAN_SIGNERS,
  536. SOURCE_EMITTER_CHAIN_ID + 1,
  537. SOURCE_EMITTER_ADDRESS,
  538. false
  539. )
  540. );
  541. vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
  542. pyth.parsePriceFeedUpdates{value: updateFee}(
  543. updateData,
  544. priceIds,
  545. 0,
  546. MAX_UINT64
  547. );
  548. }
  549. function testParsePriceFeedUpdatesRevertsIfUpdateSourceAddressIsInvalid()
  550. public
  551. {
  552. uint numMessages = 10;
  553. (
  554. bytes32[] memory priceIds,
  555. PriceFeedMessage[] memory messages
  556. ) = generateRandomPriceMessages(numMessages);
  557. (
  558. bytes[] memory updateData,
  559. uint updateFee
  560. ) = createBatchedUpdateDataFromMessagesWithConfig(
  561. messages,
  562. MerkleUpdateConfig(
  563. MERKLE_TREE_DEPTH,
  564. NUM_GUARDIAN_SIGNERS,
  565. SOURCE_EMITTER_CHAIN_ID,
  566. 0x00000000000000000000000000000000000000000000000000000000000000aa, // Random wrong source address
  567. false
  568. )
  569. );
  570. vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
  571. pyth.parsePriceFeedUpdates{value: updateFee}(
  572. updateData,
  573. priceIds,
  574. 0,
  575. MAX_UINT64
  576. );
  577. }
  578. function testParseTwapPriceFeedUpdates() public {
  579. bytes32[] memory priceIds = new bytes32[](1);
  580. priceIds[0] = basePriceIds[0];
  581. // Create update data directly from base TWAP messages
  582. bytes[] memory updateData = new bytes[](2);
  583. TwapPriceFeedMessage[]
  584. memory startMessages = new TwapPriceFeedMessage[](1);
  585. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  586. 1
  587. );
  588. startMessages[0] = baseTwapStartMessages[0];
  589. endMessages[0] = baseTwapEndMessages[0];
  590. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  591. startMessages,
  592. MerkleUpdateConfig(
  593. MERKLE_TREE_DEPTH,
  594. NUM_GUARDIAN_SIGNERS,
  595. SOURCE_EMITTER_CHAIN_ID,
  596. SOURCE_EMITTER_ADDRESS,
  597. false
  598. )
  599. );
  600. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  601. endMessages,
  602. MerkleUpdateConfig(
  603. MERKLE_TREE_DEPTH,
  604. NUM_GUARDIAN_SIGNERS,
  605. SOURCE_EMITTER_CHAIN_ID,
  606. SOURCE_EMITTER_ADDRESS,
  607. false
  608. )
  609. );
  610. uint updateFee = pyth.getTwapUpdateFee(updateData);
  611. // Parse the TWAP updates
  612. PythStructs.TwapPriceFeed[] memory twapPriceFeeds = pyth
  613. .parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  614. // Validate results
  615. assertEq(twapPriceFeeds[0].id, basePriceIds[0]);
  616. assertEq(
  617. twapPriceFeeds[0].startTime,
  618. baseTwapStartMessages[0].publishTime
  619. );
  620. assertEq(twapPriceFeeds[0].endTime, baseTwapEndMessages[0].publishTime);
  621. assertEq(twapPriceFeeds[0].twap.expo, baseTwapStartMessages[0].expo);
  622. // Expected TWAP price: (210_000 - 100_000) / (1100 - 1000) = 1100
  623. assertEq(twapPriceFeeds[0].twap.price, int64(1100));
  624. // Expected TWAP conf: (18_000 - 10_000) / (1100 - 1000) = 80
  625. assertEq(twapPriceFeeds[0].twap.conf, uint64(80));
  626. // Validate the downSlotsRatio is 0 in our test implementation
  627. assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));
  628. }
  629. function testParseTwapPriceFeedUpdatesRevertsWithInvalidUpdateDataLength()
  630. public
  631. {
  632. bytes32[] memory priceIds = new bytes32[](1);
  633. priceIds[0] = bytes32(uint256(1));
  634. // Create invalid update data with wrong length
  635. bytes[] memory updateData = new bytes[](1); // Should be 2
  636. updateData[0] = new bytes(1);
  637. vm.expectRevert(PythErrors.InvalidUpdateData.selector);
  638. pyth.parseTwapPriceFeedUpdates{value: 0}(updateData, priceIds);
  639. }
  640. function testParseTwapPriceFeedUpdatesRevertsWithMismatchedPriceIds()
  641. public
  642. {
  643. bytes32[] memory priceIds = new bytes32[](1);
  644. priceIds[0] = bytes32(uint256(1));
  645. // Copy base messages
  646. TwapPriceFeedMessage[]
  647. memory startMessages = new TwapPriceFeedMessage[](1);
  648. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  649. 1
  650. );
  651. startMessages[0] = baseTwapStartMessages[0];
  652. endMessages[0] = baseTwapEndMessages[0];
  653. // Change end message priceId to create mismatch
  654. endMessages[0].priceId = bytes32(uint256(2));
  655. // Create update data
  656. bytes[] memory updateData = new bytes[](2);
  657. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  658. startMessages,
  659. MerkleUpdateConfig(
  660. MERKLE_TREE_DEPTH,
  661. NUM_GUARDIAN_SIGNERS,
  662. SOURCE_EMITTER_CHAIN_ID,
  663. SOURCE_EMITTER_ADDRESS,
  664. false
  665. )
  666. );
  667. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  668. endMessages,
  669. MerkleUpdateConfig(
  670. MERKLE_TREE_DEPTH,
  671. NUM_GUARDIAN_SIGNERS,
  672. SOURCE_EMITTER_CHAIN_ID,
  673. SOURCE_EMITTER_ADDRESS,
  674. false
  675. )
  676. );
  677. uint updateFee = pyth.getTwapUpdateFee(updateData);
  678. vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
  679. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  680. }
  681. function testParseTwapPriceFeedUpdatesRevertsWithInvalidTimeOrdering()
  682. public
  683. {
  684. bytes32[] memory priceIds = new bytes32[](1);
  685. priceIds[0] = bytes32(uint256(1));
  686. // Copy base messages
  687. TwapPriceFeedMessage[]
  688. memory startMessages = new TwapPriceFeedMessage[](1);
  689. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  690. 1
  691. );
  692. startMessages[0] = baseTwapStartMessages[0];
  693. endMessages[0] = baseTwapEndMessages[0];
  694. // Modify times to create invalid ordering
  695. startMessages[0].publishTime = 1100;
  696. startMessages[0].publishSlot = 1100;
  697. endMessages[0].publishTime = 1000;
  698. endMessages[0].publishSlot = 1000;
  699. endMessages[0].prevPublishTime = 900;
  700. // Create update data
  701. bytes[] memory updateData = new bytes[](2);
  702. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  703. startMessages,
  704. MerkleUpdateConfig(
  705. MERKLE_TREE_DEPTH,
  706. NUM_GUARDIAN_SIGNERS,
  707. SOURCE_EMITTER_CHAIN_ID,
  708. SOURCE_EMITTER_ADDRESS,
  709. false
  710. )
  711. );
  712. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  713. endMessages,
  714. MerkleUpdateConfig(
  715. MERKLE_TREE_DEPTH,
  716. NUM_GUARDIAN_SIGNERS,
  717. SOURCE_EMITTER_CHAIN_ID,
  718. SOURCE_EMITTER_ADDRESS,
  719. false
  720. )
  721. );
  722. uint updateFee = pyth.getTwapUpdateFee(updateData);
  723. vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
  724. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  725. }
  726. function testParseTwapPriceFeedUpdatesRevertsWithMismatchedExponents()
  727. public
  728. {
  729. bytes32[] memory priceIds = new bytes32[](1);
  730. priceIds[0] = bytes32(uint256(1));
  731. // Copy base messages
  732. TwapPriceFeedMessage[]
  733. memory startMessages = new TwapPriceFeedMessage[](1);
  734. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  735. 1
  736. );
  737. startMessages[0] = baseTwapStartMessages[0];
  738. endMessages[0] = baseTwapEndMessages[0];
  739. // Change end message expo to create mismatch
  740. endMessages[0].expo = -6;
  741. // Create update data
  742. bytes[] memory updateData = new bytes[](2);
  743. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  744. startMessages,
  745. MerkleUpdateConfig(
  746. MERKLE_TREE_DEPTH,
  747. NUM_GUARDIAN_SIGNERS,
  748. SOURCE_EMITTER_CHAIN_ID,
  749. SOURCE_EMITTER_ADDRESS,
  750. false
  751. )
  752. );
  753. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  754. endMessages,
  755. MerkleUpdateConfig(
  756. MERKLE_TREE_DEPTH,
  757. NUM_GUARDIAN_SIGNERS,
  758. SOURCE_EMITTER_CHAIN_ID,
  759. SOURCE_EMITTER_ADDRESS,
  760. false
  761. )
  762. );
  763. uint updateFee = pyth.getTwapUpdateFee(updateData);
  764. vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
  765. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  766. }
  767. function testParseTwapPriceFeedUpdatesRevertsWithInvalidPrevPublishTime()
  768. public
  769. {
  770. bytes32[] memory priceIds = new bytes32[](1);
  771. priceIds[0] = bytes32(uint256(1));
  772. // Copy base messages
  773. TwapPriceFeedMessage[]
  774. memory startMessages = new TwapPriceFeedMessage[](1);
  775. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  776. 1
  777. );
  778. startMessages[0] = baseTwapStartMessages[0];
  779. endMessages[0] = baseTwapEndMessages[0];
  780. // Set invalid prevPublishTime (greater than publishTime)
  781. startMessages[0].prevPublishTime = 1100;
  782. // Create update data
  783. bytes[] memory updateData = new bytes[](2);
  784. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  785. startMessages,
  786. MerkleUpdateConfig(
  787. MERKLE_TREE_DEPTH,
  788. NUM_GUARDIAN_SIGNERS,
  789. SOURCE_EMITTER_CHAIN_ID,
  790. SOURCE_EMITTER_ADDRESS,
  791. false
  792. )
  793. );
  794. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  795. endMessages,
  796. MerkleUpdateConfig(
  797. MERKLE_TREE_DEPTH,
  798. NUM_GUARDIAN_SIGNERS,
  799. SOURCE_EMITTER_CHAIN_ID,
  800. SOURCE_EMITTER_ADDRESS,
  801. false
  802. )
  803. );
  804. uint updateFee = pyth.getTwapUpdateFee(updateData);
  805. vm.expectRevert(PythErrors.InvalidTwapUpdateData.selector);
  806. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  807. }
  808. function testParseTwapPriceFeedUpdatesRevertsWithInsufficientFee() public {
  809. bytes32[] memory priceIds = new bytes32[](1);
  810. priceIds[0] = bytes32(uint256(1));
  811. // Copy base messages
  812. TwapPriceFeedMessage[]
  813. memory startMessages = new TwapPriceFeedMessage[](1);
  814. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  815. 1
  816. );
  817. startMessages[0] = baseTwapStartMessages[0];
  818. endMessages[0] = baseTwapEndMessages[0];
  819. // Create update data
  820. bytes[] memory updateData = new bytes[](2);
  821. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  822. startMessages,
  823. MerkleUpdateConfig(
  824. MERKLE_TREE_DEPTH,
  825. NUM_GUARDIAN_SIGNERS,
  826. SOURCE_EMITTER_CHAIN_ID,
  827. SOURCE_EMITTER_ADDRESS,
  828. false
  829. )
  830. );
  831. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  832. endMessages,
  833. MerkleUpdateConfig(
  834. MERKLE_TREE_DEPTH,
  835. NUM_GUARDIAN_SIGNERS,
  836. SOURCE_EMITTER_CHAIN_ID,
  837. SOURCE_EMITTER_ADDRESS,
  838. false
  839. )
  840. );
  841. uint updateFee = pyth.getTwapUpdateFee(updateData);
  842. vm.expectRevert(PythErrors.InsufficientFee.selector);
  843. pyth.parseTwapPriceFeedUpdates{value: updateFee - 1}(
  844. updateData,
  845. priceIds
  846. );
  847. }
  848. function testParseTwapPriceFeedUpdatesMultipleFeeds() public {
  849. bytes32[] memory priceIds = new bytes32[](2);
  850. priceIds[0] = basePriceIds[0];
  851. priceIds[1] = basePriceIds[1];
  852. // Create update data with both price feeds in the same updates
  853. bytes[] memory updateData = new bytes[](2); // Just 2 updates (start/end) for both price feeds
  854. // Combine both price feeds in the same messages
  855. TwapPriceFeedMessage[]
  856. memory startMessages = new TwapPriceFeedMessage[](2);
  857. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  858. 2
  859. );
  860. // Add both price feeds to the start and end messages
  861. startMessages[0] = baseTwapStartMessages[0];
  862. startMessages[1] = baseTwapStartMessages[1];
  863. endMessages[0] = baseTwapEndMessages[0];
  864. endMessages[1] = baseTwapEndMessages[1];
  865. // Generate Merkle updates with both price feeds included
  866. MerkleUpdateConfig memory config = MerkleUpdateConfig(
  867. MERKLE_TREE_DEPTH,
  868. NUM_GUARDIAN_SIGNERS,
  869. SOURCE_EMITTER_CHAIN_ID,
  870. SOURCE_EMITTER_ADDRESS,
  871. false
  872. );
  873. // Create just 2 updates that contain both price feeds
  874. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  875. startMessages,
  876. config
  877. );
  878. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  879. endMessages,
  880. config
  881. );
  882. uint updateFee = pyth.getTwapUpdateFee(updateData);
  883. // Parse the TWAP updates
  884. PythStructs.TwapPriceFeed[] memory twapPriceFeeds = pyth
  885. .parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  886. // Validate results for first price feed
  887. assertEq(twapPriceFeeds[0].id, basePriceIds[0]);
  888. assertEq(
  889. twapPriceFeeds[0].startTime,
  890. baseTwapStartMessages[0].publishTime
  891. );
  892. assertEq(twapPriceFeeds[0].endTime, baseTwapEndMessages[0].publishTime);
  893. assertEq(twapPriceFeeds[0].twap.expo, baseTwapStartMessages[0].expo);
  894. // Expected TWAP price: (210_000 - 100_000) / (1100 - 1000) = 1100
  895. assertEq(twapPriceFeeds[0].twap.price, int64(1100));
  896. // Expected TWAP conf: (18_000 - 10_000) / (1100 - 1000) = 80
  897. assertEq(twapPriceFeeds[0].twap.conf, uint64(80));
  898. assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));
  899. // Validate results for second price feed
  900. assertEq(twapPriceFeeds[1].id, basePriceIds[1]);
  901. assertEq(
  902. twapPriceFeeds[1].startTime,
  903. baseTwapStartMessages[1].publishTime
  904. );
  905. assertEq(twapPriceFeeds[1].endTime, baseTwapEndMessages[1].publishTime);
  906. assertEq(twapPriceFeeds[1].twap.expo, baseTwapStartMessages[1].expo);
  907. // Expected TWAP price: (800_000 - 500_000) / (1100 - 1000) = 3000
  908. assertEq(twapPriceFeeds[1].twap.price, int64(3000));
  909. // Expected TWAP conf: (40_000 - 20_000) / (1100 - 1000) = 200
  910. assertEq(twapPriceFeeds[1].twap.conf, uint64(200));
  911. assertEq(twapPriceFeeds[1].downSlotsRatio, uint32(0));
  912. }
  913. function testParseTwapPriceFeedUpdatesRevertsWithMismatchedArrayLengths()
  914. public
  915. {
  916. // Case 1: Too many updates (more than 2)
  917. bytes32[] memory priceIds = new bytes32[](1);
  918. priceIds[0] = basePriceIds[0];
  919. // Create 3 updates (should only be 2)
  920. bytes[] memory updateData = new bytes[](3);
  921. TwapPriceFeedMessage[]
  922. memory startMessages = new TwapPriceFeedMessage[](1);
  923. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  924. 1
  925. );
  926. startMessages[0] = baseTwapStartMessages[0];
  927. endMessages[0] = baseTwapEndMessages[0];
  928. MerkleUpdateConfig memory config = MerkleUpdateConfig(
  929. MERKLE_TREE_DEPTH,
  930. NUM_GUARDIAN_SIGNERS,
  931. SOURCE_EMITTER_CHAIN_ID,
  932. SOURCE_EMITTER_ADDRESS,
  933. false
  934. );
  935. // Fill with valid updates, but too many of them
  936. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  937. startMessages,
  938. config
  939. );
  940. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  941. endMessages,
  942. config
  943. );
  944. updateData[2] = generateWhMerkleTwapUpdateWithSource(
  945. startMessages,
  946. config
  947. );
  948. uint updateFee = pyth.getTwapUpdateFee(updateData);
  949. vm.expectRevert(PythErrors.InvalidUpdateData.selector);
  950. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  951. // Case 2: Too few updates (less than 2)
  952. updateData = new bytes[](1); // Only 1 update (should be 2)
  953. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  954. startMessages,
  955. config
  956. );
  957. updateFee = pyth.getUpdateFee(updateData);
  958. vm.expectRevert(PythErrors.InvalidUpdateData.selector);
  959. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  960. }
  961. function testParseTwapPriceFeedUpdatesWithRequestedButNotFoundPriceId()
  962. public
  963. {
  964. // Create price IDs, including one that's not in the updates
  965. bytes32[] memory priceIds = new bytes32[](2);
  966. priceIds[0] = basePriceIds[0]; // This one exists in our updates
  967. priceIds[1] = bytes32(uint256(999)); // This one doesn't exist in our updates
  968. TwapPriceFeedMessage[]
  969. memory startMessages = new TwapPriceFeedMessage[](1);
  970. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  971. 1
  972. );
  973. startMessages[0] = baseTwapStartMessages[0]; // Only includes priceIds[0]
  974. endMessages[0] = baseTwapEndMessages[0]; // Only includes priceIds[0]
  975. MerkleUpdateConfig memory config = MerkleUpdateConfig(
  976. MERKLE_TREE_DEPTH,
  977. NUM_GUARDIAN_SIGNERS,
  978. SOURCE_EMITTER_CHAIN_ID,
  979. SOURCE_EMITTER_ADDRESS,
  980. false
  981. );
  982. bytes[] memory updateData = new bytes[](2);
  983. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  984. startMessages,
  985. config
  986. );
  987. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  988. endMessages,
  989. config
  990. );
  991. uint updateFee = pyth.getTwapUpdateFee(updateData);
  992. // Should revert because one of the requested price IDs is not found in the updates
  993. vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
  994. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  995. }
  996. }