xanoutas/solidity-security-findings huggingface dataset:
Potential loss of funds due to inflexible liquidation process in CDPVault
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L579


# Vulnerability details

## Impact

The `liquidatePositionBadDebt` function in the CDPVault contract has a critical flaw that can lead to failed liquidations or loss of funds for liquidators. The function requires the liquidator to provide an exact amount of repayment, which is impractical in real-world scenarios due to potential price fluctuations between transaction submission and execution. If the liquidator sends more than the required amount, the excess is effectively lost as there's no refund mechanism.

**This issue can result in:**
1. Failed liquidations if the collateral value changes slightly.
2. Loss of funds for liquidators if they overpay.
3. Reduced willingness of liquidators to participate, potentially destabilizing the system.


## Proof of Concept

The problematic code is in the `liquidatePositionBadDebt` function:

[liquidatePositionBadDebt](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L579)
```solidity
function liquidatePositionBadDebt(address owner, uint256 repayAmount) external whenNotPaused {
    // ... (some code omitted for brevity)
    
    uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount);
    if (calcTotalDebt(debtData) <= wmul(position.collateral, discountedPrice)) revert CDPVault__noBadDebt();
    
    uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
    if (takeCollateral < position.collateral) revert CDPVault__repayAmountNotEnough();

    takeCollateral = position.collateral;
    repayAmount = wmul(takeCollateral, discountedPrice);
    // ... (rest of the function)
}
```

The issue is done in
 ```
if (takeCollateral < position.collateral) revert CDPVault__repayAmountNotEnough();
```
because:

- The function calculates takeCollateral based on the repayAmount provided by the liquidator.
- It then checks if takeCollateral is less than the position's collateral, reverting if true.
- If the check passes, it sets takeCollateral to the position's full collateral amount.
Finally, it recalculates repayAmount based on the full collateral.

This means that the liquidator must provide a repayAmount that, when divided by the discounted price, results in a value greater than or equal to the position's collateral. Any excess amount sent by the liquidator is effectively lost.


## Tools Used
Manual analysis


## Recommended Mitigation Steps

