codehawks-2023-07-dsc-m12

[M-12] DoS of full liquidations are possible by frontrunning the liquidators

보고서

Summary

청산을 시도할 때 정확한 DSC 토큰 양을 파라미터로 받으므로, 청산 시도하는 양보다 적은 양의 DSC가 남도록 프론트러닝으로 선수쳐 청산하면 청산 시도가 revert 된다. 특히 전체 청산을 시도하는 경우 최소한의 DSC를 청산하면 막을 수 있다.

Keyword

frontrunning, integer underflow

Vulnerability

liquidate 함수는 담보가 부족한 유저의 담보를 청산자(다른 유저)가 자신의 DSC 토큰을 이용하여 청산할 수 있는 기능이다. 이 때, 소각할 DSC 토큰의 개수의 정확한 양을 지정해야 한다.

    function liquidate(address collateral, address user, uint256 debtToCover)
        external
        moreThanZero(debtToCover)
        nonReentrant
    {
        ...
@>      _burnDsc(debtToCover, user, msg.sender);
        ...
    }
 
    function _burnDsc(uint256 amountDscToBurn, address onBehalfOf, address dscFrom) private {
@>      s_DSCMinted[onBehalfOf] -= amountDscToBurn; //Undeflow will happen here
        bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn);
        if (!success) {
            revert DSCEngine__TransferFailed();
        }
        i_dsc.burn(amountDscToBurn);
    }

악성 행위자가 프론트러닝으로 약간의 DSC만 청산하여 전체 청산을 막을 수 있다.

담보가 부족한 유저의 전체 DSC를 청산하려고 liquidate를 호출했다고 하자. 이 때, 프론트러닝으로 약간의 DSC를 먼저 청산한다.

담보가 부족한 유저의 DSC 민팅량이 프론트러닝으로 인해 줄었으므로 전체 DSC를 청산하려고 시도한 트랜잭션은 revert 된다. _burnDsc 에서 언더플로우가 발생하기 때문이다.

Impact

청산 시도를 방해할 수 있다.

Mitigation

liquidate에 debtToCover 파라미터로 type(uint256).max를 넘기면 현재 잔액과 관계 없이 대상 유저의 모든 DSC를 청산하는 기능을 추가한다.

diff --git a/DSCEngine.orig.sol b/DSCEngine.sol
index e7d5c0d..6feef25 100644
--- a/DSCEngine.orig.sol
+++ b/DSCEngine.sol
@@ -227,36 +227,40 @@ contract DSCEngine is ReentrancyGuard {
      * Follows CEI: Checks, Effects, Interactions
      */
     function liquidate(address collateral, address user, uint256 debtToCover)
         external
         moreThanZero(debtToCover)
         nonReentrant
     {
         // need to check health factor of the user
         uint256 startingUserHealthFactor = _healthFactor(user);
         if (startingUserHealthFactor >= MIN_HEALTH_FACTOR) {
             revert DSCEngine__HealthFactorOk();
         }
         // We want to burn their DSC "debt"
         // And take their collateral
         // Bad User: $140 ETH, $100 DSC
         // debtToCover = $100
         // $100 of DSC == ??? ETH?
         // 0.05 ETH
+        if (debtToCover == type(uint256).max) {
+            (uint256 dscMinted,) = _getAccountInformation(user);
+            debtToCover = dscMinted;
+        }
         uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover);
         // And give them a 10% bonus
         // So we are giving the liquidator $110 of WETH for 100 DSC
         // We should implement a feature to liquidate in the event the protocol is insolvent
         // And sweep extra amounts into a treasury
         // 0.05 * 0.1 = 0.005. Getting 0.055
         uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION;
         uint256 totalCollateralToRedeem = tokenAmountFromDebtCovered + bonusCollateral;
         _redeemCollateral(user, msg.sender, collateral, totalCollateralToRedeem);
         // We need to burn the DSC
         _burnDsc(debtToCover, user, msg.sender);
 
         uint256 endingUserHealthFactor = _healthFactor(user);
         if (endingUserHealthFactor <= startingUserHealthFactor) {
             revert DSCEngine__HealthFactorNotImproved();
         }
         _revertIfHealthFactorIsBroken(msg.sender);
     }

tags: bughunting, codehawks, smart contract, solidity, frontrunning, integer overflow underflow, severity medium