In ScrvusdOracleV2::_obtain_price_params function the loop for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION) iterates an incorrect number of times specifically MAX_V2_DURATION - number_of_periods instead of the intended number_of_periods resulting in excessive adjustments to total_supply and balance_of_self. This bug distorts the scrvUSD price per share calculated in _raw_price, which undermines the oracle’s precision and reliability. this flaw affects every price calculation in the v2 mode when fewer than MAX_V2_DURATION periods have elapsed, with significant downstream consequences for stableswap-ng pools relying on accurate pricingg
The loop’s syntax, for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION), misinterprets Vyper’s range(start, bound=N) behavior, which iterates from start to N-1. Here, it runs from number_of_periods to MAX_V2_DURATION - 1, executing MAX_V2_DURATION - number_of_periods iterations. The intent, based on the preceding gain * number_of_periods adjustment to total_idle, is to apply exactly number_of_periods iterations to reflect elapsed profit periods. This mismatch over-adjusts total_supply and balance_of_self, skewing the price computation.
MAX_V2_DURATION = 4 * 12 * 4 = 192 (4 years, assuming monthly periods, though period defaults to 1 week).
self.max_v2_duration defaults to 4 * 6 = 24 (6 months with weekly periods) but can be set up to 192 via set_max_v2_duration.
The bug activates whenever number_of_periods < MAX_V2_DURATION, which is the typical case given the default self.max_v2_duration = 24 and MAX_V2_DURATION = 192. For example, if number_of_periods = 10, the loop runs 192 - 10 = 182 times instead of 10.
The Intended Behavioris likely:
number_of_periods represents the number of profit unlock periods elapsed since
last_profit_update, capped by self.max_v2_duration. The function adjusts total_idle by
gain * number_of_periods to account for accrued rewards, and the loop should apply
number_of_periods iterations to update total_supply and balance_of_self consistently, simulating share unlocking over those periods.
While The Actual Behavior is:
The loop runs MAX_V2_DURATION - number_of_periods times:
If number_of_periods = 10, it iterates 182 times.
If number_of_periods = 24 (default cap), it iterates 168 times.
Each iteration reduces total_supply and adjusts balance_of_self using:
vyper
This over-shrinks total_supply, misaligning it with the total_idle adjustment.
Example:
Initial state: total_supply = 1000, balance_of_self = 200, total_idle = 800, total_debt = 0.
number_of_periods = 2, gain = 200 * 800 // 1000 = 160, MAX_V2_DURATION = 192.
Intended:
total_idle += 160 * 2 = 320 → total_idle = 1120.
Loop runs 2 times:
Iteration 1: new_balance_of_self = 200 * (1000 - 200) // 1000 = 160, total_supply -= 200 * 200 // 1000 = 40 → total_supply = 960, balance_of_self = 160.
Iteration 2: new_balance_of_self = 160 * (960 - 160) // 960 = 133, total_supply -= 160 * 160 // 960 = 26 → total_supply = 934, balance_of_self = 133.
Price = (1120 + 0) * 10^18 // 934 ≈ 1.199 * 10^18.
Actual:
total_idle = 1120 (correct).
Loop runs 192 - 2 = 190 times, reducing total_supply excessively (e.g., after many iterations, total_supply approaches a much lower value, say ~500 after convergence).
Price = 1120 * 10^18 // 500 ≈ 2.24 * 10^18 (over 2x the intended price).
This demonstrates significant price inflation due to an over-reduced denominator.
The loop’s over-iteration shrinks total_supply and balance_of_self far beyond the intended number_of_periods, creating a disconnect with the total_idle increase.
_total_assets = total_idle + total_debt reflects gain * number_of_periods.
_total_supplyis reduced excessively, inflating the price (assets / supply).
Impacts price_v1 and price_v2, which rely on _raw_price, skewing all v2-mode price queries and updates.
oracle’s v2 mode aims to replicate scrvUSD prices with “no losses due to approximation” by assuming equal rewards over number_of_periods. Over-iteration violates this, producing prices that deviate significantly from reality
In fact Stableswap-ng pools (e.g., USDC/scrvUSD) overvalue scrvUSD, leading to:
Arbitrage losses for liquidity providers as external traders exploit the inflated price.
Pool imbalances as the peg drifts, undermining stability.
Correct the loop to iterate exactly number_of_periods times
Invalid, `bound` here has a different meaning from Python's `range(a, b)`. It is a bound of maximum iterations, meaning the loop will only go to the bounded `MAX_V2_DURATION` when `number_of_periods >= MAX_V2_DURATION`
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.