xanoutas/solidity-security-findings huggingface dataset:
Rewards might be lost due to the error that _updateRewardIndex() might advance lastBalance without advancing index for a token. 
Audit: 2024-10-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManager.sol#L74
https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManager.sol#L78


# Vulnerability details

## Proof of Concept
Rewards might be lost due to the error that _updateRewardIndex() might advance ```lastBalance``` without advancing index for a reward token. 

Analysis: 
The function ```_updateRewardIndex()``` is used to update the ```lastBalance``` and ```index``` of each reward  token. This function will be called when a user deposit, withdraw collateral or claim rewards. 

However, the function might not advance ```index``` when ```accrued.divDown(totalShares) = 0```, this might happen when ```totalShares``` is too big, and ```accrued``` is too small. One case is that the number of decimals for the reward token is too small.

[https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManager.sol#L74](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManager.sol#L74)

For example, the USDC token only has 6 decimals. 

Suppose ```accrued``` = $100 = 100*10**6, and ```totalShares``` = 200M = 200 * 10** 6 * 10**18, 
then we have  ```accrued.divDown(totalShares) = 0```. 

Furthermore, if function ```_updateRewardIndex()``` is called more frequently, either because a malicious user keeps calling ```getRewards()```  (the gas fee is low on Arbitrum) or simply because the community is large so there is a high chance that for each block (per 12 seconds on Ethereum), there is someone who calls a withdraw/deposit/getRewards function. As a result,  ```accrued``` could be small, leading to ```accrued.divDown(totalShares) = 0```.

Meanwhile, ```_updateRewardIndex()``` always advance ```lastBalance``` when ```accrued !=0 ```:

[https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManager.sol#L78](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManager.sol#L78)

This means the accrued rewards are lost! Nobody will receive the rewards since index has not changed. 

More importantly, due to the rounding down error for ```accrued.divDown(totalShares)```, there is always a slight loss for the rewards, which is accumulative over time.  


The following mitigation will fix this problem. 


## Recommended Mitigation Steps
The fix is simple: 
calculate ```deltaIndex = accrued.divDown(totalShares)```, and advance ```lastBalance``` by ```deltaIndex.mulDown(totalShares)```. In this way, ```index``` and ```lastBalance``` will always advance in the same pace, in particular if index does not advance, then ```lastBalance``` will not advance either. The rounding down error is eliminated too since the ```lastBalance``` w
SwapAction's newly implemented Kyber swaps lack slippage protection
Audit: 2024-10-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/SwapAction.sol#L293-L305


# Vulnerability details


## Proof of Concept

Previously `SwapAction` only included 4 swap options, however with the current scope we now have five, see https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/SwapAction.sol#L21-L28

```diff
enum SwapProtocol {
    BALANCER,
    UNIV3,
    PENDLE_IN,
    PENDLE_OUT,
+   KYBER
}

```

Now all these previous implementations of querying the swap protocol includes a slippage protection, for .eg if uniswap is to be used we enforce it by and :
https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/SwapAction.sol#L319-L353

```solidity
    function uniV3Swap(
        SwapType swapType,
        address assetIn,
        uint256 amount,
        uint256 limit,//@audit slippage
        address recipient,
        uint256 deadline,
        bytes memory args
    ) internal returns (uint256) {
        if (swapType == SwapType.EXACT_IN) {
            IERC20(assetIn).forceApprove(address(uniRouter), amount);
            return
                uniRouter.exactInput(
                    ExactInputParams({
                        path: args,
                        recipient: recipient,
                        amountIn: amount,
                        amountOutMinimum: limit,//@audit slippage
                        deadline: deadline
                    })
                );
        } else {
            IERC20(assetIn).forceApprove(address(uniRouter), limit);
            return
                uniRouter.exactOutput(
                    ExactOutputParams({
                        path: args,
                        recipient: recipient,
                        amountOut: amount,
                        amountInMaximum: limit,//@audit slippage
                        deadline: deadline
                    })
                );
        }
    }
```

Similarly if it's a join via pendle, the minOut parameter is sued to protect users from unfair slippage, see https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/SwapAction.sol#L374-L382

```solidity
    function pendleJoin(address recipient, uint256 minOut, bytes memory data) internal returns (uint256 netLpOut) {

..snip
        (netLpOut, , ) = pendleRouter.addLiquiditySingleToken(
            recipient,
            market,
            minOut,
            guessPtReceivedFromSy,
            input,
            limit
        );
        ..snip
    }
```

The above is the case for all other swap implementations in the swapAction.sol.

Issue however is that the new implementation of swaps via Kyber router do not include any slippage whatsoever, see https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/SwapAction.sol#L293-L305

```solidity
    /// @notice
Liquidity pool is not adjusting the interest rate on healthy repayment profit, which results in incorrect rates
Audit: 2024-10-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description

**NB:** This issue is originally a duplicate of https://github.com/code-423n4/2024-07-loopfi-findings/issues/55, where 55 and all of its duplicates show how profit is being wrongly handled while showing different exploits/scenarios. However, only 1 part of that is fixed by adding `pool.mintProfit(fee);` in `src/Flashlender.sol`, without fixing the other scenario where the profit comes in from a healthy repayment.

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. An example, if there is a large borrow amount the interest will be interest accrued large but when repaying or liquidating position the profit is not added in updateBaseInterest which provides an incorrect borrow rate than the intended rate.

## Proof of Concept

We can observe the issues in the following scenarios :

1. Deposit 200 ether and borrow 150 ether
2. After some time, interest will accumulate and repay 100 ether
3. The interest rate will be lesser than when profit is added.

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

```solidity
function test_poolNotRegisteringProfit() public {
    CDPVault vault = createCDPVault(token, 200 ether, 0, 1.25 ether, 1.0 ether, 0);
    createGaugeAndSetGauge(address(vault));
    token.mint(address(this), 500 ether);
    token.approve(address(vault), 500 ether);

    address position1 = address(new PositionOwner(vault));

    vault.deposit(position1, 200 ether);

    vm.prank(address(gauge));
    quotaKeeper.updateRates();
    vm.stopPrank();

    vault.borrow(address(this), position1, 150 ether);

    (uint256 collateral, uint256 debt, , , , ) = vault.positions(position1);
    console.log("debt1 :", debt);

    vm.warp(block.timestamp + 360 days);

    (, uint256 ac
`CDPVault.sol#liquidatePositionBadDebt()` does correctly handle profit and loss
Audit: 2024-10-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/main/src/CDPVault.sol#L702
https://github.com/code-423n4/2024-10-loopfi/blob/main/src/PoolV3.sol#L593


# Vulnerability details


## Impact

When liquidating bad debt, the profit and loss is not correctly handled. This will cause incorrect accounting to lpETH stakers.

## Bug Description

*Note: This is based on the 2024-07 Loopfi contest https://github.com/code-423n4/2024-07-loopfi-findings/issues/57 issue. This protocol team applied a fix, but the fix is incomplete.*

There are two issues that needs to be fixed in the new codebase:

1. The `profit` that is passed in `pool.repayCreditAccount(debtData.debt, profit, loss);` should actually use `debtData.accruedInterest`. This is because we should first "assume" full debt and interest is paid off, and calculate the loss part independently.

2. The `loss` is correctly calculated in PoolV3#repayCreditAccount, the if-else branch is incorrectly implemented. Currently it can't handle the case where both profit and loss is non-zero. This would cause a issue that the loss will not be accounted, and will ultimately cause loss to lpETH holders (loss will be implicitly added to the users who hold lpETH) instead of lpETH stakers.

Note that the second fix was also suggested in the original issue, but it isn't applied.

CDPVault.sol
```solidity
        takeCollateral = position.collateral;
        repayAmount = wmul(takeCollateral, discountedPrice);
        uint256 loss = calcTotalDebt(debtData) - repayAmount;
        uint256 profit;
        if (repayAmount > debtData.debt) {
@>          profit = repayAmount - debtData.debt;
        }
        ...
@>      pool.repayCreditAccount(debtData.debt, profit, loss); // U:[CM-11]
        // transfer the collateral amount from the vault to the liquidator
        token.safeTransfer(msg.sender, takeCollateral);
```

PoolV3.sol
```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]
        }
        ...
    }
```

## Proof of Concept

N/A

## Tools Used

Manual Review

#
PositionActionPendle exits pendle without slippage protection
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/PositionActionPendle.sol#L64-L74


# Vulnerability details



### Proof of Concept

PositionActionPendle exits pendle without slippage protection which will leave functions dependent on the `_onWithdraw` hook vulnerable to sandwich attacks.

The hardcoded `PoolActionParams` in `_onWithdraw` hardcodes the minimum amount out to 0, meaning any amount received from pendle interaction is accepted without any issues.

```solidity
        if (dst != collateralToken && dst != address(0)) {
            PoolActionParams memory poolActionParams = PoolActionParams({
                protocol: Protocol.PENDLE,        
>>>             minOut: 0,                 
                recipient: address(this),      
                args: abi.encode(
                    collateralToken,          
                    collateralWithdrawn,       
                    dst                        
                )
            });
```

These params are passed on to `exit` in PoolAction.sol

```solidity
    function exit(PoolActionParams memory poolActionParams) public returns (uint256 retAmount) {
        if (poolActionParams.protocol == Protocol.BALANCER) {
            retAmount = _balancerExit(poolActionParams);
>>>     } else if (poolActionParams.protocol == Protocol.PENDLE) {
            retAmount = _pendleExit(poolActionParams);
        } else revert PoolAction__exit_unsupportedProtocol();
    }
```

Which is the called when redeeming while exiting pendle.

```solidity
    function _pendleExit(PoolActionParams memory poolActionParams) internal returns (uint256 retAmount) {
        (address market, uint256 netLpIn, address tokenOut) = abi.decode(
            poolActionParams.args,
            (address, uint256, address)
        );

//...

>>>     return SY.redeem(poolActionParams.recipient, netSyToRedeem, tokenOut, poolActionParams.minOut, true);
    }
```

Since the contracts are deployed on Ethereum, which is notorious for MEV bots and frontrunning, malicious users can monitor transactions that rely on `_onWithdraw` hook to gain unfair advantages and disrupt protocol operations for their own benefit.

### Recommended Mitigation Steps

Recommend allowing the `minOut` to be specified, or hardcoding a more conventional amount, e.g 95% or using a setter function that allows the owner set the value.


## Assessed type

MEV
No slippage applied when Withdrawing collateral from the vault and `dst != collateralToken`
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/PositionActionPendle.sol#L55-L84


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/PositionActionPendle.sol#L55-L84

```solidity
    function _onWithdraw(
        address vault,
        address position,
        address dst,
        uint256 amount
    ) internal override returns (uint256) {
        uint256 collateralWithdrawn = ICDPVault(vault).withdraw(address(position), amount);
        address collateralToken = address(ICDPVault(vault).token());

        if (dst != collateralToken && dst != address(0)) {
            PoolActionParams memory poolActionParams = PoolActionParams({
                protocol: Protocol.PENDLE,
                minOut: 0,
                recipient: address(this),
                args: abi.encode(
                    collateralToken,
                    collateralWithdrawn,
                    dst
                )
            });

            bytes memory exitData = _delegateCall(
                address(poolAction),
                abi.encodeWithSelector(poolAction.exit.selector, poolActionParams)
            );

            collateralWithdrawn = abi.decode(exitData, (uint256));
        }
        return collateralWithdrawn;
    }
```

This function is used to withdraw collateral from the vault.

In the new scope there is a check to see if `dst != collateralToken` then a pool action param is then needed to withdraw the collateral from PENDLE, issue however is that when doing this [we hardcode the slippage to `0`](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/PositionActionPendle.sol#L67), causing for loss of funds for users.

## Impact

Loss of funds for users when withdrawing from the vault and `dst != collateralToken`, considering [`minOut` has been hardcoded to `0`](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/proxy/PositionActionPendle.sol#L67) allowing even `0` value to be given to them post their exit from Pendle.

## Recommended Mitigation Steps

Allow users pass in a slippage protected value and in the case where `dst != collateralToken` this value should be attached to `minOut`:

```diff
            PoolActionParams memory poolActionParams = PoolActionParams({
                protocol: Protocol.PENDLE,
-                minOut: 0,
+                minOut: _minOut,
                recipient: address(this),
                args: abi.encode(
                    collateralToken,
                    collateralWithdrawn,
                    dst
                )
            });
```



## Assessed type

Context
`_distributeRewardsPrivate` wrongly set `unserIndex = 1` on first interaction, leading to erroneous reward distribution
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManagerAbstract.sol#L68


# Vulnerability details


## Summary
User reward index is set to the wrong value on its first interaction with the Reward Manager.

## Vulnerability details
`RewardManagerAbstract::_distributeRewardsPrivate` distibute rewards to users based on:
- an index, ever increasing value representing the accrual of rewards in the vault
- their share of the vault total shares

The index works like an exchange rate: when a user deposit, the vault index is saved for that user. Then when withdrawing, the saved index is compared to the new vault index, and tell how much of the generated rewards he now own.

Let's see how works the [current implementation](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManagerAbstract.sol#L52-L79), see snippet below to follow.

1. If a user depositing for the first time, represented by `userIndex == 0` , its `userIndex` is set to a constant `INITIAL_REWARD_INDEX == 1`.
2. As the current vault index will likely not be `1`, the function continue and compute the `deltaIndex` with `userIndex == INITIAL_REWARD_INDEX == 1` 

```solidity
    function _distributeRewardsPrivate(
        address user,
        uint256 collateralAmountBefore,
        address[] memory tokens,
        uint256[] memory indexes
    ) private {
        assert(user != address(0) && user != address(this));

        //  uint256 userShares = _rewardSharesUser(user);
        uint256 userShares = collateralAmountBefore;
        for (uint256 i = 0; i < tokens.length; ++i) {
            address token = tokens[i];
            uint256 index = indexes[i];
            uint256 userIndex = userReward[token][user].index;

            if (userIndex == 0) {
                userIndex = INITIAL_REWARD_INDEX.Uint128();		<(@1)
            }

            if (userIndex == index) continue;				<(@2)

            uint256 deltaIndex = index - userIndex;

            uint256 rewardDelta = userShares.mulDown(deltaIndex);
            uint256 rewardAccrued = userReward[token][user].accrued + rewardDelta;
            userReward[token][user] = UserReward({index: index.Uint128(), accrued: rewardAccrued.Uint128()});
        }
    }
```

So, the question is: how is index computed, and would be its value? We can find this in `rewardManager::_updateRewardIndex`:
1. We see that the index at its very beginning starts with an index of `INITIAL_REWARD_INDEX == 1`
2. then, `index` is incremented by a value which is represented with decimals based on the reward token as it can be infered by the code in `RewardManager::_updateRewardIndex()` where index is incremented by `accrued.divDown(totalShares)`, where both variables are token decimals, and `divDown` is from the [PMath library](https://github.com/pendle-finance/pendle-core-v2-public/blob/7e451be619353f57a8ab722234b8f9ebe0632836
Invalid handling of flash loan fees in `PositionAction::onCreditFlashLoan`, forcing it to always revert
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/main/src/proxy/PositionAction.sol#L473


# Vulnerability details

## Description

When users take a flash loan, an amount is sent to the receiver, and then some action takes place, after that action that sent amount is expected to be paid + some fee. Users can also call `PositionAction::decreaseLever` through a proxy, to "Decrease the leverage of a position by taking out a credit flash loan to withdraw and sell collateral", after it is called, the flash loan lender sends the credit and calls `onCreditFlashLoan`, which handles all that logic.

This was reported in https://github.com/code-423n4/2024-07-loopfi-findings/issues/524, and a fix has been implemented. However, the fix is incomplete, and the `onCreditFlashLoan` will always revert when the fees are >0.

The fix added includes adding fees to the approval amounts:
```solidity
underlyingToken.forceApprove(address(leverParams.vault), subDebt + fee);
underlyingToken.forceApprove(address(flashlender), subDebt + fee);
```

That fix still misses a point: the amount coming from the flash lender is constant, and that amount will be used to repay a position's debt. The issue here is that all the amount is being used to repay without accounting for the extra fees that the flash lender will be requesting.

This causes `PositionAction::onCreditFlashLoan` to always revert.

## Proof of Concept

Add the following test in `src/test/integration/PositionAction20.lever.t.sol`:

```solidity
function test_leverageDownNotAccountingFees() public {
    // Re-initialize the system to have fees > 0
    flashlender = new Flashlender(IPoolV3(address(liquidityPool)), 0.01 ether);
    liquidityPool.setCreditManagerDebtLimit(address(flashlender), type(uint256).max);
    positionAction = new PositionAction20(
        address(flashlender),
        address(swapAction),
        address(poolAction),
        address(vaultRegistry)
    );

    uint256 depositAmount = 1_000 ether;
    uint256 borrowAmount = 200 ether;

    deal(address(token), user, depositAmount);

    address[] memory assets = new address[](2);
    assets[0] = address(underlyingToken);
    assets[1] = address(token);

    vm.startPrank(user);

    // User deposits 1k ETH collateral
    token.approve(address(vault), type(uint256).max);
    vault.deposit(address(userProxy), depositAmount);

    // User borrows 200 ETH
    userProxy.execute(
        address(positionAction),
        abi.encodeWithSelector(
            positionAction.borrow.selector,
            address(userProxy),
            address(vault),
            CreditParams({amount: borrowAmount, creditor: user, auxSwap: emptySwap})
        )
    );

    vm.expectRevert(bytes("ERC20: transfer amount exceeds balance"));
    userProxy.execute(
        address(positionAction),
        abi.encodeWithSelector(
            positionAction.decreaseLever.selector,
            LeverParams({
                position: address(userProxy),