Algo Ssstablecoinsss

AI First Flight #2
Beginner FriendlyDeFi
EXP
View results
Submission Details
Impact: high
Likelihood: low
Invalid

Missing L2 Sequencer Uptime Check Enables Unfair Liquidations on ZKsync Era

Missing L2 Sequencer Uptime Check Enables Unfair Liquidations on ZKsync Era

Description

The protocol is deployed on ZKsync Era (a ZK rollup L2) but does not check the L2 sequencer uptime status before using Chainlink oracle data. The oracle_lib._stale_check_latest_round_data() function only validates price staleness and round data consistency, without verifying sequencer availability:

def _stale_check_latest_round_data(
price_price_address: address,
) -> (uint80, int256, uint256, uint256, uint80):
# ... only checks updated_at, answered_in_round, and staleness
# Missing: sequencer uptime check

On L2 networks, when the sequencer goes down:

  • Users cannot submit transactions (no deposits, no repayments, no collateral additions)

  • Oracle prices may become stale during the downtime

  • When the sequencer recovers, oracle prices update rapidly to reflect the current market

  • Positions that were healthy before downtime may now be liquidatable

Risk

Likelihood: Low -- ZKsync Era's sequencer has been generally reliable, but centralized sequencers are a known single point of failure. Downtime events have occurred on similar L2s (Arbitrum, Optimism).

Impact: High -- During sequencer downtime:

  1. Users cannot add collateral or repay debt to maintain their health factor

  2. When the sequencer comes back online, oracle prices jump to current values

  3. Positions that were healthy before downtime become immediately liquidatable

  4. MEV bots can front-run recovery to liquidate positions before users can react

Real-World Precedent: The Chainlink L2 Sequencer Uptime Feed is specifically designed to prevent this attack vector. Aave V3 on Arbitrum/Optimism implements this check.

Proof of Concept

How the attack works:

  1. User has a healthy position: 10 ETH collateral ($20,000), 8,000 DSC minted (250% collateral ratio)

  2. ZKsync Era sequencer goes down for 4 hours

  3. During downtime, ETH crashes from $2,000 to$1,400 (30% drop)

  4. User wants to add collateral but CANNOT -- sequencer is down, no transactions accepted

  5. Sequencer recovers, oracle immediately updates to $1,400

  6. User's health factor: (14000 * 50/100 * 1e18) / 8000e18 = 0.875e18 < 1e18

  7. MEV bot immediately liquidates the user, capturing the 10% bonus

Expected outcome: Users are liquidated unfairly during sequencer recovery with no opportunity to add collateral.

def test_sequencer_downtime_causes_unfair_liquidation(
self, dsce, weth, eth_usd, dsc, some_user, liquidator
):
# User creates healthy position: 10 ETH at $2,000, mint $8,000 DSC
amount_dsc = to_wei(8_000, "ether")
with boa.env.prank(some_user):
weth.approve(dsce.address, COLLATERAL_AMOUNT)
dsce.deposit_collateral_and_mint_dsc(weth.address, COLLATERAL_AMOUNT, amount_dsc)
assert dsce.health_factor(some_user) > to_wei(1, "ether") # Healthy
# Sequencer recovers — oracle jumps to $1,400
eth_usd.updateAnswer(1400 * 10**8)
assert dsce.health_factor(some_user) < to_wei(1, "ether") # Underwater!
# Immediate liquidation — NO grace period
weth.mint(liquidator, to_wei(20, "ether"))
with boa.env.prank(liquidator):
weth.approve(dsce.address, to_wei(20, "ether"))
dsce.deposit_collateral_and_mint_dsc(weth.address, to_wei(20, "ether"), amount_dsc)
dsc.approve(dsce.address, amount_dsc)
dsce.liquidate(weth, some_user, amount_dsc)
user_dsc, _ = dsce.get_account_information(some_user)
assert user_dsc == 0 # Fully liquidated, user had no chance to act

Recommended Mitigation

Integrate a Chainlink L2 Sequencer Uptime Feed check with a grace period:

GRACE_PERIOD_TIME: constant(uint256) = 3600 # 1 hour grace period
sequencer_uptime_feed: public(address) # Set in constructor
@internal
@view
def _check_sequencer_uptime():
(round_id, answer, started_at, updated_at, answered_in_round) = (
staticcall AggregatorV3Interface(self.sequencer_uptime_feed).latestRoundData()
)
assert answer == 0, "DSCEngine__SequencerDown"
time_since_up: uint256 = block.timestamp - started_at
assert time_since_up > GRACE_PERIOD_TIME, "DSCEngine__GracePeriodNotOver"
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 5 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!