- Modify the function to accept a maximum repay amount from the liquidator, this is just an examlpe, please adjust it to fit into the code correctly.
```
function liquidatePositionBadDebt(address owner, uint256 maxRepayAmount) external whenNotPaused {
    // ... (existing code)
    
    uint256 requiredRepayAmount = wmul(position.collateral, discountedPrice);
    if (maxRepayAmount < requiredRepayAmount) revert CDPVault__repayAmountNotEnough();
    
    uint256 actua
Liquidity pool is not adjusting the interest rate on healthy repayment profit
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/PoolV3.sol#L567


# Vulnerability details

## Impact
Whenever liquidity moves into/from the liquidity pool, the interest rate is adjusted to reflect this movement, by adjusting the interest rate. This is done by calling `_updateBaseInterest`, which is being called on deposit and withdrawal. The interest rate is also being updated on the pool's profit and loss, which makes sense as they're still considered liquidity movement.

When the liquidation penalty goes to the pool, `mintProfit` is called and the rate is adjusted according to that profit. Another way where profit/loss could come in is through `repayCreditAccount`, which repays a loan, however, this function called `_updateBaseInterest`:
```solidity
_updateBaseInterest({
    expectedLiquidityDelta: -loss.toInt256(),
    availableLiquidityDelta: 0,
    checkOptimalBorrowing: false
}); // U:[LP-14B,14C,14D]
```

The above is just adjusting the interest rate of the loss and not registering it for the profit (the profit is being minted to the treasury using `_mint` and `mintProfit`).

This affects the interest rate of all of the incoming loans, where it should be increased by every profit. With the current implementation, it'll be decreased with every loss but not increased with every profit.

## Proof of Concept

Add the following in `src/test/unit/CDPVault.t.sol`:

```solidity
function test_poolNotRegisteringProfit() public {
    CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 1.0 ether, 0);
    createGaugeAndSetGauge(address(vault));
    address position = address(new PositionOwner(vault));

    uint256 depositAmount = 100 ether;
    uint256 borrowAmount = 80 ether;

    token.mint(address(this), depositAmount);
    token.approve(address(vault), depositAmount);
    underlyingToken.mint(address(this), depositAmount);
    underlyingToken.approve(address(vault), depositAmount);

    uint256 initialInterestRate = vault.pool().baseInterestRate();

    // Deposit collateral
    vault.deposit(position, depositAmount);

    // Borrow
    vault.borrow(address(this), position, borrowAmount);

    // Month passed
    vm.warp(block.timestamp + 30 days);

    uint256 debt = vault.virtualDebt(position);

    // Interest accumulated, the debt is now > 80 ether
    assertGt(debt, borrowAmount);

    // Repay, the protocol should accumulate some profit
    vault.repay(address(this), position, debt);

    // The interest stayed the same, even though it should be higher
    assertEq(vault.pool().baseInterestRate(), initialInterestRate);
}
```

## Tools Used

Manual review

## Recommended Mitigation Steps

Register incoming profit in `PoolV3::repayCreditAccount`:

```diff
function repayCreditAccount(
    uint256 repaidAmount,
    uint256 profit,
    uint256 loss
)
    external
    override
    creditManagerOnly // U:[LP-2C]
    whenNotPaused // U:[LP-2A]
    nonReentrant // U:[LP-2B]
{
    ...

    _updateBaseInterest({
Incorrect position passed in `PositionAction4626::_onDecreaseLever` forcing it to always revert
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/proxy/PositionAction4626.sol#L141


# Vulnerability details

## Impact
When the collateral used in a vault is an ERC4626 token, users can use the `PositionAction4626` to interact with their positions, and users can deposit/withdraw/leverage their positions. They can call `PositionAction4626::decreaseLever`, which takes a flash loan of the "borrow" tokens, repays the position's debt, and swaps part of the collateral to repay the flash loan. Along the way, `PositionAction4626::_onDecreaseLever` gets called to withdraw the collateral from the vault, this is done using the:
```solidity
uint256 withdrawnCollateral = ICDPVault(leverParams.vault).withdraw(address(this), subCollateral);
```
Note that `to` is set to `address(this)` which is NOT the user's proxy but the position action contract, as this is the flash loan callback and the user's delegated call.

On the other hand, in `CDPVault`:
```solidity
function withdraw(address to, uint256 amount) external whenNotPaused returns (uint256 tokenAmount) {
    tokenAmount = wdiv(amount, tokenScale);
    int256 deltaCollateral = -toInt256(tokenAmount);
    modifyCollateralAndDebt({
        owner: to,
        collateralizer: msg.sender,
        creditor: msg.sender,
        deltaCollateral: deltaCollateral,
        deltaDebt: 0
    });
}
```
It sets the owner of the position as `to`, which in our case is wrong, as the position action doesn't have any active positions but the user's proxy has.

This will cause it to always revert, blocking users from the ability to decrease the leverage of their ERC4626 positions.

## Proof of Concept

```solidity
contract PositionAction4626_Lever_Test is IntegrationTestBase {
    using SafeERC20 for ERC20;

    PRBProxy userProxy;
    address user;
    CDPVault vault;
    StakingLPEth stakingLPEth;
    PositionAction4626 positionAction;
    PermitParams emptyPermitParams;
    SwapParams emptySwap;
    PoolActionParams emptyPoolActionParams;

    bytes32[] weightedPoolIdArray;

    address constant wstETH_bb_a_WETH_BPTl = 0x41503C9D499ddbd1dCdf818a1b05e9774203Bf46;
    address constant wstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
    address constant bbaweth = 0xbB6881874825E60e1160416D6C426eae65f2459E;
    bytes32 constant poolId = 0x41503c9d499ddbd1dcdf818a1b05e9774203bf46000000000000000000000594;

    function setUp() public override {
        super.setUp();
        setGlobalDebtCeiling(15_000_000 ether);

        token = ERC20PresetMinterPauser(wstETH);

        stakingLPEth = new StakingLPEth(address(token), "Staking LP ETH", "sLPETH");
        stakingLPEth.setCooldownDuration(0);
        vault = createCDPVault(stakingLPEth, 5_000_000 ether, 0, 1.25 ether, 1.0 ether, 1.05 ether);
        createGaugeAndSetGauge(address(vault), address(stakingLPEth));

        user = vm.addr(0x12341234);
        userProxy = PRBProxy(payable(address(prbProxyRegistry.deployFor(user))));

        positionAction
Incorrect Collateral Transfer Calculation in Liquidation Function Allows Liquidators to Avoid Penalty and Accumulate bad debts in the protocol
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol?plain=1#L529


# Vulnerability details

## Incorrect Collateral Transfer Calculation in Liquidation Function Allows Liquidators to Avoid Penalty

## Inpact

The `CDPVault::liquidatePosition` function is designed to liquidate unsafe positions by selling collateral to the liquidator. The liquidator provides an amount they wish to repay, and a penalty is supposed to be subtracted from this amount. However, due to an incorrect calculation, the penalty is not effectively enforced, potentially leaving positions in bad debt.





``` js
        // compute collateral to take, debt to repay and penalty to pay
        uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
        
        uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty);
        uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);

```

Here, the amount of debt effectively repaid is `deltaDebt` (which is repayAmount - penalty). However, the collateral amount sent to the liquidator is calculated based on the full `repayAmount` rather than the actual `deltaDebt`.

This discrepancy allows the liquidator to avoid paying the penalty, undermining the intended penalty mechanism and risking the creation of bad debt within the system.



## Proof of concept


**Scenario**

1. Initial Positon : 

    * collateral : 100 ether

    * Debt : 80 ether

    * Spot Price : 1 ether
    

2. After Spot Price Update :

    *  New spot price : 0.80 ether 

    * Position Status : Unsafe but not in bad debt
    

3. Liquidation Parameters : 

    * Repay Amount : 80 ether

    * Penalty : 10%


4. Expected Result After Liquidation :

    * collateral = 0 ether

    * debt = 8 ether



### test function

```js 

       function test_liquidate_custom() public {
       // Deploy CDPVault with initial parameters        
CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 0.9 ether, 1 ether);
        createGaugeAndSetGauge(address(vault));

         // Create a position with 100 ether collateral and 80 ether debt
        _modifyCollateralAndDebt(vault, 100 ether, 80 ether);
        
       // liquidate position
        address position = address(this);
        uint256 repayAmount = 80 ether;
        
        // Update spot price to 0.80 ether
        _updateSpot(0.80 ether);
        
        // Approve repayment amount
        mockWETH.approve(address(vault), repayAmount);
        
        vault.liquidatePosition(position, repayAmount);

        // Fetch the updated position
        (uint256 collateral,uint256 debt, , , , ) = vault.positions(position);

        // Assert the expected results
        assertEq(debt, 8 ether ); 
        assertEq(collateral, 0 ether);
    }


