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

Missing Sequencer Health Check Allows Stale Oracle Prices on L2s

Summary

The ScrvusdOracleV2.vy contract lacks implementation of sequencer uptime verification, which could lead to stale prices being used when L2 sequencer is down. This is a critical security concern for L2 deployments.

Vulnerability Details

The oracle contract does not implement any checks to verify if the L2 sequencer is operational before providing price data. This is particularly critical for L2 deployments where sequencer downtime can lead to stale or manipulated prices being used.

Current implementation in ScrvusdOracleV2.vy only checks 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"

Key issues:

  1. No sequencer status verification

  2. No grace period after sequencer recovery

  3. Missing L2-specific timestamp validations

Proof of Concept

When an L2 sequencer goes down:

1 .Price updates stop but contract remains functional
2. When sequencer restarts, immediate price updates might be stale
3. Attackers can exploit this window for price manipulation

Impact

  • Price manipulation during sequencer downtime

  • Stale price usage after sequencer restarts

  • Potential economic attacks during L2 network issues

  • Risk of incorrect liquidations and unfair trades

Tools Used

  • Manual Review

  • Cross-chain deployment analysis

  • L2 security best practices verification

Recommendations

Add L2 sequencer verification:

event SequencerStatusUpdated:
is_active: bool
timestamp: uint256
struct SequencerStatus:
is_active: bool
last_update: uint256
grace_period: uint256
interface ISequencerUptime:
def latestRoundData() -> (uint80, int256, uint256, uint256, uint80): view
GRACE_PERIOD_TIME: constant(uint256) = 3600 # 1 hour
sequencer_feed: public(address)
sequencer_active: public(bool)
@external
def __init__(_initial_price: uint256, _sequencer_feed: address):
"""
@param _initial_price Initial price of asset per share (10**18)
@param _sequencer_feed Address of the sequencer uptime feed
"""
self.sequencer_feed = _sequencer_feed
# ... existing initialization code ...
@internal
def _check_sequencer() -> bool:
"""
@notice Verify L2 sequencer is operational
@return bool True if sequencer is active and within grace period
"""
round_id: uint80 = 0
answer: int256 = 0
started_at: uint256 = 0
updated_at: uint256 = 0
answered_in_round: uint80 = 0
round_id, answer, started_at, updated_at, answered_in_round = ISequencerUptime(self.sequencer_feed).latestRoundData()
assert answer == 0, "Sequencer down"
assert block.timestamp - updated_at <= GRACE_PERIOD_TIME, "Grace period expired"
return True
@external
def update_price(_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256) -> uint256:
"""
@notice Update price with sequencer verification
"""
assert self._check_sequencer(), "Sequencer inactive"
# ... existing update_price code ...

This implementation:

  • Adds proper sequencer status verification

  • Implements grace period after sequencer recovery

  • Emits events for monitoring

  • Maintains compatibility with existing functionality

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.