From c7cf789c89f5768d31e2b4c5078ab178eb5f61e8 Mon Sep 17 00:00:00 2001 From: Ikem Peter Date: Sun, 22 Feb 2026 12:54:53 +0100 Subject: [PATCH] refactor: replace panics with structured contract errors and return `Result` types. --- savings_goals/src/lib.rs | 283 ++++++++++++------ savings_goals/src/test.rs | 36 +-- .../test/test_add_to_non_existent_goal.1.json | 58 +--- .../test_lock_goal_unauthorized_panics.1.json | 52 +--- .../test_lock_nonexistent_goal_panics.1.json | 52 +--- ...est_unlock_goal_unauthorized_panics.1.json | 52 +--- .../test_withdraw_after_lock_fails.1.json | 58 +--- .../test/test_withdraw_locked.1.json | 58 +--- ...draw_time_locked_goal_before_unlock.1.json | 31 +- .../test/test_withdraw_too_much.1.json | 58 +--- .../test/test_withdraw_unauthorized.1.json | 58 +--- .../test/test_zero_amount_fails.1.json | 61 +--- 12 files changed, 311 insertions(+), 546 deletions(-) diff --git a/savings_goals/src/lib.rs b/savings_goals/src/lib.rs index 8d1fd7c..8455a66 100644 --- a/savings_goals/src/lib.rs +++ b/savings_goals/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] use soroban_sdk::{ - contract, contractimpl, contracttype, symbol_short, Address, Env, Map, String, Symbol, Vec, + contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Map, String, + Symbol, Vec, }; // Event topics @@ -41,6 +42,32 @@ pub struct GoalCompletedEvent { const INSTANCE_LIFETIME_THRESHOLD: u32 = 17280; // ~1 day const INSTANCE_BUMP_AMOUNT: u32 = 518400; // ~30 days +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum SavingsGoalError { + ContractPaused = 1, + FunctionPaused = 2, + Unauthorized = 3, + TimeLockedUnpauseNotReached = 4, + TargetAmountMustBePositive = 5, + AmountMustBePositive = 6, + GoalNotFound = 7, + BatchTooLarge = 8, + NotOwnerOfAllGoals = 9, + BatchValidationFailed = 10, + GoalLocked = 11, + TimeLocked = 12, + InsufficientBalance = 13, + UnsupportedSnapshotVersion = 14, + SnapshotChecksumMismatch = 15, + InvalidNonce = 16, + UnlockDateMustBeInFuture = 17, + NextDueDateMustBeInFuture = 18, + ScheduleNotFound = 19, + ArithmeticError = 20, +} + /// Savings goal data structure with owner tracking for access control #[contract] pub struct SavingsGoalContract; @@ -173,53 +200,60 @@ impl SavingsGoalContract { .get(func) .unwrap_or(false) } - fn require_not_paused(env: &Env, func: Symbol) { + fn require_not_paused(env: &Env, func: Symbol) -> Result<(), SavingsGoalError> { if Self::get_global_paused(env) { - panic!("Contract is paused"); + return Err(SavingsGoalError::ContractPaused); } if Self::is_function_paused(env, func) { - panic!("Function is paused"); + return Err(SavingsGoalError::FunctionPaused); } + Ok(()) } - pub fn set_pause_admin(env: Env, caller: Address, new_admin: Address) { + pub fn set_pause_admin( + env: Env, + caller: Address, + new_admin: Address, + ) -> Result<(), SavingsGoalError> { caller.require_auth(); let current = Self::get_pause_admin(&env); match current { None => { if caller != new_admin { - panic!("Unauthorized"); + return Err(SavingsGoalError::Unauthorized); } } - Some(admin) if admin != caller => panic!("Unauthorized"), + Some(admin) if admin != caller => return Err(SavingsGoalError::Unauthorized), _ => {} } env.storage() .instance() .set(&symbol_short!("PAUSE_ADM"), &new_admin); + Ok(()) } - pub fn pause(env: Env, caller: Address) { + pub fn pause(env: Env, caller: Address) -> Result<(), SavingsGoalError> { caller.require_auth(); - let admin = Self::get_pause_admin(&env).expect("No pause admin set"); + let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalError::Unauthorized)?; if admin != caller { - panic!("Unauthorized"); + return Err(SavingsGoalError::Unauthorized); } env.storage() .instance() .set(&symbol_short!("PAUSED"), &true); env.events() .publish((symbol_short!("savings"), symbol_short!("paused")), ()); + Ok(()) } - pub fn unpause(env: Env, caller: Address) { + pub fn unpause(env: Env, caller: Address) -> Result<(), SavingsGoalError> { caller.require_auth(); - let admin = Self::get_pause_admin(&env).expect("No pause admin set"); + let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalError::Unauthorized)?; if admin != caller { - panic!("Unauthorized"); + return Err(SavingsGoalError::Unauthorized); } let unpause_at: Option = env.storage().instance().get(&symbol_short!("UNP_AT")); if let Some(at) = unpause_at { if env.ledger().timestamp() < at { - panic!("Time-locked unpause not yet reached"); + return Err(SavingsGoalError::TimeLockedUnpauseNotReached); } env.storage().instance().remove(&symbol_short!("UNP_AT")); } @@ -228,12 +262,13 @@ impl SavingsGoalContract { .set(&symbol_short!("PAUSED"), &false); env.events() .publish((symbol_short!("savings"), symbol_short!("unpaused")), ()); + Ok(()) } - pub fn pause_function(env: Env, caller: Address, func: Symbol) { + pub fn pause_function(env: Env, caller: Address, func: Symbol) -> Result<(), SavingsGoalError> { caller.require_auth(); - let admin = Self::get_pause_admin(&env).expect("No pause admin set"); + let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalError::Unauthorized)?; if admin != caller { - panic!("Unauthorized"); + return Err(SavingsGoalError::Unauthorized); } let mut m: Map = env .storage() @@ -244,12 +279,17 @@ impl SavingsGoalContract { env.storage() .instance() .set(&symbol_short!("PAUSED_FN"), &m); + Ok(()) } - pub fn unpause_function(env: Env, caller: Address, func: Symbol) { + pub fn unpause_function( + env: Env, + caller: Address, + func: Symbol, + ) -> Result<(), SavingsGoalError> { caller.require_auth(); - let admin = Self::get_pause_admin(&env).expect("No pause admin set"); + let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalError::Unauthorized)?; if admin != caller { - panic!("Unauthorized"); + return Err(SavingsGoalError::Unauthorized); } let mut m: Map = env .storage() @@ -260,6 +300,7 @@ impl SavingsGoalContract { env.storage() .instance() .set(&symbol_short!("PAUSED_FN"), &m); + Ok(()) } pub fn is_paused(env: Env) -> bool { Self::get_global_paused(&env) @@ -273,27 +314,36 @@ impl SavingsGoalContract { fn get_upgrade_admin(env: &Env) -> Option
{ env.storage().instance().get(&symbol_short!("UPG_ADM")) } - pub fn set_upgrade_admin(env: Env, caller: Address, new_admin: Address) { + pub fn set_upgrade_admin( + env: Env, + caller: Address, + new_admin: Address, + ) -> Result<(), SavingsGoalError> { caller.require_auth(); let current = Self::get_upgrade_admin(&env); match current { None => { if caller != new_admin { - panic!("Unauthorized"); + return Err(SavingsGoalError::Unauthorized); } } - Some(adm) if adm != caller => panic!("Unauthorized"), + Some(adm) if adm != caller => return Err(SavingsGoalError::Unauthorized), _ => {} } env.storage() .instance() .set(&symbol_short!("UPG_ADM"), &new_admin); + Ok(()) } - pub fn set_version(env: Env, caller: Address, new_version: u32) { + pub fn set_version( + env: Env, + caller: Address, + new_version: u32, + ) -> Result<(), SavingsGoalError> { caller.require_auth(); - let admin = Self::get_upgrade_admin(&env).expect("No upgrade admin set"); + let admin = Self::get_upgrade_admin(&env).ok_or(SavingsGoalError::Unauthorized)?; if admin != caller { - panic!("Unauthorized"); + return Err(SavingsGoalError::Unauthorized); } let prev = Self::get_version(env.clone()); env.storage() @@ -303,6 +353,7 @@ impl SavingsGoalContract { (symbol_short!("savings"), symbol_short!("upgraded")), (prev, new_version), ); + Ok(()) } /// Create a new savings goal @@ -325,15 +376,15 @@ impl SavingsGoalContract { name: String, target_amount: i128, target_date: u64, - ) -> u32 { + ) -> Result { // Access control: require owner authorization owner.require_auth(); - Self::require_not_paused(&env, pause_functions::CREATE_GOAL); + Self::require_not_paused(&env, pause_functions::CREATE_GOAL)?; // Input validation if target_amount <= 0 { Self::append_audit(&env, symbol_short!("create"), &owner, false); - panic!("Target amount must be positive"); + return Err(SavingsGoalError::TargetAmountMustBePositive); } // Extend storage TTL @@ -386,7 +437,7 @@ impl SavingsGoalContract { (next_id, owner), ); - next_id + Ok(next_id) } /// Add funds to a savings goal @@ -403,15 +454,20 @@ impl SavingsGoalContract { /// - If caller is not the goal owner /// - If goal is not found /// - If amount is not positive - pub fn add_to_goal(env: Env, caller: Address, goal_id: u32, amount: i128) -> i128 { + pub fn add_to_goal( + env: Env, + caller: Address, + goal_id: u32, + amount: i128, + ) -> Result { // Access control: require caller authorization caller.require_auth(); - Self::require_not_paused(&env, pause_functions::ADD_TO_GOAL); + Self::require_not_paused(&env, pause_functions::ADD_TO_GOAL)?; // Input validation if amount <= 0 { Self::append_audit(&env, symbol_short!("add"), &caller, false); - panic!("Amount must be positive"); + return Err(SavingsGoalError::AmountMustBePositive); } // Extend storage TTL @@ -427,17 +483,20 @@ impl SavingsGoalContract { Some(g) => g, None => { Self::append_audit(&env, symbol_short!("add"), &caller, false); - panic!("Goal not found"); + return Err(SavingsGoalError::GoalNotFound); } }; // Access control: verify caller is the owner if goal.owner != caller { Self::append_audit(&env, symbol_short!("add"), &caller, false); - panic!("Goal not found"); + return Err(SavingsGoalError::GoalNotFound); } - goal.current_amount = goal.current_amount.checked_add(amount).expect("overflow"); + goal.current_amount = goal + .current_amount + .checked_add(amount) + .ok_or(SavingsGoalError::ArithmeticError)?; let new_total = goal.current_amount; let was_completed = new_total >= goal.target_amount; let previously_completed = (new_total - amount) >= goal.target_amount; @@ -481,7 +540,7 @@ impl SavingsGoalContract { ); } - new_total + Ok(new_total) } /// Batch add to multiple goals (atomic). Caller must be owner of all goals. @@ -489,11 +548,11 @@ impl SavingsGoalContract { env: Env, caller: Address, contributions: Vec, - ) -> u32 { + ) -> Result { caller.require_auth(); - Self::require_not_paused(&env, pause_functions::ADD_TO_GOAL); + Self::require_not_paused(&env, pause_functions::ADD_TO_GOAL)?; if contributions.len() as u32 > MAX_BATCH_SIZE { - panic!("Batch too large"); + return Err(SavingsGoalError::BatchTooLarge); } let goals_map: Map = env .storage() @@ -502,11 +561,13 @@ impl SavingsGoalContract { .unwrap_or_else(|| Map::new(&env)); for item in contributions.iter() { if item.amount <= 0 { - panic!("Amount must be positive"); + return Err(SavingsGoalError::AmountMustBePositive); } - let goal = goals_map.get(item.goal_id).expect("Goal not found"); + let goal = goals_map + .get(item.goal_id) + .ok_or(SavingsGoalError::GoalNotFound)?; if goal.owner != caller { - panic!("Not owner of all goals"); + return Err(SavingsGoalError::NotOwnerOfAllGoals); } } Self::extend_instance_ttl(&env); @@ -517,14 +578,16 @@ impl SavingsGoalContract { .unwrap_or_else(|| Map::new(&env)); let mut count = 0u32; for item in contributions.iter() { - let mut goal = goals.get(item.goal_id).expect("Goal not found"); + let mut goal = goals + .get(item.goal_id) + .ok_or(SavingsGoalError::GoalNotFound)?; if goal.owner != caller { - panic!("Batch validation failed"); + return Err(SavingsGoalError::BatchValidationFailed); } goal.current_amount = goal .current_amount .checked_add(item.amount) - .expect("overflow"); + .ok_or(SavingsGoalError::ArithmeticError)?; let new_total = goal.current_amount; let was_completed = new_total >= goal.target_amount; let previously_completed = (new_total - item.amount) >= goal.target_amount; @@ -564,7 +627,7 @@ impl SavingsGoalContract { (symbol_short!("savings"), symbol_short!("batch_add")), (count, caller), ); - count + Ok(count) } /// Withdraw funds from a savings goal @@ -584,15 +647,20 @@ impl SavingsGoalContract { /// - If unlock_date is set and not yet reached /// - If amount is not positive /// - If amount exceeds current balance - pub fn withdraw_from_goal(env: Env, caller: Address, goal_id: u32, amount: i128) -> i128 { + pub fn withdraw_from_goal( + env: Env, + caller: Address, + goal_id: u32, + amount: i128, + ) -> Result { // Access control: require caller authorization caller.require_auth(); - Self::require_not_paused(&env, pause_functions::WITHDRAW); + Self::require_not_paused(&env, pause_functions::WITHDRAW)?; // Input validation if amount <= 0 { Self::append_audit(&env, symbol_short!("withdraw"), &caller, false); - panic!("Amount must be positive"); + return Err(SavingsGoalError::AmountMustBePositive); } // Extend storage TTL @@ -608,20 +676,20 @@ impl SavingsGoalContract { Some(g) => g, None => { Self::append_audit(&env, symbol_short!("withdraw"), &caller, false); - panic!("Goal not found"); + return Err(SavingsGoalError::GoalNotFound); } }; // Access control: verify caller is the owner if goal.owner != caller { Self::append_audit(&env, symbol_short!("withdraw"), &caller, false); - panic!("Only the goal owner can withdraw funds"); + return Err(SavingsGoalError::Unauthorized); } // Check if goal is locked if goal.locked { Self::append_audit(&env, symbol_short!("withdraw"), &caller, false); - panic!("Cannot withdraw from a locked goal"); + return Err(SavingsGoalError::GoalLocked); } // Check time-lock @@ -629,17 +697,20 @@ impl SavingsGoalContract { let current_time = env.ledger().timestamp(); if current_time < unlock_date { Self::append_audit(&env, symbol_short!("withdraw"), &caller, false); - panic!("Goal is time-locked until unlock date"); + return Err(SavingsGoalError::TimeLocked); } } - // Check sufficient balance + // Check sufficient balance // NOTE: added check for target vs Amount is not needed if amount > goal.current_amount { Self::append_audit(&env, symbol_short!("withdraw"), &caller, false); - panic!("Insufficient balance"); + return Err(SavingsGoalError::InsufficientBalance); } - goal.current_amount = goal.current_amount.checked_sub(amount).expect("underflow"); + goal.current_amount = goal + .current_amount + .checked_sub(amount) + .ok_or(SavingsGoalError::ArithmeticError)?; let new_amount = goal.current_amount; goals.set(goal_id, goal); @@ -653,7 +724,7 @@ impl SavingsGoalContract { (goal_id, caller, amount), ); - new_amount + Ok(new_amount) } /// Lock a savings goal (prevent withdrawals) @@ -665,9 +736,9 @@ impl SavingsGoalContract { /// # Panics /// - If caller is not the goal owner /// - If goal is not found - pub fn lock_goal(env: Env, caller: Address, goal_id: u32) -> bool { + pub fn lock_goal(env: Env, caller: Address, goal_id: u32) -> Result { caller.require_auth(); - Self::require_not_paused(&env, pause_functions::LOCK); + Self::require_not_paused(&env, pause_functions::LOCK)?; Self::extend_instance_ttl(&env); let mut goals: Map = env @@ -680,13 +751,13 @@ impl SavingsGoalContract { Some(g) => g, None => { Self::append_audit(&env, symbol_short!("lock"), &caller, false); - panic!("Goal not found"); + return Err(SavingsGoalError::GoalNotFound); } }; if goal.owner != caller { Self::append_audit(&env, symbol_short!("lock"), &caller, false); - panic!("Only the goal owner can lock this goal"); + return Err(SavingsGoalError::Unauthorized); } goal.locked = true; @@ -701,7 +772,7 @@ impl SavingsGoalContract { (goal_id, caller), ); - true + Ok(true) } /// Unlock a savings goal (allow withdrawals) @@ -713,9 +784,9 @@ impl SavingsGoalContract { /// # Panics /// - If caller is not the goal owner /// - If goal is not found - pub fn unlock_goal(env: Env, caller: Address, goal_id: u32) -> bool { + pub fn unlock_goal(env: Env, caller: Address, goal_id: u32) -> Result { caller.require_auth(); - Self::require_not_paused(&env, pause_functions::UNLOCK); + Self::require_not_paused(&env, pause_functions::UNLOCK)?; Self::extend_instance_ttl(&env); let mut goals: Map = env @@ -728,13 +799,13 @@ impl SavingsGoalContract { Some(g) => g, None => { Self::append_audit(&env, symbol_short!("unlock"), &caller, false); - panic!("Goal not found"); + return Err(SavingsGoalError::GoalNotFound); } }; if goal.owner != caller { Self::append_audit(&env, symbol_short!("unlock"), &caller, false); - panic!("Only the goal owner can unlock this goal"); + return Err(SavingsGoalError::Unauthorized); } goal.locked = false; @@ -749,7 +820,7 @@ impl SavingsGoalContract { (goal_id, caller), ); - true + Ok(true) } /// Get a savings goal by ID @@ -849,19 +920,19 @@ impl SavingsGoalContract { caller: Address, nonce: u64, snapshot: GoalsExportSnapshot, - ) -> bool { + ) -> Result { caller.require_auth(); - Self::require_nonce(&env, &caller, nonce); + Self::require_nonce(&env, &caller, nonce)?; if snapshot.version != SNAPSHOT_VERSION { Self::append_audit(&env, symbol_short!("import"), &caller, false); - panic!("Unsupported snapshot version"); + return Err(SavingsGoalError::UnsupportedSnapshotVersion); } let expected = Self::compute_goals_checksum(snapshot.version, snapshot.next_id, &snapshot.goals); if snapshot.checksum != expected { Self::append_audit(&env, symbol_short!("import"), &caller, false); - panic!("Snapshot checksum mismatch"); + return Err(SavingsGoalError::SnapshotChecksumMismatch); } Self::extend_instance_ttl(&env); @@ -878,7 +949,7 @@ impl SavingsGoalContract { Self::increment_nonce(&env, &caller); Self::append_audit(&env, symbol_short!("import"), &caller, true); - true + Ok(true) } /// Return recent audit log entries. @@ -900,11 +971,12 @@ impl SavingsGoalContract { out } - fn require_nonce(env: &Env, address: &Address, expected: u64) { + fn require_nonce(env: &Env, address: &Address, expected: u64) -> Result<(), SavingsGoalError> { let current = Self::get_nonce(env.clone(), address.clone()); if expected != current { - panic!("Invalid nonce: expected {}, got {}", current, expected); + return Err(SavingsGoalError::InvalidNonce); } + Ok(()) } fn increment_nonce(env: &Env, address: &Address) { @@ -967,7 +1039,12 @@ impl SavingsGoalContract { } /// Set time-lock on a goal - pub fn set_time_lock(env: Env, caller: Address, goal_id: u32, unlock_date: u64) -> bool { + pub fn set_time_lock( + env: Env, + caller: Address, + goal_id: u32, + unlock_date: u64, + ) -> Result { caller.require_auth(); Self::extend_instance_ttl(&env); @@ -981,19 +1058,19 @@ impl SavingsGoalContract { Some(g) => g, None => { Self::append_audit(&env, symbol_short!("timelock"), &caller, false); - panic!("Goal not found"); + return Err(SavingsGoalError::GoalNotFound); } }; if goal.owner != caller { Self::append_audit(&env, symbol_short!("timelock"), &caller, false); - panic!("Only the goal owner can set time-lock"); + return Err(SavingsGoalError::Unauthorized); } let current_time = env.ledger().timestamp(); if unlock_date <= current_time { Self::append_audit(&env, symbol_short!("timelock"), &caller, false); - panic!("Unlock date must be in the future"); + return Err(SavingsGoalError::UnlockDateMustBeInFuture); } goal.unlock_date = Some(unlock_date); @@ -1003,7 +1080,7 @@ impl SavingsGoalContract { .set(&symbol_short!("GOALS"), &goals); Self::append_audit(&env, symbol_short!("timelock"), &caller, true); - true + Ok(true) } /// Create a schedule for automatic savings deposits @@ -1014,11 +1091,11 @@ impl SavingsGoalContract { amount: i128, next_due: u64, interval: u64, - ) -> u32 { + ) -> Result { owner.require_auth(); if amount <= 0 { - panic!("Amount must be positive"); + return Err(SavingsGoalError::AmountMustBePositive); } let goals: Map = env @@ -1027,15 +1104,15 @@ impl SavingsGoalContract { .get(&symbol_short!("GOALS")) .unwrap_or_else(|| Map::new(&env)); - let goal = goals.get(goal_id).expect("Goal not found"); + let goal = goals.get(goal_id).ok_or(SavingsGoalError::GoalNotFound)?; if goal.owner != owner { - panic!("Only the goal owner can create schedules"); + return Err(SavingsGoalError::Unauthorized); } let current_time = env.ledger().timestamp(); if next_due <= current_time { - panic!("Next due date must be in the future"); + return Err(SavingsGoalError::NextDueDateMustBeInFuture); } Self::extend_instance_ttl(&env); @@ -1080,7 +1157,7 @@ impl SavingsGoalContract { (next_schedule_id, owner), ); - next_schedule_id + Ok(next_schedule_id) } /// Modify a savings schedule @@ -1091,16 +1168,16 @@ impl SavingsGoalContract { amount: i128, next_due: u64, interval: u64, - ) -> bool { + ) -> Result { caller.require_auth(); if amount <= 0 { - panic!("Amount must be positive"); + return Err(SavingsGoalError::AmountMustBePositive); } let current_time = env.ledger().timestamp(); if next_due <= current_time { - panic!("Next due date must be in the future"); + return Err(SavingsGoalError::NextDueDateMustBeInFuture); } Self::extend_instance_ttl(&env); @@ -1111,10 +1188,12 @@ impl SavingsGoalContract { .get(&symbol_short!("SAV_SCH")) .unwrap_or_else(|| Map::new(&env)); - let mut schedule = schedules.get(schedule_id).expect("Schedule not found"); + let mut schedule = schedules + .get(schedule_id) + .ok_or(SavingsGoalError::ScheduleNotFound)?; if schedule.owner != caller { - panic!("Only the schedule owner can modify it"); + return Err(SavingsGoalError::Unauthorized); } schedule.amount = amount; @@ -1132,11 +1211,15 @@ impl SavingsGoalContract { (schedule_id, caller), ); - true + Ok(true) } /// Cancel a savings schedule - pub fn cancel_savings_schedule(env: Env, caller: Address, schedule_id: u32) -> bool { + pub fn cancel_savings_schedule( + env: Env, + caller: Address, + schedule_id: u32, + ) -> Result { caller.require_auth(); Self::extend_instance_ttl(&env); @@ -1147,10 +1230,12 @@ impl SavingsGoalContract { .get(&symbol_short!("SAV_SCH")) .unwrap_or_else(|| Map::new(&env)); - let mut schedule = schedules.get(schedule_id).expect("Schedule not found"); + let mut schedule = schedules + .get(schedule_id) + .ok_or(SavingsGoalError::ScheduleNotFound)?; if schedule.owner != caller { - panic!("Only the schedule owner can cancel it"); + return Err(SavingsGoalError::Unauthorized); } schedule.active = false; @@ -1165,11 +1250,11 @@ impl SavingsGoalContract { (schedule_id, caller), ); - true + Ok(true) } /// Execute due savings schedules (public, callable by anyone - keeper pattern) - pub fn execute_due_savings_schedules(env: Env) -> Vec { + pub fn execute_due_savings_schedules(env: Env) -> Result, SavingsGoalError> { Self::extend_instance_ttl(&env); let current_time = env.ledger().timestamp(); @@ -1196,7 +1281,7 @@ impl SavingsGoalContract { goal.current_amount = goal .current_amount .checked_add(schedule.amount) - .expect("overflow"); + .ok_or(SavingsGoalError::ArithmeticError)?; let is_completed = goal.current_amount >= goal.target_amount; goals.set(schedule.goal_id, goal.clone()); @@ -1252,7 +1337,7 @@ impl SavingsGoalContract { .instance() .set(&symbol_short!("GOALS"), &goals); - executed + Ok(executed) } /// Get all savings schedules for an owner diff --git a/savings_goals/src/test.rs b/savings_goals/src/test.rs index 00cf0eb..b2fd5ae 100644 --- a/savings_goals/src/test.rs +++ b/savings_goals/src/test.rs @@ -59,7 +59,6 @@ fn test_add_to_goal_increments() { } #[test] -#[should_panic] // It will panic because the goal doesn't exist fn test_add_to_non_existent_goal() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -68,7 +67,8 @@ fn test_add_to_non_existent_goal() { client.init(); env.mock_all_auths(); - client.add_to_goal(&user, &99, &500); + let res = client.try_add_to_goal(&user, &99, &500); + assert_eq!(res, Err(Ok(SavingsGoalError::GoalNotFound))); } #[test] @@ -170,7 +170,6 @@ fn test_edge_cases_large_amounts() { } #[test] -#[should_panic] fn test_zero_amount_fails() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -179,7 +178,8 @@ fn test_zero_amount_fails() { client.init(); env.mock_all_auths(); - client.create_goal(&user, &String::from_str(&env, "Fail"), &0, &2000000000); + let res = client.try_create_goal(&user, &String::from_str(&env, "Fail"), &0, &2000000000); + assert_eq!(res, Err(Ok(SavingsGoalError::TargetAmountMustBePositive))); } #[test] @@ -228,7 +228,6 @@ fn test_withdraw_from_goal() { } #[test] -#[should_panic(expected = "Insufficient balance")] fn test_withdraw_too_much() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -242,11 +241,11 @@ fn test_withdraw_too_much() { client.unlock_goal(&user, &id); client.add_to_goal(&user, &id, &100); - client.withdraw_from_goal(&user, &id, &200); + let res = client.try_withdraw_from_goal(&user, &id, &200); + assert_eq!(res, Err(Ok(SavingsGoalError::InsufficientBalance))); } #[test] -#[should_panic(expected = "Cannot withdraw from a locked goal")] fn test_withdraw_locked() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -259,11 +258,11 @@ fn test_withdraw_locked() { // Goal is locked by default client.add_to_goal(&user, &id, &500); - client.withdraw_from_goal(&user, &id, &100); + let res = client.try_withdraw_from_goal(&user, &id, &100); + assert_eq!(res, Err(Ok(SavingsGoalError::GoalLocked))); } #[test] -#[should_panic(expected = "Only the goal owner can withdraw funds")] fn test_withdraw_unauthorized() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -278,7 +277,8 @@ fn test_withdraw_unauthorized() { client.unlock_goal(&user, &id); client.add_to_goal(&user, &id, &500); - client.withdraw_from_goal(&other, &id, &100); + let res = client.try_withdraw_from_goal(&other, &id, &100); + assert_eq!(res, Err(Ok(SavingsGoalError::Unauthorized))); } #[test] @@ -610,7 +610,6 @@ fn test_unlock_goal_success() { } #[test] -#[should_panic(expected = "Only the goal owner can lock this goal")] fn test_lock_goal_unauthorized_panics() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -629,11 +628,11 @@ fn test_lock_goal_unauthorized_panics() { client.unlock_goal(&user, &id); - client.lock_goal(&other, &id); + let res = client.try_lock_goal(&other, &id); + assert_eq!(res, Err(Ok(SavingsGoalError::Unauthorized))); } #[test] -#[should_panic(expected = "Only the goal owner can unlock this goal")] fn test_unlock_goal_unauthorized_panics() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -650,11 +649,11 @@ fn test_unlock_goal_unauthorized_panics() { &2000000000, ); - client.unlock_goal(&other, &id); + let res = client.try_unlock_goal(&other, &id); + assert_eq!(res, Err(Ok(SavingsGoalError::Unauthorized))); } #[test] -#[should_panic(expected = "Cannot withdraw from a locked goal")] fn test_withdraw_after_lock_fails() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -674,7 +673,8 @@ fn test_withdraw_after_lock_fails() { client.add_to_goal(&user, &id, &500); client.lock_goal(&user, &id); - client.withdraw_from_goal(&user, &id, &100); + let res = client.try_withdraw_from_goal(&user, &id, &100); + assert_eq!(res, Err(Ok(SavingsGoalError::GoalLocked))); } #[test] @@ -704,7 +704,6 @@ fn test_withdraw_after_unlock_succeeds() { } #[test] -#[should_panic(expected = "Goal not found")] fn test_lock_nonexistent_goal_panics() { let env = Env::default(); let contract_id = env.register_contract(None, SavingsGoalContract); @@ -714,7 +713,8 @@ fn test_lock_nonexistent_goal_panics() { client.init(); env.mock_all_auths(); - client.lock_goal(&user, &99); + let res = client.try_lock_goal(&user, &99); + assert_eq!(res, Err(Ok(SavingsGoalError::GoalNotFound))); } #[test] diff --git a/savings_goals/test_snapshots/test/test_add_to_non_existent_goal.1.json b/savings_goals/test_snapshots/test/test_add_to_non_existent_goal.1.json index b10b782..3efbd17 100644 --- a/savings_goals/test_snapshots/test/test_add_to_non_existent_goal.1.json +++ b/savings_goals/test_snapshots/test/test_add_to_non_existent_goal.1.json @@ -228,27 +228,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "add_to_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Goal not found' from contract function 'Symbol(obj#13)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 99 - }, - { - "i128": { - "hi": 0, - "lo": 500 - } - } - ] + "error": { + "contract": 7 + } } } } @@ -268,12 +257,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 7 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -293,14 +282,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 7 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "add_to_goal" @@ -327,31 +316,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_lock_goal_unauthorized_panics.1.json b/savings_goals/test_snapshots/test/test_lock_goal_unauthorized_panics.1.json index ca3e6f7..612d6c2 100644 --- a/savings_goals/test_snapshots/test/test_lock_goal_unauthorized_panics.1.json +++ b/savings_goals/test_snapshots/test/test_lock_goal_unauthorized_panics.1.json @@ -734,21 +734,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "lock_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Only the goal owner can lock this goal' from contract function 'Symbol(lock_goal)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "u32": 1 - } - ] + "error": { + "contract": 3 + } } } } @@ -768,12 +763,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 3 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -793,14 +788,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 3 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "lock_goal" @@ -821,31 +816,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_lock_nonexistent_goal_panics.1.json b/savings_goals/test_snapshots/test/test_lock_nonexistent_goal_panics.1.json index 16ad5dd..d33167b 100644 --- a/savings_goals/test_snapshots/test/test_lock_nonexistent_goal_panics.1.json +++ b/savings_goals/test_snapshots/test/test_lock_nonexistent_goal_panics.1.json @@ -222,21 +222,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "lock_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Goal not found' from contract function 'Symbol(lock_goal)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 99 - } - ] + "error": { + "contract": 7 + } } } } @@ -256,12 +251,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 7 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -281,14 +276,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 7 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "lock_goal" @@ -309,31 +304,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_unlock_goal_unauthorized_panics.1.json b/savings_goals/test_snapshots/test/test_unlock_goal_unauthorized_panics.1.json index 8a9898f..0f97335 100644 --- a/savings_goals/test_snapshots/test/test_unlock_goal_unauthorized_panics.1.json +++ b/savings_goals/test_snapshots/test/test_unlock_goal_unauthorized_panics.1.json @@ -544,21 +544,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "unlock_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Only the goal owner can unlock this goal' from contract function 'Symbol(obj#61)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "u32": 1 - } - ] + "error": { + "contract": 3 + } } } } @@ -578,12 +573,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 3 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -603,14 +598,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 3 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "unlock_goal" @@ -631,31 +626,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_withdraw_after_lock_fails.1.json b/savings_goals/test_snapshots/test/test_withdraw_after_lock_fails.1.json index b10bc63..7f2c84c 100644 --- a/savings_goals/test_snapshots/test/test_withdraw_after_lock_fails.1.json +++ b/savings_goals/test_snapshots/test/test_withdraw_after_lock_fails.1.json @@ -1182,27 +1182,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "withdraw_from_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Cannot withdraw from a locked goal' from contract function 'Symbol(obj#237)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 1 - }, - { - "i128": { - "hi": 0, - "lo": 100 - } - } - ] + "error": { + "contract": 11 + } } } } @@ -1222,12 +1211,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 11 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -1247,14 +1236,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 11 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "withdraw_from_goal" @@ -1281,31 +1270,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_withdraw_locked.1.json b/savings_goals/test_snapshots/test/test_withdraw_locked.1.json index 3011979..8e452b1 100644 --- a/savings_goals/test_snapshots/test/test_withdraw_locked.1.json +++ b/savings_goals/test_snapshots/test/test_withdraw_locked.1.json @@ -820,27 +820,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "withdraw_from_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Cannot withdraw from a locked goal' from contract function 'Symbol(obj#119)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 1 - }, - { - "i128": { - "hi": 0, - "lo": 100 - } - } - ] + "error": { + "contract": 11 + } } } } @@ -860,12 +849,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 11 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -885,14 +874,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 11 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "withdraw_from_goal" @@ -919,31 +908,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_withdraw_time_locked_goal_before_unlock.1.json b/savings_goals/test_snapshots/test/test_withdraw_time_locked_goal_before_unlock.1.json index 0c90372..6016b64 100644 --- a/savings_goals/test_snapshots/test/test_withdraw_time_locked_goal_before_unlock.1.json +++ b/savings_goals/test_snapshots/test/test_withdraw_time_locked_goal_before_unlock.1.json @@ -1048,27 +1048,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "withdraw_from_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Goal is time-locked until unlock date' from contract function 'Symbol(obj#221)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 1 - }, - { - "i128": { - "hi": 0, - "lo": 1000 - } - } - ] + "error": { + "contract": 12 + } } } } @@ -1088,12 +1077,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 12 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -1113,7 +1102,7 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 12 } } ], diff --git a/savings_goals/test_snapshots/test/test_withdraw_too_much.1.json b/savings_goals/test_snapshots/test/test_withdraw_too_much.1.json index 9af502f..f42399f 100644 --- a/savings_goals/test_snapshots/test/test_withdraw_too_much.1.json +++ b/savings_goals/test_snapshots/test/test_withdraw_too_much.1.json @@ -1001,27 +1001,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "withdraw_from_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Insufficient balance' from contract function 'Symbol(obj#177)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 1 - }, - { - "i128": { - "hi": 0, - "lo": 200 - } - } - ] + "error": { + "contract": 13 + } } } } @@ -1041,12 +1030,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 13 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -1066,14 +1055,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 13 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "withdraw_from_goal" @@ -1100,31 +1089,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_withdraw_unauthorized.1.json b/savings_goals/test_snapshots/test/test_withdraw_unauthorized.1.json index 34d5144..83023b6 100644 --- a/savings_goals/test_snapshots/test/test_withdraw_unauthorized.1.json +++ b/savings_goals/test_snapshots/test/test_withdraw_unauthorized.1.json @@ -1001,27 +1001,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "withdraw_from_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Only the goal owner can withdraw funds' from contract function 'Symbol(obj#179)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "u32": 1 - }, - { - "i128": { - "hi": 0, - "lo": 100 - } - } - ] + "error": { + "contract": 3 + } } } } @@ -1041,12 +1030,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 3 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -1066,14 +1055,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 3 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "withdraw_from_goal" @@ -1100,31 +1089,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file diff --git a/savings_goals/test_snapshots/test/test_zero_amount_fails.1.json b/savings_goals/test_snapshots/test/test_zero_amount_fails.1.json index d6fb9a8..9ff3dbb 100644 --- a/savings_goals/test_snapshots/test/test_zero_amount_fails.1.json +++ b/savings_goals/test_snapshots/test/test_zero_amount_fails.1.json @@ -231,30 +231,16 @@ "v0": { "topics": [ { - "symbol": "log" + "symbol": "fn_return" + }, + { + "symbol": "create_goal" } ], "data": { - "vec": [ - { - "string": "caught panic 'Target amount must be positive' from contract function 'Symbol(obj#15)'" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "string": "Fail" - }, - { - "i128": { - "hi": 0, - "lo": 0 - } - }, - { - "u64": 2000000000 - } - ] + "error": { + "contract": 5 + } } } } @@ -274,12 +260,12 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 5 } } ], "data": { - "string": "caught error from function" + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" } } } @@ -299,14 +285,14 @@ }, { "error": { - "wasm_vm": "invalid_action" + "contract": 5 } } ], "data": { "vec": [ { - "string": "contract call failed" + "string": "contract try_call failed" }, { "symbol": "create_goal" @@ -336,31 +322,6 @@ } }, "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "wasm_vm": "invalid_action" - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false } ] } \ No newline at end of file