Algo Ssstablecoinsss

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

Hard-coded 18-decimal collateral math breaks WBTC health factors and liquidations

Root + Impact

Description

The engine scales the 8-decimal Chainlink price to 18 decimals, but it also assumes every collateral amount is already expressed with 18 token decimals. That is true for WETH and false for the explicitly supported zkSync WBTC, whose decimals() value is 8.

PRECISION: public(constant(uint256)) = 1 * (10**18)
ADDITIONAL_FEED_PRECISION: public(constant(uint256)) = 1 * (10**10)
​
return (
(convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount
) // PRECISION

For 1 WBTC, amount is 1e8. At a BTC/USD price of 60,000e8, get_usd_value() returns 60,000e8 instead of 60,000e18, underpricing collateral by exactly 1e10. The incorrect value feeds directly into _calculate_health_factor() and is compared against MIN_HEALTH_FACTOR=1e18, so economically safe WBTC positions revert when minting.

The inverse formula has the same root cause:

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

It returns a token amount in 1e18 scale even for 8-decimal WBTC. A WBTC liquidation therefore requests 1e10 too many base units and reverts against the user's recorded collateral.

Risk

Likelihood: High

  • The mismatch occurs deterministically in every WBTC valuation.

  • WBTC is one of only two supported collateral assets and the configured token has 8 decimals.

Impact: High

  • Deposited WBTC cannot support an economically meaningful DSC mint because its health factor is scaled incorrectly.

  • WBTC liquidation amounts are also wrong by 1e10, breaking the protocol's liquidation and solvency mechanism for a primary collateral.

Proof of Concept

The following Titanoboa test deploys the exact scoped contracts and uses 1e8 base units to represent 1 WBTC:

def test_wbtc_eight_decimal_amount_is_underpriced_by_ten_billion():
dsc, engine, wbtc, _, _, _ = deploy_system()
​
actual = engine.get_usd_value(wbtc.address, 10**8)
expected = 60_000 * 10**18
​
assert actual == 60_000 * 10**8
assert expected // actual == 10**10
​
user = boa.env.generate_address()
with boa.env.prank(user):
wbtc.mint_amount(10**8)
wbtc.approve(engine.address, 10**8)
engine.deposit_collateral(wbtc.address, 10**8)
​
with boa.reverts("DSCEngine__BreaksHealthFactor"):
engine.mint_dsc(10**18)

Observed result: PASS. The read-only zkSync RPC check also returns decimals()=8 for 0xBBeB516fb02a01611cBBE0453Fe3c580D7281011.

Recommended Mitigation

Normalize both the feed price and token amount using per-asset decimals. Conceptually:

usd_value = price * amount * 10**18 // (10**feed_decimals * 10**token_decimals)
token_amount = usd_value * 10**feed_decimals * 10**token_decimals // (price * 10**18)

Store validated decimals alongside each collateral/feed pair or query them immutably during deployment. Do not create a different MIN_HEALTH_FACTOR per token; the correct fix is to normalize every collateral value into the same 1e18 USD unit before calculating health factor. Add regression tests for WETH 18 decimals and WBTC 8 decimals in both conversion directions and liquidation.

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!