From e1fec22db84070c54863a5b62a730ca672c48f6c Mon Sep 17 00:00:00 2001 From: Sikkra <159844544+Sikkra@users.noreply.github.com> Date: Tue, 19 May 2026 13:04:35 -0500 Subject: [PATCH] Add emergency pause controls --- .../strategies/blend_leverage/src/lib.rs | 73 ++++++++++++++++++- .../strategies/blend_leverage/src/storage.rs | 41 +++++++++++ .../blend_leverage/src/test_leverage.rs | 72 +++++++++++++++++- 3 files changed, 184 insertions(+), 2 deletions(-) diff --git a/contracts/strategies/blend_leverage/src/lib.rs b/contracts/strategies/blend_leverage/src/lib.rs index d878a71..a2bd282 100644 --- a/contracts/strategies/blend_leverage/src/lib.rs +++ b/contracts/strategies/blend_leverage/src/lib.rs @@ -19,7 +19,8 @@ use leverage::{ shares_to_underlying, }; use soroban_sdk::{ - contract, contractimpl, token::TokenClient, Address, Bytes, Env, IntoVal, String, Val, Vec, + contract, contractevent, contractimpl, token::TokenClient, Address, Bytes, Env, IntoVal, + String, Val, Vec, }; use storage::{extend_instance_ttl, Config}; @@ -31,6 +32,37 @@ fn check_positive_amount(amount: i128) -> Result<(), StrategyError> { } } +fn ensure_not_paused(e: &Env) -> Result<(), StrategyError> { + if storage::is_paused(e) { + Err(StrategyError::InvalidArgument) + } else { + Ok(()) + } +} + +fn require_admin(e: &Env, admin: &Address) -> Result<(), StrategyError> { + admin.require_auth(); + if *admin != storage::get_admin(e) { + return Err(StrategyError::NotAuthorized); + } + Ok(()) +} + +#[contractevent(data_format = "single-value", topics = ["pause_state"])] +pub struct PauseState { + #[topic] + admin: Address, + paused: bool, +} + +fn emit_pause_state(e: &Env, admin: &Address, paused: bool) { + PauseState { + admin: admin.clone(), + paused, + } + .publish(e); +} + const STRATEGY_NAME: &str = "BlendLeverageStrategy"; #[contract] @@ -49,6 +81,7 @@ impl DeFindexStrategyTrait for BlendLeverageStrategy { /// [5] c_factor: i128 — collateral factor (1e7) /// [6] target_loops: u32 — number of leverage loops /// [7] min_hf: i128 — minimum health factor (1e7) + /// [8] admin: Address — optional emergency pause admin, defaults to keeper fn __constructor(e: Env, asset: Address, init_args: Vec) { let pool: Address = init_args .get(0) @@ -82,6 +115,10 @@ impl DeFindexStrategyTrait for BlendLeverageStrategy { .get(7) .expect("Missing: min_hf") .into_val(&e); + let admin: Address = match init_args.get(8) { + Some(value) => value.into_val(&e), + None => keeper.clone(), + }; // Look up the reserve index from the pool let pool_client = blend_contract_sdk::pool::Client::new(&e, &pool); @@ -111,6 +148,8 @@ impl DeFindexStrategyTrait for BlendLeverageStrategy { storage::set_config(&e, config); storage::set_keeper(&e, &keeper); + storage::set_admin(&e, &admin); + storage::set_paused(&e, false); } fn asset(e: Env) -> Result { @@ -127,6 +166,7 @@ impl DeFindexStrategyTrait for BlendLeverageStrategy { /// 4. Return the depositor's underlying balance fn deposit(e: Env, amount: i128, from: Address) -> Result { extend_instance_ttl(&e); + ensure_not_paused(&e)?; check_positive_amount(amount)?; from.require_auth(); @@ -205,6 +245,7 @@ impl DeFindexStrategyTrait for BlendLeverageStrategy { /// No new shares are minted — this increases per-share equity. fn harvest(e: Env, from: Address, data: Option) -> Result<(), StrategyError> { extend_instance_ttl(&e); + ensure_not_paused(&e)?; let keeper = storage::get_keeper(&e); keeper.require_auth(); @@ -354,6 +395,36 @@ impl BlendLeverageStrategy { Ok(storage::get_keeper(&e)) } + /// Emergency pause: blocks deposits and harvest re-looping, while withdrawals remain available. + pub fn pause(e: Env, admin: Address) -> Result<(), StrategyError> { + extend_instance_ttl(&e); + require_admin(&e, &admin)?; + storage::set_paused(&e, true); + emit_pause_state(&e, &admin, true); + Ok(()) + } + + /// Resume deposits and harvest re-looping after an emergency pause. + pub fn unpause(e: Env, admin: Address) -> Result<(), StrategyError> { + extend_instance_ttl(&e); + require_admin(&e, &admin)?; + storage::set_paused(&e, false); + emit_pause_state(&e, &admin, false); + Ok(()) + } + + /// Get the configured emergency pause admin. + pub fn get_admin(e: Env) -> Result { + extend_instance_ttl(&e); + Ok(storage::get_admin(&e)) + } + + /// Whether deposits and harvest re-looping are currently paused. + pub fn is_paused(e: Env) -> Result { + extend_instance_ttl(&e); + Ok(storage::is_paused(&e)) + } + /// Get current health factor (1e7 scaled). pub fn health_factor(e: Env) -> Result { extend_instance_ttl(&e); diff --git a/contracts/strategies/blend_leverage/src/storage.rs b/contracts/strategies/blend_leverage/src/storage.rs index 686d722..a6ac687 100644 --- a/contracts/strategies/blend_leverage/src/storage.rs +++ b/contracts/strategies/blend_leverage/src/storage.rs @@ -19,6 +19,8 @@ pub enum DataKey { Reserves, VaultPos(Address), Keeper, + Admin, + Paused, } // ── Config ─────────────────────────────────────────────────────────────────── @@ -149,6 +151,45 @@ pub fn get_keeper(e: &Env) -> Address { .expect("Keeper not set") } +pub fn set_admin(e: &Env, admin: &Address) { + e.storage() + .persistent() + .set(&DataKey::Admin, admin); + e.storage().persistent().extend_ttl( + &DataKey::Admin, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); +} + +pub fn get_admin(e: &Env) -> Address { + e.storage() + .persistent() + .get(&DataKey::Admin) + .expect("Admin not set") +} + +pub fn set_paused(e: &Env, paused: bool) { + e.storage().persistent().set(&DataKey::Paused, &paused); + e.storage().persistent().extend_ttl( + &DataKey::Paused, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); +} + +pub fn is_paused(e: &Env) -> bool { + let paused = e.storage().persistent().get(&DataKey::Paused).unwrap_or(false); + if paused { + e.storage().persistent().extend_ttl( + &DataKey::Paused, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + } + paused +} + // ── Instance TTL ───────────────────────────────────────────────────────────── pub fn extend_instance_ttl(e: &Env) { diff --git a/contracts/strategies/blend_leverage/src/test_leverage.rs b/contracts/strategies/blend_leverage/src/test_leverage.rs index cb359d2..181c7b4 100644 --- a/contracts/strategies/blend_leverage/src/test_leverage.rs +++ b/contracts/strategies/blend_leverage/src/test_leverage.rs @@ -391,7 +391,12 @@ extern crate std; use crate::reserves; use crate::storage; -use soroban_sdk::{testutils::Address as _, Address, Env}; +use crate::{BlendLeverageStrategy, StrategyError}; +use defindex_strategy_core::DeFindexStrategyTrait; +use soroban_sdk::{ + testutils::{Address as _, EnvTestConfig}, + Address, Env, +}; fn make_reserves(b: i128, d: i128, shares: i128) -> LeverageReserves { LeverageReserves { @@ -419,6 +424,71 @@ fn with_contract(e: &Env, f: F) { }); } +fn env_without_snapshots() -> Env { + Env::new_with_config(EnvTestConfig { + capture_snapshot_at_drop: false, + }) +} + +#[test] +fn test_admin_can_pause_and_unpause() { + let e = env_without_snapshots(); + e.mock_all_auths(); + let admin = Address::generate(&e); + let contract_id = e.register(TestStorageContract, ()); + + e.as_contract(&contract_id, || { + storage::set_admin(&e, &admin); + storage::set_paused(&e, false); + + assert_eq!(BlendLeverageStrategy::get_admin(e.clone()).unwrap(), admin.clone()); + assert!(!BlendLeverageStrategy::is_paused(e.clone()).unwrap()); + + BlendLeverageStrategy::pause(e.clone(), admin.clone()).unwrap(); + assert!(BlendLeverageStrategy::is_paused(e.clone()).unwrap()); + }); + + e.as_contract(&contract_id, || { + BlendLeverageStrategy::unpause(e.clone(), admin).unwrap(); + assert!(!BlendLeverageStrategy::is_paused(e.clone()).unwrap()); + }); +} + +#[test] +fn test_only_admin_can_pause() { + let e = env_without_snapshots(); + e.mock_all_auths(); + with_contract(&e, |e, _| { + let admin = Address::generate(e); + let other = Address::generate(e); + storage::set_admin(e, &admin); + + let result = BlendLeverageStrategy::pause(e.clone(), other); + assert_eq!(result, Err(StrategyError::NotAuthorized)); + assert!(!storage::is_paused(e)); + }); +} + +#[test] +fn test_pause_guard_blocks_new_loop_paths() { + let e = env_without_snapshots(); + with_contract(&e, |e, _| { + let user = Address::generate(e); + storage::set_paused(e, false); + assert_eq!(crate::ensure_not_paused(e), Ok(())); + + storage::set_paused(e, true); + assert_eq!( + BlendLeverageStrategy::deposit(e.clone(), 1, user.clone()), + Err(StrategyError::InvalidArgument) + ); + assert_eq!( + BlendLeverageStrategy::harvest(e.clone(), user, None), + Err(StrategyError::InvalidArgument) + ); + }); +} + #[test] fn test_deposit_first_depositor() { let e = Env::default();