Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ interface IScheduler is SchedulerEvents {

/**
* @notice Updates price feeds for a subscription.
* @dev Internally, this verifies the updateData using the Pyth contract and validates update conditions.
* @dev The updateData must contain all price feeds for the subscription, not a subset or superset.
* @dev Internally, the updateData is verified using the Pyth contract and validates update conditions.
* The call will only succeed if the update conditions for the subscription are met.
* @param subscriptionId The ID of the subscription
* @param updateData The price update data from Pyth
* @param priceIds The IDs of the price feeds to update
*/
function updatePriceFeeds(
uint256 subscriptionId,
bytes[] calldata updateData,
bytes32[] calldata priceIds
bytes[] calldata updateData
) external;

/** @notice Returns the price of a price feed without any sanity checks.
Expand Down
22 changes: 3 additions & 19 deletions target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {

function updatePriceFeeds(
uint256 subscriptionId,
bytes[] calldata updateData,
bytes32[] calldata priceIds
bytes[] calldata updateData
) external override {
uint256 startGas = gasleft();

Expand All @@ -263,21 +262,6 @@ abstract contract Scheduler is IScheduler, SchedulerState {
revert InactiveSubscription();
}

// Verify price IDs match subscription length
if (priceIds.length != params.priceIds.length) {
revert InvalidPriceIdsLength(
priceIds.length,
params.priceIds.length
);
}

// Keepers must provide priceIds in the exact same order as defined in the subscription
for (uint8 i = 0; i < priceIds.length; i++) {
if (priceIds[i] != params.priceIds[i]) {
revert InvalidPriceId(priceIds[i], params.priceIds[i]);
}
}

// Get the Pyth contract and parse price updates
IPyth pyth = IPyth(_state.pyth);
uint256 pythFee = pyth.getUpdateFee(updateData);
Expand All @@ -295,7 +279,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
uint64[] memory slots
) = pyth.parsePriceFeedUpdatesWithSlots{value: pythFee}(
updateData,
priceIds,
params.priceIds,
curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
? curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
: 0,
Expand Down Expand Up @@ -329,7 +313,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {

_storePriceUpdates(subscriptionId, priceFeeds);

_processFeesAndPayKeeper(status, startGas, priceIds.length);
_processFeesAndPayKeeper(status, startGas, params.priceIds.length);

emit PricesUpdated(subscriptionId, latestPublishTime);
}
Expand Down
39 changes: 17 additions & 22 deletions target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
bytes[] memory updateData = createMockUpdateData(initialPriceFeeds);

vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, initialPriceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);

// Verify initial state: All 3 feeds should be readable
assertTrue(
Expand Down Expand Up @@ -830,7 +830,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
emit PricesUpdated(subscriptionId, publishTime1);
vm.prank(pusher);

scheduler.updatePriceFeeds(subscriptionId, updateData1, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData1);

// Verify first update
(, SchedulerState.SubscriptionStatus memory status1) = scheduler
Expand Down Expand Up @@ -881,7 +881,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
emit PricesUpdated(subscriptionId, publishTime2);
vm.prank(pusher);

scheduler.updatePriceFeeds(subscriptionId, updateData2, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData2);

// Verify second update
(, SchedulerState.SubscriptionStatus memory status2) = scheduler
Expand Down Expand Up @@ -948,7 +948,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {

// Perform update
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, params.priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);

// Get state after
(, SchedulerState.SubscriptionStatus memory statusAfter) = scheduler
Expand Down Expand Up @@ -1050,7 +1050,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
// Expect revert due to insufficient balance for total fee
vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);
}

function testUpdatePriceFeedsRevertsOnHeartbeatUpdateConditionNotMet()
Expand All @@ -1073,15 +1073,14 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
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;
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);
scheduler.updatePriceFeeds(subscriptionId, updateData1);

// Prepare second update within heartbeat interval
vm.warp(block.timestamp + 30); // Advance time by 30 seconds (less than 60)
Expand All @@ -1097,7 +1096,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
abi.encodeWithSelector(UpdateConditionsNotMet.selector)
);
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData2, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData2);
}

function testUpdatePriceFeedsRevertsOnDeviationUpdateConditionNotMet()
Expand All @@ -1121,15 +1120,14 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
scheduler.addFunds{value: fundAmount}(subscriptionId);

// First update to set initial price
bytes32[] memory priceIds = createPriceIds();
uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
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);
scheduler.updatePriceFeeds(subscriptionId, updateData1);

// Prepare second update with price deviation less than threshold (e.g., 50 bps)
vm.warp(block.timestamp + 1000); // Advance time significantly (doesn't matter for deviation)
Expand Down Expand Up @@ -1160,7 +1158,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
abi.encodeWithSelector(UpdateConditionsNotMet.selector)
);
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData2, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData2);
}

function testUpdatePriceFeedsRevertsOnOlderTimestamp() public {
Expand All @@ -1173,7 +1171,6 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
scheduler.addFunds{value: fundAmount}(subscriptionId);

// First update to establish last updated timestamp
bytes32[] memory priceIds = createPriceIds();
uint64 publishTime1 = SafeCast.toUint64(block.timestamp);
PythStructs.PriceFeed[] memory priceFeeds1;
uint64[] memory slots1;
Expand All @@ -1182,7 +1179,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
bytes[] memory updateData1 = createMockUpdateData(priceFeeds1);

vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData1, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData1);

// Prepare second update with an older timestamp
uint64 publishTime2 = publishTime1 - 10; // Timestamp older than the first update
Expand All @@ -1204,7 +1201,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {

// Attempt to update price feeds
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData2, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData2);
}

function testUpdatePriceFeedsRevertsOnMismatchedSlots() public {
Expand All @@ -1217,7 +1214,6 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
scheduler.addFunds{value: fundAmount}(subscriptionId);

// Create two price feeds with same timestamp but different slots
bytes32[] memory priceIds = createPriceIds(2);
uint64 publishTime = SafeCast.toUint64(block.timestamp);
PythStructs.PriceFeed[] memory priceFeeds = new PythStructs.PriceFeed[](
2
Expand All @@ -1239,7 +1235,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {

// Attempt to update price feeds
vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);
}

function testUpdateSubscriptionEnforcesMinimumBalanceOnAddingFeeds()
Expand Down Expand Up @@ -1350,7 +1346,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);

// Get all latest prices (empty priceIds array)
bytes32[] memory emptyPriceIds = new bytes32[](0);
Expand Down Expand Up @@ -1392,7 +1388,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);

// Get only the first price feed
bytes32[] memory selectedPriceIds = new bytes32[](1);
Expand Down Expand Up @@ -1445,10 +1441,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
(priceFeeds, slots) = createMockPriceFeedsWithSlots(publishTime, 2);
mockParsePriceFeedUpdatesWithSlots(pyth, priceFeeds, slots);
bytes[] memory updateData = createMockUpdateData(priceFeeds);
bytes32[] memory priceIds = params.priceIds;

vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);

// Try to access from a non-whitelisted address (should succeed)
address randomUser = address(0xdead);
Expand Down Expand Up @@ -1492,7 +1487,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);

// Try to access from a non-whitelisted address (should fail)
vm.startPrank(address(0xdead));
Expand Down Expand Up @@ -1559,7 +1554,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
bytes[] memory updateData = createMockUpdateData(priceFeeds);

vm.prank(pusher);
scheduler.updatePriceFeeds(subscriptionId, updateData, priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);

// Get EMA prices
bytes32[] memory emptyPriceIds = new bytes32[](0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
uint256 startGas = gasleft();
scheduler.updatePriceFeeds(
subscriptionId,
createMockUpdateData(newPriceFeeds),
params.priceIds
createMockUpdateData(newPriceFeeds)
);
uint256 updateGasUsed = startGas - gasleft();

Expand Down Expand Up @@ -114,10 +113,6 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
address(manager)
);

// Fetch the price IDs
(SchedulerState.SubscriptionParams memory params, ) = scheduler
.getSubscription(subscriptionId);

// Create initial price feed updates
uint64 publishTime = SafeCast.toUint64(block.timestamp);
PythStructs.PriceFeed[] memory priceFeeds;
Expand All @@ -133,7 +128,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {

// Update the price feeds. We should have enough balance to cover the update
// because we funded the subscription with the minimum balance during creation.
scheduler.updatePriceFeeds(subscriptionId, updateData, params.priceIds);
scheduler.updatePriceFeeds(subscriptionId, updateData);
return subscriptionId;
}

Expand Down
Loading