diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f6053ae7e..6ff625065c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2516,6 +2516,9 @@ importers: '@pythnetwork/entropy-sdk-solidity': specifier: workspace:* version: link:../entropy_sdk/solidity + '@pythnetwork/pulse-sdk-solidity': + specifier: workspace:* + version: link:../pulse_sdk/solidity '@pythnetwork/pyth-sdk-solidity': specifier: workspace:* version: link:../sdk/solidity @@ -2620,6 +2623,22 @@ importers: specifier: 'catalog:' version: 1.4.2(prettier@3.5.3) + target_chains/ethereum/pulse_sdk/solidity: + dependencies: + '@pythnetwork/pyth-sdk-solidity': + specifier: workspace:* + version: link:../../sdk/solidity + devDependencies: + abi_generator: + specifier: workspace:* + version: link:../../abi_generator + prettier: + specifier: 'catalog:' + version: 3.5.3 + prettier-plugin-solidity: + specifier: 'catalog:' + version: 1.4.2(prettier@3.5.3) + target_chains/ethereum/sdk/js: dependencies: '@pythnetwork/price-service-client': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 090b5ff9f8..0705708581 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,6 +26,7 @@ packages: - target_chains/ethereum/contracts - target_chains/ethereum/abi_generator - target_chains/ethereum/entropy_sdk/solidity + - target_chains/ethereum/pulse_sdk/solidity - target_chains/ethereum/sdk/js - target_chains/ethereum/sdk/solidity - target_chains/ethereum/sdk/stylus/pyth-mock-solidity diff --git a/target_chains/ethereum/abi_generator/src/index.js b/target_chains/ethereum/abi_generator/src/index.js index 524e715ebc..de5caadc4f 100644 --- a/target_chains/ethereum/abi_generator/src/index.js +++ b/target_chains/ethereum/abi_generator/src/index.js @@ -25,6 +25,10 @@ function generateAbi(contracts) { sources, settings: { outputSelection, + remappings: [ + // Needed for @pythnetwork/pulse-sdk-solidity since it depends on @pythnetwork/pyth-sdk-solidity + "@pythnetwork/=./node_modules/@pythnetwork/", + ], }, }; @@ -42,9 +46,28 @@ function generateAbi(contracts) { fs.mkdirSync("abis"); } + // Report compilation failures + if (output.errors) { + // We can still generate ABIs with warnings, only throw for errors + const errors = output.errors.filter((e) => e.severity === "error"); + if (errors.length > 0) { + console.error("Compilation errors:"); + for (const error of errors) { + console.error(error.formattedMessage || error.message); + } + throw new Error("Compilation failed due to errors"); + } + } + for (let contract of contracts) { const contractFile = `${contract}.sol`; + if (!output.contracts[contractFile]) { + throw new Error(`Unable to produce ABI for ${contractFile}.`); + } + if (!output.contracts[contractFile][contract]) { + throw new Error(`Unable to produce ABI for ${contractFile}:${contract}.`); + } const abi = output.contracts[contractFile][contract].abi; fs.writeFileSync( `abis/${contract}.json`, diff --git a/target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol b/target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol deleted file mode 100644 index 73a1b7be76..0000000000 --- a/target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: Apache 2 - -pragma solidity ^0.8.0; - -import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; -import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; -import "./SchedulerEvents.sol"; -import "./SchedulerState.sol"; - -interface IScheduler is SchedulerEvents { - /** - * @notice Creates a new subscription - * @dev Requires msg.value to be at least the minimum balance for the subscription (calculated by getMinimumBalance()). - * @param subscriptionParams The parameters for the subscription - * @return subscriptionId The ID of the newly created subscription - */ - function createSubscription( - SchedulerState.SubscriptionParams calldata subscriptionParams - ) external payable returns (uint256 subscriptionId); - - /** - * @notice Gets a subscription's parameters and status - * @param subscriptionId The ID of the subscription - * @return params The subscription parameters - * @return status The subscription status - */ - function getSubscription( - uint256 subscriptionId - ) - external - view - returns ( - SchedulerState.SubscriptionParams memory params, - SchedulerState.SubscriptionStatus memory status - ); - - /** - * @notice Updates an existing subscription - * @dev You can activate or deactivate a subscription by setting isActive to true or false. Reactivating a subscription - * requires the subscription to hold at least the minimum balance (calculated by getMinimumBalance()). - * @dev Any Ether sent with this call (`msg.value`) will be added to the subscription's balance before processing the update. - * @param subscriptionId The ID of the subscription to update - * @param newSubscriptionParams The new parameters for the subscription - */ - function updateSubscription( - uint256 subscriptionId, - SchedulerState.SubscriptionParams calldata newSubscriptionParams - ) external payable; - - /** - * @notice Updates price feeds for a subscription. - * @dev The updateData must contain all price feeds for the subscription, not a subset or superset. - * @dev Internally, the updateData is verified using the Pyth contract and validates update conditions. - * The call will only succeed if the update conditions for the subscription are met. - * @param subscriptionId The ID of the subscription - * @param updateData The price update data from Pyth - */ - function updatePriceFeeds( - uint256 subscriptionId, - bytes[] calldata updateData - ) external; - - /** @notice Returns the price of a price feed without any sanity checks. - * @dev This function returns the most recent price update in this contract without any recency checks. - * This function is unsafe as the returned price update may be arbitrarily far in the past. - * - * Users of this function should check the `publishTime` in the price to ensure that the returned price is - * sufficiently recent for their application. If you are considering using this function, it may be - * safer / easier to use `getPriceNoOlderThan`. - * @return prices - please read the documentation of PythStructs.Price to understand how to use this safely. - */ - function getPricesUnsafe( - uint256 subscriptionId, - bytes32[] calldata priceIds - ) external view returns (PythStructs.Price[] memory prices); - - /** - * @notice Returns the exponentially-weighted moving average price of a price feed without any sanity checks. - * @dev This function returns the same price as `getEmaPrice` in the case where the price is available. - * However, if the price is not recent this function returns the latest available price. - * - * The returned price can be from arbitrarily far in the past; this function makes no guarantees that - * the returned price is recent or useful for any particular application. - * - * Users of this function should check the `publishTime` in the price to ensure that the returned price is - * sufficiently recent for their application. If you are considering using this function, it may be - * safer / easier to use either `getEmaPrice` or `getEmaPriceNoOlderThan`. - * @return price - please read the documentation of PythStructs.Price to understand how to use this safely. - */ - function getEmaPriceUnsafe( - uint256 subscriptionId, - bytes32[] calldata priceIds - ) external view returns (PythStructs.Price[] memory price); - - /** - * @notice Adds funds to a subscription's balance - * @param subscriptionId The ID of the subscription - */ - function addFunds(uint256 subscriptionId) external payable; - - /** - * @notice Withdraws funds from a subscription's balance. - * @dev A minimum balance must be maintained for active subscriptions. To withdraw past - * the minimum balance limit, deactivate the subscription first. - * @param subscriptionId The ID of the subscription - * @param amount The amount to withdraw - */ - function withdrawFunds(uint256 subscriptionId, uint256 amount) external; - - /** - * @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 returns (uint256 minimumBalanceInWei); - - /** - * @notice Gets all active subscriptions with their parameters, paginated. - * @dev This function has no access control to allow keepers to discover active subscriptions. - * @dev Note that the order of subscription IDs returned may not be sequential and can change - * when subscriptions are deactivated or reactivated. - * @param startIndex The starting index within the list of active subscriptions (NOT the subscription ID). - * @param maxResults The maximum number of results to return starting from startIndex. - * @return subscriptionIds Array of active subscription IDs - * @return subscriptionParams Array of subscription parameters for each active subscription - * @return totalCount Total number of active subscriptions - */ - function getActiveSubscriptions( - uint256 startIndex, - uint256 maxResults - ) - external - view - returns ( - uint256[] memory subscriptionIds, - SchedulerState.SubscriptionParams[] memory subscriptionParams, - uint256 totalCount - ); -} diff --git a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol index ce6a1d3dc1..eb3c53e6e2 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol @@ -6,11 +6,13 @@ 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 "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol"; +import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerConstants.sol"; import "./SchedulerState.sol"; -import "./SchedulerErrors.sol"; -abstract contract Scheduler is IScheduler, SchedulerState { +abstract contract Scheduler is IScheduler, SchedulerState, SchedulerConstants { function _initialize( address admin, address pythAddress, @@ -28,7 +30,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { } function createSubscription( - SubscriptionParams memory subscriptionParams + SchedulerStructs.SubscriptionParams memory subscriptionParams ) external payable override returns (uint256 subscriptionId) { _validateSubscriptionParams(subscriptionParams); @@ -39,12 +41,12 @@ abstract contract Scheduler is IScheduler, SchedulerState { // Ensure enough funds were provided if (msg.value < minimumBalance) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } // Check deposit limit for permanent subscriptions if (subscriptionParams.isPermanent && msg.value > MAX_DEPOSIT_LIMIT) { - revert MaxDepositLimitExceeded(); + revert SchedulerErrors.MaxDepositLimitExceeded(); } // Set subscription to active @@ -56,9 +58,8 @@ abstract contract Scheduler is IScheduler, SchedulerState { _state.subscriptionParams[subscriptionId] = subscriptionParams; // Initialize subscription status - SubscriptionStatus storage status = _state.subscriptionStatuses[ - subscriptionId - ]; + SchedulerStructs.SubscriptionStatus storage status = _state + .subscriptionStatuses[subscriptionId]; status.priceLastUpdatedAt = 0; status.balanceInWei = msg.value; status.totalUpdates = 0; @@ -75,21 +76,19 @@ abstract contract Scheduler is IScheduler, SchedulerState { function updateSubscription( uint256 subscriptionId, - SubscriptionParams memory newParams + SchedulerStructs.SubscriptionParams memory newParams ) external payable override onlyManager(subscriptionId) { - SubscriptionStatus storage currentStatus = _state.subscriptionStatuses[ - subscriptionId - ]; - SubscriptionParams storage currentParams = _state.subscriptionParams[ - subscriptionId - ]; + SchedulerStructs.SubscriptionStatus storage currentStatus = _state + .subscriptionStatuses[subscriptionId]; + SchedulerStructs.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(); + revert SchedulerErrors.CannotUpdatePermanentSubscription(); } // If subscription is inactive and will remain inactive, no need to validate parameters @@ -109,7 +108,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { uint8(newParams.priceIds.length) ); if (currentStatus.balanceInWei < minimumBalance) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } } @@ -122,7 +121,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { // Check if balance meets minimum requirement if (currentStatus.balanceInWei < minimumBalance) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } currentParams.isActive = true; @@ -153,36 +152,37 @@ abstract contract Scheduler is IScheduler, SchedulerState { emit SubscriptionUpdated(subscriptionId); } - /** - * @notice Validates subscription parameters. - * @param params The subscription parameters to validate. - */ + /// @notice Validates subscription parameters. + /// @param params The subscription parameters to validate. function _validateSubscriptionParams( - SubscriptionParams memory params + SchedulerStructs.SubscriptionParams memory params ) internal pure { // No zero‐feed subscriptions if (params.priceIds.length == 0) { - revert EmptyPriceIds(); + revert SchedulerErrors.EmptyPriceIds(); } // Price ID limits and uniqueness - if (params.priceIds.length > MAX_PRICE_IDS_PER_SUBSCRIPTION) { - revert TooManyPriceIds( + if ( + params.priceIds.length > + SchedulerConstants.MAX_PRICE_IDS_PER_SUBSCRIPTION + ) { + revert SchedulerErrors.TooManyPriceIds( params.priceIds.length, - MAX_PRICE_IDS_PER_SUBSCRIPTION + SchedulerConstants.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]); + revert SchedulerErrors.DuplicatePriceId(params.priceIds[i]); } } } // Whitelist size limit and uniqueness if (params.readerWhitelist.length > MAX_READER_WHITELIST_SIZE) { - revert TooManyWhitelistedReaders( + revert SchedulerErrors.TooManyWhitelistedReaders( params.readerWhitelist.length, MAX_READER_WHITELIST_SIZE ); @@ -190,7 +190,9 @@ abstract contract Scheduler is IScheduler, SchedulerState { 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]); + revert SchedulerErrors.DuplicateWhitelistAddress( + params.readerWhitelist[i] + ); } } } @@ -200,19 +202,19 @@ abstract contract Scheduler is IScheduler, SchedulerState { !params.updateCriteria.updateOnHeartbeat && !params.updateCriteria.updateOnDeviation ) { - revert InvalidUpdateCriteria(); + revert SchedulerErrors.InvalidUpdateCriteria(); } if ( params.updateCriteria.updateOnHeartbeat && params.updateCriteria.heartbeatSeconds == 0 ) { - revert InvalidUpdateCriteria(); + revert SchedulerErrors.InvalidUpdateCriteria(); } if ( params.updateCriteria.updateOnDeviation && params.updateCriteria.deviationThresholdBps == 0 ) { - revert InvalidUpdateCriteria(); + revert SchedulerErrors.InvalidUpdateCriteria(); } } @@ -276,15 +278,13 @@ abstract contract Scheduler is IScheduler, SchedulerState { ) external override { uint256 startGas = gasleft(); - SubscriptionStatus storage status = _state.subscriptionStatuses[ - subscriptionId - ]; - SubscriptionParams storage params = _state.subscriptionParams[ - subscriptionId - ]; + SchedulerStructs.SubscriptionStatus storage status = _state + .subscriptionStatuses[subscriptionId]; + SchedulerStructs.SubscriptionParams storage params = _state + .subscriptionParams[subscriptionId]; if (!params.isActive) { - revert InactiveSubscription(); + revert SchedulerErrors.InactiveSubscription(); } // Get the Pyth contract and parse price updates @@ -293,7 +293,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { // If we don't have enough balance, revert if (status.balanceInWei < pythFee) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } // Parse the price feed updates with an acceptable timestamp range of [0, now+10s]. @@ -320,7 +320,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { uint64 slot = slots[0]; for (uint8 i = 1; i < slots.length; i++) { if (slots[i] != slot) { - revert PriceSlotMismatch(); + revert SchedulerErrors.PriceSlotMismatch(); } } @@ -344,18 +344,16 @@ abstract contract Scheduler is IScheduler, SchedulerState { 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. - * @return The timestamp of the update if the trigger criteria is met, reverts if not met. - */ + /// @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. + /// @return The timestamp of the update if the trigger criteria is met, reverts if not met. function _validateShouldUpdatePrices( uint256 subscriptionId, - SubscriptionParams storage params, - SubscriptionStatus storage status, + SchedulerStructs.SubscriptionParams storage params, + SchedulerStructs.SubscriptionStatus storage status, PythStructs.PriceFeed[] memory priceFeeds ) internal view returns (uint256) { // Use the most recent timestamp, as some asset markets may be closed. @@ -378,7 +376,10 @@ abstract contract Scheduler is IScheduler, SchedulerState { // Validate that the update timestamp is not too old if (updateTimestamp < minAllowedTimestamp) { - revert TimestampTooOld(updateTimestamp, block.timestamp); + revert SchedulerErrors.TimestampTooOld( + updateTimestamp, + block.timestamp + ); } // Reject updates if they're older than the latest stored ones @@ -386,7 +387,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { status.priceLastUpdatedAt > 0 && updateTimestamp <= status.priceLastUpdatedAt ) { - revert TimestampOlderThanLastUpdate( + revert SchedulerErrors.TimestampOlderThanLastUpdate( updateTimestamp, status.priceLastUpdatedAt ); @@ -446,28 +447,25 @@ abstract contract Scheduler is IScheduler, SchedulerState { } } - revert UpdateConditionsNotMet(); + revert SchedulerErrors.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. - */ + /// @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(); + revert SchedulerErrors.InactiveSubscription(); } - SubscriptionParams storage params = _state.subscriptionParams[ - subscriptionId - ]; + SchedulerStructs.SubscriptionParams storage params = _state + .subscriptionParams[subscriptionId]; // If no price IDs provided, return all price feeds for the subscription if (priceIds.length == 0) { @@ -481,7 +479,10 @@ abstract contract Scheduler is IScheduler, SchedulerState { ][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)); + revert SchedulerErrors.InvalidPriceId( + params.priceIds[i], + bytes32(0) + ); } allFeeds[i] = priceFeed; } @@ -500,7 +501,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { // 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)); + revert SchedulerErrors.InvalidPriceId(priceIds[i], bytes32(0)); } requestedFeeds[i] = priceFeed; } @@ -528,7 +529,29 @@ abstract contract Scheduler is IScheduler, SchedulerState { return prices; } - function getEmaPriceUnsafe( + function getPricesNoOlderThan( + uint256 subscriptionId, + bytes32[] calldata priceIds, + uint256 age_seconds + ) + external + view + override + onlyWhitelistedReader(subscriptionId) + returns (PythStructs.Price[] memory prices) + { + SchedulerStructs.SubscriptionStatus memory status = _state + .subscriptionStatuses[subscriptionId]; + + // Use distance (absolute difference) since pythnet timestamps + // may be slightly ahead of this chain. + if (distance(block.timestamp, status.priceLastUpdatedAt) > age_seconds) + revert PythErrors.StalePrice(); + + prices = this.getPricesUnsafe(subscriptionId, priceIds); + } + + function getEmaPricesUnsafe( uint256 subscriptionId, bytes32[] calldata priceIds ) @@ -549,23 +572,43 @@ abstract contract Scheduler is IScheduler, SchedulerState { return prices; } + function getEmaPricesNoOlderThan( + uint256 subscriptionId, + bytes32[] calldata priceIds, + uint256 age_seconds + ) + external + view + override + onlyWhitelistedReader(subscriptionId) + returns (PythStructs.Price[] memory prices) + { + SchedulerStructs.SubscriptionStatus memory status = _state + .subscriptionStatuses[subscriptionId]; + + // Use distance (absolute difference) since pythnet timestamps + // may be slightly ahead of this chain. + if (distance(block.timestamp, status.priceLastUpdatedAt) > age_seconds) + revert PythErrors.StalePrice(); + + prices = this.getEmaPricesUnsafe(subscriptionId, priceIds); + } + /// BALANCE MANAGEMENT function addFunds(uint256 subscriptionId) external payable override { - SubscriptionParams storage params = _state.subscriptionParams[ - subscriptionId - ]; - SubscriptionStatus storage status = _state.subscriptionStatuses[ - subscriptionId - ]; + SchedulerStructs.SubscriptionParams storage params = _state + .subscriptionParams[subscriptionId]; + SchedulerStructs.SubscriptionStatus storage status = _state + .subscriptionStatuses[subscriptionId]; if (!params.isActive) { - revert InactiveSubscription(); + revert SchedulerErrors.InactiveSubscription(); } // Check deposit limit for permanent subscriptions if (params.isPermanent && msg.value > MAX_DEPOSIT_LIMIT) { - revert MaxDepositLimitExceeded(); + revert SchedulerErrors.MaxDepositLimitExceeded(); } status.balanceInWei += msg.value; @@ -576,7 +619,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { uint8(params.priceIds.length) ); if (status.balanceInWei < minimumBalance) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } } } @@ -585,20 +628,18 @@ abstract contract Scheduler is IScheduler, SchedulerState { uint256 subscriptionId, uint256 amount ) external override onlyManager(subscriptionId) { - SubscriptionStatus storage status = _state.subscriptionStatuses[ - subscriptionId - ]; - SubscriptionParams storage params = _state.subscriptionParams[ - subscriptionId - ]; + SchedulerStructs.SubscriptionStatus storage status = _state + .subscriptionStatuses[subscriptionId]; + SchedulerStructs.SubscriptionParams storage params = _state + .subscriptionParams[subscriptionId]; // Prevent withdrawals from permanent subscriptions if (params.isPermanent) { - revert CannotUpdatePermanentSubscription(); + revert SchedulerErrors.CannotUpdatePermanentSubscription(); } if (status.balanceInWei < amount) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } // If subscription is active, ensure minimum balance is maintained @@ -607,7 +648,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { uint8(params.priceIds.length) ); if (status.balanceInWei - amount < minimumBalance) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } } @@ -626,8 +667,8 @@ abstract contract Scheduler is IScheduler, SchedulerState { view override returns ( - SubscriptionParams memory params, - SubscriptionStatus memory status + SchedulerStructs.SubscriptionParams memory params, + SchedulerStructs.SubscriptionStatus memory status ) { return ( @@ -646,7 +687,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { override returns ( uint256[] memory subscriptionIds, - SubscriptionParams[] memory subscriptionParams, + SchedulerStructs.SubscriptionParams[] memory subscriptionParams, uint256 totalCount ) { @@ -654,7 +695,11 @@ abstract contract Scheduler is IScheduler, SchedulerState { // If startIndex is beyond the total count, return empty arrays if (startIndex >= totalCount) { - return (new uint256[](0), new SubscriptionParams[](0), totalCount); + return ( + new uint256[](0), + new SchedulerStructs.SubscriptionParams[](0), + totalCount + ); } // Calculate how many results to return (bounded by maxResults and remaining items) @@ -665,7 +710,9 @@ abstract contract Scheduler is IScheduler, SchedulerState { // Create arrays for subscription IDs and parameters subscriptionIds = new uint256[](resultCount); - subscriptionParams = new SubscriptionParams[](resultCount); + subscriptionParams = new SchedulerStructs.SubscriptionParams[]( + resultCount + ); // Populate the arrays with the requested page of active subscriptions for (uint256 i = 0; i < resultCount; i++) { @@ -679,10 +726,8 @@ abstract contract Scheduler is IScheduler, SchedulerState { 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. - */ + /// @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) { @@ -694,7 +739,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { modifier onlyManager(uint256 subscriptionId) { if (_state.subscriptionManager[subscriptionId] != msg.sender) { - revert Unauthorized(); + revert SchedulerErrors.Unauthorized(); } _; } @@ -725,15 +770,13 @@ abstract contract Scheduler is IScheduler, SchedulerState { } if (!isWhitelisted) { - revert Unauthorized(); + revert SchedulerErrors.Unauthorized(); } _; } - /** - * @notice Adds a subscription to the active subscriptions list. - * @param subscriptionId The ID of the subscription to add. - */ + /// @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) { @@ -746,10 +789,8 @@ abstract contract Scheduler is IScheduler, SchedulerState { } } - /** - * @notice Removes a subscription from the active subscriptions list. - * @param subscriptionId The ID of the subscription to remove. - */ + /// @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]; @@ -773,11 +814,9 @@ abstract contract Scheduler is IScheduler, SchedulerState { } } - /** - * @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. - */ + /// @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 @@ -789,16 +828,14 @@ abstract contract Scheduler is IScheduler, SchedulerState { } } - /** - * @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. - */ + /// @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, + SchedulerStructs.SubscriptionStatus storage status, uint256 startGas, uint256 numPriceIds ) internal { @@ -810,7 +847,7 @@ abstract contract Scheduler is IScheduler, SchedulerState { // Check balance if (status.balanceInWei < totalKeeperFee) { - revert InsufficientBalance(); + revert SchedulerErrors.InsufficientBalance(); } status.balanceInWei -= totalKeeperFee; @@ -819,7 +856,16 @@ abstract contract Scheduler is IScheduler, SchedulerState { // Pay keeper and update status (bool sent, ) = msg.sender.call{value: totalKeeperFee}(""); if (!sent) { - revert KeeperPaymentFailed(); + revert SchedulerErrors.KeeperPaymentFailed(); + } + } + + /// @notice Helper to calculate the distance (absolute difference) between two timestamps. + function distance(uint x, uint y) internal pure returns (uint) { + if (x > y) { + return x - y; + } else { + return y - x; } } } diff --git a/target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol b/target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol deleted file mode 100644 index 0aaf0d0d6c..0000000000 --- a/target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache 2 - -pragma solidity ^0.8.0; - -// Authorization errors -error Unauthorized(); - -// Subscription state errors -error InactiveSubscription(); -error InsufficientBalance(); -error CannotUpdatePermanentSubscription(); - -// Price feed errors -error InvalidPriceId(bytes32 providedPriceId, bytes32 expectedPriceId); -error InvalidPriceIdsLength(uint256 providedLength, uint256 expectedLength); -error EmptyPriceIds(); -error TooManyPriceIds(uint256 provided, uint256 maximum); -error DuplicatePriceId(bytes32 priceId); -error PriceSlotMismatch(); - -// Update criteria errors -error InvalidUpdateCriteria(); -error UpdateConditionsNotMet(); -error TimestampTooOld( - uint256 providedUpdateTimestamp, - uint256 currentTimestamp -); -error TimestampOlderThanLastUpdate( - uint256 providedUpdateTimestamp, - uint256 lastUpdatedAt -); - -// Whitelist errors -error TooManyWhitelistedReaders(uint256 provided, uint256 maximum); -error DuplicateWhitelistAddress(address addr); - -// Payment errors -error KeeperPaymentFailed(); -error MaxDepositLimitExceeded(); diff --git a/target_chains/ethereum/contracts/contracts/pulse/SchedulerGovernance.sol b/target_chains/ethereum/contracts/contracts/pulse/SchedulerGovernance.sol index 18f6bf13bc..ac1eb52399 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/SchedulerGovernance.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/SchedulerGovernance.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import "./SchedulerState.sol"; -import "./SchedulerErrors.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol"; /** * @dev `SchedulerGovernance` defines governance capabilities for the Pulse contract. @@ -44,7 +44,8 @@ abstract contract SchedulerGovernance is SchedulerState { * @dev The proposed admin accepts the admin transfer. */ function acceptAdmin() external { - if (msg.sender != _state.proposedAdmin) revert Unauthorized(); + if (msg.sender != _state.proposedAdmin) + revert SchedulerErrors.Unauthorized(); address oldAdmin = _state.admin; _state.admin = msg.sender; diff --git a/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol b/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol index f4557cf297..43e931bd48 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol @@ -3,32 +3,9 @@ pragma solidity ^0.8.0; import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol"; contract SchedulerState { - /// Maximum number of price feeds per subscription - uint8 public constant MAX_PRICE_IDS_PER_SUBSCRIPTION = 255; - /// Maximum number of addresses in the reader whitelist - uint8 public constant MAX_READER_WHITELIST_SIZE = 255; - /// Maximum deposit limit for permanent subscriptions in wei - uint256 public constant MAX_DEPOSIT_LIMIT = 100 ether; - - /// Maximum time in the past (relative to current block timestamp) - /// for which a price update timestamp is considered valid - /// when validating the update conditions. - /// @dev Note: We don't use this when parsing update data from the Pyth contract - /// because don't want to reject update data if it contains a price from a market - /// that closed a few days ago, since it will contain a timestamp from the last - /// trading period. We enforce this value ourselves against the maximum - /// timestamp in the provided update data. - uint64 public constant PAST_TIMESTAMP_MAX_VALIDITY_PERIOD = 1 hours; - - /// Maximum time in the future (relative to current block timestamp) - /// for which a price update timestamp is considered valid - uint64 public constant FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD = 10 seconds; - /// Fixed gas overhead component used in keeper fee calculation. - /// This is a rough estimate of the tx overhead for a keeper to call updatePriceFeeds. - uint256 public constant GAS_OVERHEAD = 30000; - struct State { /// Monotonically increasing counter for subscription IDs uint256 subscriptionNumber; @@ -44,9 +21,9 @@ contract SchedulerState { /// Minimum balance required per price feed in a subscription uint128 minimumBalancePerFeed; /// Sub ID -> subscription parameters (which price feeds, when to update, etc) - mapping(uint256 => SubscriptionParams) subscriptionParams; + mapping(uint256 => SchedulerStructs.SubscriptionParams) subscriptionParams; /// Sub ID -> subscription status (metadata about their sub) - mapping(uint256 => SubscriptionStatus) subscriptionStatuses; + mapping(uint256 => SchedulerStructs.SubscriptionStatus) subscriptionStatuses; /// Sub ID -> price ID -> latest parsed price update for the subscribed feed mapping(uint256 => mapping(bytes32 => PythStructs.PriceFeed)) priceUpdates; /// Sub ID -> manager address @@ -60,29 +37,6 @@ contract SchedulerState { } State internal _state; - struct SubscriptionParams { - bytes32[] priceIds; - address[] readerWhitelist; - bool whitelistEnabled; - bool isActive; - bool isPermanent; - UpdateCriteria updateCriteria; - } - - struct SubscriptionStatus { - uint256 priceLastUpdatedAt; - uint256 balanceInWei; - uint256 totalUpdates; - uint256 totalSpent; - } - - struct UpdateCriteria { - bool updateOnHeartbeat; - uint32 heartbeatSeconds; - bool updateOnDeviation; - uint32 deviationThresholdBps; - } - /** * @dev Returns the minimum balance required per feed in a subscription. */ diff --git a/target_chains/ethereum/contracts/contracts/pulse/SchedulerUpgradeable.sol b/target_chains/ethereum/contracts/contracts/pulse/SchedulerUpgradeable.sol index 114ec6dddd..39d1afec10 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/SchedulerUpgradeable.sol +++ b/target_chains/ethereum/contracts/contracts/pulse/SchedulerUpgradeable.sol @@ -7,7 +7,8 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "./Scheduler.sol"; import "./SchedulerGovernance.sol"; -import "./SchedulerErrors.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol"; + contract SchedulerUpgradeable is Initializable, Ownable2StepUpgradeable, @@ -55,7 +56,7 @@ contract SchedulerUpgradeable is /// Authorize actions that both admin and owner can perform function _authorizeAdminAction() internal view override { if (msg.sender != owner() && msg.sender != _state.admin) - revert Unauthorized(); + revert SchedulerErrors.Unauthorized(); } function upgradeTo(address newImplementation) external override onlyProxy { diff --git a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol index c412bbbfb1..c0b904411a 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol @@ -5,13 +5,13 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerEvents.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol"; +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "../contracts/pulse/SchedulerUpgradeable.sol"; -import "../contracts/pulse/IScheduler.sol"; -import "../contracts/pulse/SchedulerState.sol"; -import "../contracts/pulse/SchedulerEvents.sol"; -import "../contracts/pulse/SchedulerErrors.sol"; import "./utils/PulseSchedulerTestUtils.t.sol"; -import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; contract MockReader { address private _scheduler; @@ -32,7 +32,7 @@ contract MockReader { bytes32[] memory priceIds ) external view returns (PythStructs.Price[] memory) { return - IScheduler(_scheduler).getEmaPriceUnsafe(subscriptionId, priceIds); + IScheduler(_scheduler).getEmaPricesUnsafe(subscriptionId, priceIds); } function verifyPriceFeeds( @@ -105,7 +105,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { } function testCreateSubscription() public { - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory params = createDefaultSubscriptionParams(2, address(reader)); bytes32[] memory priceIds = params.priceIds; // Get the generated price IDs @@ -125,8 +125,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify subscription was added correctly ( - SchedulerState.SubscriptionParams memory storedParams, - SchedulerState.SubscriptionStatus memory status + SchedulerStructs.SubscriptionParams memory storedParams, + SchedulerStructs.SubscriptionStatus memory status ) = scheduler.getSubscription(subscriptionId); assertEq( @@ -176,15 +176,15 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { newReaderWhitelist[0] = address(reader); newReaderWhitelist[1] = address(0x123); - SchedulerState.UpdateCriteria memory newUpdateCriteria = SchedulerState - .UpdateCriteria({ + SchedulerStructs.UpdateCriteria + memory newUpdateCriteria = SchedulerStructs.UpdateCriteria({ updateOnHeartbeat: true, heartbeatSeconds: 120, // Changed from 60 updateOnDeviation: true, deviationThresholdBps: 200 // Changed from 100 }); - SchedulerState.SubscriptionParams memory newParams = SchedulerState + SchedulerStructs.SubscriptionParams memory newParams = SchedulerStructs .SubscriptionParams({ priceIds: newPriceIds, readerWhitelist: newReaderWhitelist, @@ -206,7 +206,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updateSubscription(subscriptionId, newParams); // Verify subscription was updated correctly - (SchedulerState.SubscriptionParams memory storedParams, ) = scheduler + (SchedulerStructs.SubscriptionParams memory storedParams, ) = scheduler .getSubscription(subscriptionId); assertEq( @@ -283,9 +283,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { } bytes32 removedPriceId = initialPriceIds[numInitialFeeds - 1]; // The ID we removed - (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler + (SchedulerStructs.SubscriptionParams memory currentParams, ) = scheduler .getSubscription(subscriptionId); - SchedulerState.SubscriptionParams memory newParams = currentParams; // Copy existing params + SchedulerStructs.SubscriptionParams memory newParams = currentParams; // Copy existing params newParams.priceIds = newPriceIds; // Update price IDs vm.expectEmit(); // Expect SubscriptionUpdated @@ -298,7 +298,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { removedIdArray[0] = removedPriceId; vm.expectRevert( abi.encodeWithSelector( - InvalidPriceId.selector, + SchedulerErrors.InvalidPriceId.selector, removedPriceId, bytes32(0) ) @@ -339,7 +339,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { { // Setup subscription with heartbeat criteria uint32 heartbeatSeconds = 60; // 60 second heartbeat - SchedulerState.UpdateCriteria memory criteria = SchedulerState + SchedulerStructs.UpdateCriteria memory criteria = SchedulerStructs .UpdateCriteria({ updateOnHeartbeat: true, heartbeatSeconds: heartbeatSeconds, @@ -376,7 +376,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ) = _setupSubscriptionAndFirstUpdate(); // Verify priceLastUpdatedAt is set - (, SchedulerState.SubscriptionStatus memory status) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status) = scheduler .getSubscription(subscriptionId); assertEq( status.priceLastUpdatedAt, @@ -385,11 +385,11 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // 2. Update subscription to add price IDs - (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler + (SchedulerStructs.SubscriptionParams memory currentParams, ) = scheduler .getSubscription(subscriptionId); bytes32[] memory newPriceIds = createPriceIds(3); - SchedulerState.SubscriptionParams memory newParams = currentParams; + SchedulerStructs.SubscriptionParams memory newParams = currentParams; newParams.priceIds = newPriceIds; // Update the subscription @@ -422,7 +422,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updatePriceFeeds(subscriptionId, updateData); // Verify the update was processed - (, SchedulerState.SubscriptionStatus memory status) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status) = scheduler .getSubscription(subscriptionId); assertEq( status.priceLastUpdatedAt, @@ -438,7 +438,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // This should fail because we haven't waited for heartbeatSeconds since the last update vm.expectRevert( - abi.encodeWithSelector(UpdateConditionsNotMet.selector) + abi.encodeWithSelector( + SchedulerErrors.UpdateConditionsNotMet.selector + ) ); vm.prank(pusher); scheduler.updatePriceFeeds(subscriptionId, updateData); @@ -446,7 +448,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { function testcreateSubscriptionWithInsufficientFundsReverts() public { uint8 numFeeds = 2; - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory params = createDefaultSubscriptionParams( numFeeds, address(reader) @@ -458,7 +460,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Try to add subscription with insufficient funds - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector) + ); scheduler.createSubscription{value: minimumBalance - 1 wei}(params); } @@ -478,7 +482,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { assertEq(activeIds[2], subId3, "Initial: ID 3 should be active"); // --- Deactivate the middle subscription (ID 2) --- - (SchedulerState.SubscriptionParams memory params2, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params2, ) = scheduler .getSubscription(subId2); params2.isActive = false; vm.expectEmit(); @@ -503,7 +507,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // ID 3 takes the place of ID 2 // --- Deactivate the last subscription (ID 3, now at index 1) --- - (SchedulerState.SubscriptionParams memory params3, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params3, ) = scheduler .getSubscription(subId3); params3.isActive = false; vm.expectEmit(); @@ -563,7 +567,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // --- Deactivate all remaining subscriptions --- // Deactivate ID 1 (first element) - (SchedulerState.SubscriptionParams memory params1, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params1, ) = scheduler .getSubscription(subId1); params1.isActive = false; vm.expectEmit(); @@ -639,7 +643,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Get initial balance (which includes minimum balance) - (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler + (, SchedulerStructs.SubscriptionStatus memory initialStatus) = scheduler .getSubscription(subscriptionId); uint256 initialBalance = initialStatus.balanceInWei; @@ -648,7 +652,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.addFunds{value: fundAmount}(subscriptionId); // Verify funds were added - (, SchedulerState.SubscriptionStatus memory status) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status) = scheduler .getSubscription(subscriptionId); assertEq( @@ -666,14 +670,14 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Get subscription parameters and calculate minimum balance - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); uint256 minimumBalance = scheduler.getMinimumBalance( uint8(params.priceIds.length) ); // Deactivate the subscription - SchedulerState.SubscriptionParams memory testParams = params; + SchedulerStructs.SubscriptionParams memory testParams = params; testParams.isActive = false; scheduler.updateSubscription(subscriptionId, testParams); @@ -683,8 +687,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify balance is now below minimum ( - SchedulerState.SubscriptionParams memory testUpdatedParams, - SchedulerState.SubscriptionStatus memory testUpdatedStatus + SchedulerStructs.SubscriptionParams memory testUpdatedParams, + SchedulerStructs.SubscriptionStatus memory testUpdatedStatus ) = scheduler.getSubscription(subscriptionId); assertEq( testUpdatedStatus.balanceInWei, @@ -693,12 +697,18 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Try to add funds to inactive subscription (should fail with InactiveSubscription) - vm.expectRevert(abi.encodeWithSelector(InactiveSubscription.selector)); + vm.expectRevert( + abi.encodeWithSelector( + SchedulerErrors.InactiveSubscription.selector + ) + ); scheduler.addFunds{value: 1 wei}(subscriptionId); // Try to reactivate with insufficient balance (should fail) testUpdatedParams.isActive = true; - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector) + ); scheduler.updateSubscription(subscriptionId, testUpdatedParams); } @@ -708,7 +718,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { 2, address(reader) ); - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); uint256 minimumBalance = scheduler.getMinimumBalance( uint8(params.priceIds.length) @@ -740,7 +750,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify balance is now below minimum ( , - SchedulerState.SubscriptionStatus memory statusAfterUpdates + SchedulerStructs.SubscriptionStatus memory statusAfterUpdates ) = scheduler.getSubscription(subscriptionId); assertTrue( statusAfterUpdates.balanceInWei < minimumBalance, @@ -752,7 +762,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { uint256 insufficientFunds = minimumBalance - statusAfterUpdates.balanceInWei - 1; - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector) + ); scheduler.addFunds{value: insufficientFunds}(subscriptionId); // Add sufficient funds to get back above minimum @@ -764,7 +776,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify balance is now above minimum ( , - SchedulerState.SubscriptionStatus memory statusAfterAddingFunds + SchedulerStructs.SubscriptionStatus memory statusAfterAddingFunds ) = scheduler.getSubscription(subscriptionId); assertTrue( statusAfterAddingFunds.balanceInWei >= minimumBalance, @@ -778,7 +790,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler, address(reader) ); - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); uint256 minimumBalance = scheduler.getMinimumBalance( uint8(params.priceIds.length) @@ -795,7 +807,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.withdrawFunds(subscriptionId, extraFunds); // Verify funds were withdrawn - (, SchedulerState.SubscriptionStatus memory status) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status) = scheduler .getSubscription(subscriptionId); assertEq( @@ -810,7 +822,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Try to withdraw below minimum balance - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector) + ); scheduler.withdrawFunds(subscriptionId, 1 wei); // Deactivate subscription @@ -837,7 +851,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Verify subscription was created as non-permanent initially - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); assertFalse(params.isPermanent, "Should not be permanent initially"); @@ -846,7 +860,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updateSubscription(subscriptionId, params); // Verify subscription is now permanent - (SchedulerState.SubscriptionParams memory storedParams, ) = scheduler + (SchedulerStructs.SubscriptionParams memory storedParams, ) = scheduler .getSubscription(subscriptionId); assertTrue( storedParams.isPermanent, @@ -854,11 +868,13 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Test 1: Cannot disable isPermanent flag - SchedulerState.SubscriptionParams memory updatedParams = storedParams; + SchedulerStructs.SubscriptionParams memory updatedParams = storedParams; updatedParams.isPermanent = false; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, updatedParams); @@ -873,7 +889,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { updatedParams.priceIds = reducedPriceIds; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, updatedParams); @@ -886,7 +904,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.addFunds{value: extraFunds}(subscriptionId); vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.withdrawFunds(subscriptionId, 0.1 ether); @@ -904,7 +924,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { updatedParams.priceIds = expandedPriceIds; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, updatedParams); @@ -922,7 +944,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { storedParams.updateCriteria.heartbeatSeconds + 60; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, updatedParams); @@ -930,7 +954,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { updatedParams = storedParams; updatedParams.whitelistEnabled = !storedParams.whitelistEnabled; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, updatedParams); @@ -945,7 +971,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { expandedWhitelist[storedParams.readerWhitelist.length] = address(0x456); updatedParams.readerWhitelist = expandedWhitelist; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, updatedParams); @@ -962,7 +990,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { updatedParams.readerWhitelist = reducedWhitelist; vm.expectRevert( abi.encodeWithSelector( - CannotUpdatePermanentSubscription.selector + SchedulerErrors.CannotUpdatePermanentSubscription.selector ) ); scheduler.updateSubscription(subscriptionId, updatedParams); @@ -972,7 +1000,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { updatedParams = storedParams; updatedParams.isActive = false; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, updatedParams); } @@ -985,7 +1015,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Verify it's not permanent - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); assertFalse( @@ -1004,23 +1034,27 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify we can't make it non-permanent again params.isPermanent = false; vm.expectRevert( - abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector) + abi.encodeWithSelector( + SchedulerErrors.CannotUpdatePermanentSubscription.selector + ) ); scheduler.updateSubscription(subscriptionId, params); } function testPermanentSubscriptionDepositLimit() public { // Test 1: Creating a permanent subscription with deposit exceeding MAX_DEPOSIT_LIMIT should fail - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory params = createDefaultSubscriptionParams(2, address(reader)); params.isPermanent = true; - uint256 maxDepositLimit = 100 ether; // Same as MAX_DEPOSIT_LIMIT in SchedulerState + uint256 maxDepositLimit = scheduler.MAX_DEPOSIT_LIMIT(); uint256 excessiveDeposit = maxDepositLimit + 1 ether; vm.deal(address(this), excessiveDeposit); vm.expectRevert( - abi.encodeWithSelector(MaxDepositLimitExceeded.selector) + abi.encodeWithSelector( + SchedulerErrors.MaxDepositLimitExceeded.selector + ) ); scheduler.createSubscription{value: excessiveDeposit}(params); @@ -1034,8 +1068,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify subscription was created correctly ( - SchedulerState.SubscriptionParams memory storedParams, - SchedulerState.SubscriptionStatus memory status + SchedulerStructs.SubscriptionParams memory storedParams, + SchedulerStructs.SubscriptionStatus memory status ) = scheduler.getSubscription(subscriptionId); assertTrue( @@ -1053,13 +1087,15 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { vm.deal(address(this), largeAdditionalFunds); vm.expectRevert( - abi.encodeWithSelector(MaxDepositLimitExceeded.selector) + abi.encodeWithSelector( + SchedulerErrors.MaxDepositLimitExceeded.selector + ) ); scheduler.addFunds{value: largeAdditionalFunds}(subscriptionId); // Test 4: Adding funds to a permanent subscription within MAX_DEPOSIT_LIMIT should succeed // Create a non-permanent subscription to test partial funding - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory nonPermanentParams = createDefaultSubscriptionParams( 2, address(reader) @@ -1082,7 +1118,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify funds were added correctly ( , - SchedulerState.SubscriptionStatus memory nonPermanentStatus + SchedulerStructs.SubscriptionStatus memory nonPermanentStatus ) = scheduler.getSubscription(nonPermanentSubId); assertEq( @@ -1095,7 +1131,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { uint256 largeDeposit = maxDepositLimit * 2; vm.deal(address(this), largeDeposit); - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory unlimitedParams = createDefaultSubscriptionParams( 2, address(reader) @@ -1105,8 +1141,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { }(unlimitedParams); // Verify subscription was created with the large deposit - (, SchedulerState.SubscriptionStatus memory unlimitedStatus) = scheduler - .getSubscription(unlimitedSubId); + ( + , + SchedulerStructs.SubscriptionStatus memory unlimitedStatus + ) = scheduler.getSubscription(unlimitedSubId); assertEq( unlimitedStatus.balanceInWei, @@ -1123,7 +1161,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Get initial balance - (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler + (, SchedulerStructs.SubscriptionStatus memory initialStatus) = scheduler .getSubscription(subscriptionId); uint256 initialBalance = initialStatus.balanceInWei; @@ -1136,7 +1174,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.addFunds{value: fundAmount}(subscriptionId); // Verify funds were added - (, SchedulerState.SubscriptionStatus memory status) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status) = scheduler .getSubscription(subscriptionId); assertEq( @@ -1177,7 +1215,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updatePriceFeeds(subscriptionId, updateData1); // Verify first update - (, SchedulerState.SubscriptionStatus memory status1) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status1) = scheduler .getSubscription(subscriptionId); assertEq( status1.priceLastUpdatedAt, @@ -1228,7 +1266,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updatePriceFeeds(subscriptionId, updateData2); // Verify second update - (, SchedulerState.SubscriptionStatus memory status2) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status2) = scheduler .getSubscription(subscriptionId); assertEq( status2.priceLastUpdatedAt, @@ -1267,7 +1305,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Prepare update data - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); ( PythStructs.PriceFeed[] memory priceFeeds, @@ -1283,7 +1321,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Get state before uint256 pusherBalanceBefore = pusher.balance; - (, SchedulerState.SubscriptionStatus memory statusBefore) = scheduler + (, SchedulerStructs.SubscriptionStatus memory statusBefore) = scheduler .getSubscription(subscriptionId); console.log( "Subscription balance before update:", @@ -1295,7 +1333,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updatePriceFeeds(subscriptionId, updateData); // Get state after - (, SchedulerState.SubscriptionStatus memory statusAfter) = scheduler + (, SchedulerStructs.SubscriptionStatus memory statusAfter) = scheduler .getSubscription(subscriptionId); // Calculate total fee deducted from subscription @@ -1378,7 +1416,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.addFunds{value: fundAmount}(subscriptionId); // Get and print the subscription balance before attempting the update - (, SchedulerState.SubscriptionStatus memory status) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status) = scheduler .getSubscription(subscriptionId); console.log( "Subscription balance before update:", @@ -1392,7 +1430,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Expect revert due to insufficient balance for total fee - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector) + ); vm.prank(pusher); scheduler.updatePriceFeeds(subscriptionId, updateData); } @@ -1402,7 +1442,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { { // Add a subscription with only heartbeat criteria (60 seconds) uint32 heartbeat = 60; - SchedulerState.UpdateCriteria memory criteria = SchedulerState + SchedulerStructs.UpdateCriteria memory criteria = SchedulerStructs .UpdateCriteria({ updateOnHeartbeat: true, heartbeatSeconds: heartbeat, @@ -1437,7 +1477,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Expect revert because heartbeat condition is not met vm.expectRevert( - abi.encodeWithSelector(UpdateConditionsNotMet.selector) + abi.encodeWithSelector( + SchedulerErrors.UpdateConditionsNotMet.selector + ) ); vm.prank(pusher); scheduler.updatePriceFeeds(subscriptionId, updateData2); @@ -1448,7 +1490,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { { // Add a subscription with only deviation criteria (100 bps / 1%) uint16 deviationBps = 100; - SchedulerState.UpdateCriteria memory criteria = SchedulerState + SchedulerStructs.UpdateCriteria memory criteria = SchedulerStructs .UpdateCriteria({ updateOnHeartbeat: false, heartbeatSeconds: 0, @@ -1499,7 +1541,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Expect revert because deviation condition is not met vm.expectRevert( - abi.encodeWithSelector(UpdateConditionsNotMet.selector) + abi.encodeWithSelector( + SchedulerErrors.UpdateConditionsNotMet.selector + ) ); vm.prank(pusher); scheduler.updatePriceFeeds(subscriptionId, updateData2); @@ -1537,7 +1581,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Expect revert with TimestampOlderThanLastUpdate (checked in _validateShouldUpdatePrices) vm.expectRevert( abi.encodeWithSelector( - TimestampOlderThanLastUpdate.selector, + SchedulerErrors.TimestampOlderThanLastUpdate.selector, publishTime2, publishTime1 ) @@ -1575,7 +1619,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { bytes[] memory updateData = createMockUpdateData(priceFeeds); // Expect revert with PriceSlotMismatch error - vm.expectRevert(abi.encodeWithSelector(PriceSlotMismatch.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.PriceSlotMismatch.selector) + ); // Attempt to update price feeds vm.prank(pusher); @@ -1591,8 +1637,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { address(reader) ); ( - SchedulerState.SubscriptionParams memory currentParams, - SchedulerState.SubscriptionStatus memory initialStatus + SchedulerStructs.SubscriptionParams memory currentParams, + SchedulerStructs.SubscriptionStatus memory initialStatus ) = scheduler.getSubscription(subscriptionId); uint256 initialMinimumBalance = scheduler.getMinimumBalance( initialNumFeeds @@ -1605,12 +1651,14 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Prepare new params with more feeds (4) uint8 newNumFeeds = 4; - SchedulerState.SubscriptionParams memory newParams = currentParams; + SchedulerStructs.SubscriptionParams memory newParams = currentParams; newParams.priceIds = createPriceIds(newNumFeeds); // Increase feeds newParams.isActive = true; // Keep it active // Action 1: Try to update with insufficient funds - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector) + ); scheduler.updateSubscription(subscriptionId, newParams); // Action 2: Supply enough funds to the updateSubscription call to meet the new minimum balance @@ -1623,7 +1671,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Verification 2: Update should now succeed - (SchedulerState.SubscriptionParams memory updatedParams, ) = scheduler + (SchedulerStructs.SubscriptionParams memory updatedParams, ) = scheduler .getSubscription(subscriptionId); assertEq( updatedParams.priceIds.length, @@ -1643,10 +1691,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Prepare params to add feeds (4) but also deactivate uint8 newNumFeeds_deact = 4; ( - SchedulerState.SubscriptionParams memory currentParams_deact, + SchedulerStructs.SubscriptionParams memory currentParams_deact, ) = scheduler.getSubscription(subId_deact); - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory newParams_deact = currentParams_deact; newParams_deact.priceIds = createPriceIds(newNumFeeds_deact); newParams_deact.isActive = false; // Deactivate @@ -1656,7 +1704,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verification 3: Subscription should be inactive and have 4 feeds ( - SchedulerState.SubscriptionParams memory updatedParams_deact, + SchedulerStructs.SubscriptionParams memory updatedParams_deact, ) = scheduler.getSubscription(subId_deact); assertFalse( @@ -1699,7 +1747,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { } // Check that balance is now below minimum for 1 feed - (, SchedulerState.SubscriptionStatus memory status_reduce) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status_reduce) = scheduler .getSubscription(subId_reduce); uint256 minBalanceForOneFeed = scheduler.getMinimumBalance(1); assertTrue( @@ -1709,16 +1757,18 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Prepare params to reduce feeds from 2 to 1 ( - SchedulerState.SubscriptionParams memory currentParams_reduce, + SchedulerStructs.SubscriptionParams memory currentParams_reduce, ) = scheduler.getSubscription(subId_reduce); - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory newParams_reduce = currentParams_reduce; newParams_reduce.priceIds = new bytes32[](1); newParams_reduce.priceIds[0] = currentParams_reduce.priceIds[0]; // Action 4: Update should fail due to insufficient balance - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector) + ); scheduler.updateSubscription(subId_reduce, newParams_reduce); // Add funds to cover minimum balance for 1 feed @@ -1734,7 +1784,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify the subscription now has 1 feed ( - SchedulerState.SubscriptionParams memory updatedParams_reduce, + SchedulerStructs.SubscriptionParams memory updatedParams_reduce, ) = scheduler.getSubscription(subId_reduce); assertEq( @@ -1841,7 +1891,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // Get params and modify them - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); params.whitelistEnabled = false; params.readerWhitelist = new address[](0); @@ -1887,7 +1937,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.addFunds{value: 1 ether}(subscriptionId); // Get the price IDs from the created subscription - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); bytes32[] memory priceIds = params.priceIds; @@ -1908,7 +1958,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Try to access from a non-whitelisted address (should fail) vm.startPrank(address(0xdead)); bytes32[] memory emptyPriceIds = new bytes32[](0); - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.Unauthorized.selector) + ); scheduler.getPricesUnsafe(subscriptionId, emptyPriceIds); vm.stopPrank(); @@ -1974,7 +2026,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Get EMA prices bytes32[] memory emptyPriceIds = new bytes32[](0); - PythStructs.Price[] memory emaPrices = scheduler.getEmaPriceUnsafe( + PythStructs.Price[] memory emaPrices = scheduler.getEmaPricesUnsafe( subscriptionId, emptyPriceIds ); @@ -2011,7 +2063,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { bytes32[] memory priceIds = createPriceIds(); address[] memory emptyWhitelist = new address[](0); - SchedulerState.UpdateCriteria memory updateCriteria = SchedulerState + SchedulerStructs.UpdateCriteria memory updateCriteria = SchedulerStructs .UpdateCriteria({ updateOnHeartbeat: true, heartbeatSeconds: 60, @@ -2019,8 +2071,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { deviationThresholdBps: 100 }); - SchedulerState.SubscriptionParams memory pusherParams = SchedulerState - .SubscriptionParams({ + SchedulerStructs.SubscriptionParams + memory pusherParams = SchedulerStructs.SubscriptionParams({ priceIds: priceIds, readerWhitelist: emptyWhitelist, whitelistEnabled: false, @@ -2038,7 +2090,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Get active subscriptions directly - should work without any special permissions uint256[] memory activeIds; - SchedulerState.SubscriptionParams[] memory activeParams; + SchedulerStructs.SubscriptionParams[] memory activeParams; uint256 totalCount; (activeIds, activeParams, totalCount) = scheduler @@ -2056,7 +2108,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Verify subscription params for (uint i = 0; i < activeIds.length; i++) { ( - SchedulerState.SubscriptionParams memory storedParams, + SchedulerStructs.SubscriptionParams memory storedParams, ) = scheduler.getSubscription(activeIds[i]); @@ -2114,22 +2166,26 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { uint256 initialSubId = 0; // For update tests // === Empty Price IDs === - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory emptyPriceIdsParams = createDefaultSubscriptionParams( 1, address(reader) ); emptyPriceIdsParams.priceIds = new bytes32[](0); - vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.EmptyPriceIds.selector) + ); scheduler.createSubscription{value: 1 ether}(emptyPriceIdsParams); initialSubId = addTestSubscription(scheduler, address(reader)); // Create a valid one for update test - vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector)); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.EmptyPriceIds.selector) + ); scheduler.updateSubscription(initialSubId, emptyPriceIdsParams); // === Duplicate Price IDs === - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory duplicatePriceIdsParams = createDefaultSubscriptionParams( 2, address(reader) @@ -2138,18 +2194,24 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { duplicatePriceIdsParams.priceIds[1] = duplicateId; vm.expectRevert( - abi.encodeWithSelector(DuplicatePriceId.selector, duplicateId) + abi.encodeWithSelector( + SchedulerErrors.DuplicatePriceId.selector, + duplicateId + ) ); scheduler.createSubscription{value: 1 ether}(duplicatePriceIdsParams); initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert( - abi.encodeWithSelector(DuplicatePriceId.selector, duplicateId) + abi.encodeWithSelector( + SchedulerErrors.DuplicatePriceId.selector, + duplicateId + ) ); scheduler.updateSubscription(initialSubId, duplicatePriceIdsParams); // === Too Many Whitelist Readers === - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory largeWhitelistParams = createDefaultSubscriptionParams( 1, address(reader) @@ -2163,7 +2225,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { vm.expectRevert( abi.encodeWithSelector( - TooManyWhitelistedReaders.selector, + SchedulerErrors.TooManyWhitelistedReaders.selector, largeWhitelist.length, scheduler.MAX_READER_WHITELIST_SIZE() ) @@ -2173,7 +2235,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert( abi.encodeWithSelector( - TooManyWhitelistedReaders.selector, + SchedulerErrors.TooManyWhitelistedReaders.selector, largeWhitelist.length, scheduler.MAX_READER_WHITELIST_SIZE() ) @@ -2181,7 +2243,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updateSubscription(initialSubId, largeWhitelistParams); // === Duplicate Whitelist Address === - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory duplicateWhitelistParams = createDefaultSubscriptionParams( 1, address(reader) @@ -2193,7 +2255,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { vm.expectRevert( abi.encodeWithSelector( - DuplicateWhitelistAddress.selector, + SchedulerErrors.DuplicateWhitelistAddress.selector, address(reader) ) ); @@ -2202,14 +2264,14 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { initialSubId = addTestSubscription(scheduler, address(reader)); vm.expectRevert( abi.encodeWithSelector( - DuplicateWhitelistAddress.selector, + SchedulerErrors.DuplicateWhitelistAddress.selector, address(reader) ) ); scheduler.updateSubscription(initialSubId, duplicateWhitelistParams); // === Invalid Heartbeat (Zero Seconds) === - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory invalidHeartbeatParams = createDefaultSubscriptionParams( 1, address(reader) @@ -2217,15 +2279,23 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { invalidHeartbeatParams.updateCriteria.updateOnHeartbeat = true; invalidHeartbeatParams.updateCriteria.heartbeatSeconds = 0; // Invalid - vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); + vm.expectRevert( + abi.encodeWithSelector( + SchedulerErrors.InvalidUpdateCriteria.selector + ) + ); scheduler.createSubscription{value: 1 ether}(invalidHeartbeatParams); initialSubId = addTestSubscription(scheduler, address(reader)); - vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); + vm.expectRevert( + abi.encodeWithSelector( + SchedulerErrors.InvalidUpdateCriteria.selector + ) + ); scheduler.updateSubscription(initialSubId, invalidHeartbeatParams); // === Invalid Deviation (Zero Bps) === - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory invalidDeviationParams = createDefaultSubscriptionParams( 1, address(reader) @@ -2233,11 +2303,19 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { invalidDeviationParams.updateCriteria.updateOnDeviation = true; invalidDeviationParams.updateCriteria.deviationThresholdBps = 0; // Invalid - vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); + vm.expectRevert( + abi.encodeWithSelector( + SchedulerErrors.InvalidUpdateCriteria.selector + ) + ); scheduler.createSubscription{value: 1 ether}(invalidDeviationParams); initialSubId = addTestSubscription(scheduler, address(reader)); - vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector)); + vm.expectRevert( + abi.encodeWithSelector( + SchedulerErrors.InvalidUpdateCriteria.selector + ) + ); scheduler.updateSubscription(initialSubId, invalidDeviationParams); } @@ -2283,7 +2361,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { scheduler.updatePriceFeeds(subscriptionId, updateData); // Verify last updated timestamp - (, SchedulerState.SubscriptionStatus memory status) = scheduler + (, SchedulerStructs.SubscriptionStatus memory status) = scheduler .getSubscription(subscriptionId); assertEq( status.priceLastUpdatedAt, @@ -2330,7 +2408,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // Expect revert with TimestampTooOld (checked in _validateShouldUpdatePrices) vm.expectRevert( abi.encodeWithSelector( - TimestampTooOld.selector, + SchedulerErrors.TimestampTooOld.selector, stalePublishTime1, // The latest timestamp from the update currentTime ) @@ -2377,7 +2455,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { bytes32 removedPriceId = initialPriceIds[numInitialFeeds - 1]; // 2. Action: Update subscription to remove the last price feed - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); // Create new price IDs array without the last ID @@ -2394,7 +2472,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { // 3. Verification: // - Verify that the removed price ID is no longer part of the subscription's price IDs - (SchedulerState.SubscriptionParams memory updatedParams, ) = scheduler + (SchedulerStructs.SubscriptionParams memory updatedParams, ) = scheduler .getSubscription(subscriptionId); assertEq( updatedParams.priceIds.length, @@ -2428,7 +2506,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { removedIdArray[0] = removedPriceId; vm.expectRevert( abi.encodeWithSelector( - InvalidPriceId.selector, + SchedulerErrors.InvalidPriceId.selector, removedPriceId, bytes32(0) ) @@ -2446,7 +2524,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ); // 2. Prepare params with too many price IDs (MAX_PRICE_IDS_PER_SUBSCRIPTION + 1) - (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler + (SchedulerStructs.SubscriptionParams memory currentParams, ) = scheduler .getSubscription(subscriptionId); uint16 tooManyFeeds = uint16( @@ -2454,13 +2532,13 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils { ) + 1; bytes32[] memory tooManyPriceIds = createPriceIds(tooManyFeeds); - SchedulerState.SubscriptionParams memory newParams = currentParams; + SchedulerStructs.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, + SchedulerErrors.TooManyPriceIds.selector, tooManyFeeds, scheduler.MAX_PRICE_IDS_PER_SUBSCRIPTION() ) diff --git a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol index 48fefda6c3..aa16ca3cd4 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol @@ -7,10 +7,10 @@ import "forge-std/console.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "../contracts/pulse/SchedulerUpgradeable.sol"; -import "../contracts/pulse/IScheduler.sol"; -import "../contracts/pulse/SchedulerState.sol"; -import "../contracts/pulse/SchedulerEvents.sol"; -import "../contracts/pulse/SchedulerErrors.sol"; +import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerEvents.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol"; import "./utils/PulseSchedulerTestUtils.t.sol"; contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils { @@ -54,7 +54,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils { // Setup: Create subscription and perform initial update vm.prank(manager); uint256 subscriptionId = _setupSubscriptionWithInitialUpdate(numFeeds); - (SchedulerState.SubscriptionParams memory params, ) = scheduler + (SchedulerStructs.SubscriptionParams memory params, ) = scheduler .getSubscription(subscriptionId); // Advance time to meet heartbeat criteria @@ -161,8 +161,10 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils { // Deactivate every other subscription for (uint256 i = 0; i < numSubscriptions; i++) { if (i % 2 == 1) { - (SchedulerState.SubscriptionParams memory params, ) = scheduler - .getSubscription(subscriptionIds[i]); + ( + SchedulerStructs.SubscriptionParams memory params, + + ) = scheduler.getSubscription(subscriptionIds[i]); params.isActive = false; scheduler.updateSubscription(subscriptionIds[i], params); } diff --git a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol index 77fcc72f1c..4cd48409be 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import "forge-std/console.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../contracts/pulse/SchedulerUpgradeable.sol"; -import "../contracts/pulse/SchedulerErrors.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol"; contract SchedulerInvalidMagic is SchedulerUpgradeable { function schedulerUpgradableMagic() public pure override returns (uint32) { return 0x12345678; // Incorrect magic @@ -70,7 +70,9 @@ contract PulseSchedulerGovernanceTest is Test { function testProposeAdminByUnauthorized() public { address unauthorized = address(5); vm.prank(unauthorized); - vm.expectRevert(Unauthorized.selector); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.Unauthorized.selector) + ); scheduler.proposeAdmin(admin2); } @@ -91,7 +93,9 @@ contract PulseSchedulerGovernanceTest is Test { address unauthorized = address(5); vm.prank(unauthorized); - vm.expectRevert(Unauthorized.selector); + vm.expectRevert( + abi.encodeWithSelector(SchedulerErrors.Unauthorized.selector) + ); scheduler.acceptAdmin(); } diff --git a/target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol index 400eac8bb5..d311bfd3d1 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol @@ -6,6 +6,7 @@ import "forge-std/Test.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "../../contracts/pulse/SchedulerUpgradeable.sol"; import "../../contracts/pulse/SchedulerState.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol"; import "./MockPriceFeedTestUtils.sol"; abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils { @@ -14,7 +15,7 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils { SchedulerUpgradeable scheduler, address whitelistedReader ) internal returns (uint256) { - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory params = createDefaultSubscriptionParams( 2, whitelistedReader @@ -31,7 +32,7 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils { uint8 numFeeds, address whitelistedReader ) internal returns (uint256) { - SchedulerState.SubscriptionParams + SchedulerStructs.SubscriptionParams memory params = createDefaultSubscriptionParams( numFeeds, whitelistedReader @@ -45,14 +46,14 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils { /// Helper function to add a test subscription with specific update criteria function addTestSubscriptionWithUpdateCriteria( SchedulerUpgradeable scheduler, - SchedulerState.UpdateCriteria memory updateCriteria, + SchedulerStructs.UpdateCriteria memory updateCriteria, address whitelistedReader ) internal returns (uint256) { bytes32[] memory priceIds = createPriceIds(); address[] memory readerWhitelist = new address[](1); readerWhitelist[0] = whitelistedReader; - SchedulerState.SubscriptionParams memory params = SchedulerState + SchedulerStructs.SubscriptionParams memory params = SchedulerStructs .SubscriptionParams({ priceIds: priceIds, readerWhitelist: readerWhitelist, @@ -72,12 +73,12 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils { function createDefaultSubscriptionParams( uint8 numFeeds, address whitelistedReader - ) internal pure returns (SchedulerState.SubscriptionParams memory) { + ) internal pure returns (SchedulerStructs.SubscriptionParams memory) { bytes32[] memory priceIds = createPriceIds(numFeeds); address[] memory readerWhitelist = new address[](1); readerWhitelist[0] = whitelistedReader; - SchedulerState.UpdateCriteria memory updateCriteria = SchedulerState + SchedulerStructs.UpdateCriteria memory updateCriteria = SchedulerStructs .UpdateCriteria({ updateOnHeartbeat: true, heartbeatSeconds: 60, @@ -86,7 +87,7 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils { }); return - SchedulerState.SubscriptionParams({ + SchedulerStructs.SubscriptionParams({ priceIds: priceIds, readerWhitelist: readerWhitelist, whitelistEnabled: true, diff --git a/target_chains/ethereum/contracts/package.json b/target_chains/ethereum/contracts/package.json index 9c364e8732..3d1d60d897 100644 --- a/target_chains/ethereum/contracts/package.json +++ b/target_chains/ethereum/contracts/package.json @@ -42,6 +42,7 @@ "@nomad-xyz/excessively-safe-call": "^0.0.1-rc.1", "@pythnetwork/contract-manager": "workspace:*", "@pythnetwork/entropy-sdk-solidity": "workspace:*", + "@pythnetwork/pulse-sdk-solidity": "workspace:*", "@pythnetwork/pyth-sdk-solidity": "workspace:*", "@pythnetwork/xc-admin-common": "workspace:*", "dotenv": "^10.0.0", diff --git a/target_chains/ethereum/pulse_sdk/solidity/IScheduler.sol b/target_chains/ethereum/pulse_sdk/solidity/IScheduler.sol new file mode 100644 index 0000000000..1ba78ae02b --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/IScheduler.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; +import "./SchedulerEvents.sol"; +import "./SchedulerStructs.sol"; + +interface IScheduler is SchedulerEvents { + /// @notice Creates a new subscription + /// @dev Requires msg.value to be at least the minimum balance for the subscription (calculated by getMinimumBalance()). + /// @param subscriptionParams The parameters for the subscription + /// @return subscriptionId The ID of the newly created subscription + function createSubscription( + SchedulerStructs.SubscriptionParams calldata subscriptionParams + ) external payable returns (uint256 subscriptionId); + + /// @notice Gets a subscription's parameters and status + /// @param subscriptionId The ID of the subscription + /// @return params The subscription parameters + /// @return status The subscription status + function getSubscription( + uint256 subscriptionId + ) + external + view + returns ( + SchedulerStructs.SubscriptionParams memory params, + SchedulerStructs.SubscriptionStatus memory status + ); + + /// @notice Updates an existing subscription + /// @dev You can activate or deactivate a subscription by setting isActive to true or false. Reactivating a subscription + /// requires the subscription to hold at least the minimum balance (calculated by getMinimumBalance()). + /// @dev Any Ether sent with this call (`msg.value`) will be added to the subscription's balance before processing the update. + /// @param subscriptionId The ID of the subscription to update + /// @param newSubscriptionParams The new parameters for the subscription + function updateSubscription( + uint256 subscriptionId, + SchedulerStructs.SubscriptionParams calldata newSubscriptionParams + ) external payable; + + /// @notice Updates price feeds for a subscription. + /// @dev The updateData must contain all price feeds for the subscription, not a subset or superset. + /// @dev Internally, the updateData is verified using the Pyth contract and validates update conditions. + /// The call will only succeed if the update conditions for the subscription are met. + /// @param subscriptionId The ID of the subscription + /// @param updateData The price update data from Pyth + function updatePriceFeeds( + uint256 subscriptionId, + bytes[] calldata updateData + ) external; + + /// @notice Returns the price of a price feed without any sanity checks. + /// @dev This function returns the most recent price update in this contract without any recency checks. + /// This function is unsafe as the returned price update may be arbitrarily far in the past. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use `getPricesNoOlderThan`. + /// @return prices - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPricesUnsafe( + uint256 subscriptionId, + bytes32[] calldata priceIds + ) external view returns (PythStructs.Price[] memory prices); + + /// @notice Returns the price that is no older than `age` seconds of the current time. + /// @dev This function is a sanity-checked version of `getPriceUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return prices - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPricesNoOlderThan( + uint256 subscriptionId, + bytes32[] calldata priceIds, + uint256 age + ) external view returns (PythStructs.Price[] memory prices); + + /// @notice Returns the exponentially-weighted moving average price of a price feed without any sanity checks. + /// @dev This function returns the same price as `getEmaPrice` in the case where the price is available. + /// However, if the price is not recent this function returns the latest available price. + /// + /// The returned price can be from arbitrarily far in the past; this function makes no guarantees that + /// the returned price is recent or useful for any particular application. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use either `getEmaPrice` or `getEmaPriceNoOlderThan`. + /// @return prices - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPricesUnsafe( + uint256 subscriptionId, + bytes32[] calldata priceIds + ) external view returns (PythStructs.Price[] memory prices); + + /// @notice Returns the exponentially-weighted moving average price that is no older than `age_seconds` seconds + /// of the current time. + /// @dev This function is a sanity-checked version of `getEmaPricesUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return prices - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPricesNoOlderThan( + uint256 subscriptionId, + bytes32[] calldata priceIds, + uint256 age_seconds + ) external view returns (PythStructs.Price[] memory prices); + + /// @notice Adds funds to a subscription's balance + /// @param subscriptionId The ID of the subscription + function addFunds(uint256 subscriptionId) external payable; + + /// @notice Withdraws funds from a subscription's balance. + /// @dev A minimum balance must be maintained for active subscriptions. To withdraw past + /// the minimum balance limit, deactivate the subscription first. + /// @param subscriptionId The ID of the subscription + /// @param amount The amount to withdraw + function withdrawFunds(uint256 subscriptionId, uint256 amount) external; + + /// @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 returns (uint256 minimumBalanceInWei); + + /// @notice Gets all active subscriptions with their parameters, paginated. + /// @dev This function has no access control to allow keepers to discover active subscriptions. + /// @dev Note that the order of subscription IDs returned may not be sequential and can change + /// when subscriptions are deactivated or reactivated. + /// @param startIndex The starting index within the list of active subscriptions (NOT the subscription ID). + /// @param maxResults The maximum number of results to return starting from startIndex. + /// @return subscriptionIds Array of active subscription IDs + /// @return subscriptionParams Array of subscription parameters for each active subscription + /// @return totalCount Total number of active subscriptions + function getActiveSubscriptions( + uint256 startIndex, + uint256 maxResults + ) + external + view + returns ( + uint256[] memory subscriptionIds, + SchedulerStructs.SubscriptionParams[] memory subscriptionParams, + uint256 totalCount + ); +} diff --git a/target_chains/ethereum/pulse_sdk/solidity/README.md b/target_chains/ethereum/pulse_sdk/solidity/README.md new file mode 100644 index 0000000000..cb09e4fca1 --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/README.md @@ -0,0 +1,128 @@ +# Pyth Pulse Solidity SDK + +The Pyth Pulse Solidity SDK allows you to interact with the Pyth Pulse protocol, which automatically pushes Pyth price updates to on-chain contracts based on configurable conditions. This SDK provides the interfaces and data structures needed to integrate with the Pulse service. + +## Install + +### Truffle/Hardhat + +If you are using Truffle or Hardhat, simply install the NPM package: + +```bash +npm install @pythnetwork/pulse-sdk-solidity +``` + +### Foundry + +If you are using Foundry, you will need to create an NPM project if you don't already have one. +From the root directory of your project, run: + +```bash +npm init -y +npm install @pythnetwork/pulse-sdk-solidity +``` + +Then add the following line to your `remappings.txt` file: + +```text +@pythnetwork/pulse-sdk-solidity/=node_modules/@pythnetwork/pulse-sdk-solidity +``` + +## Usage + +To use the SDK, you need the address of a Pulse contract on your blockchain. + +```solidity +import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol"; +import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol"; + +IScheduler pulse = IScheduler(
); +``` + +## Key Data Structures + +### SubscriptionParams + +This struct defines the parameters for a Pulse subscription: + +```solidity +struct SubscriptionParams { + bytes32[] priceIds; // Array of Pyth price feed IDs to subscribe to + address[] readerWhitelist; // Optional array of addresses allowed to read prices + bool whitelistEnabled; // Whether to enforce whitelist or allow anyone to read + bool isActive; // Whether the subscription is active + bool isPermanent; // Whether the subscription can be updated + UpdateCriteria updateCriteria; // When to update the price feeds +} +``` + +### SubscriptionStatus + +This struct tracks the current status of a Pulse subscription: + +```solidity +struct SubscriptionStatus { + uint256 priceLastUpdatedAt; // Timestamp of the last update. All feeds in the subscription are updated together. + uint256 balanceInWei; // Balance that will be used to fund the subscription's upkeep. + uint256 totalUpdates; // Tracks update count across all feeds in the subscription (increments by number of feeds per update) + uint256 totalSpent; // Counter of total fees paid for subscription upkeep in wei. +} +``` + +### UpdateCriteria + +This struct defines when price feeds should be updated: + +```solidity +struct UpdateCriteria { + bool updateOnHeartbeat; // Should update based on time elapsed + uint32 heartbeatSeconds; // Time interval for heartbeat updates + bool updateOnDeviation; // Should update on price deviation + uint32 deviationThresholdBps; // Price deviation threshold in basis points +} +``` + +## Creating a Subscription + +```solidity +SchedulerStructs.SubscriptionParams memory params = SchedulerStructs.SubscriptionParams({ + priceIds: new bytes32[](1), + readerWhitelist: new address[](1), + whitelistEnabled: true, + isActive: true, + isPermanent: false, + updateCriteria: SchedulerStructs.UpdateCriteria({ + updateOnHeartbeat: true, + heartbeatSeconds: 60, + updateOnDeviation: true, + deviationThresholdBps: 100 + }) +}); + +params.priceIds[0] = bytes32(...); // Pyth price feed ID +params.readerWhitelist[0] = address(...); // Allowed reader + +uint256 minBalance = pulse.getMinimumBalance(uint8(params.priceIds.length)); +uint256 subscriptionId = pulse.createSubscription{value: minBalance}(params); +``` + +## Updating a Subscription + +You can update an existing subscription's parameters using the `updateSubscription` method. Only the subscription manager (the address that created it) can update a subscription, and permanent subscriptions cannot be updated afterwards. + +## Reading Price Feeds + +```solidity +bytes32[] memory priceIds = new bytes32[](1); +priceIds[0] = bytes32(...); // Pyth price feed ID + +// Specify maximum age in seconds (e.g., 300 seconds = 5 minutes) +uint256 maxAge = 300; +PythStructs.Price[] memory prices = pulse.getPricesNoOlderThan(subscriptionId, priceIds, maxAge); + +// Access price data +int64 price = prices[0].price; +uint64 conf = prices[0].conf; +int32 expo = prices[0].expo; +uint publishTime = prices[0].publishTime; +``` diff --git a/target_chains/ethereum/pulse_sdk/solidity/SchedulerConstants.sol b/target_chains/ethereum/pulse_sdk/solidity/SchedulerConstants.sol new file mode 100644 index 0000000000..28c2cb4e0d --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/SchedulerConstants.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +// This contract holds the Scheduler structs +contract SchedulerConstants { + /// Maximum number of price feeds per subscription + uint8 public constant MAX_PRICE_IDS_PER_SUBSCRIPTION = 255; + /// Maximum number of addresses in the reader whitelist + uint8 public constant MAX_READER_WHITELIST_SIZE = 255; + /// Maximum deposit limit for permanent subscriptions in wei + uint256 public constant MAX_DEPOSIT_LIMIT = 100 ether; + + /// Maximum time in the past (relative to current block timestamp) + /// for which a price update timestamp is considered valid + /// when validating the update conditions. + /// @dev Note: We don't use this when parsing update data from the Pyth contract + /// because don't want to reject update data if it contains a price from a market + /// that closed a few days ago, since it will contain a timestamp from the last + /// trading period. We enforce this value ourselves against the maximum + /// timestamp in the provided update data. + uint64 public constant PAST_TIMESTAMP_MAX_VALIDITY_PERIOD = 1 hours; + + /// Maximum time in the future (relative to current block timestamp) + /// for which a price update timestamp is considered valid + uint64 public constant FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD = 10 seconds; + /// Fixed gas overhead component used in keeper fee calculation. + /// This is a rough estimate of the tx overhead for a keeper to call updatePriceFeeds. + uint256 public constant GAS_OVERHEAD = 30000; +} diff --git a/target_chains/ethereum/pulse_sdk/solidity/SchedulerErrors.sol b/target_chains/ethereum/pulse_sdk/solidity/SchedulerErrors.sol new file mode 100644 index 0000000000..526677f6dc --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/SchedulerErrors.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +library SchedulerErrors { + // Authorization errors + /// 0x82b42900 + error Unauthorized(); + + // Subscription state errors + /// 0xe7262b66 + error InactiveSubscription(); + /// 0xf4d678b8 + error InsufficientBalance(); + /// 0xf6181305 + error CannotUpdatePermanentSubscription(); + + // Price feed errors + /// 0xae2eaaa9 + error InvalidPriceId(bytes32 providedPriceId, bytes32 expectedPriceId); + /// 0xf14f93d1 + error InvalidPriceIdsLength(uint256 providedLength, uint256 expectedLength); + /// 0x94ec8d9a + error EmptyPriceIds(); + /// 0xb3d1acf6 + error TooManyPriceIds(uint256 provided, uint256 maximum); + /// 0xe3509591 + error DuplicatePriceId(bytes32 priceId); + /// 0xe56ccfaa + error PriceSlotMismatch(); + + // Update criteria errors + /// 0xa7bcd3ae + error InvalidUpdateCriteria(); + /// 0x7e8b0263 + error UpdateConditionsNotMet(); + /// 0x38fdebae + error TimestampTooOld( + uint256 providedUpdateTimestamp, + uint256 currentTimestamp + ); + /// 0x06daa54d + error TimestampOlderThanLastUpdate( + uint256 providedUpdateTimestamp, + uint256 lastUpdatedAt + ); + + // Whitelist errors + /// 0xbe4b60f7 + error TooManyWhitelistedReaders(uint256 provided, uint256 maximum); + /// 0x9941ad5f + error DuplicateWhitelistAddress(address addr); + + // Payment errors + /// 0xec58cd53 + error KeeperPaymentFailed(); + /// 0x82fcf1e2 + error MaxDepositLimitExceeded(); +} diff --git a/target_chains/ethereum/contracts/contracts/pulse/SchedulerEvents.sol b/target_chains/ethereum/pulse_sdk/solidity/SchedulerEvents.sol similarity index 93% rename from target_chains/ethereum/contracts/contracts/pulse/SchedulerEvents.sol rename to target_chains/ethereum/pulse_sdk/solidity/SchedulerEvents.sol index 7f0c242032..26bd2fb679 100644 --- a/target_chains/ethereum/contracts/contracts/pulse/SchedulerEvents.sol +++ b/target_chains/ethereum/pulse_sdk/solidity/SchedulerEvents.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "./SchedulerState.sol"; +import "./SchedulerStructs.sol"; interface SchedulerEvents { event SubscriptionCreated( diff --git a/target_chains/ethereum/pulse_sdk/solidity/SchedulerStructs.sol b/target_chains/ethereum/pulse_sdk/solidity/SchedulerStructs.sol new file mode 100644 index 0000000000..9cea432ed2 --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/SchedulerStructs.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +/// @title SchedulerStructs +/// @notice Contains data structures used by the Pyth Pulse protocol +contract SchedulerStructs { + /// @notice Parameters defining a Pulse subscription + struct SubscriptionParams { + bytes32[] priceIds; // Array of Pyth price feed IDs to subscribe to + address[] readerWhitelist; // Optional array of addresses allowed to read prices + bool whitelistEnabled; // Whether to enforce whitelist or allow anyone to read + bool isActive; // Whether the subscription is active + bool isPermanent; // Whether the subscription can be updated + UpdateCriteria updateCriteria; // When to update the price feeds + } + + /// @notice Status information for a Pulse subscription + struct SubscriptionStatus { + uint256 priceLastUpdatedAt; // Timestamp of the last update. All feeds in the subscription are updated together. + uint256 balanceInWei; // Balance that will be used to fund the subscription's upkeep. + uint256 totalUpdates; // Tracks update count across all feeds in the subscription (increments by number of feeds per update) + uint256 totalSpent; // Counter of total fees paid for subscription upkeep in wei. + } + + /// @notice Criteria for when price feeds should be updated + struct UpdateCriteria { + bool updateOnHeartbeat; // Should update based on time elapsed + uint32 heartbeatSeconds; // Time interval for heartbeat updates + bool updateOnDeviation; // Should update based on price deviation + uint32 deviationThresholdBps; // Price deviation threshold in basis points + } +} diff --git a/target_chains/ethereum/pulse_sdk/solidity/abis/IScheduler.json b/target_chains/ethereum/pulse_sdk/solidity/abis/IScheduler.json new file mode 100644 index 0000000000..45dc0bd324 --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/abis/IScheduler.json @@ -0,0 +1,674 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PricesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "SubscriptionActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "manager", + "type": "address" + } + ], + "name": "SubscriptionCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "SubscriptionDeactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "SubscriptionUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "addFunds", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + }, + { + "internalType": "address[]", + "name": "readerWhitelist", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "whitelistEnabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isPermanent", + "type": "bool" + }, + { + "components": [ + { + "internalType": "bool", + "name": "updateOnHeartbeat", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "heartbeatSeconds", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "updateOnDeviation", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "deviationThresholdBps", + "type": "uint32" + } + ], + "internalType": "struct SchedulerStructs.UpdateCriteria", + "name": "updateCriteria", + "type": "tuple" + } + ], + "internalType": "struct SchedulerStructs.SubscriptionParams", + "name": "subscriptionParams", + "type": "tuple" + } + ], + "name": "createSubscription", + "outputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "startIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxResults", + "type": "uint256" + } + ], + "name": "getActiveSubscriptions", + "outputs": [ + { + "internalType": "uint256[]", + "name": "subscriptionIds", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + }, + { + "internalType": "address[]", + "name": "readerWhitelist", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "whitelistEnabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isPermanent", + "type": "bool" + }, + { + "components": [ + { + "internalType": "bool", + "name": "updateOnHeartbeat", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "heartbeatSeconds", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "updateOnDeviation", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "deviationThresholdBps", + "type": "uint32" + } + ], + "internalType": "struct SchedulerStructs.UpdateCriteria", + "name": "updateCriteria", + "type": "tuple" + } + ], + "internalType": "struct SchedulerStructs.SubscriptionParams[]", + "name": "subscriptionParams", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "totalCount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + }, + { + "internalType": "uint256", + "name": "age_seconds", + "type": "uint256" + } + ], + "name": "getEmaPricesNoOlderThan", + "outputs": [ + { + "components": [ + { + "internalType": "int64", + "name": "price", + "type": "int64" + }, + { + "internalType": "uint64", + "name": "conf", + "type": "uint64" + }, + { + "internalType": "int32", + "name": "expo", + "type": "int32" + }, + { + "internalType": "uint256", + "name": "publishTime", + "type": "uint256" + } + ], + "internalType": "struct PythStructs.Price[]", + "name": "prices", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + } + ], + "name": "getEmaPricesUnsafe", + "outputs": [ + { + "components": [ + { + "internalType": "int64", + "name": "price", + "type": "int64" + }, + { + "internalType": "uint64", + "name": "conf", + "type": "uint64" + }, + { + "internalType": "int32", + "name": "expo", + "type": "int32" + }, + { + "internalType": "uint256", + "name": "publishTime", + "type": "uint256" + } + ], + "internalType": "struct PythStructs.Price[]", + "name": "prices", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "numPriceFeeds", + "type": "uint8" + } + ], + "name": "getMinimumBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "minimumBalanceInWei", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + }, + { + "internalType": "uint256", + "name": "age", + "type": "uint256" + } + ], + "name": "getPricesNoOlderThan", + "outputs": [ + { + "components": [ + { + "internalType": "int64", + "name": "price", + "type": "int64" + }, + { + "internalType": "uint64", + "name": "conf", + "type": "uint64" + }, + { + "internalType": "int32", + "name": "expo", + "type": "int32" + }, + { + "internalType": "uint256", + "name": "publishTime", + "type": "uint256" + } + ], + "internalType": "struct PythStructs.Price[]", + "name": "prices", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + } + ], + "name": "getPricesUnsafe", + "outputs": [ + { + "components": [ + { + "internalType": "int64", + "name": "price", + "type": "int64" + }, + { + "internalType": "uint64", + "name": "conf", + "type": "uint64" + }, + { + "internalType": "int32", + "name": "expo", + "type": "int32" + }, + { + "internalType": "uint256", + "name": "publishTime", + "type": "uint256" + } + ], + "internalType": "struct PythStructs.Price[]", + "name": "prices", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "getSubscription", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + }, + { + "internalType": "address[]", + "name": "readerWhitelist", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "whitelistEnabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isPermanent", + "type": "bool" + }, + { + "components": [ + { + "internalType": "bool", + "name": "updateOnHeartbeat", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "heartbeatSeconds", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "updateOnDeviation", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "deviationThresholdBps", + "type": "uint32" + } + ], + "internalType": "struct SchedulerStructs.UpdateCriteria", + "name": "updateCriteria", + "type": "tuple" + } + ], + "internalType": "struct SchedulerStructs.SubscriptionParams", + "name": "params", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "priceLastUpdatedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balanceInWei", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalUpdates", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalSpent", + "type": "uint256" + } + ], + "internalType": "struct SchedulerStructs.SubscriptionStatus", + "name": "status", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "internalType": "bytes[]", + "name": "updateData", + "type": "bytes[]" + } + ], + "name": "updatePriceFeeds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes32[]", + "name": "priceIds", + "type": "bytes32[]" + }, + { + "internalType": "address[]", + "name": "readerWhitelist", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "whitelistEnabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isPermanent", + "type": "bool" + }, + { + "components": [ + { + "internalType": "bool", + "name": "updateOnHeartbeat", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "heartbeatSeconds", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "updateOnDeviation", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "deviationThresholdBps", + "type": "uint32" + } + ], + "internalType": "struct SchedulerStructs.UpdateCriteria", + "name": "updateCriteria", + "type": "tuple" + } + ], + "internalType": "struct SchedulerStructs.SubscriptionParams", + "name": "newSubscriptionParams", + "type": "tuple" + } + ], + "name": "updateSubscription", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerConstants.json b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerConstants.json new file mode 100644 index 0000000000..cd65ffcb36 --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerConstants.json @@ -0,0 +1,80 @@ +[ + { + "inputs": [], + "name": "FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GAS_OVERHEAD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_DEPOSIT_LIMIT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_PRICE_IDS_PER_SUBSCRIPTION", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_READER_WHITELIST_SIZE", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PAST_TIMESTAMP_MAX_VALIDITY_PERIOD", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerErrors.json b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerErrors.json new file mode 100644 index 0000000000..c49a72e167 --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerErrors.json @@ -0,0 +1,170 @@ +[ + { + "inputs": [], + "name": "CannotUpdatePermanentSubscription", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "priceId", + "type": "bytes32" + } + ], + "name": "DuplicatePriceId", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "DuplicateWhitelistAddress", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPriceIds", + "type": "error" + }, + { + "inputs": [], + "name": "InactiveSubscription", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "providedPriceId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "expectedPriceId", + "type": "bytes32" + } + ], + "name": "InvalidPriceId", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "providedLength", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expectedLength", + "type": "uint256" + } + ], + "name": "InvalidPriceIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidUpdateCriteria", + "type": "error" + }, + { + "inputs": [], + "name": "KeeperPaymentFailed", + "type": "error" + }, + { + "inputs": [], + "name": "MaxDepositLimitExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "PriceSlotMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "providedUpdateTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lastUpdatedAt", + "type": "uint256" + } + ], + "name": "TimestampOlderThanLastUpdate", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "providedUpdateTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentTimestamp", + "type": "uint256" + } + ], + "name": "TimestampTooOld", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "provided", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maximum", + "type": "uint256" + } + ], + "name": "TooManyPriceIds", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "provided", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maximum", + "type": "uint256" + } + ], + "name": "TooManyWhitelistedReaders", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateConditionsNotMet", + "type": "error" + } +] diff --git a/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerEvents.json b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerEvents.json new file mode 100644 index 0000000000..fda05cfc8e --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerEvents.json @@ -0,0 +1,79 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PricesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "SubscriptionActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "manager", + "type": "address" + } + ], + "name": "SubscriptionCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "SubscriptionDeactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "subscriptionId", + "type": "uint256" + } + ], + "name": "SubscriptionUpdated", + "type": "event" + } +] diff --git a/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerStructs.json b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerStructs.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerStructs.json @@ -0,0 +1 @@ +[] diff --git a/target_chains/ethereum/pulse_sdk/solidity/package.json b/target_chains/ethereum/pulse_sdk/solidity/package.json new file mode 100644 index 0000000000..d54148e1e7 --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/package.json @@ -0,0 +1,40 @@ +{ + "name": "@pythnetwork/pulse-sdk-solidity", + "version": "1.0.0", + "description": "Automatically update price feeds with Pyth Pulse", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/pyth-network/pyth-crosschain", + "directory": "target_chains/ethereum/pulse_sdk/solidity" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test:format": "prettier --check .", + "fix:format": "prettier --write .", + "build": "generate-abis IScheduler SchedulerConstants SchedulerErrors SchedulerEvents SchedulerStructs", + "test": "git diff --exit-code abis" + }, + "keywords": [ + "pyth", + "solidity", + "price feed", + "pulse" + ], + "author": "Douro Labs", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/pyth-network/pyth-crosschain/issues" + }, + "homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/pulse_sdk/solidity", + "devDependencies": { + "abi_generator": "workspace:*", + "prettier": "catalog:", + "prettier-plugin-solidity": "catalog:" + }, + "dependencies": { + "@pythnetwork/pyth-sdk-solidity": "workspace:*" + } +} diff --git a/target_chains/ethereum/pulse_sdk/solidity/prettier.config.js b/target_chains/ethereum/pulse_sdk/solidity/prettier.config.js new file mode 100644 index 0000000000..9ddff614ce --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/prettier.config.js @@ -0,0 +1,5 @@ +import solidity from "prettier-plugin-solidity"; + +export default { + plugins: [solidity], +}; diff --git a/target_chains/ethereum/pulse_sdk/solidity/turbo.json b/target_chains/ethereum/pulse_sdk/solidity/turbo.json new file mode 100644 index 0000000000..9d364662ea --- /dev/null +++ b/target_chains/ethereum/pulse_sdk/solidity/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["abis/**"] + }, + "test": { + "dependsOn": ["build"] + } + } +}