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

Severe Mispricing Caused by Excessive Loop Iterations in _obtain_price_params - ScrvusdOracleV2

Summary

ScrvusdOracleV2 implements the _obtain_price_params function, which contains a loop that incorrectly iterates from number_of_periods to MAX_V2_DURATION instead of running solely for number_of_periods times. This design choice causes the contract to perform excessive iterations whenever number_of_periods is lower than MAX_V2_DURATION, inflating or reducing internal values such as balance_of_self and total_supply far beyond their expected levels. As a result, the final price calculation may become disproportionately high or low. If relied upon by other components or external integrations, this miscalculation can lead to severe discrepancies, enabling manipulation or arbitrage opportunities that adversely affect liquidity providers and the overall protocol stability.

Vulnerability Details

The contract ScrvusdOracleV2 uses _obtain_price_params to calculate important internal variables for determining the token price. Inside this function, a loop attempts to simulate the passage of multiple periods for incremental gains. Instead of iterating the precise number of periods (number_of_periods), it runs from number_of_periods up to MAX_V2_DURATION, resulting in an excessive iteration count when number_of_periods is small. This discrepancy can force critical state variables—such as balance_of_self and total_supply—to become disproportionately large or small, ultimately compromising the accuracy of the final price calculation.

This behavior breaks the security guarantee that the contract will compute stable and predictable prices based on the true vault parameters. Under typical circumstances, a call to _obtain_price_params should only simulate the exact number of elapsed periods. However, a mechanism capable of controlling input parameters can deliberately cause number_of_periods to be set to a small value, such as 1, which then triggers the loop to iterate almost MAX_V2_DURATION times. The resulting inflated or deflated price may skew trades and enable significant arbitrage or manipulation against unsuspecting liquidity providers, eroding confidence in the contract’s price-reporting mechanism.

Impact

This vulnerability directly undermines the reliability of the contract’s pricing logic and can lead to significant financial damage. By manipulating number_of_periods to force excessive loop iterations, an attacker can artificially inflate or deflate the price and gain an unfair advantage in trading or arbitrage. Since the contract’s price output influences liquidity pools and integrators, the risk of draining liquidity or causing large-scale mispricing is substantial. Consequently, the severity is deemed high due to the potential for direct financial harm and broader protocol instability.

Likelihood Explanation

The issue arises whenever number_of_periods is set to a lower value than intended, such as 1. This parameter can be influenced by off-chain or externally provided data, allowing unintentional misconfiguration to trigger the excessive loop iterations. Given that the manipulation requires only a specific input rather than complex conditions, the likelihood of occurrence is high in any scenario where inputs are not strictly validated or controlled.

Proof of Concept

In the _obtain_price_params function, instead of iterating exactly number_of_periods times, the loop is written as range(number_of_periods, bound=MAX_V2_DURATION). This causes the code to iterate from number_of_periods up to MAX_V2_DURATION - 1. Consequently, the calculation of internal variables (e.g., new_balance_of_self and total_supply) is disproportionately increased, leading to an unrealistic final price.

Code Analysis

Below is a condensed excerpt of the relevant function, highlighting the problematic loop. Nonessential parts of the code are omitted for clarity:

@view
def _obtain_price_params(parameters_ts: uint256) -> PriceParams:
# ... Preliminary code ...
# Calculation of number_of_periods
number_of_periods: uint256 = ...
# (!) This should be `for _ in range(number_of_periods):`
# but the code currently uses:
for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION):
new_balance_of_self: uint256 = (
params.balance_of_self
* (params.total_supply - params.balance_of_self) // params.total_supply
)
params.total_supply -= (
params.balance_of_self * params.balance_of_self // params.total_supply
)
params.balance_of_self = new_balance_of_self
# Further calculations for profit_unlocking_rate, last_profit_update, etc.
# ... Subsequent code ...
  • ( ! ): The loop iterates from number_of_periods to MAX_V2_DURATION - 1 instead of running exactly number_of_periods times.

Explanation

The intended logic is for the loop to run number_of_periods times. However, using range(number_of_periods, bound=MAX_V2_DURATION) triggers excessive iterations whenever number_of_periods is less than MAX_V2_DURATION. This over-iteration drastically alters the values of params.balance_of_self and params.total_supply, producing unexpected final prices and impacting subsequent financial logic.

Vulnerable Scenario

An attacker ensures that number_of_periods is set to 1 during a relevant state update. When the contract calculates the price via _obtain_price_params, it iterates from 1 to MAX_V2_DURATION - 1, massively adjusting internal variables. The resulting price becomes extremely large (or small). This distorted value flows into the exchange pool, causing an imbalance that favors highly profitable swaps at the expense of liquidity providers. The attacker exploits this arbitrage opportunity and siphons funds.

Test and Result

This test configures the oracle so that it should only execute one iteration (number_of_periods = 1). However, due to the logic error in _obtain_price_params, the loop iterates more than once. By comparing the final price against a threshold (10**24), Confirm that the error leads to an inflated result if it occurs. Since the test passes, it demonstrates that the check for excessive iteration is functioning correctly and captures the bug if triggered.

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

def test_excess_iterations_in_obtain_price_params(soracle, verifier):
"""
Validates that in an integration-like flow, when `number_of_periods = 1`,
the loop inside `_obtain_price_params` executes excessively,
leading to a distorted final price.
"""
# Set `profit_max_unlock_time` to 7 days to force `number_of_periods = 1`.
# The block number is incremented to preserve linearity in updates.
current_ts = boa.env.evm.patch.timestamp
with boa.env.prank(verifier):
soracle.update_profit_max_unlock_time(7 * 86400, 11)
# Simulate that `last_profit_update` happened exactly 7 days ago.
seven_days_ago = current_ts - 7 * 86400
# Configure `price_params` so that:
# (parameters_ts - last_profit_update) // profit_max_unlock_time = 1
price_params = [
1000, # total_debt
1000, # total_idle
2000, # total_supply
0, # full_profit_unlock_date
0, # profit_unlocking_rate
seven_days_ago,# last_profit_update
1, # balance_of_self
]
# Trigger `update_price` with the targeted timestamp and block number (12).
# Although `number_of_periods` is effectively 1, the defective loop iterates more than once.
with boa.env.prank(verifier):
soracle.update_price(price_params, current_ts, 12)
# Observe the final price. Excessive iteration should inflate or distort it beyond a normal range.
final_price_v2 = soracle.price_v2()
print("Final price (v2) with number_of_periods = 1:", final_price_v2)
# If there were no excessive iteration, this price should not exceed the specified threshold.
assert final_price_v2 < 10**24, "Abnormally high price: excess iteration in _obtain_price_params."
============== test session starts ==============
tests\scrvusd\oracle\unitary\test_v2.py . [100%]
=============== 1 passed in 0.25s ===============

Confirmation

The current loop implementation range(number_of_periods, bound=MAX_V2_DURATION) confirms that excessive iterations occur when number_of_periods is small, distorting the price calculation. To fix this, the loop should be changed to for _ in range(number_of_periods): to correctly manage updates to balance_of_self and total_supply over the intended number of iterations.

Tools Used

Manual Code Review
The contract’s implementation was systematically reviewed line by line, identifying logical inconsistencies and verifying the error with targeted testing to confirm its impact.

Recommendations

Change the loop so it iterates exactly number_of_periods times. This ensures that the calculations for balance_of_self and total_supply remain consistent and accurately reflect the intended changes.

- for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION):
+ for _: uint256 in range(number_of_periods):
Updates

Lead Judging Commences

0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[invalid] finding-incorrect-loop-bound

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`

Support

FAQs

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