From c17f3f0f5a9a3410def1d0f7a1b186076ba19976 Mon Sep 17 00:00:00 2001 From: rajiabdulrahmon2020-ui <302102038+rajiabdulrahmon2020-ui@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:57:43 +0100 Subject: [PATCH] Add emergency per-key trading freezes --- creator-keys/src/emergency_pause.rs | 74 ++++++++++++++++++++++++++++- creator-keys/src/lib.rs | 30 ++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/creator-keys/src/emergency_pause.rs b/creator-keys/src/emergency_pause.rs index 40702703..5d14d3d4 100644 --- a/creator-keys/src/emergency_pause.rs +++ b/creator-keys/src/emergency_pause.rs @@ -12,7 +12,7 @@ use crate::events::{ PlatformResumedEvent, }; use crate::{read_global_pause_admins, ContractError, GLOBAL_PAUSE_THRESHOLD}; -use soroban_sdk::{contracterror, contracttype, Address, Env, Vec}; +use soroban_sdk::{contracterror, contracttype, Address, Env, String, Vec}; /// Timelock delay between queueing and executing a platform resume (24 hours). pub const PLATFORM_RESUME_DELAY_SECS: u64 = 86_400; @@ -54,6 +54,17 @@ pub enum EmergencyPauseDataKey { ResumeEta, /// Per-key emergency pause override. KeyPaused(Address), + /// Emergency trading freeze and its optional metadata. + KeyFreeze(Address), +} + +/// Stored freeze details and the public status returned to callers. +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct KeyFreezeStatus { + pub frozen: bool, + pub reason: Option, + pub expiry_timestamp: Option, } /// Validates that `signers` are distinct members of the global-pause admin set, @@ -110,9 +121,70 @@ pub fn assert_trading_allowed(env: &Env, key_id: &Address) -> Result<(), Contrac if is_platform_paused(env) || is_key_paused(env, key_id) { return Err(ContractError::GlobalTradingHalted); } + if get_freeze_status(env, key_id).frozen { + return Err(ContractError::KeyFrozen); + } + Ok(()) +} + +/// Freezes trading for one key. Only the protocol admin may call this. An +/// omitted expiry keeps the freeze active until `unfreeze_key` is called. +pub fn freeze_key( + env: &Env, + admin: &Address, + key_id: &Address, + reason: &Option, + expiry_timestamp: &Option, +) -> Result<(), ContractError> { + admin.require_auth(); + crate::assert_is_admin(env, admin)?; + if expiry_timestamp.is_some_and(|expiry| expiry <= env.ledger().timestamp()) { + return Err(ContractError::InvalidFreezeExpiry); + } + let status = KeyFreezeStatus { + frozen: true, + reason: reason.clone(), + expiry_timestamp: expiry_timestamp.clone(), + }; + env.storage() + .persistent() + .set(&EmergencyPauseDataKey::KeyFreeze(key_id.clone()), &status); Ok(()) } +/// Removes a key freeze before its expiry. Only the protocol admin may call. +pub fn unfreeze_key(env: &Env, admin: &Address, key_id: &Address) -> Result<(), ContractError> { + admin.require_auth(); + crate::assert_is_admin(env, admin)?; + env.storage() + .persistent() + .remove(&EmergencyPauseDataKey::KeyFreeze(key_id.clone())); + Ok(()) +} + +/// Returns the live freeze status. Expired freezes are treated as cleared +/// immediately, without requiring a separate transaction to clean storage. +pub fn get_freeze_status(env: &Env, key_id: &Address) -> KeyFreezeStatus { + let stored: Option = env + .storage() + .persistent() + .get(&EmergencyPauseDataKey::KeyFreeze(key_id.clone())); + match stored { + Some(status) + if status + .expiry_timestamp + .is_none_or(|expiry| env.ledger().timestamp() < expiry) => + { + status + } + _ => KeyFreezeStatus { + frozen: false, + reason: None, + expiry_timestamp: None, + }, + } +} + /// Halts all buys and sells across every key, effective immediately. pub fn pause_platform(env: &Env, signers: &Vec
) -> Result<(), EmergencyPauseError> { let actor = assert_multisig(env, signers)?; diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index 4a3142cf..e476d6dc 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -213,6 +213,10 @@ pub enum ContractError { KeyAlertLimitReached = 108, /// No active price alert with the given id belongs to the wallet. AlertNotFound = 109, + /// Trading is suspended for this key by an emergency freeze. + KeyFrozen = 110, + /// A key freeze expiry must be later than the current ledger timestamp. + InvalidFreezeExpiry = 111, } /// Errors raised by the staking entrypoints @@ -12935,6 +12939,32 @@ impl CreatorKeysContract { emergency_pause::is_key_paused(&env, &key_id) } + /// Freezes buy and sell activity for `key_id` immediately. + /// + /// Only the protocol admin may call this. `reason` is returned by + /// `get_freeze_status`; an optional `expiry_timestamp` automatically + /// disables the freeze once the ledger timestamp reaches it. + pub fn freeze_key( + env: Env, + admin: Address, + key_id: Address, + reason: Option, + expiry_timestamp: Option, + ) -> Result<(), ContractError> { + emergency_pause::freeze_key(&env, &admin, &key_id, &reason, &expiry_timestamp) + } + + /// Clears an active key freeze before its expiry. Only the protocol admin + /// may call this. + pub fn unfreeze_key(env: Env, admin: Address, key_id: Address) -> Result<(), ContractError> { + emergency_pause::unfreeze_key(&env, &admin, &key_id) + } + + /// Returns whether a key is frozen and, while active, its reason and expiry. + pub fn get_freeze_status(env: Env, key_id: Address) -> emergency_pause::KeyFreezeStatus { + emergency_pause::get_freeze_status(&env, &key_id) + } + // ========================================================================= // #1030 — Price alerts // =========================================================================