Algo Ssstablecoinsss

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

get_usd_value assumes 18-decimal collateral; WBTC on ZKsync is 8 decimals

get_usd_value assumes 18-decimal collateral; WBTC on ZKsync is 8 decimals

Severity

High

Description

In src/dsc_engine.vy, collateral is valued in USD with:

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

ADDITIONAL_FEED_PRECISION is 1e10 and PRECISION is 1e18. This is correct only when amount has 18 decimals (WETH).

The same pattern is used in _get_token_amount_from_usd. The engine never reads token decimals(), and FEED_PRECISION is unused.

The contest targets ZKsync Era with WETH and WBTC. On that chain, WETH is 18 decimals and WBTC is 8 decimals. One WBTC (amount = 1e8) is valued about 1e10 times too low compared to the intended USD price.

_get_token_amount_from_usd is inverted the same way, so liquidation and redeem sizing for WBTC are also wrong.

Risk

Impact: High. WBTC deposits are almost worthless in accounting. Users cannot mint a fair amount of DSC against WBTC. Health factor, mint limits, and liquidation amounts for WBTC are incorrect. Any non-18-decimal collateral in a fork breaks the same way.

Likelihood: High on the documented WETH + WBTC / ZKsync path.

Proof of Concept

Engine math (same as _get_usd_value):

PRECISION = 10**18
ADDITIONAL_FEED_PRECISION = 10**10
def get_usd_value(price_8dec: int, amount: int) -> int:
return (price_8dec * ADDITIONAL_FEED_PRECISION * amount) // PRECISION
# 1 WETH @ $2000 (price = 2000e8, amount = 1e18)
print(get_usd_value(2000 * 10**8, 10**18) / 1e18)
# 2000.0 (correct)
# 1 WBTC @ $60000 (price = 60000e8, amount = 1e8, real WBTC decimals)
print(get_usd_value(60000 * 10**8, 10**8) / 1e18)
# 6e-06 (expected 60000)
# undervalue factor = 10_000_000_000

At health factor 1, max DSC is about half of reported collateral USD, so roughly 3e-6 DSC per 1 WBTC instead of about 30000 DSC.

Live decimals on ZKsync (addresses from moccasin.toml):

cast call 0xBBeB516fb02a01611cBBE0453Fe3c580D7281011 "decimals()(uint8)" \
--rpc-url https://mainnet.era.zksync.io
# returns 8 (WBTC)
cast call 0xf00DAD97284D0c6F06dc4Db3c32454D4292c6813 "decimals()(uint8)" \
--rpc-url https://mainnet.era.zksync.io
# returns 18 (WETH)

Recommended Mitigation

Normalize each collateral amount to 18 decimals before the price formula, and reverse that scale in _get_token_amount_from_usd. Store decimals at init or read IERC20Detailed(token).decimals().

# Example for _get_usd_value when token_decimals <= 18
amount_wads: uint256 = amount * 10**(18 - token_decimals)
return (
(convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount_wads
) // PRECISION
# Example for _get_token_amount_from_usd (return token native units)
amount_wads: uint256 = (
(usd_amount_in_wei * PRECISION)
// (convert(price, uint256) * ADDITIONAL_FEED_PRECISION)
)
return amount_wads // 10**(18 - token_decimals)

Add unit tests with 8-decimal collateral (WBTC shape), not only 18-decimal mocks.

Updates

Lead Judging Commences

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