xanoutas/solidity-security-findings huggingface dataset:
The liquidator's collateral reward amount will be smaller than expected because it is not properly scaled with tokenScale.
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/4f508781a49ffa53511e7e5ed6cda0ff0eb5bdc5/src/PoolV3.sol#L182-#L184
https://github.com/code-423n4/2024-07-loopfi/blob/4f508781a49ffa53511e7e5ed6cda0ff0eb5bdc5/src/CDPVault.sol#L529
https://github.com/code-423n4/2024-07-loopfi/blob/4f508781a49ffa53511e7e5ed6cda0ff0eb5bdc5/src/CDPVault.sol#L601


# Vulnerability details

## Impact
The liquidator's collateral reward amount will be smaller than expected because it is not properly scaled with tokenScale.

## Vulnerability Details

The liquidator's collateral reward amount will be smaller than expected if the collateral token's decimals are not not properly scaled with tokenScale. Liquidation is crucial and all unsafe positions must be liquidated to maintain the protocol's solvency. However, because the liquidator's collateral rewards are correctly scaled to the collateral's token decimals using tokenScale, users are not incentivized to liquidate unhealthy positions.

Collateral tokens are scaled to prevent their value from being too small or inconsistent. `takeCollateral`, the liquidator's reward is gotten from `repayAmount` which is not scaled. This might make it to be smaller than expected.
## Proof of Concept
Underlying tokens are set to 18 decimals at deployment. [PoolV3.sol#L182-#L184](https://github.com/code-423n4/2024-07-loopfi/blob/4f508781a49ffa53511e7e5ed6cda0ff0eb5bdc5/src/PoolV3.sol#L182-#L184)
```solidity
        if (ERC20(underlyingToken_).decimals() != 18) {
            revert IncompatibleDecimalsException();
        }
```

But as seen below, `repayAmount` and `takeCollateral` are not scaled. This will lead to the liquidator receiving a smaller reward amount.
[CDPVault.sol#L529](https://github.com/code-423n4/2024-07-loopfi/blob/4f508781a49ffa53511e7e5ed6cda0ff0eb5bdc5/src/CDPVault.sol#L529)
```solidity
    function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused {
        // validate params
        if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters();
            ...
@>      uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
            ...
        // transfer the collateral amount from the vault to the liquidator
        // @audit takeCollateral is not scaled, hence it might be smaller.
        token.safeTransfer(msg.sender, takeCollateral);
        ...
    }
```
`takeCollateral` is also not scaled when liquidating positions with bad debt, which makes the liquidator's collateral reward to be smaller than expected.

[CDPVault.sol#L601](https://github.com/code-423n4/2024-07-loopfi/blob/4f508781a49ffa53511e7e5ed6cda0ff0eb5bdc5/src/CDPVault.sol#L601)
```solidity
    function liquidatePositionBadDebt(address owner, uint256 repayAmount) external whenNotPaused {
        // validate params
        if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters();

        // compute collateral to take, debt to
`AuraVault::_chainlinkSpot` can result in stale prices
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When users claim their Aura RewardsPool rewards, using `AuraVault::claim`, the protocol checks the current price of the `BAL` token, which is fetched from a Chainlink feed. In return, users would have to deposit some WETH according to some calculations done in `_previewReward`. 
However, `_chainlinkSpot` doesn't check the last `updatedAt` of the price feed before confirming that the price is valid.

This is wrong and could result in stale BAL prices, affecting users claiming their rewards.


## Tools Used

Manual review

## Recommended Mitigation Steps

Introduce a stale period and compare the updated date with that period when returning the spot price.

```diff
function _chainlinkSpot() private view returns (uint256 price) {
    bool isValid;
    try AggregatorV3Interface(BAL_CHAINLINK_FEED).latestRoundData() returns (
        uint80 /*roundId*/,
        int256 answer,
        uint256 /*startedAt*/,
-       uint256 /*updatedAt*/,
+       uint256 updatedAt,
        uint80 /*answeredInRound*/
    ) {
        price = wdiv(uint256(answer), BAL_CHAINLINK_DECIMALS);
-       isValid = (price > 0);
+       isValid = (price > 0 && block.timestamp - updatedAt <= STALE_PERIOD);
    } catch {}

    if (!isValid) revert AuraVault__chainlinkSpot_invalidPrice();
}
```





## Assessed type

Oracle
persisting RewardPerSecond can DOS reward claims
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/1965b8e310ff99078fdde1ea85d18fb078fb4232/src/reward/ChefIncentivesController.sol#L443


# Vulnerability details

## Impact
The purpose of the persistRewardsPerSecond flag is to allow the owner to persist the reward per second used to calculate rewards. This can be called to disable making the RPS follow the emission schedule.

The rewards per second can be persisted by owner via setting persistRewardsPerSecond to true in  ChefIncentivesController.sol by calling the setRewardsPerSecond. In addition, The default value for persistRewardsPerSecond is set to true on contract initialization. 

RPS helps determine how much rewards are available since last reward time and also the accrued reward per share. It is used by the following function:

````
    /**
     * @dev Returns new rewards since last reward time.
     * @param pool pool info
     * @param _totalAllocPoint allocation point of the pool
     */
    function _newRewards(
        VaultInfo memory pool,
        uint256 _totalAllocPoint
    ) internal view returns (uint256 newReward, uint256 newAccRewardPerShare) {
        uint256 lpSupply = pool.totalSupply;
        if (lpSupply > 0) {
            uint256 duration = block.timestamp - pool.lastRewardTime;
            uint256 rawReward = duration * rewardsPerSecond;

            uint256 rewards = availableRewards();
             console.log("available rewards are:", rewards);
            if (rewards < rawReward) {
                rawReward = rewards;
            }
            newReward = (rawReward * pool.allocPoint) / _totalAllocPoint;
           
            newAccRewardPerShare = (newReward * ACC_REWARD_PRECISION) / lpSupply;
        }
    }
````
The above confirms that if RPS is 0, it means there will be no rewards to distribute to users.

The problem with setting it to true becomes evident if we look at the function below:

````
function _updateEmissions() internal {
        if (block.timestamp > endRewardTime()) {
            _massUpdatePools();
            lastRPS = rewardsPerSecond;
            rewardsPerSecond = 0;
            return;
        }
        setScheduledRewardsPerSecond();
    }
````


The function above is responsible for updating em missions during an epoch. its triggered in two locations:

1. when a user makes a call to claim rewards.
2. or when the owner wants to add a new pool.

The end of the epoch is determined by the endRewardTime function, if it has been reached the _updateEmissions resets RPS to 0. It also makes a call to the setScheduledRewardsPerSecond function which ensures RPS is set according to the emmision schedule. 

````   
 /**
     * @dev Updates RPS.
     */
    function setScheduledRewardsPerSecond() internal {
        if (!persistRewardsPerSecond) {
            uint256 length = emissionSchedule.length;
            uint256 i = emissionScheduleIndex;
            uint128 offset = uint128(block.timestamp - startTime);
            for (; i <
`PositionAction` allows leveraging positions on unregistered vaults
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users can interact with their positions using the `PositionAction` contract, all actions fired from `PositionAction` should only hit registered vaults, this is done by adding the `onlyRegisteredVault` modifier. However, both `increaseLever` and `decreaseLever` functions miss this modifier.

This allows users to call these functions on unregistered vaults.

## Proof of Concept

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

```solidity
function test_leverageUnregisteredVault() public {
    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);

    // Vault is registered
    assertTrue(vaultRegistry.isVaultRegistered(address(vault)));

    vm.startPrank(user);
    // Deposit 1k ETH collateral
    token.approve(address(vault), type(uint256).max);
    vault.deposit(address(userProxy), depositAmount);

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

    // Collateral is 1k ETH, debt is 200 ETH
    (uint256 collateral, uint256 debt, , , , ) = vault.positions(address(userProxy));
    assertEq(collateral, depositAmount);
    assertEq(debt, borrowAmount);

    // Unregister the vault
    vaultRegistry.removeVault(ICDPVault(address(vault)));
    assertFalse(vaultRegistry.isVaultRegistered(address(vault)));

    // Increase leverage
    vm.prank(user);
    userProxy.execute(
        address(positionAction),
        abi.encodeWithSelector(
            positionAction.increaseLever.selector,
            LeverParams({
                position: address(userProxy),
                vault: address(vault),
                collateralToken: address(token),
                primarySwap: SwapParams({
                    swapProtocol: SwapProtocol.BALANCER,
                    swapType: SwapType.EXACT_IN,
                    assetIn: address(underlyingToken),
                    amount: borrowAmount,
                    limit: 0,
                    recipient: address(positionAction),
                    deadline: block.timestamp,
                    args: abi.encode(weightedPoolIdArray, assets)
                }),
                auxSwap: emptySwap,
                auxAction: emptyPoolActionParams
            }),
            address(0),
            0,
            address(user),
            emptyPermitParams
        )
    );

    // Collate
Chainlink oracle will return the wrong price for asset if the underlying aggregator hits minAnswer.
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/oracle/ChainlinkOracle.sol#L106


# Vulnerability details

## Impact
Chainlink oracle will return the wrong price for asset if the underlying aggregator hits minAnswer. In the event that an asset crashes (i.e. LUNA) the protocol can be manipulated to give out loans at an inflated price because there is no check for minAnswer from the aggregator. 

## Vulnerability details
Note: This issue is different from [analyzer M-4](https://github.com/code-423n4/2024-07-loopfi/blob/main/4naly3er-report.md#m-4-chainlinks-latestrounddata-might-return-stale-or-incorrect-results) as it is caused by a lack of check for minPrice/maxPrice from the agregator. Examples of issues related to `analyzer M-4 ` are these issue: [1](https://github.com/code-423n4/2022-12-tigris-findings/issues/655), [2](https://github.com/code-423n4/2022-10-inverse-findings/issues/584) and [3](https://github.com/sherlock-audit/2023-02-blueberry-judging/issues/94)

Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened to [Venus on BSC when LUNA imploded](https://rekt.news/venus-blizz-rekt/).
## Proof of Concept
As seen below, there is no check for minPrice/maxPrice from the aggregator. [ChainlinkOracle.sol#L106](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/oracle/ChainlinkOracle.sol#L106)
```solidity
    function _fetchAndValidate(address token) internal view returns (bool isValid, uint256 price) {
        Oracle memory oracle = oracles[token];
        try AggregatorV3Interface(oracle.aggregator).latestRoundData() returns (
            uint80 /*roundId*/,
            int256 answer,
            uint256 /*startedAt*/,
            uint256 updatedAt,
            uint80 /*answeredInRound*/
        ) {
            isValid = (answer > 0 && block.timestamp - updatedAt <= oracle.stalePeriod);
@>          return (isValid, wdiv(uint256(answer), oracle.aggregatorScale));
        } catch {
            // return the default values (false, 0) on failure
        }

    }
```

There is also no check for minPrice/maxPrice from the aggregator in the `PendleLPOracle`. [PendleLPOracle.sol#L121](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/oracle/PendleLPOracle.sol#L121)

```solidity
    function _fetchAndValidate() internal view returns (bool isValid, uint256 price) {
        try AggregatorV3Interface(aggregator).latestRoundData() returns (
            uint80 roundId, int256 answer, uint256 /*startedAt*/, uint256 updatedAt, uint80 answeredInRoun
Anyone can deposit/withdraw when `CDPVault` is Paused
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
[`CDPVault`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L367-L460) implements a pausing functionality through [`Pause`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/utils/Pause.sol) contract which is meant to allow the halting of some functions in the contract. In `CDPVault`'s case the functions that have the `whenNotPaused` modifier are:
* [`setParameter`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L195-L202)
* [`deposit`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L223-L233)
* [`withdraw`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L239-L249)
* [`liquidatePosition`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L509-L574)
* [`liquidatePositionBadDebt`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L579-L632)

However, if someone pauses the contract, users can still make deposits and withdrawals via the public [`modifyCollateralAndDebt`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L367-L460) function. This happens because the function `hasPermission` returns `true` if `msg.sender` equals the `owner`, i.e. the two inputs of the function are the same. This should not happen since it can have grave consequences in the event that a hack is occurring or an exploit is discovered.

## Proof of Concept
Bellow is the coded PoC for the bug. Paste it in [`CDPVault.t.sol`](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/test/unit/CDPVault.t.sol):
```solidity
    function test_canDepositWithContractPaused_cid() public {
        address bob = makeAddr("bob");
        CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 1 ether, 1 ether);
        createGaugeAndSetGauge(address(vault));
        
        // bob creates a position
        uint256 depositAmount = 100 ether;
        deal(address(token), bob, depositAmount);
        // vm.startPrank(bob);
        // token.approve(address(vault), 100 ether);
        // vault.deposit(bob, 100 ether);
        // vm.stopPrank();

        // contract is paused
        vault.pause();

        //bob can still make deposits
        vm.startPrank(bob);
        token.approve(address(vault), depositAmount);
        int256 deltaCollateral = int256(depositAmount);
        vault.modifyCollateralAndDebt(bob, bob, bob, deltaCollateral, 0);
        vm.stopPrank();    
        (uint256 collateral, , , , , ) = vault.position
Incorrect spender in ERC20 permit approval prevents token transfers
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/proxy/TransferAction.sol#L65-L83


# Vulnerability details

## Impact
The current implementation uses the transfer recipient (`to`) as the spender in the ERC20 permit approval which is incorrect. This error prevents the contract from successfully transferring tokens using the permit mechanism, as the contract itself is not granted the necessary approval. As a result, all transfers using the PERMIT approval type will fail, rendering this feature non-functional.

## Proof of Concept
Let's  look at tthe `_transferFrom` function: https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/proxy/TransferAction.sol#L65-L83

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

Here, the `safePermit` function is called with `to` as the second argument, which represents the spender. However, `to` is actually the recipient of the transfer, not the entity that should be approved to spend tokens. The correct spender should be the contract itself (`address(this)`), as it's the one executing the `transferFrom` operation.

This mistake means that the subsequent `safeTransferFrom` call will fail because the contract (`address(this)`) has not been granted approval to spend tokens on behalf of the `from` address.


## Tools Used
Manual review

## Recommended Mitigation Steps
Replace the `to` address with `address(this)` in the `safePermit` call:

```diff
} else if (params.approvalType == ApprovalType.PERMIT) {
    // Consume a standard ERC20 permit message
    IERC20Permit(token).safePermit(
        from,
-       to,
+       address(this),
        params.approvalAmount,
        params.deadline,
        params.v,
        params.r,
        params.s
    );
    IERC20(token).safeTransferFrom(from, to, amount);
}
```


## Assessed type

Token-Transfer
CDPVault.sol: Temporary DOS on Positions with Accrued Interest
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The design of the code allows any user to pay the debt of a position, including their own due to this the attack can be performed permissionlessly at any time. There is a bug in the `calcDecrease` function that allows an arbitrary user to brick a position for a certain number of days or hours relative to the interest accrued by paying a portion of the accrued interest. This bug arises when the `newCumulativeIndex` is calculated in `calcDecrease`. When paying a portion of the interest an attacker can break the implied invariant `cumulativeIndexNow >= cumulativeIndexLastUpdate` due to an incorrect implementation of the calcDecrease function from the CreditLogic library.

## Proof of Concept 
The test below shows that the modifyCollateralAndDebt function will revert until the baseInterestNow is more than the cumulativeIndexLastUpdate. 

Note that this will also revert when using LiquidatePosition as well as LiquidatePositionBadDebt.


Add the test below to `2024-07-loopfi/src/test/unit/CDPVault.t.sol:`
```solidity
 function test_closePosition() public {
        CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 1.0 ether, 0);
        createGaugeAndSetGauge(address(vault));

        // create position
        token.mint(address(this), 200 ether);
        token.approve(address(vault), 200 ether);
        vault.modifyCollateralAndDebt(address(this), address(this), address(this), 200 ether, 150 ether);
        assertEq(credit(address(this)), 150 ether);

        (, uint256 accruedInterest, uint256 cumulativeQuotaInterest) = vault.getDebtInfo(address(this));
        vm.prank(address(gauge));
        quotaKeeper.updateRates();
        assertEq(accruedInterest, 0, "accruedInterest before");
        assertEq(cumulativeQuotaInterest, 0, "cumulativeQuotaInterest before");
        assertEq(virtualDebt(vault, address(this)), 150 ether + accruedInterest, "Not correct");
        console.log(vault.quotasInterest(address(this)));
        assertEq(liquidityPool.quotaRevenue(), liquidityPool.totalBorrowed() / 1000, "quotaRevenue before");

        vm.warp(block.timestamp + 95 days);
        (, accruedInterest, cumulativeQuotaInterest) = vault.getDebtInfo(address(this));
        assertGt(accruedInterest, 0, "accruedInterest after 100 days");
        assertGt(cumulativeQuotaInterest, 0, "cumulativeQuotaInterest after 100 days");
        assertEq(liquidityPool.quotaRevenue(), liquidityPool.totalBorrowed() / 1000, "quotaRevenue after 100 days");
        vm.prank(address(gauge));
        quotaKeeper.updateRates();
        assertEq(
            liquidityPool.quotaRevenue(),
            liquidityPool.totalBorrowed() / 1000,
            "quotaRevenue after 100 days and updating rates (no rate change)"
        );
        vm.prank(address(voter));
        bytes memory data = abi.encode(address