Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Rust build artifacts
target/

# Soroban test snapshots
**/test_snapshots/
43 changes: 43 additions & 0 deletions contracts/loyalty_token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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");
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}

Expand All @@ -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");
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
36 changes: 36 additions & 0 deletions contracts/order/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<u64> = env
.storage()
Expand Down
42 changes: 38 additions & 4 deletions contracts/payment/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -511,4 +545,4 @@ mod test {
client.release_payment(&admin, &5); // first call succeeds
client.release_payment(&admin, &5); // reentrant/duplicate call must panic
}
}
}
92 changes: 92 additions & 0 deletions contracts/restaurant_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub enum DataKey {
Restaurant(u64),
/// Reverse lookup: owner address → restaurant ID.
OwnerToId(Address),
/// Whether the contract is paused.
Paused,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -151,6 +154,7 @@ impl RestaurantRegistry {
slug: String,
) {
caller.require_auth();
Self::assert_not_paused_for(&env, &caller);

let mut restaurant: Restaurant = env
.storage()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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");
}
}
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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);
}
}
Loading