xanoutas/solidity-security-findings huggingface dataset:
Liquidators do not need to pay off bad debt when liquidating a portion of the position
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#L62-L63


# Vulnerability details


## Impact

Liquidators do not need to pay off bad debt when liquidating a portion of the position. This will incentivize users to only liquidate 99.9999% of the vault and profiting from the better price, while leaving the bad debt to the last liquidator. And since there is no incentive to liquidate the last bit of the position, no one will perform the liquidation, which will always lead to bad debt for the protocol.

## Bug Description

Let's see how the liquidation process works:

1. The liquidator passes in a `closeRatio`, this is the percentage of the vault he wishes to liquidate.
2. The liquidator can perform a trade with a price he sets, as long as it is within the slippage limit. The liquidator benefits from this swap, since he can buy/sell baseToken lower/higher than the market price.
3. If `hasPosition == false`, and the remaining margin is positive, the margin is sent back to the recipient. However, if the remaining margin is negative, the last liquidator would need to pay off the bad debt.

The issue here is the liquidator can always just liquidate 99.9999% percentage of the position, and not paying for the bad debt at all. This would leave the bad debt for the last liquidator, which no one would want to be. Ultimately, the bad debt is left for the protocol.

Another scenario is that a user can open a position with high leverage (e.g. long baseToken with 10x leverage), if it turns insolvent, he can simply liquidate himself, and sell the token at a higher price (may be even higher than when he longed the token), and leaving the bad debt to the protocol.

```solidity
    function liquidate(
        uint256 vaultId,
>       uint256 closeRatio,
        GlobalDataLibrary.GlobalData storage globalData,
        bytes memory settlementData
    ) external returns (IPredyPool.TradeResult memory tradeResult) {
        require(closeRatio > 0 && closeRatio <= 1e18, "ICR");
        DataType.Vault storage vault = globalData.vaults[vaultId];
        DataType.PairStatus storage pairStatus = globalData.pairs[vault.openPosition.pairId];

        // update interest growth
        ApplyInterestLib.applyInterestForToken(globalData.pairs, vault.openPosition.pairId);

        // update rebalance interest growth
        Perp.updateRebalanceInterestGrowth(pairStatus, pairStatus.sqrtAssetStatus);

        // Checks the vault is danger
        (uint256 sqrtOraclePrice, uint256 slippageTolerance) =
            checkVaultIsDanger(pairStatus, vault, globalData.rebalanceFeeGrowthCache);

        IPredyPool.TradeParams memory tradeParams = IPredyPool.TradeParams(
            vault.openPosition.pairId,
            vaultId,
>           -vault.openPosition.perp.amount * int256(closeRatio) / 1e18,
>           -vault.openPosition.sqrtPerp.amount * int256(closeRatio) / 1e18,
            ""
        );

        tradeResult = Trade.
Liquidation incorrectly tries to transfer token from Market instead of liquidator if remainingMargin is negative
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#L106


# Vulnerability details


## Impact

Liquidation cannot be executed if there is bad debt (vault position is negative) after liquidation.

## Bug Description

See the liquidation flowchart here: https://docs.predy.finance/predy-v6/dev/architecture/flowchart#execliquidationcall

The liquidator would call a Market contract, which triggers the liquidation logic in PredyPool. Then tokens would then be swapped accordingly using the `SettlementCallbackLib`.

The issue is during the end of liquidation, if the position is entirely wiped out (e.g. `hasPosition == false` in the following code), the code checks whether there is still margin left in the vault.

1. If there is still margin left, the remaining margin would be sent to the `vault.recipient` address.
2. If there is bad debt (negative margin value), the liquidator must pay for this.

For case 2, the code tries to transfer token from `msg.sender` to PredyPool. However, the `msg.sender` is the Market protocol, and not the liquidator. This means liquidating a vault with negative margin is impossible, and this bad debt will never be cleared. What's worse, the lending fees would still accumulate for this vault, and the bad debt keeps getting larger.

```solidity
    function liquidate(
        uint256 vaultId,
        uint256 closeRatio,
        GlobalDataLibrary.GlobalData storage globalData,
        bytes memory settlementData
    ) external returns (IPredyPool.TradeResult memory tradeResult) {
        ...
        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));
            }
        }
        ...
    }