```

## Tools Used

manual review, foundry tests


## Recommended Mitigation Steps


1. Delta debt represents the portion of the repay amount that effectively reduc
Missing profit when function liquidatePositionBadDebt invoking pool.repayCreditAccount will cause less profit minted to the treasury
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L624


# Vulnerability details

## Impact

Less profit will be minted to the treasury.

## Proof of Concept

In the contract `CDPVault.sol`, the function `liquidatePositionBadDebt` invokes `pool.repayCreditAccount` with zero value of profit. The calculation of loss is `loss = calcTotalDebt(debtData) - repayAmount` which is equivalent to the `loss = debtData.debt + debtData.accruedInterest - repayAmount`. Therefore, the amount that the liquidator repaid to the pool is `repayAmount = debtData.debt + debtData.accruedInterest - loss`, which means the liquidator actually repaid the `debtData.accruedInterest`. However, the invocation of the `pool.repayCreditAccount(debtData.debt, 0, loss)` didn't take `debtData.accruedInterest` into consideration, which will cause less profit minted to the treasury.

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L579

```solidity
    function liquidatePositionBadDebt(address owner, uint256 repayAmount) external whenNotPaused {
        ...
        uint256 loss = calcTotalDebt(debtData) - repayAmount;
        // transfer the repay amount from the liquidator to the vault
        poolUnderlying.safeTransferFrom(msg.sender, address(pool), repayAmount);
        ...
        pool.repayCreditAccount(debtData.debt, 0, loss); // U:[CM-11]
        // transfer the collateral amount from the vault to the liquidator
        ...
    }
