Algo Ssstablecoinsss

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

Liquidation Attempts to Seize More Collateral Than User Possesses

Root + Impact

Root Cause: The liquidate() function calculates total collateral to seize (debt value + 10% bonus) without verifying that the user actually has sufficient collateral balance in the specified token.

Impact: When a user's collateral is insufficient to cover both debt and bonus, the subtraction in _redeem_collateral() causes an underflow revert. This prevents legitimate liquidations from executing, leaving bad debt in the system and causing protocol insolvency over time.

Description

Normal Behavior: During liquidation, a liquidator covers a user's debt and receives the equivalent collateral value plus a 10% bonus as incentive. The user's collateral balance should be sufficient to cover both the debt value and the bonus.

Issue: The liquidate() function calculates the total collateral to seize (debt value + 10% bonus) without verifying that the user actually has this much collateral. When the user's collateral is insufficient, the subtraction in _redeem_collateral() will underflow.
# dsc_engine.vy
@external
def liquidate(collateral: address, user: address, debt_to_cover: uint256):
assert debt_to_cover > 0, "DSCEngine__NeedsMoreThanZero"
starting_user_health_factor: uint256 = self._health_factor(user)
assert starting_user_health_factor < MIN_HEALTH_FACTOR, "DSCEngine__HealthFactorOk"
token_amount_from_debt_covered: uint256 = self._get_token_amount_from_usd(collateral, debt_to_cover)
bonus_collateral: uint256 = (token_amount_from_debt_covered * LIQUIDATION_BONUS) // LIQUIDATION_PRECISION
# @> No check that user has sufficient collateral for debt + bonus
self._redeem_collateral(
collateral,
token_amount_from_debt_covered + bonus_collateral, # @> Could exceed user's balance
user,
msg.sender,
)
# ...

Risk

Likelihood:HIGH

  • Reason 1 : Occurs when a user becomes liquidatable with collateral value between 100% and 110% of their debt (in the specific collateral type being liquidated)

  • Reason 2 : Multi-collateral users may have sufficient total collateral but insufficient in one specific token

Impact:

  • Impact 1 : Protocol becomes insolvent as underwater positions cannot be cleared

  • Impact 2 : Legitimate liquidations revert, leaving bad debt in the system

Proof of Concept

A user deposits 1 ETH at $2000 and mints $900 DSC. When ETH drops to $950, the user becomes liquidatable. A liquidator attempts to cover the full $900 debt, requiring $990 worth of ETH (including 10% bonus), which equals approximately 1.042 ETH. Since the user only has 1 ETH, the subtraction underflows and the liquidation reverts.

# Setup:
# - User deposited 1 ETH at $2000 = $2000 collateral value
# - User minted $900 DSC (222% collateralization)
# - ETH price drops to $950
# - New collateral value: $950
# - Health factor: ($950 * 50 / 100) * 1e18 / $900 = 0.527e18 < 1e18 (liquidatable)
def test_liquidation_reverts_insufficient_collateral():
# Liquidator tries to cover full $900 debt
debt_to_cover = 900e18 # $900 in DSC
# Calculate collateral to seize:
# token_amount = $900 / $950 per ETH = 0.947 ETH
# bonus = 0.947 * 10% = 0.0947 ETH
# total = 0.947 + 0.0947 = 1.042 ETH
# User only has 1 ETH deposited
# 1.042 ETH > 1 ETH
# _redeem_collateral attempts:
# user_balance[user][ETH] -= 1.042 ETH
# 1.0 ETH - 1.042 ETH = UNDERFLOW REVERT
# Liquidation fails, bad debt remains in system

Recommended Mitigation

Before attempting to redeem collateral, verify the user has sufficient balance. If the user's collateral is insufficient, cap the seizure at their available balance and proportionally reduce the debt being covered.

# dsc_engine.vy
@external
def liquidate(collateral: address, user: address, debt_to_cover: uint256):
assert debt_to_cover > 0, "DSCEngine__NeedsMoreThanZero"
starting_user_health_factor: uint256 = self._health_factor(user)
assert starting_user_health_factor < MIN_HEALTH_FACTOR, "DSCEngine__HealthFactorOk"
token_amount_from_debt_covered: uint256 = self._get_token_amount_from_usd(collateral, debt_to_cover)
bonus_collateral: uint256 = (token_amount_from_debt_covered * LIQUIDATION_BONUS) // LIQUIDATION_PRECISION
total_collateral_to_redeem: uint256 = token_amount_from_debt_covered + bonus_collateral
+ # Verify user has sufficient collateral
+ user_collateral_balance: uint256 = self.user_to_token_address_to_amount_deposited[user][collateral]
+
+ # If user doesn't have enough, take all available collateral
+ if total_collateral_to_redeem > user_collateral_balance:
+ total_collateral_to_redeem = user_collateral_balance
+ # Recalculate debt_to_cover based on available collateral
+ # debt = (collateral * 100) / 110 (removing bonus)
+ token_amount_from_debt_covered = (user_collateral_balance * LIQUIDATION_PRECISION) // (LIQUIDATION_PRECISION + LIQUIDATION_BONUS)
+ debt_to_cover = self._get_usd_value(collateral, token_amount_from_debt_covered)
self._redeem_collateral(
collateral,
- token_amount_from_debt_covered + bonus_collateral,
+ total_collateral_to_redeem,
user,
msg.sender,
)
self._burn_dsc(debt_to_cover, user, msg.sender)
# ...
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 3 days 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!