Skip to content

Commit a87ff74

Browse files
committed
update: fix logic & improve code structure and readability
1 parent 507b5ca commit a87ff74

File tree

4 files changed

+202
-123
lines changed

4 files changed

+202
-123
lines changed

target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

Lines changed: 189 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,9 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
281281
PythInternalStructs.TwapPriceInfo memory twapPriceInfo,
282282
bytes32 priceId
283283
)
284-
// TODO: Add the logic to extract the twap price info from the merkle proof
284+
// TODO: Add the logic to extract the twap price info from the merkle proof
285285
{
286-
unchecked {
286+
unchecked {
287287
bytes calldata encodedMessage;
288288
uint16 messageSize = UnsafeCalldataBytesLib.toUint16(
289289
encoded,
@@ -322,7 +322,6 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
322322
return (endOffset, twapPriceInfo, priceId);
323323
}
324324
}
325-
326325

327326
function parsePriceFeedMessage(
328327
bytes calldata encodedPriceFeed,
@@ -452,7 +451,6 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
452451
}
453452
}
454453

455-
456454
function updatePriceInfosFromAccumulatorUpdate(
457455
bytes calldata accumulatorUpdate
458456
) internal returns (uint8 numUpdates) {

target_chains/ethereum/contracts/contracts/pyth/PythInternalStructs.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,13 @@ contract PythInternalStructs {
3030
// slot 1
3131
int128 cumulativePrice;
3232
uint128 cumulativeConf;
33-
3433
// slot 2
3534
uint64 numDownSlots;
3635
uint64 publishSlot;
3736
uint64 publishTime;
3837
uint64 prevPublishTime;
3938
// slot 3
40-
39+
4140
int32 expo;
4241
}
4342

0 commit comments

Comments
 (0)