xanoutas/solidity-security-findings huggingface dataset:
PositionAction20, PositionAction4626, PositionActionPendle does not handle tokenScale for CDPVault.withdraw()
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

If tokenScale != 18, all PositionAction withdraw features will not work.

## Bug Description

*This issue was reported and confirmed in the previous contest, also by WatchPug reports. However, it is still not fixed.*

The token scale is incorrect for all `onWithdraw()` functions. The scale used for `CDPVault.withdraw(position, amount)` should be tokenScale, and the return value is WAD.

However, all use cases got them reversed, and they assume `CDPVault.withdraw(position, amount)` should be WAD, and the return value is tokenScale.

This would be an issue if tokenScale != 1e18.

PositionAction20.sol
```solidity
    /// @param amount Amount of collateral to withdraw [wad]
    /// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(address vault, address position, address /*dst*/, uint256 amount) internal override returns (uint256) {
        return ICDPVault(vault).withdraw(position, amount);
    }
```

PositionAction4626.sol
```solidity
    /// @param amount Amount of collateral to withdraw [wad]
    /// @return Amount of collateral withdrawn [CDPVault.tokenScale()]
    function _onWithdraw(address vault, address /*position*/, address dst, uint256 amount) internal override returns (uint256) {
        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;
    }
```

PositionActionPendle.sol
```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(
`PositionActionPendle.sol#_onWithdraw` does not have slippage parameter minOut set.
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

When performing withdraws on `PositionActionPendle` and exiting Pendle pools, users may lose funds due to not setting slippage.

## Bug Description

*Note: This is a new issue that was introduced by the latest code diff (Doesn't exist in the 2024-07 Loopfi contest).*

The dataflow for withdrawing on `PositionActionPendle` is: 1) User withdraws collateral (which is a Pendle token) from CDPVault, 2) User performs pendle pool exit.

The issue is in step 2, since `minOut` is set to 0, users may receive less output tokens than expected.

```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, // @audit-bug: No slippage.
                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;
    }
```

Also note that this is similar to the 2024-07 Loopfi contest finding https://github.com/code-423n4/2024-07-loopfi-findings/issues/38, which also talks about slippage in ERC4626. However, this Pendle withdraw exit pool code is new, and not existant in the last contest. Thus this should be considered a new bug.

## Proof of Concept

N/A

## Tools Used

Manual Review

## Recommended Mitigation Steps

Allow user to set a minOut parameter for withdraw functions, especially for Pendle position and ERC4626 position.


## Assessed type

Other
PositionAction.sol#decreaseLever with EXACT_IN primary swaps would always brick if swapFee is non-zero.
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

PositionAction.sol#decreaseLever with EXACT_IN primary swaps would always brick if swapFee is non-zero.

## Bug Description

