xanoutas/solidity-security-findings huggingface dataset:
Chainlink's latestRoundData() might return stale or incorrect results on PriceFeed#`getSqrtPrice`.
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/tree/main/src/PriceFeed.sol#L45-L58


# Vulnerability details

## Summary

PriceFeed#`getSqrtPrice()` might return zero price.

## Vulnerability Detail

1. Chainlink's AggregatorV3Interface#`latestRoundData()` is used to fetch the price feed data and returns the following informations. [Chainlink API Reference#latestRoundData()](https://docs.chain.link/data-feeds/api-reference#latestrounddata)
	- `roundId`: The round ID.
	- `answer`: The data that this specific feed provides. Depending on the feed you selected, this answer provides asset prices, reserves, NFT floor prices, and other types of data.
	- `startedAt`: Timestamp of when the round started.
	- `updatedAt`: Timestamp of when the round was updated.
	- `answeredInRound`: Deprecated - Previously used when answers could take multiple rounds to be computed.

2. PriceFeed#`getSqrtPrice()` are missing the following verifications for the return values of `latestRoundData()`.
	- Does not check if `updatedAt` is stale or not.

```solidity
function getSqrtPrice() external view returns (uint256 sqrtPrice) {
	//<------------------- @audit
	(, 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);
	require(updatedAteAnswer > 0 && basePrice.price > 0);
	
	uint256 price = 
		uint256(int256(basePrice.price)) * Constants.Q96 / uint256(quoteAnswer);
	...
}
```

## Impact

PriceFeed#`getSqrtPrice()` might return zero price and it will break the accounting in the protocol.

## Tool used

Manual Review

## Recommendation

1. Please define `maxOracleUpdateDuration` for example as following on `PriceFeed` contract.

```diff
	bytes32 private immutable _priceId;
	
++	uint32 private constant maxOracleUpdateDuration = 24 hours;

	uint256 private constant VALID_TIME_PERIOD = 5 * 60;
```

2. Please update PriceFeed#`getSqrtPrice()` as following.

```diff
function getSqrtPrice() external view returns (uint256 sqrtPrice) {
--	(, int256 quoteAnswer,,,) = 
--		AggregatorV3Interface(_quotePriceFeed).latestRoundData();
++	(, int256 quoteAnswer,, uint256 updatedAt,) = 
++		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);
	require(updatedAteAnswer > 0 && basePrice.price > 0);
	
++	require(block.timestamp - updatedAt > maxOracleUpdateDuration, "Stale Price");
	
	uint256 price = 
		uint256(int256(basePrice.price)) * Constants.Q96 / uint256(quoteAnswer);
	...
}
```



## Assessed type

Invalid Validation
Add default constructor that calls `_disableInitializers()` to upgradable contracts.
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/tree/main/src/PredyPool.sol#L68
https://github.com/code-423n4/2024-05-predy/tree/main/src/markets/gamma/GammaTradeMarket.sol#L67
https://github.com/code-423n4/2024-05-predy/tree/main/src/markets/perp/PerpMarketV1.sol#L90


# Vulnerability details

## Summary

`PredyPool`, `GammaTradeMarket`, `PerpMarketV1` contract uses upgradable contracts. 
Calling the `initialize()` function directly on the implementation contract behind a proxy is dangerous. 
In such case, if the implementation calls self-destruct or performs delegate calls it’s possible to delete the implementation leaving the contract bricked. 

## Vulnerability Detail

