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

Unbounded Downward Price Adjustments in the `ScrvusdOracleV2::_smoothed_price`

https://github.com/CodeHawks-Contests/2025-03-curve/blob/main/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L155-L164

Summary

The _smoothed_price function in the scrvUSD oracle contract is designed to limit rapid price changes by applying a maximum price increment over time. However, an analysis reveals that it only caps price increases, not decreases. This asymmetry allows for potentially unlimited downward price adjustments in a single update, which could lead to unexpected behavior in dependent systems relying on stable or predictable pricing. While this may be an intentional design choice, it introduces a risk of sharp price drops that could affect trading, liquidations, or other price-sensitive operations.


Vulnerability Details

The _smoothed_price function calculates a smoothed price based on a last_price and a raw_price, using a max_change value derived from self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18. The logic is intended to ensure that price changes do not exceed this maximum change per update, providing a linear approximation of price smoothing.

The key logic is as follows:

max_change: uint256 = (self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18)
if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
return last_price + max_change if raw_price > last_price else last_price - max_change
return raw_price
  • Condition: unsafe_sub(raw_price + max_change, last_price) > 2 * max_change checks if the difference between raw_price and last_price exceeds max_change in either direction.

  • Behavior:

    • If raw_price > last_price and exceeds last_price + max_change, it returns last_price + max_change, effectively capping upward movement.

    • If raw_price < last_price, it evaluates raw_price + max_change - last_price. If this is greater than 2 * max_change, it returns last_price - max_change. However, if raw_price is significantly lower, the condition fails, and raw_price is returned directly, uncapped.

Example Scenarios

  1. Price Increase:

    • last_price = 100, raw_price = 120, max_change = 10

    • raw_price + max_change - last_price = 120 + 10 - 100 = 30 > 20 (2 * max_change)

    • Returns last_price + max_change = 110, capping the increase.

  2. Price Decrease (Small):

    • last_price = 100, raw_price = 95, max_change = 10

    • raw_price + max_change - last_price = 95 + 10 - 100 = 5 < 20 (2 * max_change)

    • Returns raw_price = 95, allowing the decrease.

  3. Price Decrease (Large):

    • last_price = 100, raw_price = 70, max_change = 10

    • raw_price + max_change - last_price = 70 + 10 - 100 = -20 + 10 = -10 < 20 (2 * max_change)

    • Returns raw_price = 70, allowing an uncapped drop.

The condition fails to enforce a lower bound (e.g., last_price - max_change) for significant decreases, meaning any downward adjustment is applied fully if it doesn’t trigger the cap logic incorrectly. This asymmetry suggests that the smoothing mechanism is incomplete for downward movements.


Impact

The lack of a cap on price decreases could have the following impacts:

  • Financial Risk: Dependent contracts, might assume price stability in both directions. A sudden, uncapped price drop could trigger liquidations, arbitrage opportunities, or loss of funds if not anticipated.


Tools Used

  • Manual Code Review


Recommendations

To address this potential vulnerability, consider the following:

  1. Symmetric Smoothing:

    • Modify _smoothed_price to cap both increases and decreases explicitly:

      max_change: uint256 = (self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18)
      if raw_price > last_price:
      return min(raw_price, last_price + max_change)
      else:
      return max(raw_price, last_price - max_change)
    • This ensures price changes are bounded in both directions, aligning with the stated goal of eliminating sharp changes.

Updates

Lead Judging Commences

0xnevi Lead Judge
2 months ago
0xnevi Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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