xanoutas/solidity-security-findings huggingface dataset:
Possible approval revert for USDT token
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When the contract wants to approve USDT tokens for uniswap v3 pool in order to buy some base tokens, eg, WETH, `approve` might be reverted. This will cause `trade` failure. 

## Proof of Concept
In the predy contract, if we want to open one perp or sqrt perp position, sometimes we need to buy some base tokens. To make sure the exact output token amount, `amountInMaximum` is usually than the actual `amountIn`. Before we interact with Uniswap Pool, we need to approve `amountInMaximum` amount of quote tokens to Uniswap V3 pool.

```javascript
    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);
        }
    }
```
The vulnerability is that USDT token has one approval race condition protection. From the readme.txt, we can see that we wish to support USDT token, and USDT token does not allow to approve when the `allowance` is not zero.
For example, when we execute one order, we buy some WETH tokens with USDT tokens, there might be some left `allowance[Uniswapv3settlement][uniswapV3pool]`. When we execute another other, and we want to approve with one new value, this operation will be reverted.

```sol
    function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) {

        // To change the approve amount you first have to reduce the addresses`
        //  allowance to zero by calling `approve(_spender, 0)` if it is not
        //  already 0 to mitigate the race condition described here:
        //  https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
        require(!((_value != 0) && (allowed[msg.sender][_spender] != 0)));

        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);
    }
