code-423n4 / 2024-03-revert-lend-findings

12 stars 10 forks source link

`V3Vault.sol` accept NFT from pool no one is using with low liquidity. Allow oracle price manipulation and steal tokens from the pool #369

Closed c4-bot-5 closed 6 months ago

c4-bot-5 commented 7 months ago

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)

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/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Oracle.sol#L133-L137
    function _checkPoolPrice(address token0, address token1, uint24 fee, uint256 derivedPoolPriceX96) internal view {
        IUniswapV3Pool pool = _getPool(token0, token1, fee);
        uint256 priceX96 = _getReferencePoolPriceX96(pool, 0);
        _requireMaxDifference(priceX96, derivedPoolPriceX96, maxPoolPriceDifference);
    }
  1. To bypass this pool price safety check. Create NFT positions does not affect pool price if it have tick on extreme position. For example using USDC/DAI v3 pool with fee 10000. The liquidity for tick 1 USDC=1 DAI is just 5$ of liquidity. There is nothing to stop user from creating NFT position that only kick in when price reach 10000 USDC = 0.1 DAI. This mean V3Vault and V3Oracle will accept this NFT with extreme tick position as having fair price
  2. Oracle consider this position holding 10000 USDC and 1 DAI worth 10000$ using price feed.
  3. V3Vault allow user borrow 8000 USDC against this NFT position.
  4. Swapping some DAI for USDC in this manipulated pool.
  5. The NFT position collateral now worth less than original borrowed amount.

Tools Used

Recommended Mitigation Steps

To deny NFT from pool with low liquidity. Whitelist acceptable UniswapNFT pool seem like easiest options.

It is not viable for Oracle to check both pool both price and liquidity as reasonable.

Assessed type

Uniswap

c4-pre-sort commented 7 months ago

0xEVom marked the issue as sufficient quality report

kalinbas commented 7 months ago

This issue is based on incorrect understanding of AMM price mechanics:

The basic thing to understand is that if you buy token A with token B, the price of token A is increasing the more you buy (starting at "current price").

In this case assuming the price is correct (which is validated by the oracle when you borrow) 1 USDC = 1 DAI, as you swap DAI for USDC the price of USDC increases so you would never reach a price of 0.1 DAI = 10000 USDC in that direction.

In other words: "NFT position that only kick in when price reach 10000 USDC = 0.1 DAI" The imagined can not be created when the current price is "1 USDC = 1 DAI" It could only be created when the price is completely off to begin with, because the current price is "on the other side" of the position it would only allow you to swap 10000 USD for 0.1 DAI and not the other way around.

Yet another way to look at this is that what you are proposing would require a liquidity distribution where at the same time the pool has a correct current price, but there exits also another amount of liquidity that could be arbitraged against the current price. This state of a pool is not possible.

c4-sponsor commented 7 months ago

kalinbas (sponsor) disputed

jhsagd76 commented 6 months ago

I cannot understand the attack scenario described by the warden. I feel the warden has mistaken the price direction of the swap,just like the sponsor said above, making the entire PoC very confusing. If you want the oracle to think your position is worth 10,000 USDC, you should now be on a tick with the opposite sign.

c4-judge commented 6 months ago

jhsagd76 marked the issue as unsatisfactory: Insufficient proof