xanoutas/solidity-security-findings huggingface dataset:
Collateral value can be manipulated due to missing circuit breaker checks
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Oracle.sol#L336-L340


# Vulnerability details

## Impact

`V3Oracle::getValue()` will return an incorrect amount, over-inflating the `value` of the attacker’s collateral. The attacker can utilise this to borrow significantly more than they should.

## Proof of Concept

A Chainlink Oracle is used to fetch the price of a pool in `V3Oracle::getChainlinkPriceX96()`, [V3Oracle.sol#L337-L340](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Oracle.sol#L337-L340):

```solidity
(, int256 answer,, uint256 updatedAt,) = feedConfig.feed.latestRoundData();
if (updatedAt + feedConfig.maxFeedAge < block.timestamp || answer < 0) {
    revert ChainlinkPriceError();
}
```

There is no check to ensure the `answer` returned by `latestRoundData()` is not above or below a certain price. Some Chainlink aggregators have a built in [circuit breaker](https://blog.chain.link/defi-circuit-breakers/#what_is_a_circuit_breaker_in_defi_). This is a predetermined price band set in a pool’s [AccessControlledOffchainAggregator](https://github.com/smartcontractkit/libocr/blob/master/contract/AccessControlledOffchainAggregator.sol) contract, [OffchainAggregator.sol#L56-L59](https://github.com/smartcontractkit/libocr/blob/9e4afd8896f365b964bdf769ca28f373a3fb0300/contract/OffchainAggregator.sol#L56-L59):

```solidity
// Lowest answer the system is allowed to report in response to transmissions
int192 immutable public minAnswer;
// Highest answer the system is allowed to report in response to transmissions
int192 immutable public maxAnswer;
```

If the price of a pool is less than `minAnswer` or greater than `maxAnswer` then `answer` returned by the oracle will still be `minAnswer` or `maxAnswer` respectively. For example: the price of a pool could be `100` yet if `minAnswer` is `1e13` then `latestRoundData()` will return `answer` as `1e13`. The protocol attempts to mitigate against this issue by utilising a dual-oracle system. Whereby the Chainlink price is checked against the Uniswap pool price and if the difference is greater than `maxPoolPriceDifference` the transaction will revert. However there is a issue. In `V3Oracle::_checkPoolPrice()` the Uniswap pool price is retrieved by calling `_getReferencePoolPriceX96()` — where `0` is passed as `twapSeconds`, [V3Oracle.sol#L135](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Oracle.sol#L135):

```solidity
uint256 priceX96 = _getReferencePoolPriceX96(pool, 0);
```

If `twapSeconds` is `0` then the `sqrtPriceX96` is assigned to `pool.slot0()`. This is the price based on the current state of the pool, [V3Oracle.sol#L360-L364](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Oracle.sol#L360-L364):

```solidity
uint160 sqrtPriceX96;
// if twap seconds set to 0 jus
Slippage not checked when decreasing liquidity during liquidation
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1064-L1068


# Vulnerability details

## Impact

`V3Vault.liquidate()` contains slippage checks for accrued fees received during a liquidation. However, there are no slippage checks for tokens received when decreasing liquidity.

```solidity
if (liquidity > 0) {
    nonfungiblePositionManager.decreaseLiquidity(
        INonfungiblePositionManager.DecreaseLiquidityParams(tokenId, liquidity, 0, 0, block.timestamp)
    );
}

(amount0, amount1) = nonfungiblePositionManager.collect(
    INonfungiblePositionManager.CollectParams(tokenId, recipient, fees0, fees1)
);
```

The liquidator will not be able to check for slippage when calling `NonfungliblePositionManager.decreaseLiquidity()`.

## Tools Used

Manual Review

## Recommended Mitigation Steps

There are two potential solutions.

You can enable the liquidator to pass through `liquidityAmount0Min` and `liquidityAmount1Min`

```diff
if (liquidity > 0) {
    nonfungiblePositionManager.decreaseLiquidity(
-       INonfungiblePositionManager.DecreaseLiquidityParams(tokenId, liquidity, 0, 0, block.timestamp)
+       INonfungiblePositionManager.DecreaseLiquidityParams(tokenId, liquidity, liquidityAmount0Min, liquidityAmount1Min, block.timestamp)
    );
}
```

Or you can include the amounts received from `decreaseLiquidity` in the amounts returns from `V3Vault._sendPositionValue()`

```solidity
if (liquidity > 0) {
    (amount0, amount1) = nonfungiblePositionManager.decreaseLiquidity(
        INonfungiblePositionManager.DecreaseLiquidityParams(tokenId, liquidity, 0, 0, block.timestamp)
    );
}

(feeAmount0, feeAmount1) = nonfungiblePositionManager.collect(
    INonfungiblePositionManager.CollectParams(tokenId, recipient, fees0, fees1)
);

amount0 += feeAmount0;
amount1 += feeAmount1;
```


## Assessed type

MEV
Attacker can avoid being liquidated without repay full by front-running and partial repay
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L696-#L698


# Vulnerability details

## Vulnerability detail
In `V3Vault.liquidate()` function, there is a checking condition to make sure `debtShares` of position is equal to `debtShares` of input:

    function liquidate(LiquidateParams calldata params) external override returns (uint256 amount0, uint256 amount1) {
        .  .  .  .  .
        if (debtShares != params.debtShares) {  //<---
            revert DebtChanged();
        }
In `_repay()` function, it is allowed for user to partial repay, user only need to partial repay amount that at least `minLoanSize`:

    function _repay(uint256 tokenId, uint256 amount, bool isShare, bytes memory permitData) internal {
        .  .  .  .  .  .
        // if fully repayed
        if (currentShares == shares) {
            _cleanupLoan(tokenId, newDebtExchangeRateX96, newLendExchangeRateX96, owner);
        } else {
            // if resulting loan is too small - revert
            if (_convertToAssets(loanDebtShares, newDebtExchangeRateX96, Math.Rounding.Up) < minLoanSize) {  //<---
                revert MinLoanSize();
            }
        }

Which is set at 0 when deploy, but it can be changed later:

    // minimal size of loan (to protect from non-liquidatable positions because of gas-cost)
    uint256 public minLoanSize = 0;

So, the problem is user can front-running `liquidate()` function by repay at least `minLoanSize` amount, keep the position not liquidated although it is under-collateralized

## Impact
Position are not able to be liquidated, although it is under-collateralized, which will lead to bad debt for protocol

## Tools Used
Manual review

## Recommended Mitigation Steps
Remove that checking position when liquidating, only rely-on current `debtShares` of the position to handle


## Assessed type

Other
`V3Vault.reserveFactorX32` take away interest profit from lender into reserve without mechanism to increase it or return/reward it back to user
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1181-L1181
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L761-L783


# Vulnerability details



When admin increase `V3Vault.reserveFactorX32` to non zero value. It reduce `lastLendExchangeRateX96` speed of increasing. This mean user receive less interest than they should. Because some of these interest now transfer to the pool as reserve.

And these interest that now belong to reserve will permanently stuck inside the pool. Providing liquidity for user withdrawing or borrowing.
Lenders can never receive this profit.
With the exception of admin/governance through `withdrawReserves()`.

In Compound, they have compensation mechanism for user when they take away some of profit. Which is just Comp token reward. Or special interest rate model that allow temporary increasing supplyRate for a time.

But `V3Vault` does not have this mechanism, just admin function take away reserve (include interest belong user) without compensation.

## Impact

Unintended reduction of interest profit from lender to increase liquidity of the pool.
When admin use pool profit to turn into reserve, this profit money is taken away from lender.

## Proof of Concept

### some concept from Compound

Some of these concept from Compound:

- SupplyIndex is `lastLendExchangeRateX96` in `V3Vault`
- BorrowIndex is `lastDebtExchangeRateX96` in `V3Vault`

SupplyIndex is user to track all user principle + "virtual" interest accrued so far.
BorrowIndex is used to track all loan + "virtual" interest repayment expected from borrower.

"virtual" here mean user suppose to receive/repay this much but actual pool might have less available balance because not every debt have been repaid yet. The pool only hold collateral for now, the repay interest might happen later.

### Explain how exchange rate work in `V3Vault`

`lastLendExchangeRateX96` is used to tracking all USDC provided by user to the pool + "virtual" interest that need to pay out to all users.

The idea behind `reserveFactorX32` is it help provide liquidity to the pool so people can exit when some loan have not been repaying interest yet.

`reserveFactorX32` just reduce supply rate ranging from 0-100%.

Here is simplified math fomular:
`lastLendExchangeRateX96` = `lastLendExchangeRateX96` + time * (supplyRate - reserveFactor)

For example:
If pool suppose to payout 100$ USDC in interest, `lastLendExchangeRateX96` value increase from 1.000 to 1.001. And `reserveFactorX32` is 10%. The pool only payout 90$ USDC in interest, 10$ USDC will be part of "virtual" reserve.

Now `lastLendExchangeRateX96` will only increase from 1.000 to 1.0009. Which is less than it should be.

Withdraw share from pool will be 10% less than it should be.
Here is simplified withdraw token formula:
`assetOut = share * (lastLendExchangeRateX96) / Q96`

Of course, these 10
Due to interest rates update method, Interest-Free Loans are possible and the Cost of DoS are reduced
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1150-L1165


# Vulnerability details

## Impact
Allowing in interest free debt in 1 block could have several unwanted results:
- Allowing for in same block (not necessarly same transaction) **interest-free loans**, could be abused by wales for **arbitrage operations**, resulting in protocol users unable to borrow because of the daily limit.
- **DoS attacks**, with the new updates on the main net resulting in way lower transaction fees, a whale account with a big position could borrow a big amount of money and then repay it in the same block, resulting in him not paying any interest, and users unable to borrow in this block. The attack will then be repeated in each block resulting in DoS

=> The attack would result not only in **DoS**, but also the protocol Liquidity Providers (LPs), and the protocol would **lose potential interest payments for their deposits**.
- Note: Please also note, that in L2 blockchains, it is quite common for multiple L2 Blocks to have the same `block.timestamp`. So borrower could potentially have **interest-free debt on the span of multiple blocks.** 
## Proof of Concept

To prove the severance of the problems, we want to first demonstrate the math around the cost of a 24 hours DoS attack,and second we want to demonstrate through a coded PoC, that when borrowing in the same block, there are no fees to be paid.
**24 hours DoS costs:**
Because the debt is interest free (see next step, for PoC) the cost of a 24 hours DoS attack is the cost of the gas to borrow and repay the loan. Due to the new changes deployed to Ethereum (Ethereum's Dencun upgrade) the transactions fees on L2 like arbitrum, have massively decreased, resulting in a cost of around 0.1$ per transaction. Right now on arbitrum, on average a block is minted each 15 seconds, resulting in 4 blocks per minute, 240 blocks per hour, and 5760 blocks per day. The cost of a single attack in a single block is `(0.1 * 2)` + `0.2~0.5 usd` for the first `borrow()` to front run the other transactions in the block. The cost of a 24 hours DoS attack is then between `5760 * (0.1 * 2 + 0.2) =2304 usd`  and `5760 * (0.1 * 2 + 0.5)= 4032 usd`. This would allow a whale whith a sufficient LP position that he can use as collateral for the 10% of the Lenders deposits (which is maximum daily borrowing limit). Specially in the early days of the protocol, this doesn't necessary need to be a lot. A realistic scenario would look like this:

- Total Users Deposits 10 million USDC
- Attacker has an LP position valued at 1.3 million USDC
- Daily borrowing limit is 10% of the total deposits, so 1 million USDC
- Attack Budget for attacker ~ 5000 usd
- The attacker first borrows 1 million USDC at the start of the block (frontrun), and repay it in the same block (backrun), resulting in no interest to be paid, and users unable to borrow in the same period.
- The attacke
totalDebtShares is applied to tokens when the NFT position doesn't hold any of the asset
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol?plain=1#L1205-L1244


# Vulnerability details

## Impact

When a loan's collateral is updated via V3Vault._updateAndCheckCollateral(), Revert assumes that the NFT position has tokens applied equally to both tokens in the position and applies a debt share update to both of the position's tokens. 

However, a Uniswap V3 NFT position can have an uneven distribution between both tokens. The position can also have liquidity applied to just one token. When NFT positions like this are deposited into the vault and Revert updates its collateral, Revert unfairly updates both token's totalDebtShares not taking into account the possibility of unequal distribution. This leads to cases where a token's totalDebtShares is miscalculated, inflating the value of totalDebtShares, and leading to premature collateral limits being reached. This can prematurely block certain features within Revert, such as borrowing Vault assets.

## Proof of Concept

Below is the code for the _updateAndCheckCollateral() function:

```solidity
function _updateAndCheckCollateral(
    uint256 tokenId,
    uint256 debtExchangeRateX96,
    uint256 lendExchangeRateX96,
    uint256 oldShares,
    uint256 newShares
) internal {
    if (oldShares != newShares) {
        (,, address token0, address token1,,,,,,,,) = nonfungiblePositionManager.positions(tokenId);

        // remove previous collateral - add new collateral
        if (oldShares > newShares) {
            tokenConfigs[token0].totalDebtShares -= SafeCast.toUint192(oldShares - newShares);
            tokenConfigs[token1].totalDebtShares -= SafeCast.toUint192(oldShares - newShares);
        } else {
            tokenConfigs[token0].totalDebtShares += SafeCast.toUint192(newShares - oldShares);
            tokenConfigs[token1].totalDebtShares += SafeCast.toUint192(newShares - oldShares);

            // check if current value of used collateral is more than allowed limit
            // if collateral is decreased - never revert
            uint256 lentAssets = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up);
            uint256 collateralValueLimitFactorX32 = tokenConfigs[token0].collateralValueLimitFactorX32;
            if (
                collateralValueLimitFactorX32 < type(uint32).max
                    && _convertToAssets(tokenConfigs[token0].totalDebtShares, debtExchangeRateX96, Math.Rounding.Up)
                        > lentAssets * collateralValueLimitFactorX32 / Q32
            ) {
                revert CollateralValueLimit();
            }
            collateralValueLimitFactorX32 = tokenConfigs[token1].collateralValueLimitFactorX32;
            if (
                collateralValueLimitFactorX32 < type(uint32).max
                    && _convertToAssets(tokenConfigs[token1].totalDebtShares, debtExchangeRateX96, Math.Rounding.Up)
                        > lentAssets * collateralValueLimitFactorX32 / Q32
Lowering collateral factor can lead to immediate liquidations
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol?plain=1#L856-L866
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol?plain=1#L1270-L1279
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol?plain=1#L1143-L1148


# Vulnerability details

## Impact

The owner of the vault can update a token config's collateral factor. Within the function is a check that the collateral factor must not exceed the max collateral factor which is set to 90%. However, the function does not include checks that the collateral factor is being lowered from the current level. When a collateral factor is lowered, loans can become instantly liquidatable which is unfair to loaners.

It's important to emphasis that normally admin function vulnerabilities are typically considered LOW. However, because Revert has added an additional check that the collateral factor does not reach above a certain level, Revert is assuming that any range value from `0 - max collateral factor` can be used for a token's collateral factor whenever setTokenConfig is called. This is why I believe this should be considered a MED.
 

## Proof of Concept

Let's first look at how the collateral factor is updated:

```solidity
function setTokenConfig(address token, uint32 collateralFactorX32, uint32 collateralValueLimitFactorX32)
    external
    onlyOwner
{
    if (collateralFactorX32 > MAX_COLLATERAL_FACTOR_X32) {
        revert CollateralFactorExceedsMax();
    }
    tokenConfigs[token].collateralFactorX32 = collateralFactorX32;
    tokenConfigs[token].collateralValueLimitFactorX32 = collateralValueLimitFactorX32;
    emit SetTokenConfig(token, collateralFactorX32, collateralValueLimitFactorX32);
}
```

As you can see, the only check that exists is that the collateralFactorX32 hasn't reached the max collateral factor.

Now let's see how a loan's healthiness is calculated:

```solidity
function _checkLoanIsHealthy(uint256 tokenId, uint256 debt)
    internal
    view
    returns (bool isHealthy, uint256 fullValue, uint256 collateralValue, uint256 feeValue)
{
    (fullValue, feeValue,,) = oracle.getValue(tokenId, address(asset));
    uint256 collateralFactorX32 = _calculateTokenCollateralFactorX32(tokenId);
    collateralValue = fullValue.mulDiv(collateralFactorX32, Q32);
    isHealthy = collateralValue >= debt;
}

function _calculateTokenCollateralFactorX32(uint256 tokenId) internal view returns (uint32) {
    (,, address token0, address token1,,,,,,,,) = nonfungiblePositionManager.positions(tokenId);
    uint32 factor0X32 = tokenConfigs[token0].collateralFactorX32;
    uint32 factor1X32 = tokenConfigs[token1].collateralFactorX32;
    return factor0X32 > factor1X32 ? factor1X32 : factor0X32;
}
```

To calculate the collateral factor of the loan, Revert checks which NFT position's token collateral factor is the lowest and then utilizes that value as the collateral factor. Let's say we have a current loan out such that:
setReserveFactor fails to update global interest before updating reserve factor
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol?plain=1#L1167-L1195
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol?plain=1#L837-L840


# Vulnerability details

## Impact

When the vault owner calls V3Vault.setReserveFactor(), the function updates the reserve factor for the Vault. Unfortunately, if the global interest is not updated before the reserve factor is updated, the updated reserve factor will be retroactively applied in the exchange rate formula starting at the last update. This leads to unexpected lending rate changes causing lenders to receive unexpected more or less favorable lending exchange rate depending on what the updated reserve factor value is.

## Proof of Concept

The lending rate formula is calculated _calculateGlobalInterest() and the formula can be defined as:

```solidity
(uint256 borrowRateX96, uint256 supplyRateX96) = interestRateModel.getRatesPerSecondX96(available, debt);

supplyRateX96 = supplyRateX96.mulDiv(Q32 - reserveFactorX32, Q32);

// always growing or equal
uint256 lastRateUpdate = lastExchangeRateUpdate;

if (lastRateUpdate > 0) {
    newDebtExchangeRateX96 = oldDebtExchangeRateX96 + oldDebtExchangeRateX96 * (block.timestamp - lastRateUpdate) * borrowRateX96 / Q96;
    newLendExchangeRateX96 = oldLendExchangeRateX96 + oldLendExchangeRateX96 * (block.timestamp - lastRateUpdate) * supplyRateX96 / Q96;
} else {
    newDebtExchangeRateX96 = oldDebtExchangeRateX96;
    newLendExchangeRateX96 = oldLendExchangeRateX96;
}
```

In the formula above, the supply rate is modified before being used to calculate the new lending rate as `supply rate * reserve factor`. This modified supply rate is then used to determine how much of a jump should occur in the lending rate via `newLendExchangeRateX96 = oldLendExchangeRateX96 + oldLendExchangeRateX96 * (block.timestamp - lastRateUpdate) * supplyRateX96 / Q96`. The larger the modified supply rate, the larger the jump is. The smaller the modified supply rate, the smaller the jump is. 

By not updating the lending rate before updating the reserve factor, the updated reserve factor will retroactively be applied to the past artificially influencing the lending rate.  

To best visualize this, let's look at the forge test below which shows two scenarios, one where the interest is updated before the reserve factor is updated and one where it's not. Then we can compare the different lending rate values and see how by not updating the exchange rates before updating the reserve factor, the lending rate is impacted. 

```notes
RESULTS FROM FORGE TESTS 

with interest rate update occurring after reserve factor update:

- starting lendExchangeRateX96:  79243018781103204090820932736
- after reserve update lendExchangeRateX96:  79240047527736122590199028736

with interest rate update occurring before reserve factor update:

- starting lendExchangeRateX96:  79243018781103204090820932736
- after reserve update lendExchang