Algo Ssstablecoinsss

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

Missing WBTC Precision Adjustment in Health Factor Calculation Breaks Solvency Checks Against MIN_HEALTH_FACTOR

Missing WBTC Precision Adjustment in Health Factor Calculation Breaks Solvency Checks Against MIN_HEALTH_FACTOR

Summary

  • Impact: High

  • Affected File(s): src/dsc_engine.vy:L24, src/dsc_engine.vy:L302-L317, src/dsc_engine.vy:L319-L330

  • DSCEngine verifies account solvency by checking an account's health factor against a constant MIN_HEALTH_FACTOR of 1e18.

  • When calculating health factors for WBTC (8 decimals), _get_usd_value() outputs values with 8 decimals of precision instead of 18-decimal USD wei.

  • The resulting health factor is 10 orders of magnitude too small ($10^8$ vs $10^{18}$ scale), permanently breaking health factor assertions and rendering WBTC unusable as collateral.

Vulnerability Details

Description

In dsc_engine.vy, MIN_HEALTH_FACTOR is hardcoded to 18 decimals:

MIN_HEALTH_FACTOR: public(constant(uint256)) = 1 * (10**18)

In _calculate_health_factor(), the engine computes the health factor by multiplying threshold-adjusted collateral by 1e18 and dividing by total minted DSC:

@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

In _get_usd_value(), token amounts are multiplied by price and divided by PRECISION (10^18):

@internal
@view
def _get_usd_value(token: address, amount: uint256) -> uint256:
...
@> return ((convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount) // PRECISION

Because WBTC has only 8 decimals, amount is in $10^8$ units. The calculation evaluates to:

The resulting collateral value has 8 decimals instead of 18 decimals. When plugged into _calculate_health_factor, the calculated health factor evaluates to instead of $10^{18}$, which is $10^{10}$ times below MIN_HEALTH_FACTOR.

Risk

Likelihood: High

  • Occurs on every deposit or borrow transaction involving WBTC.

Impact: High

  • Users depositing WBTC cannot mint any DSC because _revert_if_health_factor_is_broken() reverts immediately.

  • Any active WBTC position is treated by the contract as deeply underwater and eligible for liquidation despite being completely solvent.

Severity: High

Proof of Concept

def test_wbtc_health_factor_precision_broken(
dsc, eth_usd, btc_usd, wbtc, weth, some_user
):
token_addresses = [wbtc.address, weth.address]
feed_addresses = [btc_usd.address, eth_usd.address]
dsce = dsc_engine.deploy(token_addresses, feed_addresses, dsc)
dsc.set_minter(dsce.address, True)
dsc.transfer_ownership(dsce)
# 1 WBTC (8 decimals) at $100,000
btc_usd.updateAnswer(100_000 * 10**8)
one_wbtc = 1 * 10**8
dsc_mint_amount = 1 * 10**18 # 1 DSC ($1)
# User deposits $100k collateral and attempts to mint $1 debt
with boa.env.prank(some_user):
wbtc.mint_amount(one_wbtc)
wbtc.approve(dsce, one_wbtc)
# Reverts with DSCEngine__BreaksHealthFactor despite 10,000,000% collateralization ratio
with boa.reverts("DSCEngine__BreaksHealthFactor"):
dsce.deposit_collateral_and_mint_dsc(wbtc, one_wbtc, dsc_mint_amount)

Explanation: Even with $100,000 in WBTC collateral backing only $1 of DSC debt, the health check fails and reverts because the calculated health factor is .

Recommended Mitigation

Normalize collateral token amounts to 18 decimals based on token decimals() when computing collateral value:

+from ethereum.ercs import IERC20Detailed
@internal
@view
def _get_usd_value(token: address, amount: uint256) -> uint256:
price_feed: AggregatorV3Interface = AggregatorV3Interface(
self.token_address_to_price_feed[token]
)
...
+ token_decimals: uint8 = staticcall IERC20Detailed(token).decimals()
+ normalized_amount: uint256 = amount * (10**(18 - convert(token_decimals, uint256)))
return (
- (convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount
+ (convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * normalized_amount
) // PRECISION

Explanation: Normalizing amounts to 18 decimals ensures collateral_value_in_usd is in 18-decimal wei, aligning the calculated health factor with MIN_HEALTH_FACTOR = 1e18.

Updates

Lead Judging Commences

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