Upgradable contracts are an essential protocol feature, allowing for flexible updates and maintenance. 
Contracts should include a default constructor calling `_disableInitializers()` function: 
(https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/Initializable.sol#L192) of `Initializable.sol`.

## Impact

When implementing upgradable contracts, ensuring that the `initialize()` function is not accidentally called directly on the implementation contract behind the proxy is crucial. 

If this occurs and the implementation contract contains self-destruct or delegate calls, it can result in the unintended deletion of the implementation contract.

## Tool used

Manual Review

## Recommendation

1. Please update PredyPool#`constructor()` as follows.

```diff
contract PredyPool is 
	IPredyPool, IUniswapV3MintCallback, Initializable, ReentrancyGuardUpgradeable {
	...
	
	constructor() {
++		_disableInitializers();
	}
	
	...
}
```

2. Please update GammaTradeMarket#`constructor()` as follows.

```diff
contract GammaTradeMarket is 
	IFillerMarket, BaseMarketUpgradable, ReentrancyGuardUpgradeable {
	...
	
	constructor() {
++		_disableInitializers();
	}
	
	...
}
```

3. Please update PerpMarketV1#`constructor()` as follows.

```diff
contract PerpMarketV1 is BaseMarketUpgradable, ReentrancyGuardUpgradeable {
	...
	
	constructor() {
++		_disableInitializers();
	}
	
	...
}
```



## Assessed type

Upgradable
Lack of Stale Data Check in `getSqrtPrice` Function Can Lead to Incorrect Price Calculations
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/e96f378007e3dc56b184079f0c0e4fe48a72efaa/src/PriceFeed.sol#L46


# Vulnerability details

## Impact

The `getSqrtPrice` function in the `PriceFeed.sol` contract retrieves the latest price data from an oracle to compute the square root of the price. However, the function does not verify whether the retrieved price data is stale. Using stale data can result in incorrect price calculations, which may impact critical functionality reliant on accurate and up-to-date price information, leading to financial losses or erroneous contract behavior.

## Proof of Concept

In the `PriceFeed.sol` contract, the `getSqrtPrice` function is implemented as follows:

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

  // @audit no check for stale price data.

  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 function does not check if the `quoteAnswer` obtained from the oracle is based on stale data. Without this verification, the contract may use outdated prices, leading to incorrect calculations.

### Example of Stale Data Impact

If the oracle data is outdated due to network issues, delays, or oracle downtime, the `getSqrtPrice` function will use old price data, resulting in incorrect price computations. This can affect various functionalities dependent on accurate price feeds, such as asset valuation, trading, and risk management, potentially leading to significant financial losses.

## Tools Used

- Manual code review

## Recommended Mitigation Steps

To ensure the function uses up-to-date price data, implement a check for the `updatedAt` parameter from the `latestRoundData()` call and verify that it is within an acceptable time range. Here’s an example of how to refactor the function:

1. Modify the `latestRoundData` call to include the `updatedAt` parameter:

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

     // Check for stale price data
     require(
       block.timestamp - updatedAt < VALID_TIME_PERIOD,
       "Stale price data"
     );

     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) /
       uint
calls to Chainlink's `latestRoundData` can return stale or incorrect result
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://vscode.dev/github/code-423n4/2024-05-predy/blob/main/src/PriceFeed.sol#L46
https://vscode.dev/github/code-423n4/2024-05-predy/blob/main/src/libraries/PositionCalculator.sol#L143


# Vulnerability details

## Impact

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

There is no check in the `getSqrtPrice()` function to validate Chainlink's `latestRoundData`  completeness, or the returned timestamp, and this could lead to stale, outdated, or incorrect price return value, which could affect `getSqrtIndexPrice` function that depends on the getSqrtPrice() function.

## Proof of Concept

https://vscode.dev/github/code-423n4/2024-05-predy/blob/main/src/PriceFeed.sol#L45-L47

The getSqrtPrice() function from the PriceFeed.sol contract calls the
`latestRoundData` from ChainLink price feeds. However, there is
no check on the return values to validate stale data prices.

