Skip to content
Merged
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
23 changes: 16 additions & 7 deletions contracts/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,24 @@ pub enum SavingsError {
/// Config initialization can only happen once.
ConfigAlreadyInitialized = 91,

// ========== Strategy Errors (92-99) ==========
/// Returned when a yield strategy is not found in the registry.
StrategyNotFound = 92,
/// Returned when attempting to operate on a disabled strategy.
///
/// Emergency withdraw has been executed and strategy is now disabled.
StrategyDisabled = 92,

/// Returned when attempting to register a strategy that already exists.
StrategyAlreadyRegistered = 93,
/// Returned when the specified strategy does not exist.
///
/// This occurs when querying a non-existent strategy.
StrategyNotFound = 93,

/// Returned when attempting to deposit into a disabled strategy.
StrategyDisabled = 94,
/// Returned when attempting to withdraw from a plan that has already been withdrawn.
AlreadyWithdrawn = 94,

/// Returned when the specified lock plan does not exist.
LockNotFound = 95,

/// Returned when attempting to register a strategy that already exists.
StrategyAlreadyRegistered = 96,
}

#[cfg(test)]
Expand Down
171 changes: 171 additions & 0 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,177 @@ impl NesteraContract {
Ok(())
}

// ========== Emergency Functions ==========

/// Emergency withdraw - allows governance to force withdraw all funds from a strategy
/// and disable it for security. This bypasses normal withdrawal restrictions.
///
/// # Arguments
/// * `admin` - The admin address (must be governance)
/// * `user` - The user who owns the strategy
/// * `plan_type` - The type of plan (Flexi, Lock, Goal, Group)
/// * `plan_id` - The ID of the plan to withdraw from
///
/// # Returns
/// * The amount withdrawn
pub fn emergency_withdraw(
env: Env,
admin: Address,
user: Address,
plan_type: PlanType,
plan_id: u64,
) -> Result<i128, SavingsError> {
// 1. Verify admin authorization
admin.require_auth();
let stored_admin: Option<Address> = env.storage().instance().get(&DataKey::Admin);
if stored_admin != Some(admin) {
return Err(SavingsError::Unauthorized);
}

// 2. Check if strategy is already disabled
let disabled_key = DataKey::DisabledStrategy(plan_type.clone(), plan_id);
if env.storage().persistent().has(&disabled_key) {
return Err(SavingsError::StrategyDisabled);
}

// 3. Perform withdrawal based on plan type
let withdrawn_amount = match plan_type {
PlanType::Flexi => {
// For Flexi, withdraw the entire balance
let flexi_key = DataKey::FlexiBalance(user.clone());
let balance: i128 = env.storage().persistent().get(&flexi_key).unwrap_or(0);

if balance > 0 {
// Update flexi balance to 0
env.storage().persistent().set(&flexi_key, &0i128);

// Update user total balance
let user_key = DataKey::User(user.clone());
if let Some(mut user_data) =
env.storage().persistent().get::<DataKey, User>(&user_key)
{
user_data.total_balance = user_data.total_balance.saturating_sub(balance);
env.storage().persistent().set(&user_key, &user_data);
}
}
balance
}
PlanType::Lock(_) => {
// For Lock, get the lock save and withdraw if exists
let lock_key = DataKey::LockSave(plan_id);
let lock_opt: Option<LockSave> = env.storage().persistent().get(&lock_key);

if let Some(mut lock) = lock_opt {
if lock.is_withdrawn {
return Err(SavingsError::AlreadyWithdrawn);
}
let amount = lock.amount;
lock.is_withdrawn = true;
env.storage().persistent().set(&lock_key, &lock);

// Update user total balance
let user_key = DataKey::User(user.clone());
if let Some(mut user_data) =
env.storage().persistent().get::<DataKey, User>(&user_key)
{
user_data.total_balance = user_data.total_balance.saturating_sub(amount);
env.storage().persistent().set(&user_key, &user_data);
}
amount
} else {
return Err(SavingsError::LockNotFound);
}
}
PlanType::Goal(_, _, _) => {
// For Goal, get the goal save and withdraw
let goal_key = DataKey::GoalSave(plan_id);
let goal_opt: Option<GoalSave> = env.storage().persistent().get(&goal_key);

if let Some(mut goal) = goal_opt {
if goal.is_withdrawn {
return Err(SavingsError::AlreadyWithdrawn);
}
let amount = goal.current_amount;
goal.is_withdrawn = true;
env.storage().persistent().set(&goal_key, &goal);

// Update user total balance
let user_key = DataKey::User(user.clone());
if let Some(mut user_data) =
env.storage().persistent().get::<DataKey, User>(&user_key)
{
user_data.total_balance = user_data.total_balance.saturating_sub(amount);
env.storage().persistent().set(&user_key, &user_data);
}
amount
} else {
return Err(SavingsError::PlanNotFound);
}
}
PlanType::Group(_, _, _, _) => {
// For Group, get the group save and process emergency break
let group_key = DataKey::GroupSave(plan_id);
let group_opt: Option<GroupSave> = env.storage().persistent().get(&group_key);

if let Some(mut group) = group_opt {
if group.is_completed {
return Err(SavingsError::PlanCompleted);
}
// Return current amount for the user
let contribution_key = DataKey::GroupMemberContribution(plan_id, user.clone());
let contribution: i128 = env
.storage()
.persistent()
.get(&contribution_key)
.unwrap_or(0);

if contribution > 0 {
// Clear user contribution
env.storage().persistent().set(&contribution_key, &0i128);

// Update group current amount
group.current_amount = group.current_amount.saturating_sub(contribution);
env.storage().persistent().set(&group_key, &group);

// Update user total balance
let user_key = DataKey::User(user.clone());
if let Some(mut user_data) =
env.storage().persistent().get::<DataKey, User>(&user_key)
{
user_data.total_balance =
user_data.total_balance.saturating_sub(contribution);
env.storage().persistent().set(&user_key, &user_data);
}
}
contribution
} else {
return Err(SavingsError::PlanNotFound);
}
}
};

// 4. Mark strategy as disabled
env.storage().persistent().set(&disabled_key, &true);
ttl::extend_config_ttl(&env, &disabled_key);

// 5. Emit event
env.events().publish(
(Symbol::new(&env, "emergency_withdraw"), user, plan_id),
withdrawn_amount,
);

Ok(withdrawn_amount)
}

/// Checks if a strategy is disabled
pub fn is_strategy_disabled(env: Env, plan_type: PlanType, plan_id: u64) -> bool {
let disabled_key = DataKey::DisabledStrategy(plan_type, plan_id);
env.storage()
.persistent()
.get(&disabled_key)
.unwrap_or(false)
}

// --- Remaining views and utilities ---
pub fn get_savings_plan(env: Env, user: Address, plan_id: u64) -> Option<SavingsPlan> {
env.storage()
Expand Down
6 changes: 6 additions & 0 deletions contracts/src/storage_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ pub enum SavingsError {
LockNotMatured = 5,
AlreadyWithdrawn = 6,
Unauthorized = 7,
/// Returned when attempting to operate on a disabled strategy
StrategyDisabled = 8,
/// Returned when the specified strategy does not exist
StrategyNotFound = 9,
}

/// Represents a Goal Save plan with target amount
Expand Down Expand Up @@ -190,6 +194,8 @@ pub enum DataKey {
GroupRate,
/// Maps duration (days) to interest rate
LockRate(u64),
/// Maps (plan_type, plan_id) to disabled status
DisabledStrategy(PlanType, u64),
}

/// Payload structure that the admin signs off-chain
Expand Down