sherlock-audit / 2023-10-real-wagmi-judging

16 stars 14 forks source link

Kral01 - [H-01] Particularly high value of MINIMUM_BORROWED_AMOUNT can make the protocol unusable. #178

Closed sherlock-admin2 closed 11 months ago

sherlock-admin2 commented 11 months ago

Kral01

high

[H-01] Particularly high value of MINIMUM_BORROWED_AMOUNT can make the protocol unusable.

Summary

The high value of MINIMUM_BORROWED_AMOUNT in Constants.sol can make the protocol unusable for most pools where SaleToken is a 'lower' valued token than HoldToken.

Vulnerability Detail

There is a check in LiquidityManager.sol that checks if the borrowedAmount is greater than the constant MINIMUM_BORROWED_AMOUNT = 100000. Else it reverts. This is particularly tricky when the SaleToken is a 'lower' valued token than HoldToken.

Impact

Lets take an example of a standard pool of WBTC/WETH, when WETH is the saleToken and WBTC is the holdToken, the user has to provide absurd amounts of liquidity to interact with the protocol.

Here is an end-end coded PoC that shows how 'large' amount of Liquidity we have to provide.

  1. Create a Makefile and get a recent block number and MAINNET_RPC in your .env file.
include .env

test_func:
    @forge test --fork-url ${MAINNET_RPC} --fork-block-number ${MAINNET_BLOCK} -vvvv --ffi --mt ${P}
  1. Now paste this into your directory and run make test_func P=test_Borrow
    
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.10;
    import "forge-std/Test.sol";
    import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
    import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
    import { LiquidityBorrowingManager } from "contracts/LiquidityBorrowingManager.sol";
    import { AggregatorMock } from "contracts/mock/AggregatorMock.sol";
    import { HelperContract } from "../testsHelpers/HelperContract.sol";
    import { INonfungiblePositionManager } from "contracts/interfaces/INonfungiblePositionManager.sol";

import {ApproveSwapAndPay} from "contracts/abstract/ApproveSwapAndPay.sol";

import {LiquidityManager} from "contracts/abstract/LiquidityManager.sol";

import {TickMath} from "../../contracts/vendor0.8/uniswap/TickMath.sol";

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

contract ContractTest is Test, HelperContract { IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IUniswapV3Pool WBTC_WETH_500_POOL = IUniswapV3Pool(0x4585FE77225b41b697C938B018E2Ac67Ac5a20c0); IUniswapV3Pool WETH_USDT_500_POOL = IUniswapV3Pool(0x11b815efB8f581194ae79006d24E0d814B7697F6); address constant NONFUNGIBLE_POSITION_MANAGER_ADDRESS = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; /// Mainnet, Goerli, Arbitrum, Optimism, Polygon address constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; /// Mainnet, Goerli, Arbitrum, Optimism, Polygon address constant UNISWAP_V3_QUOTER_V2 = 0x61fFE014bA17989E743c5F6cB21bF9697530B21e; bytes32 constant UNISWAP_V3_POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; /// Mainnet, Goerli, Arbitrum, Optimism, Polygon address constant alice = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; address constant bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; AggregatorMock aggregatorMock; LiquidityBorrowingManager borrowingManager;

uint256 tokenId;

uint256 deadline = block.timestamp + 15;

function setUp() public {
    vm.createSelectFork("mainnet", 17_329_500);
    vm.label(address(WETH), "WETH");
    vm.label(address(USDT), "USDT");
    vm.label(address(WBTC), "WBTC");
    vm.label(address(WBTC_WETH_500_POOL), "WBTC_WETH_500_POOL");
    vm.label(address(WETH_USDT_500_POOL), "WETH_USDT_500_POOL");
    vm.label(address(this), "ContractTest");
    aggregatorMock = new AggregatorMock(UNISWAP_V3_QUOTER_V2);
    borrowingManager = new LiquidityBorrowingManager(
        NONFUNGIBLE_POSITION_MANAGER_ADDRESS,
        UNISWAP_V3_QUOTER_V2,
        UNISWAP_V3_FACTORY,
        UNISWAP_V3_POOL_INIT_CODE_HASH
    );
    vm.label(address(borrowingManager), "LiquidityBorrowingManager");
    vm.label(address(aggregatorMock), "AggregatorMock");
    deal(address(USDT), address(this), 1000000000e6);
    deal(address(WBTC), address(this), 10e8);
    deal(address(WETH), address(this), 100e18);
    deal(address(USDT), alice, 1000000000000000000000000e6);
    deal(address(WBTC), alice, 1000e8);
    deal(address(WETH), alice, 100000000000000000e18);

    deal(address(USDT), bob, 1000000000e6);
    deal(address(WBTC), bob, 1000e8);
    deal(address(WETH), bob, 10000e18);
    //deal eth to alice
    deal(alice, 10000 ether);
    deal(bob, 1000 ether);

    // deal(address(USDT), address(borrowingManager), 1000000000e6);
    // deal(address(WBTC), address(borrowingManager), 10e8);
    // deal(address(WETH), address(borrowingManager), 100e18);

    _maxApproveIfNecessary(address(WBTC), address(borrowingManager), type(uint256).max);
    _maxApproveIfNecessary(address(WETH), address(borrowingManager), type(uint256).max);
    _maxApproveIfNecessary(address(USDT), address(borrowingManager), type(uint256).max);

    vm.startPrank(alice);
    _maxApproveIfNecessary(address(WBTC), address(borrowingManager), type(uint256).max);
   // _maxApproveIfNecessary(address(WETH), address(borrowingManager), type(uint256).max);
   IERC20(address(WETH)).approve(address(borrowingManager), type(uint256).max);
   IERC20(address(WBTC)).approve(address(borrowingManager), type(uint256).max);
   IERC20(address(WETH)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);
   IERC20(address(WBTC)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);
    _maxApproveIfNecessary(address(USDT), address(borrowingManager), type(uint256).max);
    _maxApproveIfNecessary(address(WBTC), address(this), type(uint256).max);
    _maxApproveIfNecessary(address(WETH), address(this), type(uint256).max);
    _maxApproveIfNecessary(address(USDT), address(this), type(uint256).max);

    // _maxApproveIfNecessary(
    //     address(WBTC),
    //     NONFUNGIBLE_POSITION_MANAGER_ADDRESS,
    //     type(uint256).max
    // );
    // _maxApproveIfNecessary(
    //     address(WETH),
    //     NONFUNGIBLE_POSITION_MANAGER_ADDRESS,
    //     type(uint256).max
    // );
    _maxApproveIfNecessary(
        address(USDT),
        NONFUNGIBLE_POSITION_MANAGER_ADDRESS,
        type(uint256).max
    );

    ( tokenId,,,)= mintPositionAndApprove();
    vm.stopPrank();

    vm.startPrank(bob);
    IERC20(address(WETH)).approve(address(borrowingManager), type(uint256).max);
    vm.stopPrank();

    vm.startPrank(address(borrowingManager));
    IERC20(address(WETH)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);
   IERC20(address(WBTC)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);
    vm.stopPrank();
}

function test_SetUpState() public {
    assertEq(WBTC_WETH_500_POOL.token0(), address(WBTC));
    assertEq(WBTC_WETH_500_POOL.token1(), address(WETH));
    assertEq(WETH_USDT_500_POOL.token0(), address(WETH));
    assertEq(WETH_USDT_500_POOL.token1(), address(USDT));
    assertEq(USDT.balanceOf(address(this)), 1000000000e6);
    assertEq(WBTC.balanceOf(address(this)), 10e8);
    assertEq(WETH.balanceOf(address(this)), 100e18);
    assertEq(borrowingManager.owner(), address(this));
    assertEq(borrowingManager.dailyRateOperator(), address(this));
    assertEq(
        borrowingManager.computePoolAddress(address(USDT), address(WETH), 500),
        address(WETH_USDT_500_POOL)
    );
    assertEq(
        borrowingManager.computePoolAddress(address(WBTC), address(WETH), 500),
        address(WBTC_WETH_500_POOL)
    );
    assertEq(
        address(borrowingManager.underlyingPositionManager()),
        NONFUNGIBLE_POSITION_MANAGER_ADDRESS
    );
}

LiquidityManager.LoanInfo[] loans;
function createBorrowParams(uint256 _tokenId)public returns(LiquidityBorrowingManager.BorrowParams memory borrow ){
    bytes memory swapData = "";

    LiquidityManager.LoanInfo memory loanInfo = LiquidityManager.LoanInfo({
        liquidity: 10000e7,
        tokenId: _tokenId //5500 = 1319241402 500 = 119931036 10 = 2398620
        });

        loans.push(loanInfo);

        LiquidityManager.LoanInfo[] memory loanInfoArrayMemory = loans;

     borrow = LiquidityBorrowingManager.BorrowParams({
        internalSwapPoolfee: 500,
        saleToken: address(WETH), //token1 - WETH
        holdToken: address(WBTC), //token0 - WBTC 
        minHoldTokenOut: 1,
        maxCollateral: 10e8,
        externalSwap: ApproveSwapAndPay.SwapParams({
            swapTarget: address(0),
            swapAmountInDataIndex: 0,
            maxGasForCall: 0,
            swapData: swapData
        }),
        loans: loanInfoArrayMemory
});

}

function  mintPositionAndApprove()public returns (uint256 _tokenId, uint128 liquidity, uint256 amount0, uint256 amount1){

     INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
        token0:address(WBTC),
        token1:address(WETH),
        fee:3000,
        tickLower: 253320,//TickMath.MIN_TICK,
        tickUpper: 264600, //TickMath.MAX_TICK ,
        amount0Desired:10e8,
        amount1Desired:100e18,
        amount0Min:0,
        amount1Min:0,
        recipient:alice,
        deadline:block.timestamp 
    });
    (_tokenId,  liquidity,  amount0,  amount1)  = INonfungiblePositionManager(NONFUNGIBLE_POSITION_MANAGER_ADDRESS).mint{value: 1 ether}(mintParams);
    INonfungiblePositionManager(NONFUNGIBLE_POSITION_MANAGER_ADDRESS).approve(address(borrowingManager), _tokenId);
}