This could lead to stale prices according to the ChainLink documentation:
[here](https://docs.chain.link/data-feeds/historical-data)

## Tools Used

Manual Review

## Recommended Mitigation Steps

Add checks for the returned data after calling `latestRoundData()`







## Assessed type

Oracle
Chainlink Oracle priceFeed Data May Return Stale Prices
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/main/src/PriceFeed.sol#L44-L58


# Vulnerability details

## Impact
The `PriceFeed` contract does not sufficiently validate the Chainlink oracle data feed for stale prices. The failure to validate the freshness of the price may result in the usage of stale prices, leading to incorrect calculations where price matters.

## Proof of Concept
In the `PriceFeed` contract, the `getSqrtPrice()` function retrieves the price of `Quote Token`  using Chainlink's latestRoundData function, without validating the freshness of the returned price. It simply takes the price from the returned data, ignoring other returned parameters such as updatedAt and roundId. 
The unverified return price is used to calculate the square root price of the base token, which is calculated in terms of the quote token in #L54 of the code snippet below.

```solidity
    function getSqrtPrice() external view returns (uint256 sqrtPrice) {
46:     (, 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);

54:     uint256 price = uint256(int256(basePrice.price)) * Constants.Q96 / uint256(quoteAnswer);
        price = price * Constants.Q96 / _decimalsDiff;

        sqrtPrice = FixedPointMathLib.sqrt(price);
    }
```
The `sqrtPrice`sqrtPrice` return price is used for slippage checking in the `SlippageLib.sol#checkPrice()` function.
```solidity
    function checkPrice(
        uint256 sqrtBasePrice,
        IPredyPool.TradeResult memory tradeResult,
        uint256 slippageTolerance,
        uint256 maxAcceptableSqrtPriceRange
    ) internal pure {
27:     uint256 basePrice = Math.calSqrtPriceToPrice(sqrtBasePrice);

        if (tradeResult.averagePrice == 0) {
            revert InvalidAveragePrice();
        }

33:        if (tradeResult.averagePrice > 0) {
            // short
35:         if (basePrice.lower(slippageTolerance) > uint256(tradeResult.averagePrice)) {
36:             revert SlippageTooLarge();
37:         }
38:     } else if (tradeResult.averagePrice < 0) {
            // long
40:         if (basePrice.upper(slippageTolerance) < uint256(-tradeResult.averagePrice)) {
41:             revert SlippageTooLarge();
42:         }
43:     }

45:     if (
46:         maxAcceptableSqrtPriceRange > 0
47:             && (
48:                 tradeResult.sqrtPrice < sqrtBasePrice * 1e8 / maxAcceptableSqrtPriceRange
49:                     || sqrtBasePrice * maxAcceptableSqrtPriceRange / 1e8 < tradeResult.sqrtPrice
50:             )
51:     ) {
52:         revert OutOfAcceptablePriceRange();
53:     }
    }
```
Failure to verify the latestness of prices may result in the use of outdated prices, and slippage checks on trading results may not be performed correctly.
According to Chainl
Dangerous use of deadline parameter in swapExactIn and swapExactOut
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/settlements/UniswapSettlement.sol#L22-L56


# Vulnerability details

## Description
In the `UniswapSettlement.sol` contract, the methods `swapExactIn` and `swapExactOut` use `block.timestamp` as the `deadline` argument while interacting with the Uniswap V3 Protocol via the `exactInput()` method. This practice fails to provide adequate protection against transaction delays and potential price swings.


The `deadline` parameter in exact input swaps is designed to mitigate risks associated with transaction delays and resultant price volatility. However, using `block.timestamp` as the deadline offers no such protection, as it merely reflects the current timestamp, which means the transaction will be valid whenever the miner includes it in a block.

## Impact
An attacker could intentionally delay the transaction in the mempool, allowing it to be processed much later. During this period, the Uniswap pool's price may change significantly and become unfavorable.

## Proof of Concept
https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/settlements/UniswapSettlement.sol#L22-L56

```solidity
    function swapExactIn(
        address,
        address baseToken,
        bytes memory data,
        uint256 amountIn,
        uint256 amountOutMinimum,
        address recipient
    ) external override returns (uint256 amountOut) {
        ERC20(baseToken).safeTransferFrom(msg.sender, address(this), amountIn);
        ERC20(baseToken).approve(address(_swapRouter), amountIn);

        // @audit block.timestamp as deadline means that whenever the miner decides to include the txn in a block, it will be valid at that time, since block.timestamp will be the current timestamp.
        amountOut = _swapRouter.exactInput(
            ISwapRouter.ExactInputParams(data, recipient, block.timestamp, amountIn, amountOutMinimum)
        );
    }

    function swapExactOut(
        address quoteToken,
        address,
        bytes memory data,
        uint256 amountOut,
        uint256 amountInMaximum,
        address recipient
    ) external override returns (uint256 amountIn) {
        ERC20(quoteToken).safeTransferFrom(msg.sender, address(this), amountInMaximum);
        ERC20(quoteToken).approve(address(_swapRouter), amountInMaximum);

        amountIn = _swapRouter.exactOutput(
            ISwapRouter.ExactOutputParams(data, recipient, block.timestamp, amountOut, amountInMaximum)
        );

        if (amountInMaximum > amountIn) {
            ERC20(quoteToken).safeTransfer(msg.sender, amountInMaximum - amountIn);
        }
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
It is recommended to pass valid deadline value into `swapExactIn` and `swapExactOut`, e.g., add a new field in `settlementParams` as the `deadline` value just like the slippage parameter.


## Assessed type

Timing
Chainlink oracle may return stale price
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L46
https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/PositionCalculator.sol#L143


# Vulnerability details

### Background
- There is a function in the `PriceFeed` contract for a given pair that returns the square root of the baseToken price quoted in quoteToken.

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

- This function is called by `getSqrtIndexPrice()` in the `PositionCalculator` whenever a price feed is available.

```solidity
function getSqrtIndexPrice(DataType.PairStatus memory pairStatus) internal view returns (uint256 sqrtPriceX96) {
    if (pairStatus.priceFeed != address(0)) {
        return PriceFeed(pairStatus.priceFeed).getSqrtPrice();
    } else {
        return UniHelper.convertSqrtPrice(
            UniHelper.getSqrtTWAP(pairStatus.sqrtAssetStatus.uniswapPool), pairStatus.isQuoteZero
        );
    }
}
```

- This function is, in turn, called by the function with the same name in the `PredyPool` contract and by the internal `calculateMinMargin()` function in the `PositionCalculator`.

1. **PredyPool's `getSqrtIndexPrice()` Function**

    ```solidity
    
    function getSqrtIndexPrice(uint256 pairId) external view returns (uint256) {
        return PositionCalculator.getSqrtIndexPrice(globalData.pairs[pairId]);
    }

    ```

    This function is called by:
    - The `autoHedge()` and `autoClose()` routines in the `GammaTradeMarket` contract to perform actions like hedging or closing positions based on the position price.
    - The `predyTradeAfterCallback()` function in the `PrepMarketV1` contract, via the internal `_calculateInitialMargin()` routine, to calculate the margin amount that users need to provide when executing orders.

2. **PositionCalculator's Internal `calculateMinMargin()` Function**

    ```solidity

    function calculateMinMargin(
        DataType.PairStatus memory pairStatus,
        DataType.Vault memory vault,
        DataType.FeeAmount memory feeAmount
    ) internal view returns (int256 minMargin, int256 vaultValue, bool hasPosition, uint256 sqrtOraclePrice) {
        int256 minValue;
        uint256 debtValue;

        sqrtOraclePrice = getSqrtIndexPrice(pairStatus);

        (minValue, vaultValue, debtValue, hasPosition) = calculateMinValue(
            vault.margin,
            getPositionWithFeeAmount(v
latestRoundData() has no check for round completeness
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L46
 https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/PositionCalculator.sol#L143


# Vulnerability details

### Background
- There is a function in the `PriceFeed` contract for a given pair that returns the square root of the baseToken price quoted in quoteToken.

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

- This function is called by `getSqrtIndexPrice()` in the `PositionCalculator` whenever a price feed is available.

```solidity
function getSqrtIndexPrice(DataType.PairStatus memory pairStatus) internal view returns (uint256 sqrtPriceX96) {
   if (pairStatus.priceFeed != address(0)) {
       return PriceFeed(pairStatus.priceFeed).getSqrtPrice();
   } else {
       return UniHelper.convertSqrtPrice(
           UniHelper.getSqrtTWAP(pairStatus.sqrtAssetStatus.uniswapPool), pairStatus.isQuoteZero
       );
   }
}
```

- This function is, in turn, called by the function with the same name in the `PredyPool` contract and by the internal `calculateMinMargin()` function in the `PositionCalculator`.

1. **PredyPool's `getSqrtIndexPrice()` Function**

   ```solidity

   function getSqrtIndexPrice(uint256 pairId) external view returns (uint256) {
       return PositionCalculator.getSqrtIndexPrice(globalData.pairs[pairId]);
   }

   ```

   This function is called by:
   - The `autoHedge()` and `autoClose()` routines in the `GammaTradeMarket` contract to perform actions like hedging or closing positions based on the position price.
   - The `predyTradeAfterCallback()` function in the `PrepMarketV1` contract, via the internal `_calculateInitialMargin()` routine, to calculate the margin amount that users need to provide when executing orders.

2. **PositionCalculator's Internal `calculateMinMargin()` Function**

   ```solidity

   function calculateMinMargin(
       DataType.PairStatus memory pairStatus,
       DataType.Vault memory vault,
       DataType.FeeAmount memory feeAmount
   ) internal view returns (int256 minMargin, int256 vaultValue, bool hasPosition, uint256 sqrtOraclePrice) {
       int256 minValue;
       uint256 debtValue;

       sqrtOraclePrice = getSqrtIndexPrice(pairStatus);

       (minValue, vaultValue, debtValue, hasPosition) = calculateMinValue(
           vault.margin,
           getPositionWithFeeAmount(vault.openPosition, feeAmount),