@@ -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,27 +282,27 @@ 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 );
304+ status.balanceInWei -= pythFee;
305+ status.totalSpent += pythFee;
301306
302307 // Verify all price feeds have the same Pythnet slot.
303308 // All feeds in a subscription must be updated at the same time.
@@ -312,36 +317,21 @@ abstract contract Scheduler is IScheduler, SchedulerState {
312317 // is more recent than latest stored update's. Reverts if not.
313318 _validateShouldUpdatePrices (subscriptionId, params, status, priceFeeds);
314319
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
320+ // Update status and store the updates
321+ uint256 latestPublishTime = 0 ; // Use the most recent publish time from the validated feeds
334322 for (uint8 i = 0 ; i < priceFeeds.length ; i++ ) {
335- _state.priceUpdates[subscriptionId][ priceFeeds[i].id] = priceFeeds[
336- i
337- ];
323+ if ( priceFeeds[i].price.publishTime > latestPublishTime) {
324+ latestPublishTime = priceFeeds[i].price.publishTime;
325+ }
338326 }
339- status.priceLastUpdatedAt = priceFeeds[ 0 ].price.publishTime ;
340- status.balanceInWei -= pythFee ;
341- status.totalUpdates += 1 ;
342- status.totalSpent += pythFee ;
327+ status.priceLastUpdatedAt = latestPublishTime ;
328+ status.totalUpdates += priceFeeds. length ;
329+
330+ _storePriceUpdates (subscriptionId, priceFeeds) ;
343331
344- emit PricesUpdated (subscriptionId, priceFeeds[0 ].price.publishTime);
332+ _processFeesAndPayKeeper (status, startGas, priceIds.length );
333+
334+ emit PricesUpdated (subscriptionId, latestPublishTime);
345335 }
346336
347337 /**
@@ -737,4 +727,53 @@ abstract contract Scheduler is IScheduler, SchedulerState {
737727 _state.activeSubscriptionIndex[subscriptionId] = 0 ;
738728 }
739729 }
730+
731+ /**
732+ * @notice Internal function to store the parsed price feeds.
733+ * @param subscriptionId The ID of the subscription.
734+ * @param priceFeeds The array of price feeds to store.
735+ */
736+ function _storePriceUpdates (
737+ uint256 subscriptionId ,
738+ PythStructs.PriceFeed[] memory priceFeeds
739+ ) internal {
740+ for (uint8 i = 0 ; i < priceFeeds.length ; i++ ) {
741+ _state.priceUpdates[subscriptionId][priceFeeds[i].id] = priceFeeds[
742+ i
743+ ];
744+ }
745+ }
746+
747+ /**
748+ * @notice Internal function to calculate total fees, deduct from balance, and pay the keeper.
749+ * @dev This function sends funds to `msg.sender`, so be sure that this is being called by a keeper.
750+ * @dev Note that the Pyth fee is already paid in the parsePriceFeedUpdatesWithSlots call.
751+ * @param status Storage reference to the subscription's status.
752+ * @param startGas Gas remaining at the start of the parent function call.
753+ * @param numPriceIds Number of price IDs being updated.
754+ */
755+ function _processFeesAndPayKeeper (
756+ SubscriptionStatus storage status ,
757+ uint256 startGas ,
758+ uint256 numPriceIds
759+ ) internal {
760+ // Calculate fee components
761+ uint256 gasCost = (startGas - gasleft () + GAS_OVERHEAD) * tx .gasprice ;
762+ uint256 keeperSpecificFee = uint256 (_state.singleUpdateKeeperFeeInWei) *
763+ numPriceIds;
764+ uint256 totalKeeperFee = gasCost + keeperSpecificFee;
765+
766+ // Check balance
767+ if (status.balanceInWei < totalKeeperFee) {
768+ revert InsufficientBalance ();
769+ }
770+
771+ // Pay keeper and update status if successful
772+ (bool sent , ) = msg .sender .call {value: totalKeeperFee}("" );
773+ if (! sent) {
774+ revert KeeperPaymentFailed ();
775+ }
776+ status.balanceInWei -= totalKeeperFee;
777+ status.totalSpent += totalKeeperFee;
778+ }
740779}
0 commit comments