Przeglądaj źródła

[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
Amin Moghaddam 1 rok temu
rodzic
commit
d0ceb076d8

+ 12 - 19
target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

@@ -168,17 +168,7 @@ abstract contract Pyth is
                 index += attestationSize;
 
                 // Store the attestation
-                uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
-
-                if (info.publishTime > latestPublishTime) {
-                    setLatestPriceInfo(priceId, info);
-                    emit PriceFeedUpdate(
-                        priceId,
-                        info.publishTime,
-                        info.price,
-                        info.conf
-                    );
-                }
+                updateLatestPriceIfNecessary(priceId, info);
             }
 
             emit BatchPriceFeedUpdate(vm.emitterChainId, vm.sequence);
@@ -486,12 +476,12 @@ abstract contract Pyth is
                     );
 
                     for (uint j = 0; j < numUpdates; j++) {
-                        PythInternalStructs.PriceInfo memory info;
+                        PythInternalStructs.PriceInfo memory priceInfo;
                         bytes32 priceId;
                         uint64 prevPublishTime;
                         (
                             offset,
-                            info,
+                            priceInfo,
                             priceId,
                             prevPublishTime
                         ) = extractPriceInfoFromMerkleProof(
@@ -499,6 +489,7 @@ abstract contract Pyth is
                             encoded,
                             offset
                         );
+                        updateLatestPriceIfNecessary(priceId, priceInfo);
                         {
                             // check whether caller requested for this data
                             uint k = findIndexOfPriceId(priceIds, priceId);
@@ -509,7 +500,7 @@ abstract contract Pyth is
                                 continue;
                             }
 
-                            uint publishTime = uint(info.publishTime);
+                            uint publishTime = uint(priceInfo.publishTime);
                             // Check the publish time of the price is within the given range
                             // and only fill the priceFeedsInfo if it is.
                             // If is not, default id value of 0 will still be set and
@@ -524,7 +515,7 @@ abstract contract Pyth is
                                     priceFeeds,
                                     k,
                                     priceId,
-                                    info,
+                                    priceInfo,
                                     publishTime
                                 );
                             }
@@ -576,7 +567,7 @@ abstract contract Pyth is
                         }
 
                         (
-                            PythInternalStructs.PriceInfo memory info,
+                            PythInternalStructs.PriceInfo memory priceInfo,
 
                         ) = parseSingleAttestationFromBatch(
                                 encoded,
@@ -584,7 +575,9 @@ abstract contract Pyth is
                                 attestationSize
                             );
 
-                        uint publishTime = uint(info.publishTime);
+                        updateLatestPriceIfNecessary(priceId, priceInfo);
+
+                        uint publishTime = uint(priceInfo.publishTime);
                         // Check the publish time of the price is within the given range
                         // and only fill the priceFeedsInfo if it is.
                         // If is not, default id value of 0 will still be set and
@@ -598,7 +591,7 @@ abstract contract Pyth is
                                 priceFeeds,
                                 k,
                                 priceId,
-                                info,
+                                priceInfo,
                                 publishTime
                             );
                         }
@@ -727,6 +720,6 @@ abstract contract Pyth is
     }
 
     function version() public pure returns (string memory) {
-        return "1.3.3";
+        return "1.4.3";
     }
 }

+ 1 - 10
target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol

@@ -371,16 +371,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
                     priceId,
                     prevPublishTime
                 ) = extractPriceInfoFromMerkleProof(digest, encoded, offset);
-                uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
-                if (priceInfo.publishTime > latestPublishTime) {
-                    setLatestPriceInfo(priceId, priceInfo);
-                    emit PriceFeedUpdate(
-                        priceId,
-                        priceInfo.publishTime,
-                        priceInfo.price,
-                        priceInfo.conf
-                    );
-                }
+                updateLatestPriceIfNecessary(priceId, priceInfo);
             }
         }
         if (offset != encoded.length) revert PythErrors.InvalidUpdateData();

+ 13 - 3
target_chains/ethereum/contracts/contracts/pyth/PythSetters.sol

@@ -4,17 +4,27 @@
 pragma solidity ^0.8.0;
 
 import "./PythState.sol";
+import "@pythnetwork/pyth-sdk-solidity/IPythEvents.sol";
 
