Algo Ssstablecoinsss

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

Collateral valuation hard-codes 18-decimal amounts, so real WBTC (8 decimals) is valued 1e10x too low — WBTC collateral cannot back any mint and WBTC liquidations compute a seize amount 1e10x too large and always revert

Summary

_get_usd_value and _get_token_amount_from_usd convert between token amounts and USD using fixed precision constants that assume every collateral amount is denominated in 18 decimals. The protocol's stated collateral set is WETH (18 decimals) and WBTC, which has 8 decimals. For WBTC the USD value of a deposit is understated by a factor of 10^10, and the token amount computed for a given USD debt is overstated by the same factor. The repository's own mocks hide this because both mock collateral tokens are (incorrectly) built with 18 decimals.

Vulnerability Details

In src/dsc_engine.vy:

# _get_usd_value — treats `amount` as an 18-decimal quantity
return ((convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount) // PRECISION
​
# _get_token_amount_from_usd — same fixed scaling in reverse
return (usd_amount_in_wei * PRECISION) // (convert(price, uint256) * ADDITIONAL_FEED_PRECISION)

Neither function ever queries the token's actual decimals(). With a $60,000 BTC price (feed answer 60000 * 10**8):

  • get_usd_value(wbtc, 10**8) (1 WBTC in real units) returns 6,000,000,000,000 instead of 60,000 * 10**18 — the position is counted as $0.000006.

  • get_token_amount_from_usd(wbtc, 60_000 * 10**18) returns 10^18 units — i.e. 10,000,000,000 WBTC in 8-decimal terms — for a position that holds 10^8 units.

Impact

Two concrete failures against the real WBTC token:

  1. WBTC is unusable as borrowing collateral. $60,000 of deposited WBTC has a counted adjusted value of 3 * 1012 wei, so minting even 1 DSC reverts with DSCEngine__BreaksHealthFactor; the maximum mintable amount against 1 WBTC is exactly 3 * 1012 wei of DSC (0.000003 DSC).

  2. WBTC positions can never be liquidated. liquidate computes the seize amount via _get_token_amount_from_usd, obtaining 1018 units for $60,000 of debt coverage, then _redeem_collateral tries to subtract that from a deposited balance denominated in 108-scale units and underflows, reverting the whole liquidation. Any DSC debt that is (also) backed by WBTC cannot be seized through the WBTC leg.

Proof of Concept

Boa/pytest test using an 8-decimal WBTC mock (passes locally):

def test_poc_wbtc_8_decimals_valued_1e10_too_low(dsce_wbtc8, some_user):
engine, wbtc8 = dsce_wbtc8 # engine deployed with an 8-decimal WBTC mock, BTC feed $60,000
one_wbtc = 10**8
assert engine.get_usd_value(wbtc8, one_wbtc) == 6_000_000_000_000 # should be 60_000e18
​
with boa.env.prank(some_user):
wbtc8.mock_mint()
wbtc8.approve(engine, one_wbtc)
engine.deposit_collateral(wbtc8, one_wbtc)
with boa.reverts("DSCEngine__BreaksHealthFactor"):
engine.mint_dsc(to_wei(1, "ether")) # even 1 DSC is unmintable
engine.mint_dsc(3_000_000_000_000) # max possible: 0.000003 DSC
​
def test_poc_wbtc_liquidation_amount_overflows_position(dsce_wbtc8):
engine, wbtc8 = dsce_wbtc8
# $60,000 of debt coverage "equals" 1e18 WBTC units; a 1-WBTC position holds 1e8.
assert engine.get_token_amount_from_usd(wbtc8, to_wei(60_000, "ether")) == 10**18

Recommended Mitigation

Read each collateral token's decimals() (cache it at registration) and normalize amounts to 18 decimals inside _get_usd_value and _get_token_amount_from_usd before applying the feed precision — e.g. scale by 10 ** (18 - token_decimals) on the way in and the inverse on the way out. Add a test with a real 8-decimal WBTC mock; the current 18-decimal mocks mask the bug.

Updates

Lead Judging Commences

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