Algo Ssstablecoinsss

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

oracle_lib never validates price > 0 — a zero Chainlink answer passes the stale check, values all collateral at zero, and makes healthy positions instantly liquidatable (or bricks redemptions via divide-by-zero)

Description

oracle_lib is the protocol's single line of defence against bad Chainlink data — its whole stated purpose is "used to check the Chainlink Oracle for stale data ... If a price is stale, functions will revert." It validates staleness but never validates that the returned price is positive:

@internal
@view
def _stale_check_latest_round_data(price_price_address: address) -> (uint80, int256, uint256, uint256, uint80):
...
(round_id, price, started_at, updated_at, answered_in_round) = staticcall price_price.latestRoundData()
assert updated_at != 0, "DSCEngine_StalePrice"
assert answered_in_round >= round_id, "DSCEngine_StalePrice"
seconds_since: uint256 = block.timestamp - updated_at
assert seconds_since <= TIMEOUT, "DSCEngine_StalePrice"
return (round_id, price, started_at, updated_at, answered_in_round)

latestRoundData() returns price as an int256. Chainlink aggregators can, under fault conditions (a broken feed, a mis-configured aggregator, or a min/max-answer floor being hit), return price == 0. There is no assert price > 0 here, so a zero answer flows straight into the engine.

Downstream in dsc_engine.vy, both consumers convert the price to uint256 and use it directly:

# _get_usd_value:
return ((convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount) // PRECISION
# _get_token_amount_from_usd:
return (usd_amount_in_wei * PRECISION) // (convert(price, uint256) * ADDITIONAL_FEED_PRECISION)

With price == 0:

  • convert(0, uint256) is 0 (no revert), so _get_usd_value returns 0 for every collateral token. Every user's _get_account_collateral_value becomes 0, their health factor collapses to 0, and liquidate's starting_user_health_factor < MIN_HEALTH_FACTOR gate passes for everyone — perfectly healthy positions become liquidatable at a zero valuation, letting a liquidator seize collateral for near-nothing.

  • _get_token_amount_from_usd divides by (0 * 1e10) == 0 and reverts, so redemptions and liquidations that route through it are simultaneously bricked.

(A negative price is caught only incidentally: Vyper 0.4.0's convert(price, uint256) reverts on a negative int256, which is itself an uncontrolled revert / DoS rather than a clean handled error.)

The one guard the library exists to provide — rejecting an invalid price — is missing for the most basic invalid value.

Risk

Impact: Medium. A zero feed answer is silently accepted as a real price. It either values all collateral at zero (enabling unjust liquidation of healthy users and mispriced seizes) or reverts core flows, depending on which path is hit. The library's stated safety guarantee does not hold.

Likelihood: Low. Requires the Chainlink feed to actually return a non-positive answer, which is uncommon but is exactly the fault condition this library was written to defend against.

Proof of Concept

# Feed misbehaves and returns a fresh round with price = 0
latestRoundData() -> (round_id=101, price=0, started_at=t, updated_at=block.timestamp, answered_in_round=101)
_stale_check_latest_round_data(feed):
updated_at != 0 # passes
answered_in_round >= round_id # passes (101 >= 101)
seconds_since <= TIMEOUT # passes (fresh)
-> returns price = 0 (NO price>0 assert)
# For a victim holding $50k WETH collateral, 20k DSC minted:
_get_usd_value(WETH, amount) -> 0
_health_factor(victim) -> 0 (< MIN_HEALTH_FACTOR)
liquidate(WETH, victim, debt) -> starting HF 0 < 1e18 passes -> victim liquidated on a zero price

Expected: a zero/negative price is rejected and the engine freezes safely (the intended "revert on bad data" behaviour). Actual: a zero price is treated as valid and prices all collateral at zero.

Recommended Mitigation

Add a positive-price assertion in _stale_check_latest_round_data, and prefer the feed heartbeat over the deprecated answeredInRound field:

assert price > 0, "DSCEngine_StalePrice"
assert updated_at != 0, "DSCEngine_StalePrice"
seconds_since: uint256 = block.timestamp - updated_at
assert seconds_since <= TIMEOUT, "DSCEngine_StalePrice"

With price > 0 enforced, a zero (or negative) answer reverts as the library intends, freezing the engine instead of valuing collateral at zero.

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!