Algo Ssstablecoinsss

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

# Oracle staleness library never checks `price > 0`, so a zero Chainlink answer values all collateral at \$0 and triggers mass wrongful liquidations

Oracle staleness library never checks price > 0, so a zero Chainlink answer values all collateral at $0 and triggers mass wrongful liquidations

Severity: High · Impact: High · Likelihood: Medium

Description

  • oracle_lib is meant to be the single guard that decides whether a Chainlink answer is usable; if the data is bad it must revert so the engine freezes (per the file's own docstring).

  • It validates the timestamp, round completeness, and a 72-hour timeout, but it never validates that price > 0. A feed returning 0 (or a negative value) with a fresh timestamp passes all checks and is used as a real price.

(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"
@> # no `assert price > 0` — a zero/negative answer is accepted
return (round_id, price, started_at, updated_at, answered_in_round)

Risk

Likelihood:

  • Occurs whenever a Chainlink feed reports 0 with a current timestamp — a documented real-world failure mode (feed misconfiguration, a market/aggregator glitch), which the repo itself simulates via mock_more_debt_dsc crashing the price to 0.

Impact:

  • With price 0, _get_usd_value returns 0 for every position, so all users' health factors collapse below MIN_HEALTH_FACTOR and become liquidatable at once — collateral is seized from users who are genuinely overcollateralized (loss of funds).

  • _get_token_amount_from_usd divides by the zero price and reverts, bricking liquidation and redemption paths (denial of service).

Proof of Concept

Save the block below as tests/poc_h2.py inside the cloned repo and run mox test tests/poc_h2.py. It uses only the contest's own source files and deploys everything inline — no conftest fixtures. A fully healthy position is driven below the minimum health factor purely by a zero price.

import boa
from eth_utils import to_wei
from src import dsc_engine, decentralized_stable_coin
from src.mocks import mock_token, MockV3Aggregator
MIN_HEALTH_FACTOR = to_wei(1, "ether")
def _deploy():
dsc = decentralized_stable_coin.deploy()
weth = mock_token.deploy()
other = mock_token.deploy()
eth_usd = MockV3Aggregator.deploy(8, 2_000 * 10**8) # $2,000, 8 decimals
other_usd = MockV3Aggregator.deploy(8, 2_000 * 10**8)
engine = dsc_engine.deploy(
[weth.address, other.address], [eth_usd.address, other_usd.address], dsc
)
dsc.set_minter(engine.address, True)
dsc.transfer_ownership(engine.address)
return engine, weth, eth_usd
def test_zero_price_accepted_and_zeroes_collateral():
engine, weth, eth_usd = _deploy()
# A user deposits 10 WETH ($20,000) and mints 100 DSC -> very healthy.
user = boa.env.generate_address("user")
with boa.env.prank(user):
weth.mock_mint() # mints 10 WETH to the user
weth.approve(engine.address, to_wei(10, "ether"))
engine.deposit_collateral_and_mint_dsc(
weth.address, to_wei(10, "ether"), to_wei(100, "ether")
)
assert engine.health_factor(user) > MIN_HEALTH_FACTOR
assert engine.get_usd_value(weth.address, to_wei(10, "ether")) == to_wei(20_000, "ether")
# Chainlink returns 0 with a fresh timestamp.
eth_usd.updateAnswer(0)
# BUG: oracle_lib has no `price > 0` check, so 0 is accepted (no revert)...
assert engine.get_usd_value(weth.address, to_wei(10, "ether")) == 0
# ...all collateral now values at $0 and the healthy user is liquidatable...
assert engine.health_factor(user) < MIN_HEALTH_FACTOR
# ...and USD->token conversion divides by zero -> revert (DoS).
with boa.reverts():
engine.get_token_amount_from_usd(weth.address, to_wei(100, "ether"))

Recommended Mitigation

Reject non-positive prices in the staleness check so bad data freezes the protocol (the intended behavior) instead of being used.

assert updated_at != 0, "DSCEngine_StalePrice"
assert answered_in_round >= round_id, "DSCEngine_StalePrice"
+ assert price > 0, "DSCEngine_InvalidPrice"
seconds_since: uint256 = block.timestamp - updated_at
assert seconds_since <= TIMEOUT, "DSCEngine_StalePrice"
Updates

Lead Judging Commences

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