Scheduler.sol 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "@openzeppelin/contracts/utils/math/SafeCast.sol";
  4. import "@openzeppelin/contracts/utils/math/SignedMath.sol";
  5. import "@openzeppelin/contracts/utils/math/Math.sol";
  6. import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
  7. import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
  8. import "./IScheduler.sol";
  9. import "./SchedulerState.sol";
  10. import "./SchedulerErrors.sol";
  11. abstract contract Scheduler is IScheduler, SchedulerState {
  12. function _initialize(
  13. address admin,
  14. address pythAddress,
  15. uint128 minimumBalancePerFeed,
  16. uint128 singleUpdateKeeperFeeInWei
  17. ) internal {
  18. require(admin != address(0), "admin is zero address");
  19. require(pythAddress != address(0), "pyth is zero address");
  20. _state.pyth = pythAddress;
  21. _state.admin = admin;
  22. _state.subscriptionNumber = 1;
  23. _state.minimumBalancePerFeed = minimumBalancePerFeed;
  24. _state.singleUpdateKeeperFeeInWei = singleUpdateKeeperFeeInWei;
  25. }
  26. function createSubscription(
  27. SubscriptionParams memory subscriptionParams
  28. ) external payable override returns (uint256 subscriptionId) {
  29. // Validate params and set default gas config
  30. _validateAndPrepareSubscriptionParams(subscriptionParams);
  31. // Calculate minimum balance required for this subscription
  32. uint256 minimumBalance = this.getMinimumBalance(
  33. uint8(subscriptionParams.priceIds.length)
  34. );
  35. // Ensure enough funds were provided
  36. if (msg.value < minimumBalance) {
  37. revert InsufficientBalance();
  38. }
  39. // Set subscription to active
  40. subscriptionParams.isActive = true;
  41. subscriptionId = _state.subscriptionNumber++;
  42. // Store the subscription parameters
  43. _state.subscriptionParams[subscriptionId] = subscriptionParams;
  44. // Initialize subscription status
  45. SubscriptionStatus storage status = _state.subscriptionStatuses[
  46. subscriptionId
  47. ];
  48. status.priceLastUpdatedAt = 0;
  49. status.balanceInWei = msg.value;
  50. status.totalUpdates = 0;
  51. status.totalSpent = 0;
  52. // Map subscription ID to manager
  53. _state.subscriptionManager[subscriptionId] = msg.sender;
  54. _addToActiveSubscriptions(subscriptionId);
  55. emit SubscriptionCreated(subscriptionId, msg.sender);
  56. return subscriptionId;
  57. }
  58. function updateSubscription(
  59. uint256 subscriptionId,
  60. SubscriptionParams memory newParams
  61. ) external payable override onlyManager(subscriptionId) {
  62. SubscriptionStatus storage currentStatus = _state.subscriptionStatuses[
  63. subscriptionId
  64. ];
  65. SubscriptionParams storage currentParams = _state.subscriptionParams[
  66. subscriptionId
  67. ];
  68. // Add incoming funds to balance
  69. currentStatus.balanceInWei += msg.value;
  70. // Updates to permanent subscriptions are not allowed
  71. if (currentParams.isPermanent) {
  72. revert CannotUpdatePermanentSubscription();
  73. }
  74. // If subscription is inactive and will remain inactive, no need to validate parameters
  75. bool wasActive = currentParams.isActive;
  76. bool willBeActive = newParams.isActive;
  77. if (!wasActive && !willBeActive) {
  78. // Update subscription parameters
  79. _state.subscriptionParams[subscriptionId] = newParams;
  80. emit SubscriptionUpdated(subscriptionId);
  81. return;
  82. }
  83. // Validate the new parameters, including setting default gas config
  84. _validateAndPrepareSubscriptionParams(newParams);
  85. // Check minimum balance if number of feeds increases and subscription remains active
  86. if (
  87. willBeActive &&
  88. newParams.priceIds.length > currentParams.priceIds.length
  89. ) {
  90. uint256 minimumBalance = this.getMinimumBalance(
  91. uint8(newParams.priceIds.length)
  92. );
  93. if (currentStatus.balanceInWei < minimumBalance) {
  94. revert InsufficientBalance();
  95. }
  96. }
  97. // Handle activation/deactivation
  98. if (!wasActive && willBeActive) {
  99. // Reactivating a subscription - ensure minimum balance
  100. uint256 minimumBalance = this.getMinimumBalance(
  101. uint8(newParams.priceIds.length)
  102. );
  103. // Check if balance meets minimum requirement
  104. if (currentStatus.balanceInWei < minimumBalance) {
  105. revert InsufficientBalance();
  106. }
  107. currentParams.isActive = true;
  108. _addToActiveSubscriptions(subscriptionId);
  109. emit SubscriptionActivated(subscriptionId);
  110. } else if (wasActive && !willBeActive) {
  111. // Deactivating a subscription
  112. currentParams.isActive = false;
  113. _removeFromActiveSubscriptions(subscriptionId);
  114. emit SubscriptionDeactivated(subscriptionId);
  115. }
  116. // Clear price updates for removed price IDs before updating params
  117. _clearRemovedPriceUpdates(
  118. subscriptionId,
  119. currentParams.priceIds,
  120. newParams.priceIds
  121. );
  122. // Update subscription parameters
  123. _state.subscriptionParams[subscriptionId] = newParams;
  124. emit SubscriptionUpdated(subscriptionId);
  125. }
  126. /**
  127. * @notice Validates subscription parameters and sets default gas config if needed.
  128. * @dev This function modifies the passed-in params struct in place for gas config defaults.
  129. * @param params The subscription parameters to validate and prepare.
  130. */
  131. function _validateAndPrepareSubscriptionParams(
  132. SubscriptionParams memory params
  133. ) internal pure {
  134. // No zero‐feed subscriptions
  135. if (params.priceIds.length == 0) {
  136. revert EmptyPriceIds();
  137. }
  138. // Price ID limits and uniqueness
  139. if (params.priceIds.length > MAX_PRICE_IDS_PER_SUBSCRIPTION) {
  140. revert TooManyPriceIds(
  141. params.priceIds.length,
  142. MAX_PRICE_IDS_PER_SUBSCRIPTION
  143. );
  144. }
  145. for (uint i = 0; i < params.priceIds.length; i++) {
  146. for (uint j = i + 1; j < params.priceIds.length; j++) {
  147. if (params.priceIds[i] == params.priceIds[j]) {
  148. revert DuplicatePriceId(params.priceIds[i]);
  149. }
  150. }
  151. }
  152. // Whitelist size limit and uniqueness
  153. if (params.readerWhitelist.length > MAX_READER_WHITELIST_SIZE) {
  154. revert TooManyWhitelistedReaders(
  155. params.readerWhitelist.length,
  156. MAX_READER_WHITELIST_SIZE
  157. );
  158. }
  159. for (uint i = 0; i < params.readerWhitelist.length; i++) {
  160. for (uint j = i + 1; j < params.readerWhitelist.length; j++) {
  161. if (params.readerWhitelist[i] == params.readerWhitelist[j]) {
  162. revert DuplicateWhitelistAddress(params.readerWhitelist[i]);
  163. }
  164. }
  165. }
  166. // Validate update criteria
  167. if (
  168. !params.updateCriteria.updateOnHeartbeat &&
  169. !params.updateCriteria.updateOnDeviation
  170. ) {
  171. revert InvalidUpdateCriteria();
  172. }
  173. if (
  174. params.updateCriteria.updateOnHeartbeat &&
  175. params.updateCriteria.heartbeatSeconds == 0
  176. ) {
  177. revert InvalidUpdateCriteria();
  178. }
  179. if (
  180. params.updateCriteria.updateOnDeviation &&
  181. params.updateCriteria.deviationThresholdBps == 0
  182. ) {
  183. revert InvalidUpdateCriteria();
  184. }
  185. }
  186. /**
  187. * @notice Internal helper to clear stored PriceFeed data for price IDs removed from a subscription.
  188. * @param subscriptionId The ID of the subscription being updated.
  189. * @param currentPriceIds The array of price IDs currently associated with the subscription.
  190. * @param newPriceIds The new array of price IDs for the subscription.
  191. */
  192. function _clearRemovedPriceUpdates(
  193. uint256 subscriptionId,
  194. bytes32[] storage currentPriceIds,
  195. bytes32[] memory newPriceIds
  196. ) internal {
  197. // Iterate through old price IDs
  198. for (uint i = 0; i < currentPriceIds.length; i++) {
  199. bytes32 oldPriceId = currentPriceIds[i];
  200. bool found = false;
  201. // Check if the old price ID exists in the new list
  202. for (uint j = 0; j < newPriceIds.length; j++) {
  203. if (newPriceIds[j] == oldPriceId) {
  204. found = true;
  205. break; // Found it, no need to check further
  206. }
  207. }
  208. // If not found in the new list, delete its stored update data
  209. if (!found) {
  210. delete _state.priceUpdates[subscriptionId][oldPriceId];
  211. }
  212. }
  213. }
  214. function updatePriceFeeds(
  215. uint256 subscriptionId,
  216. bytes[] calldata updateData,
  217. bytes32[] calldata priceIds
  218. ) external override {
  219. uint256 startGas = gasleft();
  220. SubscriptionStatus storage status = _state.subscriptionStatuses[
  221. subscriptionId
  222. ];
  223. SubscriptionParams storage params = _state.subscriptionParams[
  224. subscriptionId
  225. ];
  226. if (!params.isActive) {
  227. revert InactiveSubscription();
  228. }
  229. // Verify price IDs match subscription length
  230. if (priceIds.length != params.priceIds.length) {
  231. revert InvalidPriceIdsLength(
  232. priceIds.length,
  233. params.priceIds.length
  234. );
  235. }
  236. // Keepers must provide priceIds in the exact same order as defined in the subscription
  237. for (uint8 i = 0; i < priceIds.length; i++) {
  238. if (priceIds[i] != params.priceIds[i]) {
  239. revert InvalidPriceId(priceIds[i], params.priceIds[i]);
  240. }
  241. }
  242. // Get the Pyth contract and parse price updates
  243. IPyth pyth = IPyth(_state.pyth);
  244. uint256 pythFee = pyth.getUpdateFee(updateData);
  245. // If we don't have enough balance, revert
  246. if (status.balanceInWei < pythFee) {
  247. revert InsufficientBalance();
  248. }
  249. // Parse the price feed updates with an acceptable timestamp range of [-1h, +10s] from now.
  250. // We will validate the trigger conditions ourselves.
  251. uint64 curTime = SafeCast.toUint64(block.timestamp);
  252. (
  253. PythStructs.PriceFeed[] memory priceFeeds,
  254. uint64[] memory slots
  255. ) = pyth.parsePriceFeedUpdatesWithSlots{value: pythFee}(
  256. updateData,
  257. priceIds,
  258. curTime > PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
  259. ? curTime - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD
  260. : 0,
  261. curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD
  262. );
  263. status.balanceInWei -= pythFee;
  264. status.totalSpent += pythFee;
  265. // Verify all price feeds have the same Pythnet slot.
  266. // All feeds in a subscription must be updated at the same time.
  267. uint64 slot = slots[0];
  268. for (uint8 i = 1; i < slots.length; i++) {
  269. if (slots[i] != slot) {
  270. revert PriceSlotMismatch();
  271. }
  272. }
  273. // Verify that update conditions are met, and that the timestamp
  274. // is more recent than latest stored update's. Reverts if not.
  275. _validateShouldUpdatePrices(subscriptionId, params, status, priceFeeds);
  276. // Update status and store the updates
  277. uint256 latestPublishTime = 0; // Use the most recent publish time from the validated feeds
  278. for (uint8 i = 0; i < priceFeeds.length; i++) {
  279. if (priceFeeds[i].price.publishTime > latestPublishTime) {
  280. latestPublishTime = priceFeeds[i].price.publishTime;
  281. }
  282. }
  283. status.priceLastUpdatedAt = latestPublishTime;
  284. status.totalUpdates += priceFeeds.length;
  285. _storePriceUpdates(subscriptionId, priceFeeds);
  286. _processFeesAndPayKeeper(status, startGas, priceIds.length);
  287. emit PricesUpdated(subscriptionId, latestPublishTime);
  288. }
  289. /**
  290. * @notice Validates whether the update trigger criteria is met for a subscription. Reverts if not met.
  291. * @param subscriptionId The ID of the subscription (needed for reading previous prices).
  292. * @param params The subscription's parameters struct.
  293. * @param status The subscription's status struct.
  294. * @param priceFeeds The array of price feeds to validate.
  295. */
  296. function _validateShouldUpdatePrices(
  297. uint256 subscriptionId,
  298. SubscriptionParams storage params,
  299. SubscriptionStatus storage status,
  300. PythStructs.PriceFeed[] memory priceFeeds
  301. ) internal view returns (bool) {
  302. // Use the most recent timestamp, as some asset markets may be closed.
  303. // Closed markets will have a publishTime from their last trading period.
  304. // Since we verify all updates share the same Pythnet slot, we still ensure
  305. // that all price feeds are synchronized from the same update cycle.
  306. uint256 updateTimestamp = 0;
  307. for (uint8 i = 0; i < priceFeeds.length; i++) {
  308. if (priceFeeds[i].price.publishTime > updateTimestamp) {
  309. updateTimestamp = priceFeeds[i].price.publishTime;
  310. }
  311. }
  312. // Reject updates if they're older than the latest stored ones
  313. if (
  314. status.priceLastUpdatedAt > 0 &&
  315. updateTimestamp <= status.priceLastUpdatedAt
  316. ) {
  317. revert TimestampOlderThanLastUpdate(
  318. updateTimestamp,
  319. status.priceLastUpdatedAt
  320. );
  321. }
  322. // If updateOnHeartbeat is enabled and the heartbeat interval has passed, trigger update
  323. if (params.updateCriteria.updateOnHeartbeat) {
  324. uint256 lastUpdateTime = status.priceLastUpdatedAt;
  325. if (
  326. lastUpdateTime == 0 ||
  327. updateTimestamp >=
  328. lastUpdateTime + params.updateCriteria.heartbeatSeconds
  329. ) {
  330. return true;
  331. }
  332. }
  333. // If updateOnDeviation is enabled, check if any price has deviated enough
  334. if (params.updateCriteria.updateOnDeviation) {
  335. for (uint8 i = 0; i < priceFeeds.length; i++) {
  336. // Get the previous price feed for this price ID using subscriptionId
  337. PythStructs.PriceFeed storage previousFeed = _state
  338. .priceUpdates[subscriptionId][priceFeeds[i].id];
  339. // If there's no previous price, this is the first update
  340. if (previousFeed.id == bytes32(0)) {
  341. return true;
  342. }
  343. // Calculate the deviation percentage
  344. int64 currentPrice = priceFeeds[i].price.price;
  345. int64 previousPrice = previousFeed.price.price;
  346. // Skip if either price is zero to avoid division by zero
  347. if (previousPrice == 0 || currentPrice == 0) {
  348. continue;
  349. }
  350. // Calculate absolute deviation basis points (scaled by 1e4)
  351. uint256 numerator = SignedMath.abs(
  352. currentPrice - previousPrice
  353. );
  354. uint256 denominator = SignedMath.abs(previousPrice);
  355. uint256 deviationBps = Math.mulDiv(
  356. numerator,
  357. 10_000,
  358. denominator
  359. );
  360. // If deviation exceeds threshold, trigger update
  361. if (
  362. deviationBps >= params.updateCriteria.deviationThresholdBps
  363. ) {
  364. return true;
  365. }
  366. }
  367. }
  368. revert UpdateConditionsNotMet();
  369. }
  370. /// FETCH PRICES
  371. /**
  372. * @notice Internal helper function to retrieve price feeds for a subscription.
  373. * @param subscriptionId The ID of the subscription.
  374. * @param priceIds The specific price IDs requested, or empty array to get all.
  375. * @return priceFeeds An array of PriceFeed structs corresponding to the requested IDs.
  376. */
  377. function _getPricesInternal(
  378. uint256 subscriptionId,
  379. bytes32[] calldata priceIds
  380. ) internal view returns (PythStructs.PriceFeed[] memory priceFeeds) {
  381. if (!_state.subscriptionParams[subscriptionId].isActive) {
  382. revert InactiveSubscription();
  383. }
  384. SubscriptionParams storage params = _state.subscriptionParams[
  385. subscriptionId
  386. ];
  387. // If no price IDs provided, return all price feeds for the subscription
  388. if (priceIds.length == 0) {
  389. PythStructs.PriceFeed[]
  390. memory allFeeds = new PythStructs.PriceFeed[](
  391. params.priceIds.length
  392. );
  393. for (uint8 i = 0; i < params.priceIds.length; i++) {
  394. PythStructs.PriceFeed storage priceFeed = _state.priceUpdates[
  395. subscriptionId
  396. ][params.priceIds[i]];
  397. // Check if the price feed exists (price ID is valid and has been updated)
  398. if (priceFeed.id == bytes32(0)) {
  399. revert InvalidPriceId(params.priceIds[i], bytes32(0));
  400. }
  401. allFeeds[i] = priceFeed;
  402. }
  403. return allFeeds;
  404. }
  405. // Return only the requested price feeds
  406. PythStructs.PriceFeed[]
  407. memory requestedFeeds = new PythStructs.PriceFeed[](
  408. priceIds.length
  409. );
  410. for (uint8 i = 0; i < priceIds.length; i++) {
  411. PythStructs.PriceFeed storage priceFeed = _state.priceUpdates[
  412. subscriptionId
  413. ][priceIds[i]];
  414. // Check if the price feed exists (price ID is valid and has been updated)
  415. if (priceFeed.id == bytes32(0)) {
  416. revert InvalidPriceId(priceIds[i], bytes32(0));
  417. }
  418. requestedFeeds[i] = priceFeed;
  419. }
  420. return requestedFeeds;
  421. }
  422. function getPricesUnsafe(
  423. uint256 subscriptionId,
  424. bytes32[] calldata priceIds
  425. )
  426. external
  427. view
  428. override
  429. onlyWhitelistedReader(subscriptionId)
  430. returns (PythStructs.Price[] memory prices)
  431. {
  432. PythStructs.PriceFeed[] memory priceFeeds = _getPricesInternal(
  433. subscriptionId,
  434. priceIds
  435. );
  436. prices = new PythStructs.Price[](priceFeeds.length);
  437. for (uint i = 0; i < priceFeeds.length; i++) {
  438. prices[i] = priceFeeds[i].price;
  439. }
  440. return prices;
  441. }
  442. function getEmaPriceUnsafe(
  443. uint256 subscriptionId,
  444. bytes32[] calldata priceIds
  445. )
  446. external
  447. view
  448. override
  449. onlyWhitelistedReader(subscriptionId)
  450. returns (PythStructs.Price[] memory prices)
  451. {
  452. PythStructs.PriceFeed[] memory priceFeeds = _getPricesInternal(
  453. subscriptionId,
  454. priceIds
  455. );
  456. prices = new PythStructs.Price[](priceFeeds.length);
  457. for (uint i = 0; i < priceFeeds.length; i++) {
  458. prices[i] = priceFeeds[i].emaPrice;
  459. }
  460. return prices;
  461. }
  462. /// BALANCE MANAGEMENT
  463. function addFunds(uint256 subscriptionId) external payable override {
  464. if (!_state.subscriptionParams[subscriptionId].isActive) {
  465. revert InactiveSubscription();
  466. }
  467. _state.subscriptionStatuses[subscriptionId].balanceInWei += msg.value;
  468. }
  469. function withdrawFunds(
  470. uint256 subscriptionId,
  471. uint256 amount
  472. ) external override onlyManager(subscriptionId) {
  473. SubscriptionStatus storage status = _state.subscriptionStatuses[
  474. subscriptionId
  475. ];
  476. SubscriptionParams storage params = _state.subscriptionParams[
  477. subscriptionId
  478. ];
  479. // Prevent withdrawals from permanent subscriptions
  480. if (params.isPermanent) {
  481. revert CannotUpdatePermanentSubscription();
  482. }
  483. if (status.balanceInWei < amount) {
  484. revert InsufficientBalance();
  485. }
  486. // If subscription is active, ensure minimum balance is maintained
  487. if (params.isActive) {
  488. uint256 minimumBalance = this.getMinimumBalance(
  489. uint8(params.priceIds.length)
  490. );
  491. if (status.balanceInWei - amount < minimumBalance) {
  492. revert InsufficientBalance();
  493. }
  494. }
  495. status.balanceInWei -= amount;
  496. (bool sent, ) = msg.sender.call{value: amount}("");
  497. require(sent, "Failed to send funds");
  498. }
  499. // FETCH SUBSCRIPTIONS
  500. function getSubscription(
  501. uint256 subscriptionId
  502. )
  503. external
  504. view
  505. override
  506. returns (
  507. SubscriptionParams memory params,
  508. SubscriptionStatus memory status
  509. )
  510. {
  511. return (
  512. _state.subscriptionParams[subscriptionId],
  513. _state.subscriptionStatuses[subscriptionId]
  514. );
  515. }
  516. // This function is intentionally public with no access control to allow keepers to discover active subscriptions
  517. function getActiveSubscriptions(
  518. uint256 startIndex,
  519. uint256 maxResults
  520. )
  521. external
  522. view
  523. override
  524. returns (
  525. uint256[] memory subscriptionIds,
  526. SubscriptionParams[] memory subscriptionParams,
  527. uint256 totalCount
  528. )
  529. {
  530. totalCount = _state.activeSubscriptionIds.length;
  531. // If startIndex is beyond the total count, return empty arrays
  532. if (startIndex >= totalCount) {
  533. return (new uint256[](0), new SubscriptionParams[](0), totalCount);
  534. }
  535. // Calculate how many results to return (bounded by maxResults and remaining items)
  536. uint256 resultCount = totalCount - startIndex;
  537. if (resultCount > maxResults) {
  538. resultCount = maxResults;
  539. }
  540. // Create arrays for subscription IDs and parameters
  541. subscriptionIds = new uint256[](resultCount);
  542. subscriptionParams = new SubscriptionParams[](resultCount);
  543. // Populate the arrays with the requested page of active subscriptions
  544. for (uint256 i = 0; i < resultCount; i++) {
  545. uint256 subscriptionId = _state.activeSubscriptionIds[
  546. startIndex + i
  547. ];
  548. subscriptionIds[i] = subscriptionId;
  549. subscriptionParams[i] = _state.subscriptionParams[subscriptionId];
  550. }
  551. return (subscriptionIds, subscriptionParams, totalCount);
  552. }
  553. /**
  554. * @notice Returns the minimum balance an active subscription of a given size needs to hold.
  555. * @param numPriceFeeds The number of price feeds in the subscription.
  556. */
  557. function getMinimumBalance(
  558. uint8 numPriceFeeds
  559. ) external view override returns (uint256 minimumBalanceInWei) {
  560. // TODO: Consider adding a base minimum balance independent of feed count
  561. return uint256(numPriceFeeds) * this.getMinimumBalancePerFeed();
  562. }
  563. // ACCESS CONTROL MODIFIERS
  564. modifier onlyManager(uint256 subscriptionId) {
  565. if (_state.subscriptionManager[subscriptionId] != msg.sender) {
  566. revert Unauthorized();
  567. }
  568. _;
  569. }
  570. modifier onlyWhitelistedReader(uint256 subscriptionId) {
  571. // Manager is always allowed
  572. if (_state.subscriptionManager[subscriptionId] == msg.sender) {
  573. _;
  574. return;
  575. }
  576. // If whitelist is not used, allow any reader
  577. if (!_state.subscriptionParams[subscriptionId].whitelistEnabled) {
  578. _;
  579. return;
  580. }
  581. // Check if caller is in whitelist
  582. address[] storage whitelist = _state
  583. .subscriptionParams[subscriptionId]
  584. .readerWhitelist;
  585. bool isWhitelisted = false;
  586. for (uint i = 0; i < whitelist.length; i++) {
  587. if (whitelist[i] == msg.sender) {
  588. isWhitelisted = true;
  589. break;
  590. }
  591. }
  592. if (!isWhitelisted) {
  593. revert Unauthorized();
  594. }
  595. _;
  596. }
  597. /**
  598. * @notice Adds a subscription to the active subscriptions list.
  599. * @param subscriptionId The ID of the subscription to add.
  600. */
  601. function _addToActiveSubscriptions(uint256 subscriptionId) internal {
  602. // Only add if not already in the list
  603. if (_state.activeSubscriptionIndex[subscriptionId] == 0) {
  604. _state.activeSubscriptionIds.push(subscriptionId);
  605. // Store the index as 1-based, 0 means not in the list
  606. _state.activeSubscriptionIndex[subscriptionId] = _state
  607. .activeSubscriptionIds
  608. .length;
  609. }
  610. }
  611. /**
  612. * @notice Removes a subscription from the active subscriptions list.
  613. * @param subscriptionId The ID of the subscription to remove.
  614. */
  615. function _removeFromActiveSubscriptions(uint256 subscriptionId) internal {
  616. uint256 index = _state.activeSubscriptionIndex[subscriptionId];
  617. // Only remove if it's in the list
  618. if (index > 0) {
  619. // Adjust index to be 0-based instead of 1-based
  620. index = index - 1;
  621. // If it's not the last element, move the last element to its position
  622. if (index < _state.activeSubscriptionIds.length - 1) {
  623. uint256 lastId = _state.activeSubscriptionIds[
  624. _state.activeSubscriptionIds.length - 1
  625. ];
  626. _state.activeSubscriptionIds[index] = lastId;
  627. _state.activeSubscriptionIndex[lastId] = index + 1; // 1-based index
  628. }
  629. // Remove the last element
  630. _state.activeSubscriptionIds.pop();
  631. _state.activeSubscriptionIndex[subscriptionId] = 0;
  632. }
  633. }
  634. /**
  635. * @notice Internal function to store the parsed price feeds.
  636. * @param subscriptionId The ID of the subscription.
  637. * @param priceFeeds The array of price feeds to store.
  638. */
  639. function _storePriceUpdates(
  640. uint256 subscriptionId,
  641. PythStructs.PriceFeed[] memory priceFeeds
  642. ) internal {
  643. for (uint8 i = 0; i < priceFeeds.length; i++) {
  644. _state.priceUpdates[subscriptionId][priceFeeds[i].id] = priceFeeds[
  645. i
  646. ];
  647. }
  648. }
  649. /**
  650. * @notice Internal function to calculate total fees, deduct from balance, and pay the keeper.
  651. * @dev This function sends funds to `msg.sender`, so be sure that this is being called by a keeper.
  652. * @dev Note that the Pyth fee is already paid in the parsePriceFeedUpdatesWithSlots call.
  653. * @param status Storage reference to the subscription's status.
  654. * @param startGas Gas remaining at the start of the parent function call.
  655. * @param numPriceIds Number of price IDs being updated.
  656. */
  657. function _processFeesAndPayKeeper(
  658. SubscriptionStatus storage status,
  659. uint256 startGas,
  660. uint256 numPriceIds
  661. ) internal {
  662. // Calculate fee components
  663. uint256 gasCost = (startGas - gasleft() + GAS_OVERHEAD) * tx.gasprice;
  664. uint256 keeperSpecificFee = uint256(_state.singleUpdateKeeperFeeInWei) *
  665. numPriceIds;
  666. uint256 totalKeeperFee = gasCost + keeperSpecificFee;
  667. // Check balance
  668. if (status.balanceInWei < totalKeeperFee) {
  669. revert InsufficientBalance();
  670. }
  671. status.balanceInWei -= totalKeeperFee;
  672. status.totalSpent += totalKeeperFee;
  673. // Pay keeper and update status
  674. (bool sent, ) = msg.sender.call{value: totalKeeperFee}("");
  675. if (!sent) {
  676. revert KeeperPaymentFailed();
  677. }
  678. }
  679. }