Algo Ssstablecoinsss

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

Strict Health Factor Improvement Requirement in liquidate() Prevents Liquidation of Positions with 100%-110% Collateralization, Leading to Bad Debt

Root + Impact

Description

  • Normal Behavior:
    The liquidate function is intended to improve a borrower's health factor by repaying part of their debt in exchange for collateral plus a 10% liquidation bonus.


  • Specific Issue:
    The function requires that the health factor strictly improves after liquidation (dsc_engine.vy:131). Given the 10% liquidation bonus (dsc_engine.vy:121), a liquidation can only improve the health factor if the collateral value C exceeds 1.1 * D. For positions where C is between 1.0 * D and 1.1 * D (already unhealthy but not deeply underwater), any liquidation either worsens the health factor or reverts due to insufficient collateral. These positions cannot be liquidated and accumulate as bad debt as the market continues to fall.

# dsc_engine.vy:121 — 10% liquidation bonus
LIQUIDATION_BONUS: constant(uint256) = 10
​
# dsc_engine.vy:131 — strict improvement requirement
assert new_health_factor > old_health_factor, "Health factor must improve"

Risk

Likelihood:

  • This condition occurs naturally whenever a position's collateral value falls into the 100%-110% range of its debt.

  • No attacker action is required — it is a structural flaw in the liquidation logic.

Impact:

  • Positions with 100%-110% collateralization cannot be liquidated.

  • As prices fall, these positions become permanently insolvent, creating bad debt for the protocol.

  • The protocol (and its stablecoin holders) absorb the losses.

Proof of Concept

The test sets up a position with 1 WETH collateral ($105,000 after price drop) and 100,000 DSC debt. The health factor is 1.05, which is below 1.0 — the position is unhealthy. However, because the collateral value (105,000) is less than 1.1 * debt (110,000), any liquidation with a 10% bonus would either worsen the health factor or revert due to insufficient collateral. The test confirms that liquidate reverts, leaving the position unliquidatable. As ETH price continues to fall, this position becomes bad debt for the protocol.

import boa
from eth_utils import to_wei
​
from src import decentralized_stable_coin, dsc_engine
from src.mocks import MockV3Aggregator, mock_token
​
​
def test_liquidation_reverts_in_critical_range():
dsc = decentralized_stable_coin.deploy()
weth = mock_token.deploy()
eth_usd = MockV3Aggregator.deploy(8, 100_000 * 10**8)
engine = dsc_engine.deploy(
[weth.address],
[eth_usd.address],
dsc.address,
)
dsc.set_minter(engine.address, True)
dsc.transfer_ownership(engine.address)
​
victim = boa.env.generate_address()
liquidator = boa.env.generate_address()
one_weth = to_wei(1, "ether")
​
# Victim deposits 1 WETH ($100,000) and mints 100,000 DSC
# Health factor = 1.0 (exactly at the boundary)
with boa.env.prank(victim):
weth.mint_amount(one_weth)
weth.approve(engine.address, one_weth)
engine.deposit_collateral_and_mint_dsc(
weth.address, one_weth, to_wei(100_000, "ether")
)
​
# ETH price drops slightly to $105,000 -> C = 105,000, D = 100,000
# Health factor = 1.05 (unhealthy but above 1.0)
eth_usd.updateAnswer(105_000 * 10**8)
​
# Verify the position is unhealthy
assert engine.health_factor(victim) < to_wei(1, "ether")
print("Health factor before liquidation:", engine.health_factor(victim))
​
# Liquidator attempts to liquidate
debt_to_cover = to_wei(1_000, "ether")
with boa.env.prank(liquidator):
dsc.mint_amount(debt_to_cover)
dsc.approve(engine.address, debt_to_cover)
# This reverts because the health factor cannot strictly improve
with boa.reverts():
engine.liquidate(weth.address, victim, debt_to_cover)
​
print("Liquidation reverted: position cannot be liquidated")

Recommended Mitigation

Option 1: Allow liquidation when the health factor does not worsen.

Option 2: Adjust the liquidation bonus based on the position's health factor.

Option 3: Allow partial liquidation with a dynamic bonus.

#option1
- assert new_health_factor > old_health_factor, "Health factor must improve"
+ assert new_health_factor >= old_health_factor, "Health factor must not worsen"
​
#option2
- LIQUIDATION_BONUS: constant(uint256) = 10
+ # Use a smaller bonus for positions close to the liquidation threshold
+ bonus: uint256 = 5 if old_health_factor < to_wei(1, "ether") else 10
​
#option3
+ # Reduce the bonus proportionally to the collateralization ratio
+ bonus: uint256 = min(10, (collateral_value * 100) / debt_value - 100)
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour 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!