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, ¶ms.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
- 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).
check_circuit_breaker sets is_market_paused_key(market X) = true and emits CircuitBreakerTripped.
- Deposits, withdrawals, and new order creation/execution on
market X are correctly blocked (order_handler/deposit_handler/withdrawal_handler all check the flag).
- 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.
Problem
liquidation_handler::liquidate_position/execute_partial_liquidation(contracts/liquidation_handler/src/lib.rs) andadl_handler::execute_adl(contracts/adl_handler/src/lib.rs) never checkis_market_paused_key— the oracle circuit-breaker pause flag thatcheck_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.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_handlerandadl_handlernever read the flag at all —check_liquidatable,liquidate_position,execute_partial_liquidation,is_adl_required, andexecute_adlall 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." Butcheck_circuit_breakeronly setsis_market_paused_key— it does not reject the manipulated price submission itself (the extreme price is still stored and readable viaget_primary_price/require_price_fresh). The pause flag is the only subsequent guard against that stored bad price being used, andorder_handleris the sole consumer that checks it. A keeper (compromised or simply racing) can still callliquidation_handler::liquidate_positionoradl_handler::execute_adlagainst 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
set_prices/set_prices_simplestores a price formarket Xthat deviates from the last price by more thancircuit_breaker_factor_key(market X).check_circuit_breakersetsis_market_paused_key(market X) = trueand emitsCircuitBreakerTripped.market Xare correctly blocked (order_handler/deposit_handler/withdrawal_handlerall check the flag).liquidation_handler::liquidate_positionoradl_handler::execute_adlagainstmarket Xusing the same tripped, still-stored price. Neither function checksis_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 inorder_handlerto:liquidation_handler::check_liquidatable,liquidate_position,execute_partial_liquidation, andadl_handler::is_adl_required,execute_adl. Add regression tests mirroringdeposit_handler/withdrawal_handler's existing pause tests (issue #366), asserting each liquidation/ADL entrypoint reverts when the target market is paused.