function test_Borrow() public{
    vm.startPrank(alice);
    console.log("alice", alice);

     console.log("Before borrow");
     //console.log(IERC20(address(WETH)).balanceOf(address(alice)));
     uint256 BalanceBefore = IERC20(address(WBTC)).balanceOf(address(alice));

    LiquidityBorrowingManager.BorrowParams memory AliceBorrowing = createBorrowParams(tokenId);

    borrowingManager.borrow(AliceBorrowing,deadline );

    console.log("After borrow");
    //console.log(IERC20(address(WETH)).balanceOf(address(alice)));
     uint256 BalanceAfter = IERC20(address(WBTC)).balanceOf(address(alice));

     uint256 amountOfWBTCSpent = BalanceBefore - BalanceAfter;
     console.log("Amount spent", amountOfWBTCSpent);

}

}

Feel free to adjust the liquidity in `LoanInfo` and see that lower liquidity will revert.

As you can see from the outputs, the amount of  `WBTC` spent is quite a chunk  and you can see how much WBTC is held by the largest holders in Ethereum -> [here](https://www.coincarp.com/currencies/wrapped-bitcoin/richlist/) this is the case between WBTC and WETH, this value can go even higher when using pools like WBTC/USDC, WETH/USDC and since the protocol is supposed to  use any Uniswap V3 pool, there are a lot more other [pools](https://info.uniswap.org/#/pools) with higher price differences which will make the `Borrow` function revert everytime a user tries to use the protocol.

## Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L135C6-L140C6

```solidity
 if (borrowedAmount > Constants.MINIMUM_BORROWED_AMOUNT) {
            ++borrowedAmount;
        } else {
            revert TooLittleBorrowedLiquidity(liquidity);
        }

This condition is harder to satisfy when the saleToken is significantly 'lower' valued than the holdToken

Tool used

Manual Review

Recommendation

Set an appropriate value for MINIMUM_BORROWED_AMOUNT .

Duplicate of #181