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

Oracle Price Manipulation Through Smoothing Algorithm Exploitation

Summary

The ScrvusdOracleV2.vy contract's price smoothing algorithm in _smoothed_price() can be manipulated through strategic transaction timing, This vulnerability allows attackers to influence price updates for profit.

Vulnerability Details

The vulnerable smoothing algorithm in _smoothed_price():

@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
# Linear approximation used instead of exponential
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
# Vulnerable bounds check
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

Issues:

  1. Linear approximation instead of exponential allows for predictable price movements

  2. Block timestamp manipulation potential

  3. Unsafe subtraction usage

  4. No minimum update interval

  5. Predictable max_change calculation

Impact

  1. Price Manipulation:

    • Attackers can front-run price updates

    • Strategic transaction timing can force favorable price bounds

    • Profit from arbitrage between smoothed and actual prices

  2. Financial Risks:

    • Incorrect liquidations

    • Unfair lending/borrowing rates

    • System-wide economic imbalances

Proof of Concept

def exploit_smoothing():
# Initial state
initial_price = 1000e18
max_increment = 0.001e18 # 0.1% per second
# Step 1: Wait for maximum time delta
time_delta = 3600 # 1 hour
max_change = (max_increment * time_delta * initial_price) // 1e18
# Step 2: Front-run with large trade to push raw_price
target_price = initial_price + (max_change * 2)
# Step 3: Oracle update will be bounded
bounded_price = initial_price + max_change
# Step 4: Profit from arbitrage
profit = target_price - bounded_price
return profit

Tools Used

  • Manual code review

  • Custom price simulation tools

  • Temporal analysis framework

Recommendations

  1. Implement Chainlink-style Price Aggregation:

struct PriceRound:
round_id: uint80
price: uint256
timestamp: uint256
previous_round_id: uint80
@external
def submit_price_round(
_round_id: uint80,
_price: uint256,
_timestamp: uint256,
_previous_round_id: uint80,
_validator_signature: Bytes[65]
) -> bool:
"""
@notice Submit new price round with validator signature
"""
# Verify round sequencing
assert _round_id > self.latest_round_id, "Invalid round"
assert _previous_round_id == self.latest_round_id, "Wrong previous"
# Verify timestamp
assert _timestamp >= self.latest_timestamp, "Old timestamp"
assert _timestamp <= block.timestamp, "Future timestamp"
# Verify price bounds
self._verify_price_bounds(_price)
# Verify validator signature
self._verify_validator_signature(
_round_id,
_price,
_timestamp,
_previous_round_id,
_validator_signature
)
# Store round
self.price_rounds[_round_id] = PriceRound({
round_id: _round_id,
price: _price,
timestamp: _timestamp,
previous_round_id: _previous_round_id
})
# Update latest round
self.latest_round_id = _round_id
self.latest_timestamp = _timestamp
log PriceRoundSubmitted(_round_id, _price, _timestamp)
return True
  1. Implement TWAP with Geometric Mean:

struct Observation:
timestamp: uint256
price: uint256
cumulative_price: uint256
@internal
def _calculate_twap(
_start_timestamp: uint256,
_end_timestamp: uint256
) -> uint256:
"""
@notice Calculate TWAP between timestamps
"""
assert _end_timestamp > _start_timestamp, "Invalid interval"
assert _end_timestamp <= block.timestamp, "Future time"
# Get observations
start_obs: Observation = self._get_observation(_start_timestamp)
end_obs: Observation = self._get_observation(_end_timestamp)
# Calculate TWAP
time_elapsed: uint256 = end_obs.timestamp - start_obs.timestamp
price_change: uint256 = end_obs.cumulative_price - start_obs.cumulative_price
return price_change * 1e18 / time_elapsed
  1. Add Multi-Oracle Consensus:

struct OracleSource:
oracle: address
weight: uint256
active: bool
@internal
def _get_consensus_price() -> uint256:
"""
@notice Get weighted consensus price from multiple oracles
"""
total_weight: uint256 = 0
weighted_sum: uint256 = 0
# Get prices from all active oracles
for i in range(MAX_ORACLES):
source: OracleSource = self.oracle_sources[i]
if not source.active:
continue
price: uint256 = Oracle(source.oracle).get_price()
weighted_sum += price * source.weight
total_weight += source.weight
assert total_weight > 0, "No active oracles"
# Calculate weighted average
return weighted_sum / total_weight
  1. Implement Circuit Breakers:

struct CircuitBreaker:
max_price_deviation: uint256 # Max allowed deviation (in bps)
min_time_between_updates: uint256 # Minimum seconds between updates
max_valid_time: uint256 # Maximum seconds price remains valid
triggered: bool
@internal
def _check_circuit_breakers(
_new_price: uint256,
_last_price: uint256
) -> bool:
"""
@notice Verify price update against circuit breakers
"""
breaker: CircuitBreaker = self.circuit_breaker
# Check update frequency
assert block.timestamp - self.last_update >= breaker.min_time_between_updates, "Too frequent"
# Check price deviation
price_change: uint256 = abs(_new_price - _last_price)
max_allowed_change: uint256 = _last_price * breaker.max_price_deviation / 10000
if price_change > max_allowed_change:
self.circuit_breaker.triggered = True
log CircuitBreakerTriggered(_new_price, _last_price)
return False
return True
  1. Add Fallback Mechanism:

@view
@external
def get_safe_price() -> uint256:
"""
@notice Get price with fallback mechanisms
"""
# Try primary price source
try:
price: uint256 = self._get_consensus_price()
if self._is_price_valid(price):
return price
except:
pass
# Try TWAP fallback
try:
twap: uint256 = self._calculate_twap(
block.timestamp - 3600, # 1 hour TWAP
block.timestamp
)
if self._is_price_valid(twap):
return twap
except:
pass
# Use last valid price if recent enough
if block.timestamp - self.last_valid_update <= self.circuit_breaker.max_valid_time:
return self.last_valid_price
# Revert if no valid price available
raise "No valid price available"

These improvements provide:

  • Multiple oracle sources for price consensus

  • Geometric mean TWAP calculations

  • Circuit breakers for price protection

  • Fallback mechanisms for resilience

  • Proper validation and bounds checking

The implementation should use a combination of these mechanisms to ensure robust and manipulation-resistant price feeds.

Updates

Lead Judging Commences

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

[invalid] finding-timestamp-manipulation

Extremely theoretical finding. No proof that and economic analysis of if such a manipulation is profitable.

Support

FAQs

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