From 3689703e6106b5d3c578597196f22aad1caacbef Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Wed, 25 Feb 2026 02:27:28 -0600 Subject: [PATCH 1/5] style: apply cargo fmt formatting --- .../identity-registry-contract/src/events.rs | 2 +- contracts/payment-vault-contract/src/types.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/identity-registry-contract/src/events.rs b/contracts/identity-registry-contract/src/events.rs index 493d0b7..1e82f1f 100644 --- a/contracts/identity-registry-contract/src/events.rs +++ b/contracts/identity-registry-contract/src/events.rs @@ -1,5 +1,5 @@ use crate::types::ExpertStatus; -use soroban_sdk::{contracttype, Address, Env, Symbol, String}; +use soroban_sdk::{contracttype, Address, Env, String, Symbol}; // The Event Data Structure #[contracttype] diff --git a/contracts/payment-vault-contract/src/types.rs b/contracts/payment-vault-contract/src/types.rs index 03a3c2c..6b5e4c5 100644 --- a/contracts/payment-vault-contract/src/types.rs +++ b/contracts/payment-vault-contract/src/types.rs @@ -15,12 +15,12 @@ pub enum BookingStatus { #[contracttype] #[derive(Clone, Debug)] pub struct BookingRecord { - pub id: u64, // Storage key identifier - pub user: Address, // User who created the booking - pub expert: Address, // Expert providing consultation - pub rate_per_second: i128, // Payment rate per second - pub max_duration: u64, // Maximum booked duration in seconds - pub total_deposit: i128, // Total deposit (rate_per_second * max_duration) - pub status: BookingStatus, // Current booking status - pub created_at: u64, // Ledger timestamp when booking was created + pub id: u64, // Storage key identifier + pub user: Address, // User who created the booking + pub expert: Address, // Expert providing consultation + pub rate_per_second: i128, // Payment rate per second + pub max_duration: u64, // Maximum booked duration in seconds + pub total_deposit: i128, // Total deposit (rate_per_second * max_duration) + pub status: BookingStatus, // Current booking status + pub created_at: u64, // Ledger timestamp when booking was created } From 7c73ace1c089bf5e8af0c3a8485b5c8471c1fe03 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Wed, 25 Feb 2026 02:27:38 -0600 Subject: [PATCH 2/5] feat: add IsPaused storage key and ContractPaused error variant Add the circuit breaker data layer: IsPaused DataKey with set_paused/is_paused helpers, and ContractPaused error variant (code 8) for enforcement. --- contracts/payment-vault-contract/src/error.rs | 3 ++- .../payment-vault-contract/src/storage.rs | 25 +++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/contracts/payment-vault-contract/src/error.rs b/contracts/payment-vault-contract/src/error.rs index 28760ff..c64a89b 100644 --- a/contracts/payment-vault-contract/src/error.rs +++ b/contracts/payment-vault-contract/src/error.rs @@ -11,4 +11,5 @@ pub enum VaultError { BookingNotPending = 5, InvalidAmount = 6, ReclaimTooEarly = 7, -} \ No newline at end of file + ContractPaused = 8, +} diff --git a/contracts/payment-vault-contract/src/storage.rs b/contracts/payment-vault-contract/src/storage.rs index 55e8ce3..c2bc3d4 100644 --- a/contracts/payment-vault-contract/src/storage.rs +++ b/contracts/payment-vault-contract/src/storage.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{contracttype, Address, Env}; use crate::types::{BookingRecord, BookingStatus}; +use soroban_sdk::{contracttype, Address, Env}; #[contracttype] #[derive(Clone)] @@ -7,10 +7,11 @@ pub enum DataKey { Admin, Token, Oracle, - Booking(u64), // Booking ID -> BookingRecord - BookingCounter, // Counter for generating unique booking IDs - UserBookings(Address), // User Address -> Vec of booking IDs + Booking(u64), // Booking ID -> BookingRecord + BookingCounter, // Counter for generating unique booking IDs + UserBookings(Address), // User Address -> Vec of booking IDs ExpertBookings(Address), // Expert Address -> Vec of booking IDs + IsPaused, // Circuit breaker flag } // --- Admin --- @@ -45,6 +46,18 @@ pub fn get_oracle(env: &Env) -> Address { env.storage().instance().get(&DataKey::Oracle).unwrap() } +// --- Pause (Circuit Breaker) --- +pub fn set_paused(env: &Env, paused: bool) { + env.storage().instance().set(&DataKey::IsPaused, &paused); +} + +pub fn is_paused(env: &Env) -> bool { + env.storage() + .instance() + .get(&DataKey::IsPaused) + .unwrap_or(false) +} + // --- Booking Counter --- pub fn get_next_booking_id(env: &Env) -> u64 { let current: u64 = env @@ -53,7 +66,9 @@ pub fn get_next_booking_id(env: &Env) -> u64 { .get(&DataKey::BookingCounter) .unwrap_or(0); let next = current + 1; - env.storage().instance().set(&DataKey::BookingCounter, &next); + env.storage() + .instance() + .set(&DataKey::BookingCounter, &next); next } From 6ffc8ca717e4eb25ed331533bda45a7bfd33bee5 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Wed, 25 Feb 2026 02:27:46 -0600 Subject: [PATCH 3/5] feat: add contract_paused event emitter Emit a "paused" event with a bool indicating the new pause state whenever the admin toggles the circuit breaker. --- .../payment-vault-contract/src/events.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/contracts/payment-vault-contract/src/events.rs b/contracts/payment-vault-contract/src/events.rs index 2ff8dfe..6216ada 100644 --- a/contracts/payment-vault-contract/src/events.rs +++ b/contracts/payment-vault-contract/src/events.rs @@ -1,9 +1,16 @@ -use soroban_sdk::{Address, Env, symbol_short}; +use soroban_sdk::{symbol_short, Address, Env}; /// Emitted when a new booking is created -pub fn booking_created(env: &Env, booking_id: u64, user: &Address, expert: &Address, deposit: i128) { +pub fn booking_created( + env: &Env, + booking_id: u64, + user: &Address, + expert: &Address, + deposit: i128, +) { let topics = (symbol_short!("booked"), booking_id); - env.events().publish(topics, (user.clone(), expert.clone(), deposit)); + env.events() + .publish(topics, (user.clone(), expert.clone(), deposit)); } /// Emitted when a session is finalized @@ -17,6 +24,12 @@ pub fn session_reclaimed(env: &Env, booking_id: u64, amount: i128) { env.events().publish(topics, amount); } +/// Emitted when the contract is paused or unpaused +pub fn contract_paused(env: &Env, paused: bool) { + let topics = (symbol_short!("paused"),); + env.events().publish(topics, paused); +} + /// Emitted when an expert rejects a pending session pub fn session_rejected(env: &Env, booking_id: u64, reason: &str) { let topics = (symbol_short!("reject"), booking_id); From c9513fbfd59dbe0067c4630dd8146f61c5010875 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Wed, 25 Feb 2026 02:27:54 -0600 Subject: [PATCH 4/5] feat: implement pause/unpause admin functions and pause guards Add admin-only pause() and unpause() contract methods with require_auth. Add is_paused guard at the top of all state-changing functions (book_session, finalize_session, reclaim_stale_session, reject_session) that returns ContractPaused error when the circuit breaker is active. Read-only getters remain unaffected. --- .../payment-vault-contract/src/contract.rs | 63 ++++++++++++------- contracts/payment-vault-contract/src/lib.rs | 24 ++++--- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/contracts/payment-vault-contract/src/contract.rs b/contracts/payment-vault-contract/src/contract.rs index 0673145..0238418 100644 --- a/contracts/payment-vault-contract/src/contract.rs +++ b/contracts/payment-vault-contract/src/contract.rs @@ -1,14 +1,14 @@ -use soroban_sdk::{Address, Env, token}; -use crate::storage; -use crate::types::{BookingRecord, BookingStatus}; use crate::error::VaultError; use crate::events; +use crate::storage; +use crate::types::{BookingRecord, BookingStatus}; +use soroban_sdk::{token, Address, Env}; pub fn initialize_vault( env: &Env, admin: &Address, token: &Address, - oracle: &Address + oracle: &Address, ) -> Result<(), VaultError> { // 1. Check if already initialized if storage::has_admin(env) { @@ -23,6 +23,22 @@ pub fn initialize_vault( Ok(()) } +pub fn pause(env: &Env) -> Result<(), VaultError> { + let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?; + admin.require_auth(); + storage::set_paused(env, true); + events::contract_paused(env, true); + Ok(()) +} + +pub fn unpause(env: &Env) -> Result<(), VaultError> { + let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?; + admin.require_auth(); + storage::set_paused(env, false); + events::contract_paused(env, false); + Ok(()) +} + pub fn book_session( env: &Env, user: &Address, @@ -30,6 +46,10 @@ pub fn book_session( rate_per_second: i128, max_duration: u64, ) -> Result { + if storage::is_paused(env) { + return Err(VaultError::ContractPaused); + } + // Require authorization from the user creating the booking user.require_auth(); @@ -84,13 +104,16 @@ pub fn finalize_session( booking_id: u64, actual_duration: u64, ) -> Result<(), VaultError> { + if storage::is_paused(env) { + return Err(VaultError::ContractPaused); + } + // 1. Require Oracle authorization let oracle = storage::get_oracle(env); oracle.require_auth(); // 2. Get booking and verify it exists - let booking = storage::get_booking(env, booking_id) - .ok_or(VaultError::BookingNotFound)?; + let booking = storage::get_booking(env, booking_id).ok_or(VaultError::BookingNotFound)?; // 3. Verify booking is in Pending status if booking.status != BookingStatus::Pending { @@ -134,17 +157,16 @@ pub fn finalize_session( /// 24 hours in seconds const RECLAIM_TIMEOUT: u64 = 86400; -pub fn reclaim_stale_session( - env: &Env, - user: &Address, - booking_id: u64, -) -> Result<(), VaultError> { +pub fn reclaim_stale_session(env: &Env, user: &Address, booking_id: u64) -> Result<(), VaultError> { + if storage::is_paused(env) { + return Err(VaultError::ContractPaused); + } + // 1. Require user authorization user.require_auth(); // 2. Get booking and verify it exists - let booking = storage::get_booking(env, booking_id) - .ok_or(VaultError::BookingNotFound)?; + let booking = storage::get_booking(env, booking_id).ok_or(VaultError::BookingNotFound)?; // 3. Verify the caller is the booking owner if booking.user != *user { @@ -177,17 +199,16 @@ pub fn reclaim_stale_session( Ok(()) } -pub fn reject_session( - env: &Env, - expert: &Address, - booking_id: u64, -) -> Result<(), VaultError> { +pub fn reject_session(env: &Env, expert: &Address, booking_id: u64) -> Result<(), VaultError> { + if storage::is_paused(env) { + return Err(VaultError::ContractPaused); + } + // 1. Require expert authorization expert.require_auth(); // 2. Get booking and verify it exists - let booking = storage::get_booking(env, booking_id) - .ok_or(VaultError::BookingNotFound)?; + let booking = storage::get_booking(env, booking_id).ok_or(VaultError::BookingNotFound)?; // 3. Verify the caller is the expert in the booking if booking.expert != *expert { @@ -212,4 +233,4 @@ pub fn reject_session( events::session_rejected(env, booking_id, "Expert declined session"); Ok(()) -} \ No newline at end of file +} diff --git a/contracts/payment-vault-contract/src/lib.rs b/contracts/payment-vault-contract/src/lib.rs index 8fa71f6..b02aa3f 100644 --- a/contracts/payment-vault-contract/src/lib.rs +++ b/contracts/payment-vault-contract/src/lib.rs @@ -4,13 +4,13 @@ mod contract; mod error; mod events; mod storage; -mod types; #[cfg(test)] mod test; +mod types; -use soroban_sdk::{contract, contractimpl, Address, Env, Vec}; use crate::error::VaultError; use crate::types::BookingRecord; +use soroban_sdk::{contract, contractimpl, Address, Env, Vec}; #[contract] pub struct PaymentVaultContract; @@ -22,11 +22,23 @@ impl PaymentVaultContract { env: Env, admin: Address, token: Address, - oracle: Address + oracle: Address, ) -> Result<(), VaultError> { contract::initialize_vault(&env, &admin, &token, &oracle) } + /// Pause the contract (Admin-only) + /// Halts all state-changing operations in an emergency + pub fn pause(env: Env) -> Result<(), VaultError> { + contract::pause(&env) + } + + /// Unpause the contract (Admin-only) + /// Resumes normal contract operations + pub fn unpause(env: Env) -> Result<(), VaultError> { + contract::unpause(&env) + } + /// Book a session with an expert /// User deposits tokens upfront based on rate_per_second * max_duration pub fn book_session( @@ -61,11 +73,7 @@ impl PaymentVaultContract { /// Reject a pending session (Expert-only) /// Experts can reject a pending booking, instantly refunding the user - pub fn reject_session( - env: Env, - expert: Address, - booking_id: u64, - ) -> Result<(), VaultError> { + pub fn reject_session(env: Env, expert: Address, booking_id: u64) -> Result<(), VaultError> { contract::reject_session(&env, &expert, booking_id) } From 8cb661675e177336d8008b3299cc092a5170309d Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Wed, 25 Feb 2026 02:28:03 -0600 Subject: [PATCH 5/5] test: add circuit breaker pausability tests Cover all acceptance criteria: - pause blocks book_session, finalize_session, reclaim_stale_session, reject_session - unpause resumes normal operations - read-only getters remain active while paused --- contracts/payment-vault-contract/src/test.rs | 200 ++++++++++++++++++- 1 file changed, 196 insertions(+), 4 deletions(-) diff --git a/contracts/payment-vault-contract/src/test.rs b/contracts/payment-vault-contract/src/test.rs index 15a0c6f..d6d5475 100644 --- a/contracts/payment-vault-contract/src/test.rs +++ b/contracts/payment-vault-contract/src/test.rs @@ -383,7 +383,8 @@ fn test_reclaim_stale_session_success() { let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // Advance ledger timestamp by 25 hours (90000 seconds) - env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + env.ledger() + .set_timestamp(env.ledger().timestamp() + 90_000); // User tries to reclaim after 25 hours (should succeed) let result = client.try_reclaim_stale_session(&user, &booking_id); @@ -419,7 +420,8 @@ fn test_reclaim_stale_session_wrong_user() { let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // Advance ledger timestamp by 25 hours - env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + env.ledger() + .set_timestamp(env.ledger().timestamp() + 90_000); // Other user tries to reclaim (should fail - not authorized) let result = client.try_reclaim_stale_session(&other_user, &booking_id); @@ -455,7 +457,8 @@ fn test_reclaim_already_finalized() { client.finalize_session(&booking_id, &50); // Advance ledger timestamp by 25 hours - env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + env.ledger() + .set_timestamp(env.ledger().timestamp() + 90_000); // User tries to reclaim after finalization (should fail - not pending) let result = client.try_reclaim_stale_session(&user, &booking_id); @@ -583,7 +586,8 @@ fn test_reject_already_reclaimed_session() { let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); // Advance time and user reclaims - env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + env.ledger() + .set_timestamp(env.ledger().timestamp() + 90_000); client.reclaim_stale_session(&user, &booking_id); // Expert tries to reject after reclamation (should fail - not pending) @@ -639,3 +643,191 @@ fn test_reject_nonexistent_booking() { assert!(result.is_err()); } +// ==================== Pausability (Circuit Breaker) Tests ==================== + +#[test] +fn test_pause_blocks_book_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Admin pauses the contract + let result = client.try_pause(); + assert!(result.is_ok()); + + // User tries to book a session while paused (should fail) + let result = client.try_book_session(&user, &expert, &10, &100); + assert!(result.is_err()); + + // Verify user's balance is unchanged + assert_eq!(token.balance(&user), 10_000); +} + +#[test] +fn test_pause_blocks_finalize_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Book session while unpaused + let booking_id = client.book_session(&user, &expert, &10, &100); + + // Admin pauses the contract + client.pause(); + + // Oracle tries to finalize while paused (should fail) + let result = client.try_finalize_session(&booking_id, &50); + assert!(result.is_err()); +} + +#[test] +fn test_pause_blocks_reclaim_stale_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Book session while unpaused + let booking_id = client.book_session(&user, &expert, &10, &100); + + // Advance time past reclaim timeout + env.ledger() + .set_timestamp(env.ledger().timestamp() + 90_000); + + // Admin pauses the contract + client.pause(); + + // User tries to reclaim while paused (should fail) + let result = client.try_reclaim_stale_session(&user, &booking_id); + assert!(result.is_err()); +} + +#[test] +fn test_pause_blocks_reject_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Book session while unpaused + let booking_id = client.book_session(&user, &expert, &10, &100); + + // Admin pauses the contract + client.pause(); + + // Expert tries to reject while paused (should fail) + let result = client.try_reject_session(&expert, &booking_id); + assert!(result.is_err()); +} + +#[test] +fn test_unpause_resumes_operations() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Admin pauses the contract + client.pause(); + + // Booking should fail while paused + let result = client.try_book_session(&user, &expert, &10, &100); + assert!(result.is_err()); + + // Admin unpauses the contract + let result = client.try_unpause(); + assert!(result.is_ok()); + + // Booking should succeed after unpause + let booking_id = client.book_session(&user, &expert, &10, &100); + assert_eq!(booking_id, 1); + assert_eq!(token.balance(&user), 9_000); + assert_eq!(token.balance(&client.address), 1_000); +} + +#[test] +fn test_read_only_functions_work_while_paused() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Book a session before pausing + let booking_id = client.book_session(&user, &expert, &10, &100); + + // Admin pauses the contract + client.pause(); + + // Read-only functions should still work + let booking = client.get_booking(&booking_id); + assert!(booking.is_some()); + assert_eq!(booking.unwrap().id, booking_id); + + let user_bookings = client.get_user_bookings(&user); + assert_eq!(user_bookings.len(), 1); + + let expert_bookings = client.get_expert_bookings(&expert); + assert_eq!(expert_bookings.len(), 1); +}