xanoutas/solidity-security-findings huggingface dataset:
Reallocation depends on the slot0 price, which can be manipulated.
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L206


# Vulnerability details

## Impact
Anyone can invoke the `reallocate` function to reallocate the LP position to be within the desired range. Whether reallocation is necessary depends on the slot0 price. However, the slot0 price can be manipulated, potentially leading to the LP position being out of the current range and resulting in a loss of yield for the protocol.

## Proof of Concept

reallocate first read slot0 [Perp.sol::reallocate](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L202-L206) to get current price:
```solidity
	function reallocate(
	    DataType.PairStatus storage _assetStatusUnderlying,
	    SqrtPerpAssetStatus storage _sqrtAssetStatus
	) internal returns (bool, bool, int256 deltaPositionBase, int256 deltaPositionQuote) {
>      (uint160 currentSqrtPrice, int24 currentTick,,,,,) = IUniswapV3Pool(_sqrtAssetStatus.uniswapPool).slot0();
```

and then compare current price with the threshold [Perp.sol::reallocate](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L236-L246):
```solidity
// if the current tick does reach the threshold, then rebalance
int24 tick;
bool isOutOfRange;

if (currentTick < _sqrtAssetStatus.tickLower) {
    // lower out
    isOutOfRange = true;
    tick = _sqrtAssetStatus.tickLower;
} else if (currentTick < _sqrtAssetStatus.tickUpper) {
    // in range
    isOutOfRange = false;
} else {
    // upper out
    isOutOfRange = true;
    tick = _sqrtAssetStatus.tickUpper;
}
```

Once out of range protocol invoke [Perp.sol::swapForOutOfRange](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L304-L326) to update rebalance position:


```solidity
    function swapForOutOfRange(
        DataType.PairStatus storage pairStatus,
        uint160 _currentSqrtPrice,
        int24 _tick,
        uint128 _totalLiquidityAmount
    ) internal returns (int256 deltaPositionBase, int256 deltaPositionQuote) {
        uint160 tickSqrtPrice = TickMath.getSqrtRatioAtTick(_tick);

        // 1/_currentSqrtPrice - 1/tickSqrtPrice
        int256 deltaPosition0 =
            LPMath.calculateAmount0ForLiquidity(_currentSqrtPrice, tickSqrtPrice, _totalLiquidityAmount, true);

        // _currentSqrtPrice - tickSqrtPrice
        int256 deltaPosition1 =
            LPMath.calculateAmount1ForLiquidity(_currentSqrtPrice, tickSqrtPrice, _totalLiquidityAmount, true);

        if (pairStatus.isQuoteZero) {
            deltaPositionQuote = -deltaPosition0;
            deltaPositionBase = -deltaPosition1;
        } else {
            deltaPositionBase = -deltaPosition0;
            deltaPositionQuote = -deltaPosition1;
        }

        updateRebalancePosition(pairStatus, deltaPosition0, deltaPosition1);
    }
```




## Tools Used
Foundry
## Recommended Mitigation Steps
it's recommend to use TWAP price instead of slot0 price to get the current price.


## Assessed
Missing deadline checks allow pending transactions to be maliciously executed
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/tree/main/src/settlements/UniswapSettlement.sol#L34
https://github.com/code-423n4/2024-05-predy/tree/main/src/markets/spot/SpotMarket.sol#L268


# Vulnerability details

## Impact
The UniswapSettlement contract does not allow users to submit a deadline for their actions which execute swaps on Uniswap V3. This missing feature enables pending transactions to be maliciously executed at a later point.

Automated Market Makers (AMMs) offer users the ability to limit the execution time of their pending actions, such as swaps or adding/removing liquidity, by including a deadline timestamp as a parameter. This feature is commonly implemented in platforms like Uniswap V2 and Uniswap V3. Without this option, users risk executing trades under unfavorable conditions:

