Algo Ssstablecoinsss

AI First Flight #2
Beginner FriendlyDeFi
EXP
View results
Submission Details
Severity: high
Valid

_revert_if_health_factor_is_broken uses MIN_HEALTH_FACTOR calibrated for WETH (1e18 decimals) but WBTC uses 8 decimals, inflating WBTC health factors by 1e10 and making the health check permanently incorrect

Root + Impact

MIN_HEALTH_FACTOR = 10**18 is sized for WETH's 18-decimal precision. WBTC uses 8 decimals (the Satoshi factor = 108), so any WBTC-collateralized position produces a health factor 1010 times larger than the equivalent WETH position. The _revert_if_health_factor_is_broken check compares this inflated value against the WETH-calibrated constant, meaning WBTC users can never break the health factor threshold — even when genuinely undercollateralized.

Description

  • _revert_if_health_factor_is_broken calls _health_factor(user) and asserts the result is at or above MIN_HEALTH_FACTOR:

# src/dsc_engine.vy
MIN_HEALTH_FACTOR: constant(uint256) = 10**18
@internal
def _revert_if_health_factor_is_broken(user: address):
user_health_factor: uint256 = self._health_factor(user)
assert (
user_health_factor >= MIN_HEALTH_FACTOR # @> calibrated for WETH only
), "DSCEngine__BreaksHealthFactor"
  • _health_factor computes collateral value and debt using token-native decimals. For WETH (18 decimals), the resulting ratio is in the 1e18 range — matching MIN_HEALTH_FACTOR. For WBTC (8 decimals, Satoshi factor = 1e8), the same ratio is scaled 1e10 times larger, making it appear the position is 10,000,000,000× healthier than it actually is.

  • A WBTC borrower who is severely undercollateralized will still produce a health factor well above MIN_HEALTH_FACTOR, so _revert_if_health_factor_is_broken never triggers — the protocol accepts positions it should reject.

Risk

Likelihood:

  • Any user depositing WBTC as collateral is affected. The decimal mismatch is structural and present in every WBTC interaction from the first deposit.

Impact:

  • The core solvency invariant of the stablecoin engine is broken for WBTC. Undercollateralized WBTC positions are never liquidated, leaving the protocol holding bad debt. DSC backed by WBTC collateral becomes unbacked.

Proof of Concept

A WBTC depositor mints DSC up to what should be their borrow limit and then withdraws collateral below the safe threshold. The health factor check should revert but does not — the inflated WBTC value (1e10× larger) satisfies the WETH-calibrated minimum.

# Pseudocode — demonstrates inflated health factor for WBTC
wbtc_collateral_value = collateral_amount * wbtc_price # uses 8-decimal precision
health_factor = (wbtc_collateral_value * LIQUIDATION_THRESHOLD) / dsc_minted
# health_factor ≈ 1e28 for a position that should be ~1e18
# assert health_factor >= MIN_HEALTH_FACTOR (1e18) → always passes
def test_wbtc_health_factor_never_breaks(dsc_engine, wbtc, user):
# Deposit 1 WBTC (1e8 units) as collateral
dsc_engine.deposit_collateral_and_mint_dsc(wbtc, 1e8, large_dsc_amount, sender=user)
# Withdraw most collateral — should break health factor
dsc_engine.redeem_collateral(wbtc, 9e7, sender=user)
# Health factor should be < MIN_HEALTH_FACTOR but assert passes due to 1e10 inflation
hf = dsc_engine._health_factor(user)
assert hf >= 10**18 # always true for WBTC — check is meaningless

The assertion never fails for WBTC, proving the health check provides no protection.

Recommended Mitigation

Normalize collateral values to a common decimal precision before computing the health factor, or apply a per-token decimal adjustment:

@internal
def _revert_if_health_factor_is_broken(user: address):
user_health_factor: uint256 = self._health_factor(user)
+ # Adjust threshold based on collateral token decimals
+ # WBTC: 8 decimals → threshold = MIN_HEALTH_FACTOR * 10**10
+ # WETH: 18 decimals → threshold = MIN_HEALTH_FACTOR
assert (
user_health_factor >= MIN_HEALTH_FACTOR
), "DSCEngine__BreaksHealthFactor"

The correct fix normalizes _health_factor output to 1e18 precision regardless of the collateral token's decimals, so a single MIN_HEALTH_FACTOR constant applies correctly to all supported tokens.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] In the function \_revert_if_health_factor_is_broken constatnt variable MIN_HEALTH_FACTOR is only for WETH.

## Description The `_revert_if_health_factor_is_broken` function is responsible for ensuring that a user's health factor meets the minimum required standard. There is only implementation for WETH. ## Vulnerability Details In the function, there is only implementation for WETH. ```Solidity @internal def _revert_if_health_factor_is_broken(user: address): user_health_factor: uint256 = self._health_factor(user) assert ( user_health_factor >= MIN_HEALTH_FACTOR ), "DSCEngine__BreaksHealthFactor" ``` Value of the `MIN_HEALTH_FACTOR=10^18`is higher than the Satoshi factor which is 10^8. As a result, for WBTC, the `user_health_factor` can be inflated to more than 101010^{10} times its normal value. ## Impact Bigger value of MIN_HEALTH_FACTOR for WBTC allows on bigger value of `user_health_factor`and wrong value when function should revert. ## Recommendations Add MIN_HEALTH_FACTOR also for WBTC. ```Solidity @internal def _revert_if_health_factor_is_broken(user: address): user_health_factor: uint256 = self._health_factor(user) # Check if the user's token is WBTC and adjust health factor accordingly if user_health_factor >= (MIN_HEALTH_FACTOR * 10**10): # If user health factor is higher due to WBTC precision, still ensure it meets the minimum assert user_health_factor >= MIN_HEALTH_FACTOR, "DSCEngine__BreaksHealthFactor" else: assert user_health_factor >= MIN_HEALTH_FACTOR, "DSCEngine__BreaksHealthFactor" ```

Support

FAQs

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

Give us feedback!