diff --git a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol index 4ee6730070..f894a4b7f8 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol @@ -2117,4 +2117,128 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Required to receive ETH when withdrawing funds receive() external payable {} + + function testUpdateSubscriptionRemovesPriceUpdatesForRemovedPriceIds() + public + { + // 1. Setup: Add subscription with 3 price feeds, update prices + uint8 numInitialFeeds = 3; + uint256 subscriptionId = addTestSubscriptionWithFeeds( + scheduler, + numInitialFeeds, + address(reader) + ); + scheduler.addFunds{value: 1 ether}(subscriptionId); + + // Get initial price IDs and create mock price feeds + bytes32[] memory initialPriceIds = createPriceIds(numInitialFeeds); + uint64 publishTime = SafeCast.toUint64(block.timestamp); + + // Setup and perform initial price update + ( + PythStructs.PriceFeed[] memory priceFeeds, + uint64[] memory slots + ) = createMockPriceFeedsWithSlots(publishTime, numInitialFeeds); + mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots); + + vm.prank(pusher); + scheduler.updatePriceFeeds( + subscriptionId, + createMockUpdateData(priceFeeds) + ); + + // Store the removed price ID for later use + bytes32 removedPriceId = initialPriceIds[numInitialFeeds - 1]; + + // 2. Action: Update subscription to remove the last price feed + (SchedulerState.SubscriptionParams memory params, ) = scheduler + .getSubscription(subscriptionId); + + // Create new price IDs array without the last ID + bytes32[] memory newPriceIds = new bytes32[](numInitialFeeds - 1); + for (uint i = 0; i < newPriceIds.length; i++) { + newPriceIds[i] = initialPriceIds[i]; + } + + params.priceIds = newPriceIds; + + vm.expectEmit(); + emit SubscriptionUpdated(subscriptionId); + scheduler.updateSubscription(subscriptionId, params); + + // 3. Verification: + // - Verify that the removed price ID is no longer part of the subscription's price IDs + (SchedulerState.SubscriptionParams memory updatedParams, ) = scheduler + .getSubscription(subscriptionId); + assertEq( + updatedParams.priceIds.length, + numInitialFeeds - 1, + "Subscription should have one less price ID" + ); + + bool removedPriceIdFound = false; + for (uint i = 0; i < updatedParams.priceIds.length; i++) { + if (updatedParams.priceIds[i] == removedPriceId) { + removedPriceIdFound = true; + break; + } + } + assertFalse( + removedPriceIdFound, + "Removed price ID should not be in the subscription's price IDs" + ); + + // - Querying all feeds should return only the remaining feeds + PythStructs.Price[] memory allPricesAfterUpdate = scheduler + .getPricesUnsafe(subscriptionId, new bytes32[](0)); + assertEq( + allPricesAfterUpdate.length, + newPriceIds.length, + "Querying all should only return remaining feeds" + ); + + // - Verify that trying to get the price of the removed feed directly reverts + bytes32[] memory removedIdArray = new bytes32[](1); + removedIdArray[0] = removedPriceId; + vm.expectRevert( + abi.encodeWithSelector( + InvalidPriceId.selector, + removedPriceId, + bytes32(0) + ) + ); + scheduler.getPricesUnsafe(subscriptionId, removedIdArray); + } + + function testUpdateSubscriptionRevertsWithTooManyPriceIds() public { + // 1. Setup: Create a subscription with a valid number of price IDs + uint8 initialNumFeeds = 2; + uint256 subscriptionId = addTestSubscriptionWithFeeds( + scheduler, + initialNumFeeds, + address(reader) + ); + + // 2. Prepare params with too many price IDs (MAX_PRICE_IDS_PER_SUBSCRIPTION + 1) + (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler + .getSubscription(subscriptionId); + + uint16 tooManyFeeds = uint16( + scheduler.MAX_PRICE_IDS_PER_SUBSCRIPTION() + ) + 1; + bytes32[] memory tooManyPriceIds = createPriceIds(tooManyFeeds); + + SchedulerState.SubscriptionParams memory newParams = currentParams; + newParams.priceIds = tooManyPriceIds; + + // 3. Expect revert when trying to update with too many price IDs + vm.expectRevert( + abi.encodeWithSelector( + TooManyPriceIds.selector, + tooManyFeeds, + scheduler.MAX_PRICE_IDS_PER_SUBSCRIPTION() + ) + ); + scheduler.updateSubscription(subscriptionId, newParams); + } }