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

The Oracle's Max Price Growth Is 100x Higher Than Documented (6307% vs 63% APY)

Relevant GitHub Links

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L98
https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L155-L160

Summary

The ScrvusdOracleV2 contract uses a max_price_increment value that is supposed to be "linearly approximated to max 63% APY" according to the documentation. However, the actual implementation allows for a maximum price growth of approximately 6307% APY, which is 100 times higher than documented. This discrepancy could lead to unexpected price movements, potentially affecting StableSwap pools and other integrations relying on this oracle.

Vulnerability Details

In the ScrvusdOracleV2.vy contract, there's a max_price_increment parameter set during initialization:

# 2 * 10 ** 12 is equivalent to
# 1) 0.02 bps per second or 0.24 bps per block on Ethereum
# 2) linearly approximated to max 63% APY
self.max_price_increment = 2 * 10**12

This parameter is used in the _smoothed_price function to limit how quickly the price can change:

@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
# Ideally should be (max_price_increment / 10**18) ** (block.timestamp - self.last_update)
# Using linear approximation to simplify calculations
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
# -max_change <= (raw_price - last_price) <= max_change
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

However, when calculating the maximum yearly growth, we find that the actual APY is much higher than documented:

def test_max_apy():
# Constants from the contract
max_price_increment = 2 * 10**12
seconds_in_year = 365 * 24 * 60 * 60 # 31,536,000 seconds
initial_price = 10**18 # 1.0 in fixed point representation
# Use the exact formula from _smoothed_price function
# max_change = max_price_increment * time_delta * last_price / 10**18
max_change_in_one_year = (max_price_increment * seconds_in_year * initial_price) // 10**18
# Calculate APY as a percentage
max_apy = (max_change_in_one_year * 100) // initial_price
print(f"Max APY according to contract algorithm: {max_apy}%")
# Verify the commented value of ~63% APY
assert 60 <= max_apy <= 65, f"Expected ~63% APY, got {max_apy}%"

Running this test yields:

Max APY according to contract algorithm: 6307%
AssertionError: Expected ~63% APY, got 6307%

This indicates that the contract's smoothing mechanism allows for price changes approximately 100 times higher than documented, which could lead to unexpected behaviors in integrating protocols.

Impact

The impact of this vulnerability is significant for protocols integrating with this oracle:

  1. Unexpected Price Movements: Systems using this oracle would experience price changes that are 100x faster than documented, potentially breaking assumptions about maximum price growth rates.

  2. Pool Vulnerability: As stated in the README, "If not precise enough, this can lead to MEV in the liquidity pool, at a loss for the liquidity providers." With a much higher growth ceiling, the risk of MEV (Miner Extractable Value) exploitation increases.

  3. Arbitrage Opportunities: The excessive price movement allowance could create arbitrage opportunities that would not exist if the oracle behaved as documented, leading to potential value extraction from liquidity pools.

  4. Protocol Integration Issues: The documentation states that "the oracle is controlled by a DAO and its parameters can be changed by a vote." Systems may be designed assuming a 63% APY limit, but the actual implementation allows for much faster price changes.

The README also mentions: "Taking the minimum pool fee as 1bps means that the oracle should not jump more than 1 bps per block." The current implementation could significantly exceed this guideline under certain conditions.

Tools Used

  • Manual code review

  • Custom test script

Recommendations

To align the code with the documented behavior, the max_price_increment value should be reduced by a factor of 100:

# Change from:
self.max_price_increment = 2 * 10**12
# To:
self.max_price_increment = 2 * 10**10

This adjustment would ensure the maximum APY is approximately 63% as documented.

Additionally, it's recommended to add explicit tests to verify this behavior in the test suite to prevent similar issues in the future.

Updates

Lead Judging Commences

0xnevi Lead Judge
3 months ago
0xnevi Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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