Algo Ssstablecoinsss

AI First Flight #2
Beginner FriendlyDeFi
EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

High: Missing Collateral Balance Check in `_redeem_collateral` Causes Liquidation DoS

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

  1. Liquidation DoS: Liquidations will fail for users whose collateral balance is slightly less than the calculated liquidation amount due to rounding.

  2. Protocol Insolvency: If liquidations fail, unhealthy positions cannot be closed, leaving the protocol undercollateralized.

  3. 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):
# 1. User deposits exactly 10 ETH
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"))
# 2. Drop price to make user unhealthy
eth_usd.updateAnswer(18 * 10**8) # $18
# 3. Liquidator tries to liquidate the full debt
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)
# This will REVERT because the calculated collateral amount
# exceeds the user's 10 ETH deposit due to rounding!
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 # Cap to actual deposit to prevent rounding reverts
self._redeem_collateral(collateral, amount_to_redeem, user, msg.sender)
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!