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:
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price
)
# 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:
Linear approximation instead of exponential allows for predictable price movements
Block timestamp manipulation potential
Unsafe subtraction usage
No minimum update interval
Predictable max_change calculation
Impact
-
Price Manipulation:
Attackers can front-run price updates
Strategic transaction timing can force favorable price bounds
Profit from arbitrage between smoothed and actual prices
-
Financial Risks:
Proof of Concept
def exploit_smoothing():
initial_price = 1000e18
max_increment = 0.001e18
time_delta = 3600
max_change = (max_increment * time_delta * initial_price) // 1e18
target_price = initial_price + (max_change * 2)
bounded_price = initial_price + max_change
profit = target_price - bounded_price
return profit
Tools Used
Recommendations
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
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
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
Implement Circuit Breakers:
struct CircuitBreaker:
max_price_deviation: uint256
min_time_between_updates: uint256
max_valid_time: uint256
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
Add Fallback Mechanism:
@view
@external
def get_safe_price() -> uint256:
"""
@notice Get price with fallback mechanisms
"""
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
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.