Skip to content

Commit d0ceb07

Browse files
authored
[evm] Persist price updates if more recent (#1208)
* Persist price info if it is more recent in parse functions * Refactor setLatestPrice to include checks and event in a single place * Add test cases
1 parent a941941 commit d0ceb07

File tree

7 files changed

+188
-78
lines changed

7 files changed

+188
-78
lines changed

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

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,7 @@ abstract contract Pyth is
168168
index += attestationSize;
169169

170170
// Store the attestation
171-
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
172-
173-
if (info.publishTime > latestPublishTime) {
174-
setLatestPriceInfo(priceId, info);
175-
emit PriceFeedUpdate(
176-
priceId,
177-
info.publishTime,
178-
info.price,
179-
info.conf
180-
);
181-
}
171+
updateLatestPriceIfNecessary(priceId, info);
182172
}
183173

184174
emit BatchPriceFeedUpdate(vm.emitterChainId, vm.sequence);
@@ -486,19 +476,20 @@ abstract contract Pyth is
486476
);
487477

488478
for (uint j = 0; j < numUpdates; j++) {
489-
PythInternalStructs.PriceInfo memory info;
479+
PythInternalStructs.PriceInfo memory priceInfo;
490480
bytes32 priceId;
491481
uint64 prevPublishTime;
492482
(
493483
offset,
494-
info,
484+
priceInfo,
495485
priceId,
496486
prevPublishTime
497487
) = extractPriceInfoFromMerkleProof(
498488
digest,
499489
encoded,
500490
offset
501491
);
492+
updateLatestPriceIfNecessary(priceId, priceInfo);
502493
{
503494
// check whether caller requested for this data
504495
uint k = findIndexOfPriceId(priceIds, priceId);
@@ -509,7 +500,7 @@ abstract contract Pyth is
509500
continue;
510501
}
511502

512-
uint publishTime = uint(info.publishTime);
503+
uint publishTime = uint(priceInfo.publishTime);
513504
// Check the publish time of the price is within the given range
514505
// and only fill the priceFeedsInfo if it is.
515506
// If is not, default id value of 0 will still be set and
@@ -524,7 +515,7 @@ abstract contract Pyth is
524515
priceFeeds,
525516
k,
526517
priceId,
527-
info,
518+
priceInfo,
528519
publishTime
529520
);
530521
}
@@ -576,15 +567,17 @@ abstract contract Pyth is
576567
}
577568

578569
(
579-
PythInternalStructs.PriceInfo memory info,
570+
PythInternalStructs.PriceInfo memory priceInfo,
580571

581572
) = parseSingleAttestationFromBatch(
582573
encoded,
583574
index,
584575
attestationSize
585576
);
586577

587-
uint publishTime = uint(info.publishTime);
578+
updateLatestPriceIfNecessary(priceId, priceInfo);
579+
580+
uint publishTime = uint(priceInfo.publishTime);
588581
// Check the publish time of the price is within the given range
589582
// and only fill the priceFeedsInfo if it is.
590583
// If is not, default id value of 0 will still be set and
@@ -598,7 +591,7 @@ abstract contract Pyth is
598591
priceFeeds,
599592
k,
600593
priceId,
601-
info,
594+
priceInfo,
602595
publishTime
603596
);
604597
}
@@ -727,6 +720,6 @@ abstract contract Pyth is
727720
}
728721

729722
function version() public pure returns (string memory) {
730-
return "1.3.3";
723+
return "1.4.3";
731724
}
732725
}

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

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -371,16 +371,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
371371
priceId,
372372
prevPublishTime
373373
) = extractPriceInfoFromMerkleProof(digest, encoded, offset);
374-
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
375-
if (priceInfo.publishTime > latestPublishTime) {
376-
setLatestPriceInfo(priceId, priceInfo);
377-
emit PriceFeedUpdate(
378-
priceId,
379-
priceInfo.publishTime,
380-
priceInfo.price,
381-
priceInfo.conf
382-
);
383-
}
374+
updateLatestPriceIfNecessary(priceId, priceInfo);
384375
}
385376
}
386377
if (offset != encoded.length) revert PythErrors.InvalidUpdateData();

target_chains/ethereum/contracts/contracts/pyth/PythSetters.sol

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,27 @@
44
pragma solidity ^0.8.0;
55

66
import "./PythState.sol";
7+
import "@pythnetwork/pyth-sdk-solidity/IPythEvents.sol";
78

