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

Stale Price Risk Due to Missing Timestamp Validation in Oracle Updates

Summary

The ScrvusdOracleV2.vy contract lacks proper timestamp validation in the update_price() function, While it checks block numbers, it fails to validate the freshness of price data through timestamp checks, potentially allowing stale prices to be used.

Vulnerability Details

In ScrvusdOracleV2.vy, the update_price() function only validates block numbers:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated" # @audit only block number check
self.last_block_number = _block_number

Critical issues:

  1. No validation of _ts against current block.timestamp

  2. No maximum age check for price data

  3. No heartbeat verification

  4. Missing checks for future timestamps

  5. No minimum update interval enforcement

Impact

  1. Price Staleness:

    • Outdated prices could be used for critical operations

    • Potential for price manipulation through delayed updates

    • Risk of incorrect liquidations or unfair trades

  2. System Risks:

    • Protocol could operate on stale data during market volatility

    • MEV opportunities through price update manipulation

    • Potential economic attacks through strategic update timing

Proof of Concept

def demonstrate_stale_price():
# Initial state
oracle = ScrvusdOracleV2.deploy()
# Step 1: Set initial price
initial_timestamp = chain.time()
oracle.update_price([...], initial_timestamp, chain.height())
# Step 2: Fast forward time significantly
chain.mine(timedelta=days(7))
# Step 3: Update with old timestamp still works
# This should fail but doesn't
oracle.update_price([...], initial_timestamp + 1, chain.height())
# Price is now stale but considered valid
assert oracle.price_v2() != actual_market_price

Tools Used

  • Manual code review

  • Temporal analysis

Recommendations

  1. Implement Comprehensive Timestamp Validation:

# Add constants for time checks
MAX_PRICE_AGE: constant(uint256) = 3600 # 1 hour
MIN_UPDATE_INTERVAL: constant(uint256) = 60 # 1 minute
FUTURE_THRESHOLD: constant(uint256) = 60 # 1 minute
@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT],
_ts: uint256,
_block_number: uint256
) -> uint256:
"""
@notice Update price with comprehensive timestamp validation
"""
# Access control
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Block number validation
assert self.last_block_number <= _block_number, "Outdated block"
# Timestamp freshness validation
assert _ts <= block.timestamp + FUTURE_THRESHOLD, "Future timestamp"
assert _ts >= block.timestamp - MAX_PRICE_AGE, "Stale timestamp"
assert _ts >= self.last_update + MIN_UPDATE_INTERVAL, "Too frequent"
# Update state
self.last_block_number = _block_number
self.last_update = block.timestamp
# Existing price update logic...
  1. Add Heartbeat Verification:

struct HeartbeatConfig:
max_delay: uint256
min_interval: uint256
last_heartbeat: uint256
@view
def _verify_heartbeat(_ts: uint256) -> bool:
"""
@notice Verify price update heartbeat
"""
config: HeartbeatConfig = self.heartbeat_config
# Check maximum delay
assert block.timestamp - _ts <= config.max_delay, "Exceeds max delay"
# Check minimum interval
assert _ts - config.last_heartbeat >= config.min_interval, "Too frequent"
return True
@external
def update_heartbeat_config(
_max_delay: uint256,
_min_interval: uint256
) -> bool:
"""
@notice Update heartbeat configuration
"""
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert _max_delay >= _min_interval, "Invalid config"
self.heartbeat_config = HeartbeatConfig({
max_delay: _max_delay,
min_interval: _min_interval,
last_heartbeat: block.timestamp
})
return True
  1. Implement Price Deviation Checks:

MAX_PRICE_DEVIATION: constant(uint256) = 1000 # 10% in basis points
@view
def _validate_price_deviation(
_new_price: uint256,
_old_price: uint256
) -> bool:
"""
@notice Validate price deviation is within acceptable bounds
"""
if _old_price == 0:
return True
# Calculate deviation in basis points
deviation: uint256 = 0
if _new_price > _old_price:
deviation = (_new_price - _old_price) * 10000 / _old_price
else:
deviation = (_old_price - _new_price) * 10000 / _old_price
assert deviation <= MAX_PRICE_DEVIATION, "Excessive deviation"
return True
  1. Add Emergency Circuit Breaker:

event CircuitBreakerTriggered:
reason: String[64]
timestamp: uint256
@external
def trigger_circuit_breaker(_reason: String[64]) -> bool:
"""
@notice Trigger circuit breaker to pause price updates
"""
access_control._check_role(EMERGENCY_ROLE, msg.sender)
self.circuit_breaker_active = True
log CircuitBreakerTriggered(_reason, block.timestamp)
return True
@view
def _check_circuit_breaker() -> bool:
"""
@notice Check if circuit breaker is active
"""
assert not self.circuit_breaker_active, "Circuit breaker active"
return True

These improvements provide:

  • Strict timestamp validation

  • Heartbeat verification

  • Price deviation checks

  • Emergency circuit breaker

  • Clear error messages and logging

The implementation should use all these mechanisms together to ensure robust price updates and prevent stale or manipulated data from being used.

Updates

Lead Judging Commences

0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality
Assigned finding tags:

[invalid] finding-missing-sequencer-check-stale-price

I believe this to be at best informational severity as - The moment sequencer is up again, the price updates that retrieve storage values from mainnet will be pushed. To note, price updates are retrieved from storage proofs are retrieved from Ethereum scrvUSD contract, so the concept of the next updated price being outdated is not possible, given mainnet does not utilize sequencers. - There are no problems with small lags if used in liquidity pools due to fees. Fees generate spread within which price can be lagged. - All price updates are subjected to smoothing, and as you can see from the historical price movements as seen [here](https://coinmarketcap.com/currencies/savings-crvusd/), there is never a large discrepancy in prices (absolute terms), and even more unlikely given sequencer downtimes will unlikely be long. This small price changes can be safely arbitrage aligning with [protocol design](https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#parameters) , along with the above mentioned fees - Combined with the above, the max price increments can be temporarily increased to more effectively match the most updated price.

Support

FAQs

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