Skip to content

liquidation_handler and adl_handler never check is_market_paused_key, bypassing the oracle circuit breaker #623

Description

@abayomicornelius

Problem

liquidation_handler::liquidate_position / execute_partial_liquidation (contracts/liquidation_handler/src/lib.rs) and adl_handler::execute_adl (contracts/adl_handler/src/lib.rs) never check is_market_paused_key — the oracle circuit-breaker pause flag that check_circuit_breaker (contracts/oracle/src/lib.rs:642-694) sets when a submitted price deviates from the last accepted price by more than a market's configured threshold.

grep -n "is_market_paused" contracts/order_handler/src/lib.rs contracts/liquidation_handler/src/lib.rs contracts/adl_handler/src/lib.rs
contracts/order_handler/src/lib.rs:691:        if ds.get_bool(&is_market_paused_key(&env, &params.market)) {
contracts/order_handler/src/lib.rs:960:        if ds.get_bool(&is_market_paused_key(&env, &order.market)) {
# — no matches in liquidation_handler or adl_handler

order_handler::create_order/execute_order (issue #366, already fixed for deposit/withdrawal handlers and present here too) correctly reject when the market is paused. liquidation_handler and adl_handler never read the flag at all — check_liquidatable, liquidate_position, execute_partial_liquidation, is_adl_required, and execute_adl all proceed against the oracle's current (possibly circuit-breaker-tripped) price with no pause gate.

Why it matters

docs/oracle-risk.md §2.2 documents exactly this attack surface: "By moving the price against open positions, the attacker can mark all opposing OI as liquidatable in one ledger... Forcing liquidation of longs: set price low... Forcing liquidation of shorts: set price high..." and §4.1 names the circuit breaker (issue #203, closed/implemented) as the "highest priority" mitigation specifically because it "hard-caps the per-ledger manipulation_factor." But check_circuit_breaker only sets is_market_paused_key — it does not reject the manipulated price submission itself (the extreme price is still stored and readable via get_primary_price/require_price_fresh). The pause flag is the only subsequent guard against that stored bad price being used, and order_handler is the sole consumer that checks it. A keeper (compromised or simply racing) can still call liquidation_handler::liquidate_position or adl_handler::execute_adl against the very price that just tripped the breaker, on the very market the breaker just paused — exactly the forced-liquidation half of the attack the circuit breaker exists to stop.

Failure scenario

  1. Oracle signer key is compromised (or a legitimate keeper submits a bad price by error). set_prices/set_prices_simple stores a price for market X that deviates from the last price by more than circuit_breaker_factor_key(market X).
  2. check_circuit_breaker sets is_market_paused_key(market X) = true and emits CircuitBreakerTripped.
  3. Deposits, withdrawals, and new order creation/execution on market X are correctly blocked (order_handler/deposit_handler/withdrawal_handler all check the flag).
  4. The attacker (or their liquidation-keeper bot, if colluding, or simply any keeper racing ahead of a human pause response) calls liquidation_handler::liquidate_position or adl_handler::execute_adl against market X using the same tripped, still-stored price. Neither function checks is_market_paused_key, so the call proceeds and seizes trader collateral / force-closes profitable positions at the manipulated price.

Suggested fix

Add the same if ds.get_bool(&is_market_paused_key(&env, &market)) { panic_with_error!(...) } guard already used in order_handler to: liquidation_handler::check_liquidatable, liquidate_position, execute_partial_liquidation, and adl_handler::is_adl_required, execute_adl. Add regression tests mirroring deposit_handler/withdrawal_handler's existing pause tests (issue #366), asserting each liquidation/ADL entrypoint reverts when the target market is paused.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions