xanoutas/solidity-security-findings huggingface dataset:
`V3Vault.sol` permit signature does not check receiving token address is USDC
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L717-L725
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L893-L898
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L877-L917


# Vulnerability details


In `V3Vault.sol` there all 3 instances of `permit2.permitTransferFrom()`, all 3 does not check token transfered in is USDC token.
Allowing user to craft permit signature from any ERC20 token and Vault will accept it as USDC.

## Impact

User can steal all USDC from vault using permit signature of any ERC20 token.

## Proof of Concept

Here is how Vault accept USDC from user. Vault will accept `Uniswap.Permit2` signature transfer allowance from Permit2 then to vault contract.
<https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L877C1-L917C6>

```solidity
    if (params.permitData.length > 0) {
        (ISignatureTransfer.PermitTransferFrom memory permit, bytes memory signature) =
            abi.decode(params.permitData, (ISignatureTransfer.PermitTransferFrom, bytes));
        permit2.permitTransferFrom(
            permit,
            ISignatureTransfer.SignatureTransferDetails(address(this), state.liquidatorCost),
            msg.sender,
            signature
        );
    } else {
        // take value from liquidator
        SafeERC20.safeTransferFrom(IERC20(asset), msg.sender, address(this), state.liquidatorCost);
    }
```

Below is permit signature struct that can be decoded from user provided data.

```solidity
interface ISignatureTransfer is IEIP712 {
    /// @notice The token and amount details for a transfer signed in the permit transfer signature
    struct TokenPermissions {
        // ERC20 token address
        address token;
        // the maximum amount that can be spent
        uint256 amount;
    }

    /// @notice The signed permit message for a single token transfer
    struct PermitTransferFrom {
        TokenPermissions permitted;
        // a unique value for every token owner's signature to prevent signature replays
        uint256 nonce;
        // deadline on the permit signature
        uint256 deadline;
    }
}
```

`V3Vault.sol` need to check `TokenPermissions.token` is USDC, same as vault main asset.

`Uniswap.permit2.permitTransferFrom()` only check sign signature is correct. This is meaningless is Vault does not validate input data.

This allow user to use any ERC20 token. give allowance and permit to `Uniswap.Permit2`
Vault will accept any transfer token from `Permit2` as USDC.

Allowing user to deposit any ERC20 token and steal USDC from vault.

## Tools Used

## Recommended Mitigation Steps

fix missing user input validation in 3 all instances of permit2
<https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L717C1-L725C15>
<https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L893C1-L898C15>
<https://github.c
Debt may be bigger than lent assets.
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
If debt becomes bigger than lent, all withdraw function call will be reverted.
On the other hand, the interest accrued from debt will be distributed to reserves more than the intended values.
After all, this means the main invariant of the protocol may be broken down.

## Proof of Concept
`README.md#L99-L100` describes as follows.
```
## Main invariants
- Debt can never be bigger than Lent assets (after all loans are payed back or liquidated).
```
In general, the amount of lent is equal to or greater than the amount of debt.
However, since the interest accrued from debt is used separately for lent's interest and reserves, so the increment of debt is bigger than lent's increment.
Therefore, lent may be bigger than lent in the following cases.
1. If the amount of debt is equal to the amount of lent, the amount of debt will exceed the amount of lent after short time elapsed.
2. If the amount of debt is smaller than lent, the amount of debt will exceed the amount of lent after sufficiently long time elapsed.

The details are as follows.
`V3Vault.sol#_calculateGlobalInterest` function is the following.
```solidity
    function _calculateGlobalInterest()
        internal
        view
        returns (uint256 newDebtExchangeRateX96, uint256 newLendExchangeRateX96)
    {
        ......
1175:   (, uint256 available,) = _getAvailableBalance(oldDebtExchangeRateX96, oldLendExchangeRateX96);
        ......
        (uint256 borrowRateX96, uint256 supplyRateX96) = interestRateModel.getRatesPerSecondX96(available, debt);

1179:   supplyRateX96 = supplyRateX96.mulDiv(Q32 - reserveFactorX32, Q32);
        ......
    }
```
`V3Vault.sol#_getAvailableBalance` function of `L1175` is the following.
```solidity
    function _getAvailableBalance(uint256 debtExchangeRateX96, uint256 lendExchangeRateX96)
        internal
        view
        returns (uint256 balance, uint256 available, uint256 reserves)
    {
        balance = totalAssets();

        uint256 debt = _convertToAssets(debtSharesTotal, debtExchangeRateX96, Math.Rounding.Up);
        uint256 lent = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Down);

1027:   reserves = balance + debt > lent ? balance + debt - lent : 0;
        available = balance > reserves ? balance - reserves : 0;
    }
```
Generally, since `lent >= debt` and `balance + debt > lent` holds in `L1027`, `reserves = balance + debt - lent <= balance` and `available = balance - (balance + debt - lent) = lent - debt` holds true.

