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.
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.
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.
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.
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.
Below is the relevant code snippet, with the vulnerable line marked ((!)
):
self._total_supply(parameters, ts)
retrieves the current total_supply
after subtracting unlocked shares.
If total_supply
is 0, the division (... // self._total_supply(...))
reverts immediately.
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()
.
A group of users withdraws all liquidity from scrvUSD
, leaving total_supply
at 0.
The oracle receives a call to update_price()
to refresh its parameters.
The contract internally calls _raw_price()
, which attempts to divide by total_supply
(now 0).
The transaction reverts at this step, preventing any price update.
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
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.
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.
Prevent total_supply
from ever being zero or handle the condition gracefully. A simple fix is to check total_supply
before the division:
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.
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.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.