@@ -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