Algo Ssstablecoinsss

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

WBTC (8 decimals) is mispriced by 1e10x — unusable as collateral and unliquidatable

Root + Impact

Description

Both pricing functions in DSCEngine assume every collateral token has 18 decimals, but real WBTC has 8 decimals. The error is a factor of 1e10 in both directions, and it breaks WBTC — one of only two collateral assets — completely.

_get_usd_value undervalues WBTC collateral 1e10x: 1 WBTC at $60,000 is valued at $0.000006, so a user can mint at most 0.000003 DSC against a whole BTC. WBTC is effectively unusable as collateral.

@> return (
@> (convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount
@> ) // PRECISION

_get_token_amount_from_usd overstates the WBTC owed on liquidation 1e10x: liquidating a WBTC position tries to seize more WBTC than the user deposited, the balance subtraction underflows, and the liquidation reverts. WBTC-backed bad debt can never be cleared.

@> return (
@> (usd_amount_in_wei * PRECISION) // (
@> convert(price, uint256) * ADDITIONAL_FEED_PRECISION
@> )
@> )

The repo's own mocks hide this: src/mocks/mock_token.vy gives every token 18 decimals, so the existing tests never exercise real 8-decimal WBTC.

Risk

Likelihood: High — the error is deterministic; any use of WBTC (a listed collateral token) triggers it, and the mock suite masks it.

Impact: High — half of the protocol's collateral system is broken. Users cannot mint DSC against WBTC at all, and WBTC-backed positions can never be liquidated, so bad debt in WBTC positions can never be cleared and the protocol can accrue unliquidatable undercollateralized debt.

Proof of Concept

The PoCs use a WBTC mock with real 8 decimals (src/mocks/mock_wbtc_8dec.vy) and a BTC feed at $60,000. The first test shows the 1e10x undervaluation and that minting 100 DSC against $60,000 of BTC reverts. The second test shows the 1e10x overstatement and that liquidating a WBTC position reverts because it would seize 1.1 WBTC from a 1 WBTC deposit.

def test_H1_wbtc_collateral_undervalued_1e10(setup):
s = setup
_deposit_wbtc(s)
​
# 1 WBTC at $60,000 should be worth 60,000e18 in USD terms.
actual = s["engine"].get_usd_value(s["wbtc"], ONE_WBTC)
expected = to_wei(60_000, "ether")
assert actual == 6_000_000_000_000, f"got {actual}"
assert actual * 10**10 == expected, "WBTC is undervalued by 1e10"
​
_, collateral_value = s["engine"].get_account_information(s["user"])
assert collateral_value == 6_000_000_000_000
​
# The user deposited $60,000 of BTC but cannot even mint 100 DSC.
with boa.env.prank(s["user"]):
with boa.reverts("DSCEngine__BreaksHealthFactor"):
s["engine"].mint_dsc(to_wei(100, "ether"))
​
def test_H1_wbtc_liquidation_amount_overstated_1e10(setup):
s = setup
_deposit_wbtc(s)
​
# 100 DSC of BTC at $60k should be 100/60000 BTC = 166,667 raw units.
actual = s["engine"].get_token_amount_from_usd(s["wbtc"], to_wei(100, "ether"))
assert actual == 1_666_666_666_666_666, f"got {actual}"
# Strip the 1e10 overstatement and you get exactly the right answer:
assert actual // 10**10 == 166_666, "WBTC amount overstated by 1e10"
​
# The most the user can mint against 1 WBTC is dust: 3e12 wei DSC.
max_mint = 3 * 10**12
with boa.env.prank(s["user"]):
s["engine"].mint_dsc(max_mint)
assert s["dsc"].balanceOf(s["user"]) == max_mint
​
# Crash BTC to $30,000 -> position is now liquidatable (HF = 0.5e18).
s["btc_feed"].updateAnswer(30_000 * 10**8)
assert s["engine"].health_factor(s["user"]) == to_wei(0.5, "ether")
​
# Liquidator has DSC to cover the dust debt.
with boa.env.prank(s["liquidator"]):
s["weth"].approve(s["engine"], to_wei(10, "ether"))
s["engine"].deposit_collateral_and_mint_dsc(
s["weth"], to_wei(10, "ether"), to_wei(1_000, "ether")
)
s["dsc"].approve(s["engine"], max_mint)
# Seizing the dust debt would take 1.1 WBTC > the 1 WBTC deposited.
with boa.reverts():
s["engine"].liquidate(s["wbtc"], s["user"], max_mint)

Recommended Mitigation

Scale by each collateral token's actual decimals — read decimals() from the token (or store a per-token decimal factor at deploy time) — in both _get_usd_value and _get_token_amount_from_usd, instead of assuming 18 decimals. Also fix the mocks so the WBTC mock uses 8 decimals, so the test suite exercises the real configuration.

# _get_usd_value: divide by 10 ** token_decimals, not by 10**18:
- return ((convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount) // PRECISION
+ return ((convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount) // (10 ** token_decimals)
​
# _get_token_amount_from_usd: multiply by 10 ** token_decimals:
- return (usd_amount_in_wei * PRECISION) // (convert(price, uint256) * ADDITIONAL_FEED_PRECISION)
+ return (usd_amount_in_wei * (10 ** token_decimals)) // (convert(price, uint256) * ADDITIONAL_FEED_PRECISION)
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 24 minutes 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!