8-
contract PythSetters is PythState {
9+
contract PythSetters is PythState, IPythEvents {
910
function setWormhole(address wh) internal {
1011
_state.wormhole = payable(wh);
1112
}
1213

13-
function setLatestPriceInfo(
14+
function updateLatestPriceIfNecessary(
1415
bytes32 priceId,
1516
PythInternalStructs.PriceInfo memory info
1617
) internal {
17-
_state.latestPriceInfo[priceId] = info;
18+
uint64 latestPublishTime = _state.latestPriceInfo[priceId].publishTime;
19+
if (info.publishTime > latestPublishTime) {
20+
_state.latestPriceInfo[priceId] = info;
21+
emit PriceFeedUpdate(
22+
priceId,
23+
info.publishTime,
24+
info.price,
25+
info.conf
26+
);
27+
}
1828
}
1929

2030
function setSingleUpdateFeeInWei(uint fee) internal {

target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,85 @@ contract PythWormholeMerkleAccumulatorTest is
898898
);
899899
}
900900

901+
function testParsePriceFeedUniqueWithWormholeMerkleUpdatesLatestPriceIfNecessary(
902+
uint seed
903+
) public {
904+
setRandSeed(seed);
905+
906+
uint64 numPriceFeeds = (getRandUint64() % 10) + 2;
907+
PriceFeedMessage[]
908+
memory priceFeedMessages = generateRandomPriceFeedMessage(
909+
numPriceFeeds
910+
);
911+
uint64 publishTime = getRandUint64();
912+
bytes32[] memory priceIds = new bytes32[](1);
913+
priceIds[0] = priceFeedMessages[0].priceId;
914+
for (uint64 i = 0; i < numPriceFeeds; i++) {
915+
priceFeedMessages[i].priceId = priceFeedMessages[0].priceId;
916+
priceFeedMessages[i].publishTime = publishTime;
917+
priceFeedMessages[i].prevPublishTime = publishTime;
918+
}
919+
920+
// firstUpdate is the one we expect to be returned and latestUpdate is the one we expect to be stored
921+
uint latestUpdate = (getRandUint() % numPriceFeeds);
922+
priceFeedMessages[latestUpdate].prevPublishTime = publishTime + 1000;
923+
priceFeedMessages[latestUpdate].publishTime = publishTime + 1000;
924+
925+
uint firstUpdate = (getRandUint() % numPriceFeeds);
926+
while (firstUpdate == latestUpdate) {
927+
firstUpdate = (getRandUint() % numPriceFeeds);
928+
}
929+
priceFeedMessages[firstUpdate].prevPublishTime = publishTime - 1;
930+
(
931+
bytes[] memory updateData,
932+
uint updateFee
933+
) = createWormholeMerkleUpdateData(priceFeedMessages);
934+
935+
// firstUpdate is returned but latestUpdate is stored
936+
PythStructs.PriceFeed[] memory priceFeeds = pyth
937+
.parsePriceFeedUpdatesUnique{value: updateFee}(
938+
updateData,
939+
priceIds,
940+
publishTime,
941+
MAX_UINT64
942+
);
943+
assertEq(priceFeeds.length, 1);
944+
assertParsedPriceFeedEqualsMessage(
945+
priceFeeds[0],
946+
priceFeedMessages[firstUpdate],
947+
priceIds[0]
948+
);
949+
assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);
950+
951+
// increase the latestUpdate publish time and make a new updateData
952+
priceFeedMessages[latestUpdate].publishTime = publishTime + 2000;
953+
(updateData, updateFee) = createWormholeMerkleUpdateData(
954+
priceFeedMessages
955+
);
956+
957+
// since there is a revert, the latestUpdate is not stored
958+
vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
959+
pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
960+
updateData,
961+
priceIds,
962+
publishTime - 1,
963+
MAX_UINT64
964+
);
965+
assertEq(
966+
pyth.getPriceUnsafe(priceIds[0]).publishTime,
967+
publishTime + 1000
968+
);
969+
970+
// there is no revert, the latestPrice is updated with the latestUpdate
971+
pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
972+
updateData,
973+
priceIds,
974+
publishTime,
975+
MAX_UINT64
976+
);
977+
assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);
978+
}
979+
901980
function testParsePriceFeedWithWormholeMerkleWorksRandomDistinctUpdatesInput(
902981
uint seed
903982
) public {

target_chains/ethereum/contracts/forge-test/Pyth.t.sol

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,4 +525,79 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
525525
MAX_UINT64
526526
);
527527
}
528+
529+
function testParsePriceFeedUpdatesLatestPriceIfNecessary() public {
530+
uint numAttestations = 10;
531+
(
532+
bytes32[] memory priceIds,
533+
PriceAttestation[] memory attestations
534+
) = generateRandomPriceAttestations(numAttestations);
535+
536+
for (uint i = 0; i < numAttestations; i++) {
537+
// Set status to Trading so publishTime is used
538+
attestations[i].status = PriceAttestationStatus.Trading;
539+
attestations[i].publishTime = uint64((getRandUint() % 101)); // All between [0, 100]
540+
}
541+
542+
(
543+
bytes[] memory updateData,
544+
uint updateFee
545+
) = createBatchedUpdateDataFromAttestations(attestations);
546+
547+
// Request for parse within the given time range should work and update the latest price
548+
pyth.parsePriceFeedUpdates{value: updateFee}(
549+
updateData,
550+
priceIds,
551+
0,
552+
100
553+
);
554+
555+
// Check if the latest price is updated
556+
for (uint i = 0; i < numAttestations; i++) {
557+
assertEq(
558+
pyth.getPriceUnsafe(priceIds[i]).publishTime,
559+
attestations[i].publishTime
560+
);
561+
}
562+
563+
for (uint i = 0; i < numAttestations; i++) {
564+
// Set status to Trading so publishTime is used
565+
attestations[i].status = PriceAttestationStatus.Trading;
566+
attestations[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200]
567+
}
568+
569+
(updateData, updateFee) = createBatchedUpdateDataFromAttestations(
570+
attestations
571+
);
572+
573+
// Request for parse after the time range should revert.
574+
vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
575+
pyth.parsePriceFeedUpdates{value: updateFee}(
576+
updateData,
577+
priceIds,
578+
300,
579+
400
580+
);
581+
582+
// parse function reverted so publishTimes should remain less than or equal to 100
583+
for (uint i = 0; i < numAttestations; i++) {
584+
assertGe(100, pyth.getPriceUnsafe(priceIds[i]).publishTime);
585+
}
586+
587+
// Time range is now fixed, so parse should work and update the latest price
588+
pyth.parsePriceFeedUpdates{value: updateFee}(
589+
updateData,
590+
priceIds,
591+
100,
592+
200
593+
);
594+
595+
// Check if the latest price is updated
596+
for (uint i = 0; i < numAttestations; i++) {
597+
assertEq(
598+
pyth.getPriceUnsafe(priceIds[i]).publishTime,
599+
attestations[i].publishTime
600+
);
601+
}
602+
}
528603
}

