DeFiLayer 1Layer 2
14,723 OP
View results
Submission Details
Severity: high
Invalid

Division-by-Zero in _raw_price() ScrvusdOracleV2

Summary

ScrvusdOracleV2 contains a logical error triggered when total_supply becomes zero during execution. The division in _raw_price() attempts to calculate the price per share using (... // total_supply), which reverts if total_supply is 0. Once this error occurs, any function relying on _raw_price() (including update_price(), price_vX(), etc.) becomes unusable. This not only prevents further price updates but also disrupts the core oracle functionality, potentially leaving the system incapable of accurately reflecting asset prices.

Vulnerability Details

The ScrvusdOracleV2 contract fails to handle situations where total_supply is zero, resulting in a division-by-zero error. This flaw compromises the oracle’s core security guarantee—its ability to continuously report and update the price of scrvUSD. When total_supply reaches zero, any call to functions that use _raw_price() (including update_price(), price_vX(), etc.) will revert, effectively disabling all price-based functionality.

A malicious or erroneous data submission (e.g., state proofs or parameters incorrectly indicating zero total_supply) could trigger this condition. Because the contract does not validate total_supply > 0 before division, once total_supply is set to zero—whether through normal usage (e.g., mass liquidation or withdrawal) or via crafted input parameters—any price computation will fail. As a result, the oracle becomes unusable and no longer reflects accurate asset values in the pool or other dependent systems.

Impact

This issue can completely halt the oracle's core functionality. Once total_supply is zero, any price-related calls revert, preventing the contract from updating or reporting new prices. As the oracle underpins critical operations (e.g., stableswaps, redemptions, cross-chain bridging), this failure can freeze important protocol flows and potentially allow inaccurate pricing to persist. The widespread disruption to price feeds represents a major risk, justifying a high impact rating for this vulnerability.

Likelihood Explanation

While total_supply reaching zero may seem unlikely under normal conditions—given that it would require either mass withdrawals or a deliberately crafted update—there are realistic scenarios where it could happen. A large-scale redemption of liquidity or erroneous/malicious state updates (e.g., from a buggy off-chain prover) can push total_supply to zero. Once the value is set to zero, the division-by-zero revert is guaranteed on any subsequent price computation. Consequently, although the scenario is not routine, it is sufficiently plausible to warrant concern.

Proof of Concept

If the total_supply variable ever becomes 0 (for instance, after all shares are unlocked and deducted from total_supply), the division in _raw_price() triggers a divide-by-zero, causing the transaction to revert. Consequently, no price updates or queries are possible, rendering the oracle function inoperable under this condition.

Code Analysis

Below is the relevant code snippet, with the vulnerable line marked ((!)):

def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
parameters: PriceParams = self._obtain_price_params(parameters_ts)
# (!) Division using _total_supply(...) that can be zero
return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)
  1. self._total_supply(parameters, ts) retrieves the current total_supply after subtracting unlocked shares.

  2. If total_supply is 0, the division (... // self._total_supply(...)) reverts immediately.

Explanation

Once total_supply drops to 0, the division by zero prevents _raw_price() from completing. This in turn breaks any function relying on _raw_price(), such as update_price(), price_v0(), price_v1(), and price_v2().

Vulnerable Scenario

  1. A group of users withdraws all liquidity from scrvUSD, leaving total_supply at 0.

  2. The oracle receives a call to update_price() to refresh its parameters.

  3. The contract internally calls _raw_price(), which attempts to divide by total_supply (now 0).

  4. The transaction reverts at this step, preventing any price update.

Test and Result

The test intentionally arranges parameters to force a division by zero in _raw_price() by driving total_supply down to zero. Setting profit_max_unlock_time to 1 second ensures a large number of periods, and a higher balance_of_self relative to total_supply speeds up its depletion. The with boa.reverts(): block confirms that this division by zero triggers a revert as expected, and the test passing (1 passed) validates that the contract correctly reverts under these conditions.

  • Add the following test to tests/scrvusd/oracle/unitary/test_v2.py

def test_division_by_zero_when_total_supply_is_zero_in_extended_flow(soracle, verifier):
"""
Forces a scenario where 'total_supply' becomes zero during internal calculations,
causing a division-by-zero revert in '_raw_price()' when calling 'update_price()'.
"""
# Set 'profit_max_unlock_time' to 1 second to accumulate multiple periods
next_block = soracle.last_block_number() + 1
with boa.env.prank(verifier):
soracle.update_profit_max_unlock_time(1, next_block)
# Place 'last_profit_update' far in the past to enforce several periods
current_ts = boa.env.evm.patch.timestamp
old_ts = current_ts - 1000 # simulates 1000s elapsed since the last update
# Define parameters that drive 'total_supply' down to zero:
# - total_supply starts at 10
# - balance_of_self is set to 40, large enough to rapidly diminish total_supply each iteration
# - last_profit_update is old_ts to trigger many periods in the calculation
price_params_forced = [
0, # total_debt
0, # total_idle
10, # total_supply
0, # full_profit_unlock_date
0, # profit_unlocking_rate
old_ts,
40, # balance_of_self
]
# Call 'update_price', which invokes '_raw_price()' and triggers a division by zero
next_block += 1 # increment one block to preserve linearity
with boa.env.prank(verifier):
with boa.reverts():
soracle.update_price(price_params_forced, current_ts, next_block)
============== test session starts ===============
tests\scrvusd\oracle\unitary\test_v2.py . [100%]
=============== 1 passed in 0.26s ================

Confirmation

This sequence confirms the divide-by-zero vulnerability:

  • total_supply = 0

  • The code attempts division by total_supply

  • The transaction reverts

  • All functions that rely on _raw_price() become unusable, and the oracle functionality is effectively lost.

Tools Used

Manual Code Review
A systematic, line-by-line inspection of the contract code was performed to identify logical discrepancies. This thorough analysis confirmed the zero total_supply condition through targeted tests, ensuring the vulnerability was reproducible and verifiable.

Recommendations

Prevent total_supply from ever being zero or handle the condition gracefully. A simple fix is to check total_supply before the division:

- return self._total_assets(params) * 10**18 // self._total_supply(params, ts)
+ total_supply_current = self._total_supply(params, ts)
+ if total_supply_current == 0:
+ return 0 # or revert with a descriptive error
+ return self._total_assets(params) * 10**18 // total_supply_current

This ensures the function either returns a safe fallback value or explicitly reverts with a meaningful message, preserving the contract’s reliability and preventing unexpected revert scenarios.

Updates

Lead Judging Commences

0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

[invalid] finding-division-by-zero

Note that `total_supply` and `profit_unlocking_rate` is initially set to 1 and 0 respectively when the `ScrvusdOracleV2.vy` is deployed 1. `total_supply` and `profit_unlocking_rate` is part of the price param updates within `update_price`, which must have gone through verification via the OOS `StateProofVerifier` contract, so there is no evidence that a 0 supply is allowed either via a 0 supply update or an extremely high `profit_unlocking_rate`. 2. Since price is retrieved via values retrived from the V3Vault, if there is no supply, there is arguably no price to be posted. As such, reverting is arguably the correct choice since a 0 price value is not expected from scrvUSD, which is a stable coin.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.