```
## Tools Used
Manual

## Recommended Mitigation Steps
```diff
diff --git a/src/settlements/UniswapSettlement.sol b/src/settlements/UniswapSettlement.sol
index 7bb011a..6c8c63b 100644
--- a/src/settlements/UniswapSettlement.sol
+++ b/src/settlements/UniswapSettlement.sol
@@ -44,6 +44,7 @@ contract UniswapSettlement is ISettlement {
         address recipient
     ) external override returns (uint256 amountIn) {
         ERC20(quoteToken).safeTransferFrom(msg.sender, address(this), amoun
Upgraded Q -> 2 from #259 [1719594465362]
Audit: 2024-05-predy-findings
Severity: MEDIUM

Judge has assessed an item in Issue #259 as 2 risk. The relevant finding follows:

 ## QA-09 Trades are being finalized with an easily manipulatable prices which might cause frequent reverts

### Proof of Concept

Take a look at the fucntion that ends up being called whenever there is a need to exefcute a trade of any kind https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/Trade.sol#L32-L74

```solidity
    function trade(
        GlobalDataLibrary.GlobalData storage globalData,
        IPredyPool.TradeParams memory tradeParams,
        bytes memory settlementData
    ) external returns (IPredyPool.TradeResult memory tradeResult) {
        DataType.PairStatus storage pairStatus = globalData.pairs[tradeParams.pairId];
        Perp.UserStatus storage openPosition = globalData.vaults[tradeParams.vaultId].openPosition;

        // settle user balance and fee
        DataType.FeeAmount memory realizedFee =
            settleUserBalanceAndFee(pairStatus, globalData.rebalanceFeeGrowthCache, openPosition);

        // calculate required token amounts
        (int256 underlyingAmountForSqrt, int256 stableAmountForSqrt) = Perp.computeRequiredAmounts(
            pairStatus.sqrtAssetStatus, pairStatus.isQuoteZero, openPosition, tradeParams.tradeAmountSqrt
        );

        tradeResult.sqrtPrice = getSqrtPrice(pairStatus.sqrtAssetStatus.uniswapPool, pairStatus.isQuoteZero);//@audit slot0 is used to place trades which is easily manipulatable

        // swap tokens

        SwapStableResult memory swapResult = swap(
            globalData,
            tradeParams.pairId,
            SwapStableResult(-tradeParams.tradeAmount, underlyingAmountForSqrt, realizedFee.feeAmountBase, 0),
            settlementData,
            tradeResult.sqrtPrice,//@audit
            tradeParams.vaultId
        );

        tradeResult.averagePrice = swapResult.averagePrice;

        // add asset or debt
        tradeResult.payoff = Perp.updatePosition(
            pairStatus,
            openPosition,
            Perp.UpdatePerpParams(tradeParams.tradeAmount, swapResult.amountPerp),
            Perp.UpdateSqrtPerpParams(tradeParams.tradeAmountSqrt, swapResult.amountSqrtPerp + stableAmountForSqrt)
        );

        tradeResult.fee = realizedFee.feeAmountQuote + swapResult.fee;
        tradeResult.vaultId = tradeParams.vaultId;
    }
```

Function works fine, only issue is that it hardcodes the use of `slot0` when chosing the price that the swap gets finalized on.

Going to the implementation of `getSqrtPrice` we can see that it ends up querying the `UniHelper.getSqrtPrice` i.e https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/Trade.sol#L112-L114

```solidity
    function getSqrtPrice(address uniswapPoolAddress, bool isQuoteZero) internal view returns (uint256 sqrtPriceX96) {
        return UniHelper.convertSqrtPrice(
Upgraded Q -> 2 from #254 [1719594414430]
Audit: 2024-05-predy-findings
Severity: MEDIUM

Judge has assessed an item in Issue #254 as 2 risk. The relevant finding follows:

 ## [L-5] Chainlink's latestRoundData return stale or incorrect result

### Impact

On PriceFeed.sol, you are using latestRoundData, but there is no check if the return value indicates stale data. The current check ``quoteAnswer > 0`` not enough to ensure the staleness . 

This could lead to stale prices according to the Chainlink documentation: https://docs.chain.link/data-feeds/price-feeds/historical-data Related report: https://github.com/code-423n4/2021-05-fairside-findings/issues/70

```solidity
FILE:2024-05-predy/src/PriceFeed.sol

    (, 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);

        

```
https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L46-L52

### Recommended Mitigation
Incorporate the required checks to ensure the staleness 

```diff

(, int256 quoteAnswer,,,) = AggregatorV3Interface(_quotePriceFeed).latestRoundData();

+    (uint80 roundID,int256 quoteAnswer,uint256 timestamp,uint256 updatedAt,) = AggregatorV3Interface(_quotePriceFeed).latestRoundData();

        IPyth.Price memory basePrice = IPyth(_pyth).getPriceNoOlderThan(_priceId, VALID_TIME_PERIOD);

+ require(updatedAt >= roundID, "Stale price");
+ require(timestamp != 0,"Round not complete");

+ if (updatedAt < block.timestamp - maxDelayTime) //maxDelayTime minimum allowed delay 
+            revert PRICE_OUTDATED();
        
        require(basePrice.expo == -8, "INVALID_EXP");

        require(quoteAnswer > 0 && basePrice.price > 0);

```
Upgraded Q -> 2 from #254 [1719594348550]
Audit: 2024-05-predy-findings
Severity: MEDIUM

Judge has assessed an item in Issue #254 as 2 risk. The relevant finding follows:

 ## [L-2] Usage of `slot0` is extremely easy to manipulate

### Impact
The function uses slot0 to obtain the current tick (tickCurrent). Since slot0 is easy to manipulate, attackers can influence the value of tickCurrent.

Manipulating tickCurrent can lead to incorrect values for feeGrowthBelow0X128, feeGrowthBelow1X128, feeGrowthAbove0X128, and feeGrowthAbove1X128. Consequently, feeGrowthInside0X128 and feeGrowthInside1X128 values will also be incorrect.

[slot0(https://docs.uniswap.org/contracts/v3/reference/core/interfaces/pool/IUniswapV3PoolState#slot0) is the most recent data point and is therefore extremely easy to manipulate.


```solidity
FILE:2024-05-predy/src/libraries/UniHelper.sol

104: (, int24 tickCurrent,,,,,) = IUniswapV3Pool(uniswapPoolAddress).slot0();

```
https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/UniHelper.sol#L104

### Recommended Mitigations
To make any calculation use a TWAP instead of slot0.
Upgraded Q -> 2 from #252 [1719594295360]
Audit: 2024-05-predy-findings
Severity: MEDIUM

Judge has assessed an item in Issue #252 as 2 risk. The relevant finding follows:

 ## 6. No check for stale prices upon querying chainlink tokens

Links to affected code *

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L46


### Impact

The `getSqrtPrice` function queries chainlink's `latestRoundData` but doesn't check if the price returned is stale. Using a stale price can cause the calculations depending on the `getSqrtPrice` function to be inaccurate, which, for example, can mistakenly consider a user to be ripe for liquidation when in reality they're not and unexpectedly allow the user to be liquidated.

```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);
```

### Recommended Mitigation Steps

Consider checking for stale prices.

```solidity
(uint80 roundID, int256 feedPrice, , uint256 timestamp, uint80 answeredInRound) = feed.latestRoundData();
require(feedPrice > 0, "Chainlink price <= 0"); 
require(answeredInRound >= roundID, "Stale price");
require(timestamp != 0, "Round not complete");
require(timeStamp < block.timestamp - staleTime, "Stale price")
```
Upgraded Q -> 2 from #253 [1719594215976]
Audit: 2024-05-predy-findings
Severity: MEDIUM

Judge has assessed an item in Issue #253 as 2 risk. The relevant finding follows:

 ## [QA-26]  `callUniswapObserve` will show incorrect price for negative ticks because it does not round up for them


### Impact
In case if `int24(tickCumulatives[i] - tickCumulatives[i+1])` is negative and `((tickCumulatives[i] - tickCumulatives[i+1]) % secondsAgo != 0`, then returned tick will be bigger than it should be which opens possibility for some price manipulations and arbitrage opportunities.
If `int24(tickCumulatives[1] - tickCumulatives[0])` is negative and `((tickCumulatives[1] - tickCumulatives[0]) % secondsAgo != 0`, then returned tick will be bigger than it should be which places Predy protocol wanting prices to be right not be able to achieve this goal.

### Proof of Concept
Take a look at: https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/UniHelper.sol#L35-L74

```solidity
function callUniswapObserve(IUniswapV3Pool uniswapPool, uint256 ago) internal view returns (uint160, uint256) {
        uint32[] memory secondsAgos = new uint32[](2);

        secondsAgos[0] = uint32(ago);
        secondsAgos[1] = 0;

        (bool success, bytes memory data) =
            address(uniswapPool).staticcall(abi.encodeWithSelector(IUniswapV3PoolOracle.observe.selector, secondsAgos));

        if (!success) {
            if (keccak256(data) != keccak256(abi.encodeWithSignature("Error(string)", "OLD"))) {
                revertBytes(data);
            }

            (,, uint16 index, uint16 cardinality,,,) = uniswapPool.slot0();

            (uint32 oldestAvailableAge,,, bool initialized) = uniswapPool.observations((index + 1) % cardinality);

            if (!initialized) {
                (oldestAvailableAge,,,) = uniswapPool.observations(0);
            }

            ago = block.timestamp - oldestAvailableAge;
            secondsAgos[0] = uint32(ago);

            (success, data) = address(uniswapPool).staticcall(
                abi.encodeWithSelector(IUniswapV3PoolOracle.observe.selector, secondsAgos)
            );
            if (!success) {
                revertBytes(data);
            }
        }

        int56[] memory tickCumulatives = abi.decode(data, (int56[]));

        int24 tick = int24((tickCumulatives[1] - tickCumulatives[0]) / int56(int256(ago)));

        uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick);

        return (sqrtPriceX96, ago);
```
This function is used to get twap price tick using uniswap. It gets `tickCumulatives` array which is then used to calculate `int24 tick`. 

The problem is that if `int24(tickCumulatives[1] - tickCumulatives[0])` is negative, then the tick should be rounded down as it's done in the[ uniswap library](https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/OracleLibrary.sol#L36).

As result, in case if `int24(tickCumulatives[1] - tickCumulatives[0])` is negative and `(tickCumul
No Protection of Uninitialized Implementation Contracts From Attacker
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/2fb1e0ec7a52fc06c2e9c8e561bccba84302e4bb/src/PredyPool.sol#L68
https://github.com/code-423n4/2024-05-predy/blob/2fb1e0ec7a52fc06c2e9c8e561bccba84302e4bb/src/base/BaseHookCallbackUpgradable.sol#L13
https://github.com/code-423n4/2024-05-predy/blob/2fb1e0ec7a52fc06c2e9c8e561bccba84302e4bb/src/base/BaseMarketUpgradable.sol#L36


# Vulnerability details

## Impact
In the contracts that implement `Openzeppelin`’s `Upgreadable` model, uninitialized `Implementation` contract can be taken over by an attacker with `initialize()` function.

## Proof of Concept
###### Scenario:
1. `Proxy` & `Implementation` are deployed.
2. The `Proxy` delegates calls to `Implementation.initialize()` which sets the `owner` and switches `initialized` to `true` in the state of the `Proxy`.
3. The storage of `Implementation` however is still intact e.g `owner` is unset and `initialized` is `false`.
4. An attacker calls `initialize()` directly on `Implementation` and sets himself as the `owner`.
5. From here, he has full control to perform any maliceous activities.

## Tools Used
Manual Review

## Recommended Mitigation Steps
From [Openzeppelin Docs](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable):


*Do not leave an implementation contract uninitialized. An uninitialized implementation contract can be taken over by an attacker, which may impact the proxy. To prevent the implementation contract from being used, you should invoke the _disableInitializers function in the constructor to automatically lock it when it is deployed*:

```diff
-   constructor() {}
+   constructor() {
        _disableInitializers();
    }
```








## Assessed type

Other
Missing check for the max/min price in `getSqrtPrice()`
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The `getSqrtPrice()` function uses the `aggregatorV3` to get/call the `latestRoundData()`. However, the function does not check for the `min` and `max` price returned.

Without a check for `minimum` and `maximum allowable prices`, the function might return prices that are unreasonably high or low. If the function returns a price that is significantly outside the expected range, it could lead to financial losses for users or the system itself.

## Proof of Concept
[getSqrtPrice()](https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L45-L58)

```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);
    }
```
The function only requires that the `quoteAnswer > 0`.
However, it does not check for the `min/max price` allowable and therefore when the `getSqrtPrice()` returns the price value, this value can be anything absurdly odd.

## Tools Used
Manual Review

## Recommended Mitigation Steps
As explained in [this Documentation](https://docs.chain.link/data-feeds#check-the-latest-answer-against-reasonable-limits):
A circuit breaker should be implemented on the oracle so that when the price edges close to `minAnswer` or `maxAnswer` it starts reverting.

```diff
+   require(quoteAnswer < _maxPrice, "Upper price bound breached");          
+   require(quoteAnswer > _minPrice, "Lower price bound breached");
```





## Assessed type

Oracle
Unset `deadline` allows for malicious swap executions.
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The protocol is using `block.timestamp` as the `deadline` argument while performing swaps, which completely defeats the purpose of using a `deadline`.

Failure to provide a proper `deadline` value enables pending transactions to be maliciously executed at a later point. Transactions that provide an insufficient amount of gas such that they are not mined within a reasonable amount of time, can be picked by `malicious actors` or `MEV` bots and executed later in detriment of the submitter.

## Proof of Concept
[swapExactIn](https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/settlements/UniswapSettlement.sol#L33-L35)
```solidity
        amountOut = _swapRouter.exactInput(
            ISwapRouter.ExactInputParams(data, recipient, block.timestamp, amountIn, amountOutMinimum)
        );
```

[swapExactOut](https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/settlements/UniswapSettlement.sol#L49-L51)
```solidity
        amountIn = _swapRouter.exactOutput(
            ISwapRouter.ExactOutputParams(data, recipient, block.timestamp, amountOut, amountInMaximum)
        );
```
Using `block.timestamp` as the `deadline` is effectively a no-operation that has no effect nor protection. Since `block.timestamp` will take the timestamp value when the transaction gets mined, the check will end up comparing `block.timestamp` against the same value.

See [this](https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/base/PeripheryValidation.sol#L7).

## Tools Used
Manual Review

## Recommended Mitigation Steps
Include a check to ensure that the current `block.timestamp` is less than or equal to the `deadline` specified in the parameters. If the current timestamp is greater than the `deadline`, the function should `revert` the transaction.





## Assessed type

Timing
Inadequate price validation in `getSqrtPrice()` results in usage of stale prices. 
Audit: 2024-05-predy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The `getSqrtPrice()` function does not sufficiently validate the price returned from chainlink's `latestRoundData()` as it only ensures that the price returned is a `positive non-zero` number but does not check for `staleness` of the feed. 

During feed staleness, the value returned is `> 0` (passing the check) but will remain the same even if the actual price has altered leading to incorrect prices being returned.

## Proof of Concept
[getSqrtPrice()](https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/PriceFeed.sol#L45-L58)

```solidity
    function getSqrtPrice() external view returns (uint256 sqrtPrice) {
        (, int256 quoteAnswer,,,) = AggregatorV3Interface(_quotePriceFeed).latestRoundData();
        //...
        require(quoteAnswer > 0 && basePrice.price > 0);

        //...
    }
```
This function only queries the answer(`quoteAnswer`) from the `latestRoundData()`. As such, there is no way to perform checks concerning the validity of the `price` returned by the oracle.

Due to this insufficient validation, `price` returned can be `stale` resulting in the protocol consuming such prices for various operations that could cause issues.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Validate data feed sufficiently:

```diff
    // @audit The threshold for the age of the price data (set as required)
+   uint256 public priceAgeThreshold = 2 hours;

    // @audit Updates the threshold for the age of the price data
+   function updatePriceAgeThreshold(uint256 _priceAgeThreshold ) external onlyOwner {
+       if (_priceAgeThreshold <= 1 hours || _priceAgeThreshold >= 10 hours) { // @audit Set your own as per requirement
+           revert Oracle_INVALID_INPUT();
+       }
+       priceAgeThreshold = _priceAgeThreshold ;
+   }


    function getSqrtPrice() external view returns (uint256 sqrtPrice) {
        // @audit First fetch the data
-       (, int256 quoteAnswer,,,) = AggregatorV3Interface(_quotePriceFeed).latestRoundData();
+       (, int256 quoteAnswer,, uint256 updatedAt,) = AggregatorV3Interface(_quotePriceFeed).latestRoundData();
        //...

+       if (block.timestamp - updatedAt > chainlinkPriceAgeThreshold) {
+           revert Oracle_DATA_OUT_OF_DATE();
+       }
        require(quoteAnswer > 0 && basePrice.price > 0);

        //...
    }
```











## Assessed type

Oracle