diff --git a/target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol b/target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol index 4a5cbca6a3..73a1b7be76 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol @@ -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. diff --git a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol index 9c4f419d4d..cc178efaac 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol @@ -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(); @@ -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); @@ -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, @@ -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); } diff --git a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol index 288de918af..b32f05b206 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol @@ -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( @@ -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 @@ -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 @@ -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 @@ -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() @@ -1073,7 +1073,6 @@ 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; @@ -1081,7 +1080,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { 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) @@ -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() @@ -1121,7 +1120,6 @@ 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; @@ -1129,7 +1127,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { 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) @@ -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 { @@ -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; @@ -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 @@ -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 { @@ -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 @@ -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() @@ -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); @@ -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); @@ -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); @@ -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)); @@ -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); diff --git a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol index 835e8cd2c4..4bbd2f93df 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol @@ -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(); @@ -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; @@ -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; }