xanoutas/solidity-security-findings huggingface dataset:
Invalid handling of risdual amount in `PositionAction::onCreditFlashLoan`, forcing it to 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#L492-L499


# Vulnerability details

## Description

Users can 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. When doing so, users are supposed to swap their collateral withdrawn into debt tokens so that the flash loan can be repaid.

The protocol tries to handle the residual amount from the swap (swapped - paid debt), by trying to repay extra debt for the designated position, using:
```solidity
if (residualAmount > 0) {
    underlyingToken.forceApprove(address(leverParams.vault), residualAmount);
    ICDPVault(leverParams.vault).modifyCollateralAndDebt(
        leverParams.position,
        address(this),
        address(this),
        0,
        -toInt256(residualAmount)
    );
}
```

However, this is invalid for 2 main reasons:
1. This is trying to repay extra debt than what the user is trying to, which is passed in the `primarySwap.amount`.
2. If the user tries to repay his whole debt using `decreaseLever` the TX will revert, as it'll try to repay some nonexistent debt.

## Proof of Concept
Add the following test in `src/test/integration/PositionAction20.lever.t.sol`:
```solidity
function test_leverageDownWrongResidualHandling() public {
    uint256 depositAmount = 1_000 ether;
    uint256 borrowAmount = 200 ether;

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

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

    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})
        )
    );

    userProxy.execute(
        address(positionAction),
        abi.encodeWithSelector(
            positionAction.decreaseLever.selector,
            LeverParams({
                position: address(userProxy),
                vault: address(vault),
                collateralToken: address(token),
                primarySwap: SwapParams({
                    swapProtocol: SwapProtocol.BALANCER,
                    swapType: SwapType.EXACT_IN,
                    assetIn: address(token),
                    amount: vault.virtualDebt(address(userProxy)),
                    limit: 0,
                    recipient: address(positionAction),
                    residualRecipient: address(positionAction),
                    deadline: block.timestamp,
`kyberSwap` will not work due to incorrectly decoded returned swap values
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

### Proof of Concept

When conducting swaps, the option to swap through Kyber MetaAggregationRouterV2 is made available by the `SwapProtocol.KYBER` enum. When the swap is conducted, `kyberSwap` is called, with the asset, amount and payload passed in. However, external/public functions in kyberRouter return two uint256 parameters, but the decoded result only attempts to return one, which will cause an error upon decoding.

As can be seen from the [router](https://etherscan.io/address/0x6131b5fae19ea4f9d964eac0408e4408b66337b5#code) on etherscan, the swap functions, regardless of the payload in use, always return two uint256 parameters, `returnAmount` and `gasUsed`.

```solidity
  function swapGeneric(SwapExecutionParams calldata execution)
    external
    payable
    returns (uint256 returnAmount, uint256 gasUsed)
  {
```

```solidity
  function swap(SwapExecutionParams calldata execution)
    external
    payable
    returns (uint256 returnAmount, uint256 gasUsed)
  {
```

```solidity
  function swapSimpleMode(
    IAggregationExecutor caller,
    SwapDescriptionV2 memory desc,
    bytes calldata executorData,
    bytes calldata clientData
  ) public returns (uint256 returnAmount, uint256 gasUsed) {
```

So when `kyberSwap` is called, the `result` data is attempted to be decoded but being that two parameters are returned, instead of the expected one, the decoding fails, reverting the entire transaction.

```solidity
    function kyberSwap(address assetIn, uint256 amountIn, bytes memory payload) internal returns (uint256) {
        IERC20(assetIn).forceApprove(address(kyberRouter), amountIn);
        (bool success, bytes memory result) = kyberRouter.call(payload);
        if (!success) _revertBytes(result);
        return abi.decode(result, (uint256)); // @audit
    } 
```
### Recommended Mitigation Steps

Decode the parameters properly.


## Assessed type

en/de-code
`modifyCollateralAndDebt()` doesn't work as intended when the vault is paused since debts can still be increased
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L422-L533


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L422-L533

```solidity
    function modifyCollateralAndDebt(
        address owner,
        address collateralizer,
        address creditor,
        int256 deltaCollateral,
        int256 deltaDebt
    ) public {
        if (
            // position is either more safe than before or msg.sender has the permission from the owner
            ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) ||
            // msg.sender has the permission of the collateralizer to collateralize the position using their cash
            (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) ||
            // msg.sender has the permission of the creditor to use their credit to repay the debt
            (deltaDebt < 0 && !hasPermission(creditor, msg.sender))
        ) revert CDPVault__modifyCollateralAndDebt_noPermission();

        // if the vault is paused allow only debt decreases
        if (deltaDebt > 0 || deltaCollateral != 0){
            _requireNotPaused();//@audit
        }

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

        uint256 newDebt;
        uint256 newCumulativeIndex;

        uint256 profit;
        int256 quotaRevenueChange;
        ..snip

        emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt);
    }
```

This function is used to modify a Position's collateral and debt balances, it checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM, etc.

Now one of the new changes in scope as hinted by the `@audit `tag is that in the case where the vault is paused only debt decreases should be accepted, however this is not enforced, considering a call is made to `_requireNotPaused()` that lacks any implementation across scope whatsoever, thereby allowing for debt to be increased even if the vault is paused.

## Impact

Modifying the position is broken, considering even if the revert that's supposed to occur [here](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L440) during an increase of debt whenever protocol is paused would not, since `_requireNotPaused()` lacks any implementation whatsoever.

## Recommended Mitigation Steps

Implement the `_requireNotPaused()` functionality and correctly check that the protocol is indeed not paused in the case where an attempt is made [to increase the debt](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L439).



## Assessed type

Context
When adding new quoted tokens, worse off rates for Loopfi is always used for all types of tokens
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/quotas/GaugeV3.sol#L231-L260


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/quotas/PoolQuotaKeeperV3.sol#L161-L179

```solidity
    function addQuotaToken(
        address token,
        uint16 rate
    )
        external
        override
        gaugeOnly // U:[PQK-3]
    {
        if (quotaTokensSet.contains(token)) {
            revert TokenAlreadyAddedException(); // U:[PQK-6]
        }

        // The rate will be set during a general epoch update in the gauge
        quotaTokensSet.add(token); // U:[PQK-5]
        totalQuotaParams[token].cumulativeIndexLU = 1; // U:[PQK-5]
        totalQuotaParams[token].rate = rate;
        emit AddQuotaToken(token); // U:[PQK-5]
    }
```

This function is used to add a new token. In the previous scope, when the token is added [the rates are set to `0` by default](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/quotas/PoolQuotaKeeperV3.sol#L172) up until a general epoch update before the real rate gets set for the token via https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/quotas/GaugeV3.sol#L77-L92, which is why [this bug case](https://github.com/code-423n4/2024-07-loopfi-findings/issues?q=is%3Aissue+is%3Aopen+Zero+rates+on+new+quoted+tokens+allow+an+attacker+to+take+an+interest+free+quota) existed where an attacker can request a very high quota and pay zero fees all through.

Now, in order to mitigate that, Loopfi now atomically updates the rates when adding the tokens. The issue, however, is that the rates being set are not favorable to the protocol. Consider `minRates` are hardcoded to be passed for whatever type of tokens to be added, see https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/quotas/GaugeV3.sol#L231-L260

```solidity
    /// @notice Adds a new quoted token to the gauge and sets the initial rate params
    ///         If token is not added to the quota keeper, adds it there as well
    /// @param token Address of the token to add
    /// @param minRate The minimal interest rate paid on token's quotas
    /// @param maxRate The maximal interest rate paid on token's quotas
    function addQuotaToken(
        address token,
        uint16 minRate,
        uint16 maxRate
    )
        external
        override
        nonZeroAddress(token) // U:[GA-04]
        configuratorOnly // U:[GA-03]
    {
        if (isTokenAdded(token) || token == IPoolV3(pool).underlyingToken()) {
            revert TokenNotAllowedException(); // U:[GA-04]
        }
        _checkParams({minRate: minRate, maxRate: maxRate}); // U:[GA-04]

        quotaRateParams[token] = QuotaRateParams({
            minRate: minRate,
            maxRate: maxRate,
            tota
Core interactions would still be accessible after protocol pause
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L198
https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L240-L304


# Vulnerability details


## Proof of Concept

By design it is expected that when the CDPVault.sol contract is paused by the Dao, core functionalities like the deposits and withdrawals should no longer be accessible.

Which is one of the reasons why the pauser role is granted to begin with in the constructor: https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L198

```solidity
        _grantRole(PAUSER_ROLE, config.pauseAdmin);
```

Issue however is that the core functionalities like deposit all lack the `whenNotPaused` modifier which then means that even if the protocol gets paused, users can still integrate the vault, see https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/CDPVault.sol#L240-L304

```solidity
    function deposit(address to, uint256 amount) external returns (uint256 tokenAmount) {
        tokenAmount = wdiv(amount, tokenScale);
        int256 deltaCollateral = toInt256(tokenAmount);
        modifyCollateralAndDebt({
            owner: to,
            collateralizer: msg.sender,
            creditor: msg.sender,
            deltaCollateral: deltaCollateral,
            deltaDebt: 0
        });
    }

    function withdraw(address to, uint256 amount) external returns (uint256 tokenAmount) {
        tokenAmount = wdiv(amount, tokenScale);
        int256 deltaCollateral = -toInt256(tokenAmount);
        modifyCollateralAndDebt({
            owner: to,
            collateralizer: msg.sender,
            creditor: msg.sender,
            deltaCollateral: deltaCollateral,
            deltaDebt: 0
        });
    }

    function borrow(address borrower, address position, uint256 amount) external returns (int256 deltaDebt) {
        uint256 scaledAmount = wdiv(amount, poolUnderlyingScale);
        deltaDebt = toInt256(scaledAmount);
        modifyCollateralAndDebt({
            owner: position,
            collateralizer: position,
            creditor: borrower,
            deltaCollateral: 0,
            deltaDebt: deltaDebt
        });

        return deltaDebt;
    }

    function repay(address borrower, address position, uint256 amount) external returns (int256 deltaDebt){
        uint256 scaledAmount = wdiv(amount, poolUnderlyingScale);
        deltaDebt = -toInt256(scaledAmount);
        modifyCollateralAndDebt({
            owner: position,
            collateralizer: position,
            creditor: borrower,
            deltaCollateral: 0,
            deltaDebt: deltaDebt
        });

        return deltaDebt;
    }
```

Also this seems to be a change to sort [this issue](https://github.com/code-423n4/2024-07-loopfi-findings/issues?q=is%3Aissue+is%3Aopen+WhenNotPaused+modifier+in+the+C
Accumulated rewards would be frozen when the token's index is `0`
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#L18-L19
https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManagerAbstract.sol#L52-L79
https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManager.sol#L118-L133


# Vulnerability details


<details>
<summary>Background</summary>

The contracts for taking care of the Pendle rewards was added to [the scope for this update audit](https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/scope.txt#L26), after conversations with the sponsors it was understood that this implementation (of `RewardManagerAbstract` & `RewardManager` ) was inspired/forked by/from the Pendle implementation found [here](https://github.com/pendle-finance/pendle-core-v2-public/blob/7e451be619353f57a8ab722234b8f9ebe0632836/contracts/core/RewardManager), problem however is that Loopfi's implementation is incomplete as it leaves out a core check which was included [in the native Pendle implementation at `RewardManagerAbstract.sol#L58`](https://github.com/pendle-finance/pendle-core-v2-public/blob/7e451be619353f57a8ab722234b8f9ebe0632836/contracts/core/RewardManager/RewardManagerAbstract.sol#L58) to ensure accumulated rewards are never blocked nor frozen.

After further discussions with the sponsors it was concluded that leaving out the check (i.e using `if (userIndex == index) continue;` instead of `if (userIndex == index || index == 0) continue;`) makes the implementation vulnerable to the very same case from the Q3 Spearbit Audit of Pendle where accumulated rewards could be frozen for some tokens linked [here](https://github.com/pendle-finance/pendle-core-v2-public/blob/7e451be619353f57a8ab722234b8f9ebe0632836/audits/Spearbit.pdf) which they had not known about prior to their audit with Code4rena.

</details>

## Proof of Concept

First note how the initial reward index has been set as `1`: https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManagerAbstract.sol#L18-L19

```solidity
    uint256 internal constant INITIAL_REWARD_INDEX = 1;

```

Now take a look at https://github.com/code-423n4/2024-10-loopfi/blob/d219f0132005b00a68f505edc22b34f9a8b49766/src/pendle-rewards/RewardManagerAbstract.sol#L52-L79

```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];
TransferAction.sol passes incorrect spender for ERC2612 PERMIT
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

TransferAction.sol passes incorrect spender for ERC2612 PERMIT. This will cause `PositionAction#increaseLever` calls that use ERC2616 permit to fail.

## Bug Description

This issue is similar to 2024-07 Loopfi contest https://github.com/code-423n4/2024-07-loopfi-findings/issues/152.

The second parameter of `IERC20Permit(token).safePermit()` is incorrect. It should be the spender of the allowance, instead of who the tokens are transferred to.

This means, it should be `address(this)` instead of `to`.

The impact of this issue is whereever `_transferFrom()` is called with `to != address(this)`, the ERC2616 permit transfer would fail. There exists a call in `PositionAction.sol` when increasing leverage, it needs to transfer the amount of upfront tokens from the proxy to PositionAction contract.

TransferAction.sol
```solidity
    function _transferFrom(
        address token,
        address from,
        address to,
        uint256 amount,
        PermitParams memory params
    ) internal {
        if (params.approvalType == ApprovalType.PERMIT2) {
            ...
        } else if (params.approvalType == ApprovalType.PERMIT) {
            // Consume a standard ERC20 permit message
            IERC20Permit(token).safePermit(
                from,
>               to,
                params.approvalAmount,
                params.deadline,
                params.v,
                params.r,
                params.s
            );
            IERC20(token).safeTransferFrom(from, to, amount);
        } else {
            // No signature provided, just transfer tokens.
            IERC20(token).safeTransferFrom(from, to, amount);
        }
    }
```

SafeERC20
```solidity
    function safePermit(
        IERC20Permit token,
        address owner,
>       address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        uint256 nonceBefore = token.nonces(owner);
        token.permit(owner, spender, value, deadline, v, r, s);
        uint256 nonceAfter = token.nonces(owner);
        require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
    }
```

PositionAction#increaseLever() will have an issue. Because `self` is not `address(this)` when triggered by delegatecall through a PRBProxy. `self` is the PositionAction.sol, while `address(this)` is the proxy's address.

PositionAction.sol
```solidity
    function increaseLever(
        LeverParams calldata leverParams,
        address upFrontToken,
        uint256 upFrontAmount,
        address collateralizer,
        PermitParams calldata permitParams
    ) external onlyDelegatecall {
        ...
        if (upFrontAmount > 0) {
            if (collateralizer == address(this)) {
                IERC20(upFrontToken).safeTransfer(self, upFrontAmount); // if tokens are on
`PositionAction4626.sol#_onWithdraw` should withdraw from position CDPVault position instead of `address(this)`.
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

`PositionAction4626.sol#_onWithdraw` does not withdraw from the correct position - it should withdraw from `position` instead of `address(this)`. 

## Bug Description

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

Only the bug in the `_onDeposit()` was fixed, but not the one in `_onWithdraw()`.

`PositionAction4626.sol#_onWithdraw` does not withdraw from the correct position - it should withdraw from `position` instead of `address(this)`.

```solidity
    function _onDeposit(address vault, address position, address src, uint256 amount) internal override returns (uint256) {
        address collateral = address(ICDPVault(vault).token());

        // if the src is not the collateralToken, we need to deposit the underlying into the ERC4626 vault
        if (src != collateral) {
            address underlying = IERC4626(collateral).asset();
            IERC20(underlying).forceApprove(collateral, amount);
            amount = IERC4626(collateral).deposit(amount, address(this));
        }

        IERC20(collateral).forceApprove(vault, amount);

        // @audit-note: This was fixed.
        return ICDPVault(vault).deposit(position, amount);
    }


    function _onWithdraw(
        address vault,
        address /*position*/,
        address dst,
        uint256 amount
    ) internal override returns (uint256) {
        // @audit-note: This is still a bug.
@>      uint256 collateralWithdrawn = ICDPVault(vault).withdraw(address(this), amount);

        // if collateral is not the dst token, we need to withdraw the underlying from the ERC4626 vault
        address collateral = address(ICDPVault(vault).token());
        if (dst != collateral) {
            collateralWithdrawn = IERC4626(collateral).redeem(collateralWithdrawn, address(this), address(this));
        }

        return collateralWithdrawn;
    }
```

## Proof of Concept

N/A

## Tools Used

Manual Review

## Recommended Mitigation Steps

```diff
-       uint256 collateralWithdrawn = ICDPVault(vault).withdraw(address(this), amount);
+       uint256 collateralWithdrawn = ICDPVault(vault).withdraw(position, amount);
```


## Assessed type

Other