Algo Ssstablecoinsss

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

Missing Positive Price Validation Allows Chainlink Zero/Negative Price to Wrap to Maximum uint256 — Unlimited DSC Minting

Missing Positive Price Validation Allows Chainlink Zero/Negative Price to Wrap to Maximum uint256 — Unlimited DSC Minting

Scope

  • src/oracle_lib.vy

  • src/dsc_engine.vy

Description

The _stale_check_latest_round_data() function in oracle_lib.vy validates price staleness but does not validate that the returned price value from latestRoundData() is greater than zero. The price field is typed as int256, and Chainlink can return 0 or negative values during circuit breaker events or oracle failures.

The unchecked price value is returned to dsc_engine.vy and passed directly to convert(price, uint256). In Vyper 0.4.0, this conversion reinterprets the two's-complement bit pattern: a value of -1 (int256) becomes 2^256 - 1 (uint256) ≈ 1.16×10^77. This causes the collateral USD value calculation to return an astronomically large number, bypassing the health factor check and allowing any user to mint unlimited amounts of DSC against any collateral.

# oracle_lib.vy: No price > 0 validation
def _stale_check_latest_round_data(price_feed: AggregatorV3Interface) -> (uint80, int256, uint256, uint256, uint80):
(round_id, price, started_at, updated_at, answered_in_round) = staticcall price_feed.latestRoundData()
@> # price is int256 — can be 0 or negative during oracle failures. Not validated.
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
# dsc_engine.vy: Unsafe int256 → uint256 conversion
def _get_usd_value(token: address, amount: uint256) -> uint256:
(round_id, price, started_at, updated_at, answered_in_round) = oracle_lib._stale_check_latest_round_data(price_feed)
@> return ((convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount) // PRECISION
# If price = -1 → convert(-1, uint256) = 2^256-1 → USD value = astronomical → unlimited DSC minting

Risk

Likelihood: Low

  • Chainlink returning zero or negative prices requires an oracle circuit breaker event or critical infrastructure failure — unlikely under normal conditions.

  • On ZKsync Era, oracle infrastructure is newer and less battle-tested than on Ethereum mainnet.

Impact: High

  • A price of -1 (int256) converts to ~1.16×10^77 USD value per token.

  • The health factor becomes effectively infinite — the attacker mints the protocol's entire DSC supply with 1 wei of collateral.

  • Protocol becomes instantly insolvent.

Severity: Medium

Proof of Concept

When the Chainlink ETH/USD oracle enters a circuit breaker state and returns a price of 0 or negative, the unsafe conversion allows an attacker to mint unlimited DSC. The attack requires only 1 wei of WETH as collateral.

# titanoboa PoC: simulate Chainlink returning price = -1
import boa
from eth_utils import to_wei
# Mock oracle returns negative price (circuit breaker simulation)
eth_usd.updateAnswer(-1) # int256(-1)
# At this point, oracle_lib accepts the price (passes staleness checks)
# convert(-1, uint256) = 2^256 - 1 ≈ 1.16e77
with boa.env.prank(attacker):
weth.approve(dsce, 1)
dsce.deposit_collateral(weth, 1) # 1 wei WETH
# _get_usd_value(weth, 1) = (2^256-1 * 1e10 * 1) // 1e18 = 1.16e69 USD
# health_factor = 1.16e69 // 2 / 0 → if no DSC minted yet, health = max_uint
dsce.mint_dsc(to_wei(1_000_000, "ether")) # Mint $1M DSC with 1 wei WETH — SUCCEEDS

Recommended Mitigation

Add an explicit positive price check in oracle_lib.vy before returning the price. This is the standard defensive pattern recommended by Chainlink documentation.

(round_id, price, started_at, updated_at, answered_in_round) = staticcall price_feed.latestRoundData()
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 7 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!