@@ -250,6 +250,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
250250 bytes [] calldata updateData ,
251251 bytes32 [] calldata priceIds
252252 ) external override {
253+ uint256 startGas = gasleft ();
254+
253255 SubscriptionStatus storage status = _state.subscriptionStatuses[
254256 subscriptionId
255257 ];
@@ -261,9 +263,12 @@ abstract contract Scheduler is IScheduler, SchedulerState {
261263 revert InactiveSubscription ();
262264 }
263265
264- // Verify price IDs match subscription
266+ // Verify price IDs match subscription length
265267 if (priceIds.length != params.priceIds.length ) {
266- revert InvalidPriceIdsLength (priceIds[0 ], params.priceIds[0 ]);
268+ revert InvalidPriceIdsLength (
269+ priceIds.length ,
270+ params.priceIds.length
271+ );
267272 }
268273
269274 // Keepers must provide priceIds in the exact same order as defined in the subscription
@@ -277,26 +282,24 @@ abstract contract Scheduler is IScheduler, SchedulerState {
277282 IPyth pyth = IPyth (_state.pyth);
278283 uint256 pythFee = pyth.getUpdateFee (updateData);
279284
280- // Check if subscription has enough balance
285+ // If we don't have enough balance, revert
281286 if (status.balanceInWei < pythFee) {
282287 revert InsufficientBalance ();
283288 }
284289
285290 // Parse the price feed updates with an acceptable timestamp range of [-1h, +10s] from now.
286291 // We will validate the trigger conditions ourselves.
287292 uint64 curTime = SafeCast.toUint64 (block .timestamp );
288- uint64 maxPublishTime = curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD;
289- uint64 minPublishTime = curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
290- ? curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
291- : 0 ;
292293 (
293294 PythStructs.PriceFeed[] memory priceFeeds ,
294295 uint64 [] memory slots
295296 ) = pyth.parsePriceFeedUpdatesWithSlots {value: pythFee}(
296297 updateData,
297298 priceIds,
298- minPublishTime,
299- maxPublishTime
299+ curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
300+ ? curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
301+ : 0 ,
302+ curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD
300303 );
301304
302305 // Verify all price feeds have the same Pythnet slot.
@@ -312,36 +315,21 @@ abstract contract Scheduler is IScheduler, SchedulerState {
312315 // is more recent than latest stored update's. Reverts if not.
313316 _validateShouldUpdatePrices (subscriptionId, params, status, priceFeeds);
314317
315- // Store the price updates, update status, and emit event
316- _storePriceUpdatesAndStatus (
317- subscriptionId,
318- status,
319- priceFeeds,
320- pythFee
321- );
322- }
323-
324- /**
325- * @notice Stores the price updates, updates subscription status, and emits event.
326- */
327- function _storePriceUpdatesAndStatus (
328- uint256 subscriptionId ,
329- SubscriptionStatus storage status ,
330- PythStructs.PriceFeed[] memory priceFeeds ,
331- uint256 pythFee
332- ) internal {
333- // Store the price updates
318+ // Update status and store the updates
319+ uint256 latestPublishTime = 0 ; // Use the most recent publish time from the validated feeds
334320 for (uint8 i = 0 ; i < priceFeeds.length ; i++ ) {
335- _state.priceUpdates[subscriptionId][ priceFeeds[i].id] = priceFeeds[
336- i
337- ];
321+ if ( priceFeeds[i].price.publishTime > latestPublishTime) {
322+ latestPublishTime = priceFeeds[i].price.publishTime;
323+ }
338324 }
339- status.priceLastUpdatedAt = priceFeeds[0 ].price.publishTime;
340- status.balanceInWei -= pythFee;
341- status.totalUpdates += 1 ;
342- status.totalSpent += pythFee;
325+ status.priceLastUpdatedAt = latestPublishTime;
326+ status.totalUpdates += priceFeeds.length ;
343327
344- emit PricesUpdated (subscriptionId, priceFeeds[0 ].price.publishTime);
328+ _storePriceUpdates (subscriptionId, priceFeeds);
329+
330+ _processFeesAndPayKeeper (status, startGas, priceIds.length , pythFee);
331+
332+ emit PricesUpdated (subscriptionId, latestPublishTime);
345333 }
346334
347335 /**
@@ -737,4 +725,55 @@ abstract contract Scheduler is IScheduler, SchedulerState {
737725 _state.activeSubscriptionIndex[subscriptionId] = 0 ;
738726 }
739727 }
728+
729+ /**
730+ * @notice Internal function to store the parsed price feeds.
731+ * @param subscriptionId The ID of the subscription.
732+ * @param priceFeeds The array of price feeds to store.
733+ */
734+ function _storePriceUpdates (
735+ uint256 subscriptionId ,
736+ PythStructs.PriceFeed[] memory priceFeeds
737+ ) internal {
738+ for (uint8 i = 0 ; i < priceFeeds.length ; i++ ) {
739+ _state.priceUpdates[subscriptionId][priceFeeds[i].id] = priceFeeds[
740+ i
741+ ];
742+ }
743+ }
744+
745+ /**
746+ * @notice Internal function to calculate total fees, deduct from balance, and pay the keeper.
747+ * @dev This function sends funds to `msg.sender`, so be sure that this is being called by a keeper.
748+ * @param status Storage reference to the subscription's status.
749+ * @param startGas Gas remaining at the start of the parent function call.
750+ * @param numPriceIds Number of price IDs being updated.
751+ * @param pythFee Fee paid to Pyth for the update.
752+ */
753+ function _processFeesAndPayKeeper (
754+ SubscriptionStatus storage status ,
755+ uint256 startGas ,
756+ uint256 numPriceIds ,
757+ uint256 pythFee
758+ ) internal {
759+ // Calculate fee components
760+ uint256 gasCost = (startGas - gasleft () + GAS_OVERHEAD) * tx .gasprice ;
761+ uint256 keeperSpecificFee = uint256 (_state.singleUpdateKeeperFeeInWei) *
762+ numPriceIds;
763+ uint256 totalKeeperFee = gasCost + keeperSpecificFee;
764+ uint256 totalFee = totalKeeperFee + pythFee; // pythFee is already paid in the parsePriceFeedUpdatesWithSlots call
765+
766+ // Check balance
767+ if (status.balanceInWei < totalFee) {
768+ revert InsufficientBalance ();
769+ }
770+
771+ // Update status and pay keeper
772+ status.balanceInWei -= totalFee;
773+ status.totalSpent += totalFee;
774+ (bool sent , ) = msg .sender .call {value: totalKeeperFee}("" ); // Pay only the keeper portion
775+ if (! sent) {
776+ revert KeeperPaymentFailed ();
777+ }
778+ }
740779}
0 commit comments