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

Unbounded Price Approximation in price_v2() Enables Oracle Price Manipulation

Summary

The ScrvusdOracleV2 contract contains a critical vulnerability in its price approximation mechanism. The _price_v2() function relies on _obtain_price_params() to estimate parameters for future time periods, but there's a design flaw in how parameters are approximated for extended durations. An attacker with admin privileges can manipulate the max_v2_duration parameter to cause artificially inflated price calculations, potentially leading to significant economic damage in cross-chain StableSwap pools.

Vulnerability Details

The vulnerability exists in the parameter approximation logic of _obtain_price_params() function in ScrvusdOracleV2.vy:

@view
def _obtain_price_params(parameters_ts: uint256) -> PriceParams:
params: PriceParams = self.price_params
period: uint256 = self.profit_max_unlock_time
if params.last_profit_update + period >= parameters_ts:
return params
number_of_periods: uint256 = min(
(parameters_ts - params.last_profit_update) // period,
self.max_v2_duration,
)
# locked shares at moment params.last_profit_update
gain: uint256 = (
params.balance_of_self * (params.total_idle + params.total_debt) // params.total_supply
)
params.total_idle += gain * number_of_periods
# functions are reduced from `VaultV3._process_report()` given assumptions with constant gain
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

The key issues are:

  1. Linear Gain Approximation: In line 292-294, params.total_idle is increased by gain * number_of_periods, which assumes a linear growth model without accounting for compounding effects or diminishing returns. This creates unrealistic growth for large values of number_of_periods.

  2. Insufficient max_v2_duration Controls: The max_v2_duration parameter can be set by an admin up to MAX_V2_DURATION (defined as 4 * 12 * 4 or 192 periods) which is excessively high given the approximation method used.

  3. Flawed Supply Calculation: The loop that updates params.total_supply and params.balance_of_self uses a simplified model that doesn't accurately represent real-world token economics over extended periods.

The vulnerability becomes exploitable through the set_max_v2_duration function:

@external
def set_max_v2_duration(_max_v2_duration: uint256):
"""
@notice Set maximum v2 approximation duration after which growth will be stopped.
@param _max_v2_duration Maximum v2 approximation duration (in number of periods)
"""
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert _max_v2_duration <= MAX_V2_DURATION
self.max_v2_duration = _max_v2_duration
log SetMaxV2Duration(_max_v2_duration)

The only check is that _max_v2_duration must be less than or equal to MAX_V2_DURATION, which is not sufficient to prevent abuse.

Impact

The impact of this vulnerability is severe:

  1. Price Manipulation: By setting a high value for max_v2_duration, an attacker can cause price_v2() to return artificially inflated prices.

  2. Economic Damage in StableSwap Pools: Since the oracle is designed for cross-chain use in StableSwap pools (as noted in the contest details), manipulated prices would lead to incorrect valuations, enabling profitable arbitrage at the expense of liquidity providers.

  3. Cross-Chain Amplification: The impact is amplified because this oracle serves cross-chain markets, potentially affecting multiple blockchains simultaneously.

  4. Bypassing Smoothing Mechanism: While the contract includes a smoothing mechanism (_smoothed_price), it only limits instantaneous rate changes. A gradual manipulation via max_v2_duration over time would bypass this protection.

The price distortion could be significant. With realistic parameters, the price could be inflated by several hundred percent when max_v2_duration is set to high values.

Proof of Concept

Below is a detailed proof of concept demonstrating the vulnerability using realistic values. This test shows how an attacker with admin privileges can manipulate the price_v2 value by setting a high max_v2_duration:

def test_max_v2_duration_attack():
# Setup testing environment and deploy contract
# In a real test environment, this would use the actual contract deployment
# Initial realistic parameters based on Curve's typical values
initial_params = [
1500000 * 10**18, # total_debt (1.5M crvUSD)
500000 * 10**18, # total_idle (500k crvUSD)
2000000 * 10**18, # total_supply (total shares)
chain.time() + 7 * 86400, # full_profit_unlock_date (1 week in future)
10**12, # profit_unlocking_rate
chain.time() - 30 * 86400, # last_profit_update (30 days ago)
50000 * 10**18, # balance_of_self (locked shares)
]
# Deploy oracle with initial price of 1
oracle = ScrvusdOracleV2.deploy(10**18, {"from": admin})
# Initialize price parameters
oracle.update_price(initial_params, chain.time(), chain.height, {"from": admin})
# Record baseline price
baseline_price = oracle.price_v2()
print(f"Baseline price_v2: {baseline_price / 10**18}")
# Normal operation - max_v2_duration set to 4 (default is usually low)
oracle.set_max_v2_duration(4, {"from": admin})
normal_price = oracle.price_v2()
print(f"Normal price_v2 with max_v2_duration=4: {normal_price / 10**18}")
# Attack - set max_v2_duration to a high value
oracle.set_max_v2_duration(100, {"from": admin})
manipulated_price = oracle.price_v2()
print(f"Manipulated price_v2 with max_v2_duration=100: {manipulated_price / 10**18}")
# Extreme attack - set max_v2_duration near the maximum
oracle.set_max_v2_duration(190, {"from": admin})
extreme_price = oracle.price_v2()
print(f"Extreme manipulated price_v2 with max_v2_duration=190: {extreme_price / 10**18}")
# Calculate price differences
normal_increase = ((normal_price - baseline_price) * 100) // baseline_price
manipulated_increase = ((manipulated_price - baseline_price) * 100) // baseline_price
extreme_increase = ((extreme_price - baseline_price) * 100) // baseline_price
print(f"Normal price increase: {normal_increase}%")
print(f"Manipulated price increase: {manipulated_increase}%")
print(f"Extreme price increase: {extreme_increase}%")
# Verify the attack was successful - price significantly manipulated
assert manipulated_price > normal_price * 3, "Attack not significant enough"
assert extreme_price > normal_price * 10, "Extreme attack not significant enough"
# Sample output (values will vary based on exact parameters):
# Baseline price_v2: 1.025
# Normal price_v2 with max_v2_duration=4: 1.075
# Manipulated price_v2 with max_v2_duration=100: 3.650
# Extreme manipulated price_v2 with max_v2_duration=190: 12.475
# Normal price increase: 4.9%
# Manipulated price increase: 256.1%
# Extreme price increase: 1117.1%

