xanoutas/solidity-security-findings huggingface dataset:
Incorrect Repayment Priority in `calcDecrease()` Function.
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

There is an issue with the repayment priority logic in the `calcDecrease()` function. The problem lies in the `else` block of the `if (amountToRepay >= cumulativeQuotaInterest)` condition, triggered when the repayment amount is less than the cumulative quota interest.

In this case, the function incorrectly sets the `newCumulativeQuotaInterest` to `cumulativeQuotaInterest - quotaInterestPaid`, where `quotaInterestPaid` is equal to the repayment amount (`amountToRepay`). This leads to a violation of the expected behavior, which should prioritize reducing the cumulative quota interest by the repayment amount. [CDPVault.sol#L665-L683](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L665-L683)
```solidity
        if (cumulativeQuotaInterest != 0 && amountToRepay != 0) {
            // All interest accrued on the quota interest is taken by the DAO to be distributed to LP stakers, dLP stakers and the DAO

            if (amountToRepay >= cumulativeQuotaInterest) {
                amountToRepay -= cumulativeQuotaInterest; // U:[CL-3]
                profit += cumulativeQuotaInterest; // U:[CL-3]

                newCumulativeQuotaInterest = 0; // U:[CL-3]
            } else {
                // If amount is not enough to repay quota interest + DAO fee, then send all to the stakers
                uint256 quotaInterestPaid = amountToRepay; // U:[CL-3]
                profit += amountToRepay; // U:[CL-3]
                amountToRepay = 0; // U:[CL-3]

                newCumulativeQuotaInterest = uint128(cumulativeQuotaInterest - quotaInterestPaid); // U:[CL-3]
            }
        } else {
            newCumulativeQuotaInterest = cumulativeQuotaInterest;
        }
```

## Impact
When the repayment amount is less than the cumulative quota interest, the function incorrectly calculates the new cumulative quota interest, resulting in an incorrect state of the position.

If the cumulative quota interest is not reduced correctly, it may result in users paying more interest than expected or the protocol not receiving the correct amount of interest payments.

## Proof of Concept
1. A user has a position with a cumulative quota interest of 2 tokens.
2. The user attempts to repay 1 token of debt.
3. The calcDecrease() function is called with the following parameters:
   * `amount` (repayment amount) = 1
   * `cumulativeQuotaInteres`t = 2

4. Inside the `else` block of the `if (amountToRepay >= cumulativeQuotaInterest)` condition, the function sets:
   * `quotaInterestPaid` = 1
   * `newCumulativeQuotaInterest` = `cumulativeQuotaInterest - quotaInterestPaid` = 2 - 1 = 1

5. However, according to the expected behavior, the `newCumulativeQuotaInterest` should be equal to `cumulativeQuotaInterest - amount` = 2 - 1 = 1.

## Tools Used
Manual review

#
The Chainlink's `latestRoundData` price feed can return stale or incorrect results.
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://vscode.dev/github/code-423n4/2024-07-loopfi/blob/main/src/vendor/AuraVault.sol#L367-L375


# Vulnerability details

## Impact

The `_chainlinkSpot()` fetches the latestRoundData() from a Chainlink oracle feed, without checking the round completeness or the timestamp to ensure that the reported price is not stale. Using a stale price feed can result in wrong calculations in the vault shares math which can lead to an exploit from an attacker.

## Proof of Concept

No checks for `_chainlinkSpot()` latestRoundData to validate if the value returned indicates stale data. Reasons for a price feed to stop updating are listed [here](https://ethereum.stackexchange.com/questions/133242/how-future-resilient-is-a-chainlink-price-feed/133843#133843)

    function _chainlinkSpot() private view returns (uint256 price) {
        bool isValid;
        try AggregatorV3Interface(BAL_CHAINLINK_FEED).latestRoundData() returns (
            uint80 /*roundId*/,
            int256 answer,
            uint256 /*startedAt*/,
            uint256 /*updatedAt*/,
            uint80 /*answeredInRound*/
        ) {
            price = wdiv(uint256(answer), BAL_CHAINLINK_DECIMALS);
            isValid = (price > 0);

 This could lead to stale prices according to the Chainlink [documentation](https://docs.chain.link/data-feeds/historical-data)

## Tools Used

Manual review

## Recommended Mitigation Steps

Validate `updatedAt` of `latestRoundData()` and check that the price age is not above a threshold and validated.


## Assessed type

Oracle
No incentive to write off bad debt when remaining collateral is very small
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When a position has only a small amount of collateral left due to a previous call to partially liquidate a position via the liquidatePosition, there is little incentive to liquidate the remainder of the position. The maximum a user can recover is the remaining collateral, which can be too small to cover the gas fees required for the transaction. As a consequence bad debt can accumulate.

````
        uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
````

As can be seen above, the takeCollateral is based off the repayAmount divided by the discounted price. It is then slashed from current positions collateral as so :

````
        position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt);
````

## Proof of Concept
````

    function test_liquidate_Too_Little_Remaining() public {
        CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 1 ether, 1 ether);
        createGaugeAndSetGauge(address(vault));
        // create position, taking max debt
        _updateSpot(1 ether);
        _modifyCollateralAndDebt(vault, 100 ether, 80 ether);

        // liquidate position
        address position = address(this);
        uint256 repayAmount = 79.9 ether;
        _updateSpot(0.8 ether);

        mockWETH.approve(address(vault), repayAmount);
        console.log(vault.totalDebt(), "totalDebt");
        uint256 creditBefore = credit(address(this));
        uint256 virtualDebtBefore = virtualDebt(vault, position);
        (uint256 collateral, , , , , ) = vault.positions(position);
        console.log(collateral, "collateral before");
        vault.liquidatePosition(position, repayAmount);
        
        (uint256 collateralAfter, , , , , ) = vault.positions(position);

        console.log(collateralAfter/1e18, "collateral after");
        // @audit collateral left in position is less than one ether
        assertLt(collateralAfter, 1 ether);
        assertGt(collateralAfter, 0);
    }
````

## Tools Used
Manual Review

## Recommended Mitigation Steps
It should be not possible liquidate a position and leave dust collateral, this can be mitigated in a number of ways, including introducing liquidation limit, if the liquidator will reduce collateral by 95% then this call should revert.


## Assessed type

Other
ChainlinkOracle.sol#spot() function will return wrong price when prices drop to minAnswer
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/oracle/ChainlinkOracle.sol#L96-L110


# Vulnerability details

## Impact
When aggregator prices drop below minAnswer or rises above maxAnswer, wrong prices will be returned which can be manipulated to take loans at wrong prices or prevent liquidations when minAnswer is returned instead of the actual lower prices.

## Proof of Concept
Chainlink aggregators have a price boundary that includes minimum and maximum prices that can be returned. When there is massive and rapid drop in price of an asset(for example LUNA crash), The price oracle will continue to return the minimum price stipulated instead of the actual price allowing users to take loans at prices higher than the worth of their collateral. This is exactly the cause of [Venus crash when LUNA rapidly dropped in price](https://rekt.news/venus-blizz-rekt/).

The ChainlinkOracle.sol does not check for the minimum and maximum boundary prices which can allow the [Venus on BSC situation](https://rekt.news/venus-blizz-rekt/) to possbily be repeated.

```solidity
function _fetchAndValidate(address token) internal view returns (bool isValid, uint256 price) {
        Oracle memory oracle = oracles[token];
        try AggregatorV3Interface(oracle.aggregator).latestRoundData() returns (
            uint80 /*roundId*/,
            int256 answer,
            uint256 /*startedAt*/,
            uint256 updatedAt,
            uint80 /*answeredInRound*/
        ) {
            isValid = (answer > 0 && block.timestamp - updatedAt <= oracle.stalePeriod);
            return (isValid, wdiv(uint256(answer), oracle.aggregatorScale));
        } catch {
            // return the default values (false, 0) on failure
        }
    }
```

## Tools Used
Manual review


## Recommended Mitigation Steps
The `answer` from the `oracle.aggregator.latestRoundData()` should be checked to ensure the `answer` is within the minimum and maximum prices used by the aggregators in order not to use a wrong price.

```diff
function _fetchAndValidate(address token) internal view returns (bool isValid, uint256 price) {
        Oracle memory oracle = oracles[token];
        try AggregatorV3Interface(oracle.aggregator).latestRoundData() returns (
            uint80 /*roundId*/,
            int256 answer,
            uint256 /*startedAt*/,
            uint256 updatedAt,
            uint80 /*answeredInRound*/
        ) {
__            isValid = (answer > 0 && block.timestamp - updatedAt <= oracle.stalePeriod);
++            isValid = (answer > 0 && block.timestamp - updatedAt <= oracle.stalePeriod && answer > minPrice && answer < maxPrice);
            return (isValid, wdiv(uint256(answer), oracle.aggregatorScale));
        } catch {
            // return the default values (false, 0) on failure
        }
    }

```


## Assessed type

Oracle
CDPVault will hold a lot of Pendle LP tokens as collateral, which will generate PENDLE incentives and reward tokens BUT the vault does not have a method to redeem these rewards
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability Details 
CPDVaults will hold Pendle LP tokens as collateral [(proof from the docs.)](https://docs.loopfi.xyz/the-protocol/looping-leveraged-restaking/supported-strategies)

Therefore it is reasonable to assume that there will be a large number of Pendle LP tokens held within these vaults.

Simply for holding Pendle LP tokens the vaults will generate large amounts of PENDLE incentives and other reward tokens (like WETH for LP for GLP pool).

The issue is that there is no function to redeem these rewards from pendle and then distribute the rewards to LoopFi users. This is also recommended by [pendle themselves in their docs](https://docs.pendle.finance/Developers/Oracles/HowToIntegratePtAndLpOracle#handling-of-pendle-lps-rewards)

## Impact
All rewards generated from holding Pendle LP tokens will be lost

## Recommended Mitigation Steps
Add the following function to redeem all the rewards.

```solidity
function redeemPendleRewards() external {
    // returns the list of reward tokens
    address[] memory rewardTokens = IPMarket(token).getRewardTokens();
    
    // redeems the user's reward
    // returns amounts of reward tokens redeemed, in the same order as `getRewardTokens()`
    uint256[] memory rewardAmounts = IPMarket(token).redeemRewards(address(this));
    ...
    ...
    ...    
}
```

Note the protocol should replace ... with logic to handle rewards distribution 


## Assessed type

Other
When protocolFee is present in the flashLender contract, the 'decreaseLever' function always reverts to 'insufficient allowance'
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/proxy/PositionAction.sol#L381
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/proxy/PositionAction.sol#L444
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/proxy/PositionAction.sol#L489


# Vulnerability details

## Impact
Users who enter a loopfi position can adjust their risk by decreasing their leverage or repaying their debt before the healthfactor goes down to the liquidation rate.

However, If the user calls the 'decreaseLever' function and the 'protocolFee' of the 'flashLender' contract is greater than 0, the 'decreaseLever' function will always be reverted.

This problem prevents users from lowering leverage to manage liquidation risk when protocolfee exists, exposing them to liquidation risk.

## Proof of Concept

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/proxy/PositionAction.sol#L365-L369

```
function decreaseLever(
        LeverParams calldata leverParams,
        uint256 subCollateral,
        address residualRecipient
    ) external onlyDelegatecall {
        '''''''''

        flashlender.creditFlashLoan(
            ICreditFlashBorrower(self),
            loanAmount,
            abi.encode(leverParams, subCollateral, residualRecipient)
        );


        '''''''''
    }

```

'decreaseLever' function calls the 'creditFlashLoan' function of the flashLender contract, and the 'onCreditFlashLoan' function is called back.


https://github.com/code-423n4/2024-07-loopfi/blob/main/src/Flashlender.sol#L117-L137
```
    function creditFlashLoan(
        ICreditFlashBorrower receiver,
        uint256 amount,
        bytes calldata data
    ) external override nonReentrant returns (bool) {
        uint256 fee = wmul(amount, protocolFee);
        uint256 total = amount + fee;

        pool.lendCreditAccount(amount, address(receiver));

        emit CreditFlashLoan(address(receiver), amount, fee);

        if (receiver.onCreditFlashLoan(msg.sender, amount, fee, data) != CALLBACK_SUCCESS_CREDIT)
            revert Flash__creditFlashLoan_callbackFailed();

        // reverts if not enough Stablecoin have been send back
        underlyingToken.transferFrom(address(receiver), address(pool), total);
        pool.repayCreditAccount(total - fee, fee, 0);

        return true;
    }
```
'creditFlashLoan' function is a flashloan function that lends the underlying token to the receiver and then returns the loan amount plus protocol fee at the end.

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/proxy/PositionAction.sol#L489
```
function decreaseLever(
        LeverParams calldata leverParams,
        uint256 subCollateral,
        address residualRecipient
    ) external onlyDelegatecall {
        '''''''''

        underlyingToken.forceApprove(address(flashlender), subDebt);


        '''''''''
    }
```
Before returning the debt, the approval amount is set through the 'forceApprove' function in the 'onCreditFlashLoan' functio
StakingLPEth locks an excessive amount of lpETH to prevent the inflation attack, this can lead to loss of funds/DOS in edge cases
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability Details 
The `StakingLPEth` contract is an ERC4626 vault. To prevent the well known inflation attack the protocol has implemented a `MIN_SHARES = 0.01 ether`. 

After every deposit/withdraw, there is a `_checkMinShares()` function to ensure the minimum number of shares is >= 0.01 ether. 

The issue is that 0.01 ether is a significant amount of funds to lock in the contract. It can lead to the following fund loss/DOS scenario

1. User A deposits 1 lpETH
2. User B deposits 0.005 lpETH (~$20)
3. User C deposits 0.005 lpETH (~$20)
4. User A withdraws 1 lpETH
5. User B/C withdraw attempts will revert indefinitely 

Simply using MIN_SHARES = 1000, which is a dust amount, is enough to make the inflation attack 1000x more expensive for the attacker i.e. to steal 1 ETH the attacker will lose 1000 ETH, while locking up only a dust amount.

## Impact
Users can have lost funds up to 0.01 lpETH

## Recommended Mitigation Steps
Very simple solution

Use MIN_SHARES = 1000


## Assessed type

ERC4626
The deposit and withdraw functions are meant to be pausable but an attacker can easily bypass this by directly calling modifyCollateralAndDebt() function to deposit/withdraw
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability Details 
In the `CDPVault` contract the `deposit` and `withdraw` functions are meant to revert when the protocol is paused, this is because they have the `whenNotPaused` modifier.

The purpose of the deposit/withdraw functions is to slightly manipulate the user input params and pass them into `modifyCollateralAndDebt()` function to actually change the state.

The issue is that an attacker can easily bypass this by directly calling the public `modifyCollateralAndDebt` function with the appropriate params for a deposit/withdraw. Since this function does not have the `whenNotPaused` modifier, the Tx will go through even if the protocol is paused.

## Impact
Attacker can deposit/withdraw through `modifyCollateralAndDebt` when the protocol is paused.

If there is an exploit involving the depositing/withdrawing, pausing the protocol will not stop the attacker.

## Recommended Mitigation Steps
There are a few solutions

1. Make `modifyCollateralAndDebt()` an internal function
2. If `modifyCollateralAndDebt()` must remain public, revert if paused when `(deltaCollateral > 0 || deltaCollateral < 0)`

Make sure repayments are not paused else borrowers will unfairly accumulate interest and may be quickly liquidated by a bot once unpaused





## Assessed type

Other
Missing staleness check when trying to fetch the price from chainlink in AuraVault
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/vendor/AuraVault.sol#L365-L379
https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/vendor/AuraVault.sol#L381-L391


# Vulnerability details

## Impact
This can cause the AuraVault contract to operate on stale prices.

## Proof of Concept

When calculating the rewards for the `AuraVault`, the function `AuraVault::_previewReward` is called:

```solidity
    function _previewReward(
        uint256 balReward,
        uint256 auraReward,
        VaultConfig memory config
    ) private view returns (uint256 amount) {
        amount = (balReward * _chainlinkSpot()) / IOracle(feed).spot(asset());
        amount = amount + (auraReward * _getAuraSpot()) / IOracle(feed).spot(asset());
        amount = (amount * (INCENTIVE_BASIS - config.claimerIncentive)) / INCENTIVE_BASIS;
    }
```

As we can see this funtion calls `_chailinkSpot` and `_getAuraSpot` to get the current USD prices from the chainlink data feed.

```solidity
        bool isValid;
        try AggregatorV3Interface(BAL_CHAINLINK_FEED).latestRoundData() returns (
            uint80 /*roundId*/,
            int256 answer,
            uint256 /*startedAt*/,
            uint256 /*updatedAt*/,
            uint80 /*answeredInRound*/
        ) {
            price = wdiv(uint256(answer), BAL_CHAINLINK_DECIMALS);
            isValid = (price > 0);
        } catch {}

        if (!isValid) revert AuraVault__chainlinkSpot_invalidPrice();
    }

    function _getAuraSpot() internal view returns (uint256 price) {
        uint256 ethPrice;
        (, int256 answer, , , ) = AggregatorV3Interface(ETH_CHAINLINK_FEED).latestRoundData();
        ethPrice = wdiv(uint256(answer), ETH_CHAINLINK_DECIMALS);

        IPriceOracle.OracleAverageQuery[] memory queries = new IPriceOracle.OracleAverageQuery[](1);
        queries[0] = IPriceOracle.OracleAverageQuery(IPriceOracle.Variable.PAIR_PRICE, 1800, 0);
        uint256[] memory results = IPriceOracle(auraPriceOracle).getTimeWeightedAverage(queries);

        price = wmul(results[0], ethPrice);
    }
```

As we can see from above for the `_chainlinkSpot` function the only check done is `answer > 0`, without checking that the data feed's price is stale, The same is with the `_getAuraSpot` function.

If the price were to go stale the contract will distribute incorrect rewards that are calculated from stale prices.

## Tools Used
Manual analysis

## Recommended Mitigation Steps

Add the same checks as in the [ChainlinkOracle::_fetchAndValidate](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/oracle/ChainlinkOracle.sol#L105)


## Assessed type

Oracle
Lack of `whenNotPaused` modifier in `CDPVault::_modifyPosition`, `CDPVault::modifyCollateralAndDebt`, `CDPVault::borrow` could cause protocol to accumulate bad debt.
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The OpenZeppelin's modifier `whenNotPaused` used in `CDPVault` is to ensure that core functions are only callable when the protocol's kill switch isn't active. Functions such as `deposit`, `withdraw` and `liquidation` functionalities have the modifier. But, other vital functions such as  `CDPVault::_modifyPosition`, `CDPVault::modifyCollateralAndDebt`, `CDPVault::borrow` all lack this modifier. The impact of this is that even when the protocol is paused users can still borrow collateral against their credit and modify their positions as they see fit. If they accumulate bad debt they can't face liquidation as all liquidation functions will be paused. The ussers do need to have deposited `collateral` before the protocol pause.


## Proof of Concept

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

function _modifyPosition(
        address owner,
        Position memory position,
        uint256 newDebt,
        uint256 newCumulativeIndex,
        int256 deltaCollateral,
        uint256 totalDebt_
    ) internal returns (Position memory) { //@audit POSITIONS CAN BE MODIIED EVEN WHEN PROTOCOL IS PAUSED. USERS CAN AVOID LIQUIDATION. ACCUMULATE BAD DEBT. MISING whenNotPaused MODIFIER
        uint256 currentDebt = position.debt;
        /* SNIIPPED */

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

```
    function borrow(address borrower, address position, uint256 amount) external { //@audit MISING whenNotPaused MODIFIER. USERS CAN ACCUMULATE BAD DEBT WHILE CONTRACT IS PAUSED.
    /*SNNIPPED*/

```

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

```
function modifyCollateralAndDebt(
        address owner,
        address collateralizer,
        address creditor,
        int256 deltaCollateral,
        int256 deltaDebt
    ) public { //@audit POSITIONS CAN BE MODIIED EVEN WHEN PROTOCOL IS PAUSED. USERS CAN AVOID LIQUIDATION. ACCUMULATE BAD DEBT. MISING whenNotPaused MODIFIER
    /*SNIPPED*/
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

Add the modifier to these funcctions.


## Assessed type

Access Control