| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 |
- // SPDX-License-Identifier: Apache 2
- pragma solidity ^0.8.0;
- import "@openzeppelin/contracts/utils/math/SafeCast.sol";
- import "@openzeppelin/contracts/utils/math/SignedMath.sol";
- import "@openzeppelin/contracts/utils/math/Math.sol";
- import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
- import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
- import "./IScheduler.sol";
- import "./SchedulerState.sol";
- import "./SchedulerErrors.sol";
- abstract contract Scheduler is IScheduler, SchedulerState {
- function _initialize(
- address admin,
- address pythAddress,
- uint128 minimumBalancePerFeed,
- uint128 singleUpdateKeeperFeeInWei
- ) internal {
- require(admin != address(0), "admin is zero address");
- require(pythAddress != address(0), "pyth is zero address");
- _state.pyth = pythAddress;
- _state.admin = admin;
- _state.subscriptionNumber = 1;
- _state.minimumBalancePerFeed = minimumBalancePerFeed;
- _state.singleUpdateKeeperFeeInWei = singleUpdateKeeperFeeInWei;
- }
- function createSubscription(
- SubscriptionParams memory subscriptionParams
- ) external payable override returns (uint256 subscriptionId) {
- // Validate params and set default gas config
- _validateAndPrepareSubscriptionParams(subscriptionParams);
- // Calculate minimum balance required for this subscription
- uint256 minimumBalance = this.getMinimumBalance(
- uint8(subscriptionParams.priceIds.length)
- );
- // Ensure enough funds were provided
- if (msg.value < minimumBalance) {
- revert InsufficientBalance();
- }
- // Set subscription to active
- subscriptionParams.isActive = true;
- subscriptionId = _state.subscriptionNumber++;
- // Store the subscription parameters
- _state.subscriptionParams[subscriptionId] = subscriptionParams;
- // Initialize subscription status
- SubscriptionStatus storage status = _state.subscriptionStatuses[
- subscriptionId
- ];
- status.priceLastUpdatedAt = 0;
- status.balanceInWei = msg.value;
- status.totalUpdates = 0;
- status.totalSpent = 0;
- // Map subscription ID to manager
- _state.subscriptionManager[subscriptionId] = msg.sender;
- _addToActiveSubscriptions(subscriptionId);
- emit SubscriptionCreated(subscriptionId, msg.sender);
- return subscriptionId;
- }
- function updateSubscription(
- uint256 subscriptionId,
- SubscriptionParams memory newParams
- ) external payable override onlyManager(subscriptionId) {
- SubscriptionStatus storage currentStatus = _state.subscriptionStatuses[
- subscriptionId
- ];
- SubscriptionParams storage currentParams = _state.subscriptionParams[
- subscriptionId
- ];
- // Add incoming funds to balance
- currentStatus.balanceInWei += msg.value;
- // Updates to permanent subscriptions are not allowed
- if (currentParams.isPermanent) {
- revert CannotUpdatePermanentSubscription();
- }
- // If subscription is inactive and will remain inactive, no need to validate parameters
- bool wasActive = currentParams.isActive;
- bool willBeActive = newParams.isActive;
- if (!wasActive && !willBeActive) {
- // Update subscription parameters
- _state.subscriptionParams[subscriptionId] = newParams;
- emit SubscriptionUpdated(subscriptionId);
- return;
- }
- // Validate the new parameters, including setting default gas config
- _validateAndPrepareSubscriptionParams(newParams);
- // Check minimum balance if number of feeds increases and subscription remains active
- if (
- willBeActive &&
- newParams.priceIds.length > currentParams.priceIds.length
- ) {
- uint256 minimumBalance = this.getMinimumBalance(
- uint8(newParams.priceIds.length)
- );
- if (currentStatus.balanceInWei < minimumBalance) {
- revert InsufficientBalance();
- }
- }
- // Handle activation/deactivation
- if (!wasActive && willBeActive) {
- // Reactivating a subscription - ensure minimum balance
- uint256 minimumBalance = this.getMinimumBalance(
- uint8(newParams.priceIds.length)
- );
- // Check if balance meets minimum requirement
- if (currentStatus.balanceInWei < minimumBalance) {
- revert InsufficientBalance();
- }
- currentParams.isActive = true;
- _addToActiveSubscriptions(subscriptionId);
- emit SubscriptionActivated(subscriptionId);
- } else if (wasActive && !willBeActive) {
- // Deactivating a subscription
- currentParams.isActive = false;
- _removeFromActiveSubscriptions(subscriptionId);
- emit SubscriptionDeactivated(subscriptionId);
- }
- // Clear price updates for removed price IDs before updating params
- _clearRemovedPriceUpdates(
- subscriptionId,
- currentParams.priceIds,
- newParams.priceIds
- );
- // Update subscription parameters
- _state.subscriptionParams[subscriptionId] = newParams;
- emit SubscriptionUpdated(subscriptionId);
- }
- /**
- * @notice Validates subscription parameters and sets default gas config if needed.
- * @dev This function modifies the passed-in params struct in place for gas config defaults.
- * @param params The subscription parameters to validate and prepare.
- */
- function _validateAndPrepareSubscriptionParams(
- SubscriptionParams memory params
- ) internal pure {
- // No zero‐feed subscriptions
- if (params.priceIds.length == 0) {
- revert EmptyPriceIds();
- }
- // Price ID limits and uniqueness
- if (params.priceIds.length > MAX_PRICE_IDS_PER_SUBSCRIPTION) {
- revert TooManyPriceIds(
- params.priceIds.length,
- MAX_PRICE_IDS_PER_SUBSCRIPTION
- );
- }
- for (uint i = 0; i < params.priceIds.length; i++) {
- for (uint j = i + 1; j < params.priceIds.length; j++) {
- if (params.priceIds[i] == params.priceIds[j]) {
- revert DuplicatePriceId(params.priceIds[i]);
- }
- }
- }
- // Whitelist size limit and uniqueness
- if (params.readerWhitelist.length > MAX_READER_WHITELIST_SIZE) {
- revert TooManyWhitelistedReaders(
- params.readerWhitelist.length,
- MAX_READER_WHITELIST_SIZE
- );
- }
- for (uint i = 0; i < params.readerWhitelist.length; i++) {
- for (uint j = i + 1; j < params.readerWhitelist.length; j++) {
- if (params.readerWhitelist[i] == params.readerWhitelist[j]) {
- revert DuplicateWhitelistAddress(params.readerWhitelist[i]);
- }
- }
- }
- // Validate update criteria
- if (
- !params.updateCriteria.updateOnHeartbeat &&
- !params.updateCriteria.updateOnDeviation
- ) {
- revert InvalidUpdateCriteria();
- }
- if (
- params.updateCriteria.updateOnHeartbeat &&
- params.updateCriteria.heartbeatSeconds == 0
- ) {
- revert InvalidUpdateCriteria();
- }
- if (
- params.updateCriteria.updateOnDeviation &&
- params.updateCriteria.deviationThresholdBps == 0
- ) {
- revert InvalidUpdateCriteria();
- }
- }
- /**
- * @notice Internal helper to clear stored PriceFeed data for price IDs removed from a subscription.
- * @param subscriptionId The ID of the subscription being updated.
- * @param currentPriceIds The array of price IDs currently associated with the subscription.
- * @param newPriceIds The new array of price IDs for the subscription.
- */
- function _clearRemovedPriceUpdates(
- uint256 subscriptionId,
- bytes32[] storage currentPriceIds,
- bytes32[] memory newPriceIds
- ) internal {
- // Iterate through old price IDs
- for (uint i = 0; i < currentPriceIds.length; i++) {
- bytes32 oldPriceId = currentPriceIds[i];
- bool found = false;
- // Check if the old price ID exists in the new list
- for (uint j = 0; j < newPriceIds.length; j++) {
- if (newPriceIds[j] == oldPriceId) {
- found = true;
- break; // Found it, no need to check further
- }
- }
- // If not found in the new list, delete its stored update data
- if (!found) {
- delete _state.priceUpdates[subscriptionId][oldPriceId];
- }
- }
- }
- function updatePriceFeeds(
- uint256 subscriptionId,
- bytes[] calldata updateData,
- bytes32[] calldata priceIds
- ) external override {
- uint256 startGas = gasleft();
- SubscriptionStatus storage status = _state.subscriptionStatuses[
- subscriptionId
- ];
- SubscriptionParams storage params = _state.subscriptionParams[
- subscriptionId
- ];
- if (!params.isActive) {
- 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);
- // If we don't have enough balance, revert
- if (status.balanceInWei < pythFee) {
- revert InsufficientBalance();
- }
- // Parse the price feed updates with an acceptable timestamp range of [-1h, +10s] from now.
- // We will validate the trigger conditions ourselves.
- uint64 curTime = SafeCast.toUint64(block.timestamp);
- (
- PythStructs.PriceFeed[] memory priceFeeds,
- uint64[] memory slots
- ) = pyth.parsePriceFeedUpdatesWithSlots{value: pythFee}(
- updateData,
- priceIds,
- curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
- ? curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
- : 0,
- curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD
- );
- status.balanceInWei -= pythFee;
- status.totalSpent += pythFee;
- // Verify all price feeds have the same Pythnet slot.
- // All feeds in a subscription must be updated at the same time.
- uint64 slot = slots[0];
- for (uint8 i = 1; i < slots.length; i++) {
- if (slots[i] != slot) {
- revert PriceSlotMismatch();
- }
- }
- // Verify that update conditions are met, and that the timestamp
- // is more recent than latest stored update's. Reverts if not.
- _validateShouldUpdatePrices(subscriptionId, params, status, priceFeeds);
- // Update status and store the updates
- uint256 latestPublishTime = 0; // Use the most recent publish time from the validated feeds
- for (uint8 i = 0; i < priceFeeds.length; i++) {
- if (priceFeeds[i].price.publishTime > latestPublishTime) {
- latestPublishTime = priceFeeds[i].price.publishTime;
- }
- }
- status.priceLastUpdatedAt = latestPublishTime;
- status.totalUpdates += priceFeeds.length;
- _storePriceUpdates(subscriptionId, priceFeeds);
- _processFeesAndPayKeeper(status, startGas, priceIds.length);
- emit PricesUpdated(subscriptionId, latestPublishTime);
- }
- /**
- * @notice Validates whether the update trigger criteria is met for a subscription. Reverts if not met.
- * @param subscriptionId The ID of the subscription (needed for reading previous prices).
- * @param params The subscription's parameters struct.
- * @param status The subscription's status struct.
- * @param priceFeeds The array of price feeds to validate.
- */
- function _validateShouldUpdatePrices(
- uint256 subscriptionId,
- SubscriptionParams storage params,
- SubscriptionStatus storage status,
- PythStructs.PriceFeed[] memory priceFeeds
- ) internal view returns (bool) {
- // Use the most recent timestamp, as some asset markets may be closed.
- // Closed markets will have a publishTime from their last trading period.
- // Since we verify all updates share the same Pythnet slot, we still ensure
- // that all price feeds are synchronized from the same update cycle.
- uint256 updateTimestamp = 0;
- for (uint8 i = 0; i < priceFeeds.length; i++) {
- if (priceFeeds[i].price.publishTime > updateTimestamp) {
- updateTimestamp = priceFeeds[i].price.publishTime;
- }
- }
- // Reject updates if they're older than the latest stored ones
- if (
- status.priceLastUpdatedAt > 0 &&
- updateTimestamp <= status.priceLastUpdatedAt
- ) {
- revert TimestampOlderThanLastUpdate(
- updateTimestamp,
- status.priceLastUpdatedAt
- );
- }
- // If updateOnHeartbeat is enabled and the heartbeat interval has passed, trigger update
- if (params.updateCriteria.updateOnHeartbeat) {
- uint256 lastUpdateTime = status.priceLastUpdatedAt;
- if (
- lastUpdateTime == 0 ||
- updateTimestamp >=
- lastUpdateTime + params.updateCriteria.heartbeatSeconds
- ) {
- return true;
- }
- }
- // If updateOnDeviation is enabled, check if any price has deviated enough
- if (params.updateCriteria.updateOnDeviation) {
- for (uint8 i = 0; i < priceFeeds.length; i++) {
- // Get the previous price feed for this price ID using subscriptionId
- PythStructs.PriceFeed storage previousFeed = _state
- .priceUpdates[subscriptionId][priceFeeds[i].id];
- // If there's no previous price, this is the first update
- if (previousFeed.id == bytes32(0)) {
- return true;
- }
- // Calculate the deviation percentage
- int64 currentPrice = priceFeeds[i].price.price;
- int64 previousPrice = previousFeed.price.price;
- // Skip if either price is zero to avoid division by zero
- if (previousPrice == 0 || currentPrice == 0) {
- continue;
- }
- // Calculate absolute deviation basis points (scaled by 1e4)
- uint256 numerator = SignedMath.abs(
- currentPrice - previousPrice
- );
- uint256 denominator = SignedMath.abs(previousPrice);
- uint256 deviationBps = Math.mulDiv(
- numerator,
- 10_000,
- denominator
- );
- // If deviation exceeds threshold, trigger update
- if (
- deviationBps >= params.updateCriteria.deviationThresholdBps
- ) {
- return true;
- }
- }
- }
- revert UpdateConditionsNotMet();
- }
- /// FETCH PRICES
- /**
- * @notice Internal helper function to retrieve price feeds for a subscription.
- * @param subscriptionId The ID of the subscription.
- * @param priceIds The specific price IDs requested, or empty array to get all.
- * @return priceFeeds An array of PriceFeed structs corresponding to the requested IDs.
- */
- function _getPricesInternal(
- uint256 subscriptionId,
- bytes32[] calldata priceIds
- ) internal view returns (PythStructs.PriceFeed[] memory priceFeeds) {
- if (!_state.subscriptionParams[subscriptionId].isActive) {
- revert InactiveSubscription();
- }
- SubscriptionParams storage params = _state.subscriptionParams[
- subscriptionId
- ];
- // If no price IDs provided, return all price feeds for the subscription
- if (priceIds.length == 0) {
- PythStructs.PriceFeed[]
- memory allFeeds = new PythStructs.PriceFeed[](
- params.priceIds.length
- );
- for (uint8 i = 0; i < params.priceIds.length; i++) {
- PythStructs.PriceFeed storage priceFeed = _state.priceUpdates[
- subscriptionId
- ][params.priceIds[i]];
- // Check if the price feed exists (price ID is valid and has been updated)
- if (priceFeed.id == bytes32(0)) {
- revert InvalidPriceId(params.priceIds[i], bytes32(0));
- }
- allFeeds[i] = priceFeed;
- }
- return allFeeds;
- }
- // Return only the requested price feeds
- PythStructs.PriceFeed[]
- memory requestedFeeds = new PythStructs.PriceFeed[](
- priceIds.length
- );
- for (uint8 i = 0; i < priceIds.length; i++) {
- PythStructs.PriceFeed storage priceFeed = _state.priceUpdates[
- subscriptionId
- ][priceIds[i]];
- // Check if the price feed exists (price ID is valid and has been updated)
- if (priceFeed.id == bytes32(0)) {
- revert InvalidPriceId(priceIds[i], bytes32(0));
- }
- requestedFeeds[i] = priceFeed;
- }
- return requestedFeeds;
- }
- function getPricesUnsafe(
- uint256 subscriptionId,
- bytes32[] calldata priceIds
- )
- external
- view
- override
- onlyWhitelistedReader(subscriptionId)
- returns (PythStructs.Price[] memory prices)
- {
- PythStructs.PriceFeed[] memory priceFeeds = _getPricesInternal(
- subscriptionId,
- priceIds
- );
- prices = new PythStructs.Price[](priceFeeds.length);
- for (uint i = 0; i < priceFeeds.length; i++) {
- prices[i] = priceFeeds[i].price;
- }
- return prices;
- }
- function getEmaPriceUnsafe(
- uint256 subscriptionId,
- bytes32[] calldata priceIds
- )
- external
- view
- override
- onlyWhitelistedReader(subscriptionId)
- returns (PythStructs.Price[] memory prices)
- {
- PythStructs.PriceFeed[] memory priceFeeds = _getPricesInternal(
- subscriptionId,
- priceIds
- );
- prices = new PythStructs.Price[](priceFeeds.length);
- for (uint i = 0; i < priceFeeds.length; i++) {
- prices[i] = priceFeeds[i].emaPrice;
- }
- return prices;
- }
- /// BALANCE MANAGEMENT
- function addFunds(uint256 subscriptionId) external payable override {
- if (!_state.subscriptionParams[subscriptionId].isActive) {
- revert InactiveSubscription();
- }
- _state.subscriptionStatuses[subscriptionId].balanceInWei += msg.value;
- }
- function withdrawFunds(
- uint256 subscriptionId,
- uint256 amount
- ) external override onlyManager(subscriptionId) {
- SubscriptionStatus storage status = _state.subscriptionStatuses[
- subscriptionId
- ];
- SubscriptionParams storage params = _state.subscriptionParams[
- subscriptionId
- ];
- // Prevent withdrawals from permanent subscriptions
- if (params.isPermanent) {
- revert CannotUpdatePermanentSubscription();
- }
- if (status.balanceInWei < amount) {
- revert InsufficientBalance();
- }
- // If subscription is active, ensure minimum balance is maintained
- if (params.isActive) {
- uint256 minimumBalance = this.getMinimumBalance(
- uint8(params.priceIds.length)
- );
- if (status.balanceInWei - amount < minimumBalance) {
- revert InsufficientBalance();
- }
- }
- status.balanceInWei -= amount;
- (bool sent, ) = msg.sender.call{value: amount}("");
- require(sent, "Failed to send funds");
- }
- // FETCH SUBSCRIPTIONS
- function getSubscription(
- uint256 subscriptionId
- )
- external
- view
- override
- returns (
- SubscriptionParams memory params,
- SubscriptionStatus memory status
- )
- {
- return (
- _state.subscriptionParams[subscriptionId],
- _state.subscriptionStatuses[subscriptionId]
- );
- }
- // This function is intentionally public with no access control to allow keepers to discover active subscriptions
- function getActiveSubscriptions(
- uint256 startIndex,
- uint256 maxResults
- )
- external
- view
- override
- returns (
- uint256[] memory subscriptionIds,
- SubscriptionParams[] memory subscriptionParams,
- uint256 totalCount
- )
- {
- totalCount = _state.activeSubscriptionIds.length;
- // If startIndex is beyond the total count, return empty arrays
- if (startIndex >= totalCount) {
- return (new uint256[](0), new SubscriptionParams[](0), totalCount);
- }
- // Calculate how many results to return (bounded by maxResults and remaining items)
- uint256 resultCount = totalCount - startIndex;
- if (resultCount > maxResults) {
- resultCount = maxResults;
- }
- // Create arrays for subscription IDs and parameters
- subscriptionIds = new uint256[](resultCount);
- subscriptionParams = new SubscriptionParams[](resultCount);
- // Populate the arrays with the requested page of active subscriptions
- for (uint256 i = 0; i < resultCount; i++) {
- uint256 subscriptionId = _state.activeSubscriptionIds[
- startIndex + i
- ];
- subscriptionIds[i] = subscriptionId;
- subscriptionParams[i] = _state.subscriptionParams[subscriptionId];
- }
- return (subscriptionIds, subscriptionParams, totalCount);
- }
- /**
- * @notice Returns the minimum balance an active subscription of a given size needs to hold.
- * @param numPriceFeeds The number of price feeds in the subscription.
- */
- function getMinimumBalance(
- uint8 numPriceFeeds
- ) external view override returns (uint256 minimumBalanceInWei) {
- // TODO: Consider adding a base minimum balance independent of feed count
- return uint256(numPriceFeeds) * this.getMinimumBalancePerFeed();
- }
- // ACCESS CONTROL MODIFIERS
- modifier onlyManager(uint256 subscriptionId) {
- if (_state.subscriptionManager[subscriptionId] != msg.sender) {
- revert Unauthorized();
- }
- _;
- }
- modifier onlyWhitelistedReader(uint256 subscriptionId) {
- // Manager is always allowed
- if (_state.subscriptionManager[subscriptionId] == msg.sender) {
- _;
- return;
- }
- // If whitelist is not used, allow any reader
- if (!_state.subscriptionParams[subscriptionId].whitelistEnabled) {
- _;
- return;
- }
- // Check if caller is in whitelist
- address[] storage whitelist = _state
- .subscriptionParams[subscriptionId]
- .readerWhitelist;
- bool isWhitelisted = false;
- for (uint i = 0; i < whitelist.length; i++) {
- if (whitelist[i] == msg.sender) {
- isWhitelisted = true;
- break;
- }
- }
- if (!isWhitelisted) {
- revert Unauthorized();
- }
- _;
- }
- /**
- * @notice Adds a subscription to the active subscriptions list.
- * @param subscriptionId The ID of the subscription to add.
- */
- function _addToActiveSubscriptions(uint256 subscriptionId) internal {
- // Only add if not already in the list
- if (_state.activeSubscriptionIndex[subscriptionId] == 0) {
- _state.activeSubscriptionIds.push(subscriptionId);
- // Store the index as 1-based, 0 means not in the list
- _state.activeSubscriptionIndex[subscriptionId] = _state
- .activeSubscriptionIds
- .length;
- }
- }
- /**
- * @notice Removes a subscription from the active subscriptions list.
- * @param subscriptionId The ID of the subscription to remove.
- */
- function _removeFromActiveSubscriptions(uint256 subscriptionId) internal {
- uint256 index = _state.activeSubscriptionIndex[subscriptionId];
- // Only remove if it's in the list
- if (index > 0) {
- // Adjust index to be 0-based instead of 1-based
- index = index - 1;
- // If it's not the last element, move the last element to its position
- if (index < _state.activeSubscriptionIds.length - 1) {
- uint256 lastId = _state.activeSubscriptionIds[
- _state.activeSubscriptionIds.length - 1
- ];
- _state.activeSubscriptionIds[index] = lastId;
- _state.activeSubscriptionIndex[lastId] = index + 1; // 1-based index
- }
- // Remove the last element
- _state.activeSubscriptionIds.pop();
- _state.activeSubscriptionIndex[subscriptionId] = 0;
- }
- }
- /**
- * @notice Internal function to store the parsed price feeds.
- * @param subscriptionId The ID of the subscription.
- * @param priceFeeds The array of price feeds to store.
- */
- function _storePriceUpdates(
- uint256 subscriptionId,
- PythStructs.PriceFeed[] memory priceFeeds
- ) internal {
- for (uint8 i = 0; i < priceFeeds.length; i++) {
- _state.priceUpdates[subscriptionId][priceFeeds[i].id] = priceFeeds[
- i
- ];
- }
- }
- /**
- * @notice Internal function to calculate total fees, deduct from balance, and pay the keeper.
- * @dev This function sends funds to `msg.sender`, so be sure that this is being called by a keeper.
- * @dev Note that the Pyth fee is already paid in the parsePriceFeedUpdatesWithSlots call.
- * @param status Storage reference to the subscription's status.
- * @param startGas Gas remaining at the start of the parent function call.
- * @param numPriceIds Number of price IDs being updated.
- */
- function _processFeesAndPayKeeper(
- SubscriptionStatus storage status,
- uint256 startGas,
- uint256 numPriceIds
- ) internal {
- // Calculate fee components
- uint256 gasCost = (startGas - gasleft() + GAS_OVERHEAD) * tx.gasprice;
- uint256 keeperSpecificFee = uint256(_state.singleUpdateKeeperFeeInWei) *
- numPriceIds;
- uint256 totalKeeperFee = gasCost + keeperSpecificFee;
- // Check balance
- if (status.balanceInWei < totalKeeperFee) {
- revert InsufficientBalance();
- }
- status.balanceInWei -= totalKeeperFee;
- status.totalSpent += totalKeeperFee;
- // Pay keeper and update status
- (bool sent, ) = msg.sender.call{value: totalKeeperFee}("");
- if (!sent) {
- revert KeeperPaymentFailed();
- }
- }
- }
|