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

Critical Cross-Chain Synchronization Vulnerability in scrvUSD Oracle

Summary

The scrvUSD oracle system lacks proper cross-chain synchronization mechanisms and L2 sequencer health checks, This creates opportunities for price manipulation across different chains.

Vulnerability Details

Based on the project context from README.md, scrvUSD is designed to work across multiple chains. However, the current implementation has several critical issues:

  1. No L2 Sequencer Health Validation:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT],
_ts: uint256,
_block_number: uint256
) -> uint256:
# Missing L2 sequencer health check
# Missing cross-chain block time validation
assert self.last_block_number <= _block_number, "Outdated"
  1. Hardcoded Block Times:
    From pyproject.toml and test configurations, the system assumes consistent block times across chains

  2. Missing Cross-Chain State Verification:
    The IBlockHashRetain.vyi interface lacks mechanisms to verify cross-chain state consistency:

@external
def apply() -> uint256:
"""
@notice Apply a block hash/state root.
# Missing cross-chain state verification
# Missing L2 sequencer uptime verification
# Missing chain-specific block time adjustments
"""
...

Impact

  • Price manipulation across chains

  • Stale prices during L2 sequencer downtime

  • Incorrect liquidations due to chain-specific block time differences

  • System-wide oracle manipulation

  • Financial losses across multiple chains

Tools Used

  • Manual Review

Recommendations

  1. Implement L2 Sequencer Health Check:

struct SequencerStatus:
is_active: bool
last_update_timestamp: uint256
grace_period_time: uint256
interface ISequencerUptime:
def latestRoundData() -> (uint80, int256, uint256, uint256, uint80): view
def latestAnswer() -> int256: view
@external
def check_sequencer_status(_chain_id: uint256) -> bool:
"""
@notice Verify L2 sequencer is operational
"""
sequencer: address = self.sequencer_registry[_chain_id]
assert sequencer != empty(address), "Invalid sequencer"
status: SequencerStatus = ISequencerUptime(sequencer).latestRoundData()
# Check if sequencer is active
if not status.is_active:
return False
# Verify not stale
if block.timestamp - status.last_update_timestamp > status.grace_period_time:
return False
return True
  1. Implement Chain-Specific Block Time Configuration:

struct ChainConfig:
block_time: uint256
min_confirmations: uint256
max_delay: uint256
sequencer_address: address
chain_configs: public(HashMap[uint256, ChainConfig])
@external
def set_chain_config(_chain_id: uint256, _config: ChainConfig):
"""
@notice Set chain-specific configuration
"""
assert msg.sender == self.admin, "Not admin"
assert _config.block_time > 0, "Invalid block time"
assert _config.min_confirmations > 0, "Invalid confirmations"
self.chain_configs[_chain_id] = _config
@view
@internal
def _validate_block_time(_chain_id: uint256, _block_number: uint256, _timestamp: uint256) -> bool:
"""
@notice Validate block time for specific chain
"""
config: ChainConfig = self.chain_configs[_chain_id]
# Calculate expected timestamp
expected_ts: uint256 = self.genesis_timestamp + (_block_number * config.block_time)
# Allow for some deviation
max_deviation: uint256 = config.block_time / 2
return abs(_timestamp - expected_ts) <= max_deviation
  1. Add Cross-Chain State Verification:

struct CrossChainState:
root: bytes32
timestamp: uint256
confirmations: uint256
verified_by: DynArray[address, 100]
cross_chain_states: public(HashMap[uint256, HashMap[uint256, CrossChainState]])
@external
def verify_cross_chain_state(
_source_chain: uint256,
_target_chain: uint256,
_block_number: uint256,
_state_root: bytes32
) -> bool:
"""
@notice Verify cross-chain state consistency
"""
# Check sequencer health first
assert self.check_sequencer_status(_target_chain), "Sequencer down"
# Get chain configs
source_config: ChainConfig = self.chain_configs[_source_chain]
target_config: ChainConfig = self.chain_configs[_target_chain]
# Verify block times
assert self._validate_block_time(_source_chain, _block_number, block.timestamp), "Invalid block time"
# Update cross-chain state
state: CrossChainState = self.cross_chain_states[_source_chain][_block_number]
state.verified_by.append(msg.sender)
# Check if enough confirmations
if len(state.verified_by) >= source_config.min_confirmations:
self._finalize_cross_chain_state(_source_chain, _target_chain, _block_number, state)
return True
return False
@internal
def _finalize_cross_chain_state(
_source_chain: uint256,
_target_chain: uint256,
_block_number: uint256,
_state: CrossChainState
):
"""
@notice Finalize cross-chain state after verification
"""
# Implement finalization logic
# Update price feeds
# Emit events
...
  1. Implement Emergency Shutdown for Chain-Specific Issues:

emergency_shutdown: public(HashMap[uint256, bool])
shutdown_timestamp: public(HashMap[uint256, uint256])
@external
def emergency_shutdown_chain(_chain_id: uint256):
"""
@notice Emergency shutdown for specific chain
"""
assert msg.sender == self.admin, "Not admin"
assert not self.check_sequencer_status(_chain_id), "Sequencer active"
self.emergency_shutdown[_chain_id] = True
self.shutdown_timestamp[_chain_id] = block.timestamp
# Emit shutdown event
log ChainEmergencyShutdown(_chain_id, block.timestamp)

These enhancements provide:

  • L2 sequencer health monitoring

  • Chain-specific block time handling

  • Cross-chain state verification

  • Emergency shutdown capabilities

  • Proper synchronization across chains

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.