Summary
The _get_token_amount_from_usd function in dsc_engine.vy performs division by the oracle price without checking if the price is zero. If the Chainlink price feed returns 0 (due to feed failure or manipulation), the division causes a revert, making the liquidate function unusable and permanently freezing the protocol.
Description
@internal
@view
def _get_token_amount_from_usd(
token: address, usd_amount_in_wei: uint256
) -> uint256:
# ... oracle call ...
return (
(usd_amount_in_wei * PRECISION)
convert(price, uint256) * ADDITIONAL_FEED_PRECISION
)
)
The _stale_check_latest_round_data function checks for stale prices but doesn't validate that price > 0. When price = 0:
-
convert(price, uint256) * ADDITIONAL_FEED_PRECISION = 0
-
Division by zero causes revert
-
liquidate() becomes permanently unusable
-
Users with unhealthy positions cannot be liquidated
-
Protocol becomes insolvent
Risk
Severity: Medium
Likelihood: Low
Impact: High
If oracle price becomes 0:
-
All liquidations fail permanently
-
Protocol cannot recover unhealthy positions
-
Total protocol insolvency
-
Complete loss of user funds
Proof of Concept
import boa
from eth_utils import to_wei
def test_division_by_zero_dos(dsce, weth, dsc, some_user, liquidator, eth_usd):
with boa.env.prank(some_user):
weth.approve(dsce.address, to_wei(10, "ether"))
dsce.deposit_collateral_and_mint_dsc(weth.address, to_wei(10, "ether"), to_wei(190, "ether"))
eth_usd.updateAnswer(18 * 10**8)
eth_usd.updateAnswer(0)
with boa.env.prank(liquidator):
weth.approve(dsce.address, to_wei(1, "ether"))
dsce.deposit_collateral(weth.address, to_wei(1, "ether"))
dsc.approve(dsce.address, to_wei(190, "ether"))
with boa.reverts():
dsce.liquidate(weth.address, some_user, to_wei(190, "ether"))
print("✅ DIVISION BY ZERO DoS CONFIRMED")
print("Protocol is permanently frozen")
Run: mox test -k test_division_by_zero_dos
Recommended Mitigation
Add zero price validation in _stale_check_latest_round_data:
@internal
@view
def _stale_check_latest_round_data(
price_price_address: address,
) -> (uint80, int256, uint256, uint256, uint80):
# ... existing code ...
(
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"
assert price > 0, "DSCEngine_InvalidPrice" # Add this check
seconds_since: uint256 = block.timestamp - updated_at
assert seconds_since <= TIMEOUT, "DSCEngine_StalePrice"
return (round_id, price, started_at, updated_at, answered_in_round)
This ensures the protocol reverts early with a clear error message when price is invalid, preventing division by zero.