For instance, Alice intends to swap 100 tokens for 1 ETH and then sell the 1 ETH for 1000 DAI. She submits the transaction to the mempool but selects a low transaction fee, making it unappealing to miners. As a result, her transaction remains pending in the mempool for an extended period—potentially hours, days, or even longer. When gas fees eventually drop, making her transaction viable for inclusion in a block, the swap is executed. However, by this time, the price of ETH may have changed significantly. Alice still receives 1 ETH, but its value in DAI might be much lower, resulting in a poor trade due to the delayed transaction she forgot about.

This situation can be further exploited through Miner Extractable Value (MEV):

While Alice's swap transaction is pending in the mempool, token prices may rise significantly. This means that when executed, Alice would receive more ETH. However, her transaction's maximum slippage parameters become outdated, allowing for substantial slippage. MEV bots can detect this pending transaction and exploit the outdated slippage settings by sandwiching Alice's transaction, leading to considerable profit for the bot and significant loss for Alice.

Given that Predy's market swaps are built directly on Uniswap V3, it should also provide deadline parameters to users for transactions involving swaps. Currently, no such deadline parameter is available. Although the slippage is [set in the contracts](https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/settlements/UniswapSettlement.sol#L27), but the deadline is set [to be `block.timestamp`](https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/settlements/UniswapSettlement.sol#L34). This simply means that it needs to be a quick swap and is considered to be executed at the moment of call. But we should take into consideration that this tx might be executed some times later than what was intended to be.

