Skip to content

Latest commit

 

History

History
153 lines (106 loc) · 4.42 KB

042.md

File metadata and controls

153 lines (106 loc) · 4.42 KB

Small Shamrock Rook

High

In NUMA liquidations, the max liquidation profit value can be greatly exceeded

Summary

The following check is used to limit the liquidator profit in liquidateNumaBorrower():

if (lstLiquidatorProfit > maxLstProfitForLiquidations) {
    vaultProfit = lstLiquidatorProfit - maxLstProfitForLiquidations;
}

lstLiquidatorProfit is obtained via:

lstLiquidatorProfit = receivedlst - lstProvidedEstimate;

Where lstProvidedEstimate is calculated as:

uint lstProvidedEstimate = vaultManager.numaToToken(
  numaAmount,
  last_lsttokenvalueWei,
  decimals,
  criticalScaleForNumaPriceAndSellFee
);

Whenever calling VaultManager.numaToToken(), the vault’s sell fee must be applied to the output to calculate the correct value, and this is evident in the rest of the codebase.

The issue is that lstProvidedEstimate (obtained via VaultManager.numaToToken() does not account for the sell fee incurred when NUMA is converted to rETH. This causes the liquidated numaAmount to be overvalued, inflating lstProvidedEstimate

Root Cause

Not accounting for sell fee when using VaultManager.numaToToken()

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. Perform a profitable liquidation on a large position.
  2. Profit value exceeds the maximum profit that has been set.

Impact

Not accounting for the sell fee deflates the lstLiquidatorProfit value from the true value, allowing the actual liquidation profit to exceed maxLstProfitForLiquidations

PoC

Add the following test to Lending.t.sol:

Foundry test
function testJ_profitMoreThanMaxProfit() public {
      // make sure to use the logs to clearly show the info.
    
      prepare_numaBorrow_JRV4();
    
      vm.roll(block.number + blocksPerYear / 4);
      cNuma.accrueInterest();
      (, uint liquidity, uint shortfall, uint badDebt) = comptroller
          .getAccountLiquidityIsolate(userA, cReth, cNuma);
      console.log(liquidity);
      console.log(shortfall);
      console.log(badDebt);
      // liquidate
    
      vm.startPrank(vault.owner());
      uint256 sellFee = vaultManager.getSellFeeScalingUpdate();
    
      vm.startPrank(userC);
    
      uint numaAmountBuy = 1000 ether;
      rEth.approve(address(vault), 2 * numaAmountBuy);
      vault.buy(2 * numaAmountBuy, numaAmountBuy, userC);
    
      uint balC = rEth.balanceOf(userC);
      uint numaBalance = numa.balanceOf(userC);
      uint256 lstValueOfNumaLiquidated = vault.numaToLst(cNuma.borrowBalanceCurrent(userA));
    
      (
          ,
          uint256 criticalScale,
    
      ) = vault.updateVaultAndUpdateDebasing();
    
      uint256 noFee_lstValueOfNumaLiquidated 
      = vaultManager.numaToToken(
          cNuma.borrowBalanceCurrent(userA),
          vault.last_lsttokenvalueWei(),
          1e18,
          criticalScale
      );
    
      numa.approve(address(vault), numaBalance);
      vault.liquidateNumaBorrower(userA, type(uint256).max, false, false);
    
      console.log("rETH received: %e", rEth.balanceOf(userC) - balC);
    
      console.log("rEth value spent: %e", lstValueOfNumaLiquidated);
      
      // actual profit greatly exceeds the max profit of 1e19 
      console.log("profit: %e", (rEth.balanceOf(userC) - balC) - lstValueOfNumaLiquidated);
    }

Note that the max LST liquidation profit is 1e19 (can be checked by adding console logs since there's no getter function).

Console output:

rETH received: 9.33073160044779735711e20
rEth value spent: 8.76919502042540748925e20
profit: 5.6153658002238986786e19

The profit greatly exceeds the maxLstLiquidatorProfit of 1e19.

Mitigation

Use the numaToLst() function instead as this accounts for the sell fee:

-uint lstProvidedEstimate = vaultManager.numaToToken(
-       numaAmount,
-       last_lsttokenvalueWei,
-       decimals,
-        criticalScaleForNumaPriceAndSellFee
- );

+ uint lstProvidedEstimate = this.numaToLst(numaAmount);

Applying the fix, here is the console output when re-running the PoC:

rETH received: 8.86919502042540748925e20
rEth value spent: 8.76919502042540748925e20
profit: 1e19

We can see that the profit is correctly capped at 1e19