Răsfoiți Sursa

fix(pulse-scheduler): validate atomic subscription updates using slots instead of timestamps (#2590)

* feat: add parsePriceFeedUpdatesWithSlots to Pyth contract

* fix: add constants

* fix: use slots (not timestamp) to verify atomic sub update

* refactor: move structs to PythInternalStructs, rename

* feat: bump @pythnetwork/pyth-sdk-solidity version
Tejas Badadare 7 luni în urmă
părinte
comite
7642a395b9

+ 22 - 12
target_chains/ethereum/contracts/contracts/pulse/scheduler/Scheduler.sol

@@ -239,17 +239,21 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         // Parse price feed updates with an expected timestamp range of [-10s, now]
         // We will validate the trigger conditions and timestamps ourselves
         // using the returned PriceFeeds.
-        uint64 maxPublishTime = SafeCast.toUint64(block.timestamp);
-        uint64 minPublishTime = maxPublishTime - 10 seconds;
-        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
+        uint64 curTime = SafeCast.toUint64(block.timestamp);
+        uint64 maxPublishTime = curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD;
+        uint64 minPublishTime = curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD;
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = pyth.parsePriceFeedUpdatesWithSlots{
             value: pythFee
         }(updateData, priceIds, minPublishTime, maxPublishTime);
 
-        // Verify all price feeds have the same timestamp
-        uint256 timestamp = priceFeeds[0].price.publishTime;
-        for (uint8 i = 1; i < priceFeeds.length; i++) {
-            if (priceFeeds[i].price.publishTime != timestamp) {
-                revert PriceTimestampMismatch();
+        // Verify all price feeds have the same Pythnet slot.
+        // All feeds in a subscription must be updated at the same time.
+        uint64 slot = slots[0];
+        for (uint8 i = 1; i < slots.length; i++) {
+            if (slots[i] != slot) {
+                revert PriceSlotMismatch();
             }
         }
 
@@ -291,7 +295,6 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
     /**
      * @notice Validates whether the update trigger criteria is met for a subscription. Reverts if not met.
-     * @dev This function assumes that all updates in priceFeeds have the same timestamp. The caller is expected to enforce this invariant.
      * @param subscriptionId The ID of the subscription (needed for reading previous prices).
      * @param params The subscription's parameters struct.
      * @param status The subscription's status struct.
@@ -303,9 +306,16 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         SubscriptionStatus storage status,
         PythStructs.PriceFeed[] memory priceFeeds
     ) internal view returns (bool) {
-        // SECURITY NOTE: this check assumes that all updates in priceFeeds have the same timestamp.
-        // The caller is expected to enforce this invariant.
-        uint256 updateTimestamp = priceFeeds[0].price.publishTime;
+        // Use the most recent timestamp, as some asset markets may be closed.
+        // Closed markets will have a publishTime from their last trading period.
+        // Since we verify all updates share the same Pythnet slot, we still ensure
+        // that all price feeds are synchronized from the same update cycle.
+        uint256 updateTimestamp = 0;
+        for (uint8 i = 0; i < priceFeeds.length; i++) {
+            if (priceFeeds[i].price.publishTime > updateTimestamp) {
+                updateTimestamp = priceFeeds[i].price.publishTime;
+            }
+        }
 
         // Reject updates if they're older than the latest stored ones
         if (

+ 1 - 1
target_chains/ethereum/contracts/contracts/pulse/scheduler/SchedulerErrors.sol

@@ -9,7 +9,7 @@ error InvalidPriceId(bytes32 providedPriceId, bytes32 expectedPriceId);
 error InvalidPriceIdsLength(bytes32 providedLength, bytes32 expectedLength);
 error InvalidUpdateCriteria();
 error InvalidGasConfig();
-error PriceTimestampMismatch();
+error PriceSlotMismatch();
 error TooManyPriceIds(uint256 provided, uint256 maximum);
 error UpdateConditionsNotMet();
 error TimestampOlderThanLastUpdate(

+ 8 - 0
target_chains/ethereum/contracts/contracts/pulse/scheduler/SchedulerState.sol

@@ -12,6 +12,14 @@ contract SchedulerState {
     /// Default max fee multiplier
     uint32 public constant DEFAULT_MAX_PRIORITY_FEE_MULTIPLIER_CAP_PCT = 10_000;
 
+    // TODO: make these updateable via governance
+    /// Maximum time in the past (relative to current block timestamp)
+    /// for which a price update timestamp is considered valid
+    uint64 public constant PAST_TIMESTAMP_MAX_VALIDITY_PERIOD = 1 hours;
+    /// Maximum time in the future (relative to current block timestamp)
+    /// for which a price update timestamp is considered valid
+    uint64 public constant FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD = 10 seconds;
+
     struct State {
         /// Monotonically increasing counter for subscription IDs
         uint256 subscriptionNumber;

+ 173 - 103
target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

@@ -179,111 +179,152 @@ abstract contract Pyth is
         if (price.publishTime == 0) revert PythErrors.PriceFeedNotFound();
     }
 
+    /// @dev Helper function to parse a single price update within a Merkle proof.
+    /// Parsed price feeds will be stored in the context.
+    function _parseSingleMerkleUpdate(
+        PythInternalStructs.MerkleData memory merkleData,
+        bytes calldata encoded,
+        uint offset,
+        PythInternalStructs.UpdateParseContext memory context
+    ) internal pure returns (uint newOffset) {
+        PythInternalStructs.PriceInfo memory priceInfo;
+        bytes32 priceId;
+        uint64 prevPublishTime;
+
+        (
+            newOffset,
+            priceInfo,
+            priceId,
+            prevPublishTime
+        ) = extractPriceInfoFromMerkleProof(merkleData.digest, encoded, offset);
+
+        uint k = 0;
+        for (; k < context.priceIds.length; k++) {
+            if (context.priceIds[k] == priceId) {
+                break;
+            }
+        }
+
+        // Check if the priceId was requested and not already filled
+        if (k < context.priceIds.length && context.priceFeeds[k].id == 0) {
+            uint publishTime = uint(priceInfo.publishTime);
+            if (
+                publishTime >= context.config.minPublishTime &&
+                publishTime <= context.config.maxPublishTime &&
+                (!context.config.checkUniqueness ||
+                    context.config.minPublishTime > prevPublishTime)
+            ) {
+                context.priceFeeds[k].id = priceId;
+                context.priceFeeds[k].price.price = priceInfo.price;
+                context.priceFeeds[k].price.conf = priceInfo.conf;
+                context.priceFeeds[k].price.expo = priceInfo.expo;
+                context.priceFeeds[k].price.publishTime = publishTime;
+                context.priceFeeds[k].emaPrice.price = priceInfo.emaPrice;
+                context.priceFeeds[k].emaPrice.conf = priceInfo.emaConf;
+                context.priceFeeds[k].emaPrice.expo = priceInfo.expo;
+                context.priceFeeds[k].emaPrice.publishTime = publishTime;
+                context.slots[k] = merkleData.slot;
+            }
+        }
+    }
+
+    /// @dev Processes a single entry from the updateData array.
+    function _processSingleUpdateDataBlob(
+        bytes calldata singleUpdateData,
+        PythInternalStructs.UpdateParseContext memory context
+    ) internal view {
+        // Check magic number and length first
+        if (
+            singleUpdateData.length <= 4 ||
+            UnsafeCalldataBytesLib.toUint32(singleUpdateData, 0) !=
+            ACCUMULATOR_MAGIC
+        ) {
+            revert PythErrors.InvalidUpdateData();
+        }
+
+        uint offset;
+        {
+            UpdateType updateType;
+            (offset, updateType) = extractUpdateTypeFromAccumulatorHeader(
+                singleUpdateData
+            );
+
+            if (updateType != UpdateType.WormholeMerkle) {
+                revert PythErrors.InvalidUpdateData();
+            }
+        }
+
+        // Extract Merkle data
+        PythInternalStructs.MerkleData memory merkleData;
+        bytes calldata encoded;
+        (
+            offset,
+            merkleData.digest,
+            merkleData.numUpdates,
+            encoded,
+            merkleData.slot
+        ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate(
+            singleUpdateData,
+            offset
+        );
+
+        // Process each update within the Merkle proof
+        for (uint j = 0; j < merkleData.numUpdates; j++) {
+            offset = _parseSingleMerkleUpdate(
+                merkleData,
+                encoded,
+                offset,
+                context
+            );
+        }
+
+        // Check final offset
+        if (offset != encoded.length) {
+            revert PythErrors.InvalidUpdateData();
+        }
+    }
+
     function parsePriceFeedUpdatesInternal(
         bytes[] calldata updateData,
         bytes32[] calldata priceIds,
         PythInternalStructs.ParseConfig memory config
-    ) internal returns (PythStructs.PriceFeed[] memory priceFeeds) {
+    )
+        internal
+        returns (
+            PythStructs.PriceFeed[] memory priceFeeds,
+            uint64[] memory slots
+        )
+    {
         {
             uint requiredFee = getUpdateFee(updateData);
             if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
         }
-        unchecked {
-            priceFeeds = new PythStructs.PriceFeed[](priceIds.length);
-            for (uint i = 0; i < updateData.length; i++) {
-                if (
-                    updateData[i].length > 4 &&
-                    UnsafeCalldataBytesLib.toUint32(updateData[i], 0) ==
-                    ACCUMULATOR_MAGIC
-                ) {
-                    uint offset;
-                    {
-                        UpdateType updateType;
-                        (
-                            offset,
-                            updateType
-                        ) = extractUpdateTypeFromAccumulatorHeader(
-                            updateData[i]
-                        );
-
-                        if (updateType != UpdateType.WormholeMerkle) {
-                            revert PythErrors.InvalidUpdateData();
-                        }
-                    }
 
-                    bytes20 digest;
-                    uint8 numUpdates;
-                    bytes calldata encoded;
-                    (
-                        offset,
-                        digest,
-                        numUpdates,
-                        encoded
-                    ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
-                        updateData[i],
-                        offset
-                    );
+        // Create the context struct that holds all shared parameters
+        PythInternalStructs.UpdateParseContext memory context;
+        context.priceIds = priceIds;
+        context.config = config;
+        context.priceFeeds = new PythStructs.PriceFeed[](priceIds.length);
+        context.slots = new uint64[](priceIds.length);
 
-                    for (uint j = 0; j < numUpdates; j++) {
-                        PythInternalStructs.PriceInfo memory priceInfo;
-                        bytes32 priceId;
-                        uint64 prevPublishTime;
-                        (
-                            offset,
-                            priceInfo,
-                            priceId,
-                            prevPublishTime
-                        ) = extractPriceInfoFromMerkleProof(
-                            digest,
-                            encoded,
-                            offset
-                        );
-                        {
-                            // check whether caller requested for this data
-                            uint k = findIndexOfPriceId(priceIds, priceId);
-
-                            // If priceFeed[k].id != 0 then it means that there was a valid
-                            // update for priceIds[k] and we don't need to process this one.
-                            if (k == priceIds.length || priceFeeds[k].id != 0) {
-                                continue;
-                            }
-
-                            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
-                            // this will allow other updates for this price id to be processed.
-                            if (
-                                publishTime >= config.minPublishTime &&
-                                publishTime <= config.maxPublishTime &&
-                                (!config.checkUniqueness ||
-                                    config.minPublishTime > prevPublishTime)
-                            ) {
-                                fillPriceFeedFromPriceInfo(
-                                    priceFeeds,
-                                    k,
-                                    priceId,
-                                    priceInfo,
-                                    publishTime
-                                );
-                            }
-                        }
-                    }
-                    if (offset != encoded.length)
-                        revert PythErrors.InvalidUpdateData();
-                } else {
-                    revert PythErrors.InvalidUpdateData();
-                }
+        unchecked {
+            // Process each update, passing the context struct
+            // Parsed results will be filled in context.priceFeeds and context.slots
+            for (uint i = 0; i < updateData.length; i++) {
+                _processSingleUpdateDataBlob(updateData[i], context);
             }
+        }
 
-            for (uint k = 0; k < priceIds.length; k++) {
-                if (priceFeeds[k].id == 0) {
-                    revert PythErrors.PriceFeedNotFoundWithinRange();
-                }
+        // Check all price feeds were found
+        for (uint k = 0; k < priceIds.length; k++) {
+            if (context.priceFeeds[k].id == 0) {
+                revert PythErrors.PriceFeedNotFoundWithinRange();
             }
         }
-    }
 
+        // Return results
+        return (context.priceFeeds, context.slots);
+    }
     function parsePriceFeedUpdates(
         bytes[] calldata updateData,
         bytes32[] calldata priceIds,
@@ -294,6 +335,31 @@ abstract contract Pyth is
         payable
         override
         returns (PythStructs.PriceFeed[] memory priceFeeds)
+    {
+        (priceFeeds, ) = parsePriceFeedUpdatesInternal(
+            updateData,
+            priceIds,
+            PythInternalStructs.ParseConfig(
+                minPublishTime,
+                maxPublishTime,
+                false
+            )
+        );
+    }
+
+    function parsePriceFeedUpdatesWithSlots(
+        bytes[] calldata updateData,
+        bytes32[] calldata priceIds,
+        uint64 minPublishTime,
+        uint64 maxPublishTime
+    )
+        external
+        payable
+        override
+        returns (
+            PythStructs.PriceFeed[] memory priceFeeds,
+            uint64[] memory slots
+        )
     {
         return
             parsePriceFeedUpdatesInternal(
@@ -339,8 +405,10 @@ abstract contract Pyth is
             offset,
             digest,
             numUpdates,
-            encoded
-        ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
+            encoded,
+            // slot ignored
+
+        ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate(
             updateData,
             offset
         );
@@ -477,16 +545,15 @@ abstract contract Pyth is
         override
         returns (PythStructs.PriceFeed[] memory priceFeeds)
     {
-        return
-            parsePriceFeedUpdatesInternal(
-                updateData,
-                priceIds,
-                PythInternalStructs.ParseConfig(
-                    minPublishTime,
-                    maxPublishTime,
-                    true
-                )
-            );
+        (priceFeeds, ) = parsePriceFeedUpdatesInternal(
+            updateData,
+            priceIds,
+            PythInternalStructs.ParseConfig(
+                minPublishTime,
+                maxPublishTime,
+                true
+            )
+        );
     }
 
     function getTotalFee(
@@ -514,7 +581,9 @@ abstract contract Pyth is
         uint k,
         bytes32 priceId,
         PythInternalStructs.PriceInfo memory info,
-        uint publishTime
+        uint publishTime,
+        uint64[] memory slots,
+        uint64 slot
     ) private pure {
         priceFeeds[k].id = priceId;
         priceFeeds[k].price.price = info.price;
@@ -525,6 +594,7 @@ abstract contract Pyth is
         priceFeeds[k].emaPrice.conf = info.emaConf;
         priceFeeds[k].emaPrice.expo = info.expo;
         priceFeeds[k].emaPrice.publishTime = publishTime;
+        slots[k] = slot;
     }
 
     function queryPriceFeed(

+ 11 - 6
target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol

@@ -112,7 +112,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
         }
     }
 
-    function extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
+    function extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate(
         bytes calldata accumulatorUpdate,
         uint encodedOffset
     )
@@ -122,7 +122,8 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
             uint offset,
             bytes20 digest,
             uint8 numUpdates,
-            bytes calldata encoded
+            bytes calldata encoded,
+            uint64 slot
         )
     {
         unchecked {
@@ -175,8 +176,10 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
                     if (updateType != UpdateType.WormholeMerkle)
                         revert PythErrors.InvalidUpdateData();
 
-                    // This field is not used
-                    // uint64 slot = UnsafeBytesLib.toUint64(encodedPayload, payloadoffset);
+                    slot = UnsafeBytesLib.toUint64(
+                        encodedPayload,
+                        payloadOffset
+                    );
                     payloadOffset += 8;
 
                     // This field is not used
@@ -467,12 +470,14 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
         uint offset;
         bytes20 digest;
         bytes calldata encoded;
+        uint64 slot;
         (
             offset,
             digest,
             numUpdates,
-            encoded
-        ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
+            encoded,
+            slot
+        ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate(
             accumulatorUpdate,
             encodedOffset
         );

+ 20 - 0
target_chains/ethereum/contracts/contracts/pyth/PythInternalStructs.sol

@@ -15,6 +15,26 @@ contract PythInternalStructs {
         bool checkUniqueness;
     }
 
+    /// Internal struct to hold parameters for update processing
+    /// @dev Storing these variable in a struct rather than local variables
+    /// helps reduce stack depth when passing arguments to functions.
+    struct UpdateParseContext {
+        bytes32[] priceIds;
+        ParseConfig config;
+        PythStructs.PriceFeed[] priceFeeds;
+        uint64[] slots;
+    }
+
+    /// The initial Merkle header data in an AccumulatorUpdate. The encoded bytes
+    /// are kept in calldata for gas efficiency.
+    /// @dev Storing these variable in a struct rather than local variables
+    /// helps reduce stack depth when passing arguments to functions.
+    struct MerkleData {
+        bytes20 digest;
+        uint8 numUpdates;
+        uint64 slot;
+    }
+
     struct PriceInfo {
         // slot 1
         uint64 publishTime;

+ 77 - 69
target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol

@@ -88,15 +88,15 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         reader = new MockReader(address(proxy));
 
-        // Start tests at timestamp 100 to avoid underflow when we set
-        // `minPublishTime = timestamp - 10 seconds` in updatePriceFeeds
-        vm.warp(100);
+        // Start tests at a high timestamp to avoid underflow when we set
+        // `minPublishTime = timestamp - 1 hour` in updatePriceFeeds
+        vm.warp(100000);
 
         // Give pusher 100 ETH for testing
         vm.deal(pusher, 100 ether);
     }
 
-    function testcreateSubscription() public {
+    function testCreateSubscription() public {
         // Create subscription parameters
         bytes32[] memory priceIds = createPriceIds();
         address[] memory readerWhitelist = new address[](1);
@@ -269,11 +269,14 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         bytes32[] memory initialPriceIds = createPriceIds(numInitialFeeds);
         uint64 publishTime = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory initialPriceFeeds = createMockPriceFeeds(
+        PythStructs.PriceFeed[] memory initialPriceFeeds;
+        uint64[] memory slots;
+        (initialPriceFeeds, slots) = createMockPriceFeedsWithSlots(
             publishTime,
             numInitialFeeds
         );
-        mockParsePriceFeedUpdates(pyth, initialPriceFeeds);
+
+        mockParsePriceFeedUpdatesWithSlots(pyth, initialPriceFeeds, slots);
         bytes[] memory updateData = createMockUpdateData(initialPriceFeeds);
 
         vm.prank(pusher);
@@ -575,10 +578,14 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
         // Create price feeds and mock Pyth response for first update
         bytes32[] memory priceIds = createPriceIds();
         uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds1 = createMockPriceFeeds(
-            publishTime1
+        PythStructs.PriceFeed[] memory priceFeeds1;
+        uint64[] memory slots;
+        (priceFeeds1, slots) = createMockPriceFeedsWithSlots(
+            publishTime1,
+            priceIds.length
         );
-        mockParsePriceFeedUpdates(pyth, priceFeeds1);
+
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots);
         bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
 
         // Perform first update
@@ -630,7 +637,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
             priceFeeds2[i].emaPrice.publishTime = publishTime2;
         }
 
-        mockParsePriceFeedUpdates(pyth, priceFeeds2); // Mock for the second call
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots); // Mock for the second call
         bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
 
         // Perform second update
@@ -686,14 +693,13 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
         );
         uint256 fundAmount = 1 ether;
         scheduler.addFunds{value: fundAmount}(subscriptionId);
-
         // First update to set initial timestamp
         bytes32[] memory priceIds = createPriceIds();
         uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds1 = createMockPriceFeeds(
-            publishTime1
-        );
-        mockParsePriceFeedUpdates(pyth, priceFeeds1);
+        PythStructs.PriceFeed[] memory priceFeeds1;
+        uint64[] memory slots1;
+        (priceFeeds1, slots1) = createMockPriceFeedsWithSlots(publishTime1, 2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots1);
         bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
         vm.prank(pusher);
         scheduler.updatePriceFeeds(subscriptionId, updateData1, priceIds);
@@ -701,10 +707,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
         // Prepare second update within heartbeat interval
         vm.warp(block.timestamp + 30); // Advance time by 30 seconds (less than 60)
         uint64 publishTime2 = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds2 = createMockPriceFeeds(
-            publishTime2 // Same prices, just new timestamp
-        );
-        mockParsePriceFeedUpdates(pyth, priceFeeds2); // Mock the response for the second update
+        PythStructs.PriceFeed[] memory priceFeeds2;
+        uint64[] memory slots2;
+        (priceFeeds2, slots2) = createMockPriceFeedsWithSlots(publishTime2, 2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots2);
         bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
 
         // Expect revert because heartbeat condition is not met
@@ -736,10 +742,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
         // First update to set initial price
         bytes32[] memory priceIds = createPriceIds();
         uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds1 = createMockPriceFeeds(
-            publishTime1
-        );
-        mockParsePriceFeedUpdates(pyth, priceFeeds1);
+        PythStructs.PriceFeed[] memory priceFeeds1;
+        uint64[] memory slots;
+        (priceFeeds1, slots) = createMockPriceFeedsWithSlots(publishTime1, 2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots);
         bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
         vm.prank(pusher);
         scheduler.updatePriceFeeds(subscriptionId, updateData1, priceIds);
@@ -765,7 +771,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
             priceFeeds2[i].price.publishTime = publishTime2;
         }
 
-        mockParsePriceFeedUpdates(pyth, priceFeeds2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots);
         bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
 
         // Expect revert because deviation condition is not met
@@ -785,10 +791,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
         // First update to establish last updated timestamp
         bytes32[] memory priceIds = createPriceIds();
         uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds1 = createMockPriceFeeds(
-            publishTime1
-        );
-        mockParsePriceFeedUpdates(pyth, priceFeeds1);
+        PythStructs.PriceFeed[] memory priceFeeds1;
+        uint64[] memory slots1;
+        (priceFeeds1, slots1) = createMockPriceFeedsWithSlots(publishTime1, 2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds1, slots1);
         bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);
 
         vm.prank(pusher);
@@ -796,11 +802,11 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         // Prepare second update with an older timestamp
         uint64 publishTime2 = publishTime1 - 10; // Timestamp older than the first update
-        PythStructs.PriceFeed[] memory priceFeeds2 = createMockPriceFeeds(
-            publishTime2
-        );
+        PythStructs.PriceFeed[] memory priceFeeds2;
+        uint64[] memory slots2;
+        (priceFeeds2, slots2) = createMockPriceFeedsWithSlots(publishTime2, 2);
         // Mock Pyth response to return feeds with the older timestamp
-        mockParsePriceFeedUpdates(pyth, priceFeeds2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds2, slots2);
         bytes[] memory updateData2 = createMockUpdateData(priceFeeds2);
 
         // Expect revert with TimestampOlderThanLastUpdate (checked in _validateShouldUpdatePrices)
@@ -817,30 +823,32 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
         scheduler.updatePriceFeeds(subscriptionId, updateData2, priceIds);
     }
 
-    function testUpdatePriceFeedsRevertsOnMismatchedTimestamps() public {
+    function testUpdatePriceFeedsRevertsOnMismatchedSlots() public {
         // First add a subscription and funds
         uint256 subscriptionId = addTestSubscription();
         uint256 fundAmount = 1 ether;
         scheduler.addFunds{value: fundAmount}(subscriptionId);
 
-        // Create two price feeds with mismatched timestamps
+        // Create two price feeds with same timestamp but different slots
         bytes32[] memory priceIds = createPriceIds(2);
-        uint64 time1 = SafeCast.toUint64(block.timestamp);
-        uint64 time2 = time1 + 10;
+        uint64 publishTime = SafeCast.toUint64(block.timestamp);
         PythStructs.PriceFeed[] memory priceFeeds = new PythStructs.PriceFeed[](
             2
         );
-        priceFeeds[0] = createSingleMockPriceFeed(time1);
-        priceFeeds[1] = createSingleMockPriceFeed(time2);
+        priceFeeds[0] = createSingleMockPriceFeed(publishTime);
+        priceFeeds[1] = createSingleMockPriceFeed(publishTime);
 
-        // Mock Pyth response to return these feeds
-        mockParsePriceFeedUpdates(pyth, priceFeeds);
-        bytes[] memory updateData = createMockUpdateData(priceFeeds); // Data needs to match expected length
+        // Create slots array with different slot values
+        uint64[] memory slots = new uint64[](2);
+        slots[0] = 100;
+        slots[1] = 200; // Different slot
 
-        // Expect revert with PriceTimestampMismatch error
-        vm.expectRevert(
-            abi.encodeWithSelector(PriceTimestampMismatch.selector)
-        );
+        // Mock Pyth response to return these feeds with mismatched slots
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
+        bytes[] memory updateData = createMockUpdateData(priceFeeds);
+
+        // Expect revert with PriceSlotMismatch error
+        vm.expectRevert(abi.encodeWithSelector(PriceSlotMismatch.selector));
 
         // Attempt to update price feeds
         vm.prank(pusher);
@@ -855,10 +863,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         bytes32[] memory priceIds = createPriceIds();
         uint64 publishTime = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
-            publishTime
-        );
-        mockParsePriceFeedUpdates(pyth, priceFeeds);
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         vm.prank(pusher);
@@ -893,11 +901,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         bytes32[] memory priceIds = createPriceIds(3);
         uint64 publishTime = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
-            publishTime,
-            3
-        );
-        mockParsePriceFeedUpdates(pyth, priceFeeds);
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 3);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         vm.prank(pusher);
@@ -964,10 +971,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         // Update price feeds for the subscription
         uint64 publishTime = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
-            publishTime
-        );
-        mockParsePriceFeedUpdates(pyth, priceFeeds);
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         vm.prank(pusher);
@@ -1025,20 +1032,20 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         // Update price feeds for the subscription
         uint64 publishTime = SafeCast.toUint64(block.timestamp + 10); // Slightly different time
-        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(
             publishTime,
             priceIds.length
         );
-        mockParsePriceFeedUpdates(pyth, priceFeeds);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         vm.prank(pusher);
         scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
 
-        // Try to access from the non-whitelisted address (should fail)
-        address randomUser = address(0xdead);
-        address manager = address(this); // Test contract is the manager
-        vm.startPrank(randomUser);
+        // Try to access from a non-whitelisted address (should fail)
+        vm.startPrank(address(0xdead));
         bytes32[] memory emptyPriceIds = new bytes32[](0);
         vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
         scheduler.getPricesUnsafe(subscriptionId, emptyPriceIds);
@@ -1063,7 +1070,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
         );
 
         // Try to access from the manager address (should succeed)
-        vm.startPrank(manager);
+        // Test contract is the manager
+        vm.startPrank(address(this));
         PythStructs.Price[] memory pricesFromManager = scheduler
             .getPricesUnsafe(subscriptionId, emptyPriceIds);
         assertEq(
@@ -1082,9 +1090,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
 
         bytes32[] memory priceIds = createPriceIds();
         uint64 publishTime = SafeCast.toUint64(block.timestamp);
-        PythStructs.PriceFeed[] memory priceFeeds = createMockPriceFeeds(
-            publishTime
-        );
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
 
         // Ensure EMA prices are set in the mock price feeds
         for (uint i = 0; i < priceFeeds.length; i++) {
@@ -1094,7 +1102,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseTestUtils {
             priceFeeds[i].emaPrice.expo = priceFeeds[i].price.expo;
         }
 
-        mockParsePriceFeedUpdates(pyth, priceFeeds);
+        mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         vm.prank(pusher);

+ 43 - 1
target_chains/ethereum/contracts/forge-test/Pyth.t.sol

@@ -204,7 +204,6 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
         );
     }
 
-    /// Testing parsePriceFeedUpdates method.
     function testParsePriceFeedUpdatesWorks(uint seed) public {
         setRandSeed(seed);
         uint numMessages = 1 + (getRandUint() % 10);
@@ -237,6 +236,49 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils {
         }
     }
 
+    function testParsePriceFeedUpdatesWithSlotsWorks(uint seed) public {
+        setRandSeed(seed);
+        uint numMessages = 1 + (getRandUint() % 10);
+        (
+            bytes32[] memory priceIds,
+            PriceFeedMessage[] memory messages
+        ) = generateRandomPriceMessages(numMessages);
+
+        (
+            bytes[] memory updateData,
+            uint updateFee
+        ) = createBatchedUpdateDataFromMessages(messages);
+        (
+            PythStructs.PriceFeed[] memory priceFeeds,
+            uint64[] memory slots
+        ) = pyth.parsePriceFeedUpdatesWithSlots{value: updateFee}(
+                updateData,
+                priceIds,
+                0,
+                MAX_UINT64
+            );
+
+        assertEq(priceFeeds.length, numMessages);
+        assertEq(slots.length, numMessages);
+
+        for (uint i = 0; i < numMessages; i++) {
+            assertEq(priceFeeds[i].id, priceIds[i]);
+            assertEq(priceFeeds[i].price.price, messages[i].price);
+            assertEq(priceFeeds[i].price.conf, messages[i].conf);
+            assertEq(priceFeeds[i].price.expo, messages[i].expo);
+            assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime);
+            assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice);
+            assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf);
+            assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo);
+            assertEq(
+                priceFeeds[i].emaPrice.publishTime,
+                messages[i].publishTime
+            );
+            // Check that the slot returned is 1, as set in generateWhMerkleUpdateWithSource
+            assertEq(slots[i], 1);
+        }
+    }
+
     function testParsePriceFeedUpdatesWorksWithOverlappingWithinTimeRangeUpdates()
         public
     {

+ 47 - 0
target_chains/ethereum/contracts/forge-test/utils/PulseTestUtils.t.sol

@@ -74,6 +74,29 @@ abstract contract PulseTestUtils is Test {
         return createMockPriceFeeds(publishTime, 1)[0];
     }
 
+    // Helper function to create mock price feeds with slots
+    function createMockPriceFeedsWithSlots(
+        uint256 publishTime,
+        uint256 numFeeds
+    )
+        internal
+        pure
+        returns (
+            PythStructs.PriceFeed[] memory priceFeeds,
+            uint64[] memory slots
+        )
+    {
+        priceFeeds = createMockPriceFeeds(publishTime, numFeeds);
+        slots = new uint64[](numFeeds);
+
+        // Set all slots to the publishTime as a mock value
+        for (uint256 i = 0; i < numFeeds; i++) {
+            slots[i] = uint64(publishTime);
+        }
+
+        return (priceFeeds, slots);
+    }
+
     // Helper function to create mock price feeds with default 2 feeds
     function createMockPriceFeeds(
         uint256 publishTime
@@ -137,6 +160,30 @@ abstract contract PulseTestUtils is Test {
         );
     }
 
+    // Helper function to mock Pyth response with slots
+    function mockParsePriceFeedUpdatesWithSlots(
+        address pyth,
+        PythStructs.PriceFeed[] memory priceFeeds,
+        uint64[] memory slots
+    ) internal {
+        uint expectedFee = MOCK_PYTH_FEE_PER_FEED * priceFeeds.length;
+
+        vm.mockCall(
+            pyth,
+            abi.encodeWithSelector(IPyth.getUpdateFee.selector),
+            abi.encode(expectedFee)
+        );
+
+        vm.mockCall(
+            pyth,
+            expectedFee,
+            abi.encodeWithSelector(
+                IPyth.parsePriceFeedUpdatesWithSlots.selector
+            ),
+            abi.encode(priceFeeds, slots)
+        );
+    }
+
     // Helper function to create mock update data for variable feeds
     function createMockUpdateData(
         PythStructs.PriceFeed[] memory priceFeeds

+ 1 - 1
target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol

@@ -148,7 +148,7 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
         bytes memory wormholePayload = abi.encodePacked(
             uint32(0x41555756), // PythAccumulator.ACCUMULATOR_WORMHOLE_MAGIC
             uint8(PythAccumulator.UpdateType.WormholeMerkle),
-            uint64(0), // Slot, not used in target networks
+            uint64(1), // Slot, not used in target networks
             uint32(0), // Ring size, not used in target networks
             rootDigest
         );

+ 15 - 0
target_chains/ethereum/sdk/solidity/AbstractPyth.sol

@@ -136,6 +136,21 @@ abstract contract AbstractPyth is IPyth {
         override
         returns (PythStructs.PriceFeed[] memory priceFeeds);
 
+    function parsePriceFeedUpdatesWithSlots(
+        bytes[] calldata updateData,
+        bytes32[] calldata priceIds,
+        uint64 minPublishTime,
+        uint64 maxPublishTime
+    )
+        external
+        payable
+        virtual
+        override
+        returns (
+            PythStructs.PriceFeed[] memory priceFeeds,
+            uint64[] memory slots
+        );
+
     function parseTwapPriceFeedUpdates(
         bytes[] calldata updateData,
         bytes32[] calldata priceIds

+ 21 - 0
target_chains/ethereum/sdk/solidity/IPyth.sol

@@ -164,4 +164,25 @@ interface IPyth is IPythEvents {
         uint64 minPublishTime,
         uint64 maxPublishTime
     ) external payable returns (PythStructs.PriceFeed[] memory priceFeeds);
+
+    /// @dev Same as `parsePriceFeedUpdates`, but also returns the Pythnet slot
+    /// associated with each price update.
+    /// @param updateData Array of price update data.
+    /// @param priceIds Array of price ids.
+    /// @param minPublishTime minimum acceptable publishTime for the given `priceIds`.
+    /// @param maxPublishTime maximum acceptable publishTime for the given `priceIds`.
+    /// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order).
+    /// @return slots Array of the Pythnet slot corresponding to the given `priceIds` (with the same order).
+    function parsePriceFeedUpdatesWithSlots(
+        bytes[] calldata updateData,
+        bytes32[] calldata priceIds,
+        uint64 minPublishTime,
+        uint64 maxPublishTime
+    )
+        external
+        payable
+        returns (
+            PythStructs.PriceFeed[] memory priceFeeds,
+            uint64[] memory slots
+        );
 }

+ 34 - 10
target_chains/ethereum/sdk/solidity/MockPyth.sol

@@ -86,11 +86,15 @@ contract MockPyth is AbstractPyth {
         uint64 minPublishTime,
         uint64 maxPublishTime,
         bool unique
-    ) internal returns (PythStructs.PriceFeed[] memory feeds) {
+    )
+        internal
+        returns (PythStructs.PriceFeed[] memory feeds, uint64[] memory slots)
+    {
         uint requiredFee = getUpdateFee(updateData);
         if (msg.value < requiredFee) revert PythErrors.InsufficientFee();
 
         feeds = new PythStructs.PriceFeed[](priceIds.length);
+        slots = new uint64[](priceIds.length);
 
         for (uint i = 0; i < priceIds.length; i++) {
             for (uint j = 0; j < updateData.length; j++) {
@@ -101,6 +105,7 @@ contract MockPyth is AbstractPyth {
                 );
 
                 uint publishTime = feeds[i].price.publishTime;
+                slots[i] = uint64(publishTime); // use PublishTime as mock slot
                 if (priceFeeds[feeds[i].id].price.publishTime < publishTime) {
                     priceFeeds[feeds[i].id] = feeds[i];
                     emit PriceFeedUpdate(
@@ -135,14 +140,13 @@ contract MockPyth is AbstractPyth {
         uint64 minPublishTime,
         uint64 maxPublishTime
     ) external payable override returns (PythStructs.PriceFeed[] memory feeds) {
-        return
-            parsePriceFeedUpdatesInternal(
-                updateData,
-                priceIds,
-                minPublishTime,
-                maxPublishTime,
-                false
-            );
+        (feeds, ) = parsePriceFeedUpdatesInternal(
+            updateData,
+            priceIds,
+            minPublishTime,
+            maxPublishTime,
+            false
+        );
     }
 
     function parsePriceFeedUpdatesUnique(
@@ -151,13 +155,33 @@ contract MockPyth is AbstractPyth {
         uint64 minPublishTime,
         uint64 maxPublishTime
     ) external payable override returns (PythStructs.PriceFeed[] memory feeds) {
+        (feeds, ) = parsePriceFeedUpdatesInternal(
+            updateData,
+            priceIds,
+            minPublishTime,
+            maxPublishTime,
+            true
+        );
+    }
+
+    function parsePriceFeedUpdatesWithSlots(
+        bytes[] calldata updateData,
+        bytes32[] calldata priceIds,
+        uint64 minPublishTime,
+        uint64 maxPublishTime
+    )
+        external
+        payable
+        override
+        returns (PythStructs.PriceFeed[] memory feeds, uint64[] memory slots)
+    {
         return
             parsePriceFeedUpdatesInternal(
                 updateData,
                 priceIds,
                 minPublishTime,
                 maxPublishTime,
-                true
+                false
             );
     }
 

+ 1 - 1
target_chains/ethereum/sdk/solidity/README.md

@@ -92,7 +92,7 @@ You can find a list of available price feeds [here](https://pyth.network/develop
 
 ### ABIs
 
-When making changes to a contract interface, please make sure to update the ABI files too. You can update it using `pnpm generate-abi` and it will update the ABI files in [abis](./abis) directory.
+When making changes to a contract interface, please make sure to update the ABI files too. You can update it using `pnpm turbo run build:abis` and it will update the ABI files in [abis](./abis) directory.
 
 ### Releases
 

+ 100 - 0
target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json

@@ -566,6 +566,106 @@
     "stateMutability": "payable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes[]",
+        "name": "updateData",
+        "type": "bytes[]"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      },
+      {
+        "internalType": "uint64",
+        "name": "minPublishTime",
+        "type": "uint64"
+      },
+      {
+        "internalType": "uint64",
+        "name": "maxPublishTime",
+        "type": "uint64"
+      }
+    ],
+    "name": "parsePriceFeedUpdatesWithSlots",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32",
+            "name": "id",
+            "type": "bytes32"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "price",
+            "type": "tuple"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "emaPrice",
+            "type": "tuple"
+          }
+        ],
+        "internalType": "struct PythStructs.PriceFeed[]",
+        "name": "priceFeeds",
+        "type": "tuple[]"
+      },
+      {
+        "internalType": "uint64[]",
+        "name": "slots",
+        "type": "uint64[]"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
   {
     "inputs": [
       {

+ 100 - 0
target_chains/ethereum/sdk/solidity/abis/IPyth.json

@@ -456,6 +456,106 @@
     "stateMutability": "payable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes[]",
+        "name": "updateData",
+        "type": "bytes[]"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      },
+      {
+        "internalType": "uint64",
+        "name": "minPublishTime",
+        "type": "uint64"
+      },
+      {
+        "internalType": "uint64",
+        "name": "maxPublishTime",
+        "type": "uint64"
+      }
+    ],
+    "name": "parsePriceFeedUpdatesWithSlots",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32",
+            "name": "id",
+            "type": "bytes32"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "price",
+            "type": "tuple"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "emaPrice",
+            "type": "tuple"
+          }
+        ],
+        "internalType": "struct PythStructs.PriceFeed[]",
+        "name": "priceFeeds",
+        "type": "tuple[]"
+      },
+      {
+        "internalType": "uint64[]",
+        "name": "slots",
+        "type": "uint64[]"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
   {
     "inputs": [
       {

+ 100 - 0
target_chains/ethereum/sdk/solidity/abis/MockPyth.json

@@ -705,6 +705,106 @@
     "stateMutability": "payable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes[]",
+        "name": "updateData",
+        "type": "bytes[]"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      },
+      {
+        "internalType": "uint64",
+        "name": "minPublishTime",
+        "type": "uint64"
+      },
+      {
+        "internalType": "uint64",
+        "name": "maxPublishTime",
+        "type": "uint64"
+      }
+    ],
+    "name": "parsePriceFeedUpdatesWithSlots",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32",
+            "name": "id",
+            "type": "bytes32"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "price",
+            "type": "tuple"
+          },
+          {
+            "components": [
+              {
+                "internalType": "int64",
+                "name": "price",
+                "type": "int64"
+              },
+              {
+                "internalType": "uint64",
+                "name": "conf",
+                "type": "uint64"
+              },
+              {
+                "internalType": "int32",
+                "name": "expo",
+                "type": "int32"
+              },
+              {
+                "internalType": "uint256",
+                "name": "publishTime",
+                "type": "uint256"
+              }
+            ],
+            "internalType": "struct PythStructs.Price",
+            "name": "emaPrice",
+            "type": "tuple"
+          }
+        ],
+        "internalType": "struct PythStructs.PriceFeed[]",
+        "name": "feeds",
+        "type": "tuple[]"
+      },
+      {
+        "internalType": "uint64[]",
+        "name": "slots",
+        "type": "uint64[]"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
   {
     "inputs": [
       {

+ 1 - 1
target_chains/ethereum/sdk/solidity/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-sdk-solidity",
-  "version": "4.0.0",
+  "version": "4.1.0",
   "description": "Read prices from the Pyth oracle",
   "type": "module",
   "repository": {