diff --git a/contracts/.gitignore b/contracts/.gitignore index 8e0729c..5a975a2 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -1,2 +1,5 @@ # Rust build artifacts target/ + +# Soroban test snapshots +**/test_snapshots/ diff --git a/contracts/loyalty_token/src/lib.rs b/contracts/loyalty_token/src/lib.rs index 8476394..6dcafa6 100644 --- a/contracts/loyalty_token/src/lib.rs +++ b/contracts/loyalty_token/src/lib.rs @@ -47,6 +47,8 @@ pub enum DataKey { Balance(Address), /// Allowances: (owner, spender) -> (amount, expiration_ledger). Allowance(Address, Address), + /// Whether the contract is paused. + Paused, } // Token metadata (stored once at init) @@ -108,6 +110,7 @@ impl LoyaltyToken { pub fn mint(env: Env, caller: Address, to: Address, amount: i128) { caller.require_auth(); Self::assert_admin_or_minter(&env, &caller); + Self::assert_not_paused_for(&env, &caller); if amount <= 0 { panic!("amount must be positive"); @@ -130,6 +133,31 @@ impl LoyaltyToken { .publish((symbol_short!("mint"), symbol_short!("BITE")), (to, amount)); } + // ----------------------------------------------------------------------- + // Pause / unpause + // ----------------------------------------------------------------------- + + /// Pause the contract (admin only). All state-mutating calls by non-admins + /// will be rejected while paused. + pub fn pause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &true); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("pause")), env.ledger().timestamp()); + } + + /// Unpause the contract (admin only). + pub fn unpause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &false); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("unpause")), env.ledger().timestamp()); + } + /// Update the authorised minter address (admin only). pub fn set_minter(env: Env, caller: Address, new_minter: Address) { caller.require_auth(); @@ -196,6 +224,7 @@ impl LoyaltyToken { /// Transfer `amount` BITE from `from` to `to`. pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { from.require_auth(); + Self::assert_not_paused_for(&env, &from); Self::do_transfer(&env, &from, &to, amount); } @@ -220,6 +249,7 @@ impl LoyaltyToken { expiration_ledger: u32, ) { from.require_auth(); + Self::assert_not_paused_for(&env, &from); if amount < 0 { panic!("allowance amount cannot be negative"); } @@ -256,6 +286,7 @@ impl LoyaltyToken { /// Transfer `amount` on behalf of `from` using a prior allowance. pub fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128) { spender.require_auth(); + Self::assert_not_paused_for(&env, &spender); let current = Self::get_allowance(&env, &from, &spender); if current < amount { @@ -281,12 +312,14 @@ impl LoyaltyToken { /// Burn `amount` BITE from `from`'s account. pub fn burn(env: Env, from: Address, amount: i128) { from.require_auth(); + Self::assert_not_paused_for(&env, &from); Self::do_burn(&env, &from, amount); } /// Burn `amount` BITE from `from` using a spender's allowance. pub fn burn_from(env: Env, spender: Address, from: Address, amount: i128) { spender.require_auth(); + Self::assert_not_paused_for(&env, &spender); let current = Self::get_allowance(&env, &from, &spender); if current < amount { @@ -421,6 +454,16 @@ impl LoyaltyToken { } } + fn assert_not_paused_for(env: &Env, caller: &Address) { + let paused: bool = env.storage().instance().get(&DataKey::Paused).unwrap_or(false); + if paused { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + if caller != &admin { + panic!("contract is paused"); + } + } + } + fn assert_admin_or_minter(env: &Env, caller: &Address) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); let minter: Address = env.storage().instance().get(&DataKey::Minter).unwrap(); diff --git a/contracts/order/src/lib.rs b/contracts/order/src/lib.rs index 154697f..8d9671d 100644 --- a/contracts/order/src/lib.rs +++ b/contracts/order/src/lib.rs @@ -123,6 +123,7 @@ impl OrderContract { notes: String, ) -> u64 { customer.require_auth(); + Self::assert_not_paused_for(&env, &customer); if items.is_empty() { panic!("order must contain at least one item"); @@ -191,6 +192,7 @@ impl OrderContract { pub fn cancel_order(env: Env, caller: Address, order_id: u64) { caller.require_auth(); + Self::assert_not_paused_for(&env, &caller); let mut order = Self::load_order(&env, order_id); let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); @@ -335,6 +337,40 @@ impl OrderContract { } } + fn assert_not_paused_for(env: &Env, caller: &Address) { + let paused: bool = env.storage().instance().get(&DataKey::Paused).unwrap_or(false); + if paused { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + if caller != &admin { + panic!("contract is paused"); + } + } + } + + // ----------------------------------------------------------------------- + // Pause / unpause + // ----------------------------------------------------------------------- + + /// Pause the contract (admin only). + pub fn pause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &true); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("pause")), env.ledger().timestamp()); + } + + /// Unpause the contract (admin only). + pub fn unpause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &false); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("unpause")), env.ledger().timestamp()); + } + fn append_to_list(env: &Env, key: DataKey, id: u64, ttl: u32) { let mut list: Vec = env .storage() diff --git a/contracts/payment/src/lib.rs b/contracts/payment/src/lib.rs index 157d5e8..db5d008 100644 --- a/contracts/payment/src/lib.rs +++ b/contracts/payment/src/lib.rs @@ -138,6 +138,7 @@ impl PaymentContract { amount: i128, ) { payer.require_auth(); + Self::assert_not_paused_for(&env, &payer); if env.storage().persistent().has(&DataKey::Payment(order_id)) { panic!("payment already exists for this order"); @@ -195,6 +196,7 @@ impl PaymentContract { pub fn release_payment(env: Env, caller: Address, order_id: u64) { // ── CHECKS ──────────────────────────────────────────────────────── caller.require_auth(); + Self::assert_not_paused_for(&env, &caller); let mut payment: Payment = env .storage() @@ -278,6 +280,7 @@ impl PaymentContract { pub fn refund_payment(env: Env, caller: Address, order_id: u64) { caller.require_auth(); Self::assert_admin_or_panic(&env, &caller); + // Admin can always refund, even while paused – no pause guard here. let mut payment: Payment = env .storage() @@ -365,10 +368,41 @@ impl PaymentContract { panic!("unauthorized: admin only"); } } -} -// --------------------------------------------------------------------------- -// Tests + fn assert_not_paused_for(env: &Env, caller: &Address) { + let paused: bool = env.storage().instance().get(&DataKey::Paused).unwrap_or(false); + if paused { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + if caller != &admin { + panic!("contract is paused"); + } + } + } + + // ----------------------------------------------------------------------- + // Pause / unpause + // ----------------------------------------------------------------------- + + /// Pause the contract (admin only). + pub fn pause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &true); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("pause")), env.ledger().timestamp()); + } + + /// Unpause the contract (admin only). + pub fn unpause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &false); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("unpause")), env.ledger().timestamp()); + } +} // --------------------------------------------------------------------------- #[cfg(test)] @@ -511,4 +545,4 @@ mod test { client.release_payment(&admin, &5); // first call succeeds client.release_payment(&admin, &5); // reentrant/duplicate call must panic } -} \ No newline at end of file +} diff --git a/contracts/restaurant_registry/src/lib.rs b/contracts/restaurant_registry/src/lib.rs index 77e2ff0..e4f4a3a 100644 --- a/contracts/restaurant_registry/src/lib.rs +++ b/contracts/restaurant_registry/src/lib.rs @@ -50,6 +50,8 @@ pub enum DataKey { Restaurant(u64), /// Reverse lookup: owner address → restaurant ID. OwnerToId(Address), + /// Whether the contract is paused. + Paused, } // --------------------------------------------------------------------------- @@ -91,6 +93,7 @@ impl RestaurantRegistry { /// - If the owner already has a registered restaurant. pub fn register_restaurant(env: Env, owner: Address, name: String, slug: String) -> u64 { owner.require_auth(); + Self::assert_not_paused_for(&env, &owner); if env .storage() @@ -151,6 +154,7 @@ impl RestaurantRegistry { slug: String, ) { caller.require_auth(); + Self::assert_not_paused_for(&env, &caller); let mut restaurant: Restaurant = env .storage() @@ -188,6 +192,7 @@ impl RestaurantRegistry { /// - If `caller` is not the restaurant owner. pub fn owner_close_restaurant(env: Env, caller: Address, restaurant_id: u64) { caller.require_auth(); + Self::assert_not_paused_for(&env, &caller); let mut restaurant: Restaurant = env .storage() @@ -326,6 +331,47 @@ impl RestaurantRegistry { pub fn admin(env: Env) -> Address { env.storage().instance().get(&DataKey::Admin).unwrap() } + + // ----------------------------------------------------------------------- + // Pause / unpause + // ----------------------------------------------------------------------- + + /// Pause the contract (admin only). + pub fn pause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &true); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("pause")), env.ledger().timestamp()); + } + + /// Unpause the contract (admin only). + pub fn unpause_contract(env: Env, caller: Address) { + caller.require_auth(); + Self::assert_admin_or_panic(&env, &caller); + env.storage().instance().set(&DataKey::Paused, &false); + env.storage().instance().extend_ttl(17_280, 17_280); + env.events() + .publish((symbol_short!("ctrl"), symbol_short!("unpause")), env.ledger().timestamp()); + } + + fn assert_admin_or_panic(env: &Env, caller: &Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + if caller != &admin { + panic!("unauthorized: admin only"); + } + } + + fn assert_not_paused_for(env: &Env, caller: &Address) { + let paused: bool = env.storage().instance().get(&DataKey::Paused).unwrap_or(false); + if paused { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + if caller != &admin { + panic!("contract is paused"); + } + } + } } // --------------------------------------------------------------------------- @@ -502,4 +548,50 @@ mod test { &String::from_str(&env, "second"), ); } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_blocks_register() { + let (env, client) = setup(); + let admin = Address::generate(&env); + let owner = Address::generate(&env); + client.initialize(&admin); + client.pause_contract(&admin); + client.register_restaurant( + &owner, + &String::from_str(&env, "Blocked"), + &String::from_str(&env, "blocked"), + ); + } + + #[test] + fn test_pause_admin_can_still_register() { + let (env, client) = setup(); + let admin = Address::generate(&env); + client.initialize(&admin); + client.pause_contract(&admin); + // Admin can still register while paused. + let id = client.register_restaurant( + &admin, + &String::from_str(&env, "Admin Rest"), + &String::from_str(&env, "admin-rest"), + ); + assert_eq!(id, 1); + } + + #[test] + fn test_unpause_restores_register() { + let (env, client) = setup(); + let admin = Address::generate(&env); + let owner = Address::generate(&env); + client.initialize(&admin); + client.pause_contract(&admin); + client.unpause_contract(&admin); + let id = client.register_restaurant( + &owner, + &String::from_str(&env, "Back Open"), + &String::from_str(&env, "back-open"), + ); + assert!(id > 0); + } }