```

## Proof of Concept

N/A

## Tools Used

Manual review

## Recommended Mitigation Steps

Transfer quoteTokens from the liquidator instead of the Market protocol.


## Assessed type

Token-Transfer
Gamma trading may be blocked when there is not enough liquidity
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Traders' gamma position may not be closed because there is not enough liquidity to burn. Traders may face more risk because they cannot close their liquidity as they expect.

## Proof of Concept
When traders open one gamma position with one positive `_tradeSqrtAmount`, predy protocol will mint some uniswap v3 liquidity according to the related pair's configuration. If traders open one gamma position with one negative `_tradeSqrtAmount`, predy protocol will burn some uniswap v3 pool liquidities.

The vulnerability is that one trader can burn other traders' minted liquidity when the trader open one gamma position with one negative `_tradeSqrtAmount`.

For example:
1. Alice opens one gamma position with +100 `_tradeSqrtAmount`, Assume we will mint 100 uniswap v3 liquidity.
2. Bob opens one gamma position with -50 `_tradeSqrtAmount`, predy pool will burn 50 liquidity.
3. If Alice wants to close her position, predy pool expects to burn 100 uniswap v3 liquidities and will be reverted because there is not enough liquidity left in predy pool.

```javascript
    function computeRequiredAmounts(
        SqrtPerpAssetStatus storage _sqrtAssetStatus,
        bool _isQuoteZero,
        UserStatus memory _userStatus,
        int256 _tradeSqrtAmount
    ) internal returns (int256 requiredAmountUnderlying, int256 requiredAmountStable) {
        ......
        int256 requiredAmount0;
        int256 requiredAmount1;
        if (_tradeSqrtAmount > 0) {
            (requiredAmount0, requiredAmount1) = increase(_sqrtAssetStatus, uint256(_tradeSqrtAmount));
            if (_sqrtAssetStatus.totalAmount == _sqrtAssetStatus.borrowedAmount) {
                // if available liquidity was 0 and added first liquidity then update last fee growth
                saveLastFeeGrowth(_sqrtAssetStatus);
            }
        } else if (_tradeSqrtAmount < 0) {
            (requiredAmount0, requiredAmount1) = decrease(_sqrtAssetStatus, uint256(-_tradeSqrtAmount));
        }
```

### Poc
Add this test in ExecuteOrder.t.sol, and this test case will be reverted because there is not enough liquidity.
```javascript
    function testExecuteOrderSucceedsForOpen() public {
        GammaOrder memory order =
            _createOrder(from1, 0, block.timestamp + 100, 1, 0, 0, 1000, 2 * 1e6, Constants.Q96);
        //IPredyPool.TradeResult memory tradeResult = gammaTradeMarket.executeTrade(order, _sign(order, fromPrivateKey1), _getSettlementDataV3(Constants.Q96));
        IPredyPool.TradeResult memory tradeResult = gammaTradeMarket.executeTrade(order, _sign(order, fromPrivateKey1), _getSettlementDataV3(Constants.Q96));

        order = _createOrder(from2, 1, block.timestamp + 100, 1, 0, 0, -500, 2 * 1e6, Constants.Q96);
        tradeResult = gammaTradeMarket.executeTrade(order, _sign(order, fromPrivateKey2), _getSettlemen
signature replay to steal traders' fund via permit2
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/markets/perp/PerpMarketV1.sol#L327-L338


# Vulnerability details

## Impact
Hackers can make use of trader' signed signature to transfer traders' tokens to hack themselves.

## Proof of Concept
Predy protocol makes use of permit2 to support gas-free trading. Traders will hash their order and sign one signature to the filler. The filler role will trigger the trade and transfer traders' token via permit2 with the correct signatures.

Considering that fillers get traders' signature off-chain, and predy protocol will be deployed on Layer2 chain, hackers cannot receive the correct and unused signature information on-chain before the signature is used.

However, there are some scenarios that hackers can get one correct and unused signature.
1. When the filler triggers the trade, and the trade transaction is reverted because of some unexpected condition, for example, current price doesn't match the limited price and so on.
2. Predy protocol will be deployed in several Layer2 chains. Hackers can make cross-chain signature replay. For example, hackers can find one valid signature in Arb chain and try to use it on Op chain.

In permit2, function `permitWitnessTransferFrom` does not limit the transfer's destination. Because the `to` address does not belong to one part of hash. Once hackers get one valid and unused and not-expired signature, hackers can transfer tokens directly via `permitWitnessTransferFrom`.
```javascript
    struct SignatureTransferDetails {
        // recipient address
        address to;
        // spender requested amount
        uint256 requestedAmount;
    }
    function permitWitnessTransferFrom(
        PermitTransferFrom memory permit,
        SignatureTransferDetails calldata transferDetails,
        address owner,
        bytes32 witness,
        string calldata witnessTypeString,
        bytes calldata signature
    ) external {
        _permitTransferFrom(
            permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature
        );
    }
```

## Tools Used
Manual

## Recommended Mitigation Steps
Consider to use permit2's permit()/transferFrom() function. 



## Assessed type

Error
It is possible to block stablecoin (USDC, USDT) swaps in the UniswapSettlement contract
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The `UniswapSettlement` contract will not execute swaps with USDC and USDT tokens.

## Proof of Concept
In this report we'd like to escalate the bot finding L-01:
https://github.com/code-423n4/2024-05-predy/blob/main/4naly3er-report.md#l-1-approvesafeapprove-may-revert-if-the-current-approval-is-not-zero

Let's check the `swapExactOut` function in the `UniswapSettlement` contract:
```solidity
    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);
        }
    }
```
We can notice two things:
1. `amountInMaximum` is approved to the router
2. it is expected that in some swaps not all tokens will be consumed by the router

This means that a non-zero allowance may be left. This is unacceptable for USDC(USDT) tokens and the next swap will fail due to `approve` transaction revert.

Note, that you cannot swap 0 tokens in the Uniswap, so it won't be possible to approve 0 tokens with `swapExactIn`:
https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L596-L603
```solidity
    function swap(
        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external override noDelegateCall returns (int256 amount0, int256 amount1) {
>>      require(amountSpecified != 0, 'AS');
``` 

And even if somehow allowance was set to zero, an attacker can easily block the contract again.

Since the `UniswapSettlement` contract and stablecoins are widely used in the perp operations, we'd like to draw sponsor's attention to this issue and increase it's severity.

## Tools Used
Manual review

## Recommended Mitigation Steps
Set allowance to zero after the swap.


## Assessed type

DoS
Gamma trading may be blocked because liquidity is reallocated.
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Function `reallocate` may burn current position's liquidity and mint liquidity in one new position. Gamma trading may be reverted because there is not enough liquidity to burn.

## Proof of Concept
When one pair meets the allocation condition, the pair's liquidities in Uniswap v3 pool will be burned and minted in one new position based on current tick and range.

The vulnerability exists in function `getAvailableLiquidityAmount`. When we calculate the liquidities, we get liquidities via position key `_controllerAddress, _tickLower, _tickUpper`. The issue is that it's quite probable that several pairs have the same `_tickLower` and `_tickUpper`. 

For example: 
PairA and PairB share the same `_tickLower` and `_tickUpper`. When PairA match the allocation condition, all liquidities belong to both PairA and PairB will be allocated to the new position. 
Now if one trader wants to trade in PairB and need to burns some liquidities, the operation will be reverted because there is zero balance in current position.

```javascript
    function reallocate(
        DataType.PairStatus storage _assetStatusUnderlying,
        SqrtPerpAssetStatus storage _sqrtAssetStatus
    ) internal returns (bool, bool, int256 deltaPositionBase, int256 deltaPositionQuote) {
        // get related uniswap v3 pool' currentTick
        (uint160 currentSqrtPrice, int24 currentTick,,,,,) = IUniswapV3Pool(_sqrtAssetStatus.uniswapPool).slot0();
        ......
        uint128 totalLiquidityAmount = getAvailableLiquidityAmount(
            address(this), _sqrtAssetStatus.uniswapPool, _sqrtAssetStatus.tickLower, _sqrtAssetStatus.tickUpper
        );
        ......
        rebalanceForInRange(_assetStatusUnderlying, _sqrtAssetStatus, currentTick, totalLiquidityAmount);
}

```
```javascript
    function getAvailableLiquidityAmount(
        address _controllerAddress,
        address _uniswapPool,
        int24 _tickLower,
        int24 _tickUpper
    ) internal view returns (uint128) {
        bytes32 positionKey = PositionKey.compute(_controllerAddress, _tickLower, _tickUpper);

        (uint128 liquidity,,,,) = IUniswapV3Pool(_uniswapPool).positions(positionKey);

        return liquidity;
    }
```

## Tools Used
Manual

## Recommended Mitigation Steps
When we allocate one pair, we should burn/mint the liquidities belong to this pair.





## Assessed type

DoS
Liquidation process may be reverted because of slipage control
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/SlippageLib.sol#L21-L44


# Vulnerability details

## Impact
Liquidation process may be reverted when the difference between TWAP price and actual price is larger than slippage control.

## Proof of Concept
When one position is not safe, the liquidator will liquidate this position. When we liquidate one position, we will check the difference between the actual price and twap price. If the difference is larger than the `slippageTolerance`, the liquidation will be reverted because the system thinks the liquidator takes too much profit.

The vulnerability is that in some extreme market condition, some base tokens(eg, ARB) may increase or drop rapidly. This will cause the difference between the actual price and the TWAP(30minites) price is large. If some positions are unsafe, liquidation process may be reverted because of the slippage control.

The slippage control aims to prevent the liquidator to gain too much profits via arbitrage. However, this slippage control might block some normal case.

 
```javascript
    function liquidate(
        uint256 vaultId,
        uint256 closeRatio,
        GlobalDataLibrary.GlobalData storage globalData,
        bytes memory settlementData
    ) external returns (IPredyPool.TradeResult memory tradeResult) {
        ......
        // Check if the price is within the slippage tolerance range to ensure that the price does not become
        // excessively favorable to the liquidator.
        // sqrtOraclePrice --> twap price
        SlippageLib.checkPrice(
            sqrtOraclePrice,
            tradeResult,
            slippageTolerance,
            tradeParams.tradeAmountSqrt == 0 ? 0 : _MAX_ACCEPTABLE_SQRT_PRICE_RANGE
        );

```
### Poc
This will be reverted because of `Reason: SlippageTooLarge()`
```javascript
    function testLiquidateSucceedsWithInsolvent() public {
        IPredyPool.TradeParams memory tradeParams =
            IPredyPool.TradeParams(1, 0, -48 * 1e7, 0, abi.encode(_getTradeAfterParams(1e8)));

        _tradeMarket.trade(tradeParams, _getSettlementData(Constants.Q96));

        _movePrice(true, 8 * 1e16);

        vm.warp(block.timestamp + 1 minutes);

        _movePrice(true, 2 * 1e16);

        vm.warp(block.timestamp + 29 minutes);
        _tradeMarket.execLiquidationCall(1, 1e18, _getSettlementData(Constants.Q96 * 13000 / 10000));
        //checkMarginEqZero(1);
    }
```
## Tools Used
Manual

## Recommended Mitigation Steps
Bring chainlink oracle to calculate the slippage control.





## Assessed type

DoS
Liquidators may get more profits than expected
Audit: 2024-05-predy-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-predy/blob/a9246db5f874a91fb71c296aac6a66902289306a/src/libraries/logic/LiquidationLogic.sol#L39-L109


# Vulnerability details

## Impact
Liquidators may gain more profits and pass loss to the protocol.

## Proof of Concept
When one position is not safe, liquidators can liquidate this position. If this position's left margin is negative, the liquidator needs to cover these negative margin. This aims to avoid the liquidator gain unfairly profit and pass loss to the protocol.
But the vulnerability is that this mechanism only works when the liquidator liquidates the whole position. It means if the liquidator liquidates 99% of the unsafe position, the liquidator can bypass the above mechanism's check and gain more profit.
```javascript
    function liquidate(
        uint256 vaultId,
        uint256 closeRatio,
        GlobalDataLibrary.GlobalData storage globalData,
        bytes memory settlementData
    ) external returns (IPredyPool.TradeResult memory tradeResult) {
        require(closeRatio > 0 && closeRatio <= 1e18, "ICR");
        ......
        uint256 sentMarginAmount = 0;
        // hasPosition means the position is empty.
        if (!hasPosition) {
            int256 remainingMargin = vault.margin;
            if (remainingMargin > 0) {
                ......
            } 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));
            }
        }
```
### Poc
In below case, when we liquidate 100% position, this liquidation process will be reverted. But when we liquidate 99% position, the liquidation process can work.
```javascript
    function testLiquidateSucceedsWithInsolvent() public {
        IPredyPool.TradeParams memory tradeParams =
            IPredyPool.TradeParams(1, 0, -48 * 1e7, 0, abi.encode(_getTradeAfterParams(1e8)));

        _tradeMarket.trade(tradeParams, _getSettlementData(Constants.Q96));

        _movePrice(true, 8 * 1e16);

        vm.warp(block.timestamp + 1 minutes);

        _movePrice(true, 2 * 1e16);

        vm.warp(block.timestamp + 29 minutes);
        console.log("Trigger liquidate");
        _tradeMarket.execLiquidationCall(1, 0.99e18, _getSettlementData(Constants.Q96 * 12500 / 10000));
        //checkMarginEqZero(1);
    }
```
## Tools Used
Manual

## Recommended Mitigation Steps
Even if this position is not completed closed, we need to check whether the left margin is negative to prevent the liquidators gain too much unexpected profits.







## Assessed type

Context