On the other hand, `InterestRateModel.sol#getRatesPerSecondX96` function of `L1179` is the following.
```solidity
    function getRatesPerSecondX96(uint256 cash, uint256 debt)
        public
        view
        override
        returns (uint256 borrowRateX96, uint256 supplyRateX96)
    {
64:     uint256 utilizationRateX96 = getUtilizationRateX96(cash, debt);
        .
Repayment sent to 0 address
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When a loan is fully repaid the loan storage is deleted. Since loan is a storage
refernce to the loan, loan.borrower will return address(0), after the loan has been deleted. This will result in the collateral being transfered to address(0) instead of the borrower. Some ERC20 tokens will revert when being sent to address(0) but a large number will simply be sent there and lost forever

## Proof of Concept
```

    function _removeTokenFromOwner(address from, uint256 tokenId) internal {
        uint256 lastTokenIndex = ownedTokens[from].length - 1;
        uint256 tokenIndex = ownedTokensIndex[tokenId];
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = ownedTokens[from][lastTokenIndex];
            ownedTokens[from][tokenIndex] = lastTokenId;
            ownedTokensIndex[lastTokenId] = tokenIndex;
        }
        ownedTokens[from].pop();
        // Note that ownedTokensIndex[tokenId] is not deleted. There is no need to delete it - gas optimization
        delete tokenOwner[tokenId]; 
```
The loan storsge associated with the tokenId being repaid is deleted reference so when `loans[tokenId]` is deleted so is loan. So loan.borrower is now address(0)
```
 function _cleanupLoan(uint256 tokenId, uint256 debtExchangeRateX96, uint256 lendExchangeRateX96, address owner)
        internal
    {
        _removeTokenFromOwner(owner, tokenId);
        _updateAndCheckCollateral(tokenId, debtExchangeRateX96, lendExchangeRateX96, loans[tokenId].debtShares, 0);
        delete loans[tokenId];
        nonfungiblePositionManager.safeTransferFrom(address(this), owner, tokenId);
        emit Remove(tokenId, owner);
    }
```


## Tools Used
Manual Review

## Recommended Mitigation Steps
```
 function _cleanupLoan(uint256 tokenId, uint256 debtExchangeRateX96, uint256 lendExchangeRateX96, address owner)
        internal
    {
        _removeTokenFromOwner(owner, tokenId);
        _updateAndCheckCollateral(tokenId, debtExchangeRateX96, lendExchangeRateX96, loans[tokenId].debtShares, 0);
  _      delete loans[tokenId];
        nonfungiblePositionManager.safeTransferFrom(address(this), owner, tokenId);
+       delete loans[tokenId]
        emit Remove(tokenId, owner);
    }
