Algo Ssstablecoinsss

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

Engine treats WBTC as an 18-decimal token, so real WBTC collateral is undervalued by 10^10 and its liquidations misfire

Root + Impact

The engine values every collateral token as if amounts were 18-decimal base units, with a single fixed price-feed precision and no per-token decimal handling. WETH is 18 decimals, but the real WBTC token (the exact token configured for this system on ZKsync Era in moccasin.toml) has 8 decimals. As a result, 1 WBTC deposited in its real base units (10^8) is credited as 10^-10 of a WBTC: depositors get essentially zero minting power for WBTC, and liquidation amounts computed in 18-decimal terms exceed what WBTC depositors actually deposited, so redeeming collateral during a WBTC liquidation underflows and reverts.

Description

_get_usd_value applies one formula to both collaterals, with no decimal adjustment per token:

# src/dsc_engine.vy
@internal
@view
def _get_usd_value(token: address, amount: uint256) -> uint256:
price_feed: AggregatorV3Interface = AggregatorV3Interface(self.token_address_to_price_feed[token])
...
return (convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount // PRECISION

and _get_token_amount_from_usd inverts the same 18-decimal assumption when liquidate() computes how much collateral to seize. Nothing in deposit_collateral, the constructor, or the deployment configuration scales WBTC's 8-decimal amounts, while moccasin.toml points the ZKsync deployment at the real 8-decimal WBTC contract (0xBBeB516fb02a01611cBBE0453Fe3c580D7281011).

Risk

Likelihood: High - this is not an edge case; it triggers for every genuine WBTC deposit on the configured deployment, because the mismatch is in the unit convention itself.
Impact: Medium - the WBTC leg of the protocol is effectively broken: WBTC depositors receive ~zero borrowing power, and WBTC-backed positions cannot be liquidated correctly (the seize amount is computed 10^10 times too large relative to deposits and the redemption underflows), which can leave bad debt in the system.

Proof of Concept

Executed locally with titanoboa (BTC feed mock at $2,000, matching the repo's own mock setup): the engine values 10^18 units at the full $2,000, but 1 WBTC expressed in its real base units (10^8) is valued at exactly 10^10 times less.

v18 = engine.get_usd_value(wbtc.address, 10**18) # 2000 * 10**18 (full price)
v8 = engine.get_usd_value(wbtc.address, 10**8) # 1 WBTC in satoshis -> 2000 * 10**8
assert v8 * 10**10 == v18 # real WBTC undervalued by a factor of 10**10

Recommended Mitigation

Track each collateral token's decimals and normalize amounts to 18 decimals inside the valuation and seizure math (for WBTC, scale by 10^10), or explicitly require an 18-decimal wrapped BTC token as collateral:

@internal
@view
def _get_usd_value(token: address, amount: uint256) -> uint256:
...
- return (convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount // PRECISION
+ normalized_amount: uint256 = amount * (10 ** convert(18 - self.token_decimals[token], uint256))
+ return (convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * normalized_amount // PRECISION
Updates

Lead Judging Commences

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