Algo Ssstablecoinsss

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

Users can mint at the liquidation boundary because there is no separate borrow LTV

Root + Impact

Description

  • The protocol should prevent users from opening positions that are immediately at the liquidation boundary. In lending and overcollateralized stablecoin systems, the borrow LTV is normally lower than the liquidation threshold so users have a safety buffer before liquidation.

  • DSCEngine uses LIQUIDATION_THRESHOLD as both the minting limit and the liquidation threshold. A user can mint until their health factor is exactly MIN_HEALTH_FACTOR, so even a tiny oracle price decrease makes the position liquidatable and exposes the user to liquidation bonus loss.

# src/dsc_engine.vy
LIQUIDATION_THRESHOLD: public(constant(uint256)) = 50
MIN_HEALTH_FACTOR: public(constant(uint256)) = 1 * (10**18)
@internal
def _mint_dsc(amount_dsc_to_mint: uint256):
assert amount_dsc_to_mint > 0, "DSCEngine__NeedsMoreThanZero"
self.user_to_dsc_minted[msg.sender] += amount_dsc_to_mint
@> self._revert_if_health_factor_is_broken(msg.sender)
extcall DSC.mint(msg.sender, amount_dsc_to_mint)
@internal
@pure
def _calculate_health_factor(
total_dsc_minted: uint256, collateral_value_in_usd: uint256
) -> uint256:
if total_dsc_minted == 0:
return max_value(uint256)
@> collateral_adjusted_for_threshold: uint256 = (
@> collateral_value_in_usd * LIQUIDATION_THRESHOLD
@> ) // LIQUIDATION_PRECISION
return (collateral_adjusted_for_threshold * (10**18)) // total_dsc_minted

Risk

Likelihood:

  • This occurs whenever a user mints the maximum amount accepted by _revert_if_health_factor_is_broken.

  • Normal market movement or a small oracle price decrease immediately pushes the position below MIN_HEALTH_FACTOR.

Impact:

  • Users can open protocol-approved positions that become liquidatable after a tiny price movement.

  • Liquidators can capture the liquidation bonus from users who had no borrow buffer, causing avoidable collateral loss and poor protocol safety.

Proof of Concept

def test_user_can_mint_at_boundary_and_become_liquidatable_after_tiny_drop(dsce, weth, eth_usd, some_user):
# ETH price in the default mock is 2_000e8.
collateral_amount = 10 * 10**18
collateral_value = dsce.get_usd_value(weth, collateral_amount) # 20_000e18
# Because LIQUIDATION_THRESHOLD is 50%, the exact accepted max debt is 10_000e18.
max_mint = (collateral_value * dsce.LIQUIDATION_THRESHOLD()) // dsce.LIQUIDATION_PRECISION()
with boa.env.prank(some_user):
weth.approve(dsce, collateral_amount)
dsce.deposit_collateral_and_mint_dsc(weth, collateral_amount, max_mint)
assert dsce.health_factor(some_user) == dsce.MIN_HEALTH_FACTOR()
# A tiny price decrease makes the position liquidatable.
eth_usd.updateAnswer(1999 * 10**8)
assert dsce.health_factor(some_user) < dsce.MIN_HEALTH_FACTOR()

Recommended Mitigation

- LIQUIDATION_THRESHOLD: public(constant(uint256)) = 50
+ MAX_MINT_LTV: public(constant(uint256)) = 45
+ LIQUIDATION_THRESHOLD: public(constant(uint256)) = 50

Use MAX_MINT_LTV when validating new debt creation in _mint_dsc, and keep LIQUIDATION_THRESHOLD for liquidation eligibility and health factor checks.

Updates

Lead Judging Commences

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