Quellcode durchsuchen

fix(pulse): reset priceLastUpdatedAt when price IDs are added in updateSubscription (#2674)

* fix(pulse): reset priceLastUpdatedAt when price IDs change in updateSubscription

Co-Authored-By: Tejas Badadare <tejas@dourolabs.xyz>

* fix(pulse): reset priceLastUpdatedAt only when new price IDs are added

Co-Authored-By: Tejas Badadare <tejas@dourolabs.xyz>

* test: fix test

* test: fix refs to mockParsePriceFeedUpdatesWithSlots

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Tejas Badadare <tejas@dourolabs.xyz>
Co-authored-by: Tejas Badadare <tejasbadadare@gmail.com>
devin-ai-integration[bot] vor 5 Monaten
Ursprung
Commit
1acf74bd29

+ 30 - 2
target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol

@@ -136,12 +136,17 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         }
 
         // Clear price updates for removed price IDs before updating params
-        _clearRemovedPriceUpdates(
+        bool newPriceIdsAdded = _clearRemovedPriceUpdates(
             subscriptionId,
             currentParams.priceIds,
             newParams.priceIds
         );
 
+        // Reset priceLastUpdatedAt to 0 if new price IDs were added
+        if (newPriceIdsAdded) {
+            _state.subscriptionStatuses[subscriptionId].priceLastUpdatedAt = 0;
+        }
+
         // Update subscription parameters
         _state.subscriptionParams[subscriptionId] = newParams;
 
@@ -216,12 +221,13 @@ abstract contract Scheduler is IScheduler, SchedulerState {
      * @param subscriptionId The ID of the subscription being updated.
      * @param currentPriceIds The array of price IDs currently associated with the subscription.
      * @param newPriceIds The new array of price IDs for the subscription.
+     * @return newPriceIdsAdded True if any new price IDs were added, false otherwise.
      */
     function _clearRemovedPriceUpdates(
         uint256 subscriptionId,
         bytes32[] storage currentPriceIds,
         bytes32[] memory newPriceIds
-    ) internal {
+    ) internal returns (bool newPriceIdsAdded) {
         // Iterate through old price IDs
         for (uint i = 0; i < currentPriceIds.length; i++) {
             bytes32 oldPriceId = currentPriceIds[i];
@@ -240,6 +246,28 @@ abstract contract Scheduler is IScheduler, SchedulerState {
                 delete _state.priceUpdates[subscriptionId][oldPriceId];
             }
         }
+
+        // Check if any new price IDs were added
+        for (uint i = 0; i < newPriceIds.length; i++) {
+            bytes32 newPriceId = newPriceIds[i];
+            bool found = false;
+
+            // Check if the new price ID exists in the current list
+            for (uint j = 0; j < currentPriceIds.length; j++) {
+                if (currentPriceIds[j] == newPriceId) {
+                    found = true;
+                    break;
+                }
+            }
+
+            // If a new price ID was added, mark as changed
+            if (!found) {
+                newPriceIdsAdded = true;
+                break;
+            }
+        }
+
+        return newPriceIdsAdded;
     }
 
     function updatePriceFeeds(

+ 112 - 0
target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol

@@ -332,6 +332,118 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
     }
 
+    // Helper function to reduce stack depth in testUpdateSubscriptionResetsPriceLastUpdatedAt
+    function _setupSubscriptionAndFirstUpdate()
+        private
+        returns (uint256 subscriptionId, uint64 publishTime)
+    {
+        // Setup subscription with heartbeat criteria
+        uint32 heartbeatSeconds = 60; // 60 second heartbeat
+        SchedulerState.UpdateCriteria memory criteria = SchedulerState
+            .UpdateCriteria({
+                updateOnHeartbeat: true,
+                heartbeatSeconds: heartbeatSeconds,
+                updateOnDeviation: false,
+                deviationThresholdBps: 0
+            });
+
+        subscriptionId = addTestSubscriptionWithUpdateCriteria(
+            scheduler,
+            criteria,
+            address(reader)
+        );
+        scheduler.addFunds{value: 1 ether}(subscriptionId);
+
+        // Update prices to set priceLastUpdatedAt to a non-zero value
+        publishTime = SafeCast.toUint64(block.timestamp);
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
+        mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
+        bytes[] memory updateData = createMockUpdateData(priceFeeds);
+
+        vm.prank(pusher);
+        scheduler.updatePriceFeeds(subscriptionId, updateData);
+
+        return (subscriptionId, publishTime);
+    }
+
+    function testUpdateSubscriptionResetsPriceLastUpdatedAt() public {
+        // 1. Setup subscription and perform first update
+        (
+            uint256 subscriptionId,
+            uint64 publishTime1
+        ) = _setupSubscriptionAndFirstUpdate();
+
+        // Verify priceLastUpdatedAt is set
+        (, SchedulerState.SubscriptionStatus memory status) = scheduler
+            .getSubscription(subscriptionId);
+        assertEq(
+            status.priceLastUpdatedAt,
+            publishTime1,
+            "priceLastUpdatedAt should be set to the first update timestamp"
+        );
+
+        // 2. Update subscription to add price IDs
+        (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler
+            .getSubscription(subscriptionId);
+        bytes32[] memory newPriceIds = createPriceIds(3);
+
+        SchedulerState.SubscriptionParams memory newParams = currentParams;
+        newParams.priceIds = newPriceIds;
+
+        // Update the subscription
+        scheduler.updateSubscription(subscriptionId, newParams);
+
+        // 3. Verify priceLastUpdatedAt is reset to 0
+        (, status) = scheduler.getSubscription(subscriptionId);
+        assertEq(
+            status.priceLastUpdatedAt,
+            0,
+            "priceLastUpdatedAt should be reset to 0 after adding new price IDs"
+        );
+
+        // 4. Verify immediate update is possible
+        _verifyImmediateUpdatePossible(subscriptionId);
+    }
+
+    function _verifyImmediateUpdatePossible(uint256 subscriptionId) private {
+        // Create new price feeds for the new price IDs
+        uint64 publishTime2 = SafeCast.toUint64(block.timestamp + 1); // Just 1 second later
+        PythStructs.PriceFeed[] memory priceFeeds;
+        uint64[] memory slots;
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime2, 3); // 3 feeds for new price IDs
+        mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
+        bytes[] memory updateData = createMockUpdateData(priceFeeds);
+
+        // This should succeed even though we haven't waited for heartbeatSeconds
+        // because priceLastUpdatedAt was reset to 0
+        vm.prank(pusher);
+        scheduler.updatePriceFeeds(subscriptionId, updateData);
+
+        // Verify the update was processed
+        (, SchedulerState.SubscriptionStatus memory status) = scheduler
+            .getSubscription(subscriptionId);
+        assertEq(
+            status.priceLastUpdatedAt,
+            publishTime2,
+            "Second update should be processed with new timestamp"
+        );
+
+        // Verify that normal heartbeat criteria apply again for subsequent updates
+        uint64 publishTime3 = SafeCast.toUint64(block.timestamp + 10); // Only 10 seconds later
+        (priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime3, 3);
+        mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
+        updateData = createMockUpdateData(priceFeeds);
+
+        // This should fail because we haven't waited for heartbeatSeconds since the last update
+        vm.expectRevert(
+            abi.encodeWithSelector(UpdateConditionsNotMet.selector)
+        );
+        vm.prank(pusher);
+        scheduler.updatePriceFeeds(subscriptionId, updateData);
+    }
+
     function testcreateSubscriptionWithInsufficientFundsReverts() public {
         uint8 numFeeds = 2;
         SchedulerState.SubscriptionParams