From 7e54d41faae06a5048b782845ddd97297cd67fa2 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 8 Oct 2024 18:32:29 +0200 Subject: [PATCH] Add Pyth unchecked publishTime detector --- .../statements/pyth_unchecked_publishtime.py | 52 +++++ ...8_20_pyth_unchecked_publishtime_sol__0.txt | 3 + .../0.8.20/pyth_unchecked_publishtime.sol | 193 ++++++++++++++++++ .../pyth_unchecked_publishtime.sol-0.8.20.zip | Bin 0 -> 10531 bytes 4 files changed, 248 insertions(+) create mode 100644 slither/detectors/statements/pyth_unchecked_publishtime.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol create mode 100644 tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol-0.8.20.zip diff --git a/slither/detectors/statements/pyth_unchecked_publishtime.py b/slither/detectors/statements/pyth_unchecked_publishtime.py new file mode 100644 index 000000000..e3e2010d6 --- /dev/null +++ b/slither/detectors/statements/pyth_unchecked_publishtime.py @@ -0,0 +1,52 @@ +from slither.detectors.abstract_detector import DetectorClassification +from slither.detectors.statements.pyth_unchecked import PythUnchecked + + +class PythUncheckedPublishTime(PythUnchecked): + """ + Documentation: This detector finds when the publishTime of a Pyth price is not checked + """ + + ARGUMENT = "pyth-unchecked-publishtime" + HELP = "Detect when the publishTime of a Pyth price is not checked" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.HIGH + + WIKI = ( + "https://github.com/crytic/slither/wiki/Detector-Documentation#pyth-unchecked-publishtime" + ) + WIKI_TITLE = "Pyth unchecked publishTime" + WIKI_DESCRIPTION = "Detect when the publishTime of a Pyth price is not checked" + WIKI_RECOMMENDATION = "Check the publishTime of a Pyth price." + + WIKI_EXPLOIT_SCENARIO = """ +```solidity +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; + +contract C { + IPyth pyth; + + constructor(IPyth _pyth) { + pyth = _pyth; + } + + function bad(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + // Use price + } +} +``` +The function `A` uses the price without checking its `publishTime` coming from the `getEmaPriceUnsafe` function. +""" + + PYTH_FUNCTIONS = [ + "getEmaPrice", + # "getEmaPriceNoOlderThan", + "getEmaPriceUnsafe", + "getPrice", + # "getPriceNoOlderThan", + "getPriceUnsafe", + ] + + PYTH_FIELD = "publishTime" diff --git a/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt new file mode 100644 index 000000000..cb331c8d5 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt @@ -0,0 +1,3 @@ +Pyth price publishTime field is not checked in C.bad(bytes32) (tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol#171-175) + - price = pyth.getEmaPriceUnsafe(id) (tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol#172) + diff --git a/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol b/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol new file mode 100644 index 000000000..74ab10fe3 --- /dev/null +++ b/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol @@ -0,0 +1,193 @@ +contract PythStructs { + // A price with a degree of uncertainty, represented as a price +- a confidence interval. + // + // The confidence interval roughly corresponds to the standard error of a normal distribution. + // Both the price and confidence are stored in a fixed-point numeric representation, + // `x * (10^expo)`, where `expo` is the exponent. + // + // Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how + // to how this price safely. + struct Price { + // Price + int64 price; + // Confidence interval around the price + uint64 conf; + // Price exponent + int32 expo; + // Unix timestamp describing when the price was published + uint publishTime; + } + + // PriceFeed represents a current aggregate price from pyth publisher feeds. + struct PriceFeed { + // The price ID. + bytes32 id; + // Latest available price + Price price; + // Latest available exponentially-weighted moving average price + Price emaPrice; + } +} + +interface IPyth { + /// @notice Returns the period (in seconds) that a price feed is considered valid since its publish time + function getValidTimePeriod() external view returns (uint validTimePeriod); + + /// @notice Returns the price and confidence interval. + /// @dev Reverts if the price has not been updated within the last `getValidTimePeriod()` seconds. + /// @param id The Pyth Price Feed ID of which to fetch the price and confidence interval. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPrice( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price and confidence interval. + /// @dev Reverts if the EMA price is not available. + /// @param id The Pyth Price Feed ID of which to fetch the EMA price and confidence interval. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPrice( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the price of a price feed without any sanity checks. + /// @dev This function returns the most recent price update in this contract without any recency checks. + /// This function is unsafe as the returned price update may be arbitrarily far in the past. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use either `getPrice` or `getPriceNoOlderThan`. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPriceUnsafe( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the price that is no older than `age` seconds of the current time. + /// @dev This function is a sanity-checked version of `getPriceUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPriceNoOlderThan( + bytes32 id, + uint age + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price of a price feed without any sanity checks. + /// @dev This function returns the same price as `getEmaPrice` in the case where the price is available. + /// However, if the price is not recent this function returns the latest available price. + /// + /// The returned price can be from arbitrarily far in the past; this function makes no guarantees that + /// the returned price is recent or useful for any particular application. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use either `getEmaPrice` or `getEmaPriceNoOlderThan`. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPriceUnsafe( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price that is no older than `age` seconds + /// of the current time. + /// @dev This function is a sanity-checked version of `getEmaPriceUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPriceNoOlderThan( + bytes32 id, + uint age + ) external view returns (PythStructs.Price memory price); + + /// @notice Update price feeds with given update messages. + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// Prices will be updated if they are more recent than the current stored prices. + /// The call will succeed even if the update is not the most recent. + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + function updatePriceFeeds(bytes[] calldata updateData) external payable; + + /// @notice Wrapper around updatePriceFeeds that rejects fast if a price update is not necessary. A price update is + /// necessary if the current on-chain publishTime is older than the given publishTime. It relies solely on the + /// given `publishTimes` for the price feeds and does not read the actual price update publish time within `updateData`. + /// + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// + /// `priceIds` and `publishTimes` are two arrays with the same size that correspond to senders known publishTime + /// of each priceId when calling this method. If all of price feeds within `priceIds` have updated and have + /// a newer or equal publish time than the given publish time, it will reject the transaction to save gas. + /// Otherwise, it calls updatePriceFeeds method to update the prices. + /// + /// @dev Reverts if update is not needed or the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + /// @param priceIds Array of price ids. + /// @param publishTimes Array of publishTimes. `publishTimes[i]` corresponds to known `publishTime` of `priceIds[i]` + function updatePriceFeedsIfNecessary( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64[] calldata publishTimes + ) external payable; + + /// @notice Returns the required fee to update an array of price updates. + /// @param updateData Array of price update data. + /// @return feeAmount The required fee in Wei. + function getUpdateFee( + bytes[] calldata updateData + ) external view returns (uint feeAmount); + + /// @notice Parse `updateData` and return price feeds of the given `priceIds` if they are all published + /// within `minPublishTime` and `maxPublishTime`. + /// + /// You can use this method if you want to use a Pyth price at a fixed time and not the most recent price; + /// otherwise, please consider using `updatePriceFeeds`. This method does not store the price updates on-chain. + /// + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// + /// + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid or there is + /// no update for any of the given `priceIds` within the given time range. + /// @param updateData Array of price update data. + /// @param priceIds Array of price ids. + /// @param minPublishTime minimum acceptable publishTime for the given `priceIds`. + /// @param maxPublishTime maximum acceptable publishTime for the given `priceIds`. + /// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order). + function parsePriceFeedUpdates( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64 minPublishTime, + uint64 maxPublishTime + ) external payable returns (PythStructs.PriceFeed[] memory priceFeeds); +} + + +contract C { + IPyth pyth; + + constructor(IPyth _pyth) { + pyth = _pyth; + } + + function bad(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + require(price.conf < 10000); + // Use price + } + + function good(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + require(price.publishTime > block.timestamp - 120); + require(price.conf < 10000); + // Use price + } + + function good2(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + require(price.conf < 10000); + if (price.publishTime <= block.timestamp - 120) { + revert(); + } + // Use price + } + +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol-0.8.20.zip b/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..178b65b38891d69b198dce4828330e4d726101e8 GIT binary patch literal 10531 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 BQ^^1T literal 0 HcmV?d00001