code-423n4 / 2024-05-predy-validation

0 stars 0 forks source link

Inconsistent Price Data Due to Sequencer Downtime and Expired Oracle Prices #633

Closed c4-bot-7 closed 3 months ago

c4-bot-7 commented 3 months ago

Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L18 https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L45

Vulnerability details

Impact

Sequencer Downtime: If the sequencer is down, the price data may be stale or incorrect, leading to inaccurate calculations and potential financial losses.

Oracle Price Expiry: Using expired price data can result in incorrect price feeds, causing potential mispricing and financial discrepancies.

Heartbeat Consistency: Inconsistent heartbeats can lead to varying data freshness, causing unreliable price feeds across different tokens.

Proof of Concept

Sequencer Downtime: Assume the sequencer is down for 10 minutes. getSqrtPrice() fetches data without checking sequencer status. The price data is stale, leading to incorrect price calculations

Expired Oracle Data: Using expired oracle data can lead to incorrect price calculations, which can result in financial losses and unreliable contract behavior.

Price feed A has a heartbeat of 1 minute, and price feed B has a heartbeat of 5 minutes. getSqrtPrice() fetches data from both feeds. The data freshness is inconsistent, leading to unreliable price calculations.

Read here

Tools Used

Manual Review

Recommended Mitigation Steps

wWhile creating a PriceFeed, ensure we have a heartbeat interval for each price feed.

--function createPriceFeed(address quotePrice, bytes32 priceId, uint256 decimalsDiff , uint256 hearbeatInterval) external returns (address)
++    function createPriceFeed(address quotePrice, bytes32 priceId, uint256 decimalsDiff , uint16 hearbeatInterval) external returns (address) {
        // deploy new pair for the every pair
--   PriceFeed priceFeed = new PriceFeed(quotePrice, _pyth, priceId, decimalsDiff);
++   PriceFeed priceFeed = new PriceFeed(quotePrice, _pyth, priceId, decimalsDiff, hearbeatInterval);

        emit PriceFeedCreated(quotePrice, priceId, decimalsDiff, address(priceFeed));

        return address(priceFeed);
    }

Now implement a check against the heartbeat interval.

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

++  require(  block.timestamp - updatedAt <= heartbeatInterval,
++  "ORACLE_HEARTBEAT_FAILED");

    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);
    }

Now, to check if the sequencer is down, please follow the example in the Chainlink documentation on how to check the sequencer status: Chainlink Sequencer Status Check.

Assessed type

Oracle