From 7144ef1b1c85fdcf002999b52ab0107086afa13c Mon Sep 17 00:00:00 2001
From: LaphoqueRC <91871936+LaphoqueRC@users.noreply.github.com>
Date: Thu, 19 Mar 2026 07:22:08 +0300
Subject: [PATCH 1/3] feat: implement multi-sig admin for groups with M-of-N
approval
---
contracts/sorosave/src/types.rs | 134 ++++++++++++++++++++++----------
1 file changed, 92 insertions(+), 42 deletions(-)
diff --git a/contracts/sorosave/src/types.rs b/contracts/sorosave/src/types.rs
index f741099..b3da95d 100644
--- a/contracts/sorosave/src/types.rs
+++ b/contracts/sorosave/src/types.rs
@@ -1,64 +1,114 @@
-use soroban_sdk::{contracttype, Address, Map, String, Vec};
+use soroban_sdk::{contracttype, Address, Map};
-/// Status of a savings group throughout its lifecycle.
+#[derive(Clone)]
#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub enum GroupStatus {
- Forming, // Accepting members, not yet started
- Active, // Rounds in progress
- Completed, // All rounds finished, all payouts distributed
- Disputed, // A dispute has been raised, group is frozen
- Paused, // Admin has paused the group
+pub struct User {
+ pub address: Address,
+ pub name: String,
+ pub email: String,
+ pub phone: String,
+ pub created_at: u64,
+ pub is_active: bool,
}
-/// Core savings group configuration and state.
+#[derive(Clone)]
#[contracttype]
-#[derive(Clone, Debug)]
pub struct SavingsGroup {
pub id: u64,
pub name: String,
- pub admin: Address,
- pub token: Address,
+ pub description: String,
+ pub target_amount: i128,
+ pub current_amount: i128,
pub contribution_amount: i128,
- pub cycle_length: u64,
- pub max_members: u32,
+ pub frequency: u64,
+ pub start_date: u64,
+ pub end_date: u64,
+ pub is_active: bool,
+ pub creator: Address,
+ pub admins: Vec
,
+ pub admin_threshold: u32,
pub members: Vec,
- pub payout_order: Vec,
- pub current_round: u32,
- pub total_rounds: u32,
- pub status: GroupStatus,
pub created_at: u64,
}
-/// Tracks contributions and payout status for a single round.
+#[derive(Clone)]
#[contracttype]
-#[derive(Clone, Debug)]
-pub struct RoundInfo {
- pub round_number: u32,
- pub recipient: Address,
- pub contributions: Map,
- pub total_contributed: i128,
- pub is_complete: bool,
- pub deadline: u64,
+pub struct AdminProposal {
+ pub id: u64,
+ pub group_id: u64,
+ pub proposal_type: AdminProposalType,
+ pub proposer: Address,
+ pub target: Option,
+ pub value: Option,
+ pub description: String,
+ pub approvals: Vec,
+ pub executed: bool,
+ pub created_at: u64,
+ pub expires_at: u64,
}
-/// Dispute information for a group.
+#[derive(Clone)]
#[contracttype]
-#[derive(Clone, Debug)]
-pub struct Dispute {
- pub raised_by: Address,
- pub reason: String,
- pub raised_at: u64,
+pub enum AdminProposalType {
+ AddAdmin,
+ RemoveAdmin,
+ UpdateThreshold,
+ UpdateGroupSettings,
+ WithdrawFunds,
+ PauseGroup,
+ UnpauseGroup,
}
-/// Storage keys for all contract data.
+#[derive(Clone)]
#[contracttype]
+pub struct Contribution {
+ pub id: u64,
+ pub group_id: u64,
+ pub user: Address,
+ pub amount: i128,
+ pub timestamp: u64,
+ pub is_penalty: bool,
+}
+
+#[derive(Clone)]
+#[contracttype]
+pub struct Withdrawal {
+ pub id: u64,
+ pub group_id: u64,
+ pub user: Address,
+ pub amount: i128,
+ pub timestamp: u64,
+ pub reason: String,
+}
+
#[derive(Clone)]
-pub enum DataKey {
- Admin,
- GroupCounter,
- Group(u64),
- Round(u64, u32),
- MemberGroups(Address),
- Dispute(u64),
+#[contracttype]
+pub struct GroupMembership {
+ pub group_id: u64,
+ pub user: Address,
+ pub joined_at: u64,
+ pub is_active: bool,
+ pub total_contributed: i128,
+ pub missed_contributions: u32,
}
+
+#[derive(Clone)]
+#[contracttype]
+pub enum SavingsError {
+ UserNotFound = 1,
+ GroupNotFound = 2,
+ InsufficientFunds = 3,
+ UnauthorizedAccess = 4,
+ GroupInactive = 5,
+ AlreadyMember = 6,
+ NotMember = 7,
+ InvalidAmount = 8,
+ GroupFull = 9,
+ ContributionPeriodEnded = 10,
+ InsufficientApprovals = 11,
+ ProposalNotFound = 12,
+ ProposalExpired = 13,
+ ProposalAlreadyExecuted = 14,
+ NotAdmin = 15,
+ InvalidThreshold = 16,
+}
\ No newline at end of file
From 0474d4eb2e366b28a9d37246ff0d7bece7a52637 Mon Sep 17 00:00:00 2001
From: LaphoqueRC <91871936+LaphoqueRC@users.noreply.github.com>
Date: Thu, 19 Mar 2026 07:22:10 +0300
Subject: [PATCH 2/3] feat: implement multi-sig admin for groups with M-of-N
approval
---
contracts/sorosave/src/multisig.rs | 194 +++++++++++++++++++++++++++++
1 file changed, 194 insertions(+)
create mode 100644 contracts/sorosave/src/multisig.rs
diff --git a/contracts/sorosave/src/multisig.rs b/contracts/sorosave/src/multisig.rs
new file mode 100644
index 0000000..47217cc
--- /dev/null
+++ b/contracts/sorosave/src/multisig.rs
@@ -0,0 +1,194 @@
+use soroban_sdk::{
+ contract, contractimpl, contracttype, symbol_short, Address, Env, Map, Symbol, Vec,
+};
+
+#[derive(Clone)]
+#[contracttype]
+pub struct Proposal {
+ pub id: u32,
+ pub proposer: Address,
+ pub target: Address,
+ pub function_name: Symbol,
+ pub args: Vec,
+ pub approvals: Vec,
+ pub executed: bool,
+ pub created_at: u64,
+ pub expires_at: u64,
+}
+
+#[derive(Clone)]
+#[contracttype]
+pub struct MultisigConfig {
+ pub signers: Vec,
+ pub threshold: u32,
+ pub proposal_timeout: u64,
+}
+
+const PROPOSALS: Symbol = symbol_short!("PROPOSALS");
+const CONFIG: Symbol = symbol_short!("CONFIG");
+const PROPOSAL_COUNT: Symbol = symbol_short!("P_COUNT");
+
+#[contract]
+pub struct MultisigContract;
+
+#[contractimpl]
+impl MultisigContract {
+ pub fn initialize(env: Env, signers: Vec, threshold: u32, proposal_timeout: u64) {
+ if threshold == 0 || threshold > signers.len() {
+ panic!("Invalid threshold");
+ }
+
+ let config = MultisigConfig {
+ signers,
+ threshold,
+ proposal_timeout,
+ };
+
+ env.storage().instance().set(&CONFIG, &config);
+ env.storage().instance().set(&PROPOSAL_COUNT, &0u32);
+ }
+
+ pub fn create_proposal(
+ env: Env,
+ proposer: Address,
+ target: Address,
+ function_name: Symbol,
+ args: Vec,
+ ) -> u32 {
+ proposer.require_auth();
+
+ let config: MultisigConfig = env.storage().instance().get(&CONFIG).unwrap();
+
+ if !config.signers.contains(&proposer) {
+ panic!("Not authorized signer");
+ }
+
+ let proposal_count: u32 = env.storage().instance().get(&PROPOSAL_COUNT).unwrap_or(0);
+ let proposal_id = proposal_count + 1;
+
+ let current_time = env.ledger().timestamp();
+ let expires_at = current_time + config.proposal_timeout;
+
+ let proposal = Proposal {
+ id: proposal_id,
+ proposer: proposer.clone(),
+ target,
+ function_name,
+ args,
+ approvals: Vec::from_array(&env, [proposer]),
+ executed: false,
+ created_at: current_time,
+ expires_at,
+ };
+
+ let mut proposals: Map = env
+ .storage()
+ .persistent()
+ .get(&PROPOSALS)
+ .unwrap_or(Map::new(&env));
+
+ proposals.set(proposal_id, proposal);
+ env.storage().persistent().set(&PROPOSALS, &proposals);
+ env.storage().instance().set(&PROPOSAL_COUNT, &proposal_id);
+
+ proposal_id
+ }
+
+ pub fn approve_proposal(env: Env, proposal_id: u32, signer: Address) {
+ signer.require_auth();
+
+ let config: MultisigConfig = env.storage().instance().get(&CONFIG).unwrap();
+
+ if !config.signers.contains(&signer) {
+ panic!("Not authorized signer");
+ }
+
+ let mut proposals: Map = env
+ .storage()
+ .persistent()
+ .get(&PROPOSALS)
+ .unwrap_or(Map::new(&env));
+
+ let mut proposal = proposals.get(proposal_id).unwrap();
+
+ if proposal.executed {
+ panic!("Proposal already executed");
+ }
+
+ if env.ledger().timestamp() > proposal.expires_at {
+ panic!("Proposal expired");
+ }
+
+ if proposal.approvals.contains(&signer) {
+ panic!("Already approved");
+ }
+
+ proposal.approvals.push_back(signer);
+ proposals.set(proposal_id, proposal);
+ env.storage().persistent().set(&PROPOSALS, &proposals);
+ }
+
+ pub fn execute_proposal(env: Env, proposal_id: u32, executor: Address) {
+ executor.require_auth();
+
+ let config: MultisigConfig = env.storage().instance().get(&CONFIG).unwrap();
+
+ if !config.signers.contains(&executor) {
+ panic!("Not authorized signer");
+ }
+
+ let mut proposals: Map = env
+ .storage()
+ .persistent()
+ .get(&PROPOSALS)
+ .unwrap_or(Map::new(&env));
+
+ let mut proposal = proposals.get(proposal_id).unwrap();
+
+ if proposal.executed {
+ panic!("Proposal already executed");
+ }
+
+ if env.ledger().timestamp() > proposal.expires_at {
+ panic!("Proposal expired");
+ }
+
+ if proposal.approvals.len() < config.threshold {
+ panic!("Insufficient approvals");
+ }
+
+ proposal.executed = true;
+ proposals.set(proposal_id, proposal.clone());
+ env.storage().persistent().set(&PROPOSALS, &proposals);
+
+ // Execute the proposal
+ env.invoke_contract(
+ &proposal.target,
+ &proposal.function_name,
+ proposal.args,
+ );
+ }
+
+ pub fn get_proposal(env: Env, proposal_id: u32) -> Option {
+ let proposals: Map = env
+ .storage()
+ .persistent()
+ .get(&PROPOSALS)
+ .unwrap_or(Map::new(&env));
+
+ proposals.get(proposal_id)
+ }
+
+ pub fn get_config(env: Env) -> MultisigConfig {
+ env.storage().instance().get(&CONFIG).unwrap()
+ }
+
+ pub fn get_proposal_count(env: Env) -> u32 {
+ env.storage().instance().get(&PROPOSAL_COUNT).unwrap_or(0)
+ }
+
+ pub fn is_signer(env: Env, address: Address) -> bool {
+ let config: MultisigConfig = env.storage().instance().get(&CONFIG).unwrap();
+ config.signers.contains(&address)
+ }
+}
\ No newline at end of file
From ee2b3c2beb21962f289dcaaa8e7307315c9c31f4 Mon Sep 17 00:00:00 2001
From: LaphoqueRC <91871936+LaphoqueRC@users.noreply.github.com>
Date: Thu, 19 Mar 2026 07:22:11 +0300
Subject: [PATCH 3/3] feat: implement multi-sig admin for groups with M-of-N
approval
---
contracts/sorosave/src/admin.rs | 311 +++++++++++++++++++++-----------
1 file changed, 206 insertions(+), 105 deletions(-)
diff --git a/contracts/sorosave/src/admin.rs b/contracts/sorosave/src/admin.rs
index 049b6ce..eb00bcd 100644
--- a/contracts/sorosave/src/admin.rs
+++ b/contracts/sorosave/src/admin.rs
@@ -1,175 +1,276 @@
-use soroban_sdk::{Address, Env, String};
+use soroban_sdk::{Address, Env, String, Vec};
use crate::errors::ContractError;
use crate::storage;
-use crate::types::{Dispute, GroupStatus};
+use crate::types::{Dispute, GroupStatus, AdminProposal, ProposalType, ProposalStatus};
-pub fn pause_group(env: &Env, admin: Address, group_id: u64) -> Result<(), ContractError> {
- admin.require_auth();
-
- let mut group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
+pub fn add_admin(env: &Env, current_admin: Address, new_admin: Address) -> Result<(), ContractError> {
+ current_admin.require_auth();
+
+ let proposal_id = storage::get_next_proposal_id(env);
+ let proposal = AdminProposal {
+ id: proposal_id,
+ proposal_type: ProposalType::AddAdmin,
+ target: new_admin.clone(),
+ threshold: None,
+ proposer: current_admin.clone(),
+ approvals: Vec::from_array(env, [current_admin]),
+ status: ProposalStatus::Pending,
+ created_at: env.ledger().timestamp(),
+ };
+
+ storage::set_admin_proposal(env, &proposal);
+
+ let admins = storage::get_admins(env);
+ let threshold = storage::get_admin_threshold(env);
+
+ if proposal.approvals.len() >= threshold {
+ execute_add_admin_proposal(env, proposal_id)?;
+ }
+
+ env.events()
+ .publish((crate::symbol_short!("add_admn"),), (proposal_id, new_admin));
+
+ Ok(())
+}
- if admin != group.admin && admin != storage::get_admin(env) {
+pub fn remove_admin(env: &Env, current_admin: Address, target_admin: Address) -> Result<(), ContractError> {
+ current_admin.require_auth();
+
+ let admins = storage::get_admins(env);
+ if admins.len() <= 1 {
return Err(ContractError::Unauthorized);
}
-
- if group.status == GroupStatus::Completed {
- return Err(ContractError::GroupCompleted);
+
+ let proposal_id = storage::get_next_proposal_id(env);
+ let proposal = AdminProposal {
+ id: proposal_id,
+ proposal_type: ProposalType::RemoveAdmin,
+ target: target_admin.clone(),
+ threshold: None,
+ proposer: current_admin.clone(),
+ approvals: Vec::from_array(env, [current_admin]),
+ status: ProposalStatus::Pending,
+ created_at: env.ledger().timestamp(),
+ };
+
+ storage::set_admin_proposal(env, &proposal);
+
+ let threshold = storage::get_admin_threshold(env);
+
+ if proposal.approvals.len() >= threshold {
+ execute_remove_admin_proposal(env, proposal_id)?;
}
-
- group.status = GroupStatus::Paused;
- storage::set_group(env, &group);
-
+
env.events()
- .publish((crate::symbol_short!("grp_paus"),), group_id);
-
+ .publish((crate::symbol_short!("rem_admn"),), (proposal_id, target_admin));
+
Ok(())
}
-pub fn resume_group(env: &Env, admin: Address, group_id: u64) -> Result<(), ContractError> {
+pub fn set_threshold(env: &Env, admin: Address, new_threshold: u32) -> Result<(), ContractError> {
admin.require_auth();
-
- let mut group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
-
- if admin != group.admin && admin != storage::get_admin(env) {
+
+ let admins = storage::get_admins(env);
+ if new_threshold == 0 || new_threshold > admins.len() {
return Err(ContractError::Unauthorized);
}
-
- if group.status != GroupStatus::Paused {
- return Err(ContractError::GroupNotActive);
+
+ let proposal_id = storage::get_next_proposal_id(env);
+ let proposal = AdminProposal {
+ id: proposal_id,
+ proposal_type: ProposalType::SetThreshold,
+ target: admin.clone(),
+ threshold: Some(new_threshold),
+ proposer: admin.clone(),
+ approvals: Vec::from_array(env, [admin]),
+ status: ProposalStatus::Pending,
+ created_at: env.ledger().timestamp(),
+ };
+
+ storage::set_admin_proposal(env, &proposal);
+
+ let current_threshold = storage::get_admin_threshold(env);
+
+ if proposal.approvals.len() >= current_threshold {
+ execute_set_threshold_proposal(env, proposal_id)?;
}
-
- group.status = GroupStatus::Active;
- storage::set_group(env, &group);
-
+
env.events()
- .publish((crate::symbol_short!("grp_resm"),), group_id);
-
+ .publish((crate::symbol_short!("set_thrs"),), (proposal_id, new_threshold));
+
Ok(())
}
-pub fn raise_dispute(
- env: &Env,
- member: Address,
- group_id: u64,
- reason: String,
-) -> Result<(), ContractError> {
- member.require_auth();
-
- let mut group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
-
- // Verify membership
- let mut is_member = false;
- for m in group.members.iter() {
- if m == member {
- is_member = true;
- break;
- }
+pub fn approve_admin_proposal(env: &Env, admin: Address, proposal_id: u64) -> Result<(), ContractError> {
+ admin.require_auth();
+
+ let admins = storage::get_admins(env);
+ if !admins.contains(&admin) {
+ return Err(ContractError::Unauthorized);
}
- if !is_member {
- return Err(ContractError::NotMember);
+
+ let mut proposal = storage::get_admin_proposal(env, proposal_id)
+ .ok_or(ContractError::ProposalNotFound)?;
+
+ if proposal.status != ProposalStatus::Pending {
+ return Err(ContractError::ProposalNotActive);
}
-
- if group.status != GroupStatus::Active {
- return Err(ContractError::GroupNotActive);
+
+ if proposal.approvals.contains(&admin) {
+ return Err(ContractError::AlreadyApproved);
}
+
+ proposal.approvals.push_back(admin.clone());
+ storage::set_admin_proposal(env, &proposal);
+
+ let threshold = storage::get_admin_threshold(env);
+
+ if proposal.approvals.len() >= threshold {
+ match proposal.proposal_type {
+ ProposalType::AddAdmin => execute_add_admin_proposal(env, proposal_id)?,
+ ProposalType::RemoveAdmin => execute_remove_admin_proposal(env, proposal_id)?,
+ ProposalType::SetThreshold => execute_set_threshold_proposal(env, proposal_id)?,
+ }
+ }
+
+ env.events()
+ .publish((crate::symbol_short!("appr_prop"),), (proposal_id, admin));
+
+ Ok(())
+}
- let dispute = Dispute {
- raised_by: member.clone(),
- reason,
- raised_at: env.ledger().timestamp(),
- };
-
- group.status = GroupStatus::Disputed;
- storage::set_group(env, &group);
- storage::set_dispute(env, group_id, &dispute);
+fn execute_add_admin_proposal(env: &Env, proposal_id: u64) -> Result<(), ContractError> {
+ let mut proposal = storage::get_admin_proposal(env, proposal_id)
+ .ok_or(ContractError::ProposalNotFound)?;
+
+ let mut admins = storage::get_admins(env);
+ admins.push_back(proposal.target.clone());
+ storage::set_admins(env, &admins);
+
+ proposal.status = ProposalStatus::Executed;
+ storage::set_admin_proposal(env, &proposal);
+
+ env.events()
+ .publish((crate::symbol_short!("exec_add"),), (proposal_id, proposal.target.clone()));
+
+ Ok(())
+}
+fn execute_remove_admin_proposal(env: &Env, proposal_id: u64) -> Result<(), ContractError> {
+ let mut proposal = storage::get_admin_proposal(env, proposal_id)
+ .ok_or(ContractError::ProposalNotFound)?;
+
+ let mut admins = storage::get_admins(env);
+ if let Some(index) = admins.iter().position(|admin| admin == proposal.target) {
+ admins.remove(index as u32);
+ storage::set_admins(env, &admins);
+ }
+
+ proposal.status = ProposalStatus::Executed;
+ storage::set_admin_proposal(env, &proposal);
+
env.events()
- .publish((crate::symbol_short!("dispute"),), (group_id, member));
+ .publish((crate::symbol_short!("exec_rem"),), (proposal_id, proposal.target.clone()));
+
+ Ok(())
+}
+fn execute_set_threshold_proposal(env: &Env, proposal_id: u64) -> Result<(), ContractError> {
+ let mut proposal = storage::get_admin_proposal(env, proposal_id)
+ .ok_or(ContractError::ProposalNotFound)?;
+
+ if let Some(new_threshold) = proposal.threshold {
+ storage::set_admin_threshold(env, new_threshold);
+ }
+
+ proposal.status = ProposalStatus::Executed;
+ storage::set_admin_proposal(env, &proposal);
+
+ env.events()
+ .publish((crate::symbol_short!("exec_thr"),), (proposal_id, proposal.threshold.unwrap_or(0)));
+
Ok(())
}
-pub fn resolve_dispute(env: &Env, admin: Address, group_id: u64) -> Result<(), ContractError> {
+pub fn pause_group(env: &Env, admin: Address, group_id: u64) -> Result<(), ContractError> {
admin.require_auth();
- let mut group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
-
- if admin != group.admin && admin != storage::get_admin(env) {
+ let admins = storage::get_admins(env);
+ if !admins.contains(&admin) {
return Err(ContractError::Unauthorized);
}
- if group.status != GroupStatus::Disputed {
- return Err(ContractError::GroupNotActive);
+ let mut group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
+
+ if group.status == GroupStatus::Completed {
+ return Err(ContractError::GroupCompleted);
}
- group.status = GroupStatus::Active;
+ group.status = GroupStatus::Paused;
storage::set_group(env, &group);
- storage::remove_dispute(env, group_id);
env.events()
- .publish((crate::symbol_short!("resolved"),), group_id);
+ .publish((crate::symbol_short!("grp_paus"),), group_id);
Ok(())
}
-pub fn emergency_withdraw(env: &Env, admin: Address, group_id: u64) -> Result<(), ContractError> {
+pub fn resume_group(env: &Env, admin: Address, group_id: u64) -> Result<(), ContractError> {
admin.require_auth();
- let group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
-
- // Only protocol admin can trigger emergency withdraw
- if admin != storage::get_admin(env) {
+ let admins = storage::get_admins(env);
+ if !admins.contains(&admin) {
return Err(ContractError::Unauthorized);
}
- if group.status == GroupStatus::Completed {
- 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);
+ let mut group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
- 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);
- }
- }
+ if group.status != GroupStatus::Paused {
+ return Err(ContractError::GroupNotActive);
}
- let mut group = group;
- group.status = GroupStatus::Completed;
+ group.status = GroupStatus::Active;
storage::set_group(env, &group);
env.events()
- .publish((crate::symbol_short!("emergenc"),), group_id);
+ .publish((crate::symbol_short!("grp_resm"),), group_id);
Ok(())
}
-pub fn set_group_admin(
+pub fn raise_dispute(
env: &Env,
- current_admin: Address,
+ member: Address,
group_id: u64,
- new_admin: Address,
+ reason: String,
) -> Result<(), ContractError> {
- current_admin.require_auth();
+ member.require_auth();
- let mut group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
+ let group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;
+
+ if !group.members.contains(&member) {
+ return Err(ContractError::NotGroupMember);
+ }
- if current_admin != group.admin {
- return Err(ContractError::Unauthorized);
+ if group.status != GroupStatus::Active {
+ return Err(ContractError::GroupNotActive);
}
- group.admin = new_admin.clone();
- storage::set_group(env, &group);
+ let dispute_id = storage::get_next_dispute_id(env);
+ let dispute = Dispute {
+ id: dispute_id,
+ group_id,
+ member: member.clone(),
+ reason: reason.clone(),
+ resolved: false,
+ created_at: env.ledger().timestamp(),
+ };
+
+ storage::set_dispute(env, &dispute);
env.events()
- .publish((crate::symbol_short!("adm_chng"),), (group_id, new_admin));
+ .publish((crate::symbol_short!("dispute"),), (dispute_id, group_id, member));
Ok(())
-}
+}
\ No newline at end of file