@@ -309,9 +309,8 @@ abstract contract Pyth is
309309    }
310310
311311    function parseTwapPriceFeedUpdates  (
312-         bytes [2 ][] calldata  updateData ,
313-         bytes32 [] calldata  priceIds ,
314-         uint8  windowSize 
312+         bytes [][] calldata  updateData ,
313+         bytes32 [] calldata  priceIds 
315314    )
316315        external 
317316        payable 
@@ -324,6 +323,7 @@ abstract contract Pyth is
324323            uint  requiredFee =  getUpdateFee (updateData[0 ]);
325324
326325            // Check if the two updateData contains the same number of priceUpdates 
326+             // by comparing fees since getUpdateFee parses the update data to count price feeds 
327327            if  (requiredFee !=  getUpdateFee (updateData[1 ])) {
328328                revert  PythErrors.InvalidUpdateData ();
329329            }
@@ -332,118 +332,139 @@ abstract contract Pyth is
332332
333333        // Parse the updateData 
334334        unchecked  {
335-         twapPriceFeeds =  new  PythStructs.TwapPriceFeed [](priceIds.length );
336-         for  (uint  i =  0 ; i <  updateData[0 ].length ; i++ ) {
337-             if  (
338-                 (updateData[0 ][i].length  >  4  && 
339-                     UnsafeCalldataBytesLib.toUint32 (updateData[0 ][i], 0 ) == 
340-                     ACCUMULATOR_MAGIC) && 
341-                 (updateData[1 ][i].length  >  4  && 
342-                     UnsafeCalldataBytesLib.toUint32 (updateData[1 ][i], 0 ) == 
343-                     ACCUMULATOR_MAGIC)
344-             ) {
345-                 // Parse the accumulator update 
346-                 // I believe the offset will be same for both updateData[0][i] and updateData[1][i] 
347-                 uint  offsetFirst;
348-                 uint  offsetSecond;
349-                 {
350-                     UpdateType updateType;
351-                     (offsetFirst, updateType) =  extractUpdateTypeFromAccumulatorHeader (updateData[0 ][i]);
352-                     if  (updateType !=  UpdateType.WormholeMerkle) {
353-                         revert  PythErrors.InvalidUpdateData ();
354-                     }
355-                     (offsetSecond, updateType) =  extractUpdateTypeFromAccumulatorHeader (updateData[1 ][i]);
356-                     if  (updateType !=  UpdateType.WormholeMerkle) {
357-                         revert  PythErrors.InvalidUpdateData ();
335+             twapPriceFeeds =  new  PythStructs.TwapPriceFeed [](priceIds.length );
336+             for  (uint  i =  0 ; i <  updateData[0 ].length ; i++ ) {
337+                 if  (
338+                     (updateData[0 ][i].length  >  4  && 
339+                         UnsafeCalldataBytesLib.toUint32 (updateData[0 ][i], 0 ) == 
340+                         ACCUMULATOR_MAGIC) && 
341+                     (updateData[1 ][i].length  >  4  && 
342+                         UnsafeCalldataBytesLib.toUint32 (updateData[1 ][i], 0 ) == 
343+                         ACCUMULATOR_MAGIC)
344+                 ) {
345+                     uint  offsetStart;
346+                     uint  offsetEnd;
347+                     {
348+                         UpdateType updateType;
349+                         (
350+                             offsetStart,
351+                             updateType
352+                         ) =  extractUpdateTypeFromAccumulatorHeader (
353+                             updateData[0 ][i]
354+                         );
355+                         if  (updateType !=  UpdateType.WormholeMerkle) {
356+                             revert  PythErrors.InvalidUpdateData ();
357+                         }
358+                         (
359+                             offsetEnd,
360+                             updateType
361+                         ) =  extractUpdateTypeFromAccumulatorHeader (
362+                             updateData[1 ][i]
363+                         );
364+                         if  (updateType !=  UpdateType.WormholeMerkle) {
365+                             revert  PythErrors.InvalidUpdateData ();
366+                         }
358367                    }
359-                 }
360368
361-                 bytes20  digestFirst;
362-                 bytes20  digestSecond;
363-                 uint8  numUpdatesFirst;
364-                 uint8  numUpdatesSecond;
365-                 bytes  calldata  encodedFirst;
366-                 bytes  calldata  encodedSecond;
367-                 (
368-                     offsetFirst,
369-                     digestFirst,
370-                     numUpdatesFirst,
371-                     encodedFirst
372-                 ) =  extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate (
373-                     updateData[0 ][i],
374-                     offsetFirst
375-                 );
376-                 (
377-                     offsetSecond,
378-                     digestSecond,
379-                     numUpdatesSecond,
380-                     encodedSecond
381-                 ) =  extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate (
382-                     updateData[1 ][i],
383-                     offsetSecond);
384-                 // I believe this check is redundant 
385-                 if  (numUpdatesFirst !=  numUpdatesSecond) {
386-                     revert  PythErrors.InvalidUpdateData ();
387-                 }
369+                     bytes20  digestStart;
370+                     bytes20  digestEnd;
371+                     uint8  numUpdatesStart;
372+                     uint8  numUpdatesEnd;
373+                     bytes  calldata  encodedStart;
374+                     bytes  calldata  encodedEnd;
375+                     (
376+                         offsetStart,
377+                         digestStart,
378+                         numUpdatesStart,
379+                         encodedStart
380+                     ) =  extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate (
381+                         updateData[0 ][i],
382+                         offsetStart
383+                     );
384+                     (
385+                         offsetEnd,
386+                         digestEnd,
387+                         numUpdatesEnd,
388+                         encodedEnd
389+                     ) =  extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate (
390+                         updateData[1 ][i],
391+                         offsetEnd
392+                     );
388393
389-                 for  (uint  j =  0 ; j <  numUpdatesFirst; j++ ) {
390-                     PythInternalStructs.TwapPriceInfo memory  twapPriceInfoFirst;
391-                     PythInternalStructs.TwapPriceInfo memory  twapPriceInfoSecond;
392-                     bytes32  priceIdFirst;
393-                     bytes32  priceIdSecond;
394- 
395-                     (offsetFirst, twapPriceInfoFirst, priceIdFirst) =  extractTwapPriceInfoFromMerkleProof (digestFirst, encodedFirst, offsetFirst);
396-                     (offsetSecond, twapPriceInfoSecond, priceIdSecond) =  extractTwapPriceInfoFromMerkleProof (digestSecond, encodedSecond, offsetSecond);
397- 
398-                     require (priceIdFirst ==  priceIdSecond, PythErrors.InvalidTwapUpdateDataSet ());
399-                 
400-                     // No Updates here. 
401-                     // check whether caller requested for this data 
402-                     uint  k =  findIndexOfPriceId (priceIds, priceIdFirst);
403-                     if  (k ==  priceIds.length  ||  twapPriceFeeds[k].id !=  0 ) {
404-                         continue ;
405-                     }
394+                     // We have already validated the number of updates in the first and second updateData so we only use numUpdatesStart here 
395+                     for  (uint  j =  0 ; j <  numUpdatesStart; j++ ) {
396+                         PythInternalStructs.TwapPriceInfo
397+                             memory  twapPriceInfoStart;
398+                         PythInternalStructs.TwapPriceInfo
399+                             memory  twapPriceInfoEnd;
400+                         bytes32  priceIdStart;
401+                         bytes32  priceIdEnd;
406402
407-                     // Since we have already validated the twap price info, we can directly use it 
408-                     validateTwapPriceInfo (twapPriceInfoFirst, twapPriceInfoSecond);
409- 
410-                     // Now we will calcualte the cumulative price and cumulative conf 
411-                     // for the first and second priceId 
412-                     // I believe the cumulative price and cumulative conf will be same for both priceIdFirst and priceIdSecond 
413-                     // because they are both from the same accumulator update 
414-                     uint64  slotDiff =  twapPriceInfoSecond.publishSlot -  twapPriceInfoFirst.publishSlot;
415-                     int128  priceDiff =  twapPriceInfoSecond.price -  twapPriceInfoFirst.price;
416-                     uint128  confDiff =  twapPriceInfoSecond.conf -  twapPriceInfoFirst.conf;
417- 
418-                     // Now we will calculate the twap price and twap conf 
419-                     // for the first and second priceId 
420-                     int128  twapPrice =  priceDiff /  slotDiff;
421-                     uint128  twapConf =  confDiff /  slotDiff;
422- 
423-                     twapPriceFeeds[k].id =  priceIdFirst;
424-                     twapPriceFeeds[k].twap.price =  twapPrice;
425-                     twapPriceFeeds[k].twap.conf =  twapConf;
426-                     twapPriceFeeds[k].twap.expo =  twapPriceInfoFirst.expo;
427-                     twapPriceFeeds[k].twap.publishTime =  twapPriceInfoSecond.publishTime;
428-                     twapPriceFeeds[k].startTime =  twapPriceInfoFirst.publishTime;
429-                     twapPriceFeeds[k].endTime =  twapPriceInfoSecond.publishTime;
430-                     //TODO: Calculate the downSlotRatio 
431-                 }
432-             if  (offsetFirst !=  encodedFirst.length ) {
433-                     revert  PythErrors.InvalidTwapUpdateData ();
434-                 }
435-                 if  (offsetSecond !=  encodedSecond.length ) {
436-                     revert  PythErrors.InvalidTwapUpdateData ();
437-                 }
438-                 if  (offsetFirst !=  offsetSecond) {
439-                     revert  PythErrors.InvalidTwapUpdateData ();
403+                         (
404+                             offsetStart,
405+                             twapPriceInfoStart,
406+                             priceIdStart
407+                         ) =  extractTwapPriceInfoFromMerkleProof (
408+                             digestStart,
409+                             encodedStart,
410+                             offsetStart
411+                         );
412+                         (
413+                             offsetEnd,
414+                             twapPriceInfoEnd,
415+                             priceIdEnd
416+                         ) =  extractTwapPriceInfoFromMerkleProof (
417+                             digestEnd,
418+                             encodedEnd,
419+                             offsetEnd
420+                         );
421+ 
422+                         if  (priceIdStart !=  priceIdEnd)
423+                             revert  PythErrors.InvalidTwapUpdateDataSet ();
424+ 
425+                         // Unlike parsePriceFeedUpdatesInternal, we don't call updateLatestPriceIfNecessary here. 
426+                         // TWAP calculations are read-only operations that compute time-weighted averages 
427+                         // without updating the contract's state, returning calculated values directly to the caller. 
428+                         {
429+                             uint  k =  findIndexOfPriceId (priceIds, priceIdStart);
430+ 
431+                             // If priceFeed[k].id != 0 then it means that there was a valid 
432+                             // update for priceIds[k] and we don't need to process this one. 
433+                             if  (
434+                                 k ==  priceIds.length  || 
435+                                 twapPriceFeeds[k].id !=  0 
436+                             ) {
437+                                 continue ;
438+                             }
439+ 
440+                             // Perform additional validation checks on the TWAP price data 
441+                             // to ensure proper time ordering, consistent exponents, and timestamp integrity 
442+                             // before using the data for calculations 
443+                             validateTwapPriceInfo (
444+                                 twapPriceInfoStart,
445+                                 twapPriceInfoEnd
446+                             );
447+ 
448+                             twapPriceFeeds[k] =  calculateTwap (
449+                                 priceIdStart,
450+                                 twapPriceInfoStart,
451+                                 twapPriceInfoEnd
452+                             );
453+                         }
454+                     }
455+                     if  (offsetStart !=  encodedStart.length ) {
456+                         revert  PythErrors.InvalidTwapUpdateData ();
457+                     }
458+                     if  (offsetEnd !=  encodedEnd.length ) {
459+                         revert  PythErrors.InvalidTwapUpdateData ();
460+                     }
461+                     if  (offsetStart !=  offsetEnd) {
462+                         revert  PythErrors.InvalidTwapUpdateData ();
463+                     }
440464                } else  {
441465                    revert  PythErrors.InvalidUpdateData ();
442466                }
443-             } else  {
444-                 revert  PythErrors.InvalidUpdateData ();
445467            }
446-         }
447468
448469            for  (uint  k =  0 ; k <  priceIds.length ; k++ ) {
449470                if  (twapPriceFeeds[k].id ==  0 ) {
@@ -454,22 +475,27 @@ abstract contract Pyth is
454475    }
455476
456477    function validateTwapPriceInfo  (
457-         PythInternalStructs.TwapPriceInfo memory  twapPriceInfoFirst ,
458-         PythInternalStructs.TwapPriceInfo memory  twapPriceInfoSecond 
478+         PythInternalStructs.TwapPriceInfo memory  twapPriceInfoStart ,
479+         PythInternalStructs.TwapPriceInfo memory  twapPriceInfoEnd 
459480    ) private  pure  {
460-         if  (twapPriceInfoFirst.expo  !=  twapPriceInfoSecond.expo) { 
461-              revert  PythErrors. InvalidTwapUpdateDataSet (); 
462-         } 
463-         if  (twapPriceInfoFirst.publishSlot  >  twapPriceInfoSecond.publishSlot ) {
464-             revert  PythErrors.InvalidTwapUpdateDataSet ();
481+         // First validate each individual data point's internal consistency 
482+         if  ( 
483+             twapPriceInfoStart.prevPublishTime  >  twapPriceInfoStart.publishTime 
484+         ) {
485+             revert  PythErrors.InvalidTwapUpdateData ();
465486        }
466-         if  (twapPriceInfoFirst .prevPublishTime >  twapPriceInfoFirst .publishTime) {
487+         if  (twapPriceInfoEnd .prevPublishTime >  twapPriceInfoEnd .publishTime) {
467488            revert  PythErrors.InvalidTwapUpdateData ();
468489        }
469-         if  (twapPriceInfoSecond.prevPublishTime >  twapPriceInfoSecond.publishTime) {
490+ 
491+         // Then validate the relationship between the two data points 
492+         if  (twapPriceInfoStart.expo !=  twapPriceInfoEnd.expo) {
470493            revert  PythErrors.InvalidTwapUpdateDataSet ();
471494        }
472-         if  (twapPriceInfoFirst.publishTime >  twapPriceInfoSecond.publishTime) {
495+         if  (twapPriceInfoStart.publishSlot >  twapPriceInfoEnd.publishSlot) {
496+             revert  PythErrors.InvalidTwapUpdateDataSet ();
497+         }
498+         if  (twapPriceInfoStart.publishTime >  twapPriceInfoEnd.publishTime) {
473499            revert  PythErrors.InvalidTwapUpdateDataSet ();
474500        }
475501    }
@@ -565,4 +591,50 @@ abstract contract Pyth is
565591    function version  () public  pure  returns  (string  memory ) {
566592        return  "1.4.4-alpha.1 " ;
567593    }
594+ 
595+     function calculateTwap  (
596+         bytes32  priceId ,
597+         PythInternalStructs.TwapPriceInfo memory  twapPriceInfoStart ,
598+         PythInternalStructs.TwapPriceInfo memory  twapPriceInfoEnd 
599+     ) private  pure  returns  (PythStructs.TwapPriceFeed memory  twapPriceFeed ) {
600+         // Calculate differences between start and end points for slots and cumulative values 
601+         // These differences represent the changes that occurred over the time window 
602+         uint64  slotDiff =  twapPriceInfoEnd.publishSlot - 
603+             twapPriceInfoStart.publishSlot;
604+         int128  priceDiff =  twapPriceInfoEnd.cumulativePrice - 
605+             twapPriceInfoStart.cumulativePrice;
606+         uint128  confDiff =  twapPriceInfoEnd.cumulativeConf - 
607+             twapPriceInfoStart.cumulativeConf;
608+ 
609+         // Calculate time-weighted average price (TWAP) and confidence by dividing 
610+         // the difference in cumulative values by the number of slots between data points 
611+         int128  twapPrice =  priceDiff /  int128 (uint128 (slotDiff));
612+         uint128  twapConf =  confDiff /  uint128 (slotDiff);
613+ 
614+         // Initialize the TWAP price feed structure 
615+         twapPriceFeed.id =  priceId;
616+ 
617+         // The conversion from int128 to int64 is safe because: 
618+         // 1. Individual prices fit within int64 by protocol design 
619+         // 2. TWAP is essentially an average price over time (cumulativePrice₂-cumulativePrice₁)/slotDiff 
620+         // 3. This average must be within the range of individual prices that went into the calculation 
621+         // We use int128 only as an intermediate type to safely handle cumulative sums 
622+         twapPriceFeed.twap.price =  int64 (twapPrice);
623+         twapPriceFeed.twap.conf =  uint64 (twapConf);
624+         twapPriceFeed.twap.expo =  twapPriceInfoStart.expo;
625+         twapPriceFeed.twap.publishTime =  twapPriceInfoEnd.publishTime;
626+         twapPriceFeed.startTime =  twapPriceInfoStart.publishTime;
627+         twapPriceFeed.endTime =  twapPriceInfoEnd.publishTime;
628+ 
629+         // Calculate downSlotRatio as a value between 0 and 1,000,000 
630+         // 0 means no slots were missed, 1,000,000 means all slots were missed 
631+         uint64  totalDownSlots =  twapPriceInfoEnd.numDownSlots - 
632+             twapPriceInfoStart.numDownSlots;
633+         uint64  downSlotsRatio =  (totalDownSlots *  1_000_000 ) /  slotDiff;
634+ 
635+         // Safely downcast to uint32 (sufficient for value range 0-1,000,000) 
636+         twapPriceFeed.downSlotRatio =  uint32 (downSlotsRatio);
637+ 
638+         return  twapPriceFeed;
639+     }
568640}
0 commit comments