```
Appropriate code logic  changes should also be made for `_removeTokenFromOwner` since there we are also actually deleting the tokenId from the owner

 


## Assessed type

ERC20
Liquidation only transfers fees to liquidator, while part or all of the decreased liquidity is transferred back to the defaulting owner.
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L685-L757
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1031-L1073


# Vulnerability details

## Impact

The `liquidate` function checks if a loan is healthy, calculates the liquidation, receives the required assets from the liquidator and then transfers the promised collateral token to the liquidator through the `_sendPositionValue` function. The function calculates whether the amount can be covered only through fees or if also a part or all of the liquidity needs to be converted. If so, liquidity is reduced by the neccesary amount and fees are collected with the liquidator as recipient. 

There are two major problems with the `decreaseLiquidity` call.
1. The decreased liquidity is **not** sent to the liquidator,  It is simply sent to the position where it remains until the ownership is transferred back to the original owner through `_cleanupLoan`
2. The `decreaseLiquidity` call is done with `0` slippage protection, meaning it is quite possible the position does not even receive any liquidity back.  


As a consequence, **only** the accumulated fees are sent to the liquidator. 

If the liquidator had set market correct `amount0Min/amount1Min` variables in the `LiquidateParams`, the liquidition will revert with `SlippageError`

Otherwise, the liquidator does not receive any of the liquidity he paid for and the defaulting owner will receive a part or all of the removed liquidity when the position is transferred back to him through `_cleanupLoan`. 

## Proof of Concept
 
We can see the following flow in the liquidate function:

1. Check if loan is unhealthy.
2. If yes, calculate the liquidation cost.
3. Receive asset equal to the liquidation cost from the liquidator.
4. Transfer the paid for collateral to the liquidator through `_sendPositionValue`.
5. Check that returned amounts from the above function are not smaller than the amount0Min/amount1Min.
6. Transfer the position back to the original owner. 


The major problem resides in  `_sendPositionValue`.

``` solidity
    function liquidate(LiquidateParams calldata params) external override returns (uint256 amount0, uint256 amount1) {
        // liquidation is not allowed during transformer mode
        if (transformedTokenId > 0) {
            revert TransformNotAllowed();
        }

        LiquidateState memory state;
      
        (state.newDebtExchangeRateX96, state.newLendExchangeRateX96) = _updateGlobalInterest();

        uint256 debtShares = loans[params.tokenId].debtShares;
        if (debtShares != params.debtShares) {
            revert DebtChanged();
        }

        state.debt = _convertToAssets(debtShares, state.newDebtExchangeRateX96, Math.Rounding.Up);

        (state.isHealthy, state.fullValue, state.collateralValue, state.feeValue) =
            _checkLoanIsHealthy(params.tokenId, s
Inflation attack due to the absence of dead shares and the reliance on balanceOf
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L877-L917
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1017-L1029
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L284-L286


# Vulnerability details

## Impact

Inflation attacks are a standard attack vector for ERC4626 Vaults and consist of 2 parts. 

1. Malicious user 1 share
2. Donate or transfer assets to the vault to inflate the assets per share

Subsequently it will cause a rounding error when other users deposit, from which the malicious user can draw profit. 


## Proof of Concept


In the `_deposit` function 

``` solidity
    function _deposit(address receiver, uint256 amount, bool isShare, bytes memory permitData)
        internal
        returns (uint256 assets, uint256 shares)
    {
        (, uint256 newLendExchangeRateX96) = _updateGlobalInterest();

        _resetDailyLendIncreaseLimit(newLendExchangeRateX96, false);

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

        if (permitData.length > 0) {
            (ISignatureTransfer.PermitTransferFrom memory permit, bytes memory signature) =
                abi.decode(permitData, (ISignatureTransfer.PermitTransferFrom, bytes));
            permit2.permitTransferFrom(
                permit, ISignatureTransfer.SignatureTransferDetails(address(this), assets), msg.sender, signature
            ); 
        } else {
            // fails if not enough token approved
            SafeERC20.safeTransferFrom(IERC20(asset), msg.sender, address(this), assets);
        }

        _mint(receiver, shares);

        if (totalSupply() > globalLendLimit) {
            revert GlobalLendLimit();
        }

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

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

There is no check to ensure that a minimum amount of liquidity needs to be deposited, so the malicious user can mint 1 wei of share with the absolute minimum amount of assets. 

The inflation of the vault assets is quite straightforward since the protocol relies on `totalAssets()` to calculate the exchange rates. And this function returns `balanceOf(address(this))`

``` solidity

    function _getAvailableBalance(uint256 debtExchangeRateX96, uint256 lendExchangeRateX96)
        internal
        view
        returns (uint256 balance, uint256 available, uint256 reserves)
    {
        balance = totalAssets();

        uint256 debt = _convertToAssets(debtSharesTotal
Liquidation can be reverted by a malicious user
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

### Description
The protocol allows users to deposit Uniswap LP NFT (token) and borrow against it. If the token loses value, the loan can be liquidated by anyone. At the end of the liquidation process, the token is being returned to its initial owner with `safeTransferFrom` function call. An issue arrises when the loan initiator is a contract and it intentionally reverts when `onERC721Received` is being called, this causes the whole tx to revert and the loan is not liquidated.

### Root cause
Flow control is given to arbitrary code, which can revert the transaction.

### Impact
Malicious users can take loans, which can never be liquidated, causing potentially substantial loss for the protocol.

### PoC

1) Create `test/integration/LiquidationBlocker.sol`
```sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract LiquidationBlocker {
  address _owner;
  bool _blockTransfer;

  constructor() {
    _owner = msg.sender;
    _blockTransfer = false;
  }

  function setBlockTransfer(bool blockTransfer) public {
    require(_owner == msg.sender, "only owner");
    _blockTransfer = blockTransfer;
  }

  function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
    require(!_blockTransfer, "Transfer blocked");
    return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
  }
}
```

2) Create `test/integration/V3VaultLiquidation.sol`

```sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "forge-std/console.sol";

// base contracts
import "./V3Vault.t.sol";
import "../../src/V3Oracle.sol";
import "../../src/V3Vault.sol";
import "../../src/InterestRateModel.sol";

import "./LiquidationBlocker.sol";

contract V3VaultLiquidation is Test {
  uint256 constant Q32 = 2 ** 32;
  uint256 constant Q96 = 2 ** 96;

  address constant WHALE_ACCOUNT = 0xF977814e90dA44bFA03b6295A0616a897441aceC;

  IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
  IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
  IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);

  INonfungiblePositionManager constant NPM = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
  address PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

  address constant CHAINLINK_USDC_USD = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6;
  address constant CHAINLINK_DAI_USD = 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9;

  address constant UNISWAP_DAI_USDC = 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168; // 0.01% pool
  address constant UNISWAP_ETH_USDC = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 0.05% pool

  address constant TEST_NFT_ACCOUNT = 0x3b8ccaa89FcD432f1334D35b10fF8547001Ce3e5;
  uint256 constant TEST_NFT = 126; // DAI/U
Users can drain protocol reserves using `borrow` function
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Issue Description

The `V3Vault` maintains an internal asset reserve which can be used to cover underwater liquidation or can be withdrawn by protocol team through `withdrawReserves` function.

Both the available and reserve balances can be calculated by the `_getAvailableBalance` function:

```solidity
function _getAvailableBalance(
    uint256 debtExchangeRateX96,
    uint256 lendExchangeRateX96
)
    internal
    view
    returns (uint256 balance, uint256 available, uint256 reserves)
{
    balance = totalAssets();

    uint256 debt = _convertToAssets(
        debtSharesTotal,
        debtExchangeRateX96,
        Math.Rounding.Up
    );
    uint256 lent = _convertToAssets(
        totalSupply(),
        lendExchangeRateX96,
        Math.Rounding.Down
    );

    reserves = balance + debt > lent ? balance + debt - lent : 0;
    available = balance > reserves ? balance - reserves : 0;
}
```

While it's expected that this asset reserve must always be preserved (if we see the `_withdraw` doesn't allow user to withdraw from it), it's possible to drain all the reserve held by the vault using the `borrow` function:

```solidity
function borrow(uint256 tokenId, uint256 assets) external override {
    bool isTransformMode = transformedTokenId > 0 &&
        transformedTokenId == tokenId &&
        transformerAllowList[msg.sender];

    (
        uint256 newDebtExchangeRateX96,
        uint256 newLendExchangeRateX96
    ) = _updateGlobalInterest();

    _resetDailyDebtIncreaseLimit(newLendExchangeRateX96, false);

    Loan storage loan = loans[tokenId];

    // if not in transfer mode - must be called from owner or the vault itself
    if (
        !isTransformMode &&
        tokenOwner[tokenId] != msg.sender &&
        address(this) != msg.sender
    ) {
        revert Unauthorized();
    }

    uint256 shares = _convertToShares(
        assets,
        newDebtExchangeRateX96,
        Math.Rounding.Up
    );

    uint256 loanDebtShares = loan.debtShares + shares;
    loan.debtShares = loanDebtShares;
    debtSharesTotal += shares;

    if (
        debtSharesTotal >
        _convertToShares(
            globalDebtLimit,
            newDebtExchangeRateX96,
            Math.Rounding.Down
        )
    ) {
        revert GlobalDebtLimit();
    }
    if (assets > dailyDebtIncreaseLimitLeft) {
        revert DailyDebtIncreaseLimit();
    } else {
        dailyDebtIncreaseLimitLeft -= assets;
    }

    //@audit doesn't check that asset balance available is sufficient to cover borrowed assets

    _updateAndCheckCollateral(
        tokenId,
        newDebtExchangeRateX96,
        newLendExchangeRateX96,
        loanDebtShares - shares,
        loanDebtShares
    );

    uint256 debt = _convertToAssets(
        loanDebtShares,
        newDebtExchangeRateX96,
        Math.Rounding.Up
    );

    if (debt < minLoanSize) {