## Proof of Concept
```Solidity
    function swapExactIn(
        address,
        address baseToken,
        bytes memory data,
        uint256 amountIn,
        uint256 amountO
Usage of slot0 to get sqrtPrice can lead to price manipulation.
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/e96f378007e3dc56b184079f0c0e4fe48a72efaa/src/libraries/Perp.sol#L206


# Vulnerability details

## Vulnerability details

## Proof of Concept
When reallocating the Lp position slot0 is used as sqrtPrice in [reallocate](https://github.com/code-423n4/2024-05-predy/blob/e96f378007e3dc56b184079f0c0e4fe48a72efaa/src/libraries/Perp.sol#L206) function. 
```solidity

    function reallocate(
        DataType.PairStatus storage _assetStatusUnderlying,
        SqrtPerpAssetStatus storage _sqrtAssetStatus
    ) internal returns (bool, bool, int256 deltaPositionBase, int256 deltaPositionQuote) {
        (uint160 currentSqrtPrice, int24 currentTick,,,,,) = IUniswapV3Pool(_sqrtAssetStatus.uniswapPool).slot0();

```

Then sqrtPrice is used to calculate additional token amount required for rebalance [swapForOutOfRange()](https://github.com/code-423n4/2024-05-predy/blob/e96f378007e3dc56b184079f0c0e4fe48a72efaa/src/libraries/Perp.sol#L305) .

```solidity
       int256 deltaPosition0 =
            LPMath.calculateAmount0ForLiquidity(_currentSqrtPrice, tickSqrtPrice, _totalLiquidityAmount, true);

        // _currentSqrtPrice - tickSqrtPrice
        int256 deltaPosition1 =
            LPMath.calculateAmount1ForLiquidity(_currentSqrtPrice, tickSqrtPrice, _totalLiquidityAmount, true);
```

[slot0](https://docs.uniswap.org/contracts/v3/reference/core/interfaces/pool/IUniswapV3PoolState#slot0) is the most recent data point so extremely easy to manipulate.
So this is allows malicious user to manipulate the LP rebalance using flash loan.


## Impact
Malicious user able to manipulate the slot0 price using flash loan so that it caused LP position manipulation.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Use [Uniswap V3](https://docs.uniswap.org/concepts/protocol/oracle) TWAP instead of pool.slot0 for LP position rebalance


## Assessed type

Uniswap
Manipulation via `slot0`
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/Perp.sol#L206


# Vulnerability details

The use of `slot0` is not always advised as `slot0` is essentially the spot price of any given pool and can be easily manipulated using a flashloan to move the price to a new tick. In the Predy protocol, the attacker can leverage this and get a desired price return when `getSqrtPrice` during a trade. This can allow them to trade at an unfair advantage at the expense of the other users.

`tradeResult.sqrtPrice = getSqrtPrice(pairStatus.sqrtAssetStatus.uniswapPool, pairStatus.isQuoteZero);`

An attacker can also leverage this to cause excessive reallocations by moving the tick and then calling the `reallocate` function multiple times. This will move the tick range used by the specific pool and prevent the lenders from collecting fees while there is no trading at the current range.

`(uint160 currentSqrtPrice, int24 currentTick,,,,,) = IUniswapV3Pool(_sqrtAssetStatus.uniswapPool).slot0();`


## Impact
Trades and reallocations can both be manipulated to hurt liquidity providers of the protocol.

## Proof of Concept
1. Current tick is 0.
2. Pool's acceptable range is +/- 10.
3. Alice makes a large swap on the UniV3 pool that Predy references, moving the tick to -10,000.
4. Alice calls `reallocate` repeatedly until the tick is at -10,000.
5. Alice swaps back to bring the tick back to 0.

## Tools Used
Manual analysis
## Recommended Mitigation Steps
Where it is important that atomic manipulation should not be used, don't use `slot0` as a reference point.


## Assessed type

Oracle
Liquidators can bypass remaining negative margin check and leave the loss to the protocol
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/logic/LiquidationLogic.sol#L89-L108


# Vulnerability details

## Impact
- Liquidators can profit from protocol insolvency
## Description
### Background
At the end of liquidation process, in the case of no remaining position, `liquidate` function checks for remaining margin of the liquidating vault.  
If there is *positive* remaining amount, it proceeds to send those margin back to vault's recipient.  
On contrary, if there is `negative` remaining amount, liquidator must pay (compensate) for those negative amount.  

This mechanism exists to protect against protocol insolvency in the case that vault's margin *cannot* cover the loss from vault's position.  

### Liquidation incentives  
Liquidators close vault's position, settle the trade and profit from slippage tolerance, set by the pool's owner.  
For instance, if the slippage tolerance is set at 5% and let's say liquidator is closing this long position  
```
amountBase: 1 ETH
amountQuote: -3,000 USDC
entryValue: 3,000 USDC
currentPrice: 2,500 USDC
```
Liquidator will get 1 ETH to sell for 2,500 USDC and have to return only `2500*0.95=2,375` USDC. The remaining short USDC will be deducted from vault's margin, in this case, `3000-2375=625` USDC  

Considering this mechanism, liquidators will always profit from the slippage.  

### Vulnerability  
If we take a look at the snippet of the code responsible for the aforementioned protection mechanism.  
[LiquidationLogic.sol#L89-L108](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/logic/LiquidationLogic.sol#L89-L108)
```
if (!hasPosition) {
    int256 remainingMargin = vault.margin;

    if (remainingMargin > 0) {
        if (vault.recipient != address(0)) {
            // Send the remaining margin to the recipient.
            vault.margin = 0;

            sentMarginAmount = uint256(remainingMargin);

            ERC20(pairStatus.quotePool.token).safeTransfer(vault.recipient, sentMarginAmount);
        }
    } else if (remainingMargin < 0) {
        vault.margin = 0;

        // To prevent the liquidator from unfairly profiting through arbitrage trades in the AMM and passing losses onto the protocol,
        // any losses that cannot be covered by the vault must be compensated by the liquidator
        ERC20(pairStatus.quotePool.token).safeTransferFrom(msg.sender, address(this), uint256(-remainingMargin));
    }
}
```
`liquidation` function only checks for negative margin if and only if the position is fully closed from liquidation.  

Therefore, if the position is not fully closed (99.99% closed), and left with negative margin, liquidators don't have to compensate for the loss.  

Considering the case where liquidation of a full position would result in negative margin, liquidators should only be incentivized to partially close a position to the point that vault's margin becomes zero because that would be their maximum profit.  

However, with
ChainlinkOracle will return the wrong price for asset if underlying aggregator hits minAnswer
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary

Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened to [Venus on BSC when LUNA imploded](https://rekt.news/venus-blizz-rekt/).

## Vulnerability Detail

ChainlinkAdapterOracle uses the [ChainlinkFeedRegistry](https://etherscan.io/address/0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf) to obtain the price of the requested tokens.

```

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

        //@audit this pulls the Aggregator for the requested pair
        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);
    }

