Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion contracts/strategies/blend_leverage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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]
Expand All @@ -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<Val>) {
let pool: Address = init_args
.get(0)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<Address, StrategyError> {
Expand All @@ -127,6 +166,7 @@ impl DeFindexStrategyTrait for BlendLeverageStrategy {
/// 4. Return the depositor's underlying balance
fn deposit(e: Env, amount: i128, from: Address) -> Result<i128, StrategyError> {
extend_instance_ttl(&e);
ensure_not_paused(&e)?;
check_positive_amount(amount)?;
from.require_auth();

Expand Down Expand Up @@ -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<Bytes>) -> Result<(), StrategyError> {
extend_instance_ttl(&e);
ensure_not_paused(&e)?;

let keeper = storage::get_keeper(&e);
keeper.require_auth();
Expand Down Expand Up @@ -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<Address, StrategyError> {
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<bool, StrategyError> {
extend_instance_ttl(&e);
Ok(storage::is_paused(&e))
}

/// Get current health factor (1e7 scaled).
pub fn health_factor(e: Env) -> Result<i128, StrategyError> {
extend_instance_ttl(&e);
Expand Down
41 changes: 41 additions & 0 deletions contracts/strategies/blend_leverage/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub enum DataKey {
Reserves,
VaultPos(Address),
Keeper,
Admin,
Paused,
}

// ── Config ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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) {
Expand Down
72 changes: 71 additions & 1 deletion contracts/strategies/blend_leverage/src/test_leverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -419,6 +424,71 @@ fn with_contract<F: FnOnce(&Env, &Address)>(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();
Expand Down