target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -428,17 +428,7 @@ contract PythExperimental is Pyth {
428428
PythInternalStructs.PriceInfo memory info,
429429
bytes32 priceId
430430
) = parseSingleAttestationFromBatch(data, 0, data.length);
431-
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
432-
433-
if (info.publishTime > latestPublishTime) {
434-
setLatestPriceInfo(priceId, info);
435-
emit PriceFeedUpdate(
436-
priceId,
437-
info.publishTime,
438-
info.price,
439-
info.conf
440-
);
441-
}
431+
updateLatestPriceIfNecessary(priceId, info);
442432
}
443433

444434
// Update a single price feed via a threshold-signed merkle proof.
@@ -459,17 +449,7 @@ contract PythExperimental is Pyth {
459449
PythInternalStructs.PriceInfo memory info,
460450
bytes32 priceId
461451
) = parseSingleAttestationFromBatch(data, 0, data.length);
462-
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
463-
464-
if (info.publishTime > latestPublishTime) {
465-
setLatestPriceInfo(priceId, info);
466-
emit PriceFeedUpdate(
467-
priceId,
468-
info.publishTime,
469-
info.price,
470-
info.conf
471-
);
472-
}
452+
updateLatestPriceIfNecessary(priceId, info);
473453
}
474454

475455
// Update a single price feed via a threshold-signed price update.
@@ -486,17 +466,7 @@ contract PythExperimental is Pyth {
486466
PythInternalStructs.PriceInfo memory info,
487467
bytes32 priceId
488468
) = parseSingleAttestationFromBatch(data, 0, data.length);
489-
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
490-
491-
if (info.publishTime > latestPublishTime) {
492-
setLatestPriceInfo(priceId, info);
493-
emit PriceFeedUpdate(
494-
priceId,
495-
info.publishTime,
496-
info.price,
497-
info.conf
498-
);
499-
}
469+
updateLatestPriceIfNecessary(priceId, info);
500470
}
501471

502472
// Update a single price feed via a "native" price update (i.e., using the default ethereum tx signature for authentication).
@@ -510,17 +480,7 @@ contract PythExperimental is Pyth {
510480
PythInternalStructs.PriceInfo memory info,
511481
bytes32 priceId
512482
) = parseSingleAttestationFromBatch(data, 0, data.length);
513-
uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
514-
515-
if (info.publishTime > latestPublishTime) {
516-
setLatestPriceInfo(priceId, info);
517-
emit PriceFeedUpdate(
518-
priceId,
519-
info.publishTime,
520-
info.price,
521-
info.conf
522-
);
523-
}
483+
updateLatestPriceIfNecessary(priceId, info);
524484
}
525485

526486
// Verify that signature is a valid ECDSA signature of messageHash by signer.

0 commit comments

Comments
 (0)