Skip to content

Commit 1acf74b

Browse files
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>
1 parent 58c3523 commit 1acf74b

File tree

2 files changed

+142
-2
lines changed

2 files changed

+142
-2
lines changed

target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,17 @@ abstract contract Scheduler is IScheduler, SchedulerState {
136136
}
137137

138138
// Clear price updates for removed price IDs before updating params
139-
_clearRemovedPriceUpdates(
139+
bool newPriceIdsAdded = _clearRemovedPriceUpdates(
140140
subscriptionId,
141141
currentParams.priceIds,
142142
newParams.priceIds
143143
);
144144

145+
// Reset priceLastUpdatedAt to 0 if new price IDs were added
146+
if (newPriceIdsAdded) {
147+
_state.subscriptionStatuses[subscriptionId].priceLastUpdatedAt = 0;
148+
}
149+
145150
// Update subscription parameters
146151
_state.subscriptionParams[subscriptionId] = newParams;
147152

@@ -216,12 +221,13 @@ abstract contract Scheduler is IScheduler, SchedulerState {
216221
* @param subscriptionId The ID of the subscription being updated.
217222
* @param currentPriceIds The array of price IDs currently associated with the subscription.
218223
* @param newPriceIds The new array of price IDs for the subscription.
224+
* @return newPriceIdsAdded True if any new price IDs were added, false otherwise.
219225
*/
220226
function _clearRemovedPriceUpdates(
221227
uint256 subscriptionId,
222228
bytes32[] storage currentPriceIds,
223229
bytes32[] memory newPriceIds
224-
) internal {
230+
) internal returns (bool newPriceIdsAdded) {
225231
// Iterate through old price IDs
226232
for (uint i = 0; i < currentPriceIds.length; i++) {
227233
bytes32 oldPriceId = currentPriceIds[i];
@@ -240,6 +246,28 @@ abstract contract Scheduler is IScheduler, SchedulerState {
240246
delete _state.priceUpdates[subscriptionId][oldPriceId];
241247
}
242248
}
249+
250+
// Check if any new price IDs were added
251+
for (uint i = 0; i < newPriceIds.length; i++) {
252+
bytes32 newPriceId = newPriceIds[i];
253+
bool found = false;
254+
255+
// Check if the new price ID exists in the current list
256+
for (uint j = 0; j < currentPriceIds.length; j++) {
257+
if (currentPriceIds[j] == newPriceId) {
258+
found = true;
259+
break;
260+
}
261+
}
262+
263+
// If a new price ID was added, mark as changed
264+
if (!found) {
265+
newPriceIdsAdded = true;
266+
break;
267+
}
268+
}
269+
270+
return newPriceIdsAdded;
243271
}
244272

245273
function updatePriceFeeds(

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

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,118 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
332332
);
333333
}
334334

335+
// Helper function to reduce stack depth in testUpdateSubscriptionResetsPriceLastUpdatedAt
336+
function _setupSubscriptionAndFirstUpdate()
337+
private
338+
returns (uint256 subscriptionId, uint64 publishTime)
339+
{
340+
// Setup subscription with heartbeat criteria
341+
uint32 heartbeatSeconds = 60; // 60 second heartbeat
342+
SchedulerState.UpdateCriteria memory criteria = SchedulerState
343+
.UpdateCriteria({
344+
updateOnHeartbeat: true,
345+
heartbeatSeconds: heartbeatSeconds,
346+
updateOnDeviation: false,
347+
deviationThresholdBps: 0
348+
});
349+
350+
subscriptionId = addTestSubscriptionWithUpdateCriteria(
351+
scheduler,
352+
criteria,
353+
address(reader)
354+
);
355+
scheduler.addFunds{value: 1 ether}(subscriptionId);
356+
357+
// Update prices to set priceLastUpdatedAt to a non-zero value
358+
publishTime = SafeCast.toUint64(block.timestamp);
359+
PythStructs.PriceFeed[] memory priceFeeds;
360+
uint64[] memory slots;
361+
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
362+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
363+
bytes[] memory updateData = createMockUpdateData(priceFeeds);
364+
365+
vm.prank(pusher);
366+
scheduler.updatePriceFeeds(subscriptionId, updateData);
367+
368+
return (subscriptionId, publishTime);
369+
}
370+
371+
function testUpdateSubscriptionResetsPriceLastUpdatedAt() public {
372+
// 1. Setup subscription and perform first update
373+
(
374+
uint256 subscriptionId,
375+
uint64 publishTime1
376+
) = _setupSubscriptionAndFirstUpdate();
377+
378+
// Verify priceLastUpdatedAt is set
379+
(, SchedulerState.SubscriptionStatus memory status) = scheduler
380+
.getSubscription(subscriptionId);
381+
assertEq(
382+
status.priceLastUpdatedAt,
383+
publishTime1,
384+
"priceLastUpdatedAt should be set to the first update timestamp"
385+
);
386+
387+
// 2. Update subscription to add price IDs
388+
(SchedulerState.SubscriptionParams memory currentParams, ) = scheduler
389+
.getSubscription(subscriptionId);
390+
bytes32[] memory newPriceIds = createPriceIds(3);
391+
392+
SchedulerState.SubscriptionParams memory newParams = currentParams;
393+
newParams.priceIds = newPriceIds;
394+
395+
// Update the subscription
396+
scheduler.updateSubscription(subscriptionId, newParams);
397+
398+
// 3. Verify priceLastUpdatedAt is reset to 0
399+
(, status) = scheduler.getSubscription(subscriptionId);
400+
assertEq(
401+
status.priceLastUpdatedAt,
402+
0,
403+
"priceLastUpdatedAt should be reset to 0 after adding new price IDs"
404+
);
405+
406+
// 4. Verify immediate update is possible
407+
_verifyImmediateUpdatePossible(subscriptionId);
408+
}
409+
410+
function _verifyImmediateUpdatePossible(uint256 subscriptionId) private {
411+
// Create new price feeds for the new price IDs
412+
uint64 publishTime2 = SafeCast.toUint64(block.timestamp + 1); // Just 1 second later
413+
PythStructs.PriceFeed[] memory priceFeeds;
414+
uint64[] memory slots;
415+
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime2, 3); // 3 feeds for new price IDs
416+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
417+
bytes[] memory updateData = createMockUpdateData(priceFeeds);
418+
419+
// This should succeed even though we haven't waited for heartbeatSeconds
420+
// because priceLastUpdatedAt was reset to 0
421+
vm.prank(pusher);
422+
scheduler.updatePriceFeeds(subscriptionId, updateData);
423+
424+
// Verify the update was processed
425+
(, SchedulerState.SubscriptionStatus memory status) = scheduler
426+
.getSubscription(subscriptionId);
427+
assertEq(
428+
status.priceLastUpdatedAt,
429+
publishTime2,
430+
"Second update should be processed with new timestamp"
431+
);
432+
433+
// Verify that normal heartbeat criteria apply again for subsequent updates
434+
uint64 publishTime3 = SafeCast.toUint64(block.timestamp + 10); // Only 10 seconds later
435+
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime3, 3);
436+
mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
437+
updateData = createMockUpdateData(priceFeeds);
438+
439+
// This should fail because we haven't waited for heartbeatSeconds since the last update
440+
vm.expectRevert(
441+
abi.encodeWithSelector(UpdateConditionsNotMet.selector)
442+
);
443+
vm.prank(pusher);
444+
scheduler.updatePriceFeeds(subscriptionId, updateData);
445+
}
446+
335447
function testcreateSubscriptionWithInsufficientFundsReverts() public {
336448
uint8 numFeeds = 2;
337449
SchedulerState.SubscriptionParams

0 commit comments

Comments
 (0)