```

ChainlinkFeedRegistry#latestRoundData pulls the associated aggregator and requests round data from it. ChainlinkAggregators have minPrice and maxPrice circuit breakers built into them. This means that if the price of the asset drops below the minPrice, the protocol will continue to value the token at minPrice instead of it's actual value. This will allow users to take out huge amounts of bad debt and bankrupt the protocol.

Example: TokenA has a minPrice of $1. The price of TokenA drops to $0.10. The aggregator still returns $1 allowing the user to borrow against TokenA as if it is $1 which is 10x it's actual value.

Note: Chainlink oracles are used a just one piece of the OracleAggregator system and it is assumed that using a combination of other oracles, a scenario like this can be avoided. However this is not the case because the other oracles also have their flaws that can still allow this to be exploited. As an example if the chainlink oracle is being used with a UniswapV3Oracle which uses a long TWAP then this will be exploitable when the TWAP is near the minPrice on the way down. In a scenario like that it wouldn't matter what the third oracle was because it would be bypassed with the two matching oracles prices. If secondary oracles like Band are used a malicious user could DDOS relayers to prevent update pricing. Once the price becomes stale the chainlink oracle would be the only oracle left and it's price would be used.

## Impact
During partial liquidation protocol is left with bed debt that liquidator is not forced to paid up.
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/2fb1e0ec7a52fc06c2e9c8e561bccba84302e4bb/src/libraries/logic/LiquidationLogic.sol#L89-L89
https://github.com/code-423n4/2024-05-predy/blob/2fb1e0ec7a52fc06c2e9c8e561bccba84302e4bb/src/libraries/logic/LiquidationLogic.sol#L101-L107


# Vulnerability details

If a vault becomes insolvent, anyone can initiate its liquidation. However, if this process results in unpaid debts that exceed the vault's coverage, the liquidator may be obligated to cover these losses.

This condition applies only when the entire debt is repaid and the user's position is closed.

Any liquidator can exploit the ability to conduct partial liquidations, such as reducing a position by 99.99%, thereby avoiding triggering checks to cover potential losses.

## Impact

Liquidation events may leave protocols with bed debt.

## Proof of Concept

1. A user engages in a perpetual trade.
2. Market prices move unfavorably, causing the user's position to become insolvent.
3. The liquidator initiates a 99.99% reduction of the user's vault, resulting in significant slippage.
4. The remaining margin in the user's vault is not rechecked to ensure it is not negative.
5. By leaving the position partially open, the liquidator avoids the obligation to pay the premium.

## Recommended Mitigation Steps

During partial liquidations, ensure the user's remaining vault margin is not negative. If it is, enforce payment by the liquidator to prevent unpaid debts.


## Assessed type

Invalid Validation
Spot price is used for in-range liquidity reallocation, resulting in liquidity manipulation
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/main/src/PredyPool.sol#L251-L257
https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L206


# Vulnerability details

The spot price is used to calculate the in-range liquidity reallocation:

The Reallocator contract initiates the rebalancing process by calling [PredyPool::reallocate()](https://github.com/code-423n4/2024-05-predy/blob/main/src/PredyPool.sol#L251C1-L258C1), which in turn calls [ReallocationLogic::reallocate()](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/logic/ReallocationLogic.sol#L27).

This function takes global data, a pair ID, and settlement data as input. It then calls the [Perp contract's reallocate function](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/logic/ReallocationLogic.sol#L49) with relevant parameters and returns whether the in-range liquidity changed.

This contract [retrieves the `currentSqrtPrice` from the UniswapV3 pool's slot0 data](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L206).

[`currentSqrtPrice` is used if the current tick is out of range](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L254C7-L257C10). If so, the [Perp::swapForOutOfRange()](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L305) function is called to swap tokens and adjust the liquidity positions:

In [Perp::swapForOutOfRange()](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol#L305) the `_currentSqrtPrice` is used to calculate the `deltaPosition0` and the `deltaPosition1` through [LPMath::calculateAmount0ForLiquidity()](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/math/LPMath.sol#L30) and [LPMath::calculateAmount1ForLiquidity()](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/math/LPMath.sol#L68) functions.

The [Perp contract](https://github.com/code-423n4/2024-05-predy/blob/main/src/libraries/Perp.sol) doesn't have any checks against the price (it doesn't use any oracles nor the TWAP), so each token liquidity is manipulable through flash loans.

## Impact

The [Reallocation flow](https://github.com/code-423n4/2024-05-predy/blob/main/src/PredyPool.sol#L251C4-L257C6) in the Predy Pool contract is susceptible to manipulation due to the lack of any checks against the retrieved in-range liquidity. It doesn't utilize TWAP. Consequently, an attacker can manipulate the spot price (slot0) through a flash loan attack. This manipulation can lead to highly inaccurate results and potential loss of funds for the users.

## Proof of Concept

Coded PoC: https://gist.github.com/cholakovvv/43fa4630fc7f62b80b11075aea279e12

Basic flow of the test:
1. Alice wants to reallocate a specific pair.
2. After that, she simply calls [PredyPool::reallocate()](https://github.com/code-423n4/2024-05-predy/blob/main/src/PredyPool.sol#L251C4-L257C6).
3. A malicious user (Bob)
Users can call autoClose/autoHedge for other user position in GammaTradeMarket
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/main/src/markets/gamma/GammaTradeMarket.sol#L227
https://github.com/code-423n4/2024-05-predy/blob/main/src/markets/gamma/GammaTradeMarket.sol#L274


# Vulnerability details


## Impact

Users can call autoClose/autoHedge for other user position in GammaTradeMarket, and manipulate other users vault position.

## Bug Description

Here we take autoClose function as an example. It does not check for any signed messages with permit2, this means users can freely call this with positionId set to another user's vaultId to manipulate other user vaults.

They can manipulate settlementParams (used in swap) and mess with the vault value, such as reducing its position value (e.g. buying baseToken at a very high price).

```solidity
    function autoClose(uint256 positionId, SettlementParamsV3 memory settlementParams)
        external
        returns (IPredyPool.TradeResult memory tradeResult)
    {
        // save user position
        GammaTradeMarketLib.UserPosition memory userPosition = userPositions[positionId];

        if (userPosition.vaultId == 0 || positionId != userPosition.vaultId) {
            revert PositionNotFound();
        }

        // check auto close condition
        uint256 sqrtPrice = _predyPool.getSqrtIndexPrice(userPosition.pairId);

        (bool closeRequired, uint256 slippageTorelance, GammaTradeMarketLib.CallbackType triggerType) =
            GammaTradeMarketLib.validateCloseCondition(userPosition, sqrtPrice);

        if (!closeRequired) {
            revert AutoCloseTriggerNotMatched();
        }

        // execute close
        DataType.Vault memory vault = _predyPool.getVault(userPosition.vaultId);

        IPredyPool.TradeParams memory tradeParams = IPredyPool.TradeParams(
            userPosition.pairId,
            userPosition.vaultId,
            -vault.openPosition.perp.amount,
            -vault.openPosition.sqrtPerp.amount,
            abi.encode(CallbackData(triggerType, userPosition.owner, 0))
        );

        if (tradeParams.tradeAmount == 0 && tradeParams.tradeAmountSqrt == 0) {
            revert AlreadyClosed();
        }

        tradeResult = _predyPool.trade(tradeParams, _getSettlementDataFromV3(settlementParams, msg.sender));

        SlippageLib.checkPrice(sqrtPrice, tradeResult, slippageTorelance, SlippageLib.MAX_ACCEPTABLE_SQRT_PRICE_RANGE);
    }
```

## Proof of Concept

N/A

## Tools Used

Manual review

## Recommended Mitigation Steps

Add a signature check in autoClose/autoHedge.


## Assessed type

Access Control