Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PriceFeed does not account for the exponent of the base price #495

Closed
c4-bot-4 opened this issue Jun 13, 2024 · 2 comments
Closed

PriceFeed does not account for the exponent of the base price #495

c4-bot-4 opened this issue Jun 13, 2024 · 2 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working insufficient quality report This report is not of sufficient quality 🤖_primary AI based primary recommendation 🤖_10_group AI based duplicate group recommendation

Comments

@c4-bot-4
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2024-05-predy/blob/main/src/PriceFeed.sol#L45-L58
https://github.com/code-423n4/2024-05-predy/blob/main/src/vendors/IPyth.sol#L5-L14
https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/PositionCalculator.sol#L142-L144

Vulnerability details

Impact

The protocol utilizes getPriceNoOlderThan from the Pyth oracle to determine the price of the base token, which is subsequently used to calculate the sqrtPrice.

The Pyth oracle returns a Price struct that contains the price and exponent.

The protocol does not take into account the exponent returned and proceeds to utilize the price without the exponent for the sqrt price calculation, therefore the sqrt price will be incorrect.

This will impact any calls to PriceFeed::getSqrtPrice(). An example is to check if positions are liquidatable if they are unsafe, if the sqrt price is incorrect, then safe positions may be liquidated or unsafe positions may be unable to be liquidated.

Proof of Concept

PriceFeed.sol#L45-L58

   function getSqrtPrice() external view returns (uint256 sqrtPrice) {
        (, int256 quoteAnswer,,,) = AggregatorV3Interface(_quotePriceFeed).latestRoundData();

@>      IPyth.Price memory basePrice = IPyth(_pyth).getPriceNoOlderThan(_priceId, VALID_TIME_PERIOD);

        require(basePrice.expo == -8, "INVALID_EXP");

        require(quoteAnswer > 0 && basePrice.price > 0);

@>      uint256 price = uint256(int256(basePrice.price)) * Constants.Q96 / uint256(quoteAnswer);
        price = price * Constants.Q96 / _decimalsDiff;

        sqrtPrice = FixedPointMathLib.sqrt(price);
    }

The IPyth.Price struct contains the following parameters:

IPyth.sol#L5-L14

    struct Price {
        // Price
        int64 price;
        // Confidence interval around the price
        uint64 conf;
        // Price exponent
        int32 expo;
        // Unix timestamp describing when the price was published
        uint256 publishTime;
    }

As mentioned in the Pyth Network Docs:

"The returned price and confidence are decimal numbers written in the form a * 10^e, where e is an exponent included in the result. For example, a price of 1234 with an exponent of -2 represents the number 12.34. The result also includes a publishTime which is the unix timestamp for the price update."

This means that the exponent must be applied to the basePrice.price to get the actual price. Most Pyth Oracles return a negative exponent, but it's possible that it may be positive, so both cases need to be handled.

Currently, the protocol uses the returned basePrice.price for the sqrtPrice calculation, which will return an incorrect sqrtPrice.

An example of where this price feed is used is in PositionCalculator::getSqrtIndexPrice

PositionCalculator.sol#L142-L144

        if (pairStatus.priceFeed != address(0)) {
            return PriceFeed(pairStatus.priceFeed).getSqrtPrice();
        }

The flow of the call in this example is LiquidationLogic::liquidate => LiquidationLogic::checkVaultIsDanger => PositionCalculator::isLiquidatable => PositionCalculator::calculateMinMargin => PositionCalculator::getSqrtIndexPrice

In that case, this would not correctly be able to determine if a position is unsafe and should be forced liquidated.

Tools Used

Manual Review

Recommended Mitigation Steps

Apply the returned expo of the Price struct to the price. The expo may be positive or negative, so both cases must be handled. If exponent is negative, ensure to negate it when dividing.

   function getSqrtPrice() external view returns (uint256 sqrtPrice) {
        (, int256 quoteAnswer,,,) = AggregatorV3Interface(_quotePriceFeed).latestRoundData();

        IPyth.Price memory basePrice = IPyth(_pyth).getPriceNoOlderThan(_priceId, VALID_TIME_PERIOD);

        require(basePrice.expo == -8, "INVALID_EXP");

        require(quoteAnswer > 0 && basePrice.price > 0);

+       int256 basePriceFinal = basePrice.expo >= 0 ? (basePrice.price * 10 ** basePrice.expo) : (basePrice.price / 10**(-basePrice.expo));

-       uint256 price = uint256(int256(basePrice.price)) * Constants.Q96 / uint256(quoteAnswer);
+       uint256 price = uint256(basePriceFinal) * Constants.Q96 / uint256(quoteAnswer);
        price = price * Constants.Q96 / _decimalsDiff;

        sqrtPrice = FixedPointMathLib.sqrt(price);
    }

Assessed type

Oracle

@c4-bot-4 c4-bot-4 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Jun 13, 2024
c4-bot-2 added a commit that referenced this issue Jun 13, 2024
@c4-bot-12 c4-bot-12 added 🤖_10_group AI based duplicate group recommendation 🤖_primary AI based primary recommendation labels Jun 14, 2024
@howlbot-integration howlbot-integration bot added the insufficient quality report This report is not of sufficient quality label Jun 19, 2024
@crypticdefense
Copy link

This report describes how the PriceFeed contract does not account for the exponent of the base price retrieved from the Pyth oracle.

As mentioned in the Pyth Network Docs:

"The returned price and confidence are decimal numbers written in the form a * 10^e, where e is an exponent included in the result. For example, a price of 1234 with an exponent of -2 represents the number 12.34. The result also includes a publishTime which is the unix timestamp for the price update."

The base price simply uses the price but does not account for the exponent. The recommended mitigation suggests to apply the exponent and account for both positive and negative exponents, since the oracle can return either one. As mentioned in the report, this will lead to an incorrect sqrtPrice calculation, affecting many different parts of the protocol, such as the ability for bad positions to be liquidated.

Here's a similar finding on solodit, where the protocol correctly applied the exponents to the price returned from the Pyth oracle, but were not account for negative exponents: #1

@crypticdefense
Copy link

Therefore, I believe this finding should be valid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working insufficient quality report This report is not of sufficient quality 🤖_primary AI based primary recommendation 🤖_10_group AI based duplicate group recommendation
Projects
None yet
Development

No branches or pull requests

3 participants