diff --git a/app/contract/contracts/quickex/src/admin.rs b/app/contract/contracts/quickex/src/admin.rs index 03a755ba..b88ad545 100644 --- a/app/contract/contracts/quickex/src/admin.rs +++ b/app/contract/contracts/quickex/src/admin.rs @@ -6,7 +6,7 @@ use crate::events::{ use crate::fee_router; use crate::storage; use crate::types::{FeeConfig, PerAssetFeeConfig, Role}; -use soroban_sdk::{Address, Env, Vec}; +use soroban_sdk::{Address, Bytes, Env, Vec}; /// Initialize the contract with an admin address. /// @@ -129,11 +129,16 @@ pub fn set_admin(env: &Env, caller: Address, new_admin: Address) -> Result<(), Q } /// Set the paused state (**Admin or Operator only**). -pub fn set_paused(env: &Env, caller: Address, new_state: bool) -> Result<(), QuickexError> { +pub fn set_paused( + env: &Env, + caller: Address, + new_state: bool, + reason: Bytes, +) -> Result<(), QuickexError> { require_any_role(env, &caller, &[Role::Admin, Role::Operator])?; storage::set_paused(env, new_state); - publish_contract_paused(env, caller, new_state); + publish_contract_paused(env, caller, new_state, reason); Ok(()) } diff --git a/app/contract/contracts/quickex/src/events.rs b/app/contract/contracts/quickex/src/events.rs index 4ffc5634..24310a32 100644 --- a/app/contract/contracts/quickex/src/events.rs +++ b/app/contract/contracts/quickex/src/events.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contractevent, Address, BytesN, Env}; +use soroban_sdk::{contractevent, Address, Bytes, BytesN, Env}; /// Canonical event schema version. /// @@ -10,7 +10,7 @@ use soroban_sdk::{contractevent, Address, BytesN, Env}; /// History: /// v1 – original schema (no version field) /// v2 – added `schema_version` to every event payload (this release) -pub const EVENT_SCHEMA_VERSION: u32 = 2; +pub const EVENT_SCHEMA_VERSION: u32 = 3; /// Testnet event topic namespace used as topic[0] for every QuickEx event. #[allow(dead_code)] @@ -75,7 +75,7 @@ pub const EVENT_SCHEMAS: &[EventSchema] = &[ EventSchema { name: "ContractPaused", topics: &[EVENT_TOPIC_ADMIN, "ContractPaused", "admin"], - payload_keys: &["paused", "schema_version", "timestamp"], + payload_keys: &["paused", "reason", "schema_version", "timestamp"], schema_version: EVENT_SCHEMA_VERSION, }, EventSchema { @@ -333,15 +333,17 @@ pub struct ContractPausedEvent { pub schema_version: u32, pub paused: bool, + pub reason: Bytes, pub timestamp: u64, } #[allow(dead_code)] -pub(crate) fn publish_contract_paused(env: &Env, admin: Address, paused: bool) { +pub(crate) fn publish_contract_paused(env: &Env, admin: Address, paused: bool, reason: Bytes) { ContractPausedEvent { admin, schema_version: EVENT_SCHEMA_VERSION, paused, + reason, timestamp: env.ledger().timestamp(), } .publish(env); diff --git a/app/contract/contracts/quickex/src/lib.rs b/app/contract/contracts/quickex/src/lib.rs index d5a35fc6..a8f83b36 100644 --- a/app/contract/contracts/quickex/src/lib.rs +++ b/app/contract/contracts/quickex/src/lib.rs @@ -211,7 +211,7 @@ impl QuickexContract { timeout_secs: u64, arbiter: Option
, ) -> Result, QuickexError> { - if storage::is_emergency_mode(&env) { + if !storage::is_feature_allowed_in_emergency(&env, storage::PauseFlag::Deposit) { return Err(QuickexError::ContractPaused); } if admin::is_paused(&env) { @@ -346,7 +346,7 @@ impl QuickexContract { timeout_secs: u64, arbiter: Option
, ) -> Result<(), QuickexError> { - if storage::is_emergency_mode(&env) { + if !storage::is_feature_allowed_in_emergency(&env, storage::PauseFlag::DepositWithCommitment) { return Err(QuickexError::ContractPaused); } if admin::is_paused(&env) { @@ -677,7 +677,20 @@ impl QuickexContract { if storage::is_emergency_mode(&env) { return Err(QuickexError::ContractPaused); } - admin::set_paused(&env, caller, new_state) + admin::set_paused(&env, caller, new_state, Bytes::new(&env)) + } + + /// Pause/unpause with an explicit reason string (Admin only). + pub fn set_paused_with_reason( + env: Env, + caller: Address, + new_state: bool, + reason: Bytes, + ) -> Result<(), QuickexError> { + if storage::is_emergency_mode(&env) { + return Err(QuickexError::ContractPaused); + } + admin::set_paused(&env, caller, new_state, reason) } /// Check if the function is currently paused. diff --git a/app/contract/contracts/quickex/src/storage.rs b/app/contract/contracts/quickex/src/storage.rs index 3665fb2c..cecc020b 100644 --- a/app/contract/contracts/quickex/src/storage.rs +++ b/app/contract/contracts/quickex/src/storage.rs @@ -192,6 +192,22 @@ pub fn is_emergency_mode(env: &Env) -> bool { env.storage().persistent().get(&key).unwrap_or(false) } +/// Check whether a given feature is permitted while emergency mode is active. +/// +/// During emergency mode most operations should be blocked. A small set of +/// safe entrypoints (e.g. withdrawals, refunds) are allowed so users can +/// recover funds. This helper centralises that allowlist. +pub fn is_feature_allowed_in_emergency(env: &Env, flag: PauseFlag) -> bool { + if !is_emergency_mode(env) { + return true; + } + + match flag { + PauseFlag::Withdrawal | PauseFlag::Refund => true, + _ => false, + } +} + // ----------------------------------------------------------------------------- // Escrow helpers // -----------------------------------------------------------------------------