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