From d3b9aac6bdc3afd6e3279f7fcc8be480103c92f3 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 2 Jan 2026 16:46:08 +0400 Subject: [PATCH 01/19] feat: PRICE submodule for Pyth price feeds --- src/interfaces/IPyth.sol | 29 + src/libraries/SafeCast.sol | 7 + .../PRICE/submodules/feeds/PythPriceFeeds.sol | 441 +++++++++ src/test/mocks/MockPyth.sol | 39 + .../submodules/feeds/PythPriceFeeds.t.sol | 865 ++++++++++++++++++ 5 files changed, 1381 insertions(+) create mode 100644 src/interfaces/IPyth.sol create mode 100644 src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol create mode 100644 src/test/mocks/MockPyth.sol create mode 100644 src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol diff --git a/src/interfaces/IPyth.sol b/src/interfaces/IPyth.sol new file mode 100644 index 000000000..58aaeda6d --- /dev/null +++ b/src/interfaces/IPyth.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.15; + +/// @notice Minimal interface for Pyth Network price feeds +/// @dev Based on Pyth Network's IPyth interface +interface IPyth { + /// @notice Price struct returned by Pyth Network + /// @param price Price value (multiply by 10^expo to get the decimal value) + /// @param conf Confidence interval, indicating that the actual asset price is ± conf (multiply by 10^expo to get the decimal value) + /// @param expo Exponent + /// @param publishTime Timestamp when price was published + struct Price { + int64 price; + uint64 conf; + int32 expo; + uint256 publishTime; + } + + /// @notice Get the price object with a published timestamp from before than `age` seconds in the past + /// @dev Returns the latest price object for the requested price feed ID, if it has been updated sufficiently recently + /// + /// @param priceId The ID of the price feed + /// @param age Maximum age of the on-chain price in seconds + /// @return price Price struct containing price, confidence interval, exponent, and publishTime + function getPriceNoOlderThan( + bytes32 priceId, + uint256 age + ) external view returns (Price memory price); +} diff --git a/src/libraries/SafeCast.sol b/src/libraries/SafeCast.sol index 4afc41c7f..07bfabb93 100644 --- a/src/libraries/SafeCast.sol +++ b/src/libraries/SafeCast.sol @@ -25,6 +25,13 @@ library SafeCast { return uint96(amount); } + function encodeUInt64(uint256 amount) internal pure returns (uint64) { + if (amount > type(uint64).max) { + revert Overflow(amount); + } + return uint64(amount); + } + function encodeUInt48(uint256 amount) internal pure returns (uint48) { if (amount > type(uint48).max) { revert Overflow(amount); diff --git a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol new file mode 100644 index 000000000..0d0c222b9 --- /dev/null +++ b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: AGPL-3.0 +/// forge-lint: disable-start(mixed-case-function) +pragma solidity >=0.8.15; + +// Interfaces +import {IPyth} from "src/interfaces/IPyth.sol"; +import {IERC165} from "@openzeppelin-4.8.0/interfaces/IERC165.sol"; +import {IVersioned} from "src/interfaces/IVersioned.sol"; + +// Libraries +import {FullMath} from "src/libraries/FullMath.sol"; +import {SafeCast} from "src/libraries/SafeCast.sol"; + +// Bophades +import {Module} from "src/Kernel.sol"; +import {Submodule, SubKeycode, toSubKeycode} from "src/Submodules.sol"; +import {PriceSubmodule} from "src/modules/PRICE/PRICE.v2.sol"; + +/// @title PythPriceFeeds +/// @author 0xJem +/// @notice Provides prices derived from Pyth Network price feed(s) +contract PythPriceFeeds is PriceSubmodule { + using FullMath for uint256; + + /// @notice Any token or pool with a decimal scale greater than this would result in an overflow + uint8 internal constant BASE_10_MAX_EXPONENT = 50; + + /// @notice Parameters for a single Pyth price feed + /// + /// @param pyth Address of the Pyth contract + /// @param priceFeedId The Pyth price feed ID + /// @param updateThreshold The maximum number of seconds elapsed since the last price feed update + /// @param maxConfidence The maximum confidence interval allowed (in output decimals scale) + struct OneFeedParams { + address pyth; + bytes32 priceFeedId; + uint48 updateThreshold; + uint64 maxConfidence; + } + + /// @notice Parameters for two Pyth price feeds + /// + /// @param firstPyth First: Address of the Pyth contract + /// @param firstPriceFeedId First: The Pyth price feed ID + /// @param firstUpdateThreshold First: The maximum number of seconds elapsed since the last price feed update + /// @param firstMaxConfidence First: The maximum confidence interval allowed (in output decimals scale) + /// @param secondPyth Second: Address of the Pyth contract + /// @param secondPriceFeedId Second: The Pyth price feed ID + /// @param secondUpdateThreshold Second: The maximum number of seconds elapsed since the last price feed update + /// @param secondMaxConfidence Second: The maximum confidence interval allowed (in output decimals scale) + struct TwoFeedParams { + address firstPyth; + bytes32 firstPriceFeedId; + uint48 firstUpdateThreshold; + uint64 firstMaxConfidence; + address secondPyth; + bytes32 secondPriceFeedId; + uint48 secondUpdateThreshold; + uint64 secondMaxConfidence; + } + + // ========== ERRORS ========== // + + /// @notice A Pyth contract address specified in the parameters is invalid + /// + /// @param paramsIndex_ The index of the parameter + /// @param feed_ The address of the Pyth contract + error Pyth_ParamsFeedInvalid(uint8 paramsIndex_, address feed_); + + /// @notice A price ID specified in the parameters is invalid + /// + /// @param paramsIndex_ The index of the parameter + /// @param priceFeedId_ The price feed ID + error Pyth_ParamsPriceIdInvalid(uint8 paramsIndex_, bytes32 priceFeedId_); + + /// @notice An update threshold specified in the parameters is invalid + /// @dev This currently occurs if the update threshold is 0 + /// + /// @param paramsIndex_ The index of the parameter + /// @param updateThreshold_ The update threshold + error Pyth_ParamsUpdateThresholdInvalid(uint8 paramsIndex_, uint48 updateThreshold_); + + /// @notice A maximum confidence specified in the parameters is invalid + /// @dev This currently occurs if the maximum confidence is 0 + /// + /// @param paramsIndex_ The index of the parameter + /// @param maxConfidence_ The maximum confidence (in output decimals scale) + error Pyth_ParamsMaxConfidenceInvalid(uint8 paramsIndex_, uint64 maxConfidence_); + + /// @notice The price feed is invalid + /// @dev This is triggered if the Pyth contract call reverts, + /// @dev and indicates that the feed address is not a valid Pyth contract. + /// + /// @param pyth_ The address of the Pyth contract + /// @param priceFeedId_ The price feed ID + error Pyth_FeedInvalid(address pyth_, bytes32 priceFeedId_); + + /// @notice The price returned by the price feed is invalid + /// @dev This could be because: + /// @dev - The price is <= 0 + /// + /// @param pyth_ The address of the Pyth contract + /// @param priceFeedId_ The price feed ID + /// @param price_ The price returned by the price feed + error Pyth_FeedPriceInvalid(address pyth_, bytes32 priceFeedId_, int64 price_); + + /// @notice The data returned by the price feed is stale + /// @dev This could be because: + /// @dev - The price feed was last updated before the update threshold + /// + /// @param pyth_ The address of the Pyth contract + /// @param priceFeedId_ The price feed ID + /// @param publishTime_ The publish time of the price feed + /// @param thresholdTimestamp_ The earliest acceptable timestamp + error Pyth_FeedPublishTimeStale( + address pyth_, + bytes32 priceFeedId_, + uint256 publishTime_, + uint256 thresholdTimestamp_ + ); + + /// @notice The confidence interval exceeds the maximum allowed + /// + /// @param pyth_ The address of the Pyth contract + /// @param priceFeedId_ The price feed ID + /// @param confidence_ The confidence interval returned by the price feed (in Pyth feed scale, i.e., 10^expo) + /// @param maxConfidence_ The maximum confidence interval allowed (in Pyth feed scale, i.e., 10^expo) + error Pyth_FeedConfidenceExcessive( + address pyth_, + bytes32 priceFeedId_, + uint64 confidence_, + uint64 maxConfidence_ + ); + + /// @notice The exponent calculation would result in an overflow + /// + /// @param pyth_ The address of the Pyth contract + /// @param priceFeedId_ The price feed ID + /// @param expo_ The exponent from the price feed + /// @param outputDecimals_ The number of decimals to return the price in + /// @param maxExponent_ The maximum exponent allowed + error Pyth_ExponentOutOfBounds( + address pyth_, + bytes32 priceFeedId_, + int32 expo_, + uint8 outputDecimals_, + uint8 maxExponent_ + ); + + // ========== CONSTRUCTOR ========== // + + constructor(Module parent_) Submodule(parent_) {} + + // ========== KERNEL FUNCTIONS =========== // + + /// @inheritdoc Submodule + function SUBKEYCODE() public pure override returns (SubKeycode) { + return toSubKeycode("PRICE.PYTH"); + } + + /// @inheritdoc Submodule + function VERSION() public pure override returns (uint8 major, uint8 minor) { + major = 1; + minor = 0; + } + + // ========== PRICE FEED FUNCTIONS ========== // + + /// @notice Validates the result of the price feed + /// @dev This function will revert if: + /// @dev - Price <= 0 + /// @dev - Publish time is before the update threshold from the current time + /// @dev - Confidence interval exceeds the maximum allowed + /// + /// @param pyth_ Pyth contract address + /// @param priceFeedId_ The price feed ID + /// @param priceData The price data returned by the Pyth contract + /// @param blockTimestamp The current block timestamp + /// @param paramsUpdateThreshold The maximum number of seconds elapsed since the last price feed update + /// @param paramsMaxConfidence The maximum confidence interval allowed (in Pyth feed scale, i.e., 10^expo) + function _validatePriceFeedResult( + address pyth_, + bytes32 priceFeedId_, + IPyth.Price memory priceData, + uint256 blockTimestamp, + uint48 paramsUpdateThreshold, + uint64 paramsMaxConfidence + ) internal pure { + if (priceData.price <= 0) + revert Pyth_FeedPriceInvalid(pyth_, priceFeedId_, priceData.price); + + if (priceData.publishTime < blockTimestamp - uint256(paramsUpdateThreshold)) + revert Pyth_FeedPublishTimeStale( + pyth_, + priceFeedId_, + priceData.publishTime, + blockTimestamp - uint256(paramsUpdateThreshold) + ); + + if (priceData.conf > paramsMaxConfidence) + revert Pyth_FeedConfidenceExcessive( + pyth_, + priceFeedId_, + priceData.conf, + paramsMaxConfidence + ); + } + + /// @notice Retrieves the latest price returned by the specified Pyth price feed. + /// @dev The result is validated using `_validatePriceFeedResult`, and will revert if invalid + /// + /// @param pyth_ Pyth contract address + /// @param priceFeedId_ The Pyth price feed ID + /// @param updateThreshold_ The maximum number of seconds elapsed since the last price feed update + /// @param maxConfidence_ The maximum confidence interval allowed (in output decimals scale) + /// @param outputDecimals_ The number of decimals to return the price in + /// @return uint256 The validated price in the scale of `outputDecimals_` + function _getFeedPrice( + address pyth_, + bytes32 priceFeedId_, + uint48 updateThreshold_, + uint64 maxConfidence_, + uint8 outputDecimals_ + ) internal view returns (uint256) { + IPyth.Price memory priceData; + { + try IPyth(pyth_).getPriceNoOlderThan(priceFeedId_, uint256(updateThreshold_)) returns ( + IPyth.Price memory pythPrice + ) { + priceData = pythPrice; + } catch (bytes memory) { + revert Pyth_FeedInvalid(pyth_, priceFeedId_); + } + } + + // Convert maxConfidence from output decimals scale to Pyth feed scale (10^expo) + // Formula: maxConfidenceInPythScale = maxConfidence * 10^expo / 10^outputDecimals + // = maxConfidence * 10^(expo - outputDecimals) + // Note: Result is cast to uint64 since it's compared against priceData.conf (uint64) + uint64 maxConfidenceInPythScale; + int256 confidenceExponent = int256(priceData.expo) - int256(uint256(outputDecimals_)); + if (confidenceExponent >= 0) { + maxConfidenceInPythScale = SafeCast.encodeUInt64( + uint256(maxConfidence_).mulDiv(10 ** uint256(confidenceExponent), 1) + ); + } else { + maxConfidenceInPythScale = SafeCast.encodeUInt64( + uint256(maxConfidence_).mulDiv(1, 10 ** uint256(-confidenceExponent)) + ); + } + + _validatePriceFeedResult( + pyth_, + priceFeedId_, + priceData, + block.timestamp, + updateThreshold_, + maxConfidenceInPythScale + ); + + // Check exponent bounds: expo + outputDecimals <= BASE_10_MAX_EXPONENT + int256 totalExponent = int256(priceData.expo) + int256(uint256(outputDecimals_)); + if (totalExponent > int256(uint256(BASE_10_MAX_EXPONENT))) { + revert Pyth_ExponentOutOfBounds( + pyth_, + priceFeedId_, + priceData.expo, + outputDecimals_, + BASE_10_MAX_EXPONENT + ); + } + + uint256 price = uint256(int256(priceData.price)); + + // Convert price: outputPrice = price * 10^(expo + outputDecimals) + // If totalExponent >= 0: multiply by 10^totalExponent + // If totalExponent < 0: divide by 10^(-totalExponent) + if (totalExponent >= 0) { + return price.mulDiv(10 ** uint256(totalExponent), 1); + } else { + return price.mulDiv(1, 10 ** uint256(-totalExponent)); + } + } + + /// @notice Returns the price from a single Pyth feed, as specified in `params_`. + /// @dev This function will revert if: + /// @dev - Any parameter is invalid + /// @dev - The exponent calculation would result in an overflow + /// @dev - The price feed's results are invalid + /// + /// @param outputDecimals_ The number of output decimals (assumed to be the same as PRICE decimals) + /// @param params_ Pyth feed parameters of type `OneFeedParams` + /// @return Price in the scale of `outputDecimals_` + function getOneFeedPrice( + address, + uint8 outputDecimals_, + bytes calldata params_ + ) external view returns (uint256) { + // Decode params + OneFeedParams memory params = abi.decode(params_, (OneFeedParams)); + if (params.pyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.pyth); + if (params.priceFeedId == bytes32(0)) + revert Pyth_ParamsPriceIdInvalid(1, params.priceFeedId); + if (params.updateThreshold == 0) + revert Pyth_ParamsUpdateThresholdInvalid(2, params.updateThreshold); + if (params.maxConfidence == 0) + revert Pyth_ParamsMaxConfidenceInvalid(3, params.maxConfidence); + + uint256 feedPrice = _getFeedPrice( + params.pyth, + params.priceFeedId, + params.updateThreshold, + params.maxConfidence, + outputDecimals_ + ); + + return feedPrice; + } + + /// @notice Returns the result of dividing the price from the first Pyth feed by the price from the second. + /// @dev For example, passing in ETH/USD and DAI/USD will return the ETH/DAI price. + /// + /// @dev This function will revert if: + /// @dev - Any parameter is invalid + /// @dev - The exponent calculation would result in an overflow + /// @dev - Any of the price feeds' results are invalid + /// + /// @param outputDecimals_ The number of output decimals (assumed to be the same as PRICE decimals) + /// @param params_ Pyth feed parameters of type `TwoFeedParams` + /// @return uint256 Price in the scale of `outputDecimals_`. + function getTwoFeedPriceDiv( + address, + uint8 outputDecimals_, + bytes calldata params_ + ) external view returns (uint256) { + // Decode params + TwoFeedParams memory params = abi.decode(params_, (TwoFeedParams)); + if (params.firstPyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.firstPyth); + if (params.firstPriceFeedId == bytes32(0)) + revert Pyth_ParamsPriceIdInvalid(1, params.firstPriceFeedId); + if (params.firstUpdateThreshold == 0) + revert Pyth_ParamsUpdateThresholdInvalid(2, params.firstUpdateThreshold); + if (params.firstMaxConfidence == 0) + revert Pyth_ParamsMaxConfidenceInvalid(3, params.firstMaxConfidence); + if (params.secondPyth == address(0)) revert Pyth_ParamsFeedInvalid(4, params.secondPyth); + if (params.secondPriceFeedId == bytes32(0)) + revert Pyth_ParamsPriceIdInvalid(5, params.secondPriceFeedId); + if (params.secondUpdateThreshold == 0) + revert Pyth_ParamsUpdateThresholdInvalid(6, params.secondUpdateThreshold); + if (params.secondMaxConfidence == 0) + revert Pyth_ParamsMaxConfidenceInvalid(7, params.secondMaxConfidence); + + // Get prices from feeds (both already converted to outputDecimals scale) + uint256 numeratorPrice = _getFeedPrice( + params.firstPyth, + params.firstPriceFeedId, + params.firstUpdateThreshold, + params.firstMaxConfidence, + outputDecimals_ + ); + uint256 denominatorPrice = _getFeedPrice( + params.secondPyth, + params.secondPriceFeedId, + params.secondUpdateThreshold, + params.secondMaxConfidence, + outputDecimals_ + ); + + // Convert to numerator/denominator price and return + uint256 priceResult = numeratorPrice.mulDiv(10 ** outputDecimals_, denominatorPrice); + + return priceResult; + } + + /// @notice Returns the result of multiplying the price from the first Pyth feed by the price from the second. + /// @dev For example, passing in ETH/DAI and DAI/USD will return the ETH/USD price. + /// + /// @dev This function will revert if: + /// @dev - Any parameter is invalid + /// @dev - The exponent calculation would result in an overflow + /// @dev - Any of the price feeds' results are invalid + /// + /// @param outputDecimals_ The number of output decimals (assumed to be the same as PRICE decimals) + /// @param params_ Pyth feed parameters of type `TwoFeedParams` + /// @return uint256 Price in the scale of `outputDecimals_`. + function getTwoFeedPriceMul( + address, + uint8 outputDecimals_, + bytes calldata params_ + ) external view returns (uint256) { + // Decode params + TwoFeedParams memory params = abi.decode(params_, (TwoFeedParams)); + if (params.firstPyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.firstPyth); + if (params.firstPriceFeedId == bytes32(0)) + revert Pyth_ParamsPriceIdInvalid(1, params.firstPriceFeedId); + if (params.firstUpdateThreshold == 0) + revert Pyth_ParamsUpdateThresholdInvalid(2, params.firstUpdateThreshold); + if (params.firstMaxConfidence == 0) + revert Pyth_ParamsMaxConfidenceInvalid(3, params.firstMaxConfidence); + if (params.secondPyth == address(0)) revert Pyth_ParamsFeedInvalid(4, params.secondPyth); + if (params.secondPriceFeedId == bytes32(0)) + revert Pyth_ParamsPriceIdInvalid(5, params.secondPriceFeedId); + if (params.secondUpdateThreshold == 0) + revert Pyth_ParamsUpdateThresholdInvalid(6, params.secondUpdateThreshold); + if (params.secondMaxConfidence == 0) + revert Pyth_ParamsMaxConfidenceInvalid(7, params.secondMaxConfidence); + + // Get prices from feeds (both already converted to outputDecimals scale) + uint256 firstPrice = _getFeedPrice( + params.firstPyth, + params.firstPriceFeedId, + params.firstUpdateThreshold, + params.firstMaxConfidence, + outputDecimals_ + ); + uint256 secondPrice = _getFeedPrice( + params.secondPyth, + params.secondPriceFeedId, + params.secondUpdateThreshold, + params.secondMaxConfidence, + outputDecimals_ + ); + + // Convert to first * second price and return + uint256 priceResult = firstPrice.mulDiv(secondPrice, 10 ** outputDecimals_); + + return priceResult; + } + + // ========== IERC165 FUNCTIONS ========== // + + /// @notice Returns whether the contract supports the given interface + /// + /// @param interfaceId The interface identifier to check + /// @return bool Whether the contract supports the interface + function supportsInterface(bytes4 interfaceId) external view virtual returns (bool) { + return + interfaceId == type(IERC165).interfaceId || interfaceId == type(IVersioned).interfaceId; + } +} +/// forge-lint: disable-end(mixed-case-function) diff --git a/src/test/mocks/MockPyth.sol b/src/test/mocks/MockPyth.sol new file mode 100644 index 000000000..077043e7b --- /dev/null +++ b/src/test/mocks/MockPyth.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +/// forge-lint: disable-start(mixed-case-variable) +pragma solidity ^0.8.0; + +import {IPyth} from "interfaces/IPyth.sol"; + +contract MockPyth is IPyth { + mapping(bytes32 => Price) public s_prices; + + function setPrice( + bytes32 priceId, + int64 price, + uint64 conf, + int32 expo, + uint256 publishTime + ) public { + s_prices[priceId] = Price({price: price, conf: conf, expo: expo, publishTime: publishTime}); + } + + function getPriceNoOlderThan( + bytes32 priceId, + uint256 age + ) external view override returns (Price memory) { + Price memory priceData = s_prices[priceId]; + + // Check if price exists + if (priceData.publishTime == 0) { + revert("PriceFeedNotFound"); + } + + // Check if price is stale + if (priceData.publishTime < block.timestamp - age) { + revert("StalePrice"); + } + + return priceData; + } +} +/// forge-lint: disable-end(mixed-case-variable) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol new file mode 100644 index 000000000..18236cd09 --- /dev/null +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -0,0 +1,865 @@ +// SPDX-License-Identifier: Unlicense +/// forge-lint: disable-start(mixed-case-variable,mixed-case-function) +pragma solidity >=0.8.0; + +// Test +import {Test} from "@forge-std-1.9.6/Test.sol"; +import {ModuleTestFixtureGenerator} from "src/test/lib/ModuleTestFixtureGenerator.sol"; + +// Mocks +import {MockPrice} from "src/test/mocks/MockPrice.v2.sol"; +import {MockPyth} from "src/test/mocks/MockPyth.sol"; + +// Libraries +import {FullMath} from "src/libraries/FullMath.sol"; +import {SafeCast} from "src/libraries/SafeCast.sol"; + +// Bophades +import {Kernel} from "src/Kernel.sol"; +import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; + +contract PythPriceFeedsTest is Test { + using FullMath for uint256; + using ModuleTestFixtureGenerator for PythPriceFeeds; + + MockPyth internal pyth; + MockPyth internal secondPyth; + + Kernel internal kernel; + MockPrice internal mockPrice; + PythPriceFeeds internal pythSubmodule; + + bytes32 internal constant PRICE_ID_1 = keccak256("PRICE_ID_1"); + bytes32 internal constant PRICE_ID_2 = keccak256("PRICE_ID_2"); + bytes32 internal constant PRICE_ID_3 = keccak256("PRICE_ID_3"); + + // Example: price=123456789, expo=-8 represents 1.23456789 + int64 internal constant PRICE_1 = 123456789; + int32 internal constant EXPO_1 = -8; + uint64 internal constant CONF_1 = 1000000; + uint256 internal constant EXPECTED_PRICE_1_18_DEC = 1234567890000000000; // 1.23456789 * 10^18 + + // Example: price=100000000, expo=8 represents 100000000 * 10^8 + int64 internal constant PRICE_2 = 100000000; + int32 internal constant EXPO_2 = 8; + uint64 internal constant CONF_2 = 2000000; + + // Example: price=500000000, expo=-18 represents 0.0000000005 + int64 internal constant PRICE_3 = 500000000; + int32 internal constant EXPO_3 = -18; + uint64 internal constant CONF_3 = 500000; + + uint8 internal constant PRICE_DECIMALS = 18; + uint48 internal constant UPDATE_THRESHOLD = 100; + // MAX_CONFIDENCE is in output decimals (18 decimals) + // CONF_1 = 1000000 with expo=-8 + // In decimal: 1000000 * 10^-8 = 0.01 + // In output decimals (18): 0.01 * 10^18 = 10000000000000000 (1e16) + // Set MAX_CONFIDENCE to 2e16 to allow CONF_1 but reject higher values + uint64 internal constant MAX_CONFIDENCE = 20000000000000000; // 2 * 10^16 in 18 decimals + + uint8 internal constant BASE_10_MAX_EXPONENT = 50; + + function setUp() public { + vm.warp(51 * 365 * 24 * 60 * 60); // Set timestamp at roughly Jan 1, 2021 + + // Set up the Pyth submodule + { + // Deploy kernel + kernel = new Kernel(); + + // Deploy mockPrice + mockPrice = new MockPrice(kernel, uint8(18), uint32(8 hours)); + mockPrice.setTimestamp(uint48(block.timestamp)); + mockPrice.setPriceDecimals(PRICE_DECIMALS); + + // Deploy Pyth submodule + pythSubmodule = new PythPriceFeeds(mockPrice); + } + + // Set up the Pyth contracts + { + pyth = new MockPyth(); + secondPyth = new MockPyth(); + + // Set up price feed 1: 1.23456789 (expo=-8) + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + // Set up price feed 2: 100000000 * 10^8 (expo=8) + pyth.setPrice(PRICE_ID_2, PRICE_2, CONF_2, EXPO_2, block.timestamp); + + // Set up price feed 3: 0.0000000005 (expo=-18) + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); + } + } + + // ========= HELPER METHODS ========= // + + function encodeOneFeedParams( + address pyth_, + bytes32 priceFeedId, + uint48 updateThreshold, + uint64 maxConfidence + ) internal pure returns (bytes memory params) { + return abi.encode(pyth_, priceFeedId, updateThreshold, maxConfidence); + } + + function encodeTwoFeedParams( + address firstPyth, + bytes32 firstPriceFeedId, + uint48 firstUpdateThreshold, + uint64 firstMaxConfidence, + address secondPyth, + bytes32 secondPriceFeedId, + uint48 secondUpdateThreshold, + uint64 secondMaxConfidence + ) internal pure returns (bytes memory params) { + return + abi.encode( + firstPyth, + firstPriceFeedId, + firstUpdateThreshold, + firstMaxConfidence, + secondPyth, + secondPriceFeedId, + secondUpdateThreshold, + secondMaxConfidence + ); + } + + // ========= ONE FEED TESTS ========= // + + // given all parameters are valid + // [X] it returns the correct price in output decimals + function test_getOneFeedPrice_success() public view { + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should match expected value for expo=-8" + ); + } + + // given the pyth contract address is zero + // [X] it reverts with Pyth_ParamsFeedInvalid + function test_getOneFeedPrice_revertsOnParamsFeedInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + 0, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(0), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceIdInvalid + function test_getOneFeedPrice_revertsOnParamsPriceIdInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceIdInvalid.selector, + 1, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getOneFeedPrice_revertsOnParamsUpdateThresholdInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 2, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams(address(pyth), PRICE_ID_1, 0, MAX_CONFIDENCE); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getOneFeedPrice_revertsOnParamsMaxConfidenceInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 3, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams(address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, 0); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given expo + outputDecimals > BASE_10_MAX_EXPONENT + // [X] it reverts with Pyth_ExponentOutOfBounds + function test_getOneFeedPrice_revertsOnExponentOutOfBounds() public { + // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT + int32 largeExpo = 45; // 45 + 18 = 63 > 50 + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, largeExpo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, + address(pyth), + PRICE_ID_1, + largeExpo, + PRICE_DECIMALS, + BASE_10_MAX_EXPONENT + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the pyth contract doesn't implement IPyth + // [X] it reverts with Pyth_FeedInvalid + function test_getOneFeedPrice_revertsOnFeedInvalid() public { + // Use a contract that doesn't implement IPyth + address invalidPyth = address(0x1234); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedInvalid.selector, + invalidPyth, + PRICE_ID_1 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + invalidPyth, + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the price is zero + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getOneFeedPrice_revertsOnFeedPriceInvalid() public { + // Set price to zero + pyth.setPrice(PRICE_ID_1, 0, CONF_1, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the price is negative + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getOneFeedPrice_revertsOnFeedPriceInvalid_negative() public { + // Set price to negative + pyth.setPrice(PRICE_ID_1, -100, CONF_1, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_1, + -100 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the publish time is before the update threshold + // [X] it reverts with Pyth_FeedPublishTimeStale + function test_getOneFeedPrice_revertsOnFeedPublishTimeStale() public { + // Set publish time to be stale + uint256 staleTime = block.timestamp - UPDATE_THRESHOLD - 1; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, staleTime); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPublishTimeStale.selector, + address(pyth), + PRICE_ID_1, + staleTime, + block.timestamp - UPDATE_THRESHOLD + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the confidence interval exceeds the maximum (after conversion to Pyth scale) + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_revertsOnFeedConfidenceExcessive() public { + // MAX_CONFIDENCE = 2e16 in output decimals (18) + // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals, so it should pass + // To exceed MAX_CONFIDENCE (2e16), we need conf * 10^10 > 2e16, so conf > 2e6 + // Use conf = 3000000 (3e6) to ensure it exceeds the threshold + // 3e6 * 10^10 = 3e16 in output decimals, which exceeds MAX_CONFIDENCE of 2e16 + uint64 highConf = 3000000; // 3e6 + pyth.setPrice(PRICE_ID_1, PRICE_1, highConf, EXPO_1, block.timestamp); + + // Calculate expected maxConfidence in Pyth scale using the same formula as the code + // confidenceExponent = expo - outputDecimals = -8 - 18 = -26 + // Since it's negative: maxConfidence_.mulDiv(1, 10^26) + int256 confidenceExponent = int256(EXPO_1) - int256(uint256(PRICE_DECIMALS)); + uint64 maxConfInPythScale; + if (confidenceExponent >= 0) { + maxConfInPythScale = SafeCast.encodeUInt64( + uint256(MAX_CONFIDENCE).mulDiv(10 ** uint256(confidenceExponent), 1) + ); + } else { + maxConfInPythScale = SafeCast.encodeUInt64( + uint256(MAX_CONFIDENCE).mulDiv(1, 10 ** uint256(-confidenceExponent)) + ); + } + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + highConf, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given expo is negative (expo = -8, outputDecimals = 18) + // [X] it correctly converts the price by multiplying + function test_getOneFeedPrice_success_expoNegative() public view { + // expo = -8, outputDecimals = 18, totalExponent = 10 + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // price = 123456789, expo = -8, outputDecimals = 18 + // outputPrice = 123456789 * 10^(10) = 1234567890000000000 + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should match expected value for negative expo" + ); + } + + // given expo is positive (expo = 8, outputDecimals = 18) + // [X] it correctly converts the price by multiplying + function test_getOneFeedPrice_success_expoPositive() public view { + // expo = 8, outputDecimals = 18, totalExponent = 26 + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_2, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // price = 100000000, expo = 8, outputDecimals = 18 + // outputPrice = 100000000 * 10^26 + assertEq( + priceInt, + 100000000 * 10 ** 26, + "Price should match expected value for positive expo" + ); + } + + // given expo is zero (expo = 0, outputDecimals = 18) + // [X] it correctly converts the price by multiplying + function test_getOneFeedPrice_success_expoZero() public { + // expo = 0, outputDecimals = 18, totalExponent = 18 + int64 price = 123456789; + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // outputPrice = 123456789 * 10^18 + assertEq(priceInt, 123456789 * 10 ** 18, "Price should match expected value for zero expo"); + } + + // given expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // [X] it returns the price without scaling + function test_getOneFeedPrice_success_expoEqualsNegativeOutputDecimals() public { + // expo = -18, outputDecimals = 18, totalExponent = 0 + int64 price = 123456789; + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // outputPrice = 123456789 (no scaling needed) + assertEq( + priceInt, + uint256(uint64(price)), + "Price should match input value when expo equals negative outputDecimals" + ); + } + + // given expo is very negative (expo = -20, outputDecimals = 18) + // [X] it correctly converts the price by dividing + function test_getOneFeedPrice_success_expoVeryNegative() public { + // expo = -20, outputDecimals = 18, totalExponent = -2 + int64 price = 100000000; + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // outputPrice = 100000000 / 10^2 = 1000000 + assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); + } + + // given expo is very positive (expo = 20, outputDecimals = 18) + // [X] it correctly converts the price by multiplying + function test_getOneFeedPrice_success_expoVeryPositive() public { + // expo = 20, outputDecimals = 18, totalExponent = 38 + int64 price = 100000000; + int32 expo = 20; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // outputPrice = 100000000 * 10^38 + assertEq( + priceInt, + 100000000 * 10 ** 38, + "Price should match expected value for very positive expo" + ); + } + + // given the confidence interval is below the maximum (after conversion) + // [X] it returns the correct price + function test_getOneFeedPrice_success_confidenceEqualsMaximum() public { + // Confidence interval equals maximum threshold + // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals + // MAX_CONFIDENCE = 2e16, so CONF_1 should pass + // This test verifies that a confidence that is below the maximum passes + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when confidence is below maximum" + ); + } + + // given the publish time equals the threshold boundary + // [X] it returns the correct price + function test_getOneFeedPrice_success_publishTimeEqualsThreshold() public { + // Publish time equals threshold boundary + uint256 thresholdTime = block.timestamp - UPDATE_THRESHOLD; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, thresholdTime); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when publish time equals threshold" + ); + } + + // ========= TWO FEED TESTS - DIV ========= // + + // given all parameters are valid for two feeds + // [X] it returns the correct divided price (first / second) in output decimals + function test_getTwoFeedPriceDiv_success() public view { + // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals + // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals + // Result: 1234567890000000000 / 500000000 = 2469135780 + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // (1234567890000000000 * 10^18) / 500000000 = 2469135780000000000000000000 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq(priceInt, expected, "Divided price should match expected calculation"); + } + + // given the first pyth contract address is zeros + // [X] it reverts with Pyth_ParamsFeedInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsFirstFeedInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + 0, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(0), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceIdInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsFirstPriceIdInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceIdInvalid.selector, + 1, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsFirstUpdateThresholdInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 2, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + 0, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsFirstMaxConfidenceInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 3, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + 0, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second pyth contract address is zero + // [X] it reverts with Pyth_ParamsFeedInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsSecondFeedInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + 4, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(0), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceIdInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsSecondPriceIdInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceIdInvalid.selector, + 5, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsSecondUpdateThresholdInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 6, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + 0, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceDiv_revertsOnParamsSecondMaxConfidenceInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 7, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + 0 + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first feed has invalid price (zero) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceDiv_revertsOnFirstFeedInvalid() public { + // First feed has invalid price + pyth.setPrice(PRICE_ID_1, 0, CONF_1, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_1, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second feed has invalid price (zero) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceDiv_revertsOnSecondFeedInvalid() public { + // Second feed has invalid price + pyth.setPrice(PRICE_ID_3, 0, CONF_3, EXPO_3, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_3, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // ========= TWO FEED TESTS - MUL ========= // + + // given all parameters are valid for two feeds + // [X] it returns the correct multiplied price (first * second) in output decimals + function test_getTwoFeedPriceMul_success() public view { + // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals + // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals + // Result: (1234567890000000000 * 500000000) / 10^18 + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // (1234567890000000000 * 500000000) / 10^18 = 617283945000 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Multiplied price should match expected calculation"); + } + + // given the first pyth contract address is zero + // [X] it reverts with Pyth_ParamsFeedInvalid + function test_getTwoFeedPriceMul_revertsOnParamsFirstFeedInvalid() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + 0, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(0), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } +} +/// forge-lint: disable-end(mixed-case-variable,mixed-case-function) From 2d8f1b3bceae130543d9d40453fc79d4f9450e0a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 6 Jan 2026 13:42:43 +0400 Subject: [PATCH 02/19] fix: rename error for invalid price feed ID --- .../PRICE/submodules/feeds/PythPriceFeeds.sol | 14 +++++++------- .../PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol index 0d0c222b9..3510549bb 100644 --- a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol +++ b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol @@ -67,11 +67,11 @@ contract PythPriceFeeds is PriceSubmodule { /// @param feed_ The address of the Pyth contract error Pyth_ParamsFeedInvalid(uint8 paramsIndex_, address feed_); - /// @notice A price ID specified in the parameters is invalid + /// @notice A price feed ID specified in the parameters is invalid /// /// @param paramsIndex_ The index of the parameter /// @param priceFeedId_ The price feed ID - error Pyth_ParamsPriceIdInvalid(uint8 paramsIndex_, bytes32 priceFeedId_); + error Pyth_ParamsPriceFeedIdInvalid(uint8 paramsIndex_, bytes32 priceFeedId_); /// @notice An update threshold specified in the parameters is invalid /// @dev This currently occurs if the update threshold is 0 @@ -300,7 +300,7 @@ contract PythPriceFeeds is PriceSubmodule { OneFeedParams memory params = abi.decode(params_, (OneFeedParams)); if (params.pyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.pyth); if (params.priceFeedId == bytes32(0)) - revert Pyth_ParamsPriceIdInvalid(1, params.priceFeedId); + revert Pyth_ParamsPriceFeedIdInvalid(1, params.priceFeedId); if (params.updateThreshold == 0) revert Pyth_ParamsUpdateThresholdInvalid(2, params.updateThreshold); if (params.maxConfidence == 0) @@ -337,14 +337,14 @@ contract PythPriceFeeds is PriceSubmodule { TwoFeedParams memory params = abi.decode(params_, (TwoFeedParams)); if (params.firstPyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.firstPyth); if (params.firstPriceFeedId == bytes32(0)) - revert Pyth_ParamsPriceIdInvalid(1, params.firstPriceFeedId); + revert Pyth_ParamsPriceFeedIdInvalid(1, params.firstPriceFeedId); if (params.firstUpdateThreshold == 0) revert Pyth_ParamsUpdateThresholdInvalid(2, params.firstUpdateThreshold); if (params.firstMaxConfidence == 0) revert Pyth_ParamsMaxConfidenceInvalid(3, params.firstMaxConfidence); if (params.secondPyth == address(0)) revert Pyth_ParamsFeedInvalid(4, params.secondPyth); if (params.secondPriceFeedId == bytes32(0)) - revert Pyth_ParamsPriceIdInvalid(5, params.secondPriceFeedId); + revert Pyth_ParamsPriceFeedIdInvalid(5, params.secondPriceFeedId); if (params.secondUpdateThreshold == 0) revert Pyth_ParamsUpdateThresholdInvalid(6, params.secondUpdateThreshold); if (params.secondMaxConfidence == 0) @@ -392,14 +392,14 @@ contract PythPriceFeeds is PriceSubmodule { TwoFeedParams memory params = abi.decode(params_, (TwoFeedParams)); if (params.firstPyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.firstPyth); if (params.firstPriceFeedId == bytes32(0)) - revert Pyth_ParamsPriceIdInvalid(1, params.firstPriceFeedId); + revert Pyth_ParamsPriceFeedIdInvalid(1, params.firstPriceFeedId); if (params.firstUpdateThreshold == 0) revert Pyth_ParamsUpdateThresholdInvalid(2, params.firstUpdateThreshold); if (params.firstMaxConfidence == 0) revert Pyth_ParamsMaxConfidenceInvalid(3, params.firstMaxConfidence); if (params.secondPyth == address(0)) revert Pyth_ParamsFeedInvalid(4, params.secondPyth); if (params.secondPriceFeedId == bytes32(0)) - revert Pyth_ParamsPriceIdInvalid(5, params.secondPriceFeedId); + revert Pyth_ParamsPriceFeedIdInvalid(5, params.secondPriceFeedId); if (params.secondUpdateThreshold == 0) revert Pyth_ParamsUpdateThresholdInvalid(6, params.secondUpdateThreshold); if (params.secondMaxConfidence == 0) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 18236cd09..2332fcde1 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -167,10 +167,10 @@ contract PythPriceFeedsTest is Test { } // given the price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceIdInvalid - function test_getOneFeedPrice_revertsOnParamsPriceIdInvalid() public { + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getOneFeedPrice_revertsOnParamsPriceFeedIdInvalid() public { bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceIdInvalid.selector, + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 1, bytes32(0) ); @@ -600,10 +600,10 @@ contract PythPriceFeedsTest is Test { } // given the first price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceIdInvalid + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid function test_getTwoFeedPriceDiv_revertsOnParamsFirstPriceIdInvalid() public { bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceIdInvalid.selector, + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 1, bytes32(0) ); @@ -692,10 +692,10 @@ contract PythPriceFeedsTest is Test { } // given the second price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceIdInvalid + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid function test_getTwoFeedPriceDiv_revertsOnParamsSecondPriceIdInvalid() public { bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceIdInvalid.selector, + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 5, bytes32(0) ); From 1dbd316a67740b5fa90b33c07d57a03c881f9554 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 6 Jan 2026 14:46:36 +0400 Subject: [PATCH 03/19] fix: more intuitive error if the Pyth contract address is incorrect --- .../PRICE/submodules/feeds/PythPriceFeeds.sol | 12 ++++++------ .../submodules/feeds/PythPriceFeeds.t.sol | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol index 3510549bb..0e77182c6 100644 --- a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol +++ b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol @@ -65,7 +65,7 @@ contract PythPriceFeeds is PriceSubmodule { /// /// @param paramsIndex_ The index of the parameter /// @param feed_ The address of the Pyth contract - error Pyth_ParamsFeedInvalid(uint8 paramsIndex_, address feed_); + error Pyth_ParamsPythInvalid(uint8 paramsIndex_, address feed_); /// @notice A price feed ID specified in the parameters is invalid /// @@ -298,7 +298,7 @@ contract PythPriceFeeds is PriceSubmodule { ) external view returns (uint256) { // Decode params OneFeedParams memory params = abi.decode(params_, (OneFeedParams)); - if (params.pyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.pyth); + if (params.pyth == address(0)) revert Pyth_ParamsPythInvalid(0, params.pyth); if (params.priceFeedId == bytes32(0)) revert Pyth_ParamsPriceFeedIdInvalid(1, params.priceFeedId); if (params.updateThreshold == 0) @@ -335,14 +335,14 @@ contract PythPriceFeeds is PriceSubmodule { ) external view returns (uint256) { // Decode params TwoFeedParams memory params = abi.decode(params_, (TwoFeedParams)); - if (params.firstPyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.firstPyth); + if (params.firstPyth == address(0)) revert Pyth_ParamsPythInvalid(0, params.firstPyth); if (params.firstPriceFeedId == bytes32(0)) revert Pyth_ParamsPriceFeedIdInvalid(1, params.firstPriceFeedId); if (params.firstUpdateThreshold == 0) revert Pyth_ParamsUpdateThresholdInvalid(2, params.firstUpdateThreshold); if (params.firstMaxConfidence == 0) revert Pyth_ParamsMaxConfidenceInvalid(3, params.firstMaxConfidence); - if (params.secondPyth == address(0)) revert Pyth_ParamsFeedInvalid(4, params.secondPyth); + if (params.secondPyth == address(0)) revert Pyth_ParamsPythInvalid(4, params.secondPyth); if (params.secondPriceFeedId == bytes32(0)) revert Pyth_ParamsPriceFeedIdInvalid(5, params.secondPriceFeedId); if (params.secondUpdateThreshold == 0) @@ -390,14 +390,14 @@ contract PythPriceFeeds is PriceSubmodule { ) external view returns (uint256) { // Decode params TwoFeedParams memory params = abi.decode(params_, (TwoFeedParams)); - if (params.firstPyth == address(0)) revert Pyth_ParamsFeedInvalid(0, params.firstPyth); + if (params.firstPyth == address(0)) revert Pyth_ParamsPythInvalid(0, params.firstPyth); if (params.firstPriceFeedId == bytes32(0)) revert Pyth_ParamsPriceFeedIdInvalid(1, params.firstPriceFeedId); if (params.firstUpdateThreshold == 0) revert Pyth_ParamsUpdateThresholdInvalid(2, params.firstUpdateThreshold); if (params.firstMaxConfidence == 0) revert Pyth_ParamsMaxConfidenceInvalid(3, params.firstMaxConfidence); - if (params.secondPyth == address(0)) revert Pyth_ParamsFeedInvalid(4, params.secondPyth); + if (params.secondPyth == address(0)) revert Pyth_ParamsPythInvalid(4, params.secondPyth); if (params.secondPriceFeedId == bytes32(0)) revert Pyth_ParamsPriceFeedIdInvalid(5, params.secondPriceFeedId); if (params.secondUpdateThreshold == 0) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 2332fcde1..46c1372d0 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -148,10 +148,10 @@ contract PythPriceFeedsTest is Test { } // given the pyth contract address is zero - // [X] it reverts with Pyth_ParamsFeedInvalid + // [X] it reverts with Pyth_ParamsPythInvalid function test_getOneFeedPrice_revertsOnParamsFeedInvalid() public { bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, address(0) ); @@ -577,10 +577,10 @@ contract PythPriceFeedsTest is Test { } // given the first pyth contract address is zeros - // [X] it reverts with Pyth_ParamsFeedInvalid + // [X] it reverts with Pyth_ParamsPythInvalid function test_getTwoFeedPriceDiv_revertsOnParamsFirstFeedInvalid() public { bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, address(0) ); @@ -669,10 +669,10 @@ contract PythPriceFeedsTest is Test { } // given the second pyth contract address is zero - // [X] it reverts with Pyth_ParamsFeedInvalid + // [X] it reverts with Pyth_ParamsPythInvalid function test_getTwoFeedPriceDiv_revertsOnParamsSecondFeedInvalid() public { bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 4, address(0) ); @@ -840,10 +840,10 @@ contract PythPriceFeedsTest is Test { } // given the first pyth contract address is zero - // [X] it reverts with Pyth_ParamsFeedInvalid + // [X] it reverts with Pyth_ParamsPythInvalid function test_getTwoFeedPriceMul_revertsOnParamsFirstFeedInvalid() public { bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsFeedInvalid.selector, + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, address(0) ); From 47d6fcf049c9c00edc7428f5930fbc89b246156a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 6 Jan 2026 15:19:46 +0400 Subject: [PATCH 04/19] test: stubs for more comprehensive tests --- src/test/mocks/MockPyth.sol | 10 +- .../submodules/feeds/PythPriceFeeds.t.sol | 248 +++++++++++++++++- 2 files changed, 242 insertions(+), 16 deletions(-) diff --git a/src/test/mocks/MockPyth.sol b/src/test/mocks/MockPyth.sol index 077043e7b..c30704ad2 100644 --- a/src/test/mocks/MockPyth.sol +++ b/src/test/mocks/MockPyth.sol @@ -7,6 +7,11 @@ import {IPyth} from "interfaces/IPyth.sol"; contract MockPyth is IPyth { mapping(bytes32 => Price) public s_prices; + // Errors copied from PythErrors.sol + + error PriceFeedNotFound(); + error StalePrice(); + function setPrice( bytes32 priceId, int64 price, @@ -17,6 +22,7 @@ contract MockPyth is IPyth { s_prices[priceId] = Price({price: price, conf: conf, expo: expo, publishTime: publishTime}); } + /// @dev Replicates the errors thrown by the Pyth contract function getPriceNoOlderThan( bytes32 priceId, uint256 age @@ -25,12 +31,12 @@ contract MockPyth is IPyth { // Check if price exists if (priceData.publishTime == 0) { - revert("PriceFeedNotFound"); + revert PriceFeedNotFound(); } // Check if price is stale if (priceData.publishTime < block.timestamp - age) { - revert("StalePrice"); + revert StalePrice(); } return priceData; diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 46c1372d0..86a41db5c 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -307,18 +307,14 @@ contract PythPriceFeedsTest is Test { } // given the publish time is before the update threshold - // [X] it reverts with Pyth_FeedPublishTimeStale - function test_getOneFeedPrice_revertsOnFeedPublishTimeStale() public { + // [X] it reverts with StalePrice + function test_getOneFeedPrice_givenStalePrice_reverts() public { // Set publish time to be stale uint256 staleTime = block.timestamp - UPDATE_THRESHOLD - 1; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, staleTime); bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedPublishTimeStale.selector, - address(pyth), - PRICE_ID_1, - staleTime, - block.timestamp - UPDATE_THRESHOLD + MockPyth.StalePrice.selector ); vm.expectRevert(err); @@ -331,9 +327,11 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } + // TODO is this needed? + // given the confidence interval exceeds the maximum (after conversion to Pyth scale) // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_revertsOnFeedConfidenceExcessive() public { + function test_getOneFeedPrice_maxConfidenceExceeded_reverts() public { // MAX_CONFIDENCE = 2e16 in output decimals (18) // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals, so it should pass // To exceed MAX_CONFIDENCE (2e16), we need conf * 10^10 > 2e16, so conf > 2e6 @@ -376,7 +374,9 @@ contract PythPriceFeedsTest is Test { } // given expo is negative (expo = -8, outputDecimals = 18) - // [X] it correctly converts the price by multiplying + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [X] it correctly converts the price function test_getOneFeedPrice_success_expoNegative() public view { // expo = -8, outputDecimals = 18, totalExponent = 10 bytes memory params = encodeOneFeedParams( @@ -397,7 +397,9 @@ contract PythPriceFeedsTest is Test { } // given expo is positive (expo = 8, outputDecimals = 18) - // [X] it correctly converts the price by multiplying + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [X] it correctly converts the price function test_getOneFeedPrice_success_expoPositive() public view { // expo = 8, outputDecimals = 18, totalExponent = 26 bytes memory params = encodeOneFeedParams( @@ -418,7 +420,9 @@ contract PythPriceFeedsTest is Test { } // given expo is zero (expo = 0, outputDecimals = 18) - // [X] it correctly converts the price by multiplying + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [X] it correctly converts the price function test_getOneFeedPrice_success_expoZero() public { // expo = 0, outputDecimals = 18, totalExponent = 18 int64 price = 123456789; @@ -437,7 +441,14 @@ contract PythPriceFeedsTest is Test { assertEq(priceInt, 123456789 * 10 ** 18, "Price should match expected value for zero expo"); } + // given expo equals positive outputDecimals (expo = 18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + // given expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive // [X] it returns the price without scaling function test_getOneFeedPrice_success_expoEqualsNegativeOutputDecimals() public { // expo = -18, outputDecimals = 18, totalExponent = 0 @@ -462,6 +473,8 @@ contract PythPriceFeedsTest is Test { } // given expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive // [X] it correctly converts the price by dividing function test_getOneFeedPrice_success_expoVeryNegative() public { // expo = -20, outputDecimals = 18, totalExponent = -2 @@ -482,6 +495,8 @@ contract PythPriceFeedsTest is Test { } // given expo is very positive (expo = 20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive // [X] it correctly converts the price by multiplying function test_getOneFeedPrice_success_expoVeryPositive() public { // expo = 20, outputDecimals = 18, totalExponent = 38 @@ -505,6 +520,8 @@ contract PythPriceFeedsTest is Test { ); } + // TODO required? + // given the confidence interval is below the maximum (after conversion) // [X] it returns the correct price function test_getOneFeedPrice_success_confidenceEqualsMaximum() public { @@ -576,7 +593,7 @@ contract PythPriceFeedsTest is Test { assertEq(priceInt, expected, "Divided price should match expected calculation"); } - // given the first pyth contract address is zeros + // given the first pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid function test_getTwoFeedPriceDiv_revertsOnParamsFirstFeedInvalid() public { bytes memory err = abi.encodeWithSelector( @@ -760,7 +777,7 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // given the first feed has invalid price (zero) + // given the first feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid function test_getTwoFeedPriceDiv_revertsOnFirstFeedInvalid() public { // First feed has invalid price @@ -787,7 +804,7 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // given the second feed has invalid price (zero) + // given the second feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid function test_getTwoFeedPriceDiv_revertsOnSecondFeedInvalid() public { // Second feed has invalid price @@ -814,6 +831,94 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } + // given the first feed publish time is < the threshold boundary + // [ ] it reverts + + // given the second feed publish time is < the threshold boundary + // [ ] it reverts + + // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT + // [ ] it reverts with Pyth_ExponentOutOfBounds + + // given the first feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo is positive (expo = 8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by dividing + + // given the first feed expo is very positive (expo = 20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by multiplying + + // given the first feed publish time is >= the threshold boundary + // [ ] it returns the correct price + + // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT + // [ ] it reverts with Pyth_ExponentOutOfBounds + + // given the second feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo is positive (expo = 8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by dividing + + // given the second feed expo is very positive (expo = 20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by multiplying + + // given the second feed publish time is >= the threshold boundary + // [ ] it returns the correct price + // ========= TWO FEED TESTS - MUL ========= // // given all parameters are valid for two feeds @@ -861,5 +966,120 @@ contract PythPriceFeedsTest is Test { ); pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); } + + // given the second pyth contract address is zero + // [ ] it reverts with Pyth_ParamsPythInvalid + + // given the first price feed ID is zero + // [ ] it reverts with Pyth_ParamsPriceFeedIdInvalid + + // given the second price feed ID is zero + // [ ] it reverts with Pyth_ParamsPriceFeedIdInvalid + + // given the first update threshold is zero + // [ ] it reverts with Pyth_ParamsUpdateThresholdInvalid + + // given the second update threshold is zero + // [ ] it reverts with Pyth_ParamsUpdateThresholdInvalid + + // given the first max confidence is zero + // [ ] it reverts with Pyth_ParamsMaxConfidenceInvalid + + // given the second max confidence is zero + // [ ] it reverts with Pyth_ParamsMaxConfidenceInvalid + + // given the first feed has invalid price (<= 0) + // [ ] it reverts with Pyth_FeedPriceInvalid + + // given the second feed has invalid price (<= 0) + // [ ] it reverts with Pyth_FeedPriceInvalid + + // given the first feed publish time is < the threshold boundary + // [ ] it reverts + + // given the second feed publish time is < the threshold boundary + // [ ] it reverts + + // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT + // [ ] it reverts with Pyth_ExponentOutOfBounds + + // given the first feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo is positive (expo = 8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the first feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by dividing + + // given the first feed expo is very positive (expo = 20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by multiplying + + // given the first feed publish time is >= the threshold boundary + // [ ] it returns the correct price + + // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT + // [ ] it reverts with Pyth_ExponentOutOfBounds + + // given the second feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo is positive (expo = 8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price + + // given the second feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by dividing + + // given the second feed expo is very positive (expo = 20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [ ] it correctly converts the price by multiplying + + // given the second feed publish time is >= the threshold boundary + // [ ] it returns the correct price } /// forge-lint: disable-end(mixed-case-variable,mixed-case-function) From f9f1696785b2b8a4d26bff1ab08427281c7f120d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 6 Jan 2026 15:54:52 +0400 Subject: [PATCH 05/19] test: fix getOneFeedPrice test for max confidence interval --- .../submodules/feeds/PythPriceFeeds.t.sol | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 86a41db5c..0a54ceff3 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -313,9 +313,7 @@ contract PythPriceFeedsTest is Test { uint256 staleTime = block.timestamp - UPDATE_THRESHOLD - 1; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, staleTime); - bytes memory err = abi.encodeWithSelector( - MockPyth.StalePrice.selector - ); + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); vm.expectRevert(err); bytes memory params = encodeOneFeedParams( @@ -327,39 +325,28 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } - // TODO is this needed? - - // given the confidence interval exceeds the maximum (after conversion to Pyth scale) - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_maxConfidenceExceeded_reverts() public { + // given expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_maxConfidenceExceeded_reverts(uint64 priceConfidence_) public { // MAX_CONFIDENCE = 2e16 in output decimals (18) // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals, so it should pass // To exceed MAX_CONFIDENCE (2e16), we need conf * 10^10 > 2e16, so conf > 2e6 - // Use conf = 3000000 (3e6) to ensure it exceeds the threshold - // 3e6 * 10^10 = 3e16 in output decimals, which exceeds MAX_CONFIDENCE of 2e16 - uint64 highConf = 3000000; // 3e6 - pyth.setPrice(PRICE_ID_1, PRICE_1, highConf, EXPO_1, block.timestamp); - - // Calculate expected maxConfidence in Pyth scale using the same formula as the code - // confidenceExponent = expo - outputDecimals = -8 - 18 = -26 - // Since it's negative: maxConfidence_.mulDiv(1, 10^26) - int256 confidenceExponent = int256(EXPO_1) - int256(uint256(PRICE_DECIMALS)); - uint64 maxConfInPythScale; - if (confidenceExponent >= 0) { - maxConfInPythScale = SafeCast.encodeUInt64( - uint256(MAX_CONFIDENCE).mulDiv(10 ** uint256(confidenceExponent), 1) - ); - } else { - maxConfInPythScale = SafeCast.encodeUInt64( - uint256(MAX_CONFIDENCE).mulDiv(1, 10 ** uint256(-confidenceExponent)) - ); - } + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + + // Set the price data + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + // Calculate the expected maxConfidence in Pyth scale + // EXPO_1 = -8, PRICE_DECIMALS = 18 + // maxConfidence = 2e16 * 1 / 10^*(-8+18) = 2e16 * 1 / 10^10 = 2e6 + uint64 maxConfInPythScale = 2e6; bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, address(pyth), PRICE_ID_1, - highConf, + priceConfidence_, maxConfInPythScale ); vm.expectRevert(err); @@ -373,11 +360,8 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } - // given expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive // [X] it correctly converts the price - function test_getOneFeedPrice_success_expoNegative() public view { + function test_getOneFeedPrice_givenExpoNegative() public view { // expo = -8, outputDecimals = 18, totalExponent = 10 bytes memory params = encodeOneFeedParams( address(pyth), From 36c5f4e1fde8dd21b2e693cf212dcefd94358e81 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 6 Jan 2026 16:54:34 +0400 Subject: [PATCH 06/19] test: stale price tests --- .../submodules/feeds/PythPriceFeeds.t.sol | 162 ++++++++++++++++-- 1 file changed, 143 insertions(+), 19 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 0a54ceff3..c9b2810bf 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -82,14 +82,16 @@ contract PythPriceFeedsTest is Test { pyth = new MockPyth(); secondPyth = new MockPyth(); + // With the default update threshold, these prices will NOT be stale + // Set up price feed 1: 1.23456789 (expo=-8) - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp - UPDATE_THRESHOLD); // Set up price feed 2: 100000000 * 10^8 (expo=8) - pyth.setPrice(PRICE_ID_2, PRICE_2, CONF_2, EXPO_2, block.timestamp); + pyth.setPrice(PRICE_ID_2, PRICE_2, CONF_2, EXPO_2, block.timestamp - UPDATE_THRESHOLD); // Set up price feed 3: 0.0000000005 (expo=-18) - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp - UPDATE_THRESHOLD); } } @@ -308,10 +310,10 @@ contract PythPriceFeedsTest is Test { // given the publish time is before the update threshold // [X] it reverts with StalePrice - function test_getOneFeedPrice_givenStalePrice_reverts() public { - // Set publish time to be stale - uint256 staleTime = block.timestamp - UPDATE_THRESHOLD - 1; - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, staleTime); + function test_getOneFeedPrice_givenStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); vm.expectRevert(err); @@ -530,12 +532,12 @@ contract PythPriceFeedsTest is Test { ); } - // given the publish time equals the threshold boundary + // given the publish time is >= the threshold boundary // [X] it returns the correct price - function test_getOneFeedPrice_success_publishTimeEqualsThreshold() public { - // Publish time equals threshold boundary - uint256 thresholdTime = block.timestamp - UPDATE_THRESHOLD; - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, thresholdTime); + function test_getOneFeedPrice_givenPublishTimeAboveThreshold(uint48 publishTime_) public { + // Bound the publish time to a reasonable value + publishTime_ = uint48(bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); bytes memory params = encodeOneFeedParams( address(pyth), @@ -548,7 +550,7 @@ contract PythPriceFeedsTest is Test { assertEq( priceInt, EXPECTED_PRICE_1_18_DEC, - "Price should be valid when publish time equals threshold" + "Price should be valid when publish time is above threshold" ); } @@ -816,10 +818,50 @@ contract PythPriceFeedsTest is Test { } // given the first feed publish time is < the threshold boundary - // [ ] it reverts + // [X] it reverts with StalePrice + function test_getTwoFeedPriceDiv_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } // given the second feed publish time is < the threshold boundary - // [ ] it reverts + // [X] it reverts with StalePrice + function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT // [ ] it reverts with Pyth_ExponentOutOfBounds @@ -860,7 +902,28 @@ contract PythPriceFeedsTest is Test { // [ ] it correctly converts the price by multiplying // given the first feed publish time is >= the threshold boundary - // [ ] it returns the correct price + // [X] it returns the correct price + function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold(uint256 publishTime_) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + // Second feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq(priceInt, expected, "Divided price should match expected when first feed at threshold"); + } // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT // [ ] it reverts with Pyth_ExponentOutOfBounds @@ -901,7 +964,28 @@ contract PythPriceFeedsTest is Test { // [ ] it correctly converts the price by multiplying // given the second feed publish time is >= the threshold boundary - // [ ] it returns the correct price + // [X] it returns the correct price + function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold(uint256 publishTime_) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + // First feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq(priceInt, expected, "Divided price should match expected when second feed at threshold"); + } // ========= TWO FEED TESTS - MUL ========= // @@ -979,10 +1063,50 @@ contract PythPriceFeedsTest is Test { // [ ] it reverts with Pyth_FeedPriceInvalid // given the first feed publish time is < the threshold boundary - // [ ] it reverts + // [X] it reverts with StalePrice + function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } // given the second feed publish time is < the threshold boundary - // [ ] it reverts + // [X] it reverts with StalePrice + function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT // [ ] it reverts with Pyth_ExponentOutOfBounds From 139383a4104ed3e49e6cbb1d067f814a6c38d806 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 12:38:50 +0400 Subject: [PATCH 07/19] chore: linting --- .../submodules/feeds/PythPriceFeeds.t.sol | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index c9b2810bf..ea14e5732 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -536,7 +536,9 @@ contract PythPriceFeedsTest is Test { // [X] it returns the correct price function test_getOneFeedPrice_givenPublishTimeAboveThreshold(uint48 publishTime_) public { // Bound the publish time to a reasonable value - publishTime_ = uint48(bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp)); + publishTime_ = uint48( + bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp) + ); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); bytes memory params = encodeOneFeedParams( @@ -842,7 +844,9 @@ contract PythPriceFeedsTest is Test { // given the second feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts(uint256 publishTime_) public { + function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts( + uint256 publishTime_ + ) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); @@ -903,7 +907,9 @@ contract PythPriceFeedsTest is Test { // given the first feed publish time is >= the threshold boundary // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold(uint256 publishTime_) public { + function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { // Bound the publish time to a reasonable value publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); @@ -922,7 +928,11 @@ contract PythPriceFeedsTest is Test { uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq(priceInt, expected, "Divided price should match expected when first feed at threshold"); + assertEq( + priceInt, + expected, + "Divided price should match expected when first feed at threshold" + ); } // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT @@ -965,7 +975,9 @@ contract PythPriceFeedsTest is Test { // given the second feed publish time is >= the threshold boundary // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold(uint256 publishTime_) public { + function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { // Bound the publish time to a reasonable value publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); @@ -984,7 +996,11 @@ contract PythPriceFeedsTest is Test { uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq(priceInt, expected, "Divided price should match expected when second feed at threshold"); + assertEq( + priceInt, + expected, + "Divided price should match expected when second feed at threshold" + ); } // ========= TWO FEED TESTS - MUL ========= // @@ -1087,7 +1103,9 @@ contract PythPriceFeedsTest is Test { // given the second feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts(uint256 publishTime_) public { + function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( + uint256 publishTime_ + ) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); From fa43a60c45914df7f7c5e7b9144840fa1d6ddbd9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 12:40:28 +0400 Subject: [PATCH 08/19] test: add tests for out of bounds expo --- .../submodules/feeds/PythPriceFeeds.t.sol | 125 +++++++++++++++++- 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index ea14e5732..1dc79157d 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -217,16 +217,23 @@ contract PythPriceFeedsTest is Test { // given expo + outputDecimals > BASE_10_MAX_EXPONENT // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getOneFeedPrice_revertsOnExponentOutOfBounds() public { + function test_getOneFeedPrice_revertsOnExponentOutOfBounds(int32 expo_) public { // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT - int32 largeExpo = 45; // 45 + 18 = 63 > 50 - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, largeExpo, block.timestamp); + // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 + expo_ = int32( + bound( + int256(expo_), + int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), + type(int32).max + ) + ); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, address(pyth), PRICE_ID_1, - largeExpo, + expo_, PRICE_DECIMALS, BASE_10_MAX_EXPONENT ); @@ -868,7 +875,41 @@ contract PythPriceFeedsTest is Test { } // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [ ] it reverts with Pyth_ExponentOutOfBounds + // [X] it reverts with Pyth_ExponentOutOfBounds + function test_getTwoFeedPriceDiv_givenFirstFeedExponentOutOfBounds_reverts(int32 expo_) public { + // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT + // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 + expo_ = int32( + bound( + int256(expo_), + int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), + type(int32).max + ) + ); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, + address(pyth), + PRICE_ID_1, + expo_, + PRICE_DECIMALS, + BASE_10_MAX_EXPONENT + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } // given the first feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum @@ -936,7 +977,43 @@ contract PythPriceFeedsTest is Test { } // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [ ] it reverts with Pyth_ExponentOutOfBounds + // [X] it reverts with Pyth_ExponentOutOfBounds + function test_getTwoFeedPriceDiv_givenSecondFeedExponentOutOfBounds_reverts( + int32 expo_ + ) public { + // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT + // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 + expo_ = int32( + bound( + int256(expo_), + int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), + type(int32).max + ) + ); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, expo_, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, + address(pyth), + PRICE_ID_3, + expo_, + PRICE_DECIMALS, + BASE_10_MAX_EXPONENT + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } // given the second feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum @@ -1127,7 +1204,41 @@ contract PythPriceFeedsTest is Test { } // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [ ] it reverts with Pyth_ExponentOutOfBounds + // [X] it reverts with Pyth_ExponentOutOfBounds + function test_getTwoFeedPriceMul_givenFirstFeedExponentOutOfBounds_reverts(int32 expo_) public { + // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT + // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 + expo_ = int32( + bound( + int256(expo_), + int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), + type(int32).max + ) + ); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, + address(pyth), + PRICE_ID_1, + expo_, + PRICE_DECIMALS, + BASE_10_MAX_EXPONENT + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } // given the first feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum From 806d5d1c93935cd24e792e7d1d57a992c69b704b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 15:03:32 +0400 Subject: [PATCH 09/19] test: complete tests --- .../submodules/feeds/PythPriceFeeds.t.sol | 2017 ++++++++++++++--- 1 file changed, 1683 insertions(+), 334 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 1dc79157d..4080cb709 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -151,7 +151,7 @@ contract PythPriceFeedsTest is Test { // given the pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getOneFeedPrice_revertsOnParamsFeedInvalid() public { + function test_getOneFeedPrice_pythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, @@ -170,7 +170,7 @@ contract PythPriceFeedsTest is Test { // given the price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getOneFeedPrice_revertsOnParamsPriceFeedIdInvalid() public { + function test_getOneFeedPrice_priceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 1, @@ -189,7 +189,7 @@ contract PythPriceFeedsTest is Test { // given the update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getOneFeedPrice_revertsOnParamsUpdateThresholdInvalid() public { + function test_getOneFeedPrice_updateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 2, @@ -203,7 +203,7 @@ contract PythPriceFeedsTest is Test { // given the max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getOneFeedPrice_revertsOnParamsMaxConfidenceInvalid() public { + function test_getOneFeedPrice_maxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 3, @@ -217,7 +217,7 @@ contract PythPriceFeedsTest is Test { // given expo + outputDecimals > BASE_10_MAX_EXPONENT // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getOneFeedPrice_revertsOnExponentOutOfBounds(int32 expo_) public { + function test_getOneFeedPrice_exponentOutOfBounds_reverts(int32 expo_) public { // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 expo_ = int32( @@ -250,7 +250,7 @@ contract PythPriceFeedsTest is Test { // given the pyth contract doesn't implement IPyth // [X] it reverts with Pyth_FeedInvalid - function test_getOneFeedPrice_revertsOnFeedInvalid() public { + function test_getOneFeedPrice_pythContractInvalid_reverts() public { // Use a contract that doesn't implement IPyth address invalidPyth = address(0x1234); @@ -270,39 +270,17 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } - // given the price is zero + // given the price is <= 0 // [X] it reverts with Pyth_FeedPriceInvalid - function test_getOneFeedPrice_revertsOnFeedPriceInvalid() public { - // Set price to zero - pyth.setPrice(PRICE_ID_1, 0, CONF_1, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedPriceInvalid.selector, - address(pyth), - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the price is negative - // [X] it reverts with Pyth_FeedPriceInvalid - function test_getOneFeedPrice_revertsOnFeedPriceInvalid_negative() public { - // Set price to negative - pyth.setPrice(PRICE_ID_1, -100, CONF_1, EXPO_1, block.timestamp); + function test_getOneFeedPrice_feedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_FeedPriceInvalid.selector, address(pyth), PRICE_ID_1, - -100 + price_ ); vm.expectRevert(err); @@ -389,32 +367,75 @@ contract PythPriceFeedsTest is Test { ); } - // given expo is positive (expo = 8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [X] it correctly converts the price - function test_getOneFeedPrice_success_expoPositive() public view { - // expo = 8, outputDecimals = 18, totalExponent = 26 + // given expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getOneFeedPrice_expoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first + // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 + // But we want to test positive expo, so expo in range [1, 32] + expo_ = int32( + bound( + int256(expo_), + 1, + int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) + ) + ); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + bytes memory params = encodeOneFeedParams( address(pyth), - PRICE_ID_2, + PRICE_ID_1, UPDATE_THRESHOLD, MAX_CONFIDENCE ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - // price = 100000000, expo = 8, outputDecimals = 18 - // outputPrice = 100000000 * 10^26 - assertEq( - priceInt, - 100000000 * 10 ** 26, - "Price should match expected value for positive expo" + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_1, + expo_ ); + vm.expectRevert(err); + + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } // given expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_expoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint64 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + maxConfidence + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + // [X] it correctly converts the price function test_getOneFeedPrice_success_expoZero() public { // expo = 0, outputDecimals = 18, totalExponent = 18 @@ -434,14 +455,37 @@ contract PythPriceFeedsTest is Test { assertEq(priceInt, 123456789 * 10 ** 18, "Price should match expected value for zero expo"); } - // given expo equals positive outputDecimals (expo = 18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - // given expo equals negative outputDecimals (expo = -18, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + // [X] it returns the price without scaling function test_getOneFeedPrice_success_expoEqualsNegativeOutputDecimals() public { // expo = -18, outputDecimals = 18, totalExponent = 0 @@ -467,13 +511,25 @@ contract PythPriceFeedsTest is Test { // given expo is very negative (expo = -20, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [X] it correctly converts the price by dividing - function test_getOneFeedPrice_success_expoVeryNegative() public { - // expo = -20, outputDecimals = 18, totalExponent = -2 - int64 price = 100000000; + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_expoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); int32 expo = -20; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); bytes memory params = encodeOneFeedParams( address(pyth), @@ -481,20 +537,14 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - // outputPrice = 100000000 / 10^2 = 1000000 - assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } - // given expo is very positive (expo = 20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [X] it correctly converts the price by multiplying - function test_getOneFeedPrice_success_expoVeryPositive() public { - // expo = 20, outputDecimals = 18, totalExponent = 38 + // [X] it correctly converts the price by dividing + function test_getOneFeedPrice_success_expoVeryNegative() public { + // expo = -20, outputDecimals = 18, totalExponent = -2 int64 price = 100000000; - int32 expo = 20; + int32 expo = -20; pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); bytes memory params = encodeOneFeedParams( @@ -505,12 +555,8 @@ contract PythPriceFeedsTest is Test { ); uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - // outputPrice = 100000000 * 10^38 - assertEq( - priceInt, - 100000000 * 10 ** 38, - "Price should match expected value for very positive expo" - ); + // outputPrice = 100000000 / 10^2 = 1000000 + assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); } // TODO required? @@ -590,7 +636,7 @@ contract PythPriceFeedsTest is Test { // given the first pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsFirstFeedInvalid() public { + function test_getTwoFeedPriceDiv_firstPythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, @@ -613,7 +659,7 @@ contract PythPriceFeedsTest is Test { // given the first price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsFirstPriceIdInvalid() public { + function test_getTwoFeedPriceDiv_firstPriceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 1, @@ -636,7 +682,7 @@ contract PythPriceFeedsTest is Test { // given the first update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsFirstUpdateThresholdInvalid() public { + function test_getTwoFeedPriceDiv_firstUpdateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 2, @@ -659,7 +705,7 @@ contract PythPriceFeedsTest is Test { // given the first max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsFirstMaxConfidenceInvalid() public { + function test_getTwoFeedPriceDiv_firstMaxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 3, @@ -682,7 +728,7 @@ contract PythPriceFeedsTest is Test { // given the second pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsSecondFeedInvalid() public { + function test_getTwoFeedPriceDiv_secondPythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 4, @@ -705,7 +751,7 @@ contract PythPriceFeedsTest is Test { // given the second price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsSecondPriceIdInvalid() public { + function test_getTwoFeedPriceDiv_secondPriceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 5, @@ -728,7 +774,7 @@ contract PythPriceFeedsTest is Test { // given the second update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsSecondUpdateThresholdInvalid() public { + function test_getTwoFeedPriceDiv_secondUpdateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 6, @@ -751,7 +797,7 @@ contract PythPriceFeedsTest is Test { // given the second max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceDiv_revertsOnParamsSecondMaxConfidenceInvalid() public { + function test_getTwoFeedPriceDiv_secondMaxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 7, @@ -774,15 +820,15 @@ contract PythPriceFeedsTest is Test { // given the first feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceDiv_revertsOnFirstFeedInvalid() public { - // First feed has invalid price - pyth.setPrice(PRICE_ID_1, 0, CONF_1, EXPO_1, block.timestamp); + function test_getTwoFeedPriceDiv_firstFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_FeedPriceInvalid.selector, address(pyth), PRICE_ID_1, - 0 + price_ ); vm.expectRevert(err); @@ -801,15 +847,15 @@ contract PythPriceFeedsTest is Test { // given the second feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceDiv_revertsOnSecondFeedInvalid() public { - // Second feed has invalid price - pyth.setPrice(PRICE_ID_3, 0, CONF_3, EXPO_3, block.timestamp); + function test_getTwoFeedPriceDiv_secondFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_FeedPriceInvalid.selector, address(pyth), PRICE_ID_3, - 0 + price_ ); vm.expectRevert(err); @@ -913,49 +959,42 @@ contract PythPriceFeedsTest is Test { // given the first feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the first feed expo is positive (expo = 8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the first feed expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the first feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the first feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by dividing + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); - // given the first feed expo is very positive (expo = 20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by multiplying + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); - // given the first feed publish time is >= the threshold boundary - // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { - // Bound the publish time to a reasonable value - publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); - // Second feed keeps publish time as set in setUp() + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative() public view { + // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals bytes memory params = encodeTwoFeedParams( address(pyth), PRICE_ID_1, @@ -969,38 +1008,24 @@ contract PythPriceFeedsTest is Test { uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Divided price should match expected when first feed at threshold" - ); + assertEq(priceInt, expected, "Price should match expected for negative expo first feed"); } - // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getTwoFeedPriceDiv_givenSecondFeedExponentOutOfBounds_reverts( - int32 expo_ - ) public { - // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT - // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 + // given the first feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first + // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 + // But we want to test positive expo, so expo in range [1, 32] expo_ = int32( bound( int256(expo_), - int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), - type(int32).max + 1, + int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) ) ); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, expo_, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, - address(pyth), - PRICE_ID_3, - expo_, - PRICE_DECIMALS, - BASE_10_MAX_EXPONENT - ); - vm.expectRevert(err); + int64 price = 100; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); bytes memory params = encodeTwoFeedParams( address(pyth), @@ -1012,111 +1037,108 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second feed expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the second feed expo is positive (expo = 8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the second feed expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the second feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_1, + expo_ + ); + vm.expectRevert(err); - // given the second feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by dividing + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } - // given the second feed expo is very positive (expo = 20, outputDecimals = 18) + // given the first feed expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by multiplying - - // given the second feed publish time is >= the threshold boundary - // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold( - uint256 publishTime_ + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ ) public { - // Bound the publish time to a reasonable value - publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - // First feed keeps publish time as set in setUp() + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint64 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); bytes memory params = encodeTwoFeedParams( address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, - MAX_CONFIDENCE, + maxConfidence, address(pyth), PRICE_ID_3, UPDATE_THRESHOLD, MAX_CONFIDENCE ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Divided price should match expected when second feed at threshold" - ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // ========= TWO FEED TESTS - MUL ========= // + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero() public { + // First feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, price, 0, expo, block.timestamp); - // given all parameters are valid for two feeds - // [X] it returns the correct multiplied price (first * second) in output decimals - function test_getTwoFeedPriceMul_success() public view { - // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals - // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals - // Result: (1234567890000000000 * 500000000) / 10^18 bytes memory params = encodeTwoFeedParams( address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, - MAX_CONFIDENCE, + type(uint64).max, address(pyth), PRICE_ID_3, UPDATE_THRESHOLD, MAX_CONFIDENCE ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - // (1234567890000000000 * 500000000) / 10^18 = 617283945000 - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq(priceInt, expected, "Multiplied price should match expected calculation"); + // First: 100 * 10^18, Second: 500000000 (18 dec) + // Result: (100 * 10^18 * 10^18) / 500000000 = 100 * 10^36 / 5e8 = 2 * 10^29 + uint256 firstPrice = uint256(100) * 10 ** 18; + uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq(priceInt, expected, "Price should match expected for zero expo first feed"); } - // given the first pyth contract address is zero - // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceMul_revertsOnParamsFirstFeedInvalid() public { + // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPythInvalid.selector, - 0, - address(0) + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e16) ); vm.expectRevert(err); bytes memory params = encodeTwoFeedParams( - address(0), + address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, MAX_CONFIDENCE, @@ -1125,45 +1147,80 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // given the second pyth contract address is zero - // [ ] it reverts with Pyth_ParamsPythInvalid - - // given the first price feed ID is zero - // [ ] it reverts with Pyth_ParamsPriceFeedIdInvalid - - // given the second price feed ID is zero - // [ ] it reverts with Pyth_ParamsPriceFeedIdInvalid - - // given the first update threshold is zero - // [ ] it reverts with Pyth_ParamsUpdateThresholdInvalid - - // given the second update threshold is zero - // [ ] it reverts with Pyth_ParamsUpdateThresholdInvalid + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { + // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); - // given the first max confidence is zero - // [ ] it reverts with Pyth_ParamsMaxConfidenceInvalid + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - // given the second max confidence is zero - // [ ] it reverts with Pyth_ParamsMaxConfidenceInvalid + // First: 1e9, Second: 500000000 (18 dec) + // Result: (1e9 * 10^18) / 500000000 = 1e27 / 5e8 = 2e18 + uint256 firstPrice = uint256(1e9); + uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected when first feed expo equals negative outputDecimals" + ); + } - // given the first feed has invalid price (<= 0) - // [ ] it reverts with Pyth_FeedPriceInvalid + // given the first feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - // given the second feed has invalid price (<= 0) - // [ ] it reverts with Pyth_FeedPriceInvalid + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); - // given the first feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); - vm.expectRevert(err); + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative() public { + // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); bytes memory params = encodeTwoFeedParams( address(pyth), @@ -1175,20 +1232,28 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1e10 / 100 = 1e8, Second: 500000000 (18 dec) + // Result: (1e8 * 10^18) / 500000000 = 1e26 / 5e8 = 2e17 + uint256 firstPrice = uint256(1e10) / 100; + uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo first feed" + ); } - // given the second feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( + // given the first feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold( uint256 publishTime_ ) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); - vm.expectRevert(err); + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + // Second feed keeps publish time as set in setUp() bytes memory params = encodeTwoFeedParams( address(pyth), @@ -1200,12 +1265,21 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Divided price should match expected when first feed at threshold" + ); } - // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT + // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getTwoFeedPriceMul_givenFirstFeedExponentOutOfBounds_reverts(int32 expo_) public { + function test_getTwoFeedPriceDiv_givenSecondFeedExponentOutOfBounds_reverts( + int32 expo_ + ) public { // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 expo_ = int32( @@ -1215,12 +1289,12 @@ contract PythPriceFeedsTest is Test { type(int32).max ) ); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, expo_, block.timestamp); bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, address(pyth), - PRICE_ID_1, + PRICE_ID_3, expo_, PRICE_DECIMALS, BASE_10_MAX_EXPONENT @@ -1237,86 +1311,1361 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // given the first feed expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price - - // given the first feed expo is positive (expo = 8, outputDecimals = 18) + // given the second feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); - // given the first feed expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); - // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } - // given the first feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative() public { + // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); - // given the first feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by dividing + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - // given the first feed expo is very positive (expo = 20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by multiplying + // First: 1234567890000000000 (18 dec), Second: 1234567890000000000 + // Result: (1234567890000000000 * 10^18) / 1234567890000000000 = 10^18 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + 10 ** PRICE_DECIMALS, + EXPECTED_PRICE_1_18_DEC + ); + assertEq(priceInt, expected, "Price should match expected for negative expo second feed"); + } - // given the first feed publish time is >= the threshold boundary - // [ ] it returns the correct price + // given the second feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first + // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 + // But we want to test positive expo, so expo in range [1, 32] + expo_ = int32( + bound( + int256(expo_), + 1, + int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) + ) + ); + int64 price = 100; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); - // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [ ] it reverts with Pyth_ExponentOutOfBounds + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); - // given the second feed expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_3, + expo_ + ); + vm.expectRevert(err); - // given the second feed expo is positive (expo = 8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } // given the second feed expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint64 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); - // given the second feed expo equals positive outputDecimals (expo = 18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + maxConfidence + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } - // given the second feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by dividing + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero() public { + // Second feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, price, 0, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + type(uint64).max + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 100 * 10^18 + // Result: (1234567890000000000 * 10^18) / (100 * 10^18) = 12345678900000000 + uint256 secondPrice = uint256(100) * 10 ** 18; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); + assertEq(priceInt, expected, "Price should match expected for zero expo second feed"); + } - // given the second feed expo is very positive (expo = 20, outputDecimals = 18) + // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) // given the confidence interval is above the maximum - // [ ] it reverts with Pyth_FeedConfidenceExcessive - // [ ] it correctly converts the price by multiplying + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - // given the second feed publish time is >= the threshold boundary - // [ ] it returns the correct price + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { + // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e9 + // Result: (1234567890000000000 * 10^18) / 1e9 = 1234567890000000000000000000 + uint256 secondPrice = uint256(1e9); + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); + assertEq( + priceInt, + expected, + "Price should match expected when second feed expo equals negative outputDecimals" + ); + } + + // given the second feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative() public { + // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e8 + // Result: (1234567890000000000 * 10^18) / 1e8 + uint256 secondPrice = uint256(1e10) / 100; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo second feed" + ); + } + + // given the second feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + // First feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Divided price should match expected when second feed at threshold" + ); + } + + // ========= TWO FEED TESTS - MUL ========= // + + // given all parameters are valid for two feeds + // [X] it returns the correct multiplied price (first * second) in output decimals + function test_getTwoFeedPriceMul_success() public view { + // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals + // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals + // Result: (1234567890000000000 * 500000000) / 10^18 + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // (1234567890000000000 * 500000000) / 10^18 = 617283945000 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Multiplied price should match expected calculation"); + } + + // given the first pyth contract address is zero + // [X] it reverts with Pyth_ParamsPythInvalid + function test_getTwoFeedPriceMul_firstPythZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, + 0, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(0), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second pyth contract address is zero + // [X] it reverts with Pyth_ParamsPythInvalid + function test_getTwoFeedPriceMul_secondPythZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, + 4, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(0), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getTwoFeedPriceMul_firstPriceFeedIdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, + 1, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getTwoFeedPriceMul_secondPriceFeedIdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, + 5, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceMul_firstUpdateThresholdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 2, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + 0, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceMul_secondUpdateThresholdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 6, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + 0, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceMul_firstMaxConfidenceZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 3, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + 0, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceMul_secondMaxConfidenceZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 7, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + 0 + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed has invalid price (<= 0) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceMul_firstFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_1, + price_ + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second feed has invalid price (<= 0) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceMul_secondFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_3, + price_ + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( + uint256 publishTime_ + ) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT + // [X] it reverts with Pyth_ExponentOutOfBounds + function test_getTwoFeedPriceMul_givenFirstFeedExponentOutOfBounds_reverts(int32 expo_) public { + // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT + // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 + expo_ = int32( + bound( + int256(expo_), + int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), + type(int32).max + ) + ); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, + address(pyth), + PRICE_ID_1, + expo_, + PRICE_DECIMALS, + BASE_10_MAX_EXPONENT + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative() public view { + // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // (1234567890000000000 * 500000000) / 10^18 = 617283945000 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Price should match expected for negative expo first feed"); + } + + // given the first feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceMul_givenFirstFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first + // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 + // But we want to test positive expo, so expo in range [1, 32] + expo_ = int32( + bound( + int256(expo_), + 1, + int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) + ) + ); + int64 price = 100; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_1, + expo_ + ); + vm.expectRevert(err); + + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint64 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + maxConfidence, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenFirstFeedExpoZero() public { + // First feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, price, 0, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + type(uint64).max, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 100 * 10^18, Second: 500000000 (18 dec) + // Result: (100 * 10^18 * 500000000) / 10^18 = 5 * 10^10 + uint256 firstPrice = uint256(100) * 10 ** 18; + uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Price should match expected for zero expo first feed"); + } + + // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { + // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1e9, Second: 500000000 (18 dec) + // Result: (1e9 * 500000000) / 10^18 = 5e17 / 1e18 = 0 (rounds down) + uint256 firstPrice = uint256(1e9); + uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected when first feed expo equals negative outputDecimals" + ); + } + + // given the first feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative() public { + // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1e10 / 100 = 1e8, Second: 500000000 (18 dec) + // Result: (1e8 * 500000000) / 10^18 = 5e16 / 1e18 = 0 (rounds down) + uint256 firstPrice = uint256(1e10) / 100; + uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo first feed" + ); + } + + // given the first feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceMul_givenFirstFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + // Second feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Multiplied price should match expected when first feed at threshold" + ); + } + + // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT + // [X] it reverts with Pyth_ExponentOutOfBounds + function test_getTwoFeedPriceMul_givenSecondFeedExponentOutOfBounds_reverts( + int32 expo_ + ) public { + // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT + // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 + expo_ = int32( + bound( + int256(expo_), + int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), + type(int32).max + ) + ); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, expo_, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, + address(pyth), + PRICE_ID_3, + expo_, + PRICE_DECIMALS, + BASE_10_MAX_EXPONENT + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative() public { + // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1234567890000000000 + // Result: (1234567890000000000 * 1234567890000000000) / 10^18 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + EXPECTED_PRICE_1_18_DEC, + 10 ** PRICE_DECIMALS + ); + assertEq(priceInt, expected, "Price should match expected for negative expo second feed"); + } + + // given the second feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceMul_givenSecondFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first + // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 + // But we want to test positive expo, so expo in range [1, 32] + expo_ = int32( + bound( + int256(expo_), + 1, + int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) + ) + ); + int64 price = 100; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_3, + expo_ + ); + vm.expectRevert(err); + + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint64 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + maxConfidence + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenSecondFeedExpoZero() public { + // Second feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, price, 0, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + type(uint64).max + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 100 * 10^18 + // Result: (1234567890000000000 * 100 * 10^18) / 10^18 = 1234567890000000000 * 100 + uint256 secondPrice = uint256(100) * 10 ** 18; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Price should match expected for zero expo second feed"); + } + + // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { + // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e9 + // Result: (1234567890000000000 * 1e9) / 10^18 = 1234567890 + uint256 secondPrice = uint256(1e9); + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected when second feed expo equals negative outputDecimals" + ); + } + + // given the second feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative() public { + // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e8 + // Result: (1234567890000000000 * 1e8) / 10^18 = 123456789 + uint256 secondPrice = uint256(1e10) / 100; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo second feed" + ); + } + + // given the second feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceMul_givenSecondFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + // First feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Multiplied price should match expected when second feed at threshold" + ); + } } /// forge-lint: disable-end(mixed-case-variable,mixed-case-function) From 23fb4f572dd60897a49f9f3e54d1a692e2abb99d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 17:09:25 +0400 Subject: [PATCH 10/19] fix: remove exponent bounds tests (unnecessary), add price feed not found tests, add checks on function call return values --- src/interfaces/IPyth.sol | 1 + .../PRICE/submodules/feeds/PythPriceFeeds.sol | 103 ++--- .../submodules/feeds/PythPriceFeeds.t.sol | 401 ++++++------------ 3 files changed, 190 insertions(+), 315 deletions(-) diff --git a/src/interfaces/IPyth.sol b/src/interfaces/IPyth.sol index 58aaeda6d..b6370aff8 100644 --- a/src/interfaces/IPyth.sol +++ b/src/interfaces/IPyth.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.8.15; +/// @title IPyth /// @notice Minimal interface for Pyth Network price feeds /// @dev Based on Pyth Network's IPyth interface interface IPyth { diff --git a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol index 0e77182c6..eca3ce0a7 100644 --- a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol +++ b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol @@ -22,8 +22,7 @@ import {PriceSubmodule} from "src/modules/PRICE/PRICE.v2.sol"; contract PythPriceFeeds is PriceSubmodule { using FullMath for uint256; - /// @notice Any token or pool with a decimal scale greater than this would result in an overflow - uint8 internal constant BASE_10_MAX_EXPONENT = 50; + uint256 internal constant _PRICE_DATA_SIZE = 128; /// @notice Parameters for a single Pyth price feed /// @@ -132,20 +131,13 @@ contract PythPriceFeeds is PriceSubmodule { uint64 maxConfidence_ ); - /// @notice The exponent calculation would result in an overflow + /// @notice The exponent from the price feed is positive, which results in loss of precision + /// @dev Positive expo values should not be accepted as they cause precision loss /// /// @param pyth_ The address of the Pyth contract /// @param priceFeedId_ The price feed ID - /// @param expo_ The exponent from the price feed - /// @param outputDecimals_ The number of decimals to return the price in - /// @param maxExponent_ The maximum exponent allowed - error Pyth_ExponentOutOfBounds( - address pyth_, - bytes32 priceFeedId_, - int32 expo_, - uint8 outputDecimals_, - uint8 maxExponent_ - ); + /// @param expo_ The exponent from the price feed (must be <= 0) + error Pyth_ExponentPositive(address pyth_, bytes32 priceFeedId_, int32 expo_); // ========== CONSTRUCTOR ========== // @@ -186,9 +178,12 @@ contract PythPriceFeeds is PriceSubmodule { uint48 paramsUpdateThreshold, uint64 paramsMaxConfidence ) internal pure { + // Price must be positive if (priceData.price <= 0) revert Pyth_FeedPriceInvalid(pyth_, priceFeedId_, priceData.price); + // Publish time must be after the update threshold + // The Pyth contract reverts if the price is stale, but this serves as a backup if (priceData.publishTime < blockTimestamp - uint256(paramsUpdateThreshold)) revert Pyth_FeedPublishTimeStale( pyth_, @@ -224,31 +219,54 @@ contract PythPriceFeeds is PriceSubmodule { ) internal view returns (uint256) { IPyth.Price memory priceData; { - try IPyth(pyth_).getPriceNoOlderThan(priceFeedId_, uint256(updateThreshold_)) returns ( - IPyth.Price memory pythPrice - ) { - priceData = pythPrice; - } catch (bytes memory) { + // Encode function call: getPriceNoOlderThan(bytes32,uint256) + bytes memory callData = abi.encodeWithSelector( + IPyth.getPriceNoOlderThan.selector, + priceFeedId_, + uint256(updateThreshold_) + ); + + // Perform low-level static call (view function) + (bool success, bytes memory returnData) = pyth_.staticcall(callData); + + if (!success) { + // If returnData is empty, it's a call failure (not a contract or function doesn't exist) + if (returnData.length == 0) { + revert Pyth_FeedInvalid(pyth_, priceFeedId_); + } + // Otherwise, bubble up the revert from the function + assembly { + revert(add(returnData, 0x20), mload(returnData)) + } + } + + // Validate return data length matches expected ABI-encoded IPyth.Price struct size + if (returnData.length != _PRICE_DATA_SIZE) { revert Pyth_FeedInvalid(pyth_, priceFeedId_); } + + // Decode the return data + priceData = abi.decode(returnData, (IPyth.Price)); + } + + // Although technically possible, a positive exponent has not been seen and would result in loss of precision + // Therefore, it is not supported + if (priceData.expo > 0) { + revert Pyth_ExponentPositive(pyth_, priceFeedId_, priceData.expo); } // Convert maxConfidence from output decimals scale to Pyth feed scale (10^expo) // Formula: maxConfidenceInPythScale = maxConfidence * 10^expo / 10^outputDecimals // = maxConfidence * 10^(expo - outputDecimals) // Note: Result is cast to uint64 since it's compared against priceData.conf (uint64) - uint64 maxConfidenceInPythScale; - int256 confidenceExponent = int256(priceData.expo) - int256(uint256(outputDecimals_)); - if (confidenceExponent >= 0) { - maxConfidenceInPythScale = SafeCast.encodeUInt64( - uint256(maxConfidence_).mulDiv(10 ** uint256(confidenceExponent), 1) - ); - } else { - maxConfidenceInPythScale = SafeCast.encodeUInt64( - uint256(maxConfidence_).mulDiv(1, 10 ** uint256(-confidenceExponent)) - ); - } + uint64 maxConfidenceInPythScale = SafeCast.encodeUInt64( + uint256(maxConfidence_).mulDiv( + 10 ** uint256(uint32(-priceData.expo)), + 10 ** uint256(outputDecimals_) + ) + ); + // Validate raw values from the price feed _validatePriceFeedResult( pyth_, priceFeedId_, @@ -258,28 +276,11 @@ contract PythPriceFeeds is PriceSubmodule { maxConfidenceInPythScale ); - // Check exponent bounds: expo + outputDecimals <= BASE_10_MAX_EXPONENT - int256 totalExponent = int256(priceData.expo) + int256(uint256(outputDecimals_)); - if (totalExponent > int256(uint256(BASE_10_MAX_EXPONENT))) { - revert Pyth_ExponentOutOfBounds( - pyth_, - priceFeedId_, - priceData.expo, - outputDecimals_, - BASE_10_MAX_EXPONENT - ); - } - uint256 price = uint256(int256(priceData.price)); - // Convert price: outputPrice = price * 10^(expo + outputDecimals) - // If totalExponent >= 0: multiply by 10^totalExponent - // If totalExponent < 0: divide by 10^(-totalExponent) - if (totalExponent >= 0) { - return price.mulDiv(10 ** uint256(totalExponent), 1); - } else { - return price.mulDiv(1, 10 ** uint256(-totalExponent)); - } + // Convert price to output decimals + // The PRICE module will handle the zero value + return price.mulDiv(10 ** uint256(outputDecimals_), 10 ** uint256(uint32(-priceData.expo))); } /// @notice Returns the price from a single Pyth feed, as specified in `params_`. @@ -366,6 +367,10 @@ contract PythPriceFeeds is PriceSubmodule { outputDecimals_ ); + // If denominatorPrice is zero, do an early exit + // The PRICE module will handle the zero value + if (denominatorPrice == 0) return 0; + // Convert to numerator/denominator price and return uint256 priceResult = numeratorPrice.mulDiv(10 ** outputDecimals_, denominatorPrice); diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 4080cb709..d1e28c7b1 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -12,7 +12,6 @@ import {MockPyth} from "src/test/mocks/MockPyth.sol"; // Libraries import {FullMath} from "src/libraries/FullMath.sol"; -import {SafeCast} from "src/libraries/SafeCast.sol"; // Bophades import {Kernel} from "src/Kernel.sol"; @@ -58,8 +57,6 @@ contract PythPriceFeedsTest is Test { // Set MAX_CONFIDENCE to 2e16 to allow CONF_1 but reject higher values uint64 internal constant MAX_CONFIDENCE = 20000000000000000; // 2 * 10^16 in 18 decimals - uint8 internal constant BASE_10_MAX_EXPONENT = 50; - function setUp() public { vm.warp(51 * 365 * 24 * 60 * 60); // Set timestamp at roughly Jan 1, 2021 @@ -99,33 +96,33 @@ contract PythPriceFeedsTest is Test { function encodeOneFeedParams( address pyth_, - bytes32 priceFeedId, - uint48 updateThreshold, - uint64 maxConfidence + bytes32 priceFeedId_, + uint48 updateThreshold_, + uint64 maxConfidence_ ) internal pure returns (bytes memory params) { - return abi.encode(pyth_, priceFeedId, updateThreshold, maxConfidence); + return abi.encode(pyth_, priceFeedId_, updateThreshold_, maxConfidence_); } function encodeTwoFeedParams( - address firstPyth, - bytes32 firstPriceFeedId, - uint48 firstUpdateThreshold, - uint64 firstMaxConfidence, - address secondPyth, - bytes32 secondPriceFeedId, - uint48 secondUpdateThreshold, - uint64 secondMaxConfidence + address firstPyth_, + bytes32 firstPriceFeedId_, + uint48 firstUpdateThreshold_, + uint64 firstMaxConfidence_, + address secondPyth_, + bytes32 secondPriceFeedId_, + uint48 secondUpdateThreshold_, + uint64 secondMaxConfidence_ ) internal pure returns (bytes memory params) { return abi.encode( - firstPyth, - firstPriceFeedId, - firstUpdateThreshold, - firstMaxConfidence, - secondPyth, - secondPriceFeedId, - secondUpdateThreshold, - secondMaxConfidence + firstPyth_, + firstPriceFeedId_, + firstUpdateThreshold_, + firstMaxConfidence_, + secondPyth_, + secondPriceFeedId_, + secondUpdateThreshold_, + secondMaxConfidence_ ); } @@ -215,39 +212,6 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } - // given expo + outputDecimals > BASE_10_MAX_EXPONENT - // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getOneFeedPrice_exponentOutOfBounds_reverts(int32 expo_) public { - // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT - // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 - expo_ = int32( - bound( - int256(expo_), - int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), - type(int32).max - ) - ); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, - address(pyth), - PRICE_ID_1, - expo_, - PRICE_DECIMALS, - BASE_10_MAX_EXPONENT - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - // given the pyth contract doesn't implement IPyth // [X] it reverts with Pyth_FeedInvalid function test_getOneFeedPrice_pythContractInvalid_reverts() public { @@ -293,11 +257,26 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } + // given the price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getOneFeedPrice_priceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + // given the publish time is before the update threshold // [X] it reverts with StalePrice function test_getOneFeedPrice_givenStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); @@ -370,16 +349,9 @@ contract PythPriceFeedsTest is Test { // given expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive function test_getOneFeedPrice_expoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first - // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 - // But we want to test positive expo, so expo in range [1, 32] - expo_ = int32( - bound( - int256(expo_), - 1, - int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) - ) - ); + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); bytes memory params = encodeOneFeedParams( @@ -437,17 +409,17 @@ contract PythPriceFeedsTest is Test { } // [X] it correctly converts the price - function test_getOneFeedPrice_success_expoZero() public { + function test_getOneFeedPrice_expoZero() public { // expo = 0, outputDecimals = 18, totalExponent = 18 int64 price = 123456789; int32 expo = 0; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); bytes memory params = encodeOneFeedParams( address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, - MAX_CONFIDENCE + type(uint64).max // Ensures confidence interval is below the maximum ); uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); @@ -504,6 +476,7 @@ contract PythPriceFeedsTest is Test { // outputPrice = 123456789 (no scaling needed) assertEq( priceInt, + /// forge-lint: disable-next-line(unsafe-typecast) uint256(uint64(price)), "Price should match input value when expo equals negative outputDecimals" ); @@ -872,14 +845,29 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // given the first feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceDiv_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + // given the first price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceDiv_givenFirstFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceDiv_givenSecondFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); vm.expectRevert(err); bytes memory params = encodeTwoFeedParams( @@ -888,21 +876,19 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE, address(pyth), - PRICE_ID_3, + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), UPDATE_THRESHOLD, MAX_CONFIDENCE ); pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // given the second feed publish time is < the threshold boundary + // given the first feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts( - uint256 publishTime_ - ) public { + function test_getTwoFeedPriceDiv_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); vm.expectRevert(err); @@ -920,28 +906,16 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); } - // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getTwoFeedPriceDiv_givenFirstFeedExponentOutOfBounds_reverts(int32 expo_) public { - // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT - // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 - expo_ = int32( - bound( - int256(expo_), - int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), - type(int32).max - ) - ); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + // given the second feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts( + uint256 publishTime_ + ) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, - address(pyth), - PRICE_ID_1, - expo_, - PRICE_DECIMALS, - BASE_10_MAX_EXPONENT - ); + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); vm.expectRevert(err); bytes memory params = encodeTwoFeedParams( @@ -1014,16 +988,9 @@ contract PythPriceFeedsTest is Test { // given the first feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive function test_getTwoFeedPriceDiv_givenFirstFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first - // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 - // But we want to test positive expo, so expo in range [1, 32] - expo_ = int32( - bound( - int256(expo_), - 1, - int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) - ) - ); + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); int64 price = 100; pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); @@ -1094,13 +1061,13 @@ contract PythPriceFeedsTest is Test { // First feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; - pyth.setPrice(PRICE_ID_1, price, 0, expo, block.timestamp); + pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); bytes memory params = encodeTwoFeedParams( address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, - type(uint64).max, + type(uint64).max, // Ensures confidence interval is below the maximum address(pyth), PRICE_ID_3, UPDATE_THRESHOLD, @@ -1275,45 +1242,6 @@ contract PythPriceFeedsTest is Test { ); } - // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getTwoFeedPriceDiv_givenSecondFeedExponentOutOfBounds_reverts( - int32 expo_ - ) public { - // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT - // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 - expo_ = int32( - bound( - int256(expo_), - int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), - type(int32).max - ) - ); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, expo_, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, - address(pyth), - PRICE_ID_3, - expo_, - PRICE_DECIMALS, - BASE_10_MAX_EXPONENT - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - // given the second feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive @@ -1377,16 +1305,9 @@ contract PythPriceFeedsTest is Test { // given the second feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive function test_getTwoFeedPriceDiv_givenSecondFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first - // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 - // But we want to test positive expo, so expo in range [1, 32] - expo_ = int32( - bound( - int256(expo_), - 1, - int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) - ) - ); + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); int64 price = 100; pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); @@ -1457,7 +1378,7 @@ contract PythPriceFeedsTest is Test { // Second feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; - pyth.setPrice(PRICE_ID_3, price, 0, expo, block.timestamp); + pyth.setPrice(PRICE_ID_3, price, 1, expo, block.timestamp); bytes memory params = encodeTwoFeedParams( address(pyth), @@ -1467,7 +1388,7 @@ contract PythPriceFeedsTest is Test { address(pyth), PRICE_ID_3, UPDATE_THRESHOLD, - type(uint64).max + type(uint64).max // Ensures confidence interval is below the maximum ); uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); @@ -1901,14 +1822,29 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); } - // given the first feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + // given the first price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceMul_givenFirstFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceMul_givenSecondFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); vm.expectRevert(err); bytes memory params = encodeTwoFeedParams( @@ -1917,21 +1853,19 @@ contract PythPriceFeedsTest is Test { UPDATE_THRESHOLD, MAX_CONFIDENCE, address(pyth), - PRICE_ID_3, + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), UPDATE_THRESHOLD, MAX_CONFIDENCE ); pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); } - // given the second feed publish time is < the threshold boundary + // given the first feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( - uint256 publishTime_ - ) public { + function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 0, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); vm.expectRevert(err); @@ -1949,28 +1883,16 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); } - // given the first feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getTwoFeedPriceMul_givenFirstFeedExponentOutOfBounds_reverts(int32 expo_) public { - // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT - // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 - expo_ = int32( - bound( - int256(expo_), - int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), - type(int32).max - ) - ); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + // given the second feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( + uint256 publishTime_ + ) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, - address(pyth), - PRICE_ID_1, - expo_, - PRICE_DECIMALS, - BASE_10_MAX_EXPONENT - ); + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); vm.expectRevert(err); bytes memory params = encodeTwoFeedParams( @@ -2044,16 +1966,9 @@ contract PythPriceFeedsTest is Test { // given the first feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive function test_getTwoFeedPriceMul_givenFirstFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first - // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 - // But we want to test positive expo, so expo in range [1, 32] - expo_ = int32( - bound( - int256(expo_), - 1, - int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) - ) - ); + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); int64 price = 100; pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); @@ -2124,13 +2039,13 @@ contract PythPriceFeedsTest is Test { // First feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; - pyth.setPrice(PRICE_ID_1, price, 0, expo, block.timestamp); + pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); bytes memory params = encodeTwoFeedParams( address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, - type(uint64).max, + type(uint64).max, // Ensures confidence interval is below the maximum address(pyth), PRICE_ID_3, UPDATE_THRESHOLD, @@ -2305,45 +2220,6 @@ contract PythPriceFeedsTest is Test { ); } - // given the second feed expo + outputDecimals > BASE_10_MAX_EXPONENT - // [X] it reverts with Pyth_ExponentOutOfBounds - function test_getTwoFeedPriceMul_givenSecondFeedExponentOutOfBounds_reverts( - int32 expo_ - ) public { - // Set expo such that expo + outputDecimals > BASE_10_MAX_EXPONENT - // expo must be > BASE_10_MAX_EXPONENT - outputDecimals = 50 - 18 = 32 - expo_ = int32( - bound( - int256(expo_), - int32(int8(BASE_10_MAX_EXPONENT) - int8(PRICE_DECIMALS) + 1), - type(int32).max - ) - ); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, expo_, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentOutOfBounds.selector, - address(pyth), - PRICE_ID_3, - expo_, - PRICE_DECIMALS, - BASE_10_MAX_EXPONENT - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - // given the second feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive @@ -2407,16 +2283,9 @@ contract PythPriceFeedsTest is Test { // given the second feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive function test_getTwoFeedPriceMul_givenSecondFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) but not so large it triggers ExponentOutOfBounds first - // expo + outputDecimals <= BASE_10_MAX_EXPONENT, so expo <= 50 - 18 = 32 - // But we want to test positive expo, so expo in range [1, 32] - expo_ = int32( - bound( - int256(expo_), - 1, - int256(uint256(BASE_10_MAX_EXPONENT)) - int256(uint256(PRICE_DECIMALS)) - ) - ); + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); int64 price = 100; pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); @@ -2487,7 +2356,7 @@ contract PythPriceFeedsTest is Test { // Second feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; - pyth.setPrice(PRICE_ID_3, price, 0, expo, block.timestamp); + pyth.setPrice(PRICE_ID_3, price, 1, expo, block.timestamp); bytes memory params = encodeTwoFeedParams( address(pyth), @@ -2497,7 +2366,7 @@ contract PythPriceFeedsTest is Test { address(pyth), PRICE_ID_3, UPDATE_THRESHOLD, - type(uint64).max + type(uint64).max // Ensures confidence interval is below the maximum ); uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); From 73743038d27b00f1cd50cccff51156b726aa5ccd Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 17:25:35 +0400 Subject: [PATCH 11/19] fix: amend max confidence parameter to be uint256 --- .../PRICE/submodules/feeds/PythPriceFeeds.sol | 12 ++++++------ .../submodules/feeds/PythPriceFeeds.t.sol | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol index eca3ce0a7..c98b109e4 100644 --- a/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol +++ b/src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol @@ -34,7 +34,7 @@ contract PythPriceFeeds is PriceSubmodule { address pyth; bytes32 priceFeedId; uint48 updateThreshold; - uint64 maxConfidence; + uint256 maxConfidence; } /// @notice Parameters for two Pyth price feeds @@ -51,11 +51,11 @@ contract PythPriceFeeds is PriceSubmodule { address firstPyth; bytes32 firstPriceFeedId; uint48 firstUpdateThreshold; - uint64 firstMaxConfidence; + uint256 firstMaxConfidence; address secondPyth; bytes32 secondPriceFeedId; uint48 secondUpdateThreshold; - uint64 secondMaxConfidence; + uint256 secondMaxConfidence; } // ========== ERRORS ========== // @@ -84,7 +84,7 @@ contract PythPriceFeeds is PriceSubmodule { /// /// @param paramsIndex_ The index of the parameter /// @param maxConfidence_ The maximum confidence (in output decimals scale) - error Pyth_ParamsMaxConfidenceInvalid(uint8 paramsIndex_, uint64 maxConfidence_); + error Pyth_ParamsMaxConfidenceInvalid(uint8 paramsIndex_, uint256 maxConfidence_); /// @notice The price feed is invalid /// @dev This is triggered if the Pyth contract call reverts, @@ -214,7 +214,7 @@ contract PythPriceFeeds is PriceSubmodule { address pyth_, bytes32 priceFeedId_, uint48 updateThreshold_, - uint64 maxConfidence_, + uint256 maxConfidence_, uint8 outputDecimals_ ) internal view returns (uint256) { IPyth.Price memory priceData; @@ -260,7 +260,7 @@ contract PythPriceFeeds is PriceSubmodule { // = maxConfidence * 10^(expo - outputDecimals) // Note: Result is cast to uint64 since it's compared against priceData.conf (uint64) uint64 maxConfidenceInPythScale = SafeCast.encodeUInt64( - uint256(maxConfidence_).mulDiv( + maxConfidence_.mulDiv( 10 ** uint256(uint32(-priceData.expo)), 10 ** uint256(outputDecimals_) ) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index d1e28c7b1..ba2570f3e 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -98,7 +98,7 @@ contract PythPriceFeedsTest is Test { address pyth_, bytes32 priceFeedId_, uint48 updateThreshold_, - uint64 maxConfidence_ + uint256 maxConfidence_ ) internal pure returns (bytes memory params) { return abi.encode(pyth_, priceFeedId_, updateThreshold_, maxConfidence_); } @@ -107,11 +107,11 @@ contract PythPriceFeedsTest is Test { address firstPyth_, bytes32 firstPriceFeedId_, uint48 firstUpdateThreshold_, - uint64 firstMaxConfidence_, + uint256 firstMaxConfidence_, address secondPyth_, bytes32 secondPriceFeedId_, uint48 secondUpdateThreshold_, - uint64 secondMaxConfidence_ + uint256 secondMaxConfidence_ ) internal pure returns (bytes memory params) { return abi.encode( @@ -382,7 +382,7 @@ contract PythPriceFeedsTest is Test { // confidenceExponent = 18 + 0 = 18 // Use a higher maxConfidence to get a meaningful threshold // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint64 maxConfidence = 1e19; + uint256 maxConfidence = 1e19; uint64 maxConfInPythScale = 10; priceConfidence_ = uint64( bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) @@ -1026,7 +1026,7 @@ contract PythPriceFeedsTest is Test { // confidenceExponent = 18 + 0 = 18 // Use a higher maxConfidence to get a meaningful threshold // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint64 maxConfidence = 1e19; + uint256 maxConfidence = 1e19; uint64 maxConfInPythScale = 10; priceConfidence_ = uint64( bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) @@ -1343,7 +1343,7 @@ contract PythPriceFeedsTest is Test { // confidenceExponent = 18 + 0 = 18 // Use a higher maxConfidence to get a meaningful threshold // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint64 maxConfidence = 1e19; + uint256 maxConfidence = 1e19; uint64 maxConfInPythScale = 10; priceConfidence_ = uint64( bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) @@ -2004,7 +2004,7 @@ contract PythPriceFeedsTest is Test { // confidenceExponent = 18 + 0 = 18 // Use a higher maxConfidence to get a meaningful threshold // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint64 maxConfidence = 1e19; + uint256 maxConfidence = 1e19; uint64 maxConfInPythScale = 10; priceConfidence_ = uint64( bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) @@ -2321,7 +2321,7 @@ contract PythPriceFeedsTest is Test { // confidenceExponent = 18 + 0 = 18 // Use a higher maxConfidence to get a meaningful threshold // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint64 maxConfidence = 1e19; + uint256 maxConfidence = 1e19; uint64 maxConfInPythScale = 10; priceConfidence_ = uint64( bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) From 0a168b4a56b9553834013f72074ac6d0325eadfc Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 17:33:11 +0400 Subject: [PATCH 12/19] test: add fork test for Pyth submodule --- .../modules/PRICE/OlympusPricev1_2Fork.t.sol | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/test/modules/PRICE/OlympusPricev1_2Fork.t.sol b/src/test/modules/PRICE/OlympusPricev1_2Fork.t.sol index 7860047f5..110def2f1 100644 --- a/src/test/modules/PRICE/OlympusPricev1_2Fork.t.sol +++ b/src/test/modules/PRICE/OlympusPricev1_2Fork.t.sol @@ -27,6 +27,7 @@ import {PRICEv1} from "src/modules/PRICE/PRICE.v1.sol"; import {OlympusPricev1_2} from "src/modules/PRICE/OlympusPrice.v1_2.sol"; import {OlympusPricev2} from "src/modules/PRICE/OlympusPrice.v2.sol"; import {ChainlinkPriceFeeds} from "modules/PRICE/submodules/feeds/ChainlinkPriceFeeds.sol"; +import {PythPriceFeeds} from "modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; import {SimplePriceFeedStrategy} from "modules/PRICE/submodules/strategies/SimplePriceFeedStrategy.sol"; import {RolesAdmin} from "src/policies/RolesAdmin.sol"; @@ -53,8 +54,12 @@ contract OlympusPricev1_2ForkTest is Test { address public constant TIMELOCK = 0x953EA3223d2dd3c1A91E9D6cca1bf7Af162C9c39; address public constant CONVERTIBLE_DEPOSIT_ACTIVATOR = 0xA0ca0F496B6295f949EddA2DF5FcD3877d5a253E; + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant PYTH = 0x4305FB66699C3B2702D4d05CF36551390A4c69C6; uint256 internal constant OHM_USD_PRICE = 20e18; + bytes32 internal constant ETH_USD_FEED_ID = + 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace; // System contracts Kernel public kernel; @@ -69,6 +74,7 @@ contract OlympusPricev1_2ForkTest is Test { // Submodules ChainlinkPriceFeeds public chainlinkPrice; + PythPriceFeeds public pythPrice; SimplePriceFeedStrategy public strategy; // Permissioned addresses @@ -158,6 +164,7 @@ contract OlympusPricev1_2ForkTest is Test { // Deploy submodules chainlinkPrice = new ChainlinkPriceFeeds(price); + pythPrice = new PythPriceFeeds(price); strategy = new SimplePriceFeedStrategy(price); // Upgrade PRICE module to v1.2 @@ -175,14 +182,20 @@ contract OlympusPricev1_2ForkTest is Test { // Install submodules vm.startPrank(moduleWriter); price.installSubmodule(chainlinkPrice); + price.installSubmodule(pythPrice); price.installSubmodule(strategy); vm.stopPrank(); + // TODO remove Chainlink mock price feed + ohmUsdPriceFeed = new MockPriceFeed(); ohmUsdPriceFeed.setDecimals(8); // Configure OHM asset with MA tracking and OHM-ETH/ETH-USD feeds _configureOhmAsset(); + + // Configure WETH asset with Pyth feed + _configureWethAsset(); } // ========== HELPER FUNCTIONS ========== // @@ -228,6 +241,45 @@ contract OlympusPricev1_2ForkTest is Test { vm.stopPrank(); } + function _configureWethAsset() internal { + // Configure WETH with real Pyth price feed for ETH/USD + uint48 updateThreshold = 24 hours; + // Max confidence in absolute price terms (output decimals, 18) + // This value allows for $10 difference + uint256 maxConfidence = 10e18; + + vm.startPrank(priceWriterV2); + + // Configure WETH with Pyth feed + PythPriceFeeds.OneFeedParams memory ethUsdParams = PythPriceFeeds.OneFeedParams( + PYTH, + ETH_USD_FEED_ID, + updateThreshold, + maxConfidence + ); + + IPRICEv2.Component[] memory feeds = new IPRICEv2.Component[](1); + feeds[0] = IPRICEv2.Component( + toSubKeycode("PRICE.PYTH"), + PythPriceFeeds.getOneFeedPrice.selector, + abi.encode(ethUsdParams) + ); + + // This will revert if calling the Pyth feed fails + price.addAsset( + address(WETH), + false, // storeMovingAverage + false, // useMovingAverage + uint32(0), // movingAverageDuration + uint48(0), // lastObservationTime + new uint256[](0), // observations + IPRICEv2.Component(toSubKeycode(bytes20(0)), bytes4(0), abi.encode(0)), // strategy + feeds + ); + + vm.stopPrank(); + } + function _warpToNextHeartbeat() internal { // Warp to the next heartbeat timestamp vm.warp(heart.lastBeat() + heart.frequency()); From 8f818f76c529bea567c8b7af22c87ae209827c73 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 17:44:47 +0400 Subject: [PATCH 13/19] test: add fuzz tests for confidence values below maximum --- .../submodules/feeds/PythPriceFeeds.t.sol | 165 +++++++++++++++++- 1 file changed, 159 insertions(+), 6 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index ba2570f3e..53cc9f8d8 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -459,7 +459,7 @@ contract PythPriceFeedsTest is Test { } // [X] it returns the price without scaling - function test_getOneFeedPrice_success_expoEqualsNegativeOutputDecimals() public { + function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals() public { // expo = -18, outputDecimals = 18, totalExponent = 0 int64 price = 123456789; int32 expo = -18; @@ -514,7 +514,7 @@ contract PythPriceFeedsTest is Test { } // [X] it correctly converts the price by dividing - function test_getOneFeedPrice_success_expoVeryNegative() public { + function test_getOneFeedPrice_expoVeryNegative() public { // expo = -20, outputDecimals = 18, totalExponent = -2 int64 price = 100000000; int32 expo = -20; @@ -532,11 +532,9 @@ contract PythPriceFeedsTest is Test { assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); } - // TODO required? - - // given the confidence interval is below the maximum (after conversion) + // given the confidence interval equals the maximum // [X] it returns the correct price - function test_getOneFeedPrice_success_confidenceEqualsMaximum() public { + function test_getOneFeedPrice_confidenceEqualsMaximum() public { // Confidence interval equals maximum threshold // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals // MAX_CONFIDENCE = 2e16, so CONF_1 should pass @@ -558,6 +556,31 @@ contract PythPriceFeedsTest is Test { ); } + // given the confidence interval is below the maximum + // [X] it correctly converts the price + function test_getOneFeedPrice_confidenceBelowMaximum(uint64 priceConfidence_) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence below maximum threshold (1 <= confidence <= 2e6) + priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when confidence is below maximum" + ); + } + // given the publish time is >= the threshold boundary // [X] it returns the correct price function test_getOneFeedPrice_givenPublishTimeAboveThreshold(uint48 publishTime_) public { @@ -985,6 +1008,68 @@ contract PythPriceFeedsTest is Test { assertEq(priceInt, expected, "Price should match expected for negative expo first feed"); } + // given the confidence interval is at the maximum (boundary case) + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceEqualsMaximum() public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence exactly at the maximum (2e6) + uint64 priceConfidence_ = 2e6; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected when confidence equals maximum for negative expo first feed" + ); + } + + // given the confidence interval is below the maximum + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceBelowMaximum( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence below the maximum (1 <= confidence <= 2e6) + priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected when confidence is below maximum for negative expo first feed" + ); + } + // given the first feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive function test_getTwoFeedPriceDiv_givenFirstFeedExpoPositive_reverts(int32 expo_) public { @@ -1302,6 +1387,74 @@ contract PythPriceFeedsTest is Test { assertEq(priceInt, expected, "Price should match expected for negative expo second feed"); } + // given the confidence interval is at the maximum (boundary case) + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceEqualsMaximum() public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence exactly at the maximum (2e6) + uint64 priceConfidence_ = 2e6; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + 10 ** PRICE_DECIMALS, + (500000000 * 10 ** PRICE_DECIMALS) / 1e8 + ); + assertEq( + priceInt, + expected, + "Price should match expected when confidence equals maximum for negative expo second feed" + ); + } + + // given the confidence interval is below the maximum + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceBelowMaximum( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence below the maximum (1 <= confidence <= 2e6) + priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + 10 ** PRICE_DECIMALS, + (500000000 * 10 ** PRICE_DECIMALS) / 1e8 + ); + assertEq( + priceInt, + expected, + "Price should match expected when confidence is below maximum for negative expo second feed" + ); + } + // given the second feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive function test_getTwoFeedPriceDiv_givenSecondFeedExpoPositive_reverts(int32 expo_) public { From a18b549f40017567eac929e03167c1ee526c06cc Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 18:04:31 +0400 Subject: [PATCH 14/19] test: more comprehensive tests --- .../submodules/feeds/PythPriceFeeds.t.sol | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol index 53cc9f8d8..e3b29146c 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol @@ -13,6 +13,11 @@ import {MockPyth} from "src/test/mocks/MockPyth.sol"; // Libraries import {FullMath} from "src/libraries/FullMath.sol"; +// Interfaces +import {IERC165} from "@openzeppelin-4.8.0/interfaces/IERC165.sol"; +import {IVersioned} from "src/interfaces/IVersioned.sol"; +import {IPyth} from "src/interfaces/IPyth.sol"; + // Bophades import {Kernel} from "src/Kernel.sol"; import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; @@ -234,6 +239,77 @@ contract PythPriceFeedsTest is Test { pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); } + // given the pyth contract returns data with incorrect length + // [X] it reverts with Pyth_FeedInvalid + function test_getOneFeedPrice_returnDataLengthInvalid_reverts() public { + address mockPyth = address(0xDEAD); + + // Mock the call to succeed but return data with wrong length (not 128 bytes) + // The _PRICE_DATA_SIZE is 128 bytes, so we return something smaller + bytes memory wrongLengthData = abi.encode(uint256(1), uint256(2)); // Only 64 bytes + + vm.mockCall( + mockPyth, + abi.encodeWithSelector( + IPyth.getPriceNoOlderThan.selector, + PRICE_ID_1, + uint256(UPDATE_THRESHOLD) + ), + wrongLengthData + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedInvalid.selector, + mockPyth, + PRICE_ID_1 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + mockPyth, + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + vm.clearMockedCalls(); + } + + // given the pyth contract reverts with return data + // [X] it bubbles up the revert + function test_getOneFeedPrice_pythRevertsWithData_bubblesUpRevert() public { + address mockPyth = address(0xBEEF); + + // Create a custom error + bytes memory customError = abi.encodeWithSignature( + "CustomError(string)", + "Test revert message" + ); + + // Mock the call to revert with data + vm.mockCallRevert( + mockPyth, + abi.encodeWithSelector( + IPyth.getPriceNoOlderThan.selector, + PRICE_ID_1, + uint256(UPDATE_THRESHOLD) + ), + customError + ); + + // Expect the revert to bubble up + vm.expectRevert(customError); + + bytes memory params = encodeOneFeedParams( + mockPyth, + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + // given the price is <= 0 // [X] it reverts with Pyth_FeedPriceInvalid function test_getOneFeedPrice_feedPriceInvalid_reverts(int64 price_) public { @@ -605,6 +681,54 @@ contract PythPriceFeedsTest is Test { ); } + // given the publish time equals exactly the update threshold boundary + // [X] it returns the correct price + function test_getOneFeedPrice_publishTimeExactlyAtThreshold() public { + // Set publish time to exactly block.timestamp - UPDATE_THRESHOLD + // This tests the boundary condition: publishTime == blockTimestamp - updateThreshold + // The validation checks: publishTime < blockTimestamp - updateThreshold (strict <) + // So when publishTime == blockTimestamp - updateThreshold, it should pass + uint256 publishTime = block.timestamp - UPDATE_THRESHOLD; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when publish time exactly equals threshold boundary" + ); + } + + // given outputDecimals is different from default (18) + // [X] it correctly converts prices to the specified output decimals + function test_getOneFeedPrice_outputDecimalsFuzz(uint8 outputDecimals_) public { + // Bound output decimals to reasonable range [8, 36] to avoid rounding issues and overflow + // Using >= 8 ensures we don't lose precision (expo = -8) + outputDecimals_ = uint8(bound(outputDecimals_, 8, 36)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), outputDecimals_, params); + + // expo = -8, price = 123456789, outputDecimals = outputDecimals_ + // Conversion formula: price * 10^outputDecimals / 10^(-expo) = price * 10^outputDecimals / 10^8 + // = 123456789 * 10^(outputDecimals_ - 8) + uint256 expected = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); + assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); + } + // ========= TWO FEED TESTS - DIV ========= // // given all parameters are valid for two feeds @@ -630,6 +754,30 @@ contract PythPriceFeedsTest is Test { assertEq(priceInt, expected, "Divided price should match expected calculation"); } + // given the second feed denominator price converts to zero + // [X] it returns zero without reverting + function test_getTwoFeedPriceDiv_denominatorPriceZero() public { + // Set up a very small price with a very negative expo that will round down to 0 + int64 smallPrice = 1; + int32 veryNegativeExpo = -19; + pyth.setPrice(PRICE_ID_3, smallPrice, CONF_3, veryNegativeExpo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // Should return 0 when denominator price converts to 0 + assertEq(priceInt, 0, "Should return 0 when denominator price converts to 0"); + } + // given the first pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid function test_getTwoFeedPriceDiv_firstPythZero_reverts() public { @@ -1712,6 +1860,34 @@ contract PythPriceFeedsTest is Test { ); } + // given outputDecimals is different from default (18) for two feeds division + // [X] it correctly converts prices to the specified output decimals + function test_getTwoFeedPriceDiv_outputDecimalsFuzz(uint8 outputDecimals_) public { + // Bound output decimals to reasonable range [18, 36] to avoid overflow + outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals_, params); + + // First: price = 123456789, expo = -8 -> converts to 123456789 * 10^(outputDecimals_ - 8) + uint256 firstPrice = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); + // Second: price = 500000000, expo = -18 -> converts to 500000000 * 10^(outputDecimals_ - 18) + uint256 secondPrice = uint256(uint64(PRICE_3)) * 10 ** (outputDecimals_ - 18); + uint256 expected = firstPrice.mulDiv(10 ** outputDecimals_, secondPrice); + assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); + } + // ========= TWO FEED TESTS - MUL ========= // // given all parameters are valid for two feeds @@ -2689,5 +2865,59 @@ contract PythPriceFeedsTest is Test { "Multiplied price should match expected when second feed at threshold" ); } + + // given outputDecimals is different from default (18) for two feeds multiplication + // [X] it correctly converts prices to the specified output decimals + function test_getTwoFeedPriceMul_outputDecimalsFuzz(uint8 outputDecimals_) public { + // Bound output decimals to reasonable range [18, 36] to avoid overflow + outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals_, params); + + // First: price = 123456789, expo = -8 -> converts to 123456789 * 10^(outputDecimals_ - 8) + uint256 firstPrice = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); + // Second: price = 500000000, expo = -18 -> converts to 500000000 * 10^(outputDecimals_ - 18) + uint256 secondPrice = uint256(uint64(PRICE_3)) * 10 ** (outputDecimals_ - 18); + uint256 expected = firstPrice.mulDiv(secondPrice, 10 ** outputDecimals_); + assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); + } + + // ========= IERC165 FUNCTIONS ========= // + + // given supportsInterface is called with IERC165 interface ID + // [X] it returns true + function test_supportsInterface_IERC165_returnsTrue() public view { + bytes4 interfaceId = type(IERC165).interfaceId; + bool supported = pythSubmodule.supportsInterface(interfaceId); + assertTrue(supported, "Should support IERC165 interface"); + } + + // given supportsInterface is called with IVersioned interface ID + // [X] it returns true + function test_supportsInterface_IVersioned_returnsTrue() public view { + bytes4 interfaceId = type(IVersioned).interfaceId; + bool supported = pythSubmodule.supportsInterface(interfaceId); + assertTrue(supported, "Should support IVersioned interface"); + } + + // given supportsInterface is called with an unsupported interface ID + // [X] it returns false + function test_supportsInterface_unsupported_returnsFalse() public view { + bytes4 interfaceId = 0x12345678; + bool supported = pythSubmodule.supportsInterface(interfaceId); + assertFalse(supported, "Should not support unsupported interface"); + } } /// forge-lint: disable-end(mixed-case-variable,mixed-case-function) From 172a1390766073fb142e4d52f61d073596f9aba4 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 7 Jan 2026 18:29:07 +0400 Subject: [PATCH 15/19] test: split into individual test files for each submodule function --- .../submodules/feeds/PythPriceFeeds.t.sol | 2923 ----------------- .../PythPriceFeeds/PythPriceFeedsTest.sol | 129 + .../PythPriceFeeds/getOneFeedPrice.t.sol | 615 ++++ .../PythPriceFeeds/getTwoFeedPriceDiv.t.sol | 1179 +++++++ .../PythPriceFeeds/getTwoFeedPriceMul.t.sol | 1029 ++++++ .../PythPriceFeeds/supportsInterface.t.sol | 39 + 6 files changed, 2991 insertions(+), 2923 deletions(-) delete mode 100644 src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol create mode 100644 src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/PythPriceFeedsTest.sol create mode 100644 src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol create mode 100644 src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol create mode 100644 src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol create mode 100644 src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/supportsInterface.t.sol diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol deleted file mode 100644 index e3b29146c..000000000 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds.t.sol +++ /dev/null @@ -1,2923 +0,0 @@ -// SPDX-License-Identifier: Unlicense -/// forge-lint: disable-start(mixed-case-variable,mixed-case-function) -pragma solidity >=0.8.0; - -// Test -import {Test} from "@forge-std-1.9.6/Test.sol"; -import {ModuleTestFixtureGenerator} from "src/test/lib/ModuleTestFixtureGenerator.sol"; - -// Mocks -import {MockPrice} from "src/test/mocks/MockPrice.v2.sol"; -import {MockPyth} from "src/test/mocks/MockPyth.sol"; - -// Libraries -import {FullMath} from "src/libraries/FullMath.sol"; - -// Interfaces -import {IERC165} from "@openzeppelin-4.8.0/interfaces/IERC165.sol"; -import {IVersioned} from "src/interfaces/IVersioned.sol"; -import {IPyth} from "src/interfaces/IPyth.sol"; - -// Bophades -import {Kernel} from "src/Kernel.sol"; -import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; - -contract PythPriceFeedsTest is Test { - using FullMath for uint256; - using ModuleTestFixtureGenerator for PythPriceFeeds; - - MockPyth internal pyth; - MockPyth internal secondPyth; - - Kernel internal kernel; - MockPrice internal mockPrice; - PythPriceFeeds internal pythSubmodule; - - bytes32 internal constant PRICE_ID_1 = keccak256("PRICE_ID_1"); - bytes32 internal constant PRICE_ID_2 = keccak256("PRICE_ID_2"); - bytes32 internal constant PRICE_ID_3 = keccak256("PRICE_ID_3"); - - // Example: price=123456789, expo=-8 represents 1.23456789 - int64 internal constant PRICE_1 = 123456789; - int32 internal constant EXPO_1 = -8; - uint64 internal constant CONF_1 = 1000000; - uint256 internal constant EXPECTED_PRICE_1_18_DEC = 1234567890000000000; // 1.23456789 * 10^18 - - // Example: price=100000000, expo=8 represents 100000000 * 10^8 - int64 internal constant PRICE_2 = 100000000; - int32 internal constant EXPO_2 = 8; - uint64 internal constant CONF_2 = 2000000; - - // Example: price=500000000, expo=-18 represents 0.0000000005 - int64 internal constant PRICE_3 = 500000000; - int32 internal constant EXPO_3 = -18; - uint64 internal constant CONF_3 = 500000; - - uint8 internal constant PRICE_DECIMALS = 18; - uint48 internal constant UPDATE_THRESHOLD = 100; - // MAX_CONFIDENCE is in output decimals (18 decimals) - // CONF_1 = 1000000 with expo=-8 - // In decimal: 1000000 * 10^-8 = 0.01 - // In output decimals (18): 0.01 * 10^18 = 10000000000000000 (1e16) - // Set MAX_CONFIDENCE to 2e16 to allow CONF_1 but reject higher values - uint64 internal constant MAX_CONFIDENCE = 20000000000000000; // 2 * 10^16 in 18 decimals - - function setUp() public { - vm.warp(51 * 365 * 24 * 60 * 60); // Set timestamp at roughly Jan 1, 2021 - - // Set up the Pyth submodule - { - // Deploy kernel - kernel = new Kernel(); - - // Deploy mockPrice - mockPrice = new MockPrice(kernel, uint8(18), uint32(8 hours)); - mockPrice.setTimestamp(uint48(block.timestamp)); - mockPrice.setPriceDecimals(PRICE_DECIMALS); - - // Deploy Pyth submodule - pythSubmodule = new PythPriceFeeds(mockPrice); - } - - // Set up the Pyth contracts - { - pyth = new MockPyth(); - secondPyth = new MockPyth(); - - // With the default update threshold, these prices will NOT be stale - - // Set up price feed 1: 1.23456789 (expo=-8) - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp - UPDATE_THRESHOLD); - - // Set up price feed 2: 100000000 * 10^8 (expo=8) - pyth.setPrice(PRICE_ID_2, PRICE_2, CONF_2, EXPO_2, block.timestamp - UPDATE_THRESHOLD); - - // Set up price feed 3: 0.0000000005 (expo=-18) - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp - UPDATE_THRESHOLD); - } - } - - // ========= HELPER METHODS ========= // - - function encodeOneFeedParams( - address pyth_, - bytes32 priceFeedId_, - uint48 updateThreshold_, - uint256 maxConfidence_ - ) internal pure returns (bytes memory params) { - return abi.encode(pyth_, priceFeedId_, updateThreshold_, maxConfidence_); - } - - function encodeTwoFeedParams( - address firstPyth_, - bytes32 firstPriceFeedId_, - uint48 firstUpdateThreshold_, - uint256 firstMaxConfidence_, - address secondPyth_, - bytes32 secondPriceFeedId_, - uint48 secondUpdateThreshold_, - uint256 secondMaxConfidence_ - ) internal pure returns (bytes memory params) { - return - abi.encode( - firstPyth_, - firstPriceFeedId_, - firstUpdateThreshold_, - firstMaxConfidence_, - secondPyth_, - secondPriceFeedId_, - secondUpdateThreshold_, - secondMaxConfidence_ - ); - } - - // ========= ONE FEED TESTS ========= // - - // given all parameters are valid - // [X] it returns the correct price in output decimals - function test_getOneFeedPrice_success() public view { - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - assertEq( - priceInt, - EXPECTED_PRICE_1_18_DEC, - "Price should match expected value for expo=-8" - ); - } - - // given the pyth contract address is zero - // [X] it reverts with Pyth_ParamsPythInvalid - function test_getOneFeedPrice_pythZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPythInvalid.selector, - 0, - address(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(0), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getOneFeedPrice_priceFeedIdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, - 1, - bytes32(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - bytes32(0), - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the update threshold is zero - // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getOneFeedPrice_updateThresholdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, - 2, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams(address(pyth), PRICE_ID_1, 0, MAX_CONFIDENCE); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the max confidence is zero - // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getOneFeedPrice_maxConfidenceZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, - 3, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams(address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, 0); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the pyth contract doesn't implement IPyth - // [X] it reverts with Pyth_FeedInvalid - function test_getOneFeedPrice_pythContractInvalid_reverts() public { - // Use a contract that doesn't implement IPyth - address invalidPyth = address(0x1234); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedInvalid.selector, - invalidPyth, - PRICE_ID_1 - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - invalidPyth, - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the pyth contract returns data with incorrect length - // [X] it reverts with Pyth_FeedInvalid - function test_getOneFeedPrice_returnDataLengthInvalid_reverts() public { - address mockPyth = address(0xDEAD); - - // Mock the call to succeed but return data with wrong length (not 128 bytes) - // The _PRICE_DATA_SIZE is 128 bytes, so we return something smaller - bytes memory wrongLengthData = abi.encode(uint256(1), uint256(2)); // Only 64 bytes - - vm.mockCall( - mockPyth, - abi.encodeWithSelector( - IPyth.getPriceNoOlderThan.selector, - PRICE_ID_1, - uint256(UPDATE_THRESHOLD) - ), - wrongLengthData - ); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedInvalid.selector, - mockPyth, - PRICE_ID_1 - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - mockPyth, - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - vm.clearMockedCalls(); - } - - // given the pyth contract reverts with return data - // [X] it bubbles up the revert - function test_getOneFeedPrice_pythRevertsWithData_bubblesUpRevert() public { - address mockPyth = address(0xBEEF); - - // Create a custom error - bytes memory customError = abi.encodeWithSignature( - "CustomError(string)", - "Test revert message" - ); - - // Mock the call to revert with data - vm.mockCallRevert( - mockPyth, - abi.encodeWithSelector( - IPyth.getPriceNoOlderThan.selector, - PRICE_ID_1, - uint256(UPDATE_THRESHOLD) - ), - customError - ); - - // Expect the revert to bubble up - vm.expectRevert(customError); - - bytes memory params = encodeOneFeedParams( - mockPyth, - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the price is <= 0 - // [X] it reverts with Pyth_FeedPriceInvalid - function test_getOneFeedPrice_feedPriceInvalid_reverts(int64 price_) public { - price_ = int64(bound(price_, type(int64).min, 0)); - pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedPriceInvalid.selector, - address(pyth), - PRICE_ID_1, - price_ - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the price feed is not found - // [X] it reverts with PriceFeedNotFound - function test_getOneFeedPrice_priceFeedNotFound_reverts() public { - bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given the publish time is before the update threshold - // [X] it reverts with StalePrice - function test_getOneFeedPrice_givenStalePrice_reverts(uint256 publishTime_) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); - - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_maxConfidenceExceeded_reverts(uint64 priceConfidence_) public { - // MAX_CONFIDENCE = 2e16 in output decimals (18) - // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals, so it should pass - // To exceed MAX_CONFIDENCE (2e16), we need conf * 10^10 > 2e16, so conf > 2e6 - priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); - - // Set the price data - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); - - // Calculate the expected maxConfidence in Pyth scale - // EXPO_1 = -8, PRICE_DECIMALS = 18 - // maxConfidence = 2e16 * 1 / 10^*(-8+18) = 2e16 * 1 / 10^10 = 2e6 - uint64 maxConfInPythScale = 2e6; - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - maxConfInPythScale - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getOneFeedPrice_givenExpoNegative() public view { - // expo = -8, outputDecimals = 18, totalExponent = 10 - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - // price = 123456789, expo = -8, outputDecimals = 18 - // outputPrice = 123456789 * 10^(10) = 1234567890000000000 - assertEq( - priceInt, - EXPECTED_PRICE_1_18_DEC, - "Price should match expected value for negative expo" - ); - } - - // given expo is positive (expo > 0) - // [X] it reverts with Pyth_ExponentPositive - function test_getOneFeedPrice_expoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error - // Keep expo within reasonable range [1, 32] to avoid overflow in calculations - expo_ = int32(bound(int256(expo_), 1, 32)); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentPositive.selector, - address(pyth), - PRICE_ID_1, - expo_ - ); - vm.expectRevert(err); - - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // given expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_expoZero_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = 0, outputDecimals = 18 - // confidenceExponent = 18 + 0 = 18 - // Use a higher maxConfidence to get a meaningful threshold - // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint256 maxConfidence = 1e19; - uint64 maxConfInPythScale = 10; - priceConfidence_ = uint64( - bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) - ); - int32 expo = 0; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - maxConfInPythScale - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - maxConfidence - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getOneFeedPrice_expoZero() public { - // expo = 0, outputDecimals = 18, totalExponent = 18 - int64 price = 123456789; - int32 expo = 0; - pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - type(uint64).max // Ensures confidence interval is below the maximum - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - // outputPrice = 123456789 * 10^18 - assertEq(priceInt, 123456789 * 10 ** 18, "Price should match expected value for zero expo"); - } - - // given expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -18, outputDecimals = 18 - // confidenceExponent = 18 + (-18) = 0 - // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 - priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); - int32 expo = -18; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e16) - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // [X] it returns the price without scaling - function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals() public { - // expo = -18, outputDecimals = 18, totalExponent = 0 - int64 price = 123456789; - int32 expo = -18; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - // outputPrice = 123456789 (no scaling needed) - assertEq( - priceInt, - /// forge-lint: disable-next-line(unsafe-typecast) - uint256(uint64(price)), - "Price should match input value when expo equals negative outputDecimals" - ); - } - - // given expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_expoVeryNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -20, outputDecimals = 18 - // confidenceExponent = 18 + (-20) = -2 - // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 - priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); - int32 expo = -20; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e18) - ); - vm.expectRevert(err); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price by dividing - function test_getOneFeedPrice_expoVeryNegative() public { - // expo = -20, outputDecimals = 18, totalExponent = -2 - int64 price = 100000000; - int32 expo = -20; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - // outputPrice = 100000000 / 10^2 = 1000000 - assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); - } - - // given the confidence interval equals the maximum - // [X] it returns the correct price - function test_getOneFeedPrice_confidenceEqualsMaximum() public { - // Confidence interval equals maximum threshold - // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals - // MAX_CONFIDENCE = 2e16, so CONF_1 should pass - // This test verifies that a confidence that is below the maximum passes - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - assertEq( - priceInt, - EXPECTED_PRICE_1_18_DEC, - "Price should be valid when confidence is below maximum" - ); - } - - // given the confidence interval is below the maximum - // [X] it correctly converts the price - function test_getOneFeedPrice_confidenceBelowMaximum(uint64 priceConfidence_) public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - // Test with confidence below maximum threshold (1 <= confidence <= 2e6) - priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - assertEq( - priceInt, - EXPECTED_PRICE_1_18_DEC, - "Price should be valid when confidence is below maximum" - ); - } - - // given the publish time is >= the threshold boundary - // [X] it returns the correct price - function test_getOneFeedPrice_givenPublishTimeAboveThreshold(uint48 publishTime_) public { - // Bound the publish time to a reasonable value - publishTime_ = uint48( - bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp) - ); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - assertEq( - priceInt, - EXPECTED_PRICE_1_18_DEC, - "Price should be valid when publish time is above threshold" - ); - } - - // given the publish time equals exactly the update threshold boundary - // [X] it returns the correct price - function test_getOneFeedPrice_publishTimeExactlyAtThreshold() public { - // Set publish time to exactly block.timestamp - UPDATE_THRESHOLD - // This tests the boundary condition: publishTime == blockTimestamp - updateThreshold - // The validation checks: publishTime < blockTimestamp - updateThreshold (strict <) - // So when publishTime == blockTimestamp - updateThreshold, it should pass - uint256 publishTime = block.timestamp - UPDATE_THRESHOLD; - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - assertEq( - priceInt, - EXPECTED_PRICE_1_18_DEC, - "Price should be valid when publish time exactly equals threshold boundary" - ); - } - - // given outputDecimals is different from default (18) - // [X] it correctly converts prices to the specified output decimals - function test_getOneFeedPrice_outputDecimalsFuzz(uint8 outputDecimals_) public { - // Bound output decimals to reasonable range [8, 36] to avoid rounding issues and overflow - // Using >= 8 ensures we don't lose precision (expo = -8) - outputDecimals_ = uint8(bound(outputDecimals_, 8, 36)); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), outputDecimals_, params); - - // expo = -8, price = 123456789, outputDecimals = outputDecimals_ - // Conversion formula: price * 10^outputDecimals / 10^(-expo) = price * 10^outputDecimals / 10^8 - // = 123456789 * 10^(outputDecimals_ - 8) - uint256 expected = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); - assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); - } - - // ========= TWO FEED TESTS - DIV ========= // - - // given all parameters are valid for two feeds - // [X] it returns the correct divided price (first / second) in output decimals - function test_getTwoFeedPriceDiv_success() public view { - // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals - // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals - // Result: 1234567890000000000 / 500000000 = 2469135780 - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // (1234567890000000000 * 10^18) / 500000000 = 2469135780000000000000000000 - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq(priceInt, expected, "Divided price should match expected calculation"); - } - - // given the second feed denominator price converts to zero - // [X] it returns zero without reverting - function test_getTwoFeedPriceDiv_denominatorPriceZero() public { - // Set up a very small price with a very negative expo that will round down to 0 - int64 smallPrice = 1; - int32 veryNegativeExpo = -19; - pyth.setPrice(PRICE_ID_3, smallPrice, CONF_3, veryNegativeExpo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // Should return 0 when denominator price converts to 0 - assertEq(priceInt, 0, "Should return 0 when denominator price converts to 0"); - } - - // given the first pyth contract address is zero - // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceDiv_firstPythZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPythInvalid.selector, - 0, - address(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(0), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceDiv_firstPriceFeedIdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, - 1, - bytes32(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - bytes32(0), - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first update threshold is zero - // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceDiv_firstUpdateThresholdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, - 2, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - 0, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first max confidence is zero - // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceDiv_firstMaxConfidenceZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, - 3, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - 0, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second pyth contract address is zero - // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceDiv_secondPythZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPythInvalid.selector, - 4, - address(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(0), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceDiv_secondPriceFeedIdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, - 5, - bytes32(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - bytes32(0), - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second update threshold is zero - // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceDiv_secondUpdateThresholdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, - 6, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - 0, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second max confidence is zero - // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceDiv_secondMaxConfidenceZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, - 7, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - 0 - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first feed has invalid price (<= 0) - // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceDiv_firstFeedPriceInvalid_reverts(int64 price_) public { - price_ = int64(bound(price_, type(int64).min, 0)); - pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedPriceInvalid.selector, - address(pyth), - PRICE_ID_1, - price_ - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second feed has invalid price (<= 0) - // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceDiv_secondFeedPriceInvalid_reverts(int64 price_) public { - price_ = int64(bound(price_, type(int64).min, 0)); - pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedPriceInvalid.selector, - address(pyth), - PRICE_ID_3, - price_ - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first price feed is not found - // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceDiv_givenFirstFeedPriceFeedNotFound_reverts() public { - bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second price feed is not found - // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceDiv_givenSecondFeedPriceFeedNotFound_reverts() public { - bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceDiv_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); - - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts( - uint256 publishTime_ - ) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first feed expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e6) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative() public view { - // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 - // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq(priceInt, expected, "Price should match expected for negative expo first feed"); - } - - // given the confidence interval is at the maximum (boundary case) - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceEqualsMaximum() public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - // Test with confidence exactly at the maximum (2e6) - uint64 priceConfidence_ = 2e6; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Price should match expected when confidence equals maximum for negative expo first feed" - ); - } - - // given the confidence interval is below the maximum - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceBelowMaximum( - uint64 priceConfidence_ - ) public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - // Test with confidence below the maximum (1 <= confidence <= 2e6) - priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Price should match expected when confidence is below maximum for negative expo first feed" - ); - } - - // given the first feed expo is positive (expo > 0) - // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error - // Keep expo within reasonable range [1, 32] to avoid overflow in calculations - expo_ = int32(bound(int256(expo_), 1, 32)); - int64 price = 100; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentPositive.selector, - address(pyth), - PRICE_ID_1, - expo_ - ); - vm.expectRevert(err); - - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the first feed expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = 0, outputDecimals = 18 - // confidenceExponent = 18 + 0 = 18 - // Use a higher maxConfidence to get a meaningful threshold - // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint256 maxConfidence = 1e19; - uint64 maxConfInPythScale = 10; - priceConfidence_ = uint64( - bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) - ); - int32 expo = 0; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - maxConfInPythScale - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - maxConfidence, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero() public { - // First feed: expo = 0, price = 100 -> 100 * 10^18 - int64 price = 100; - int32 expo = 0; - pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - type(uint64).max, // Ensures confidence interval is below the maximum - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // First: 100 * 10^18, Second: 500000000 (18 dec) - // Result: (100 * 10^18 * 10^18) / 500000000 = 100 * 10^36 / 5e8 = 2 * 10^29 - uint256 firstPrice = uint256(100) * 10 ** 18; - uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq(priceInt, expected, "Price should match expected for zero expo first feed"); - } - - // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -18, outputDecimals = 18 - // confidenceExponent = 18 + (-18) = 0 - // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 - priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); - int32 expo = -18; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e16) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { - // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) - int64 price = 1e9; - int32 expo = -18; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // First: 1e9, Second: 500000000 (18 dec) - // Result: (1e9 * 10^18) / 500000000 = 1e27 / 5e8 = 2e18 - uint256 firstPrice = uint256(1e9); - uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Price should match expected when first feed expo equals negative outputDecimals" - ); - } - - // given the first feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -20, outputDecimals = 18 - // confidenceExponent = 18 + (-20) = -2 - // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 - priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); - int32 expo = -20; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e18) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative() public { - // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 - int64 price = 1e10; - int32 expo = -20; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // First: 1e10 / 100 = 1e8, Second: 500000000 (18 dec) - // Result: (1e8 * 10^18) / 500000000 = 1e26 / 5e8 = 2e17 - uint256 firstPrice = uint256(1e10) / 100; - uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Price should match expected for very negative expo first feed" - ); - } - - // given the first feed publish time is >= the threshold boundary - // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { - // Bound the publish time to a reasonable value - publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); - // Second feed keeps publish time as set in setUp() - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Divided price should match expected when first feed at threshold" - ); - } - - // given the second feed expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - uint64(2e6) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative() public { - // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 - pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 1234567890000000000 - // Result: (1234567890000000000 * 10^18) / 1234567890000000000 = 10^18 - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( - 10 ** PRICE_DECIMALS, - EXPECTED_PRICE_1_18_DEC - ); - assertEq(priceInt, expected, "Price should match expected for negative expo second feed"); - } - - // given the confidence interval is at the maximum (boundary case) - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceEqualsMaximum() public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - // Test with confidence exactly at the maximum (2e6) - uint64 priceConfidence_ = 2e6; - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( - 10 ** PRICE_DECIMALS, - (500000000 * 10 ** PRICE_DECIMALS) / 1e8 - ); - assertEq( - priceInt, - expected, - "Price should match expected when confidence equals maximum for negative expo second feed" - ); - } - - // given the confidence interval is below the maximum - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceBelowMaximum( - uint64 priceConfidence_ - ) public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - // Test with confidence below the maximum (1 <= confidence <= 2e6) - priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( - 10 ** PRICE_DECIMALS, - (500000000 * 10 ** PRICE_DECIMALS) / 1e8 - ); - assertEq( - priceInt, - expected, - "Price should match expected when confidence is below maximum for negative expo second feed" - ); - } - - // given the second feed expo is positive (expo > 0) - // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error - // Keep expo within reasonable range [1, 32] to avoid overflow in calculations - expo_ = int32(bound(int256(expo_), 1, 32)); - int64 price = 100; - pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentPositive.selector, - address(pyth), - PRICE_ID_3, - expo_ - ); - vm.expectRevert(err); - - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // given the second feed expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = 0, outputDecimals = 18 - // confidenceExponent = 18 + 0 = 18 - // Use a higher maxConfidence to get a meaningful threshold - // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint256 maxConfidence = 1e19; - uint64 maxConfInPythScale = 10; - priceConfidence_ = uint64( - bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) - ); - int32 expo = 0; - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - maxConfInPythScale - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - maxConfidence - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero() public { - // Second feed: expo = 0, price = 100 -> 100 * 10^18 - int64 price = 100; - int32 expo = 0; - pyth.setPrice(PRICE_ID_3, price, 1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - type(uint64).max // Ensures confidence interval is below the maximum - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 100 * 10^18 - // Result: (1234567890000000000 * 10^18) / (100 * 10^18) = 12345678900000000 - uint256 secondPrice = uint256(100) * 10 ** 18; - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); - assertEq(priceInt, expected, "Price should match expected for zero expo second feed"); - } - - // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -18, outputDecimals = 18 - // confidenceExponent = 18 + (-18) = 0 - // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 - priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); - int32 expo = -18; - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - uint64(2e16) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { - // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) - int64 price = 1e9; - int32 expo = -18; - pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 1e9 - // Result: (1234567890000000000 * 10^18) / 1e9 = 1234567890000000000000000000 - uint256 secondPrice = uint256(1e9); - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); - assertEq( - priceInt, - expected, - "Price should match expected when second feed expo equals negative outputDecimals" - ); - } - - // given the second feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -20, outputDecimals = 18 - // confidenceExponent = 18 + (-20) = -2 - // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 - priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); - int32 expo = -20; - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - uint64(2e18) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative() public { - // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 - int64 price = 1e10; - int32 expo = -20; - pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 1e8 - // Result: (1234567890000000000 * 10^18) / 1e8 - uint256 secondPrice = uint256(1e10) / 100; - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); - assertEq( - priceInt, - expected, - "Price should match expected for very negative expo second feed" - ); - } - - // given the second feed publish time is >= the threshold boundary - // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { - // Bound the publish time to a reasonable value - publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - // First feed keeps publish time as set in setUp() - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); - assertEq( - priceInt, - expected, - "Divided price should match expected when second feed at threshold" - ); - } - - // given outputDecimals is different from default (18) for two feeds division - // [X] it correctly converts prices to the specified output decimals - function test_getTwoFeedPriceDiv_outputDecimalsFuzz(uint8 outputDecimals_) public { - // Bound output decimals to reasonable range [18, 36] to avoid overflow - outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18, // scale max confidence to the new output decimals - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals_, params); - - // First: price = 123456789, expo = -8 -> converts to 123456789 * 10^(outputDecimals_ - 8) - uint256 firstPrice = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); - // Second: price = 500000000, expo = -18 -> converts to 500000000 * 10^(outputDecimals_ - 18) - uint256 secondPrice = uint256(uint64(PRICE_3)) * 10 ** (outputDecimals_ - 18); - uint256 expected = firstPrice.mulDiv(10 ** outputDecimals_, secondPrice); - assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); - } - - // ========= TWO FEED TESTS - MUL ========= // - - // given all parameters are valid for two feeds - // [X] it returns the correct multiplied price (first * second) in output decimals - function test_getTwoFeedPriceMul_success() public view { - // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals - // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals - // Result: (1234567890000000000 * 500000000) / 10^18 - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // (1234567890000000000 * 500000000) / 10^18 = 617283945000 - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq(priceInt, expected, "Multiplied price should match expected calculation"); - } - - // given the first pyth contract address is zero - // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceMul_firstPythZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPythInvalid.selector, - 0, - address(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(0), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second pyth contract address is zero - // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceMul_secondPythZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPythInvalid.selector, - 4, - address(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(0), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceMul_firstPriceFeedIdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, - 1, - bytes32(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - bytes32(0), - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second price feed ID is zero - // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceMul_secondPriceFeedIdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, - 5, - bytes32(0) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - bytes32(0), - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first update threshold is zero - // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceMul_firstUpdateThresholdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, - 2, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - 0, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second update threshold is zero - // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceMul_secondUpdateThresholdZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, - 6, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - 0, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first max confidence is zero - // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceMul_firstMaxConfidenceZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, - 3, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - 0, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second max confidence is zero - // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceMul_secondMaxConfidenceZero_reverts() public { - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, - 7, - 0 - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - 0 - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first feed has invalid price (<= 0) - // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceMul_firstFeedPriceInvalid_reverts(int64 price_) public { - price_ = int64(bound(price_, type(int64).min, 0)); - pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedPriceInvalid.selector, - address(pyth), - PRICE_ID_1, - price_ - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second feed has invalid price (<= 0) - // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceMul_secondFeedPriceInvalid_reverts(int64 price_) public { - price_ = int64(bound(price_, type(int64).min, 0)); - pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedPriceInvalid.selector, - address(pyth), - PRICE_ID_3, - price_ - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first price feed is not found - // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceMul_givenFirstFeedPriceFeedNotFound_reverts() public { - bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second price feed is not found - // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceMul_givenSecondFeedPriceFeedNotFound_reverts() public { - bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); - - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second feed publish time is < the threshold boundary - // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( - uint256 publishTime_ - ) public { - // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) - publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - - bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first feed expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e6) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative() public view { - // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 - // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // (1234567890000000000 * 500000000) / 10^18 = 617283945000 - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq(priceInt, expected, "Price should match expected for negative expo first feed"); - } - - // given the first feed expo is positive (expo > 0) - // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceMul_givenFirstFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error - // Keep expo within reasonable range [1, 32] to avoid overflow in calculations - expo_ = int32(bound(int256(expo_), 1, 32)); - int64 price = 100; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentPositive.selector, - address(pyth), - PRICE_ID_1, - expo_ - ); - vm.expectRevert(err); - - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the first feed expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = 0, outputDecimals = 18 - // confidenceExponent = 18 + 0 = 18 - // Use a higher maxConfidence to get a meaningful threshold - // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint256 maxConfidence = 1e19; - uint64 maxConfInPythScale = 10; - priceConfidence_ = uint64( - bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) - ); - int32 expo = 0; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - maxConfInPythScale - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - maxConfidence, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenFirstFeedExpoZero() public { - // First feed: expo = 0, price = 100 -> 100 * 10^18 - int64 price = 100; - int32 expo = 0; - pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - type(uint64).max, // Ensures confidence interval is below the maximum - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // First: 100 * 10^18, Second: 500000000 (18 dec) - // Result: (100 * 10^18 * 500000000) / 10^18 = 5 * 10^10 - uint256 firstPrice = uint256(100) * 10 ** 18; - uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq(priceInt, expected, "Price should match expected for zero expo first feed"); - } - - // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -18, outputDecimals = 18 - // confidenceExponent = 18 + (-18) = 0 - // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 - priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); - int32 expo = -18; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e16) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { - // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) - int64 price = 1e9; - int32 expo = -18; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // First: 1e9, Second: 500000000 (18 dec) - // Result: (1e9 * 500000000) / 10^18 = 5e17 / 1e18 = 0 (rounds down) - uint256 firstPrice = uint256(1e9); - uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq( - priceInt, - expected, - "Price should match expected when first feed expo equals negative outputDecimals" - ); - } - - // given the first feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -20, outputDecimals = 18 - // confidenceExponent = 18 + (-20) = -2 - // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 - priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); - int32 expo = -20; - pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_1, - priceConfidence_, - uint64(2e18) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative() public { - // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 - int64 price = 1e10; - int32 expo = -20; - pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // First: 1e10 / 100 = 1e8, Second: 500000000 (18 dec) - // Result: (1e8 * 500000000) / 10^18 = 5e16 / 1e18 = 0 (rounds down) - uint256 firstPrice = uint256(1e10) / 100; - uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq( - priceInt, - expected, - "Price should match expected for very negative expo first feed" - ); - } - - // given the first feed publish time is >= the threshold boundary - // [X] it returns the correct price - function test_getTwoFeedPriceMul_givenFirstFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { - // Bound the publish time to a reasonable value - publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); - // Second feed keeps publish time as set in setUp() - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq( - priceInt, - expected, - "Multiplied price should match expected when first feed at threshold" - ); - } - - // given the second feed expo is negative (expo = -8, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -8, outputDecimals = 18 - // confidenceExponent = 18 + (-8) = 10 - // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 - priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - uint64(2e6) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative() public { - // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 - pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 1234567890000000000 - // Result: (1234567890000000000 * 1234567890000000000) / 10^18 - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( - EXPECTED_PRICE_1_18_DEC, - 10 ** PRICE_DECIMALS - ); - assertEq(priceInt, expected, "Price should match expected for negative expo second feed"); - } - - // given the second feed expo is positive (expo > 0) - // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceMul_givenSecondFeedExpoPositive_reverts(int32 expo_) public { - // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error - // Keep expo within reasonable range [1, 32] to avoid overflow in calculations - expo_ = int32(bound(int256(expo_), 1, 32)); - int64 price = 100; - pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_ExponentPositive.selector, - address(pyth), - PRICE_ID_3, - expo_ - ); - vm.expectRevert(err); - - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // given the second feed expo is zero (expo = 0, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = 0, outputDecimals = 18 - // confidenceExponent = 18 + 0 = 18 - // Use a higher maxConfidence to get a meaningful threshold - // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 - uint256 maxConfidence = 1e19; - uint64 maxConfInPythScale = 10; - priceConfidence_ = uint64( - bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) - ); - int32 expo = 0; - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - maxConfInPythScale - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - maxConfidence - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenSecondFeedExpoZero() public { - // Second feed: expo = 0, price = 100 -> 100 * 10^18 - int64 price = 100; - int32 expo = 0; - pyth.setPrice(PRICE_ID_3, price, 1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - type(uint64).max // Ensures confidence interval is below the maximum - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 100 * 10^18 - // Result: (1234567890000000000 * 100 * 10^18) / 10^18 = 1234567890000000000 * 100 - uint256 secondPrice = uint256(100) * 10 ** 18; - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); - assertEq(priceInt, expected, "Price should match expected for zero expo second feed"); - } - - // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -18, outputDecimals = 18 - // confidenceExponent = 18 + (-18) = 0 - // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 - priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); - int32 expo = -18; - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - uint64(2e16) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { - // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) - int64 price = 1e9; - int32 expo = -18; - pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 1e9 - // Result: (1234567890000000000 * 1e9) / 10^18 = 1234567890 - uint256 secondPrice = uint256(1e9); - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); - assertEq( - priceInt, - expected, - "Price should match expected when second feed expo equals negative outputDecimals" - ); - } - - // given the second feed expo is very negative (expo = -20, outputDecimals = 18) - // given the confidence interval is above the maximum - // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { - // expo = -20, outputDecimals = 18 - // confidenceExponent = 18 + (-20) = -2 - // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 - priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); - int32 expo = -20; - pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); - - bytes memory err = abi.encodeWithSelector( - PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, - address(pyth), - PRICE_ID_3, - priceConfidence_, - uint64(2e18) - ); - vm.expectRevert(err); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - } - - // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative() public { - // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 - int64 price = 1e10; - int32 expo = -20; - pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - // First: 1234567890000000000 (18 dec), Second: 1e8 - // Result: (1234567890000000000 * 1e8) / 10^18 = 123456789 - uint256 secondPrice = uint256(1e10) / 100; - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); - assertEq( - priceInt, - expected, - "Price should match expected for very negative expo second feed" - ); - } - - // given the second feed publish time is >= the threshold boundary - // [X] it returns the correct price - function test_getTwoFeedPriceMul_givenSecondFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { - // Bound the publish time to a reasonable value - publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); - // First feed keeps publish time as set in setUp() - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE, - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); - - uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); - assertEq( - priceInt, - expected, - "Multiplied price should match expected when second feed at threshold" - ); - } - - // given outputDecimals is different from default (18) for two feeds multiplication - // [X] it correctly converts prices to the specified output decimals - function test_getTwoFeedPriceMul_outputDecimalsFuzz(uint8 outputDecimals_) public { - // Bound output decimals to reasonable range [18, 36] to avoid overflow - outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); - pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); - - bytes memory params = encodeTwoFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18, // scale max confidence to the new output decimals - address(pyth), - PRICE_ID_3, - UPDATE_THRESHOLD, - (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals - ); - uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals_, params); - - // First: price = 123456789, expo = -8 -> converts to 123456789 * 10^(outputDecimals_ - 8) - uint256 firstPrice = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); - // Second: price = 500000000, expo = -18 -> converts to 500000000 * 10^(outputDecimals_ - 18) - uint256 secondPrice = uint256(uint64(PRICE_3)) * 10 ** (outputDecimals_ - 18); - uint256 expected = firstPrice.mulDiv(secondPrice, 10 ** outputDecimals_); - assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); - } - - // ========= IERC165 FUNCTIONS ========= // - - // given supportsInterface is called with IERC165 interface ID - // [X] it returns true - function test_supportsInterface_IERC165_returnsTrue() public view { - bytes4 interfaceId = type(IERC165).interfaceId; - bool supported = pythSubmodule.supportsInterface(interfaceId); - assertTrue(supported, "Should support IERC165 interface"); - } - - // given supportsInterface is called with IVersioned interface ID - // [X] it returns true - function test_supportsInterface_IVersioned_returnsTrue() public view { - bytes4 interfaceId = type(IVersioned).interfaceId; - bool supported = pythSubmodule.supportsInterface(interfaceId); - assertTrue(supported, "Should support IVersioned interface"); - } - - // given supportsInterface is called with an unsupported interface ID - // [X] it returns false - function test_supportsInterface_unsupported_returnsFalse() public view { - bytes4 interfaceId = 0x12345678; - bool supported = pythSubmodule.supportsInterface(interfaceId); - assertFalse(supported, "Should not support unsupported interface"); - } -} -/// forge-lint: disable-end(mixed-case-variable,mixed-case-function) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/PythPriceFeedsTest.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/PythPriceFeedsTest.sol new file mode 100644 index 000000000..bcd74d775 --- /dev/null +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/PythPriceFeedsTest.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Unlicense +/// forge-lint: disable-start(mixed-case-variable,mixed-case-function) +pragma solidity >=0.8.0; + +// Test +import {Test} from "@forge-std-1.9.6/Test.sol"; +import {ModuleTestFixtureGenerator} from "src/test/lib/ModuleTestFixtureGenerator.sol"; + +// Mocks +import {MockPrice} from "src/test/mocks/MockPrice.v2.sol"; +import {MockPyth} from "src/test/mocks/MockPyth.sol"; + +// Libraries +import {FullMath} from "src/libraries/FullMath.sol"; + +// Bophades +import {Kernel} from "src/Kernel.sol"; +import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; + +contract PythPriceFeedsTest is Test { + using FullMath for uint256; + using ModuleTestFixtureGenerator for PythPriceFeeds; + + MockPyth internal pyth; + MockPyth internal secondPyth; + + Kernel internal kernel; + MockPrice internal mockPrice; + PythPriceFeeds internal pythSubmodule; + + bytes32 internal constant PRICE_ID_1 = keccak256("PRICE_ID_1"); + bytes32 internal constant PRICE_ID_2 = keccak256("PRICE_ID_2"); + bytes32 internal constant PRICE_ID_3 = keccak256("PRICE_ID_3"); + + // Example: price=123456789, expo=-8 represents 1.23456789 + int64 internal constant PRICE_1 = 123456789; + int32 internal constant EXPO_1 = -8; + uint64 internal constant CONF_1 = 1000000; + uint256 internal constant EXPECTED_PRICE_1_18_DEC = 1234567890000000000; // 1.23456789 * 10^18 + + // Example: price=100000000, expo=8 represents 100000000 * 10^8 + int64 internal constant PRICE_2 = 100000000; + int32 internal constant EXPO_2 = 8; + uint64 internal constant CONF_2 = 2000000; + + // Example: price=500000000, expo=-18 represents 0.0000000005 + int64 internal constant PRICE_3 = 500000000; + int32 internal constant EXPO_3 = -18; + uint64 internal constant CONF_3 = 500000; + + uint8 internal constant PRICE_DECIMALS = 18; + uint48 internal constant UPDATE_THRESHOLD = 100; + // MAX_CONFIDENCE is in output decimals (18 decimals) + // CONF_1 = 1000000 with expo=-8 + // In decimal: 1000000 * 10^-8 = 0.01 + // In output decimals (18): 0.01 * 10^18 = 10000000000000000 (1e16) + // Set MAX_CONFIDENCE to 2e16 to allow CONF_1 but reject higher values + uint64 internal constant MAX_CONFIDENCE = 20000000000000000; // 2 * 10^16 in 18 decimals + + function setUp() public virtual { + vm.warp(51 * 365 * 24 * 60 * 60); // Set timestamp at roughly Jan 1, 2021 + + // Set up the Pyth submodule + { + // Deploy kernel + kernel = new Kernel(); + + // Deploy mockPrice + mockPrice = new MockPrice(kernel, uint8(18), uint32(8 hours)); + mockPrice.setTimestamp(uint48(block.timestamp)); + mockPrice.setPriceDecimals(PRICE_DECIMALS); + + // Deploy Pyth submodule + pythSubmodule = new PythPriceFeeds(mockPrice); + } + + // Set up the Pyth contracts + { + pyth = new MockPyth(); + secondPyth = new MockPyth(); + + // With the default update threshold, these prices will NOT be stale + + // Set up price feed 1: 1.23456789 (expo=-8) + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp - UPDATE_THRESHOLD); + + // Set up price feed 2: 100000000 * 10^8 (expo=8) + pyth.setPrice(PRICE_ID_2, PRICE_2, CONF_2, EXPO_2, block.timestamp - UPDATE_THRESHOLD); + + // Set up price feed 3: 0.0000000005 (expo=-18) + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp - UPDATE_THRESHOLD); + } + } + + // ========= HELPER METHODS ========= // + + function encodeOneFeedParams( + address pyth_, + bytes32 priceFeedId_, + uint48 updateThreshold_, + uint256 maxConfidence_ + ) internal pure returns (bytes memory params) { + return abi.encode(pyth_, priceFeedId_, updateThreshold_, maxConfidence_); + } + + function encodeTwoFeedParams( + address firstPyth_, + bytes32 firstPriceFeedId_, + uint48 firstUpdateThreshold_, + uint256 firstMaxConfidence_, + address secondPyth_, + bytes32 secondPriceFeedId_, + uint48 secondUpdateThreshold_, + uint256 secondMaxConfidence_ + ) internal pure returns (bytes memory params) { + return + abi.encode( + firstPyth_, + firstPriceFeedId_, + firstUpdateThreshold_, + firstMaxConfidence_, + secondPyth_, + secondPriceFeedId_, + secondUpdateThreshold_, + secondMaxConfidence_ + ); + } +} +/// forge-lint: disable-end(mixed-case-variable,mixed-case-function) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol new file mode 100644 index 000000000..36702bb08 --- /dev/null +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol @@ -0,0 +1,615 @@ +// SPDX-License-Identifier: Unlicense +/// forge-lint: disable-start(mixed-case-variable,mixed-case-function) +pragma solidity >=0.8.0; + +// Test +import {PythPriceFeedsTest} from "./PythPriceFeedsTest.sol"; +import {MockPyth} from "src/test/mocks/MockPyth.sol"; + +// Interfaces +import {IPyth} from "src/interfaces/IPyth.sol"; + +// Bophades +import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; + +contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { + // ========= ONE FEED TESTS ========= // + + // given all parameters are valid + // [X] it returns the correct price in output decimals + function test_getOneFeedPrice_success() public view { + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should match expected value for expo=-8" + ); + } + + // given the pyth contract address is zero + // [X] it reverts with Pyth_ParamsPythInvalid + function test_getOneFeedPrice_pythZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, + 0, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(0), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getOneFeedPrice_priceFeedIdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, + 1, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getOneFeedPrice_updateThresholdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 2, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams(address(pyth), PRICE_ID_1, 0, MAX_CONFIDENCE); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getOneFeedPrice_maxConfidenceZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 3, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams(address(pyth), PRICE_ID_1, UPDATE_THRESHOLD, 0); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the pyth contract doesn't implement IPyth + // [X] it reverts with Pyth_FeedInvalid + function test_getOneFeedPrice_pythContractInvalid_reverts() public { + // Use a contract that doesn't implement IPyth + address invalidPyth = address(0x1234); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedInvalid.selector, + invalidPyth, + PRICE_ID_1 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + invalidPyth, + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the pyth contract returns data with incorrect length + // [X] it reverts with Pyth_FeedInvalid + function test_getOneFeedPrice_returnDataLengthInvalid_reverts() public { + address mockPyth = address(0xDEAD); + + // Mock the call to succeed but return data with wrong length (not 128 bytes) + // The _PRICE_DATA_SIZE is 128 bytes, so we return something smaller + bytes memory wrongLengthData = abi.encode(uint256(1), uint256(2)); // Only 64 bytes + + vm.mockCall( + mockPyth, + abi.encodeWithSelector( + IPyth.getPriceNoOlderThan.selector, + PRICE_ID_1, + uint256(UPDATE_THRESHOLD) + ), + wrongLengthData + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedInvalid.selector, + mockPyth, + PRICE_ID_1 + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + mockPyth, + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + vm.clearMockedCalls(); + } + + // given the pyth contract reverts with return data + // [X] it bubbles up the revert + function test_getOneFeedPrice_pythRevertsWithData_bubblesUpRevert() public { + address mockPyth = address(0xBEEF); + + // Create a custom error + bytes memory customError = abi.encodeWithSignature( + "CustomError(string)", + "Test revert message" + ); + + // Mock the call to revert with data + vm.mockCallRevert( + mockPyth, + abi.encodeWithSelector( + IPyth.getPriceNoOlderThan.selector, + PRICE_ID_1, + uint256(UPDATE_THRESHOLD) + ), + customError + ); + + // Expect the revert to bubble up + vm.expectRevert(customError); + + bytes memory params = encodeOneFeedParams( + mockPyth, + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the price is <= 0 + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getOneFeedPrice_feedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_1, + price_ + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getOneFeedPrice_priceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given the publish time is before the update threshold + // [X] it reverts with StalePrice + function test_getOneFeedPrice_givenStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_maxConfidenceExceeded_reverts(uint64 priceConfidence_) public { + // MAX_CONFIDENCE = 2e16 in output decimals (18) + // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals, so it should pass + // To exceed MAX_CONFIDENCE (2e16), we need conf * 10^10 > 2e16, so conf > 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + + // Set the price data + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + // Calculate the expected maxConfidence in Pyth scale + // EXPO_1 = -8, PRICE_DECIMALS = 18 + // maxConfidence = 2e16 * 1 / 10^*(-8+18) = 2e16 * 1 / 10^10 = 2e6 + uint64 maxConfInPythScale = 2e6; + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getOneFeedPrice_givenExpoNegative() public view { + // expo = -8, outputDecimals = 18, totalExponent = 10 + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // price = 123456789, expo = -8, outputDecimals = 18 + // outputPrice = 123456789 * 10^(10) = 1234567890000000000 + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should match expected value for negative expo" + ); + } + + // given expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getOneFeedPrice_expoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, expo_, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_1, + expo_ + ); + vm.expectRevert(err); + + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // given expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_expoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint256 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + maxConfidence + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getOneFeedPrice_expoZero() public { + // expo = 0, outputDecimals = 18, totalExponent = 18 + int64 price = 123456789; + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + type(uint64).max // Ensures confidence interval is below the maximum + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // outputPrice = 123456789 * 10^18 + assertEq(priceInt, 123456789 * 10 ** 18, "Price should match expected value for zero expo"); + } + + // given expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // [X] it returns the price without scaling + function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals() public { + // expo = -18, outputDecimals = 18, totalExponent = 0 + int64 price = 123456789; + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // outputPrice = 123456789 (no scaling needed) + assertEq( + priceInt, + /// forge-lint: disable-next-line(unsafe-typecast) + uint256(uint64(price)), + "Price should match input value when expo equals negative outputDecimals" + ); + } + + // given expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getOneFeedPrice_expoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getOneFeedPrice_expoVeryNegative() public { + // expo = -20, outputDecimals = 18, totalExponent = -2 + int64 price = 100000000; + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + // outputPrice = 100000000 / 10^2 = 1000000 + assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); + } + + // given the confidence interval equals the maximum + // [X] it returns the correct price + function test_getOneFeedPrice_confidenceEqualsMaximum() public { + // Confidence interval equals maximum threshold + // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals + // MAX_CONFIDENCE = 2e16, so CONF_1 should pass + // This test verifies that a confidence that is below the maximum passes + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when confidence is below maximum" + ); + } + + // given the confidence interval is below the maximum + // [X] it correctly converts the price + function test_getOneFeedPrice_confidenceBelowMaximum(uint64 priceConfidence_) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence below maximum threshold (1 <= confidence <= 2e6) + priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when confidence is below maximum" + ); + } + + // given the publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getOneFeedPrice_givenPublishTimeAboveThreshold(uint48 publishTime_) public { + // Bound the publish time to a reasonable value + publishTime_ = uint48( + bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp) + ); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when publish time is above threshold" + ); + } + + // given the publish time equals exactly the update threshold boundary + // [X] it returns the correct price + function test_getOneFeedPrice_publishTimeExactlyAtThreshold() public { + // Set publish time to exactly block.timestamp - UPDATE_THRESHOLD + // This tests the boundary condition: publishTime == blockTimestamp - updateThreshold + // The validation checks: publishTime < blockTimestamp - updateThreshold (strict <) + // So when publishTime == blockTimestamp - updateThreshold, it should pass + uint256 publishTime = block.timestamp - UPDATE_THRESHOLD; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should be valid when publish time exactly equals threshold boundary" + ); + } + + // given outputDecimals is different from default (18) + // [X] it correctly converts prices to the specified output decimals + function test_getOneFeedPrice_outputDecimalsFuzz(uint8 outputDecimals_) public { + // Bound output decimals to reasonable range [8, 36] to avoid rounding issues and overflow + // Using >= 8 ensures we don't lose precision (expo = -8) + outputDecimals_ = uint8(bound(outputDecimals_, 8, 36)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), outputDecimals_, params); + + // expo = -8, price = 123456789, outputDecimals = outputDecimals_ + // Conversion formula: price * 10^outputDecimals / 10^(-expo) = price * 10^outputDecimals / 10^8 + // = 123456789 * 10^(outputDecimals_ - 8) + /// forge-lint: disable-next-line(unsafe-typecast) + uint256 expected = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); + assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); + } +} +/// forge-lint: disable-end(mixed-case-variable,mixed-case-function) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol new file mode 100644 index 000000000..b0a3c3913 --- /dev/null +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol @@ -0,0 +1,1179 @@ +// SPDX-License-Identifier: Unlicense +/// forge-lint: disable-start(mixed-case-variable,mixed-case-function) +pragma solidity >=0.8.0; + +// Test +import {PythPriceFeedsTest} from "./PythPriceFeedsTest.sol"; +import {MockPyth} from "src/test/mocks/MockPyth.sol"; + +// Libraries +import {FullMath} from "src/libraries/FullMath.sol"; + +// Bophades +import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; + +contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { + using FullMath for uint256; + + // ========= TWO FEED TESTS - DIV ========= // + + // given all parameters are valid for two feeds + // [X] it returns the correct divided price (first / second) in output decimals + function test_getTwoFeedPriceDiv_success() public view { + // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals + // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals + // Result: 1234567890000000000 / 500000000 = 2469135780 + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // (1234567890000000000 * 10^18) / 500000000 = 2469135780000000000000000000 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq(priceInt, expected, "Divided price should match expected calculation"); + } + + // given the second feed denominator price converts to zero + // [X] it returns zero without reverting + function test_getTwoFeedPriceDiv_denominatorPriceZero() public { + // Set up a very small price with a very negative expo that will round down to 0 + int64 smallPrice = 1; + int32 veryNegativeExpo = -19; + pyth.setPrice(PRICE_ID_3, smallPrice, CONF_3, veryNegativeExpo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // Should return 0 when denominator price converts to 0 + assertEq(priceInt, 0, "Should return 0 when denominator price converts to 0"); + } + + // given the first pyth contract address is zero + // [X] it reverts with Pyth_ParamsPythInvalid + function test_getTwoFeedPriceDiv_firstPythZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, + 0, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(0), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getTwoFeedPriceDiv_firstPriceFeedIdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, + 1, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceDiv_firstUpdateThresholdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 2, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + 0, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceDiv_firstMaxConfidenceZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 3, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + 0, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second pyth contract address is zero + // [X] it reverts with Pyth_ParamsPythInvalid + function test_getTwoFeedPriceDiv_secondPythZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, + 4, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(0), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getTwoFeedPriceDiv_secondPriceFeedIdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, + 5, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceDiv_secondUpdateThresholdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 6, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + 0, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceDiv_secondMaxConfidenceZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 7, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + 0 + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first feed has invalid price (<= 0) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceDiv_firstFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_1, + price_ + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second feed has invalid price (<= 0) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceDiv_secondFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_3, + price_ + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceDiv_givenFirstFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceDiv_givenSecondFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceDiv_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts( + uint256 publishTime_ + ) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative() public view { + // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq(priceInt, expected, "Price should match expected for negative expo first feed"); + } + + // given the confidence interval is at the maximum (boundary case) + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceEqualsMaximum() public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence exactly at the maximum (2e6) + uint64 priceConfidence_ = 2e6; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected when confidence equals maximum for negative expo first feed" + ); + } + + // given the confidence interval is below the maximum + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceBelowMaximum( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence below the maximum (1 <= confidence <= 2e6) + priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected when confidence is below maximum for negative expo first feed" + ); + } + + // given the first feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); + int64 price = 100; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_1, + expo_ + ); + vm.expectRevert(err); + + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the first feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint256 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + maxConfidence, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero() public { + // First feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + type(uint64).max, // Ensures confidence interval is below the maximum + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 100 * 10^18, Second: 500000000 (18 dec) + // Result: (100 * 10^18 * 10^18) / 500000000 = 100 * 10^36 / 5e8 = 2 * 10^29 + uint256 firstPrice = uint256(100) * 10 ** 18; + uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq(priceInt, expected, "Price should match expected for zero expo first feed"); + } + + // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { + // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1e9, Second: 500000000 (18 dec) + // Result: (1e9 * 10^18) / 500000000 = 1e27 / 5e8 = 2e18 + uint256 firstPrice = uint256(1e9); + uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected when first feed expo equals negative outputDecimals" + ); + } + + // given the first feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative() public { + // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1e10 / 100 = 1e8, Second: 500000000 (18 dec) + // Result: (1e8 * 10^18) / 500000000 = 1e26 / 5e8 = 2e17 + uint256 firstPrice = uint256(1e10) / 100; + uint256 expected = firstPrice.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo first feed" + ); + } + + // given the first feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + // Second feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Divided price should match expected when first feed at threshold" + ); + } + + // given the second feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative() public { + // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1234567890000000000 + // Result: (1234567890000000000 * 10^18) / 1234567890000000000 = 10^18 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + 10 ** PRICE_DECIMALS, + EXPECTED_PRICE_1_18_DEC + ); + assertEq(priceInt, expected, "Price should match expected for negative expo second feed"); + } + + // given the confidence interval is at the maximum (boundary case) + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceEqualsMaximum() public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence exactly at the maximum (2e6) + uint64 priceConfidence_ = 2e6; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + 10 ** PRICE_DECIMALS, + (500000000 * 10 ** PRICE_DECIMALS) / 1e8 + ); + assertEq( + priceInt, + expected, + "Price should match expected when confidence equals maximum for negative expo second feed" + ); + } + + // given the confidence interval is below the maximum + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceBelowMaximum( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence below the maximum (1 <= confidence <= 2e6) + priceConfidence_ = uint64(bound(priceConfidence_, 1, 2e6)); + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + 10 ** PRICE_DECIMALS, + (500000000 * 10 ** PRICE_DECIMALS) / 1e8 + ); + assertEq( + priceInt, + expected, + "Price should match expected when confidence is below maximum for negative expo second feed" + ); + } + + // given the second feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); + int64 price = 100; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_3, + expo_ + ); + vm.expectRevert(err); + + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // given the second feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint256 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + maxConfidence + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero() public { + // Second feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, price, 1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + type(uint64).max // Ensures confidence interval is below the maximum + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 100 * 10^18 + // Result: (1234567890000000000 * 10^18) / (100 * 10^18) = 12345678900000000 + uint256 secondPrice = uint256(100) * 10 ** 18; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); + assertEq(priceInt, expected, "Price should match expected for zero expo second feed"); + } + + // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { + // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e9 + // Result: (1234567890000000000 * 10^18) / 1e9 = 1234567890000000000000000000 + uint256 secondPrice = uint256(1e9); + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); + assertEq( + priceInt, + expected, + "Price should match expected when second feed expo equals negative outputDecimals" + ); + } + + // given the second feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative() public { + // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e8 + // Result: (1234567890000000000 * 10^18) / 1e8 + uint256 secondPrice = uint256(1e10) / 100; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, secondPrice); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo second feed" + ); + } + + // given the second feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + // First feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(10 ** PRICE_DECIMALS, 500000000); + assertEq( + priceInt, + expected, + "Divided price should match expected when second feed at threshold" + ); + } + + // given outputDecimals is different from default (18) for two feeds division + // [X] it correctly converts prices to the specified output decimals + function test_getTwoFeedPriceDiv_outputDecimalsFuzz(uint8 outputDecimals_) public { + // Bound output decimals to reasonable range [18, 36] to avoid overflow + outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals_, params); + + // First: price = 123456789, expo = -8 -> converts to 123456789 * 10^(outputDecimals_ - 8) + /// forge-lint: disable-next-line(unsafe-typecast) + uint256 firstPrice = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); + // Second: price = 500000000, expo = -18 -> converts to 500000000 * 10^(outputDecimals_ - 18) + /// forge-lint: disable-next-line(unsafe-typecast) + uint256 secondPrice = uint256(uint64(PRICE_3)) * 10 ** (outputDecimals_ - 18); + uint256 expected = firstPrice.mulDiv(10 ** outputDecimals_, secondPrice); + assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); + } +} +/// forge-lint: disable-end(mixed-case-variable,mixed-case-function) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol new file mode 100644 index 000000000..0a153a430 --- /dev/null +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol @@ -0,0 +1,1029 @@ +// SPDX-License-Identifier: Unlicense +/// forge-lint: disable-start(mixed-case-variable,mixed-case-function) +pragma solidity >=0.8.0; + +// Test +import {PythPriceFeedsTest} from "./PythPriceFeedsTest.sol"; +import {MockPyth} from "src/test/mocks/MockPyth.sol"; + +// Libraries +import {FullMath} from "src/libraries/FullMath.sol"; + +// Bophades +import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol"; + +contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { + using FullMath for uint256; + + // ========= TWO FEED TESTS - MUL ========= // + + // given all parameters are valid for two feeds + // [X] it returns the correct multiplied price (first * second) in output decimals + function test_getTwoFeedPriceMul_success() public view { + // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals + // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals + // Result: (1234567890000000000 * 500000000) / 10^18 + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // (1234567890000000000 * 500000000) / 10^18 = 617283945000 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + uint256(500000000), + 10 ** uint256(PRICE_DECIMALS) + ); + assertEq(priceInt, expected, "Multiplied price should match expected calculation"); + } + + // given the first pyth contract address is zero + // [X] it reverts with Pyth_ParamsPythInvalid + function test_getTwoFeedPriceMul_firstPythZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, + 0, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(0), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second pyth contract address is zero + // [X] it reverts with Pyth_ParamsPythInvalid + function test_getTwoFeedPriceMul_secondPythZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPythInvalid.selector, + 4, + address(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(0), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getTwoFeedPriceMul_firstPriceFeedIdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, + 1, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed ID is zero + // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid + function test_getTwoFeedPriceMul_secondPriceFeedIdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, + 5, + bytes32(0) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + bytes32(0), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceMul_firstUpdateThresholdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 2, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + 0, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second update threshold is zero + // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid + function test_getTwoFeedPriceMul_secondUpdateThresholdZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, + 6, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + 0, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceMul_firstMaxConfidenceZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 3, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + 0, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second max confidence is zero + // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid + function test_getTwoFeedPriceMul_secondMaxConfidenceZero_reverts() public { + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, + 7, + 0 + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + 0 + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed has invalid price (<= 0) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceMul_firstFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_1, + price_ + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second feed has invalid price (<= 0) + // [X] it reverts with Pyth_FeedPriceInvalid + function test_getTwoFeedPriceMul_secondFeedPriceInvalid_reverts(int64 price_) public { + price_ = int64(bound(price_, type(int64).min, 0)); + pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedPriceInvalid.selector, + address(pyth), + PRICE_ID_3, + price_ + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceMul_givenFirstFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second price feed is not found + // [X] it reverts with PriceFeedNotFound + function test_getTwoFeedPriceMul_givenSecondFeedPriceFeedNotFound_reverts() public { + bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000001), + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second feed publish time is < the threshold boundary + // [X] it reverts with StalePrice + function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( + uint256 publishTime_ + ) public { + // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) + publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + + bytes memory err = abi.encodeWithSelector(MockPyth.StalePrice.selector); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative() public view { + // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // (1234567890000000000 * 500000000) / 10^18 = 617283945000 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Price should match expected for negative expo first feed"); + } + + // given the first feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceMul_givenFirstFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); + int64 price = 100; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo_, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_1, + expo_ + ); + vm.expectRevert(err); + + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the first feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint256 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + maxConfidence, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenFirstFeedExpoZero() public { + // First feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_1, price, 1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + type(uint64).max, // Ensures confidence interval is below the maximum + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 100 * 10^18, Second: 500000000 (18 dec) + // Result: (100 * 10^18 * 500000000) / 10^18 = 5 * 10^10 + uint256 firstPrice = uint256(100) * 10 ** 18; + uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Price should match expected for zero expo first feed"); + } + + // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { + // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1e9, Second: 500000000 (18 dec) + // Result: (1e9 * 500000000) / 10^18 = 5e17 / 1e18 = 0 (rounds down) + uint256 firstPrice = uint256(1e9); + uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected when first feed expo equals negative outputDecimals" + ); + } + + // given the first feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_1, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative() public { + // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_1, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1e10 / 100 = 1e8, Second: 500000000 (18 dec) + // Result: (1e8 * 500000000) / 10^18 = 5e16 / 1e18 = 0 (rounds down) + uint256 firstPrice = uint256(1e10) / 100; + uint256 expected = firstPrice.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo first feed" + ); + } + + // given the first feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceMul_givenFirstFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); + // Second feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Multiplied price should match expected when first feed at threshold" + ); + } + + // given the second feed expo is negative (expo = -8, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + priceConfidence_ = uint64(bound(priceConfidence_, 2e6 + 1, type(uint64).max)); + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e6) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative() public { + // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 + pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1234567890000000000 + // Result: (1234567890000000000 * 1234567890000000000) / 10^18 + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv( + EXPECTED_PRICE_1_18_DEC, + 10 ** PRICE_DECIMALS + ); + assertEq(priceInt, expected, "Price should match expected for negative expo second feed"); + } + + // given the second feed expo is positive (expo > 0) + // [X] it reverts with Pyth_ExponentPositive + function test_getTwoFeedPriceMul_givenSecondFeedExpoPositive_reverts(int32 expo_) public { + // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error + // Keep expo within reasonable range [1, 32] to avoid overflow in calculations + expo_ = int32(bound(int256(expo_), 1, 32)); + int64 price = 100; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo_, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_ExponentPositive.selector, + address(pyth), + PRICE_ID_3, + expo_ + ); + vm.expectRevert(err); + + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // given the second feed expo is zero (expo = 0, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = 0, outputDecimals = 18 + // confidenceExponent = 18 + 0 = 18 + // Use a higher maxConfidence to get a meaningful threshold + // maxConfidence = 1e19 -> maxConfidenceInPythScale = 1e19 / 1e18 = 10 + uint256 maxConfidence = 1e19; + uint64 maxConfInPythScale = 10; + priceConfidence_ = uint64( + bound(priceConfidence_, maxConfInPythScale + 1, type(uint64).max) + ); + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + maxConfInPythScale + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + maxConfidence + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenSecondFeedExpoZero() public { + // Second feed: expo = 0, price = 100 -> 100 * 10^18 + int64 price = 100; + int32 expo = 0; + pyth.setPrice(PRICE_ID_3, price, 1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + type(uint64).max // Ensures confidence interval is below the maximum + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 100 * 10^18 + // Result: (1234567890000000000 * 100 * 10^18) / 10^18 = 1234567890000000000 * 100 + uint256 secondPrice = uint256(100) * 10 ** 18; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); + assertEq(priceInt, expected, "Price should match expected for zero expo second feed"); + } + + // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -18, outputDecimals = 18 + // confidenceExponent = 18 + (-18) = 0 + // maxConfidenceInPythScale = maxConfidence / 10^0 = 2e16 + priceConfidence_ = uint64(bound(priceConfidence_, 2e16 + 1, type(uint64).max)); + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e16) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price + function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { + // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) + int64 price = 1e9; + int32 expo = -18; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e9 + // Result: (1234567890000000000 * 1e9) / 10^18 = 1234567890 + uint256 secondPrice = uint256(1e9); + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected when second feed expo equals negative outputDecimals" + ); + } + + // given the second feed expo is very negative (expo = -20, outputDecimals = 18) + // given the confidence interval is above the maximum + // [X] it reverts with Pyth_FeedConfidenceExcessive + function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + uint64 priceConfidence_ + ) public { + // expo = -20, outputDecimals = 18 + // confidenceExponent = 18 + (-20) = -2 + // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 + priceConfidence_ = uint64(bound(priceConfidence_, 2e18 + 1, type(uint64).max)); + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, PRICE_3, priceConfidence_, expo, block.timestamp); + + bytes memory err = abi.encodeWithSelector( + PythPriceFeeds.Pyth_FeedConfidenceExcessive.selector, + address(pyth), + PRICE_ID_3, + priceConfidence_, + uint64(2e18) + ); + vm.expectRevert(err); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + } + + // [X] it correctly converts the price by dividing + function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative() public { + // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 + int64 price = 1e10; + int32 expo = -20; + pyth.setPrice(PRICE_ID_3, price, CONF_1, expo, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + // First: 1234567890000000000 (18 dec), Second: 1e8 + // Result: (1234567890000000000 * 1e8) / 10^18 = 123456789 + uint256 secondPrice = uint256(1e10) / 100; + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(secondPrice, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Price should match expected for very negative expo second feed" + ); + } + + // given the second feed publish time is >= the threshold boundary + // [X] it returns the correct price + function test_getTwoFeedPriceMul_givenSecondFeedPublishTimeAboveThreshold( + uint256 publishTime_ + ) public { + // Bound the publish time to a reasonable value + publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); + // First feed keeps publish time as set in setUp() + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE, + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), PRICE_DECIMALS, params); + + uint256 expected = EXPECTED_PRICE_1_18_DEC.mulDiv(500000000, 10 ** PRICE_DECIMALS); + assertEq( + priceInt, + expected, + "Multiplied price should match expected when second feed at threshold" + ); + } + + // given outputDecimals is different from default (18) for two feeds multiplication + // [X] it correctly converts prices to the specified output decimals + function test_getTwoFeedPriceMul_outputDecimalsFuzz(uint8 outputDecimals_) public { + // Bound output decimals to reasonable range [18, 36] to avoid overflow + outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals_) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals_, params); + + // First: price = 123456789, expo = -8 -> converts to 123456789 * 10^(outputDecimals_ - 8) + /// forge-lint: disable-next-line(unsafe-typecast) + uint256 firstPrice = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); + // Second: price = 500000000, expo = -18 -> converts to 500000000 * 10^(outputDecimals_ - 18) + /// forge-lint: disable-next-line(unsafe-typecast) + uint256 secondPrice = uint256(uint64(PRICE_3)) * 10 ** (outputDecimals_ - 18); + uint256 expected = firstPrice.mulDiv(secondPrice, 10 ** outputDecimals_); + assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); + } +} +/// forge-lint: disable-end(mixed-case-variable,mixed-case-function) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/supportsInterface.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/supportsInterface.t.sol new file mode 100644 index 000000000..de8f2b86a --- /dev/null +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/supportsInterface.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Unlicense +/// forge-lint: disable-start(mixed-case-variable,mixed-case-function) +pragma solidity >=0.8.0; + +// Test +import {PythPriceFeedsTest} from "./PythPriceFeedsTest.sol"; + +// Interfaces +import {IERC165} from "@openzeppelin-4.8.0/interfaces/IERC165.sol"; +import {IVersioned} from "src/interfaces/IVersioned.sol"; + +contract PythPriceFeedsSupportsInterfaceTest is PythPriceFeedsTest { + // ========= IERC165 FUNCTIONS ========= // + + // given supportsInterface is called with IERC165 interface ID + // [X] it returns true + function test_supportsInterface_IERC165_returnsTrue() public view { + bytes4 interfaceId = type(IERC165).interfaceId; + bool supported = pythSubmodule.supportsInterface(interfaceId); + assertTrue(supported, "Should support IERC165 interface"); + } + + // given supportsInterface is called with IVersioned interface ID + // [X] it returns true + function test_supportsInterface_IVersioned_returnsTrue() public view { + bytes4 interfaceId = type(IVersioned).interfaceId; + bool supported = pythSubmodule.supportsInterface(interfaceId); + assertTrue(supported, "Should support IVersioned interface"); + } + + // given supportsInterface is called with an unsupported interface ID + // [X] it returns false + function test_supportsInterface_unsupported_returnsFalse() public view { + bytes4 interfaceId = 0x12345678; + bool supported = pythSubmodule.supportsInterface(interfaceId); + assertFalse(supported, "Should not support unsupported interface"); + } +} +/// forge-lint: disable-end(mixed-case-variable,mixed-case-function) From d4a77315cd976ac0efe38c7509a288c675a7dc02 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 9 Jan 2026 14:20:22 +0400 Subject: [PATCH 16/19] test: remove redundant test --- .../PythPriceFeeds/getOneFeedPrice.t.sol | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol index 36702bb08..4280e9367 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol @@ -490,31 +490,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); } - // given the confidence interval equals the maximum - // [X] it returns the correct price - function test_getOneFeedPrice_confidenceEqualsMaximum() public { - // Confidence interval equals maximum threshold - // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals - // MAX_CONFIDENCE = 2e16, so CONF_1 should pass - // This test verifies that a confidence that is below the maximum passes - pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); - - bytes memory params = encodeOneFeedParams( - address(pyth), - PRICE_ID_1, - UPDATE_THRESHOLD, - MAX_CONFIDENCE - ); - uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); - - assertEq( - priceInt, - EXPECTED_PRICE_1_18_DEC, - "Price should be valid when confidence is below maximum" - ); - } - - // given the confidence interval is below the maximum + // given the confidence interval is <= the maximum // [X] it correctly converts the price function test_getOneFeedPrice_confidenceBelowMaximum(uint64 priceConfidence_) public { // expo = -8, outputDecimals = 18 From 87bb158e07d8721f54515642c97cafa684a24bec Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 9 Jan 2026 14:20:46 +0400 Subject: [PATCH 17/19] test: add more comprehensive tests for different output decimal values --- .../PythPriceFeeds/getOneFeedPrice.t.sol | 66 ++++++ .../PythPriceFeeds/getTwoFeedPriceDiv.t.sol | 197 +++++++++++++++++- .../PythPriceFeeds/getTwoFeedPriceMul.t.sol | 197 +++++++++++++++++- 3 files changed, 458 insertions(+), 2 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol index 4280e9367..8efc75bc4 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol @@ -587,5 +587,71 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { uint256 expected = uint256(uint64(PRICE_1)) * 10 ** (outputDecimals_ - 8); assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); } + + // given outputDecimals is < the expo + // given the price is < 1 + // [X] the price loses precision + function test_getOneFeedPrice_outputDecimalsLessThanExpo_priceLessThanOne() public { + // Bound output decimals to 6, which is less than the expo (8) + uint8 outputDecimals = 6; + // Set the price to 23456789 (0.23456789 * 10^8) + pyth.setPrice(PRICE_ID_1, 23456789, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), outputDecimals, params); + + // expo = -8, price = 23456789 (0.23456789 * 10^8) + // outputDecimals = 6, price = 234567 (0.234567 * 10^6) + uint256 expected = 234567; + assertEq(priceInt, expected, "Price should lose precision"); + } + + // given the price rounds down to 0 + // [X] it returns zero + function test_getOneFeedPrice_outputDecimalsLessThanExpo_priceRoundsDownToZero() public { + // Bound output decimals to 6, which is less than the expo (8) + uint8 outputDecimals = 6; + // Set the price to 89 (0.00000089 * 10^8) + // This will round down to 0 when converted to output decimals + pyth.setPrice(PRICE_ID_1, 89, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), outputDecimals, params); + + // expo = -8, price = 89 (0.00000089 * 10^8) + // outputDecimals = 6, price = 0 (89 * 10^6 / 10^8 is less than 1) + uint256 expected = 0; + assertEq(priceInt, expected, "Price should round down to zero"); + } + + // [X] the price loses precision + function test_getOneFeedPrice_outputDecimalsLessThanExpo() public { + // Bound output decimals to 6, which is less than the expo (8) + uint8 outputDecimals = 6; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), outputDecimals, params); + + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 6, price = 1234567 (1.234567 * 10^6) + uint256 expected = 1234567; + assertEq(priceInt, expected, "Price should lose precision"); + } } /// forge-lint: disable-end(mixed-case-variable,mixed-case-function) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol index b0a3c3913..d1644e241 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol @@ -1146,7 +1146,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { ); } - // given outputDecimals is different from default (18) for two feeds division + // given outputDecimals is >= the default (18) // [X] it correctly converts prices to the specified output decimals function test_getTwoFeedPriceDiv_outputDecimalsFuzz(uint8 outputDecimals_) public { // Bound output decimals to reasonable range [18, 36] to avoid overflow @@ -1175,5 +1175,200 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { uint256 expected = firstPrice.mulDiv(10 ** outputDecimals_, secondPrice); assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); } + + // given outputDecimals is < the expo of price feed one + // given that price one is < 1 + // [X] the price loses precision + function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceOneExpo_priceLessThanOne() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed one + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + // The price is also < 1 in 18 decimal scale + pyth.setPrice(PRICE_ID_3, 122222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals, params); + + // First price: + // expo = -18, price = 122222222222222222 + // outputDecimals = 9, price = 122222222.222222222 + // Second price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Expected result: 122222222 * 10^9 / 1234567890 = 99000000 + uint256 expected = 99000000; + assertEq(priceInt, expected, "Price should lose precision"); + } + + // given that price one rounds down to 0 + // [X] it returns zero + function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceOneExpo_priceRoundsDownToZero() + public + { + // Bound output decimals to 9, which is less than the expo (18) of price feed one + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This will round down to 0 when converted to output decimals + pyth.setPrice(PRICE_ID_3, 122222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals, params); + + // First price: + // expo = -18, price = 122222222 + // outputDecimals = 9, price = 0 (122222222 * 10^9 / 10^18 is less than 1) + // Expected result: 0 + uint256 expected = 0; + assertEq(priceInt, expected, "Price should round down to zero"); + } + + // [X] the price loses precision + function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceOneExpo() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed one + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + pyth.setPrice(PRICE_ID_3, 1222222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals, params); + + // First price: + // expo = -18, price = 1222222222222222222 + // outputDecimals = 9, price = 1222222222.222222222 + // Second price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Expected result: 1222222222 * 10^9 / 1234567890 = 990000008 + uint256 expected = 990000008; + assertEq(priceInt, expected, "Price should lose precision"); + } + + // given outputDecimals is < the expo of price feed two + // given that price two is < 1 + // [X] the price loses precision + function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceTwoExpo_priceLessThanOne() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed two + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + // The price is also < 1 in 18 decimal scale + pyth.setPrice(PRICE_ID_3, 122222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals, params); + + // First price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Second price: + // expo = -18, price = 122222222222222222 + // outputDecimals = 9, price = 122222222.222222222 + // Expected result: 1234567890 * 10^9 / 122222222 = 10101010027 + uint256 expected = 10101010027; + assertEq(priceInt, expected, "Price should lose precision"); + } + + // given that price two rounds down to 0 + // [X] it returns zero + function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceTwoExpo_priceRoundsDownToZero() + public + { + // Bound output decimals to 9, which is less than the expo (18) of price feed two + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This will round down to 0 when converted to output decimals + pyth.setPrice(PRICE_ID_3, 122222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals, params); + + // First price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Second price: + // expo = -18, price = 122222222 + // outputDecimals = 9, price = 0 (122222222 * 10^9 / 10^18 is less than 1) + // Expected result: 0 + uint256 expected = 0; + assertEq(priceInt, expected, "Price should round down to zero"); + } + + // [X] the price loses precision + function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceTwoExpo() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed two + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + pyth.setPrice(PRICE_ID_3, 1222222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceDiv(address(0), outputDecimals, params); + + // First price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Second price: + // expo = -18, price = 1222222222222222222 + // outputDecimals = 9, price = 1222222222.222222222 + // Expected result: 1234567890 * 10^9 / 1222222222 = 1010101001 + uint256 expected = 1010101001; + assertEq(priceInt, expected, "Price should lose precision"); + } } /// forge-lint: disable-end(mixed-case-variable,mixed-case-function) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol index 0a153a430..a3dee8c99 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol @@ -996,7 +996,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { ); } - // given outputDecimals is different from default (18) for two feeds multiplication + // given outputDecimals is different from default (18) // [X] it correctly converts prices to the specified output decimals function test_getTwoFeedPriceMul_outputDecimalsFuzz(uint8 outputDecimals_) public { // Bound output decimals to reasonable range [18, 36] to avoid overflow @@ -1025,5 +1025,200 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { uint256 expected = firstPrice.mulDiv(secondPrice, 10 ** outputDecimals_); assertEq(priceInt, expected, "Price should match expected for fuzzed output decimals"); } + + // given outputDecimals is < the expo of price feed one + // given that price one is < 1 + // [X] the price loses precision + function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceOneExpo_priceLessThanOne() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed one + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + // The price is also < 1 in 18 decimal scale + pyth.setPrice(PRICE_ID_3, 122222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals, params); + + // First price: + // expo = -18, price = 122222222222222222 + // outputDecimals = 9, price = 122222222.222222222 + // Second price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Expected result: 122222222 * 1234567890 / 10^9 = 150891630 + uint256 expected = 150891630; + assertEq(priceInt, expected, "Price should lose precision"); + } + + // given that price one rounds down to 0 + // [X] it returns zero + function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceOneExpo_priceRoundsDownToZero() + public + { + // Bound output decimals to 9, which is less than the expo (18) of price feed one + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This will round down to 0 when converted to output decimals + pyth.setPrice(PRICE_ID_3, 122222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals, params); + + // First price: + // expo = -18, price = 122222222 + // outputDecimals = 9, price = 0 (122222222 * 10^9 / 10^18 is less than 1) + // Expected result: 0 + uint256 expected = 0; + assertEq(priceInt, expected, "Price should round down to zero"); + } + + // [X] the price loses precision + function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceOneExpo() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed one + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + pyth.setPrice(PRICE_ID_3, 1222222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals, params); + + // First price: + // expo = -18, price = 1222222222222222222 + // outputDecimals = 9, price = 1222222222.222222222 + // Second price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Expected result: 1222222222 * 1234567890 / 10^9 = 1508916309 + uint256 expected = 1508916309; + assertEq(priceInt, expected, "Price should lose precision"); + } + + // given outputDecimals is < the expo of price feed two + // given that price two is < 1 + // [X] the price loses precision + function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceTwoExpo_priceLessThanOne() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed two + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + // The price is also < 1 in 18 decimal scale + pyth.setPrice(PRICE_ID_3, 122222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals, params); + + // First price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Second price: + // expo = -18, price = 122222222222222222 + // outputDecimals = 9, price = 122222222.222222222 + // Expected result: 1234567890 * 122222222 / 10^9 = 150891630 + uint256 expected = 150891630; + assertEq(priceInt, expected, "Price should lose precision"); + } + + // given that price two rounds down to 0 + // [X] it returns zero + function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceTwoExpo_priceRoundsDownToZero() + public + { + // Bound output decimals to 9, which is less than the expo (18) of price feed two + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This will round down to 0 when converted to output decimals + pyth.setPrice(PRICE_ID_3, 122222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals, params); + + // First price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Second price: + // expo = -18, price = 122222222 + // outputDecimals = 9, price = 0 (122222222 * 10^9 / 10^18 is less than 1) + // Expected result: 0 + uint256 expected = 0; + assertEq(priceInt, expected, "Price should round down to zero"); + } + + // [X] the price loses precision + function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceTwoExpo() public { + // Bound output decimals to 9, which is less than the expo (18) of price feed two + uint8 outputDecimals = 9; + pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); + // This price will lose precision when converted to output decimals + pyth.setPrice(PRICE_ID_3, 1222222222222222222, CONF_3, EXPO_3, block.timestamp); + + bytes memory params = encodeTwoFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18, // scale max confidence to the new output decimals + address(pyth), + PRICE_ID_3, + UPDATE_THRESHOLD, + (MAX_CONFIDENCE * 10 ** outputDecimals) / 10 ** 18 // scale max confidence to the new output decimals + ); + uint256 priceInt = pythSubmodule.getTwoFeedPriceMul(address(0), outputDecimals, params); + + // First price: + // expo = -8, price = 123456789 (1.23456789 * 10^8) + // outputDecimals = 9, price = 1234567890 (1.23456789 * 10^9) + // Second price: + // expo = -18, price = 1222222222222222222 + // outputDecimals = 9, price = 1222222222.222222222 + // Expected result: 1234567890 * 1222222222 / 10^9 = 1508916309 + uint256 expected = 1508916309; + assertEq(priceInt, expected, "Price should lose precision"); + } } /// forge-lint: disable-end(mixed-case-variable,mixed-case-function) From 176fdb29c0b456b4790fb55e500e3f0419762095 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 9 Jan 2026 14:22:25 +0400 Subject: [PATCH 18/19] test: remove redundant prefix in function names --- .../PythPriceFeeds/getOneFeedPrice.t.sol | 58 +++++----- .../PythPriceFeeds/getTwoFeedPriceDiv.t.sol | 104 ++++++++---------- .../PythPriceFeeds/getTwoFeedPriceMul.t.sol | 94 +++++++--------- 3 files changed, 116 insertions(+), 140 deletions(-) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol index 8efc75bc4..1f8e9e3c9 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol @@ -17,7 +17,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given all parameters are valid // [X] it returns the correct price in output decimals - function test_getOneFeedPrice_success() public view { + function test_success() public view { bytes memory params = encodeOneFeedParams( address(pyth), PRICE_ID_1, @@ -35,7 +35,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getOneFeedPrice_pythZero_reverts() public { + function test_pythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, @@ -54,7 +54,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getOneFeedPrice_priceFeedIdZero_reverts() public { + function test_priceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 1, @@ -73,7 +73,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getOneFeedPrice_updateThresholdZero_reverts() public { + function test_updateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 2, @@ -87,7 +87,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getOneFeedPrice_maxConfidenceZero_reverts() public { + function test_maxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 3, @@ -101,7 +101,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the pyth contract doesn't implement IPyth // [X] it reverts with Pyth_FeedInvalid - function test_getOneFeedPrice_pythContractInvalid_reverts() public { + function test_pythContractInvalid_reverts() public { // Use a contract that doesn't implement IPyth address invalidPyth = address(0x1234); @@ -123,7 +123,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the pyth contract returns data with incorrect length // [X] it reverts with Pyth_FeedInvalid - function test_getOneFeedPrice_returnDataLengthInvalid_reverts() public { + function test_returnDataLengthInvalid_reverts() public { address mockPyth = address(0xDEAD); // Mock the call to succeed but return data with wrong length (not 128 bytes) @@ -160,7 +160,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the pyth contract reverts with return data // [X] it bubbles up the revert - function test_getOneFeedPrice_pythRevertsWithData_bubblesUpRevert() public { + function test_pythRevertsWithData_bubblesUpRevert() public { address mockPyth = address(0xBEEF); // Create a custom error @@ -194,7 +194,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the price is <= 0 // [X] it reverts with Pyth_FeedPriceInvalid - function test_getOneFeedPrice_feedPriceInvalid_reverts(int64 price_) public { + function test_feedPriceInvalid_reverts(int64 price_) public { price_ = int64(bound(price_, type(int64).min, 0)); pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); @@ -217,7 +217,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the price feed is not found // [X] it reverts with PriceFeedNotFound - function test_getOneFeedPrice_priceFeedNotFound_reverts() public { + function test_priceFeedNotFound_reverts() public { bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); vm.expectRevert(err); @@ -232,7 +232,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the publish time is before the update threshold // [X] it reverts with StalePrice - function test_getOneFeedPrice_givenStalePrice_reverts(uint256 publishTime_) public { + function test_givenStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); @@ -252,7 +252,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_maxConfidenceExceeded_reverts(uint64 priceConfidence_) public { + function test_maxConfidenceExceeded_reverts(uint64 priceConfidence_) public { // MAX_CONFIDENCE = 2e16 in output decimals (18) // CONF_1 = 1000000 with expo=-8 converts to 1e16 in output decimals, so it should pass // To exceed MAX_CONFIDENCE (2e16), we need conf * 10^10 > 2e16, so conf > 2e6 @@ -285,7 +285,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getOneFeedPrice_givenExpoNegative() public view { + function test_givenExpoNegative() public view { // expo = -8, outputDecimals = 18, totalExponent = 10 bytes memory params = encodeOneFeedParams( address(pyth), @@ -306,7 +306,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive - function test_getOneFeedPrice_expoPositive_reverts(int32 expo_) public { + function test_expoPositive_reverts(int32 expo_) public { // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error // Keep expo within reasonable range [1, 32] to avoid overflow in calculations expo_ = int32(bound(int256(expo_), 1, 32)); @@ -333,9 +333,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_expoZero_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { + function test_expoZero_maxConfidenceExceeded_reverts(uint64 priceConfidence_) public { // expo = 0, outputDecimals = 18 // confidenceExponent = 18 + 0 = 18 // Use a higher maxConfidence to get a meaningful threshold @@ -367,7 +365,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getOneFeedPrice_expoZero() public { + function test_expoZero() public { // expo = 0, outputDecimals = 18, totalExponent = 18 int64 price = 123456789; int32 expo = 0; @@ -388,7 +386,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given expo equals negative outputDecimals (expo = -18, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals_maxConfidenceExceeded_reverts( + function test_expoEqualsNegativeOutputDecimals_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -18, outputDecimals = 18 @@ -417,7 +415,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { } // [X] it returns the price without scaling - function test_getOneFeedPrice_expoEqualsNegativeOutputDecimals() public { + function test_expoEqualsNegativeOutputDecimals() public { // expo = -18, outputDecimals = 18, totalExponent = 0 int64 price = 123456789; int32 expo = -18; @@ -443,9 +441,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given expo is very negative (expo = -20, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getOneFeedPrice_expoVeryNegative_maxConfidenceExceeded_reverts( - uint64 priceConfidence_ - ) public { + function test_expoVeryNegative_maxConfidenceExceeded_reverts(uint64 priceConfidence_) public { // expo = -20, outputDecimals = 18 // confidenceExponent = 18 + (-20) = -2 // maxConfidenceInPythScale = maxConfidence * 10^2 = 2e16 * 100 = 2e18 @@ -472,7 +468,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { } // [X] it correctly converts the price by dividing - function test_getOneFeedPrice_expoVeryNegative() public { + function test_expoVeryNegative() public { // expo = -20, outputDecimals = 18, totalExponent = -2 int64 price = 100000000; int32 expo = -20; @@ -492,7 +488,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the confidence interval is <= the maximum // [X] it correctly converts the price - function test_getOneFeedPrice_confidenceBelowMaximum(uint64 priceConfidence_) public { + function test_confidenceBelowMaximum(uint64 priceConfidence_) public { // expo = -8, outputDecimals = 18 // confidenceExponent = 18 + (-8) = 10 // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 @@ -517,7 +513,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the publish time is >= the threshold boundary // [X] it returns the correct price - function test_getOneFeedPrice_givenPublishTimeAboveThreshold(uint48 publishTime_) public { + function test_givenPublishTimeAboveThreshold(uint48 publishTime_) public { // Bound the publish time to a reasonable value publishTime_ = uint48( bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp) @@ -541,7 +537,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the publish time equals exactly the update threshold boundary // [X] it returns the correct price - function test_getOneFeedPrice_publishTimeExactlyAtThreshold() public { + function test_publishTimeExactlyAtThreshold() public { // Set publish time to exactly block.timestamp - UPDATE_THRESHOLD // This tests the boundary condition: publishTime == blockTimestamp - updateThreshold // The validation checks: publishTime < blockTimestamp - updateThreshold (strict <) @@ -566,7 +562,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given outputDecimals is different from default (18) // [X] it correctly converts prices to the specified output decimals - function test_getOneFeedPrice_outputDecimalsFuzz(uint8 outputDecimals_) public { + function test_outputDecimalsFuzz(uint8 outputDecimals_) public { // Bound output decimals to reasonable range [8, 36] to avoid rounding issues and overflow // Using >= 8 ensures we don't lose precision (expo = -8) outputDecimals_ = uint8(bound(outputDecimals_, 8, 36)); @@ -591,7 +587,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given outputDecimals is < the expo // given the price is < 1 // [X] the price loses precision - function test_getOneFeedPrice_outputDecimalsLessThanExpo_priceLessThanOne() public { + function test_outputDecimalsLessThanExpo_priceLessThanOne() public { // Bound output decimals to 6, which is less than the expo (8) uint8 outputDecimals = 6; // Set the price to 23456789 (0.23456789 * 10^8) @@ -613,7 +609,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { // given the price rounds down to 0 // [X] it returns zero - function test_getOneFeedPrice_outputDecimalsLessThanExpo_priceRoundsDownToZero() public { + function test_outputDecimalsLessThanExpo_priceRoundsDownToZero() public { // Bound output decimals to 6, which is less than the expo (8) uint8 outputDecimals = 6; // Set the price to 89 (0.00000089 * 10^8) @@ -635,7 +631,7 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { } // [X] the price loses precision - function test_getOneFeedPrice_outputDecimalsLessThanExpo() public { + function test_outputDecimalsLessThanExpo() public { // Bound output decimals to 6, which is less than the expo (8) uint8 outputDecimals = 6; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol index d1644e241..ddd0d3c53 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceDiv.t.sol @@ -19,7 +19,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given all parameters are valid for two feeds // [X] it returns the correct divided price (first / second) in output decimals - function test_getTwoFeedPriceDiv_success() public view { + function test_success() public view { // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals // Result: 1234567890000000000 / 500000000 = 2469135780 @@ -42,7 +42,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed denominator price converts to zero // [X] it returns zero without reverting - function test_getTwoFeedPriceDiv_denominatorPriceZero() public { + function test_denominatorPriceZero() public { // Set up a very small price with a very negative expo that will round down to 0 int64 smallPrice = 1; int32 veryNegativeExpo = -19; @@ -66,7 +66,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceDiv_firstPythZero_reverts() public { + function test_firstPythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, @@ -89,7 +89,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceDiv_firstPriceFeedIdZero_reverts() public { + function test_firstPriceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 1, @@ -112,7 +112,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceDiv_firstUpdateThresholdZero_reverts() public { + function test_firstUpdateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 2, @@ -135,7 +135,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceDiv_firstMaxConfidenceZero_reverts() public { + function test_firstMaxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 3, @@ -158,7 +158,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceDiv_secondPythZero_reverts() public { + function test_secondPythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 4, @@ -181,7 +181,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceDiv_secondPriceFeedIdZero_reverts() public { + function test_secondPriceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 5, @@ -204,7 +204,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceDiv_secondUpdateThresholdZero_reverts() public { + function test_secondUpdateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 6, @@ -227,7 +227,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceDiv_secondMaxConfidenceZero_reverts() public { + function test_secondMaxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 7, @@ -250,7 +250,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceDiv_firstFeedPriceInvalid_reverts(int64 price_) public { + function test_firstFeedPriceInvalid_reverts(int64 price_) public { price_ = int64(bound(price_, type(int64).min, 0)); pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); @@ -277,7 +277,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceDiv_secondFeedPriceInvalid_reverts(int64 price_) public { + function test_secondFeedPriceInvalid_reverts(int64 price_) public { price_ = int64(bound(price_, type(int64).min, 0)); pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); @@ -304,7 +304,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first price feed is not found // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceDiv_givenFirstFeedPriceFeedNotFound_reverts() public { + function test_givenFirstFeedPriceFeedNotFound_reverts() public { bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); vm.expectRevert(err); @@ -323,7 +323,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second price feed is not found // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceDiv_givenSecondFeedPriceFeedNotFound_reverts() public { + function test_givenSecondFeedPriceFeedNotFound_reverts() public { bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); vm.expectRevert(err); @@ -342,7 +342,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceDiv_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { + function test_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); @@ -365,9 +365,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceDiv_givenSecondFeedStalePrice_reverts( - uint256 publishTime_ - ) public { + function test_givenSecondFeedStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); @@ -391,7 +389,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -8, outputDecimals = 18 @@ -423,7 +421,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative() public view { + function test_givenFirstFeedExpoNegative() public view { // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals bytes memory params = encodeTwoFeedParams( @@ -444,7 +442,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the confidence interval is at the maximum (boundary case) // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceEqualsMaximum() public { + function test_givenFirstFeedExpoNegative_confidenceEqualsMaximum() public { // expo = -8, outputDecimals = 18 // confidenceExponent = 18 + (-8) = 10 // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 @@ -474,7 +472,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the confidence interval is below the maximum // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegative_confidenceBelowMaximum( + function test_givenFirstFeedExpoNegative_confidenceBelowMaximum( uint64 priceConfidence_ ) public { // expo = -8, outputDecimals = 18 @@ -506,7 +504,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoPositive_reverts(int32 expo_) public { + function test_givenFirstFeedExpoPositive_reverts(int32 expo_) public { // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error // Keep expo within reasonable range [1, 32] to avoid overflow in calculations expo_ = int32(bound(int256(expo_), 1, 32)); @@ -538,7 +536,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = 0, outputDecimals = 18 @@ -576,7 +574,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoZero() public { + function test_givenFirstFeedExpoZero() public { // First feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; @@ -604,7 +602,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -18, outputDecimals = 18 @@ -637,7 +635,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { + function test_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) int64 price = 1e9; int32 expo = -18; @@ -669,7 +667,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed expo is very negative (expo = -20, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -20, outputDecimals = 18 @@ -702,7 +700,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceDiv_givenFirstFeedExpoVeryNegative() public { + function test_givenFirstFeedExpoVeryNegative() public { // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 int64 price = 1e10; int32 expo = -20; @@ -733,9 +731,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the first feed publish time is >= the threshold boundary // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenFirstFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { + function test_givenFirstFeedPublishTimeAboveThreshold(uint256 publishTime_) public { // Bound the publish time to a reasonable value publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); @@ -764,7 +760,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -8, outputDecimals = 18 @@ -796,7 +792,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative() public { + function test_givenSecondFeedExpoNegative() public { // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -823,7 +819,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the confidence interval is at the maximum (boundary case) // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceEqualsMaximum() public { + function test_givenSecondFeedExpoNegative_confidenceEqualsMaximum() public { // expo = -8, outputDecimals = 18 // confidenceExponent = 18 + (-8) = 10 // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 @@ -856,7 +852,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the confidence interval is below the maximum // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegative_confidenceBelowMaximum( + function test_givenSecondFeedExpoNegative_confidenceBelowMaximum( uint64 priceConfidence_ ) public { // expo = -8, outputDecimals = 18 @@ -891,7 +887,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoPositive_reverts(int32 expo_) public { + function test_givenSecondFeedExpoPositive_reverts(int32 expo_) public { // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error // Keep expo within reasonable range [1, 32] to avoid overflow in calculations expo_ = int32(bound(int256(expo_), 1, 32)); @@ -923,7 +919,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = 0, outputDecimals = 18 @@ -961,7 +957,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoZero() public { + function test_givenSecondFeedExpoZero() public { // Second feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; @@ -989,7 +985,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -18, outputDecimals = 18 @@ -1022,7 +1018,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceDiv_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { + function test_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) int64 price = 1e9; int32 expo = -18; @@ -1054,7 +1050,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed expo is very negative (expo = -20, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -20, outputDecimals = 18 @@ -1087,7 +1083,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceDiv_givenSecondFeedExpoVeryNegative() public { + function test_givenSecondFeedExpoVeryNegative() public { // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 int64 price = 1e10; int32 expo = -20; @@ -1118,9 +1114,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given the second feed publish time is >= the threshold boundary // [X] it returns the correct price - function test_getTwoFeedPriceDiv_givenSecondFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { + function test_givenSecondFeedPublishTimeAboveThreshold(uint256 publishTime_) public { // Bound the publish time to a reasonable value publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); @@ -1148,7 +1142,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given outputDecimals is >= the default (18) // [X] it correctly converts prices to the specified output decimals - function test_getTwoFeedPriceDiv_outputDecimalsFuzz(uint8 outputDecimals_) public { + function test_outputDecimalsFuzz(uint8 outputDecimals_) public { // Bound output decimals to reasonable range [18, 36] to avoid overflow outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1179,7 +1173,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given outputDecimals is < the expo of price feed one // given that price one is < 1 // [X] the price loses precision - function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceOneExpo_priceLessThanOne() public { + function test_outputDecimalsLessThanPriceOneExpo_priceLessThanOne() public { // Bound output decimals to 9, which is less than the expo (18) of price feed one uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1212,9 +1206,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given that price one rounds down to 0 // [X] it returns zero - function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceOneExpo_priceRoundsDownToZero() - public - { + function test_outputDecimalsLessThanPriceOneExpo_priceRoundsDownToZero() public { // Bound output decimals to 9, which is less than the expo (18) of price feed one uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1242,7 +1234,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] the price loses precision - function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceOneExpo() public { + function test_outputDecimalsLessThanPriceOneExpo() public { // Bound output decimals to 9, which is less than the expo (18) of price feed one uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1275,7 +1267,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given outputDecimals is < the expo of price feed two // given that price two is < 1 // [X] the price loses precision - function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceTwoExpo_priceLessThanOne() public { + function test_outputDecimalsLessThanPriceTwoExpo_priceLessThanOne() public { // Bound output decimals to 9, which is less than the expo (18) of price feed two uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1308,9 +1300,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { // given that price two rounds down to 0 // [X] it returns zero - function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceTwoExpo_priceRoundsDownToZero() - public - { + function test_outputDecimalsLessThanPriceTwoExpo_priceRoundsDownToZero() public { // Bound output decimals to 9, which is less than the expo (18) of price feed two uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1341,7 +1331,7 @@ contract PythPriceFeedsGetTwoFeedPriceDivTest is PythPriceFeedsTest { } // [X] the price loses precision - function test_getTwoFeedPriceDiv_outputDecimalsLessThanPriceTwoExpo() public { + function test_outputDecimalsLessThanPriceTwoExpo() public { // Bound output decimals to 9, which is less than the expo (18) of price feed two uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol index a3dee8c99..cfd7c7b11 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getTwoFeedPriceMul.t.sol @@ -19,7 +19,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given all parameters are valid for two feeds // [X] it returns the correct multiplied price (first * second) in output decimals - function test_getTwoFeedPriceMul_success() public view { + function test_success() public view { // First feed: 1.23456789 (expo=-8) = 1234567890000000000 in 18 decimals // Second feed: 0.0000000005 (expo=-18) = 500000000 in 18 decimals // Result: (1234567890000000000 * 500000000) / 10^18 @@ -45,7 +45,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceMul_firstPythZero_reverts() public { + function test_firstPythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 0, @@ -68,7 +68,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second pyth contract address is zero // [X] it reverts with Pyth_ParamsPythInvalid - function test_getTwoFeedPriceMul_secondPythZero_reverts() public { + function test_secondPythZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPythInvalid.selector, 4, @@ -91,7 +91,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceMul_firstPriceFeedIdZero_reverts() public { + function test_firstPriceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 1, @@ -114,7 +114,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second price feed ID is zero // [X] it reverts with Pyth_ParamsPriceFeedIdInvalid - function test_getTwoFeedPriceMul_secondPriceFeedIdZero_reverts() public { + function test_secondPriceFeedIdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsPriceFeedIdInvalid.selector, 5, @@ -137,7 +137,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceMul_firstUpdateThresholdZero_reverts() public { + function test_firstUpdateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 2, @@ -160,7 +160,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second update threshold is zero // [X] it reverts with Pyth_ParamsUpdateThresholdInvalid - function test_getTwoFeedPriceMul_secondUpdateThresholdZero_reverts() public { + function test_secondUpdateThresholdZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsUpdateThresholdInvalid.selector, 6, @@ -183,7 +183,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceMul_firstMaxConfidenceZero_reverts() public { + function test_firstMaxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 3, @@ -206,7 +206,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second max confidence is zero // [X] it reverts with Pyth_ParamsMaxConfidenceInvalid - function test_getTwoFeedPriceMul_secondMaxConfidenceZero_reverts() public { + function test_secondMaxConfidenceZero_reverts() public { bytes memory err = abi.encodeWithSelector( PythPriceFeeds.Pyth_ParamsMaxConfidenceInvalid.selector, 7, @@ -229,7 +229,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceMul_firstFeedPriceInvalid_reverts(int64 price_) public { + function test_firstFeedPriceInvalid_reverts(int64 price_) public { price_ = int64(bound(price_, type(int64).min, 0)); pyth.setPrice(PRICE_ID_1, price_, CONF_1, EXPO_1, block.timestamp); @@ -256,7 +256,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed has invalid price (<= 0) // [X] it reverts with Pyth_FeedPriceInvalid - function test_getTwoFeedPriceMul_secondFeedPriceInvalid_reverts(int64 price_) public { + function test_secondFeedPriceInvalid_reverts(int64 price_) public { price_ = int64(bound(price_, type(int64).min, 0)); pyth.setPrice(PRICE_ID_3, price_, CONF_3, EXPO_3, block.timestamp); @@ -283,7 +283,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first price feed is not found // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceMul_givenFirstFeedPriceFeedNotFound_reverts() public { + function test_givenFirstFeedPriceFeedNotFound_reverts() public { bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); vm.expectRevert(err); @@ -302,7 +302,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second price feed is not found // [X] it reverts with PriceFeedNotFound - function test_getTwoFeedPriceMul_givenSecondFeedPriceFeedNotFound_reverts() public { + function test_givenSecondFeedPriceFeedNotFound_reverts() public { bytes memory err = abi.encodeWithSelector(MockPyth.PriceFeedNotFound.selector); vm.expectRevert(err); @@ -321,7 +321,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { + function test_givenFirstFeedStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); @@ -344,9 +344,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed publish time is < the threshold boundary // [X] it reverts with StalePrice - function test_getTwoFeedPriceMul_givenSecondFeedStalePrice_reverts( - uint256 publishTime_ - ) public { + function test_givenSecondFeedStalePrice_reverts(uint256 publishTime_) public { // Bound publish time to be stale (publishTime < block.timestamp - UPDATE_THRESHOLD) publishTime_ = bound(publishTime_, 1, block.timestamp - UPDATE_THRESHOLD - 1); pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); @@ -370,7 +368,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -8, outputDecimals = 18 @@ -402,7 +400,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegative() public view { + function test_givenFirstFeedExpoNegative() public view { // First feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 // Second feed: expo = -18, price = 500000000 -> 500000000 in 18 decimals bytes memory params = encodeTwoFeedParams( @@ -424,7 +422,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceMul_givenFirstFeedExpoPositive_reverts(int32 expo_) public { + function test_givenFirstFeedExpoPositive_reverts(int32 expo_) public { // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error // Keep expo within reasonable range [1, 32] to avoid overflow in calculations expo_ = int32(bound(int256(expo_), 1, 32)); @@ -456,7 +454,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoZero_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = 0, outputDecimals = 18 @@ -494,7 +492,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenFirstFeedExpoZero() public { + function test_givenFirstFeedExpoZero() public { // First feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; @@ -522,7 +520,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -18, outputDecimals = 18 @@ -555,7 +553,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { + function test_givenFirstFeedExpoNegativeEqualsOutputDecimals() public { // First feed: expo = -18, price = 1e9 -> 1e9 (no scaling) int64 price = 1e9; int32 expo = -18; @@ -587,7 +585,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed expo is very negative (expo = -20, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + function test_givenFirstFeedExpoVeryNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -20, outputDecimals = 18 @@ -620,7 +618,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceMul_givenFirstFeedExpoVeryNegative() public { + function test_givenFirstFeedExpoVeryNegative() public { // First feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 int64 price = 1e10; int32 expo = -20; @@ -651,9 +649,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the first feed publish time is >= the threshold boundary // [X] it returns the correct price - function test_getTwoFeedPriceMul_givenFirstFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { + function test_givenFirstFeedPublishTimeAboveThreshold(uint256 publishTime_) public { // Bound the publish time to a reasonable value publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, publishTime_); @@ -682,7 +678,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed expo is negative (expo = -8, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -8, outputDecimals = 18 @@ -714,7 +710,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegative() public { + function test_givenSecondFeedExpoNegative() public { // Second feed: expo = -8, price = 123456789 -> 1.23456789 in 18 decimals = 1234567890000000000 pyth.setPrice(PRICE_ID_3, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -741,7 +737,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed expo is positive (expo > 0) // [X] it reverts with Pyth_ExponentPositive - function test_getTwoFeedPriceMul_givenSecondFeedExpoPositive_reverts(int32 expo_) public { + function test_givenSecondFeedExpoPositive_reverts(int32 expo_) public { // Bound expo to be positive (> 0) to test Pyth_ExponentPositive error // Keep expo within reasonable range [1, 32] to avoid overflow in calculations expo_ = int32(bound(int256(expo_), 1, 32)); @@ -773,7 +769,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed expo is zero (expo = 0, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoZero_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = 0, outputDecimals = 18 @@ -811,7 +807,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenSecondFeedExpoZero() public { + function test_givenSecondFeedExpoZero() public { // Second feed: expo = 0, price = 100 -> 100 * 10^18 int64 price = 100; int32 expo = 0; @@ -839,7 +835,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed expo equals negative outputDecimals (expo = -18, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoNegativeEqualsOutputDecimals_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -18, outputDecimals = 18 @@ -872,7 +868,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price - function test_getTwoFeedPriceMul_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { + function test_givenSecondFeedExpoNegativeEqualsOutputDecimals() public { // Second feed: expo = -18, price = 1e9 -> 1e9 (no scaling) int64 price = 1e9; int32 expo = -18; @@ -904,7 +900,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed expo is very negative (expo = -20, outputDecimals = 18) // given the confidence interval is above the maximum // [X] it reverts with Pyth_FeedConfidenceExcessive - function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( + function test_givenSecondFeedExpoVeryNegative_maxConfidenceExceeded_reverts( uint64 priceConfidence_ ) public { // expo = -20, outputDecimals = 18 @@ -937,7 +933,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] it correctly converts the price by dividing - function test_getTwoFeedPriceMul_givenSecondFeedExpoVeryNegative() public { + function test_givenSecondFeedExpoVeryNegative() public { // Second feed: expo = -20, price = 1e10 -> 1e10 / 10^2 = 1e8 int64 price = 1e10; int32 expo = -20; @@ -968,9 +964,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given the second feed publish time is >= the threshold boundary // [X] it returns the correct price - function test_getTwoFeedPriceMul_givenSecondFeedPublishTimeAboveThreshold( - uint256 publishTime_ - ) public { + function test_givenSecondFeedPublishTimeAboveThreshold(uint256 publishTime_) public { // Bound the publish time to a reasonable value publishTime_ = bound(publishTime_, block.timestamp - UPDATE_THRESHOLD, block.timestamp); pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, publishTime_); @@ -998,7 +992,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given outputDecimals is different from default (18) // [X] it correctly converts prices to the specified output decimals - function test_getTwoFeedPriceMul_outputDecimalsFuzz(uint8 outputDecimals_) public { + function test_outputDecimalsFuzz(uint8 outputDecimals_) public { // Bound output decimals to reasonable range [18, 36] to avoid overflow outputDecimals_ = uint8(bound(outputDecimals_, 18, 36)); pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1029,7 +1023,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given outputDecimals is < the expo of price feed one // given that price one is < 1 // [X] the price loses precision - function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceOneExpo_priceLessThanOne() public { + function test_outputDecimalsLessThanPriceOneExpo_priceLessThanOne() public { // Bound output decimals to 9, which is less than the expo (18) of price feed one uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1062,9 +1056,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given that price one rounds down to 0 // [X] it returns zero - function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceOneExpo_priceRoundsDownToZero() - public - { + function test_outputDecimalsLessThanPriceOneExpo_priceRoundsDownToZero() public { // Bound output decimals to 9, which is less than the expo (18) of price feed one uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1092,7 +1084,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] the price loses precision - function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceOneExpo() public { + function test_outputDecimalsLessThanPriceOneExpo() public { // Bound output decimals to 9, which is less than the expo (18) of price feed one uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1125,7 +1117,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given outputDecimals is < the expo of price feed two // given that price two is < 1 // [X] the price loses precision - function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceTwoExpo_priceLessThanOne() public { + function test_outputDecimalsLessThanPriceTwoExpo_priceLessThanOne() public { // Bound output decimals to 9, which is less than the expo (18) of price feed two uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1158,9 +1150,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { // given that price two rounds down to 0 // [X] it returns zero - function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceTwoExpo_priceRoundsDownToZero() - public - { + function test_outputDecimalsLessThanPriceTwoExpo_priceRoundsDownToZero() public { // Bound output decimals to 9, which is less than the expo (18) of price feed two uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); @@ -1191,7 +1181,7 @@ contract PythPriceFeedsGetTwoFeedPriceMulTest is PythPriceFeedsTest { } // [X] the price loses precision - function test_getTwoFeedPriceMul_outputDecimalsLessThanPriceTwoExpo() public { + function test_outputDecimalsLessThanPriceTwoExpo() public { // Bound output decimals to 9, which is less than the expo (18) of price feed two uint8 outputDecimals = 9; pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp); From 89121b07d54d34f38f837eee595db8b157d2cf9d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 9 Jan 2026 14:59:26 +0400 Subject: [PATCH 19/19] test: add boundary test --- .../PythPriceFeeds/getOneFeedPrice.t.sol | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol index 1f8e9e3c9..02ad07653 100644 --- a/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol +++ b/src/test/modules/PRICE.v2/submodules/feeds/PythPriceFeeds/getOneFeedPrice.t.sol @@ -486,6 +486,31 @@ contract PythPriceFeedsGetOneFeedPriceTest is PythPriceFeedsTest { assertEq(priceInt, 1000000, "Price should match expected value for very negative expo"); } + // given the confidence interval is at the maximum (boundary case) + // [X] it correctly converts the price + function test_confidenceEqualsMaximum() public { + // expo = -8, outputDecimals = 18 + // confidenceExponent = 18 + (-8) = 10 + // maxConfidenceInPythScale = maxConfidence / 10^10 = 2e16 / 1e10 = 2e6 + // Test with confidence exactly at the maximum (2e6) + uint64 priceConfidence_ = 2e6; + pyth.setPrice(PRICE_ID_1, PRICE_1, priceConfidence_, EXPO_1, block.timestamp); + + bytes memory params = encodeOneFeedParams( + address(pyth), + PRICE_ID_1, + UPDATE_THRESHOLD, + MAX_CONFIDENCE + ); + uint256 priceInt = pythSubmodule.getOneFeedPrice(address(0), PRICE_DECIMALS, params); + + assertEq( + priceInt, + EXPECTED_PRICE_1_18_DEC, + "Price should match expected when confidence equals maximum" + ); + } + // given the confidence interval is <= the maximum // [X] it correctly converts the price function test_confidenceBelowMaximum(uint64 priceConfidence_) public {