-contract PythSetters is PythState {
+contract PythSetters is PythState, IPythEvents {
     function setWormhole(address wh) internal {
         _state.wormhole = payable(wh);
     }
 
-    function setLatestPriceInfo(
+    function updateLatestPriceIfNecessary(
         bytes32 priceId,
         PythInternalStructs.PriceInfo memory info
     ) internal {
-        _state.latestPriceInfo[priceId] = info;
+        uint64 latestPublishTime = _state.latestPriceInfo[priceId].publishTime;
+        if (info.publishTime > latestPublishTime) {
+            _state.latestPriceInfo[priceId] = info;
+            emit PriceFeedUpdate(
+                priceId,
+                info.publishTime,
+                info.price,
+                info.conf
+            );
+        }
     }
 
     function setSingleUpdateFeeInWei(uint fee) internal {

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

@@ -898,6 +898,85 @@ contract PythWormholeMerkleAccumulatorTest is
         );
     }
 
+    function testParsePriceFeedUniqueWithWormholeMerkleUpdatesLatestPriceIfNecessary(
+        uint seed
+    ) public {
+        setRandSeed(seed);
+
+        uint64 numPriceFeeds = (getRandUint64() % 10) + 2;
+        PriceFeedMessage[]
+            memory priceFeedMessages = generateRandomPriceFeedMessage(
+                numPriceFeeds
+            );
+        uint64 publishTime = getRandUint64();
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = priceFeedMessages[0].priceId;
+        for (uint64 i = 0; i < numPriceFeeds; i++) {
+            priceFeedMessages[i].priceId = priceFeedMessages[0].priceId;
+            priceFeedMessages[i].publishTime = publishTime;
+            priceFeedMessages[i].prevPublishTime = publishTime;
+        }
+
+        // firstUpdate is the one we expect to be returned and latestUpdate is the one we expect to be stored
+        uint latestUpdate = (getRandUint() % numPriceFeeds);
+        priceFeedMessages[latestUpdate].prevPublishTime = publishTime + 1000;
+        priceFeedMessages[latestUpdate].publishTime = publishTime + 1000;
+
+        uint firstUpdate = (getRandUint() % numPriceFeeds);
+        while (firstUpdate == latestUpdate) {
+            firstUpdate = (getRandUint() % numPriceFeeds);
+        }
+        priceFeedMessages[firstUpdate].prevPublishTime = publishTime - 1;
+        (
+            bytes[] memory updateData,
+            uint updateFee
+        ) = createWormholeMerkleUpdateData(priceFeedMessages);
+
+        // firstUpdate is returned but latestUpdate is stored
+        PythStructs.PriceFeed[] memory priceFeeds = pyth
+            .parsePriceFeedUpdatesUnique{value: updateFee}(
+            updateData,
+            priceIds,
+            publishTime,
+            MAX_UINT64
+        );
+        assertEq(priceFeeds.length, 1);
+        assertParsedPriceFeedEqualsMessage(
+            priceFeeds[0],
+            priceFeedMessages[firstUpdate],
+            priceIds[0]
+        );
+        assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);
+
+        // increase the latestUpdate publish time and make a new updateData
+        priceFeedMessages[latestUpdate].publishTime = publishTime + 2000;
+        (updateData, updateFee) = createWormholeMerkleUpdateData(
+            priceFeedMessages
+        );
+
+        // since there is a revert, the latestUpdate is not stored
+        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
+        pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
+            updateData,
+            priceIds,
+            publishTime - 1,
+            MAX_UINT64
+        );
+        assertEq(
+            pyth.getPriceUnsafe(priceIds[0]).publishTime,
+            publishTime + 1000
+        );
+
+        // there is no revert, the latestPrice is updated with the latestUpdate
+        pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
+            updateData,
+            priceIds,
+            publishTime,
+            MAX_UINT64
+        );
+        assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);
+    }
+
     function testParsePriceFeedWithWormholeMerkleWorksRandomDistinctUpdatesInput(
         uint seed
     ) public {

+ 75 - 0
target_chains/ethereum/contracts/forge-test/Pyth.t.sol

@@ -525,4 +525,79 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
             MAX_UINT64
         );
     }
