xanoutas/solidity-security-findings huggingface dataset:
Lack of Access Control for AuraVault
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/vendor/AuraVault.sol#L119-L135


# Vulnerability details

## Impact
The AuraVault contract functions that are protected by role-based access control are currently inaccessible because no addresses are assigned to the VAULT_ADMIN_ROLE and VAULT_CONFIG_ROLE. This includes critical functions such as setVaultConfig and setParameter, which are essential for:

 -  Updating contract parameters like feed and auraPriceOracle.
 -  Configuring vault settings such as claimerIncentive, lockerIncentive, and lockerRewards.

Without these roles being properly assigned, the contract cannot be managed or configured after deployment.

## Proof of Concept
The constructor of the AuraVault contract sets up the contract's state but does not grant any roles. As a result:

 -  The setVaultConfig function, which requires VAULT_ADMIN_ROLE, is inaccessible.
 -  The setParameter function, which requires VAULT_CONFIG_ROLE, is inaccessible.


```solidity
constructor(
    address rewardPool_,
    address asset_,
    address feed_,
    address auraPriceOracle_,
    uint32 maxClaimerIncentive_,
    uint32 maxLockerIncentive_,
    string memory tokenName_,
    string memory tokenSymbol_
) ERC4626(IERC20(asset_)) ERC20(tokenName_, tokenSymbol_) {
    rewardPool = rewardPool_;
    feed = feed_;
    auraPriceOracle = auraPriceOracle_;
    maxClaimerIncentive = maxClaimerIncentive_;
    maxLockerIncentive = maxLockerIncentive_;
    //@audit-issue roles not granted
}
```

## Tools Used
manual review 
## Recommended Mitigation Steps
Modify the constructor to grant the VAULT_ADMIN_ROLE and VAULT_CONFIG_ROLE to a trusted address.
```Solidity
constructor(
    address rewardPool_,
    address asset_,
    address feed_,
    address auraPriceOracle_,
    uint32 maxClaimerIncentive_,
    uint32 maxLockerIncentive_,
    string memory tokenName_,
    string memory tokenSymbol_
) ERC4626(IERC20(asset_)) ERC20(tokenName_, tokenSymbol_) {
    rewardPool = rewardPool_;
    feed = feed_;
    auraPriceOracle = auraPriceOracle_;
    maxClaimerIncentive = maxClaimerIncentive_;
    maxLockerIncentive = maxLockerIncentive_;
    
    // Grant roles to the deployer
    _setupRole(VAULT_ADMIN_ROLE, msg.sender);
    _setupRole(VAULT_CONFIG_ROLE, msg.sender);

    // Optionally, set the deployer as the default admin role
    _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

```


## Assessed type

Access Control
In the PoolV3::repayCreditAccount() function, the calculation of expectedLiquidityDelta is incorrect.
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Impact
The incorrect calculation of expectedLiquidityDelta results in an error in the system’s internal profit calculations.

## Proof of Concept
```javascript
    function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        creditManagerOnly // U:[LP-2C]
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
        uint128 repaidAmountU128 = repaidAmount.toUint128();

        DebtParams storage cmDebt = _creditManagerDebt[msg.sender];
        uint128 cmBorrowed = cmDebt.borrowed;
        if (cmBorrowed == 0) {
            revert CallerNotCreditManagerException(); // U:[LP-2C,14A]
        }

        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]

        _totalDebt.borrowed -= repaidAmountU128; // U:[LP-14B,14C,14D]
        cmDebt.borrowed = cmBorrowed - repaidAmountU128; // U:[LP-14B,14C,14D]

        emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D]
    }

```
We can see that when calling _updateBaseInterest(), the expectedLiquidityDelta misses the profit portion. This omission leads to an incorrect profit calculation.

## Tools Used
Manual Review

## Recommended Mitigation Steps
``` diff
-     expectedLiquidityDelta: -loss.toInt256(),
+     expectedLiquidityDelta: profit.toInt256() -loss.toInt256(),
```


## Assessed type

Error
The for-loop in the `withdraw` function in `MultiFeeDistribution.sol` could get into an infinite loop leading to gas exhaustion and DoS
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/reward/MultiFeeDistribution.sol#L553-L588
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/reward/MultiFeeDistribution.sol#L566
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/reward/MultiFeeDistribution.sol#L580-L581


# Vulnerability details