```


As a comparison, in the function `modifyCollateralAndDebt`, the invocation of the `pool.repayCreditAccount(debtData.debt - newDebt, profit, 0)` is correct. For example, when the repayAmount equals `maxRepayment`,
the calculation complies with the formula `repayAmount = deltaDebt + profit - loss`, where the `deltaDebt` is `debtData.debt - newDebt`, the `profit` is `debtData.accruedInterest` and the `loss` is `0`.

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L367

```solidity
    function modifyCollateralAndDebt(
        ...
    ) public {
        ....
        if (deltaDebt > 0) {
            ...
        } else if (deltaDebt < 0) {
            uint256 maxRepayment = calcTotalDebt(debtData);
            ...
            poolUnderlying.safeTransferFrom(creditor, address(pool), amount);

            ...
            if (amount == maxRepayment) {
                newDebt = 0;
                newCumulativeIndex = debtData.cumulativeIndexNow;
                profit = debtData.accruedInterest;
                newCumulativeQuotaInterest = 0;
            } else {
                (newDebt, newCumulativeIndex, profit, newCumulativeQuotaInterest) = calcDecrease(
                    amount, // delta debt
                    position.debt,
                    debtData.cumulativeIndexNow, // current cumulative base interest index in Ray
                    position.cumulativeIndexLastUpdate,
                    debtData.cumulativeQuotaInterest
                );
            }
Using TWAP Instead of Spot Price (Potential Oracle Failure + Inaccurate Liquidations)
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L522


# Vulnerability details

## Summary:

The liquidatePosition function currently uses the spot price to calculate the discounted price for liquidation. If the oracle providing the spot price fails or is manipulated, it can lead to inaccurate or unfair liquidations.

## Description:

The liquidatePosition function relies on the spotPrice() method to get the current price of the asset being liquidated. This spot price is then used to calculate the discounted price at which collateral is sold. However, spot prices can be volatile and subject to manipulation or failure of the oracle. Using a Time-Weighted Average Price (TWAP) instead can mitigate these risks by providing a more stable and manipulation-resistant price.

## Impact:

If the spot price is inaccurate or manipulated, it can lead to:

  * Liquidations happening at incorrect prices, resulting in unfair losses for the position owners.
  * Increased risk of oracle manipulation attacks.
  * Potentially significant financial losses for users and the system.

## Root Cause:

The root cause is the reliance on a potentially volatile and manipulable spot price for critical liquidation operations, rather than using a more stable TWAP.

## Proof of Concept:

```
/// @notice Liquidates a single unsafe position by selling collateral at a discounted (`liquidationDiscount`)
/// oracle price. The liquidator has to provide the amount he wants to repay or sell (`repayAmounts`) for
/// the position. From that repay amount a penalty (`liquidationPenalty`) is subtracted to mitigate against
/// profitable self liquidations. If the available collateral of a position is not sufficient to cover the debt
/// the vault accumulates 'bad debt'.
/// @dev The liquidator has to approve the vault to transfer the sum of `repayAmounts`.
/// @param owner Owner of the position to liquidate
/// @param repayAmount Amount the liquidator wants to repay [wad]
function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused {
    // validate params
    if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters();

    // load configs
    VaultConfig memory config = vaultConfig;
    LiquidationConfig memory liqConfig_ = liquidationConfig;

    // load liquidated position
    Position memory position = positions[owner];
    DebtData memory debtData = _calcDebt(position);

    // load TWAP price and calculate discounted price
@>    uint256 twapPrice = getTWAPPrice(); // Assume this function returns the TWAP price
    if (twapPrice == 0) revert CDPVault__liquidatePosition_invalidSpotPrice();
    uint256 discountedPrice = wmul(twapPrice, liqConfig_.liquidationDiscount);

    // Compute collateral to take, debt to repay, and penalty to pay
    uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
    uint256 deltaDebt = wmul(repayAmount, liqConf
Incorrect calculation of `expectedLiquidityDelta` in function `repayCreditAccount`
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/PoolV3.sol#L567


# Vulnerability details

In the contract `PoolV3.sol`, the function `repayCreditAccount` invokes `_updateBaseInterest` with `expectedLiquidityDelta` of value `-loss.toInt256()`. The calculation of `expectedLiquidityDelta` is wrong, since it only considers the situation when loss > 0 and does not consider the the situation when profit > 0. This miscalculation will lead to `expectedLiquidity()` to be smaller and further more cause the `baseInterestRate()` to be smaller, which will finally cause that the pool's interest below expectations. The correct calculation of `expectedLiquidityDelta` should be `profit.toInt256()-loss.toInt256()`.

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L529

```solidity
    function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        creditManagerOnly // U:[LP-2C]
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
        ...

        if (profit > 0) {
            _mint(treasury, convertToShares(profit)); // U:[LP-14B]
        } else if (loss > 0) {
            address treasury_ = treasury;
            uint256 sharesInTreasury = balanceOf(treasury_);
            uint256 sharesToBurn = convertToShares(loss);
            if (sharesToBurn > sharesInTreasury) {
                unchecked {
                    emit IncurUncoveredLoss({
                        creditManager: msg.sender,
                        loss: convertToAssets(sharesToBurn - sharesInTreasury)
                    }); // U:[LP-14D]
                }
                sharesToBurn = sharesInTreasury;
            }
            _burn(treasury_, sharesToBurn); // U:[LP-14C,14D]
        }

        _updateBaseInterest({
            expectedLiquidityDelta: -loss.toInt256(),
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

        ...
    }
```

In the function `_updateBaseInterest()`, the `_baseInterestRate` is calculated with `expectedLiquidity` and `availableLiquidity`.

```solidity
    function _updateBaseInterest(
        int256 expectedLiquidityDelta,
        int256 availableLiquidityDelta,
        bool checkOptimalBorrowing
    ) internal {
        uint256 expectedLiquidity_ = (expectedLiquidity().toInt256() + expectedLiquidityDelta).toUint256();
        uint256 availableLiquidity_ = (availableLiquidity().toInt256() + availableLiquidityDelta).toUint256();

        uint256 lastBaseInterestUpdate_ = lastBaseInterestUpdate;
        if (block.timestamp != lastBaseInterestUpdate_) {
            _baseInterestIndexLU = _calcBaseInterestIndex(lastBaseInterestUpdate_).toUint128(); // U:[LP-18]
            lastBaseInterestUpdate = uint40(block.timestamp);
        }

        if (block.timestamp != lastQuotaRevenueUpdate) {
            lastQuotaRevenueUpdate = uint40(block.t