+
+    function testParsePriceFeedUpdatesLatestPriceIfNecessary() public {
+        uint numAttestations = 10;
+        (
+            bytes32[] memory priceIds,
+            PriceAttestation[] memory attestations
+        ) = generateRandomPriceAttestations(numAttestations);
+
+        for (uint i = 0; i < numAttestations; i++) {
+            // Set status to Trading so publishTime is used
+            attestations[i].status = PriceAttestationStatus.Trading;
+            attestations[i].publishTime = uint64((getRandUint() % 101)); // All between [0, 100]
+        }
+
+        (
+            bytes[] memory updateData,
+            uint updateFee
+        ) = createBatchedUpdateDataFromAttestations(attestations);
+
+        // Request for parse within the given time range should work and update the latest price
+        pyth.parsePriceFeedUpdates{value: updateFee}(
+            updateData,
+            priceIds,
+            0,
+            100
+        );
+
+        // Check if the latest price is updated
+        for (uint i = 0; i < numAttestations; i++) {
+            assertEq(
+                pyth.getPriceUnsafe(priceIds[i]).publishTime,
+                attestations[i].publishTime
+            );
+        }
+
+        for (uint i = 0; i < numAttestations; i++) {
+            // Set status to Trading so publishTime is used
+            attestations[i].status = PriceAttestationStatus.Trading;
+            attestations[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200]
+        }
+
+        (updateData, updateFee) = createBatchedUpdateDataFromAttestations(
+            attestations
+        );
+
+        // Request for parse after the time range should revert.
+        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
+        pyth.parsePriceFeedUpdates{value: updateFee}(
+            updateData,
+            priceIds,
+            300,
+            400
+        );
+
+        // parse function reverted so publishTimes should remain less than or equal to 100
+        for (uint i = 0; i < numAttestations; i++) {
+            assertGe(100, pyth.getPriceUnsafe(priceIds[i]).publishTime);
+        }
+
+        // Time range is now fixed, so parse should work and update the latest price
+        pyth.parsePriceFeedUpdates{value: updateFee}(
+            updateData,
+            priceIds,
+            100,
+            200
+        );
+
+        // Check if the latest price is updated
+        for (uint i = 0; i < numAttestations; i++) {
+            assertEq(
+                pyth.getPriceUnsafe(priceIds[i]).publishTime,
+                attestations[i].publishTime
+            );
+        }
+    }
 }

+ 4 - 44
target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol

@@ -428,17 +428,7 @@ contract PythExperimental is Pyth {
             PythInternalStructs.PriceInfo memory info,
             bytes32 priceId
         ) = parseSingleAttestationFromBatch(data, 0, data.length);
-        uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
-
-        if (info.publishTime > latestPublishTime) {
-            setLatestPriceInfo(priceId, info);
-            emit PriceFeedUpdate(
-                priceId,
-                info.publishTime,
-                info.price,
-                info.conf
-            );
-        }
+        updateLatestPriceIfNecessary(priceId, info);
     }
 
     // Update a single price feed via a threshold-signed merkle proof.
@@ -459,17 +449,7 @@ contract PythExperimental is Pyth {
             PythInternalStructs.PriceInfo memory info,
             bytes32 priceId
         ) = parseSingleAttestationFromBatch(data, 0, data.length);
-        uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
-
-        if (info.publishTime > latestPublishTime) {
-            setLatestPriceInfo(priceId, info);
-            emit PriceFeedUpdate(
-                priceId,
-                info.publishTime,
-                info.price,
-                info.conf
-            );
-        }
+        updateLatestPriceIfNecessary(priceId, info);
     }
 
     // Update a single price feed via a threshold-signed price update.
@@ -486,17 +466,7 @@ contract PythExperimental is Pyth {
             PythInternalStructs.PriceInfo memory info,
             bytes32 priceId
         ) = parseSingleAttestationFromBatch(data, 0, data.length);
-        uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
-
-        if (info.publishTime > latestPublishTime) {
-            setLatestPriceInfo(priceId, info);
-            emit PriceFeedUpdate(
-                priceId,
-                info.publishTime,
-                info.price,
-                info.conf
-            );
-        }
+        updateLatestPriceIfNecessary(priceId, info);
     }
 
     // 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 {
             PythInternalStructs.PriceInfo memory info,
             bytes32 priceId
         ) = parseSingleAttestationFromBatch(data, 0, data.length);
-        uint64 latestPublishTime = latestPriceInfoPublishTime(priceId);
-
-        if (info.publishTime > latestPublishTime) {
-            setLatestPriceInfo(priceId, info);
-            emit PriceFeedUpdate(
-                priceId,
-                info.publishTime,
-                info.price,
-                info.conf
-            );
-        }
+        updateLatestPriceIfNecessary(priceId, info);
     }
 
     // Verify that signature is a valid ECDSA signature of messageHash by signer.

+ 4 - 2
target_chains/ethereum/sdk/solidity/IPyth.sol

@@ -117,7 +117,8 @@ interface IPyth is IPythEvents {
     /// within `minPublishTime` and `maxPublishTime`.
     ///
     /// You can use this method if you want to use a Pyth price at a fixed time and not the most recent price;
-    /// otherwise, please consider using `updatePriceFeeds`. This method does not store the price updates on-chain.
+    /// otherwise, please consider using `updatePriceFeeds`. This method may store the price updates on-chain, if they
+    /// are more recent than the current stored prices.
     ///
     /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling
     /// `getUpdateFee` with the length of the `updateData` array.
@@ -139,7 +140,8 @@ interface IPyth is IPythEvents {
 
     /// @notice Similar to `parsePriceFeedUpdates` but ensures the updates returned are
     /// the first updates published in minPublishTime. That is, if there are multiple updates for a given timestamp,
-    /// this method will return the first update.
+    /// this method will return the first update. This method may store the price updates on-chain, if they
+    /// are more recent than the current stored prices.
     ///
     ///
     /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid or there is