## Impact
1. Gas Exhaustion. Since the loop runs indefinitely without an exit, it will exhaust the transaction's gas, causing it to fail.
2. Denial of service. This contract becomes unusable for the user, and funds can be locked within the contract. This can also affect other users.
3. Unpredictable behaviour. Accessing out-of-bounds elements, can lead to unexpected and unpredictable behaviour.

## Description
High penalties combined with the unchecked increment in the loop, can lead to an infinite loop when processsing user earnings during a withdrawal. I will explain the conditions that trigger the bug, and the calculations involved.

## Proof of Concept
```solidity
    // **Potential Infinite Loop**: The loop that continues iterating
 @> for (i = 0; ; ) { // **missing array bounds check**
        uint256 earnedAmount = _userEarnings[_address][i].amount;
        if (earnedAmount == 0) continue;
        (
            uint256 withdrawAmount,
            uint256 penaltyFactor,
            uint256 newPenaltyAmount,
            uint256 newBurnAmount
        ) = _penaltyInfo(_userEarnings[_address][i]);

        uint256 requiredAmount = earnedAmount;
        if (remaining >= withdrawAmount) {
            remaining -= withdrawAmount;
            if (remaining == 0) i++;
        } else {
            requiredAmount = (remaining * WHOLE) / (WHOLE - penaltyFactor);
            _userEarnings[_address][i].amount = earnedAmount - requiredAmount;
            remaining = 0;

            newPenaltyAmount = (requiredAmount * penaltyFactor) / WHOLE;
            newBurnAmount = (newPenaltyAmount * burn) / WHOLE;
        }

        sumEarned -= requiredAmount;

        penaltyAmount += newPenaltyAmount;
        burnAmount += newBurnAmount;

        if (remaining == 0) {
            break;
        } else {
            if (sumEarned == 0) revert("InvalidEarned");
        }

        // **Unchecked Increment**: The line responsible for potentially causing the infinite loop
@>      unchecked {
            i++;  // This is the line that will be looped through and potentially cause an infinite loop
        }
    }
```

There are key conditions that lead to the `withdraw` function getting into an infinite loop. 
1. A high penalty configuration.
   A significant portion (upto 90%) of the vested amount is penalised if withdrawn early. This will result in only a small fraction of the amount being withdrawable per iteration. 
2. Unchecked increment in the Loop
   The loop processing the earnings has an unchecked increment `unchecked { i++; }`. This will allow `i` to increase indefinitely without bounds checking.


The `_userEarnings` mapping
absence of access control in `AuraVault`
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/vendor/AuraVault.sol#L120-L135


# Vulnerability details

## Impact
None can change the `feed`, `auraPriceOracle` and `vaultConfig` parameters

## Proof of Concept
There are no roles applied in the constructor, meaning that nobody will be able to change the `feed` and `auraPriceOracle` addresses when the `AuraVault` is deployed. The absence of `_grantRole` function in the constructor totally makes no sense and it is probably an unintentional mistake but should be pointed out 

## Tools Used
manual review
## Recommended Mitigation Steps
Put `_grantRole()` functions in the constructor like this:
```diff
    constructor(
+       address vaultAdminRole,
+       address vaultConfigRole,
        address rewardPool_,
        address asset_,
        address feed_,
        address auraPriceOracle_,
        uint32 maxClaimerIncentive_,
        uint32 maxLockerIncentive_,
        string memory tokenName_,
        string memory tokenSymbol_
    )
        ERC4626(IERC20(asset_))
        ERC20(tokenName_, tokenSymbol_)
    {
+       _grantRole( VAULT_ADMIN_ROLE, vaultAdminRole);
+       _grantRole(VAULT_CONFIG_ROLE, vaultConfigRole);
        rewardPool = rewardPool_;
        feed = feed_;
        auraPriceOracle = auraPriceOracle_;
        maxClaimerIncentive = maxClaimerIncentive_;
        maxLockerIncentive = maxLockerIncentive_;
    }

```


## Assessed type

Access Control
Dust positions can be left out or unexpected reverts can occur upon liquidations
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Dust positions can be left out or unexpected reverts can occur upon liquidations

## Proof of Concept
Upon liquidations, we calculate the collateral a user can receive like this:
```solidity
uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
```
Then, we have the following check:
```solidity
if (takeCollateral > position.collateral) revert CDPVault__tooHighRepayAmount();
```
The check disallows users to take out more collateral than the collateral of the position. Of course, every liquidator's goal would be to take all of the collateral of the position. Thus, the first issue is that a lot of unexpected reverts would occur as the `takeCollateral` variable is based on price which is very dynamic. Thus, a user trying to match his `takeCollateral` with the position's collateral can cause a revert.

