Algo Ssstablecoinsss

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

[H]Allowing users to fork this code and support arbitrary assets means that the decimal value is not necessarily equal to 18.

Root + Impact

Description

  • The user forked this code to support arbitrary assets; it functions normally, including liquidation and settlement.

  • The code assumes a default decimal of 18 and does not normalize for other decimal values.

// Root cause in the codebase with @> marks to highlight the relevant section
//1. _get_token_amount_from_usd
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
) //@> result is in 18-dec units, but returned as collateral token units without denormalization
)
//2._get_usd_value
def _get_usd_value(token: address, amount: uint256) -> uint256:
return (
(convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount
) // PRECISION
//@> raw token amount, no decimal normalization

Risk

Likelihood:

  • The current deployment uses WETH and WBTC, both of which have 18 decimals — the vulnerability is not triggered in the default configuration. However, the codebase is explicitly designed to be forked and extended to support arbitrary collateral assets (as noted in the comment on line 18). Any fork that adds a non-18-decimal token (e.g., USDC/USDT at 6 decimals, WBTC on some chains at 8 decimals) will activate this bug immediately without any additional attacker action. The trigger condition is a routine protocol configuration step, not a sophisticated exploit.


Impact:

  • The severity of the outcome is high in both directions depending on token decimals:

    Decimals < 18 (e.g., USDC = 6): Collateral value is understated by 10^18−d10^18−d. A user depositing $1,000,000 in USDC receives a health factor near zero and cannot mint any DSC. The collateral is effectively locked with no functional use — complete loss of utility for that token.

    Decimals > 18: Collateral value is overstated by 10d−1810d−18. A user can mint DSC far in excess of real collateral value, breaking the overcollateralization invariant and directly threatening DSC's USD peg.

    Both scenarios also corrupt _get_token_amount_from_usd(), meaning liquidation payouts are calculated incorrectly — liquidators receive the wrong token amount, making the liquidation mechanism unreliable.

Proof of Concept

  1. Deposit 1,000 USDC (6-decimal token, real value = $1,000)

  2. get_usd_value() returns 1e9 instead of the correct 1e21 — a 10¹² undervaluation

  3. Root cause: the formula (price * 1e10 * amount_raw) // 1e18 assumes amount_raw is always in 18-decimal units; for a 6-decimal token, amount_raw = 1e6 instead of 1e18, so the result is scaled down by 10^(18-6) = 10^12

  4. Consequence: the user's max mintable DSC is 5×10⁸ wei (~0 DSC) instead of the correct 500 DSC

  5. The final assert max_mintable_reported < 1e18 proves that even minting 1 DSC will revert due to an artificially broken health factor, locking out any user depositing non-18-decimal collateral

def test_poc_non_18_decimal_token(some_user):
from src import decentralized_stable_coin
from src.mocks import mock_token_6dec, mock_token, MockV3Aggregator
# Deploy a 6-decimal token (simulates USDC, price = $1)
usdc_mock = mock_token_6dec.deploy()
usdc_feed = MockV3Aggregator.deploy(8, 1 * 10**8) # $1 per token
# Deploy a normal 18-dec token as the second slot placeholder
dummy_token = mock_token.deploy()
dummy_feed = MockV3Aggregator.deploy(8, 1 * 10**8)
poc_dsc = decentralized_stable_coin.deploy()
poc_dsce = dsc_engine.deploy([usdc_mock, dummy_token], [usdc_feed, dummy_feed], poc_dsc)
poc_dsc.transfer_ownership(poc_dsce)
# Mint and deposit 1,000 USDC (1_000 * 1e6 = 1e9 raw units, real value = $1,000)
deposit_amount = 1_000 * 10**6 # 1,000 USDC in 6-dec raw units
with boa.env.prank(some_user):
usdc_mock.mint_amount(deposit_amount)
usdc_mock.approve(poc_dsce, deposit_amount)
poc_dsce.deposit_collateral(usdc_mock, deposit_amount)
# What the protocol reports vs the correct USD value
reported_usd = poc_dsce.get_usd_value(usdc_mock, deposit_amount)
correct_usd = 1_000 * 10**18 # $1,000 expressed in 18-dec protocol units
# Bug: protocol massively undervalues the collateral
# reported = (1e8 * 1e10 * 1e9) // 1e18 = 1e27 // 1e18 = 1e9
# correct = $1,000 = 1e3 * 1e18 = 1e21
# undervaluation factor = 1e21 / 1e9 = 1e12
assert reported_usd < correct_usd, (
f"Expected undervaluation: reported={reported_usd}, correct={correct_usd}"
)
assert reported_usd == correct_usd // (10 ** 12), (
f"Expected 10^12 undervaluation factor, got reported={reported_usd}"
)
# Impact: user can barely mint any DSC despite depositing $1,000 real value
# max mintable (reported) = 5e8 wei DSC ≈ 0.0000000005 DSC
# max mintable (correct) = 5e20 wei DSC = 500 DSC
max_mintable_reported = reported_usd * 50 // 100
max_mintable_correct = correct_usd * 50 // 100
assert max_mintable_reported == max_mintable_correct // (10 ** 12), (
"Undervaluation propagates to mint limit: user can only mint 1e-12 of the correct DSC amount"
)
# Concretely: trying to mint even 1 DSC (1e18 wei) will revert
amount_to_mint = 10 ** 18 # 1 DSC
assert max_mintable_reported < amount_to_mint, (
f"Exploit confirmed: $1,000 USDC collateral only supports minting {max_mintable_reported} wei DSC, "
f"far below 1 DSC ({amount_to_mint} wei)"
)

Recommended Mitigation

Obtain the precision of the current input token, performing normalization during calculation.

+ interface IERC20Decimals:
+ def decimals() -> uint8: view
def _get_usd_value(token: address, amount: uint256) -> uint256:
- return (
- (convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount
- ) // PRECISION
+ decimals: uint256 = convert(ERC20Detailed(token).decimals(), uint256)
+ normalized: uint256 = amount * (10 ** (18 - decimals))
+ return (convert(price,uint256)* ADDITIONAL_FEED_PRECISION * normalized) // PRECISION
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
- )
- )
+ decimals: uint256 = convert(ERC20Detailed(token).decimals(), uint256)
+ raw: uint256 = (usd_amount * PRECISION) // (convert(price, uint256) * ADDITIONAL_FEED_PRECISION)
+ return raw // (10 ** (18 - decimals))
Updates

Lead Judging Commences

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