xanoutas/solidity-security-findings huggingface dataset:
Griefing attack: attacker can create multiple borrow with dust amount to make protocol suffer bad debt due to lack of incentive to liquidate them
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#L550-#L602


# Vulnerability details

## Vulnerability details
When deploying `V3Vault` contract, `minLoanSize` is originally set to 0:

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

And in the uniswap v3, minting position does not have any limitation in value: [original code](https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol#L128)

Attacker can mint multiple positions with small amount, transfer them to `V3Vault` and use them to borrow. When the price change, and these positions is under-collateralzed, there is no incentive for anyone to liquidate them because cost of gas required to call them is more than value of token they receive back.

## Impact
Protocol will suffer bad debt because no one is willing to liquidate them

## Tools Used
Manual review

## Recommended Mitigation Steps
Variable `minLoanSize` should be set when initalizing this contract.


## Assessed type

Other
wrong implement of ' _resetDailyLendIncreaseLimit' and '_resetDailyDebtIncreaseLimit'
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#L1246
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1258


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
wrong implementation of _resetDailyLendIncreaseLimit and _resetDailyDebtIncreaseLimit.

 dailyLendIncreaseLimitLeft =
     @>           dailyLendIncreaseLimitMin > lendIncreaseLimit ? dailyLendIncreaseLimitMin : lendIncreaseLimit;

I think we are assigning the wrong value to the dailyLendIncreaseLimitLeft.
It will set the wrong value to the dailyLendIncreaseLimitLeft.


## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
 function _resetDailyLendIncreaseLimit(uint256 newLendExchangeRateX96, bool force) internal {
        // daily lend limit reset handling
        uint256 time = block.timestamp / 1 days;
        if (force || time > dailyLendIncreaseLimitLastReset) {
            uint256 lendIncreaseLimit = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up)
                * (Q32 + MAX_DAILY_LEND_INCREASE_X32) / Q32;
            dailyLendIncreaseLimitLeft =
     @>           dailyLendIncreaseLimitMin > lendIncreaseLimit ? dailyLendIncreaseLimitMin : lendIncreaseLimit;
            dailyLendIncreaseLimitLastReset = time;
        }
    }

    function _resetDailyDebtIncreaseLimit(uint256 newLendExchangeRateX96, bool force) internal {
        // daily debt limit reset handling
        uint256 time = block.timestamp / 1 days;
        if (force || time > dailyDebtIncreaseLimitLastReset) {
            uint256 debtIncreaseLimit = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up)
                * (Q32 + MAX_DAILY_DEBT_INCREASE_X32) / Q32;
            dailyDebtIncreaseLimitLeft =
       @>         dailyDebtIncreaseLimitMin > debtIncreaseLimit ? dailyDebtIncreaseLimitMin : debtIncreaseLimit;
            dailyDebtIncreaseLimitLastReset = time;
        }
    }

## Tools Used