The second issue is that there are 2 strict equalities that are ideal. Firstly, the one explained above where every user will try to make a strict equality between `takeCollateral` and the position's collateral. This is the one that users would want to match. The other one is this one:
```solidity
        if (deltaDebt == maxRepayment) {
            newDebt = 0;
            newCumulativeIndex = debtData.cumulativeIndexNow;
            profit = debtData.accruedInterest;
            position.cumulativeQuotaInterest = 0;
}     
```
As all values are computed in completely different ways, it is super unlikely that both of those equalities would occur. If `deltaDebt` is not equal to `maxRepayment`, we either cause another unexpected revert (underflow in `calcDecrease()` if deltaDebt > maxRepayment) or leave dust debt for the position. Based on different prices, values for liquidation penalties, liquidation ratios, price discounts and etc., the following scenarios can occur:
1. Revert due to `takeCollateral` being higher than position's collateral
2. Revert due to underflow in `calcDecrease()` if `deltaDebt` > `maxRepayment`
3. Dust position being left out with no collateral anymore, thus no liquidation possible
4. Dust position with some collateral, low incentives
5. Position cleared with some collateral left out
6. And others

All of these scenarios are bad for the protocol and the liquidators. The ideal scenario where the user gets all of the collateral and the position is completely cleared is extremely unlikely.
## Tools Used
Manual Review
## Recommended Mitigation Steps
Refactor and simplify the function





## Assessed type

Error
Fees Paid During Flashloan Repayment Are Not Accounted In the `expectedLiquidity_` 
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/PoolV3.sol#L208-L210
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L568-L569
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/Flashlender.sol#L87-L109


# Vulnerability details

## Summary
During a flashloan through `Flashlender::flashLoan` or `Flashlender::creditFlashloan`, the flashloan fees are not added to the `expectedLiquidity` as profit, while `penalty` is during liquidation, revealing an inconsistency in profit accounting.

