Skip to content

Commit 7642a39

Browse files
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
1 parent 31e93f0 commit 7642a39

File tree

18 files changed

+775
-205
lines changed

18 files changed

+775
-205
lines changed

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -239,17 +239,21 @@ abstract contract Scheduler is IScheduler, SchedulerState {
239239
// Parse price feed updates with an expected timestamp range of [-10s, now]
240240
// We will validate the trigger conditions and timestamps ourselves
241241
// using the returned PriceFeeds.
242-
uint64 maxPublishTime = SafeCast.toUint64(block.timestamp);
243-
uint64 minPublishTime = maxPublishTime - 10 seconds;
244-
PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
242+
uint64 curTime = SafeCast.toUint64(block.timestamp);
243+
uint64 maxPublishTime = curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD;
244+
uint64 minPublishTime = curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD;
245+
PythStructs.PriceFeed[] memory priceFeeds;
246+
uint64[] memory slots;
247+
(priceFeeds, slots) = pyth.parsePriceFeedUpdatesWithSlots{
245248
value: pythFee
246249
}(updateData, priceIds, minPublishTime, maxPublishTime);
247250

248-
// Verify all price feeds have the same timestamp
249-
uint256 timestamp = priceFeeds[0].price.publishTime;
250-
for (uint8 i = 1; i < priceFeeds.length; i++) {
251-
if (priceFeeds[i].price.publishTime != timestamp) {
252-
revert PriceTimestampMismatch();
251+
// Verify all price feeds have the same Pythnet slot.
252+
// All feeds in a subscription must be updated at the same time.
253+
uint64 slot = slots[0];
254+
for (uint8 i = 1; i < slots.length; i++) {
255+
if (slots[i] != slot) {
256+
revert PriceSlotMismatch();
253257
}
254258
}
255259

@@ -291,7 +295,6 @@ abstract contract Scheduler is IScheduler, SchedulerState {
291295

292296
/**
293297
* @notice Validates whether the update trigger criteria is met for a subscription. Reverts if not met.
294-
* @dev This function assumes that all updates in priceFeeds have the same timestamp. The caller is expected to enforce this invariant.
295298
* @param subscriptionId The ID of the subscription (needed for reading previous prices).
296299
* @param params The subscription's parameters struct.
297300
* @param status The subscription's status struct.
@@ -303,9 +306,16 @@ abstract contract Scheduler is IScheduler, SchedulerState {
303306
SubscriptionStatus storage status,
304307
PythStructs.PriceFeed[] memory priceFeeds
305308
) internal view returns (bool) {
306-
// SECURITY NOTE: this check assumes that all updates in priceFeeds have the same timestamp.
307-
// The caller is expected to enforce this invariant.
308-
uint256 updateTimestamp = priceFeeds[0].price.publishTime;
309+
// Use the most recent timestamp, as some asset markets may be closed.
310+
// Closed markets will have a publishTime from their last trading period.
311+
// Since we verify all updates share the same Pythnet slot, we still ensure
312+
// that all price feeds are synchronized from the same update cycle.
313+
uint256 updateTimestamp = 0;
314+
for (uint8 i = 0; i < priceFeeds.length; i++) {
315+
if (priceFeeds[i].price.publishTime > updateTimestamp) {
316+
updateTimestamp = priceFeeds[i].price.publishTime;
317+
}
318+
}
309319

310320
// Reject updates if they're older than the latest stored ones
311321
if (

target_chains/ethereum/contracts/contracts/pulse/scheduler/SchedulerErrors.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ error InvalidPriceId(bytes32 providedPriceId, bytes32 expectedPriceId);
99
error InvalidPriceIdsLength(bytes32 providedLength, bytes32 expectedLength);
1010
error InvalidUpdateCriteria();
1111
error InvalidGasConfig();
12-
error PriceTimestampMismatch();
12+
error PriceSlotMismatch();
1313
error TooManyPriceIds(uint256 provided, uint256 maximum);
1414
error UpdateConditionsNotMet();
1515
error TimestampOlderThanLastUpdate(

target_chains/ethereum/contracts/contracts/pulse/scheduler/SchedulerState.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ contract SchedulerState {
1212
/// Default max fee multiplier
1313
uint32 public constant DEFAULT_MAX_PRIORITY_FEE_MULTIPLIER_CAP_PCT = 10_000;
1414

15+
// TODO: make these updateable via governance
16+
/// Maximum time in the past (relative to current block timestamp)
17+
/// for which a price update timestamp is considered valid
18+
uint64 public constant PAST_TIMESTAMP_MAX_VALIDITY_PERIOD = 1 hours;
19+
/// Maximum time in the future (relative to current block timestamp)
20+
/// for which a price update timestamp is considered valid
21+
uint64 public constant FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD = 10 seconds;
22+
1523
struct State {
1624
/// Monotonically increasing counter for subscription IDs
1725
uint256 subscriptionNumber;

0 commit comments

Comments
 (0)