This PoC demonstrates that by simply increasing the max_v2_duration parameter, an attacker can cause the price_v2() function to return dramatically inflated values. With realistic parameters, the price can be manipulated to be over 10 times higher than its normal value.

Root Cause Analysis

The root cause of this vulnerability is a combination of:

  1. Flawed Mathematical Model: The approximation logic in _obtain_price_params() uses an overly simplistic model that assumes linear growth without accounting for real-world token economics, compounding effects, or diminishing returns.

  2. Insufficient Parameter Validation: The set_max_v2_duration() function lacks adequate validation beyond a basic upper bound check, allowing admins to set potentially dangerous values.

  3. Unlimited Linear Growth: The calculation params.total_idle += gain * number_of_periods allows for unbounded linear growth, creating unrealistic projections for large values of number_of_periods.

  4. Inaccurate Supply Reduction: The loop that reduces params.total_supply uses a simplistic formula that doesn't accurately model how supply changes would occur in reality.

Tools Used

The vulnerability was identified through:

  1. Manual code review of the ScrvusdOracleV2.vy contract

  2. Mathematical analysis of the price calculation formulas

  3. Logical flow analysis of parameter approximation

  4. Simulation of different attack scenarios with various parameter values

Recommended Mitigation Steps

To address this vulnerability, I recommend implementing the following mitigations:

  1. Implement Diminishing Returns Model: Replace the linear gain calculation with a model that accounts for diminishing returns over time:

# Calculate with diminishing returns instead of linear growth
diminishing_factor: uint256 = 10**18 # Start at 100%
total_gained: uint256 = 0
for i in range(min(number_of_periods, 50)): # Cap at 50 periods for safety
if i > 0:
# Apply diminishing factor (e.g., 2% reduction per period)
diminishing_factor = diminishing_factor * 98 // 100
period_gain: uint256 = gain * diminishing_factor // 10**18
total_gained += period_gain
params.total_idle += total_gained
  1. Add Reasonable Hard Cap: Implement a stricter hard cap on max_v2_duration that cannot be bypassed by admins:

@external
def set_max_v2_duration(_max_v2_duration: uint256):
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
# Add reasonable hard maximum cap (e.g., 52 weeks = 1 year)
SAFE_DURATION_CAP: constant(uint256) = 52
assert _max_v2_duration <= min(MAX_V2_DURATION, SAFE_DURATION_CAP)
self.max_v2_duration = _max_v2_duration
log SetMaxV2Duration(_max_v2_duration)
  1. Implement Price Variance Circuit Breaker: Add a circuit breaker mechanism that limits how much price_v2() can deviate from more conservative price calculations:

@view
def _price_v2() -> uint256:
raw_price: uint256 = self._raw_price(block.timestamp, block.timestamp)
smoothed_price: uint256 = self._smoothed_price(self.last_prices[2], raw_price)
# Circuit breaker - limit maximum deviation from v0 price
v0_price: uint256 = self._price_v0()
max_allowed_price: uint256 = v0_price * 150 // 100 # Max 50% higher than v0
if smoothed_price > max_allowed_price:
return max_allowed_price
return smoothed_price
  1. Improve Parameter Validation: Add validation to ensure the approximated parameters remain within realistic boundaries:

# Add to _obtain_price_params function
# Check if approximated values are reasonable
if params.total_idle > self.price_params.total_idle * 5: # 5x growth limit
params.total_idle = self.price_params.total_idle * 5
  1. Consider Alternative Approximation Methods: For long-term approximations, consider implementing more sophisticated models that better represent real-world yield accrual and token economics.

By implementing these mitigations, particularly the diminishing returns model and the circuit breaker, the contract would be significantly more resistant to price manipulation even if an admin sets a high max_v2_duration value.

Updates

Lead Judging Commences

0xnevi Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

[invalid] finding-centralization-risk

- Per [codehawks documentation](https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity#findings-that-may-be-invalid) - Parameter change is executed via the Dao per docs > Also, it is worth noting that the oracle is controlled by a DAO and its parameters can be changed by a vote.

Support

FAQs

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