Summary
The _redeem_collateral function in dsc_engine.vy does not verify that the user has sufficient collateral deposited before subtracting the requested amount. While Vyper 0.4.0 prevents silent underflows (the transaction will revert instead), this lack of validation causes a critical Denial of Service (DoS). During liquidations, rounding errors in _get_token_amount_from_usd can cause the calculated collateral amount to slightly exceed the user's actual deposit, causing the liquidation to fail and leaving the protocol permanently stuck.
Description
The _redeem_collateral function directly subtracts the amount from the user's balance without checking if they have enough:
@internal
def _redeem_collateral(
token_collateral_address: address,
amount_collateral: uint256,
_from: address,
_to: address,
):
# No balance check before subtraction!
self.user_to_token_address_to_amount_deposited[_from][
token_collateral_address
] -= amount_collateral
log CollateralRedeemed(token_collateral_address, amount_collateral, _from, _to)
success: bool = extcall IERC20(token_collateral_address).transfer(_to, amount_collateral)
assert success, "DSCEngine_TransferFailed"
In the liquidate function, the amount to redeem is calculated as token_amount_from_debt_covered + bonus_collateral. Due to integer division rounding in _get_token_amount_from_usd, this calculated amount can be 1 wei larger than the user's actual deposited collateral. Because there is no explicit check, the subtraction reverts, and the liquidation fails.
Risk
Severity: High
Likelihood: Medium
Impact: High
Liquidation DoS: Liquidations will fail for users whose collateral balance is slightly less than the calculated liquidation amount due to rounding.
Protocol Insolvency: If liquidations fail, unhealthy positions cannot be closed, leaving the protocol undercollateralized.
User Funds Locked: Legitimate users may be unable to redeem their collateral if the math results in a revert.
Proof of Concept
import boa
from eth_utils import to_wei
def test_liquidation_dos_due_to_rounding(dsce, dsc, weth, some_user, liquidator, eth_usd):
collateral_amount = to_wei(10, "ether")
with boa.env.prank(some_user):
weth.approve(dsce.address, collateral_amount)
dsce.deposit_collateral_and_mint_dsc(weth.address, collateral_amount, to_wei(19000, "ether"))
eth_usd.updateAnswer(18 * 10**8)
debt_to_cover = to_wei(19000, "ether")
with boa.env.prank(liquidator):
weth.approve(dsce.address, to_wei(20, "ether"))
dsce.deposit_collateral(weth.address, to_wei(20, "ether"))
dsc.approve(dsce.address, debt_to_cover)
with boa.reverts():
dsce.liquidate(weth.address, some_user, debt_to_cover)
print("✅ LIQUIDATION DoS EXPLOITED!")
print("Protocol is stuck because liquidation fails due to missing balance check.")
Run: mox test -k test_liquidation_dos_due_to_rounding
Recommended Mitigation
Add an explicit balance check in _redeem_collateral and cap the redemption amount in the liquidate function to prevent rounding-induced reverts:
@internal
def _redeem_collateral(
token_collateral_address: address,
amount_collateral: uint256,
_from: address,
_to: address,
):
# Explicit balance check
current_balance: uint256 = self.user_to_token_address_to_amount_deposited[_from][token_collateral_address]
assert current_balance >= amount_collateral, "DSCEngine_InsufficientCollateral"
self.user_to_token_address_to_amount_deposited[_from][token_collateral_address] -= amount_collateral
log CollateralRedeemed(token_collateral_address, amount_collateral, _from, _to)
success: bool = extcall IERC20(token_collateral_address).transfer(_to, amount_collateral)
assert success, "DSCEngine_TransferFailed"
Additionally, in the liquidate function, cap the amount:
# Inside liquidate():
amount_to_redeem: uint256 = token_amount_from_debt_covered + bonus_collateral
user_deposit: uint256 = self.user_to_token_address_to_amount_deposited[user][collateral]
if amount_to_redeem > user_deposit:
amount_to_redeem = user_deposit
self._redeem_collateral(collateral, amount_to_redeem, user, msg.sender)