## Vulnerability details
This issue is stemming from a (necessary) fix for a previous vulnerability [\[WP-M6\]](https://notes.watchpug.com/p/190becc04cemgrXz#n_9) from Watchpug report.
I will first explain the update and then show how a new issue emmerged.

With this fix, `poolV3::repayCreditAccount` do not account for `profit` as an additional `expectedLiquidity` when calling `_updateBaseInterest`.
Below the changes that followed the previous report:
```diff
        _updateBaseInterest({
-            expectedLiquidityDelta: profit.toInt256() -loss.toInt256(),
+            expectedLiquidityDelta:  -loss.toInt256(),
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

```

The reason this has been updated is explained in WP-M6:
>The expectedLiquidity() function at PoolV3.sol#L644 already includes all accrued interest (_calcBaseInterestAccrued()), which incorporates the profit from the current period (see PoolV3.sol#L564).
>
>Adding expectedLiquidityDelta to expectedLiquidity() at PoolV3.sol#L644 results in double counting of the profit.

You can see below the `_updateBaseInterest` function, calling [`expectedLiquidity()`](https://github.com/code-423n4/2024-07-loopfi/blob/main/src/PoolV3.sol#L208-L210) at `L647` and adding `expectedLiquidityDelta` to it.
Then storing the new `expectedLiquidity` to state variable `_expectedLiquidityLU` at `L660`.

```solidity
File: src/PoolV3.sol
642:     function _updateBaseInterest(
643:         int256 expectedLiquidityDelta,
644:         int256 availableLiquidityDelta,
645:         bool checkOptimalBorrowing
646:     ) internal {
647:         uint256 expectedLiquidity_ = (expectedLiquidity().toInt256() + expectedLiquidityDelta).toUint256();
648:         uint256 availableLiquidity_ = (availableLiquidity().toInt256() + availableLiquidityDelta).toUint256();
649: 
...:
...:      //* ------ some code ----- *//
...:
659: 
660:         _expectedLiquidityLU = expectedLiquidity_.toUint128();
661:         _baseInterestRate = ILinearInterestRateModelV3(interestRateModel)
662:             .calcBorrowRate({
663:                 expectedLiquidity: expectedLiquidity_,
664:                 availableLiquidity: availableLiquidity_,
665:                 checkOptimalBorrowing: checkOptimalBorrowing
666:             })
667:             .toUint128();
668:     }
```

Now, when other type of profit are earned, as during a liquidation, th
`CDPVault` has no method to retreive $PENDLE rewards generated by holding PendleLP tokens 
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary
PendleLP generate revenues through fees (auto-compounded into LP) but also PENDLE tokens rewards, the latter cannot be claimed by the vault, meaning they are lost.

## Vulnerability details
Users can deposit PendleLP tokens into `CDPVault` to borrow ETH. These PendleLP are held by `CDPVault` until user withdraw them.
Users are then incentivized to borrow ETH in order to buy more PendleLP, deposit and borrow again, leveraging the amount of deposited PendleLP.

Those PendleLP generate revenues that are for most of them auto-compounded into the LP token value (Swap fees generated by the pool), but the $PENDLE incentive must be claimed.

To claim the PENDLE reward, the holder must [call `redeemRewards`](https://etherscan.io/address/0x17be998a578fd97687b24e83954fec86dc20c979#writeContract#F6) from the PendleMarketV3 contract, to get the reward in tokens returned [by `getRewardTokens`](https://etherscan.io/address/0x17be998a578fd97687b24e83954fec86dc20c979#readContract#F11)
The contract provided in the links above (0x17be998a578fd97687b24e83954fec86dc20c979) are from the PendleLP-pufETH market, [which is a supported strategies](https://docs.loopfi.xyz/the-protocol/looping-leveraged-restaking/supported-strategies) in LoopFi.

The issue is that the rewards are based on the active balance of the user it can be seen in Pendle contracts [from the `PendleGauge::_redeemRewards` function](https://github.com/pendle-finance/pendle-core-v2-public/blob/7b3d0eb9362fd7f4e861d58856e2ac2262367c3d/contracts/core/Market/PendleGauge.sol#L38-L48)

```solidity
    /**
     * @dev Since rewardShares is based on activeBalance, user's activeBalance must be updated AFTER
        rewards is updated
     * @dev It's intended to have user's activeBalance updated when rewards is redeemed
     */
    function _redeemRewards(address user) internal virtual returns (uint256[] memory rewardsOut) {
        _updateAndDistributeRewards(user);
        _updateUserActiveBalance(user);
        rewardsOut = _doTransferOutRewards(user, user);
        emit RedeemRewards(user, rewardsOut);
    }
```

This means that when users will deposit PendleLPs in the vault, the reward will be assigned to the vault for the time being until users withdraw the PendleLPs.
If the Vault has no function to retrieve the \$PENDLE rewards, then these rewards are lost, representing a loss of revenue.

More informations can be found in the Pendle Finance documentation:

https://docs.pendle.finance/Developers/Oracles/HowToIntegratePtAndLpOracle#handling-of-pendle-lps-rewards
>Handling of Pendle LP's rewards:
>Holding Pendle LP tokens will generate PENDLE incentives and potential reward tokens (like WETH for LP for GLP pool)
>**The money market contracts will need to redeem these rewards by calling the `redeemRewards` function and implement logic to distribute these rewards to their users**
Interest rate is being incorrectly updated when there is a profit
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/PoolV3.sol#L529-L576


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/PoolV3.sol#L529-L576

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

  DebtParams storage cmDebt = _creditManagerDebt[msg.sender];
  uint128 cmBorrowed = cmDebt.borrowed;
  if (cmBorrowed == 0) {
    revert CallerNotCreditManagerException(); // U:[LP-2C,14A]
  }

  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(), //@audit
    availableLiquidityDelta: 0,
    checkOptimalBorrowing: false
  }); // U:[LP-14B,14C,14D]

  _totalDebt.borrowed -= repaidAmountU128; // U:[LP-14B,14C,14D]
  cmDebt.borrowed = cmBorrowed - repaidAmountU128; // U:[LP-14B,14C,14D]

  emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D]
}

```

This function is used to update the pool state to indicate debt repayment and it can only be called by credit managers after transferring underlying from a credit account to the pool.

Now if the transferred amount exceeds exceeds debt `principal + base interest + quota interest`,
the difference is deemed protocol's profit and is minted to to the treasury [here](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/PoolV3.sol#L549), during liquidation however the transferred amount might be insufficient to repay the debt and interest, and in that case we have a loss, now whatever the case be it profit or loss, this difference should be specified when updating the interest, but as hinted by the `@audit` tag, the the profit is never attached to the liquidity delta, forcing the protocol's accounting to be flawed considering the value of [`_expectedLiquidityLU` is never going to consider the profits](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/PoolV3.sol#L642-L669):

```solidity
function _updateBaseInterest(
  int256 expectedLiquidityDelta, //@audit this value never considers profit when been called from `repayCreditAccount()`
  int256 availa