Pyth.t.sol 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  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 testParsePriceFeedUpdatesWithSlotsStrictWorks(uint seed) public {
  245. setRandSeed(seed);
  246. uint numMessages = 1 + (getRandUint() % 10);
  247. (
  248. bytes32[] memory priceIds,
  249. PriceFeedMessage[] memory messages
  250. ) = generateRandomPriceMessages(numMessages);
  251. (
  252. bytes[] memory updateData,
  253. uint updateFee
  254. ) = createBatchedUpdateDataFromMessages(messages);
  255. (
  256. PythStructs.PriceFeed[] memory priceFeeds,
  257. uint64[] memory slots
  258. ) = pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
  259. updateData,
  260. priceIds,
  261. 0,
  262. MAX_UINT64
  263. );
  264. assertEq(priceFeeds.length, numMessages);
  265. assertEq(slots.length, numMessages);
  266. for (uint i = 0; i < numMessages; i++) {
  267. assertEq(priceFeeds[i].id, priceIds[i]);
  268. assertEq(priceFeeds[i].price.price, messages[i].price);
  269. assertEq(priceFeeds[i].price.conf, messages[i].conf);
  270. assertEq(priceFeeds[i].price.expo, messages[i].expo);
  271. assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime);
  272. assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice);
  273. assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf);
  274. assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo);
  275. assertEq(
  276. priceFeeds[i].emaPrice.publishTime,
  277. messages[i].publishTime
  278. );
  279. // Check that the slot returned is 1, as set in generateWhMerkleUpdateWithSource
  280. assertEq(slots[i], 1);
  281. }
  282. }
  283. function testParsePriceFeedUpdatesWorksWithOverlappingWithinTimeRangeUpdates()
  284. public
  285. {
  286. PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
  287. messages[0].priceId = bytes32(uint(1));
  288. messages[0].price = 1000;
  289. messages[0].publishTime = 10;
  290. messages[1].priceId = bytes32(uint(1));
  291. messages[1].price = 2000;
  292. messages[1].publishTime = 20;
  293. (
  294. bytes[] memory updateData,
  295. uint updateFee
  296. ) = createBatchedUpdateDataFromMessages(messages);
  297. bytes32[] memory priceIds = new bytes32[](1);
  298. priceIds[0] = bytes32(uint(1));
  299. PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
  300. value: updateFee
  301. }(updateData, priceIds, 0, 20);
  302. assertEq(priceFeeds.length, 1);
  303. assertEq(priceFeeds[0].id, bytes32(uint(1)));
  304. assertTrue(
  305. (priceFeeds[0].price.price == 1000 &&
  306. priceFeeds[0].price.publishTime == 10) ||
  307. (priceFeeds[0].price.price == 2000 &&
  308. priceFeeds[0].price.publishTime == 20)
  309. );
  310. }
  311. function testParsePriceFeedUpdatesWorksWithOverlappingMixedTimeRangeUpdates()
  312. public
  313. {
  314. PriceFeedMessage[] memory messages = new PriceFeedMessage[](2);
  315. messages[0].priceId = bytes32(uint(1));
  316. messages[0].price = 1000;
  317. messages[0].publishTime = 10;
  318. messages[1].priceId = bytes32(uint(1));
  319. messages[1].price = 2000;
  320. messages[1].publishTime = 20;
  321. (
  322. bytes[] memory updateData,
  323. uint updateFee
  324. ) = createBatchedUpdateDataFromMessages(messages);
  325. bytes32[] memory priceIds = new bytes32[](1);
  326. priceIds[0] = bytes32(uint(1));
  327. PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
  328. value: updateFee
  329. }(updateData, priceIds, 5, 15);
  330. assertEq(priceFeeds.length, 1);
  331. assertEq(priceFeeds[0].id, bytes32(uint(1)));
  332. assertEq(priceFeeds[0].price.price, 1000);
  333. assertEq(priceFeeds[0].price.publishTime, 10);
  334. priceFeeds = pyth.parsePriceFeedUpdates{value: updateFee}(
  335. updateData,
  336. priceIds,
  337. 15,
  338. 25
  339. );
  340. assertEq(priceFeeds.length, 1);
  341. assertEq(priceFeeds[0].id, bytes32(uint(1)));
  342. assertEq(priceFeeds[0].price.price, 2000);
  343. assertEq(priceFeeds[0].price.publishTime, 20);
  344. }
  345. function testParsePriceFeedUpdatesRevertsIfUpdateVAAIsInvalid(
  346. uint seed
  347. ) public {
  348. setRandSeed(seed);
  349. uint numMessages = 1 + (getRandUint() % 10);
  350. (
  351. bytes32[] memory priceIds,
  352. PriceFeedMessage[] memory messages
  353. ) = generateRandomPriceMessages(numMessages);
  354. (
  355. bytes[] memory updateData,
  356. uint updateFee
  357. ) = createBatchedUpdateDataFromMessagesWithConfig(
  358. messages,
  359. MerkleUpdateConfig(
  360. MERKLE_TREE_DEPTH,
  361. NUM_GUARDIAN_SIGNERS,
  362. SOURCE_EMITTER_CHAIN_ID,
  363. SOURCE_EMITTER_ADDRESS,
  364. true
  365. )
  366. );
  367. // It might revert due to different wormhole errors
  368. vm.expectRevert();
  369. pyth.parsePriceFeedUpdates{value: updateFee}(
  370. updateData,
  371. priceIds,
  372. 0,
  373. MAX_UINT64
  374. );
  375. }
  376. function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithExcessUpdateData()
  377. public
  378. {
  379. // Create a price update with more price updates than requested price IDs
  380. uint numPriceIds = 2;
  381. uint numMessages = numPriceIds + 1; // One more than the number of price IDs
  382. (
  383. bytes32[] memory priceIds,
  384. PriceFeedMessage[] memory messages
  385. ) = generateRandomPriceMessages(numMessages);
  386. // Only use a subset of the price IDs to trigger the strict check
  387. bytes32[] memory requestedPriceIds = new bytes32[](numPriceIds);
  388. for (uint i = 0; i < numPriceIds; i++) {
  389. requestedPriceIds[i] = priceIds[i];
  390. }
  391. (
  392. bytes[] memory updateData,
  393. uint updateFee
  394. ) = createBatchedUpdateDataFromMessages(messages);
  395. // Should revert in strict mode
  396. vm.expectRevert(PythErrors.InvalidArgument.selector);
  397. pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
  398. updateData,
  399. requestedPriceIds,
  400. 0,
  401. MAX_UINT64
  402. );
  403. }
  404. function testParsePriceFeedUpdatesWithSlotsStrictRevertsWithFewerUpdateData()
  405. public
  406. {
  407. // Create a price update with fewer price updates than requested price IDs
  408. uint numPriceIds = 3;
  409. uint numMessages = numPriceIds - 1; // One less than the number of price IDs
  410. (
  411. bytes32[] memory priceIds,
  412. PriceFeedMessage[] memory messages
  413. ) = generateRandomPriceMessages(numMessages);
  414. // Create a larger array of requested price IDs to trigger the strict check
  415. bytes32[] memory requestedPriceIds = new bytes32[](numPriceIds);
  416. for (uint i = 0; i < numMessages; i++) {
  417. requestedPriceIds[i] = priceIds[i];
  418. }
  419. // Add an extra price ID that won't be in the updates
  420. requestedPriceIds[numMessages] = bytes32(
  421. uint256(keccak256(abi.encodePacked("extra_id")))
  422. );
  423. (
  424. bytes[] memory updateData,
  425. uint updateFee
  426. ) = createBatchedUpdateDataFromMessages(messages);
  427. // Should revert in strict mode because we have fewer updates than price IDs
  428. vm.expectRevert(PythErrors.InvalidArgument.selector);
  429. pyth.parsePriceFeedUpdatesWithSlotsStrict{value: updateFee}(
  430. updateData,
  431. requestedPriceIds,
  432. 0,
  433. MAX_UINT64
  434. );
  435. }
  436. function testParsePriceFeedUpdatesRevertsIfUpdateSourceChainIsInvalid()
  437. public
  438. {
  439. uint numMessages = 10;
  440. (
  441. bytes32[] memory priceIds,
  442. PriceFeedMessage[] memory messages
  443. ) = generateRandomPriceMessages(numMessages);
  444. (
  445. bytes[] memory updateData,
  446. uint updateFee
  447. ) = createBatchedUpdateDataFromMessagesWithConfig(
  448. messages,
  449. MerkleUpdateConfig(
  450. MERKLE_TREE_DEPTH,
  451. NUM_GUARDIAN_SIGNERS,
  452. SOURCE_EMITTER_CHAIN_ID + 1,
  453. SOURCE_EMITTER_ADDRESS,
  454. false
  455. )
  456. );
  457. vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
  458. pyth.parsePriceFeedUpdates{value: updateFee}(
  459. updateData,
  460. priceIds,
  461. 0,
  462. MAX_UINT64
  463. );
  464. }
  465. function testParsePriceFeedUpdatesRevertsIfUpdateSourceAddressIsInvalid()
  466. public
  467. {
  468. uint numMessages = 10;
  469. (
  470. bytes32[] memory priceIds,
  471. PriceFeedMessage[] memory messages
  472. ) = generateRandomPriceMessages(numMessages);
  473. (
  474. bytes[] memory updateData,
  475. uint updateFee
  476. ) = createBatchedUpdateDataFromMessagesWithConfig(
  477. messages,
  478. MerkleUpdateConfig(
  479. MERKLE_TREE_DEPTH,
  480. NUM_GUARDIAN_SIGNERS,
  481. SOURCE_EMITTER_CHAIN_ID,
  482. 0x00000000000000000000000000000000000000000000000000000000000000aa, // Random wrong source address
  483. false
  484. )
  485. );
  486. vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
  487. pyth.parsePriceFeedUpdates{value: updateFee}(
  488. updateData,
  489. priceIds,
  490. 0,
  491. MAX_UINT64
  492. );
  493. }
  494. function testParseTwapPriceFeedUpdates() public {
  495. bytes32[] memory priceIds = new bytes32[](1);
  496. priceIds[0] = basePriceIds[0];
  497. // Create update data directly from base TWAP messages
  498. bytes[] memory updateData = new bytes[](2);
  499. TwapPriceFeedMessage[]
  500. memory startMessages = new TwapPriceFeedMessage[](1);
  501. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  502. 1
  503. );
  504. startMessages[0] = baseTwapStartMessages[0];
  505. endMessages[0] = baseTwapEndMessages[0];
  506. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  507. startMessages,
  508. MerkleUpdateConfig(
  509. MERKLE_TREE_DEPTH,
  510. NUM_GUARDIAN_SIGNERS,
  511. SOURCE_EMITTER_CHAIN_ID,
  512. SOURCE_EMITTER_ADDRESS,
  513. false
  514. )
  515. );
  516. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  517. endMessages,
  518. MerkleUpdateConfig(
  519. MERKLE_TREE_DEPTH,
  520. NUM_GUARDIAN_SIGNERS,
  521. SOURCE_EMITTER_CHAIN_ID,
  522. SOURCE_EMITTER_ADDRESS,
  523. false
  524. )
  525. );
  526. uint updateFee = pyth.getTwapUpdateFee(updateData);
  527. // Parse the TWAP updates
  528. PythStructs.TwapPriceFeed[] memory twapPriceFeeds = pyth
  529. .parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  530. // Validate results
  531. assertEq(twapPriceFeeds[0].id, basePriceIds[0]);
  532. assertEq(
  533. twapPriceFeeds[0].startTime,
  534. baseTwapStartMessages[0].publishTime
  535. );
  536. assertEq(twapPriceFeeds[0].endTime, baseTwapEndMessages[0].publishTime);
  537. assertEq(twapPriceFeeds[0].twap.expo, baseTwapStartMessages[0].expo);
  538. // Expected TWAP price: (210_000 - 100_000) / (1100 - 1000) = 1100
  539. assertEq(twapPriceFeeds[0].twap.price, int64(1100));
  540. // Expected TWAP conf: (18_000 - 10_000) / (1100 - 1000) = 80
  541. assertEq(twapPriceFeeds[0].twap.conf, uint64(80));
  542. // Validate the downSlotsRatio is 0 in our test implementation
  543. assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));
  544. }
  545. function testParseTwapPriceFeedUpdatesRevertsWithInvalidUpdateDataLength()
  546. public
  547. {
  548. bytes32[] memory priceIds = new bytes32[](1);
  549. priceIds[0] = bytes32(uint256(1));
  550. // Create invalid update data with wrong length
  551. bytes[] memory updateData = new bytes[](1); // Should be 2
  552. updateData[0] = new bytes(1);
  553. vm.expectRevert(PythErrors.InvalidUpdateData.selector);
  554. pyth.parseTwapPriceFeedUpdates{value: 0}(updateData, priceIds);
  555. }
  556. function testParseTwapPriceFeedUpdatesRevertsWithMismatchedPriceIds()
  557. public
  558. {
  559. bytes32[] memory priceIds = new bytes32[](1);
  560. priceIds[0] = bytes32(uint256(1));
  561. // Copy base messages
  562. TwapPriceFeedMessage[]
  563. memory startMessages = new TwapPriceFeedMessage[](1);
  564. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  565. 1
  566. );
  567. startMessages[0] = baseTwapStartMessages[0];
  568. endMessages[0] = baseTwapEndMessages[0];
  569. // Change end message priceId to create mismatch
  570. endMessages[0].priceId = bytes32(uint256(2));
  571. // Create update data
  572. bytes[] memory updateData = new bytes[](2);
  573. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  574. startMessages,
  575. MerkleUpdateConfig(
  576. MERKLE_TREE_DEPTH,
  577. NUM_GUARDIAN_SIGNERS,
  578. SOURCE_EMITTER_CHAIN_ID,
  579. SOURCE_EMITTER_ADDRESS,
  580. false
  581. )
  582. );
  583. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  584. endMessages,
  585. MerkleUpdateConfig(
  586. MERKLE_TREE_DEPTH,
  587. NUM_GUARDIAN_SIGNERS,
  588. SOURCE_EMITTER_CHAIN_ID,
  589. SOURCE_EMITTER_ADDRESS,
  590. false
  591. )
  592. );
  593. uint updateFee = pyth.getTwapUpdateFee(updateData);
  594. vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
  595. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  596. }
  597. function testParseTwapPriceFeedUpdatesRevertsWithInvalidTimeOrdering()
  598. public
  599. {
  600. bytes32[] memory priceIds = new bytes32[](1);
  601. priceIds[0] = bytes32(uint256(1));
  602. // Copy base messages
  603. TwapPriceFeedMessage[]
  604. memory startMessages = new TwapPriceFeedMessage[](1);
  605. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  606. 1
  607. );
  608. startMessages[0] = baseTwapStartMessages[0];
  609. endMessages[0] = baseTwapEndMessages[0];
  610. // Modify times to create invalid ordering
  611. startMessages[0].publishTime = 1100;
  612. startMessages[0].publishSlot = 1100;
  613. endMessages[0].publishTime = 1000;
  614. endMessages[0].publishSlot = 1000;
  615. endMessages[0].prevPublishTime = 900;
  616. // Create update data
  617. bytes[] memory updateData = new bytes[](2);
  618. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  619. startMessages,
  620. MerkleUpdateConfig(
  621. MERKLE_TREE_DEPTH,
  622. NUM_GUARDIAN_SIGNERS,
  623. SOURCE_EMITTER_CHAIN_ID,
  624. SOURCE_EMITTER_ADDRESS,
  625. false
  626. )
  627. );
  628. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  629. endMessages,
  630. MerkleUpdateConfig(
  631. MERKLE_TREE_DEPTH,
  632. NUM_GUARDIAN_SIGNERS,
  633. SOURCE_EMITTER_CHAIN_ID,
  634. SOURCE_EMITTER_ADDRESS,
  635. false
  636. )
  637. );
  638. uint updateFee = pyth.getTwapUpdateFee(updateData);
  639. vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
  640. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  641. }
  642. function testParseTwapPriceFeedUpdatesRevertsWithMismatchedExponents()
  643. public
  644. {
  645. bytes32[] memory priceIds = new bytes32[](1);
  646. priceIds[0] = bytes32(uint256(1));
  647. // Copy base messages
  648. TwapPriceFeedMessage[]
  649. memory startMessages = new TwapPriceFeedMessage[](1);
  650. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  651. 1
  652. );
  653. startMessages[0] = baseTwapStartMessages[0];
  654. endMessages[0] = baseTwapEndMessages[0];
  655. // Change end message expo to create mismatch
  656. endMessages[0].expo = -6;
  657. // Create update data
  658. bytes[] memory updateData = new bytes[](2);
  659. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  660. startMessages,
  661. MerkleUpdateConfig(
  662. MERKLE_TREE_DEPTH,
  663. NUM_GUARDIAN_SIGNERS,
  664. SOURCE_EMITTER_CHAIN_ID,
  665. SOURCE_EMITTER_ADDRESS,
  666. false
  667. )
  668. );
  669. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  670. endMessages,
  671. MerkleUpdateConfig(
  672. MERKLE_TREE_DEPTH,
  673. NUM_GUARDIAN_SIGNERS,
  674. SOURCE_EMITTER_CHAIN_ID,
  675. SOURCE_EMITTER_ADDRESS,
  676. false
  677. )
  678. );
  679. uint updateFee = pyth.getTwapUpdateFee(updateData);
  680. vm.expectRevert(PythErrors.InvalidTwapUpdateDataSet.selector);
  681. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  682. }
  683. function testParseTwapPriceFeedUpdatesRevertsWithInvalidPrevPublishTime()
  684. public
  685. {
  686. bytes32[] memory priceIds = new bytes32[](1);
  687. priceIds[0] = bytes32(uint256(1));
  688. // Copy base messages
  689. TwapPriceFeedMessage[]
  690. memory startMessages = new TwapPriceFeedMessage[](1);
  691. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  692. 1
  693. );
  694. startMessages[0] = baseTwapStartMessages[0];
  695. endMessages[0] = baseTwapEndMessages[0];
  696. // Set invalid prevPublishTime (greater than publishTime)
  697. startMessages[0].prevPublishTime = 1100;
  698. // Create update data
  699. bytes[] memory updateData = new bytes[](2);
  700. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  701. startMessages,
  702. MerkleUpdateConfig(
  703. MERKLE_TREE_DEPTH,
  704. NUM_GUARDIAN_SIGNERS,
  705. SOURCE_EMITTER_CHAIN_ID,
  706. SOURCE_EMITTER_ADDRESS,
  707. false
  708. )
  709. );
  710. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  711. endMessages,
  712. MerkleUpdateConfig(
  713. MERKLE_TREE_DEPTH,
  714. NUM_GUARDIAN_SIGNERS,
  715. SOURCE_EMITTER_CHAIN_ID,
  716. SOURCE_EMITTER_ADDRESS,
  717. false
  718. )
  719. );
  720. uint updateFee = pyth.getTwapUpdateFee(updateData);
  721. vm.expectRevert(PythErrors.InvalidTwapUpdateData.selector);
  722. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  723. }
  724. function testParseTwapPriceFeedUpdatesRevertsWithInsufficientFee() public {
  725. bytes32[] memory priceIds = new bytes32[](1);
  726. priceIds[0] = bytes32(uint256(1));
  727. // Copy base messages
  728. TwapPriceFeedMessage[]
  729. memory startMessages = new TwapPriceFeedMessage[](1);
  730. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  731. 1
  732. );
  733. startMessages[0] = baseTwapStartMessages[0];
  734. endMessages[0] = baseTwapEndMessages[0];
  735. // Create update data
  736. bytes[] memory updateData = new bytes[](2);
  737. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  738. startMessages,
  739. MerkleUpdateConfig(
  740. MERKLE_TREE_DEPTH,
  741. NUM_GUARDIAN_SIGNERS,
  742. SOURCE_EMITTER_CHAIN_ID,
  743. SOURCE_EMITTER_ADDRESS,
  744. false
  745. )
  746. );
  747. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  748. endMessages,
  749. MerkleUpdateConfig(
  750. MERKLE_TREE_DEPTH,
  751. NUM_GUARDIAN_SIGNERS,
  752. SOURCE_EMITTER_CHAIN_ID,
  753. SOURCE_EMITTER_ADDRESS,
  754. false
  755. )
  756. );
  757. uint updateFee = pyth.getTwapUpdateFee(updateData);
  758. vm.expectRevert(PythErrors.InsufficientFee.selector);
  759. pyth.parseTwapPriceFeedUpdates{value: updateFee - 1}(
  760. updateData,
  761. priceIds
  762. );
  763. }
  764. function testParseTwapPriceFeedUpdatesMultipleFeeds() public {
  765. bytes32[] memory priceIds = new bytes32[](2);
  766. priceIds[0] = basePriceIds[0];
  767. priceIds[1] = basePriceIds[1];
  768. // Create update data with both price feeds in the same updates
  769. bytes[] memory updateData = new bytes[](2); // Just 2 updates (start/end) for both price feeds
  770. // Combine both price feeds in the same messages
  771. TwapPriceFeedMessage[]
  772. memory startMessages = new TwapPriceFeedMessage[](2);
  773. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  774. 2
  775. );
  776. // Add both price feeds to the start and end messages
  777. startMessages[0] = baseTwapStartMessages[0];
  778. startMessages[1] = baseTwapStartMessages[1];
  779. endMessages[0] = baseTwapEndMessages[0];
  780. endMessages[1] = baseTwapEndMessages[1];
  781. // Generate Merkle updates with both price feeds included
  782. MerkleUpdateConfig memory config = MerkleUpdateConfig(
  783. MERKLE_TREE_DEPTH,
  784. NUM_GUARDIAN_SIGNERS,
  785. SOURCE_EMITTER_CHAIN_ID,
  786. SOURCE_EMITTER_ADDRESS,
  787. false
  788. );
  789. // Create just 2 updates that contain both price feeds
  790. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  791. startMessages,
  792. config
  793. );
  794. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  795. endMessages,
  796. config
  797. );
  798. uint updateFee = pyth.getTwapUpdateFee(updateData);
  799. // Parse the TWAP updates
  800. PythStructs.TwapPriceFeed[] memory twapPriceFeeds = pyth
  801. .parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  802. // Validate results for first price feed
  803. assertEq(twapPriceFeeds[0].id, basePriceIds[0]);
  804. assertEq(
  805. twapPriceFeeds[0].startTime,
  806. baseTwapStartMessages[0].publishTime
  807. );
  808. assertEq(twapPriceFeeds[0].endTime, baseTwapEndMessages[0].publishTime);
  809. assertEq(twapPriceFeeds[0].twap.expo, baseTwapStartMessages[0].expo);
  810. // Expected TWAP price: (210_000 - 100_000) / (1100 - 1000) = 1100
  811. assertEq(twapPriceFeeds[0].twap.price, int64(1100));
  812. // Expected TWAP conf: (18_000 - 10_000) / (1100 - 1000) = 80
  813. assertEq(twapPriceFeeds[0].twap.conf, uint64(80));
  814. assertEq(twapPriceFeeds[0].downSlotsRatio, uint32(0));
  815. // Validate results for second price feed
  816. assertEq(twapPriceFeeds[1].id, basePriceIds[1]);
  817. assertEq(
  818. twapPriceFeeds[1].startTime,
  819. baseTwapStartMessages[1].publishTime
  820. );
  821. assertEq(twapPriceFeeds[1].endTime, baseTwapEndMessages[1].publishTime);
  822. assertEq(twapPriceFeeds[1].twap.expo, baseTwapStartMessages[1].expo);
  823. // Expected TWAP price: (800_000 - 500_000) / (1100 - 1000) = 3000
  824. assertEq(twapPriceFeeds[1].twap.price, int64(3000));
  825. // Expected TWAP conf: (40_000 - 20_000) / (1100 - 1000) = 200
  826. assertEq(twapPriceFeeds[1].twap.conf, uint64(200));
  827. assertEq(twapPriceFeeds[1].downSlotsRatio, uint32(0));
  828. }
  829. function testParseTwapPriceFeedUpdatesRevertsWithMismatchedArrayLengths()
  830. public
  831. {
  832. // Case 1: Too many updates (more than 2)
  833. bytes32[] memory priceIds = new bytes32[](1);
  834. priceIds[0] = basePriceIds[0];
  835. // Create 3 updates (should only be 2)
  836. bytes[] memory updateData = new bytes[](3);
  837. TwapPriceFeedMessage[]
  838. memory startMessages = new TwapPriceFeedMessage[](1);
  839. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  840. 1
  841. );
  842. startMessages[0] = baseTwapStartMessages[0];
  843. endMessages[0] = baseTwapEndMessages[0];
  844. MerkleUpdateConfig memory config = MerkleUpdateConfig(
  845. MERKLE_TREE_DEPTH,
  846. NUM_GUARDIAN_SIGNERS,
  847. SOURCE_EMITTER_CHAIN_ID,
  848. SOURCE_EMITTER_ADDRESS,
  849. false
  850. );
  851. // Fill with valid updates, but too many of them
  852. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  853. startMessages,
  854. config
  855. );
  856. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  857. endMessages,
  858. config
  859. );
  860. updateData[2] = generateWhMerkleTwapUpdateWithSource(
  861. startMessages,
  862. config
  863. );
  864. uint updateFee = pyth.getTwapUpdateFee(updateData);
  865. vm.expectRevert(PythErrors.InvalidUpdateData.selector);
  866. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  867. // Case 2: Too few updates (less than 2)
  868. updateData = new bytes[](1); // Only 1 update (should be 2)
  869. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  870. startMessages,
  871. config
  872. );
  873. updateFee = pyth.getUpdateFee(updateData);
  874. vm.expectRevert(PythErrors.InvalidUpdateData.selector);
  875. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  876. }
  877. function testParseTwapPriceFeedUpdatesWithRequestedButNotFoundPriceId()
  878. public
  879. {
  880. // Create price IDs, including one that's not in the updates
  881. bytes32[] memory priceIds = new bytes32[](2);
  882. priceIds[0] = basePriceIds[0]; // This one exists in our updates
  883. priceIds[1] = bytes32(uint256(999)); // This one doesn't exist in our updates
  884. TwapPriceFeedMessage[]
  885. memory startMessages = new TwapPriceFeedMessage[](1);
  886. TwapPriceFeedMessage[] memory endMessages = new TwapPriceFeedMessage[](
  887. 1
  888. );
  889. startMessages[0] = baseTwapStartMessages[0]; // Only includes priceIds[0]
  890. endMessages[0] = baseTwapEndMessages[0]; // Only includes priceIds[0]
  891. MerkleUpdateConfig memory config = MerkleUpdateConfig(
  892. MERKLE_TREE_DEPTH,
  893. NUM_GUARDIAN_SIGNERS,
  894. SOURCE_EMITTER_CHAIN_ID,
  895. SOURCE_EMITTER_ADDRESS,
  896. false
  897. );
  898. bytes[] memory updateData = new bytes[](2);
  899. updateData[0] = generateWhMerkleTwapUpdateWithSource(
  900. startMessages,
  901. config
  902. );
  903. updateData[1] = generateWhMerkleTwapUpdateWithSource(
  904. endMessages,
  905. config
  906. );
  907. uint updateFee = pyth.getTwapUpdateFee(updateData);
  908. // Should revert because one of the requested price IDs is not found in the updates
  909. vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
  910. pyth.parseTwapPriceFeedUpdates{value: updateFee}(updateData, priceIds);
  911. }
  912. }