*Note: This is a new issue that was introduced by the latest code diff (Doesn't exist in the 2024-07 Loopfi contest).*

When performing decreaseLever, in the flashloan fallback `onCreditFlashLoan()`, we need to payoff the flashloan along with the fees.

For EXACT_IN primary swaps, if extra underlying tokens are swapped out, they are sent back to the vault as payed off debt. However, the issue here is when calculating the residual amount, it does not take flashloan fees into account.

The buggy line is `uint256 residualAmount = swapAmountOut - subDebt;`, where `subDebt` is the amount of token taken from flashloan, but fees also need to be subtracted here.

```solidity
    function onCreditFlashLoan(
        address /*initiator*/,
        uint256 /*amount*/,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        if (msg.sender != address(flashlender)) revert PositionAction__onCreditFlashLoan__invalidSender();
        (LeverParams memory leverParams, uint256 subCollateral, address residualRecipient) = abi.decode(
            data,
            (LeverParams, uint256, address)
        );

        uint256 subDebt = leverParams.primarySwap.amount;
        underlyingToken.forceApprove(address(leverParams.vault), subDebt + fee);
        // sub collateral and debt
        ICDPVault(leverParams.vault).modifyCollateralAndDebt(
            leverParams.position,
            address(this),
            address(this),
            0,
            -toInt256(subDebt)
        );
        
        // withdraw collateral and handle any CDP specific actions
        uint256 withdrawnCollateral = _onDecreaseLever(leverParams, subCollateral);

        if (leverParams.primarySwap.swapType == SwapType.EXACT_IN) {
            leverParams.primarySwap.amount = withdrawnCollateral;

            bytes memory swapData = _delegateCall(
                address(swapAction),
                abi.encodeWithSelector(swapAction.swap.selector, leverParams.primarySwap)
            );

            uint256 swapAmountOut = abi.decode(swapData, (uint256));
            
            // @audit-bug: FEES ARE NOT TAKEN INTO ACCOUNT.
@>          uint256 residualAmount = swapAmountOut - subDebt;

            // sub collateral and debt
            if (residualAmount > 0) {
                underlyingToken.forceApprove(address(leverParams.vault), residualAmount);
                ICDPVault(leverParams.vault).modifyCollateralAndDebt(
                    leverParams.position,
                    address(this),
                    address(this),
                    0,
                    -toInt256(residualAmount)
                );
            }
        } else {
            ...
        }

        underlyingTok
PositionAction.sol#decreaseLever with EXACT_OUT primary swaps would always brick if swapFee is non-zero.
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

PositionAction.sol#decreaseLever with EXACT_OUT primary swaps would always brick if swapFee is non-zero.

## Bug Description

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

When performing decreaseLever, if the primary swap is EXACT_OUT, this means we are swapping collateral token an exact amount of debt token.

However, the output amount `leverParams.primarySwap.amount` is always equal to the flashLoan amount. This means we can only repay the original flashloan amount. But if fees is non-zero, the repayment will always fail.

Note that the team made the change where `fee` is approved in `onCreditFlashLoan`, but since the amount of tokens doesn't include fees, it would still not work.

```solidity
    function decreaseLever(
        LeverParams memory leverParams,
        uint256 subCollateral,
        address residualRecipient
    ) external onlyDelegatecall onlyRegisteredVault(leverParams.vault) {
        // validate the primary swap
        if (leverParams.primarySwap.recipient != self) revert PositionAction__decreaseLever_invalidPrimarySwap();

        IPermission(leverParams.vault).modifyPermission(leverParams.position, self, true);

        if (leverParams.primarySwap.swapType == SwapType.EXACT_OUT) {
            uint256 totalDebt = ICDPVault(leverParams.vault).virtualDebt(leverParams.position);
            leverParams.primarySwap.amount = min(totalDebt, leverParams.primarySwap.amount);

            // residual recipient is required if the primary swap is an exact out swap
            if (residualRecipient == address(0)) {
                revert PositionAction__decreaseLever_invalidResidualRecipient();
            }
        }

        // take out credit flash loan
        flashlender.creditFlashLoan(
            ICreditFlashBorrower(self),
@>          leverParams.primarySwap.amount,
            abi.encode(leverParams, subCollateral, residualRecipient)
        );

        IPermission(leverParams.vault).modifyPermission(leverParams.position, self, false);
    }

    function onCreditFlashLoan(
        address /*initiator*/,
        uint256 /*amount*/,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        if (msg.sender != address(flashlender)) revert PositionAction__onCreditFlashLoan__invalidSender();
        (LeverParams memory leverParams, uint256 subCollateral, address residualRecipient) = abi.decode(
            data,
            (LeverParams, uint256, address)
        );

        uint256 subDebt = leverParams.primarySwap.amount;
        underlyingToken.forceApprove(address(leverParams.vault), subDebt + fee);
        // sub collateral and debt
        ICDPVault(leverParams.vault).modifyCollateralAndDebt(
            leverP
Locking.sol does not handle Fee-on-transfer tokens
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-loopfi/blob/main/src/Locking.sol#L59-L66


# Vulnerability details


## Impact

If the underlying token for Locking.sol is a Fee-on-transfer token, the protocol would not work as expected. Users may be unable to withdraw their tokens.

## Bug Description

*Note: This is a new issue that was introduced by the latest code diff (Doesn't exist in the 2024-07 Loopfi contest).*

When users are calling `deposit()`, the `deposits[msg.sender]` is updated with the `_amount` that user passed in. However, `_amount` may not be the exact amount of tokens the protocol received, due to fee-on-transfer.

This means for each user, the accounting of his token is larger than expected. This would be an issue when user tries to withdraw tokens.

```solidity
    function deposit(uint256 _amount) external {
        if (_amount == 0) revert AmountIsZero();

@>      deposits[msg.sender].amount += _amount;
        token.safeTransferFrom(msg.sender, address(this), _amount);

        emit Deposited(msg.sender, _amount);
    }

    function withdraw() external {
        uint256 _amount;
        if (cooldownPeriod > 0) {
            if (deposits[msg.sender].cooldownAmount == 0) revert NoTokensInCooldown();
            if (block.timestamp < deposits[msg.sender].cooldownStart + cooldownPeriod) revert CooldownPeriodNotPassed();
            _amount = deposits[msg.sender].cooldownAmount;
        } else {
            _amount = deposits[msg.sender].amount;
        }

        deposits[msg.sender].amount -= _amount;
        deposits[msg.sender].cooldownAmount = 0;
        deposits[msg.sender].cooldownStart = 0;
        token.safeTransfer(msg.sender, _amount);

        emit Withdrawn(msg.sender, _amount);
    }
```

## Proof of Concept

N/A

## Tools Used

Manual Review

## Recommended Mitigation Steps

```diff
    function deposit(uint256 _amount) external {
        if (_amount == 0) revert AmountIsZero();

-       deposits[msg.sender].amount += _amount;
+       uint256 balanceBefore = token.balanceOf(address(this));
        token.safeTransferFrom(msg.sender, address(this), _amount);
+       uint256 balanceAfter = token.balanceOf(address(this));
+       deposits[msg.sender].amount += balanceAfter - balanceBefore;

        emit Deposited(msg.sender, _amount);
    }
```


## Assessed type

ERC20
`PositionAction.sol#onCreditFlashLoan` may end up with stuck funds for EXACT_IN primary swaps.
Audit: 2024-10-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

`PositionAction.sol#onCreditFlashLoan` may end up with stuck funds for EXACT_IN primary swaps. User may lose these funds.

## Bug Description

*Note: This is a new issue that was introduced by the latest code diff (Doesn't exist in the 2024-07 Loopfi contest).*

When conducting a decreaseLever action, the final swap inside `onCreditFlashLoan()` is from collateral token to debt token. If the swap is EXACT_IN type, this means all collateral token is used for the swap. The output tokens are then split to two parts:

1. Repay the flashloan (and fees)
2. Send back to CDPVault to repay debt

However, the second part have some issues. Mainly because if the repaid amount is larger than debt amount, the repayed amount will be capped to the debt amount (See CDPVault.sol code below). This means there may be some debt tokens ending up dangling in the PositionAction.sol contract, which the user does not have access to.

To explain a bit more, this is a reasonable scenario, because the amount of collateral tokens used for swap comes from `uint256 withdrawnCollateral = _onDecreaseLever(leverParams, subCollateral);`, and for PositionAction4626, `_onDecreaseLever()` supports gathering collateral tokens by exiting from pools (e.g. Balancer). This means it is totally possible that the amount of collateral tokens used for swap values more than the user's debt in CDPVault.

PositionAction.sol
```solidity
    function onCreditFlashLoan(
        address /*initiator*/,
        uint256 /*amount*/,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        if (msg.sender != address(flashlender)) revert PositionAction__onCreditFlashLoan__invalidSender();
        (LeverParams memory leverParams, uint256 subCollateral, address residualRecipient) = abi.decode(
            data,
            (LeverParams, uint256, address)
        );

        uint256 subDebt = leverParams.primarySwap.amount;
        underlyingToken.forceApprove(address(leverParams.vault), subDebt + fee);
        // sub collateral and debt
        ICDPVault(leverParams.vault).modifyCollateralAndDebt(
            leverParams.position,
            address(this),
            address(this),
            0,
            -toInt256(subDebt)
        );
        
        // withdraw collateral and handle any CDP specific actions
@>      uint256 withdrawnCollateral = _onDecreaseLever(leverParams, subCollateral);

        if (leverParams.primarySwap.swapType == SwapType.EXACT_IN) {
            leverParams.primarySwap.amount = withdrawnCollateral;

            bytes memory swapData = _delegateCall(
                address(swapAction),
                abi.encodeWithSelector(swapAction.swap.selector, leverParams.primarySwap)
            );

            uint256 swapAmountOut = abi.decode(swapData, (uint256));
            uint256 residualAmount = swap
Upgraded Q -> 3 from #535 [1730296812865]
Audit: 2024-07-loopfi-findings
Severity: HIGH

Judge has assessed an item in Issue #535 as 3 risk. The relevant finding follows:

 [Low-6] Shouldn’t here takeCollateral calculated after minusing penalty from repayAmount
According to my understanding During the liquidation process in the Loop protocol, penalties are applied to the position being liquidated to mitigate against profitable self-liquidations.

Here’s how the penalty is calculated and applied:

specific percentage deducted from the repay amount during liquidation
The penalty is calculated as a percentage of the amount the liquidator wants to repay. The specific percentage is defined in the LiquidationConfig of the CDPVault smart contract.
while deep diving i see that

takeCollateral, amount of collateral that will transfered to a Liquidator is calculated based on repayAmount
after that deltaDebt
after that penalty calculated
        uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
        uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty); 
        uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L529-L531

So basically Token In here
poolUnderlying.safeTransferFrom(msg.sender, address(pool), repayAmount - penalty)
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L539

poolUnderlying.safeTransferFrom(msg.sender, address(pool), penalty);
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L568

Token Out here

token.safeTransfer(msg.sender, takeCollateral);
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L565

According to my understanding here penlty has no effect on takeCollateral calculation. As it calculated after takeCollateral calculation and token send to contract in 2 step.

Liquidator getting collateral according to his entered repayAmount, so no penalty effect on colateral he receiving

Mitigation
I think takeCollateral should calculated after minusing penalty from repay amount like follow

+       uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);
+       uint256 takeCollateral = wdiv(repayAmount - penalty, discountedPrice);
Upgraded Q -> 2 from #545 [1730294682585]
Audit: 2024-07-loopfi-findings
Severity: HIGH

Judge has assessed an item in Issue #545 as 2 risk. The relevant finding follows:

 [L-01] Potential over-distribution of rewards in claim function
The claim function in the AuraVault contract may lead to over-distribution of rewards due to incorrect handling of incentives. The function calculates amountIn using _previewReward(amounts[0], amounts[1], _config), which already considers the incentives. However, when distributing the rewards, the incentives are applied again, leading to potential over-distribution.

Relevant code:


function claim(uint256[] memory amounts, uint256 maxAmountIn) external returns (uint256 amountIn) {
    // Claim rewards from Aura reward pool
    IPool(rewardPool).getReward();

    // Compute assets amount to be sent to the Vault
    VaultConfig memory _config = vaultConfig;
    amountIn = _previewReward(amounts[0], amounts[1], _config);

    // Transfer assets to Vault
    require(amountIn <= maxAmountIn, "!Slippage");
    IERC20(asset()).safeTransferFrom(msg.sender, address(this), amountIn);

    // Compound assets into "asset" balance
    IERC20(asset()).safeApprove(rewardPool, amountIn);
    IPool(rewardPool).deposit(amountIn, address(this));

    // Distribute BAL rewards
    IERC20(BAL).safeTransfer(_config.lockerRewards, (amounts[0] * _config.lockerIncentive) / INCENTIVE_BASIS);
    IERC20(BAL).safeTransfer(msg.sender, amounts[0]);

    // Distribute AURA rewards
    if (block.timestamp <= INFLATION_PROTECTION_TIME) {
        IERC20(AURA).safeTransfer(_config.lockerRewards, (amounts[1] * _config.lockerIncentive) / INCENTIVE_BASIS);
        IERC20(AURA).safeTransfer(msg.sender, amounts[1]);
    } else {
        // after INFLATION_PROTECTION_TIME
        IERC20(AURA).safeTransfer(_config.lockerRewards, IERC20(AURA).balanceOf(address(this)));
    }

    emit Claimed(msg.sender, amounts[0], amounts[1], amountIn);
}
Recommendation: