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_sel
f 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_supply
is 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.