sherlock-audit / 2023-04-jojo-judging

7 stars 4 forks source link

__141345__ - Incentive to self liquidate JUSD #386

Closed sherlock-admin closed 1 year ago

sherlock-admin commented 1 year ago

141345

medium

Incentive to self liquidate JUSD

Summary

Although the JUSD contract disallow liquidation from the same address, the user can bypass it with another wallet in control. The key to prevent users from doing self liquidation is to make it not profitable for traders.

Vulnerability Detail

The isValidLiquidator() check can be circumvented by using another wallet.

File: JUSDV1/src/Impl/JUSDBank.sol
365:     function isValidLiquidator(address liquidated, address liquidator) internal view {
366:         require(
367:             liquidator != liquidated,
368:             JUSDErrors.SELF_LIQUIDATION_NOT_ALLOWED
369:         );

The trader will have incentive to use another wallet to do a self liquidation as long as the liquidationPriceOff is not less than the insurance fee.

Assuming, 3% liquidationPriceOff and 2% insurance. For a loan of 100,000 JUSD, when liquidated, the difference in liquidationPriceOff and insurance fee is 3% - 2% = 1%, 100,000 * 1% = $1,000.

Even the liquidationPriceOff is the same value as insurance fee, there is still a little profit for self liquidation, since in JUSDBank.sol#_calculateLiquidateAmount(), amount.decimalMul(priceOff) is calculated first, and used as the base amount for insuranceFee calculation. With the above example, assuming the insurance fee is increased to 3%, 100,000 JUSD, after priceOff discount, reduced amount is 97% 100,000 = 97,000. A discount of 3,000. insuranceFee is 97,000 3% = 2,910. The self liquidation will profit for 3,000 - 2,910 = 90, due to the discounted base amount for insurance fee.

File: JUSDV1/src/Impl/JUSDBank.sol
396:         uint256 price = IPriceChainLink(reserve.oracle).getAssetPrice();
397:         uint256 priceOff = price.decimalMul(
398:             DecimalMath.ONE - reserve.liquidationPriceOff
399:         );
400:         uint256 liquidateAmount = amount.decimalMul(priceOff).decimalMul(
401:             JOJOConstant.ONE - reserve.insuranceFeeRate
402:         );
403:         uint256 JUSDBorrowed = liquidatedInfo.t0BorrowBalance.decimalMul(tRate);
404:         /*
405:         liquidateAmount <= JUSDBorrowed
406:         liquidateAmount = amount * priceOff * (1-insuranceFee)
407:         actualJUSD = actualCollateral * priceOff
408:         insuranceFee = actualCollateral * priceOff * insuranceFeeRate
409:         */
410:         if (liquidateAmount <= JUSDBorrowed) {
411:             liquidateData.actualCollateral = amount;
412:             liquidateData.insuranceFee = amount.decimalMul(priceOff).decimalMul(
413:                 reserve.insuranceFeeRate
414:             );
415:             liquidateData.actualLiquidatedT0 = liquidateAmount.decimalDiv(
416:                 tRate
417:             );
418:             liquidateData.actualLiquidated = liquidateAmount;
419:         } else {
420:             //            actualJUSD = actualCollateral * priceOff
421:             //            = JUSDBorrowed * priceOff / priceOff * (1-insuranceFeeRate)
422:             //            = JUSDBorrowed / (1-insuranceFeeRate)
423:             //            insuranceFee = actualJUSD * insuranceFeeRate
424:             //            = actualCollateral * priceOff * insuranceFeeRate
425:             //            = JUSDBorrowed * insuranceFeeRate / (1- insuranceFeeRate)
426:             liquidateData.actualCollateral = JUSDBorrowed
427:                 .decimalDiv(priceOff)
428:                 .decimalDiv(JOJOConstant.ONE - reserve.insuranceFeeRate);
429:             liquidateData.insuranceFee = JUSDBorrowed
430:                 .decimalMul(reserve.insuranceFeeRate)
431:                 .decimalDiv(JOJOConstant.ONE - reserve.insuranceFeeRate);
432:             liquidateData.actualLiquidatedT0 = liquidatedInfo.t0BorrowBalance;
433:             liquidateData.actualLiquidated = JUSDBorrowed;
434:         }

Impact

The trader will have motivation to do self liquidation with another wallet in control. Effectively skip some portion of fund ought to pay to the protocol.

Code Snippet

https://github.com/sherlock-audit/2023-04-jojo/blob/main/JUSDV1/src/Impl/JUSDBank.sol#L397-L434

Tool used

Manual Review

Recommendation

In JUSDOperation.sol#initReserve(), add checks for _liquidationPriceOff and _insuranceFeeRate, to make sure the insurance fee is larger than the discount, so that self liquidation is not profitable.

Duplicate of #321