Algo Ssstablecoinsss

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

USD valuation assumes 18-decimal collateral, so WBTC (8 decimals) is mis-scaled by 1e10 — WBTC deposits are valued at ~zero and WBTC liquidation math overflows the seize amount

Description

The engine converts between token amounts and USD using only the price-feed's decimals, never the collateral token's own decimals:

ADDITIONAL_FEED_PRECISION: public(constant(uint256)) = 1 * (10**10) # 8-dec feed -> 18
PRECISION: public(constant(uint256)) = 1 * (10**18)
@internal
@view
def _get_usd_value(token: address, amount: uint256) -> uint256:
...
return (
(convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount
) // PRECISION
@internal
@view
def _get_token_amount_from_usd(token: address, usd_amount_in_wei: uint256) -> uint256:
...
return (
(usd_amount_in_wei * PRECISION) // (
convert(price, uint256) * ADDITIONAL_FEED_PRECISION
)
)

ADDITIONAL_FEED_PRECISION (1e10) scales an 8-decimal Chainlink answer up to 18 decimals. The formula then multiplies by amount and divides by PRECISION (1e18) — which is only correct when amount is expressed in 18 decimals. It never reads ERC20.decimals() for the collateral token.

The protocol's own scope names WETH and WBTC as collateral. WETH has 18 decimals (fine), but WBTC has 8 decimals. For WBTC, amount is 1e8-scaled, not 1e18-scaled, so every conversion is off by a factor of 1e10.

Verified numerically:

  • _get_usd_value(WBTC, 1e8) (one whole WBTC at $60,000) returns 6_000_000_000_000 wei ≈ $0.000006, instead of 60_000e18. WBTC collateral is undervalued by 1e10×.

  • _get_token_amount_from_usd(WBTC, 60_000e18) (the WBTC worth $60k) returns 1e18 raw units = 1e10 whole WBTC, instead of 1e8 (one WBTC). The seize amount is overstated by 1e10×.

Consequences:

  1. WBTC is unusable as collateral. A depositor who posts real WBTC gets essentially zero collateral value, so _revert_if_health_factor_is_broken blocks them from minting any meaningful DSC against it. Their WBTC is locked dead weight while the health factor treats it as worthless.

  2. WBTC liquidations are broken. In liquidate, token_amount_from_debt_covered = _get_token_amount_from_usd(collateral, debt_to_cover) is 1e10× too large, so _redeem_collateral attempts to subtract an astronomically large amount from user_to_token_address_to_amount_deposited[user][WBTC] and reverts on underflow — a healthy liquidation path cannot execute for WBTC-denominated debt.

Both the account-value accounting and the liquidation seize math are wrong for any collateral whose token decimals differ from 18. The system's advertised WETH/WBTC basket is only half-functional.

Risk

Impact: Medium. One of the two supported collateral assets (WBTC) is mis-priced by ten orders of magnitude: it cannot back minted DSC, and any position involving it cannot be liquidated normally. It does not enable theft or over-minting (WBTC is under-valued, never over-valued), but it breaks core protocol accounting for a first-class asset.

Likelihood: High. Deterministic for every WBTC interaction; triggers the moment anyone uses the second listed collateral token, with no special conditions.

Proof of Concept

# Chainlink WBTC/USD ~ $60,000, feed 8 decimals -> price = 60000e8
# WBTC token has 8 decimals -> 1 WBTC = 1e8 base units
_get_usd_value(WBTC, 1e8)
= ((60000e8 * 1e10) * 1e8) // 1e18
= 6_000_000_000_000 # $0.000006, want $60_000e18 -> under by 1e10
_get_token_amount_from_usd(WBTC, 60_000e18)
= (60_000e18 * 1e18) // (60000e8 * 1e10)
= 1e18 base units # = 1e10 WBTC, want 1e8 (=1 WBTC) -> over by 1e10

Flow: deposit_collateral(WBTC, 1e8) then mint_dsc(1e18) reverts with DSCEngine__BreaksHealthFactor, because the engine values the deposited WBTC at ~$0.000006 even though it is worth $60,000.

Recommended Mitigation

Normalise every collateral amount to 18 decimals using the token's own decimals() before applying the feed math. Store each token's decimals (or scale factor) at construction and use it in both conversion functions, e.g.:

# scale amount from token decimals to 18 in _get_usd_value:
normalized_amount: uint256 = amount * (10 ** (18 - token_decimals[token]))
return ((convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * normalized_amount) // PRECISION
# and de-normalize the result of _get_token_amount_from_usd back to token decimals.

Only after normalising can WETH (18) and WBTC (8) — or any "basket of assets" the README invites forkers to swap in — be valued and liquidated correctly.

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!