diff --git a/contracts/sorosave/src/admin.rs b/contracts/sorosave/src/admin.rs index 049b6ce..d9d4c54 100644 --- a/contracts/sorosave/src/admin.rs +++ b/contracts/sorosave/src/admin.rs @@ -48,6 +48,52 @@ pub fn resume_group(env: &Env, admin: Address, group_id: u64) -> Result<(), Cont Ok(()) } +pub fn extend_deadline( + env: &Env, + admin: Address, + group_id: u64, + extra_seconds: u64, +) -> Result<(), ContractError> { + admin.require_auth(); + + let group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?; + + if admin != group.admin && admin != storage::get_admin(env) { + return Err(ContractError::Unauthorized); + } + + if group.status != GroupStatus::Active { + return Err(ContractError::GroupNotActive); + } + + if extra_seconds == 0 || extra_seconds > group.cycle_length { + return Err(ContractError::DeadlineExtensionTooLarge); + } + + let mut round_info = storage::get_round(env, group_id, group.current_round) + .ok_or(ContractError::RoundNotActive)?; + + if round_info.deadline_extensions >= 2 { + return Err(ContractError::DeadlineExtensionLimitReached); + } + + round_info.deadline += extra_seconds; + round_info.deadline_extensions += 1; + storage::set_round(env, group_id, &round_info); + + env.events().publish( + (crate::symbol_short!("dl_ext"),), + ( + group_id, + group.current_round, + round_info.deadline, + round_info.deadline_extensions, + ), + ); + + Ok(()) +} + pub fn raise_dispute( env: &Env, member: Address, diff --git a/contracts/sorosave/src/errors.rs b/contracts/sorosave/src/errors.rs index a2b9d9d..d1cc098 100644 --- a/contracts/sorosave/src/errors.rs +++ b/contracts/sorosave/src/errors.rs @@ -22,4 +22,6 @@ pub enum ContractError { InsufficientMembers = 16, RoundNotComplete = 17, GroupCompleted = 18, + DeadlineExtensionTooLarge = 19, + DeadlineExtensionLimitReached = 20, } diff --git a/contracts/sorosave/src/group.rs b/contracts/sorosave/src/group.rs index 5033347..708124a 100644 --- a/contracts/sorosave/src/group.rs +++ b/contracts/sorosave/src/group.rs @@ -153,6 +153,7 @@ pub fn start_group(env: &Env, admin: Address, group_id: u64) -> Result<(), Contr total_contributed: 0, is_complete: false, deadline: env.ledger().timestamp() + group.cycle_length, + deadline_extensions: 0, }; storage::set_round(env, group_id, &round_info); diff --git a/contracts/sorosave/src/lib.rs b/contracts/sorosave/src/lib.rs index 454a6ca..5f06983 100644 --- a/contracts/sorosave/src/lib.rs +++ b/contracts/sorosave/src/lib.rs @@ -130,6 +130,16 @@ impl SoroSaveContract { admin::resume_group(&env, admin, group_id) } + /// Extend the current round deadline. A round can be extended at most twice. + pub fn extend_deadline( + env: Env, + admin: Address, + group_id: u64, + extra_seconds: u64, + ) -> Result<(), ContractError> { + admin::extend_deadline(&env, admin, group_id, extra_seconds) + } + /// Raise a dispute on a group. pub fn raise_dispute( env: Env, diff --git a/contracts/sorosave/src/payout.rs b/contracts/sorosave/src/payout.rs index 76ed389..11626e1 100644 --- a/contracts/sorosave/src/payout.rs +++ b/contracts/sorosave/src/payout.rs @@ -53,6 +53,7 @@ pub fn distribute_payout(env: &Env, group_id: u64) -> Result<(), ContractError> total_contributed: 0, is_complete: false, deadline: env.ledger().timestamp() + group.cycle_length, + deadline_extensions: 0, }; storage::set_round(env, group_id, &new_round); diff --git a/contracts/sorosave/src/test.rs b/contracts/sorosave/src/test.rs index f1ac1ef..fe03736 100644 --- a/contracts/sorosave/src/test.rs +++ b/contracts/sorosave/src/test.rs @@ -189,6 +189,36 @@ fn test_pause_resume_group() { assert_eq!(client.get_group(&group_id).status, GroupStatus::Active); } +#[test] +fn test_extend_deadline_limits() { + let (env, admin, client, token) = setup_env(); + let group_id = create_test_group(&env, &client, &admin, &token); + + let member1 = Address::generate(&env); + client.join_group(&member1, &group_id); + client.start_group(&admin, &group_id); + + let original_deadline = client.get_round_status(&group_id, &1).deadline; + + client.extend_deadline(&admin, &group_id, &3600); + let round = client.get_round_status(&group_id, &1); + assert_eq!(round.deadline, original_deadline + 3600); + assert_eq!(round.deadline_extensions, 1); + + client.extend_deadline(&admin, &group_id, &7200); + let round = client.get_round_status(&group_id, &1); + assert_eq!(round.deadline, original_deadline + 10800); + assert_eq!(round.deadline_extensions, 2); + + assert!(client.try_extend_deadline(&admin, &group_id, &1).is_err()); + assert!(client + .try_extend_deadline(&member1, &group_id, &3600) + .is_err()); + assert!(client + .try_extend_deadline(&admin, &group_id, &86401) + .is_err()); +} + #[test] fn test_dispute_flow() { let (env, admin, client, token) = setup_env(); diff --git a/contracts/sorosave/src/types.rs b/contracts/sorosave/src/types.rs index f741099..2182380 100644 --- a/contracts/sorosave/src/types.rs +++ b/contracts/sorosave/src/types.rs @@ -40,6 +40,7 @@ pub struct RoundInfo { pub total_contributed: i128, pub is_complete: bool, pub deadline: u64, + pub deadline_extensions: u32, } /// Dispute information for a group.