## Recommended Mitigation Steps


 function _resetDailyLendIncreaseLimit(uint256 newLendExchangeRateX96, bool force) internal {
        // daily lend limit reset handling
        uint256 time = block.timestamp / 1 days;
        if (force || time > dailyLendIncreaseLimitLastReset) {
            uint256 lendIncreaseLimit = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up)
                * (Q32 + MAX_DAILY_LEND_INCREASE_X32) / Q32;
            dailyLendIncreaseLimitLeft =
     @>           dailyLendIncreaseLimitMin > lendIncreaseLimit ?  lendIncreaseLimit: dailyLendIncreaseLimitMin ;
            dailyLendIncreaseLimitLastReset = time;
        }
    }

    function _resetDailyDebtIncreaseLimit(uint256 newLendExchangeRateX96, bool force) internal {
        // daily debt limit reset handling
        uint256 time = block.timestamp / 1 days;
        if (force
tokenOwner can self liquidate through FlashLoanLiquidator Contract
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/utils/FlashloanLiquidator.sol#L41-L65


# Vulnerability details

## Impact
In a seperate submitted report, it was stated that a user can self-liquidate. This is also possible by through the FlashLoanLiquidator Contract.

It is possible for a tokenOwner to liquidate their collaterized tokenId rather then repaying the borrowed funds by calling the FlashLoanLiquidator.liquidate() . With this, even though the caller of V3Vault.liquidate() will be the FlashLoanLiquidator Contract, the user is still self-liquidating as the owner of the tokenId and liquidator in the `FlashCallbackData`

Since the FlashLoanLiquidator.liquidate() function does not check that the caller is not the token owner, then it is possible for the tokenOwner[tokenId] to be the liquidate their own borrowed funds to get back their nonfungiblePositionManager position token and extra erc20 assets


## Proof of Concept
In https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/utils/FlashloanLiquidator.sol#L41-L65 , there is no check for caller to not be the tokenOwner.

1. Alice has collaterized her tokenId for her position and borrowed USDC assets on it.
2. After a while, LiquidateState of the tokenId is unhealthy.
3. Alice then calls the liquidate() function in the FlashLoanLiquidator contract, receives position tokens for the liquidation, receives the remainder position tokens meant for the tokenOwner
4. loan is cleared, and tokenId is sent back to Alice


## Tools Used
Manual

## Recommended Mitigation Steps
Apply a check to ensure that the caller is not owner of the tokenId, i.e `require(msg.sender != params.vault.ownerOf(params.tokenId))`.




## Assessed type

Other
Borrowers can stop liquidations by front running them with miniscule repays 
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#L696-L698


# Vulnerability details

## Impact
A malicious user can stop themselves from being liquidated effecting more loses on the vault 

## Proof of Concept
The V3Vault.liquidate requires that the debt shares supplied by the laiquidator parms is the same and the nft id debts shares else it reverts. A malicious actor could take advantage of this by repaying very small amounts eg 1,2 unit amounts when ever they are about to be liquidated to make sure the debt shares decrease without actually making the position solvent

## Tools Used
manual analysis 

## Recommended Mitigation Steps
dust repays should not be allowed and the above code block should be removed 



## Assessed type

Other
Self-Liquidation by tokenOwner is possible
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#L685-L757


# Vulnerability details

## Impact
It is possible for a tokenOwner to liquidate their collaterized tokenId rather then repaying the borrowed funds.

Since the V3Vault.liquidate() function does not check that the caller is not the token owner, then it is possible for the tokenOwner[tokenId] to be the caller liquidating their own borrowed funds to get back their nonfungiblePositionManager position token and extra erc20 assets


## Proof of Concept
In https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L685-L757 , there is no check for caller to not be the tokenOwner.

1. Alice has collaterized her tokenId for her position and borrowed USDC assets on it.
2. After a while, LiquidateState of the tokenId is unhealthy.
3. Alice then calls the liquidate() function in the V3Vault contract, pays the liquidatorCost for the tokenId, receives position tokens for the liquidation, receives the remainder position tokens meant for the tokenOwner
4. loan is cleared, and tokenId is sent back to Alice


## Tools Used
Manual

## Recommended Mitigation Steps
Apply a check to ensure that the caller is not owner of the tokenId, i.e `require(msg.sender != tokenOwner[tokenId])` .


## Assessed type

Other
Wrong globalLendLimit check
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#L906-L908


# Vulnerability details

In V3vault deposit, lend share instead of amount is mistakenly used to check whether the globalLendLimit is exceeded. As time increases, the total max available deposit amount will become more and more.

## Impact

As time increases, the total max available deposit amount will become more and more.

## Proof of Concept

In white paper:

` globalLendLimit: Limits the total lending token amount that can be deposited. It limits new deposits but does not affect existing ones.`

and it is used correctly in `maxDeposit` and `maxMint`:

```solidity
function maxDeposit(address) external view override returns (uint256) {
        (, uint256 lendExchangeRateX96) = _calculateGlobalInterest();
        uint256 value = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up);
        if (value >= globalLendLimit) {
            return 0;
        } else {
            return globalLendLimit - value;
        }
    }
```

```solidity
    /// @inheritdoc IERC4626
    function maxMint(address) external view override returns (uint256) {
        (, uint256 lendExchangeRateX96) = _calculateGlobalInterest();
        uint256 value = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up);
        if (value >= globalLendLimit) {
            return 0;
        } else {
            return _convertToShares(globalLendLimit - value, lendExchangeRateX96, Math.Rounding.Down);
        }
    }
```

but it used incorrectly in function `_deposit`:

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

```solidity
    function _deposit(address receiver, uint256 amount, bool isShare, bytes memory permitData)
        internal
        returns (uint256 assets, uint256 shares)
    {
     ···
        if (totalSupply() > globalLendLimit) {
            revert GlobalLendLimit();
        }

        if (assets > dailyLendIncreaseLimitLeft) {
            revert DailyLendIncreaseLimit();
        } else {
            dailyLendIncreaseLimitLeft -= assets;
        }

        emit Deposit(msg.sender, receiver, assets, shares);
    }
```

because vault is positive rebase token,one share represents more and more amount as time increases.So in fixed globalLendLimit,the total max available deposit amount will become more and more with time increases。

This error breaks the protocol's lend limit, so I consider it is a medium severity finding.

## Tool Used

vscode、foundary

## Recommended mitigation steps

Use this in check：
```solidity
function _deposit(address receiver, uint256 amount, bool isShare, bytes memory permitData)
    internal
    returns (uint256 assets, uint256 shares)
{
 ···
      uint256 totalAsset = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up);
      if (totalAsset > globalLendLimit) {
          revert
Calling IncreaseLiquidity in AutoCompound without Slippage Protection and block.timestamp as Deadline can Cause Loss of Funds.
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/transformers/AutoCompound.sol#L163-L167


# Vulnerability details

## Impact
Calling IncreaseLiquidity in AutoCompound without Slippage Protection and block.timestamp as Deadline can Cause Loss of Funds for the user.

## Proof of Concept
The **execute** function from the AutoCompound contract is calling the **increaseLiquidity** function from uniswap to compound fees generated from the user's position, but the calling to the **increaseLiquidity** is sending default and unrecommended values as sending 0s for the ***amount0Min and amount1Min*** and setting block.timestamp as de deadline, this means that there's no slippage protection and the tx can be executed at any time the node decide, this can cause loss of funds for the users due to mev manipulation as the transaction will accept any tx even if that results in 0 value added for the users without reverting.

As this is a transformer executed autonomously this can happen as many times as the operator bot keeps executing the Autocompound for compounding fees, so generating repeated losses for the users until is detected and corrected.

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/transformers/AutoCompound.sol#L163-L167

```solidity
(, state.compounded0, state.compounded1) = nonfungiblePositionManager.increaseLiquidity(
    INonfungiblePositionManager.IncreaseLiquidityParams(
         params.tokenId, state.maxAddAmount0, state.maxAddAmount1, 0, 0, block.timestamp
    )
 );
```


This same problem is present in the AutoRange contract when calling the mint function to create a new position the ***amount0Min and amount1Min*** are set to 0s which lets the transaction without slippage protection for the new position being minted.

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/transformers/AutoRange.sol#L198-L210


```solidity
            INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams(
                address(state.token0),
                address(state.token1),
                state.fee,
                SafeCast.toInt24(baseTick + config.lowerTickDelta), // reverts if out of valid range
                SafeCast.toInt24(baseTick + config.upperTickDelta), // reverts if out of valid range
                state.maxAddAmount0,
                state.maxAddAmount1,
                0,
                0,
                address(this), // is sent to real recipient aftwards
                params.deadline
            );
```

## Tools Used
Manual Code Review.

## Recommended Mitigation Steps
As a recommendation, you can set the minimum values accepted for the slippage as a percentage of the tokens added to the position while compounding, and allow the user to configure a deadline for this AutoCompound transformer.





## Assessed type

Other
Chainlink's `latestRoundData` might return stale  results
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Oracle.sol#L337


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
It can cause stale price.
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

On ChainlinkPriceOracle.sol, we are using latestRoundData, but there is no check if the return value indicates stale data.

         (, int256 answer,, uint256 updatedAt,) = feedConfig.feed.latestRoundData();
        if (updatedAt + feedConfig.maxFeedAge < block.timestamp || answer < 0) {
            revert ChainlinkPriceError();
        }
This could lead to stale prices according to the Chainlink documentation:

https://docs.chain.link/docs/historical-price-data/#historical-rounds
https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round
## Tools Used

## Recommended Mitigation Steps

uint80 BaseAnsweredInRound
@> (, int256 answer,, uint256 updatedAt,uint80 BaseAnsweredInRound) = feedConfig.feed.latestRoundData();
 require(BaseAnsweredInRound >= baseRoundID , "Stale price");
        if (updatedAt + feedConfig.maxFeedAge < block.timestamp || answer < 0) {
            revert ChainlinkPriceError();
        }


## Assessed type

Oracle
Users Uniswap Positions could be locked forever in Vault, because of Missing Input Validation
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#L443-L445


# Vulnerability details

## Impact
User LP Position could be forever locked in V3Vault, if the user mistakingly set  the call data in `create()` to the zero address. It is possible to misunderstand the `create()` function and set the recipient to the `x0` address, which will set the owner of this Position as the zero address in the vault, potentially locking the position in vault forever.
## Proof of Concept
This issue is possible because, the `onERC721Received()` function in the V3Vault contract, doesn't check if the recipient is the zero address, and if it is, it sets the owner of the position in vault as the zero address.

To run the test please add the following test to `test/integration/V3Vault.t.sol` :

```solidity
    function testCreatePositionToZeroAddress() external {
        vm.prank(TEST_NFT_ACCOUNT);
        NPM.approve(address(vault), TEST_NFT);
        vm.prank(TEST_NFT_ACCOUNT);
        vault.create(TEST_NFT,address(0));
        assertTrue(vault.ownerOf(TEST_NFT)== address(0));
        assertTrue(NPM.ownerOf(TEST_NFT)==address(vault));
        console.log("NFT Lost Forever");
        console.log("Owner of NFT in NPM: ",NPM.ownerOf(TEST_NFT));
        console.log("Owner of NFT in Vault: ",vault.ownerOf(TEST_NFT));
    }
```
The result of the test being executed:
```log
Ran 1 test for test/integration/V3Vault.t.sol:V3VaultIntegrationTest
[PASS] testCreatePositionToZeroAddress() (gas: 186268)
Logs:
  NFT Lost Forever
  Owner of NFT in NPM:  0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
  Owner of NFT in Vault:  0x0000000000000000000000000000000000000000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 530.37ms (929.12µs CPU time)

Ran 1 test suite in 574.87ms (530.37ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```
## Tools Used
Foundry, manual review
## Recommended Mitigation Steps

To mitigate this issue, it is recomended to add a check in the `onERC721Received()` and to set the owner of the cdp to `msg.sender` (`from` in `onERC721Received()`) if the recipient is the zero address.
A possible solution, would be something like this:

```diff
    function onERC721Received(address, address from, uint256 tokenId, bytes calldata data)
        external
        override
        returns (bytes4)
    {
        // only Uniswap v3 NFTs allowed - sent from other contract
        if (msg.sender != address(nonfungiblePositionManager) || from == address(this)) {
            revert WrongContract();
        }

        (uint256 debtExchangeRateX96, uint256 lendExchangeRateX96) = _updateGlobalInterest();

        if (transformedTokenId == 0) {
            address owner = from;
            if (data.length > 0) {
--                owner = abi.decode(data, (address));
++                address recipient = abi.decode(data, (address));
++                owner = (recipient == 0 ? owner : recipient);
            }
`InterestRateModel::getRatesPerSecondX96` borrow rate to be calculated incorrectly when the utilization rate is equal to the kink
Audit: 2024-03-revert-lend-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/InterestRateModel.sol#L58-L76


# Vulnerability details

## Impact

In `getRatesPerSecondX96` function the calculation of the `borrowRateX96` and `supplyRateX96` when the `utilizationRateX96` is equal to `kinkX96`.

When `utilizationRateX96` is equal to `kinkX96`, the `borrowRateX96` should be calculated using the `multiplierPerSecondX96` and `baseRatePerSecondX96` as follows:

```solidity
borrowRateX96 = (utilizationRateX96 * multiplierPerSecondX96 / Q96) + baseRatePerSecondX96;
```
However, the current implementation calculates `borrowRateX96` using the `jumpMultiplierPerSecondX96` instead of `multiplierPerSecondX96`:

```solidity
uint256 normalRateX96 = (kinkX96 * multiplierPerSecondX96 / Q96) + baseRatePerSecondX96;
uint256 excessUtilX96 = utilizationRateX96 - kinkX96;
borrowRateX96 = (excessUtilX96 * jumpMultiplierPerSecondX96 / Q96) + normalRateX96;
```
Since `utilizationRateX96` is equal to `kinkX96`, `excessUtilX96` will be zero, and `borrowRateX96` will be equal to `normalRateX96`. 

However, `normalRateX96` is calculated using `kinkX96` instead of `utilizationRateX96`, which is incorrect.

cause the borrow rate to be calculated incorrectly when the utilization rate is equal to the kink.




## Proof of Concept
```solidity
    function getRatesPerSecondX96(uint256 cash, uint256 debt)
        public
        view
        override
        returns (uint256 borrowRateX96, uint256 supplyRateX96)
    {
        uint256 utilizationRateX96 = getUtilizationRateX96(cash, debt);

        if (utilizationRateX96 <= kinkX96) {
            borrowRateX96 = (utilizationRateX96 * multiplierPerSecondX96 / Q96) + baseRatePerSecondX96;
        } else {
            uint256 normalRateX96 = (kinkX96 * multiplierPerSecondX96 / Q96) + baseRatePerSecondX96;
            uint256 excessUtilX96 = utilizationRateX96 - kinkX96;
            borrowRateX96 = (excessUtilX96 * jumpMultiplierPerSecondX96 / Q96) + normalRateX96;
        }

        supplyRateX96 = utilizationRateX96 * borrowRateX96 / Q96;
    }
```

## Tools Used
Manual Review
## Recommended Mitigation Steps


The calculation of `borrowRateX96` when `utilizationRateX96` is equal to `kinkX96` should be updated to use `multiplierPerSecondX96` instead of `jumpMultiplierPerSecondX96`. The corrected implementation should be:

```solidity
if (utilizationRateX96 <= kinkX96) {
    borrowRateX96 = (utilizationRateX96 * multiplierPerSecondX96 / Q96) + baseRatePerSecondX96;
} else {
    uint256 normalRateX96 = (kinkX96 * multiplierPerSecondX96 / Q96) + baseRatePerSecondX96;
    uint256 excessUtilX96 = utilizationRateX96 - kinkX96;
    borrowRateX96 = (excessUtilX96 * jumpMultiplierPerSecondX96 / Q96) + normalRateX96;
}
```
This ensures that the borrow rate is calculated correctly when the utilization rate is equal to the kink.



## Assessed type

Math