From f627e2f6bb54ea42ca57b198c28442a2debee247 Mon Sep 17 00:00:00 2001 From: Matan Date: Wed, 25 Feb 2026 16:44:10 +0200 Subject: [PATCH] feat: implement deposit collection, return, and forfeit for groups (#10) --- contracts/sorosave/src/admin.rs | 24 +++++++++++++++---- contracts/sorosave/src/group.rs | 38 ++++++++++++++++++++++++++++++- contracts/sorosave/src/lib.rs | 2 ++ contracts/sorosave/src/payout.rs | 13 +++++++++++ contracts/sorosave/src/storage.rs | 18 ++++++++++++++- contracts/sorosave/src/types.rs | 2 ++ 6 files changed, 90 insertions(+), 7 deletions(-) diff --git a/contracts/sorosave/src/admin.rs b/contracts/sorosave/src/admin.rs index 049b6ce..c839cb1 100644 --- a/contracts/sorosave/src/admin.rs +++ b/contracts/sorosave/src/admin.rs @@ -127,18 +127,32 @@ pub fn emergency_withdraw(env: &Env, admin: Address, group_id: u64) -> Result<() return Err(ContractError::GroupCompleted); } - // Calculate remaining balance and distribute equally let token_client = soroban_sdk::token::Client::new(env, &group.token); let contract_addr = env.current_contract_address(); let balance = token_client.balance(&contract_addr); if balance > 0 { - let per_member = balance / group.members.len() as i128; - if per_member > 0 { - for member in group.members.iter() { - token_client.transfer(&contract_addr, &member, &per_member); + // Calculate total deposits (these will be forfeited on default) + let deposits = storage::get_deposits(env, group_id); + let mut total_deposits = 0i128; + for member in group.members.iter() { + if let Some(deposit) = deposits.get(member) { + total_deposits += deposit; } } + + // Distributable balance = total balance - deposits (deposits are forfeited) + let distributable = balance - total_deposits; + + if distributable > 0 { + let per_member = distributable / group.members.len() as i128; + if per_member > 0 { + for member in group.members.iter() { + token_client.transfer(&contract_addr, &member, &per_member); + } + } + } + // Note: Deposits remain in contract (forfeited on emergency/default) } let mut group = group; diff --git a/contracts/sorosave/src/group.rs b/contracts/sorosave/src/group.rs index 5033347..337edad 100644 --- a/contracts/sorosave/src/group.rs +++ b/contracts/sorosave/src/group.rs @@ -10,6 +10,7 @@ pub fn create_group( name: String, token: Address, contribution_amount: i128, + deposit_amount: i128, cycle_length: u64, max_members: u32, ) -> Result { @@ -18,6 +19,9 @@ pub fn create_group( if contribution_amount <= 0 { return Err(ContractError::InvalidAmount); } + if deposit_amount < 0 { + return Err(ContractError::InvalidAmount); + } if max_members < 2 { return Err(ContractError::InsufficientMembers); } @@ -32,8 +36,9 @@ pub fn create_group( id: group_id, name, admin: admin.clone(), - token, + token: token.clone(), contribution_amount, + deposit_amount, cycle_length, max_members, members, @@ -47,6 +52,16 @@ pub fn create_group( storage::set_group(env, &group); storage::add_member_group(env, &admin, group_id); + // Collect deposit from admin if required + if deposit_amount > 0 { + let token_client = soroban_sdk::token::Client::new(env, &token); + token_client.transfer(&admin, &env.current_contract_address(), &deposit_amount); + + let mut deposits = storage::get_deposits(env, group_id); + deposits.set(admin.clone(), deposit_amount); + storage::set_deposits(env, group_id, &deposits); + } + env.events() .publish((crate::symbol_short!("grp_creat"),), group_id); @@ -73,6 +88,16 @@ pub fn join_group(env: &Env, member: Address, group_id: u64) -> Result<(), Contr } } + // Collect deposit if required + if group.deposit_amount > 0 { + let token_client = soroban_sdk::token::Client::new(env, &group.token); + token_client.transfer(&member, &env.current_contract_address(), &group.deposit_amount); + + let mut deposits = storage::get_deposits(env, group_id); + deposits.set(member.clone(), group.deposit_amount); + storage::set_deposits(env, group_id, &deposits); + } + group.members.push_back(member.clone()); storage::set_group(env, &group); storage::add_member_group(env, &member, group_id); @@ -111,6 +136,17 @@ pub fn leave_group(env: &Env, member: Address, group_id: u64) -> Result<(), Cont return Err(ContractError::NotMember); } + // Return deposit if member is leaving during Forming phase + if group.deposit_amount > 0 { + let mut deposits = storage::get_deposits(env, group_id); + if let Some(deposit) = deposits.get(member.clone()) { + let token_client = soroban_sdk::token::Client::new(env, &group.token); + token_client.transfer(&env.current_contract_address(), &member, &deposit); + deposits.remove(member.clone()); + storage::set_deposits(env, group_id, &deposits); + } + } + group.members = new_members; storage::set_group(env, &group); storage::remove_member_group(env, &member, group_id); diff --git a/contracts/sorosave/src/lib.rs b/contracts/sorosave/src/lib.rs index 454a6ca..a7ec813 100644 --- a/contracts/sorosave/src/lib.rs +++ b/contracts/sorosave/src/lib.rs @@ -35,6 +35,7 @@ impl SoroSaveContract { name: String, token: Address, contribution_amount: i128, + deposit_amount: i128, cycle_length: u64, max_members: u32, ) -> Result { @@ -44,6 +45,7 @@ impl SoroSaveContract { name, token, contribution_amount, + deposit_amount, cycle_length, max_members, ) diff --git a/contracts/sorosave/src/payout.rs b/contracts/sorosave/src/payout.rs index 76ed389..b261b78 100644 --- a/contracts/sorosave/src/payout.rs +++ b/contracts/sorosave/src/payout.rs @@ -40,6 +40,19 @@ pub fn distribute_payout(env: &Env, group_id: u64) -> Result<(), ContractError> group.status = GroupStatus::Completed; storage::set_group(env, &group); + // Return deposits to all members on successful completion + if group.deposit_amount > 0 { + let deposits = storage::get_deposits(env, group_id); + let token_client = soroban_sdk::token::Client::new(env, &group.token); + let contract_addr = env.current_contract_address(); + + for member in group.members.iter() { + if let Some(deposit) = deposits.get(member.clone()) { + token_client.transfer(&contract_addr, &member, &deposit); + } + } + } + env.events() .publish((crate::symbol_short!("grp_comp"),), group_id); } else { diff --git a/contracts/sorosave/src/storage.rs b/contracts/sorosave/src/storage.rs index 3f24bc8..570691d 100644 --- a/contracts/sorosave/src/storage.rs +++ b/contracts/sorosave/src/storage.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, Vec}; +use soroban_sdk::{Address, Env, Map, Vec}; use crate::types::{DataKey, Dispute, RoundInfo, SavingsGroup}; @@ -122,6 +122,22 @@ pub fn remove_dispute(env: &Env, group_id: u64) { env.storage().persistent().remove(&key); } +// --- Deposits --- + +pub fn get_deposits(env: &Env, group_id: u64) -> Map { + let key = DataKey::Deposits(group_id); + env.storage() + .persistent() + .get(&key) + .unwrap_or(Map::new(env)) +} + +pub fn set_deposits(env: &Env, group_id: u64, deposits: &Map) { + let key = DataKey::Deposits(group_id); + env.storage().persistent().set(&key, deposits); + extend_persistent_ttl(env, &key); +} + // --- TTL Management --- fn extend_instance_ttl(env: &Env) { diff --git a/contracts/sorosave/src/types.rs b/contracts/sorosave/src/types.rs index f741099..65f4254 100644 --- a/contracts/sorosave/src/types.rs +++ b/contracts/sorosave/src/types.rs @@ -20,6 +20,7 @@ pub struct SavingsGroup { pub admin: Address, pub token: Address, pub contribution_amount: i128, + pub deposit_amount: i128, pub cycle_length: u64, pub max_members: u32, pub members: Vec
, @@ -61,4 +62,5 @@ pub enum DataKey { Round(u64, u32), MemberGroups(Address), Dispute(u64), + Deposits(u64), }