From fa73d744a68ff0f44529951a6c3201d414cf70b2 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 29 Jul 2026 15:35:41 +0000 Subject: [PATCH] feat: audit pause unpause access control --- .../contracts/compliance/src/lib.rs | 150 +++++++++++++++- .../contracts/invoice/src/lib.rs | 107 +++++++++++ .../contracts/treasury/src/lib.rs | 168 ++++++++++++++++++ 3 files changed, 423 insertions(+), 2 deletions(-) diff --git a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs index 262e17f..e117d66 100644 --- a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs @@ -122,6 +122,7 @@ impl ComplianceContract { } pub fn accept_admin(e: Env, new_admin: Address) -> Result<(), ContractError> { + check_not_paused(&e)?; new_admin.require_auth(); let pending: Address = e .storage() @@ -163,14 +164,28 @@ impl ComplianceContract { Ok(()) } - pub fn pause(e: Env, admin: Address) { + pub fn pause(e: Env, admin: Address) -> Result<(), ContractError> { admin.require_auth(); + let stored_admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap(); + if admin != stored_admin { + return Err(ContractError::Unauthorized); + } e.storage().instance().set(&DataKey::Paused, &true); + e.events() + .publish((Symbol::new(&e, "contract_paused"),), ()); + Ok(()) } - pub fn unpause(e: Env, admin: Address) { + pub fn unpause(e: Env, admin: Address) -> Result<(), ContractError> { admin.require_auth(); + let stored_admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap(); + if admin != stored_admin { + return Err(ContractError::Unauthorized); + } e.storage().instance().set(&DataKey::Paused, &false); + e.events() + .publish((Symbol::new(&e, "contract_unpaused"),), ()); + Ok(()) } } @@ -223,4 +238,135 @@ mod tests { c.allow_address(&admin, &addr); assert!(c.is_allowed(&addr)); } + + // ── pause / unpause and admin guard tests ───────────────────────────────── + + #[test] + fn test_pause_emits_event() { + let e = Env::default(); + e.mock_all_auths(); + let contract_id = e.register(ComplianceContract, ()); + let admin = Address::generate(&e); + ComplianceContractClient::new(&e, &contract_id).initialize(&admin); + let c = ComplianceContractClient::new(&e, &contract_id); + + c.pause(&admin); + + let all_events = e.events().all(); + assert!( + all_events.iter().any(|ev| ev.0 == (contract_id, "contract_paused".into())), + "contract_paused event should be emitted" + ); + } + + #[test] + fn test_unpause_emits_event() { + let e = Env::default(); + e.mock_all_auths(); + let contract_id = e.register(ComplianceContract, ()); + let admin = Address::generate(&e); + ComplianceContractClient::new(&e, &contract_id).initialize(&admin); + let c = ComplianceContractClient::new(&e, &contract_id); + + c.pause(&admin); + c.unpause(&admin); + + let all_events = e.events().all(); + assert!( + all_events.iter().any(|ev| ev.0 == (contract_id, "contract_unpaused".into())), + "contract_unpaused event should be emitted" + ); + } + + #[test] + fn test_pause_unauthorized_fails() { + let e = Env::default(); + e.mock_all_auths(); + let contract_id = e.register(ComplianceContract, ()); + let admin = Address::generate(&e); + let non_admin = Address::generate(&e); + ComplianceContractClient::new(&e, &contract_id).initialize(&admin); + let c = ComplianceContractClient::new(&e, &contract_id); + + let res = c.try_pause(&non_admin); + assert_eq!(res, Err(Ok(ContractError::Unauthorized))); + } + + #[test] + fn test_unpause_unauthorized_fails() { + let e = Env::default(); + e.mock_all_auths(); + let contract_id = e.register(ComplianceContract, ()); + let admin = Address::generate(&e); + let non_admin = Address::generate(&e); + ComplianceContractClient::new(&e, &contract_id).initialize(&admin); + let c = ComplianceContractClient::new(&e, &contract_id); + + c.pause(&admin); + let res = c.try_unpause(&non_admin); + assert_eq!(res, Err(Ok(ContractError::Unauthorized))); + } + + #[test] + fn test_accept_admin_when_paused_fails() { + let e = Env::default(); + e.mock_all_auths(); + let contract_id = e.register(ComplianceContract, ()); + let admin = Address::generate(&e); + let new_admin = Address::generate(&e); + ComplianceContractClient::new(&e, &contract_id).initialize(&admin); + let c = ComplianceContractClient::new(&e, &contract_id); + + c.pause(&admin); + let res = c.try_accept_admin(&new_admin); + assert_eq!(res, Err(Ok(ContractError::ContractPaused))); + } + + #[test] + fn test_mutating_entrypoints_blocked_when_paused() { + let e = Env::default(); + e.mock_all_auths(); + let contract_id = e.register(ComplianceContract, ()); + let admin = Address::generate(&e); + let addr = Address::generate(&e); + ComplianceContractClient::new(&e, &contract_id).initialize(&admin); + let c = ComplianceContractClient::new(&e, &contract_id); + + c.pause(&admin); + + assert_eq!( + c.try_allow_address(&admin, &addr), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + c.try_block_address(&admin, &addr), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + c.try_allow_address_until(&admin, &addr, &1000u64), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + c.try_clear_address(&admin, &addr), + Err(Ok(ContractError::ContractPaused)) + ); + } + + #[test] + fn test_readonly_entrypoints_work_when_paused() { + let e = Env::default(); + e.mock_all_auths(); + let contract_id = e.register(ComplianceContract, ()); + let admin = Address::generate(&e); + let addr = Address::generate(&e); + ComplianceContractClient::new(&e, &contract_id).initialize(&admin); + let c = ComplianceContractClient::new(&e, &contract_id); + + c.allow_address(&admin, &addr); + c.pause(&admin); + + assert!(c.is_allowed(&addr)); + let status = c.get_address_status(&addr); + assert!(matches!(status, AddressStatus::Allowed)); + } } diff --git a/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs b/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs index 4a9d167..62551cb 100644 --- a/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs @@ -303,6 +303,7 @@ impl InvoiceContract { /// Configure the treasury contract address (admin only). pub fn set_treasury(env: Env, caller: Address, treasury: Address) -> Result<(), ContractError> { + check_not_paused(&env)?; check_admin(&env, &caller)?; env.storage() .persistent() @@ -672,4 +673,110 @@ mod tests { let result = invoice_client.try_raise_dispute(&invoice_id, &1u64, &claimant, &1u32); assert_eq!(result, Err(Ok(ContractError::ContractPaused))); } + + #[test] + fn test_set_treasury_when_paused_returns_contract_paused() { + let (env, invoice_cid, _treasury_cid, admin, _claimant) = setup_with_treasury(1000); + let invoice_client = InvoiceContractClient::new(&env, &invoice_cid); + let new_treasury = Address::generate(&env); + + invoice_client.pause(&admin); + + let result = invoice_client.try_set_treasury(&admin, &new_treasury); + assert_eq!(result, Err(Ok(ContractError::ContractPaused))); + } + + #[test] + fn test_pause_emits_event() { + let (env, invoice_cid, _treasury_cid, admin, _claimant) = setup_with_treasury(1000); + let invoice_client = InvoiceContractClient::new(&env, &invoice_cid); + + invoice_client.pause(&admin); + + let all_events = env.events().all(); + assert!( + all_events.iter().any(|ev| ev.0 == (invoice_cid, "contract_paused".into())), + "contract_paused event should be emitted" + ); + } + + #[test] + fn test_unpause_emits_event() { + let (env, invoice_cid, _treasury_cid, admin, _claimant) = setup_with_treasury(1000); + let invoice_client = InvoiceContractClient::new(&env, &invoice_cid); + + invoice_client.pause(&admin); + invoice_client.unpause(&admin); + + let all_events = env.events().all(); + assert!( + all_events.iter().any(|ev| ev.0 == (invoice_cid, "contract_unpaused".into())), + "contract_unpaused event should be emitted" + ); + } + + #[test] + fn test_mutating_entrypoints_blocked_when_paused() { + let (env, invoice_cid, _treasury_cid, admin, _claimant) = setup_with_treasury(1000); + let invoice_client = InvoiceContractClient::new(&env, &invoice_cid); + + let merchant = Address::generate(&env); + let customer = Address::generate(&env); + let token = Address::generate(&env); + let invoice_id = + invoice_client.create_invoice(&merchant, &customer, &100i128, &token, &9999, &1); + + invoice_client.pause(&admin); + + assert_eq!( + invoice_client.try_create_invoice(&merchant, &customer, &100i128, &token, &9999, &2), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + invoice_client.try_mark_paids(&soroban_sdk::vec![&env, invoice_id]), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + invoice_client.try_cancel_invoiced(&invoice_id, &merchant), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + invoice_client.try_request_refund(&invoice_id, &customer), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + invoice_client.try_release_escrow(&invoice_id, &merchant), + Err(Ok(ContractError::ContractPaused)) + ); + assert_eq!( + invoice_client.try_batch_expire(&soroban_sdk::vec![&env, invoice_id]), + Err(Ok(ContractError::ContractPaused)) + ); + } + + #[test] + fn test_readonly_entrypoints_work_when_paused() { + let (env, invoice_cid, _treasury_cid, admin, _claimant) = setup_with_treasury(1000); + let invoice_client = InvoiceContractClient::new(&env, &invoice_cid); + + let merchant = Address::generate(&env); + let customer = Address::generate(&env); + let token = Address::generate(&env); + let invoice_id = + invoice_client.create_invoice(&merchant, &customer, &100i128, &token, &9999, &1); + + invoice_client.pause(&admin); + + let invoice = invoice_client.get_invoice(&invoice_id); + assert_eq!(invoice.id, invoice_id); + + let status = invoice_client.get_invoice_status(&invoice_id); + assert_eq!(status, InvoiceStatus::Pending); + + let grace = invoice_client.get_grace_window(); + assert_eq!(grace, 86400); + + let treasury = invoice_client.get_treasury(); + assert!(treasury.is_some()); + } } diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs b/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs index 3042e8e..fdafb3b 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs @@ -253,12 +253,20 @@ impl TreasuryContract { pub fn pause(e: Env, admin: Address) -> Result<(), TreasuryError> { Self::check_admin(&e, &admin)?; e.storage().instance().set(&DataKey::Paused, &true); + e.events().publish( + (Symbol::new(&e, "contract_paused"),), + (), + ); Ok(()) } pub fn unpause(e: Env, admin: Address) -> Result<(), TreasuryError> { Self::check_admin(&e, &admin)?; e.storage().instance().set(&DataKey::Paused, &false); + e.events().publish( + (Symbol::new(&e, "contract_unpaused"),), + (), + ); Ok(()) } @@ -681,4 +689,164 @@ mod tests { let pending = c.get_pending_settlements(&None, &None); assert!(!pending.contains(&sid)); } + + // ── pause / unpause event tests ────────────────────────────────────────── + + #[test] + fn test_pause_emits_event() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.pause(&admin); + let all_events = e.events().all(); + assert!( + all_events.iter().any(|ev| ev.0 == (id, "contract_paused".into())), + "contract_paused event should be emitted" + ); + } + + #[test] + fn test_unpause_emits_event() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.pause(&admin); + c.unpause(&admin); + let all_events = e.events().all(); + assert!( + all_events.iter().any(|ev| ev.0 == (id, "contract_unpaused".into())), + "contract_unpaused event should be emitted" + ); + } + + #[test] + fn test_pause_unauthorized_fails() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let non_admin = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + let res = c.try_pause(&non_admin); + assert_eq!(res, Err(Ok(TreasuryError::Unauthorized))); + } + + #[test] + fn test_unpause_unauthorized_fails() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let non_admin = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.pause(&admin); + let res = c.try_unpause(&non_admin); + assert_eq!(res, Err(Ok(TreasuryError::Unauthorized))); + } + + #[test] + fn test_raise_dispute_when_paused_returns_contract_paused() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let token = soroban_sdk::Address::generate(&e); + let merchant = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + let sid = c.propose_settlement(&signer, &token, &100u64, &merchant); + c.pause(&admin); + let res = c.try_raise_dispute(&merchant, &sid, &1u32); + assert_eq!(res, Err(Ok(TreasuryError::ContractPaused))); + } + + #[test] + fn test_resolve_dispute_when_paused_returns_contract_paused() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let token = soroban_sdk::Address::generate(&e); + let merchant = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + let sid = c.propose_settlement(&signer, &token, &100u64, &merchant); + c.pause(&admin); + let res = c.try_resolve_dispute(&signer, &sid, &true); + assert_eq!(res, Err(Ok(TreasuryError::ContractPaused))); + } + + #[test] + fn test_deposit_when_paused_returns_contract_paused() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let from = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.pause(&admin); + let res = c.try_deposit(&from, &100u64); + assert_eq!(res, Err(Ok(TreasuryError::ContractPaused))); + } + + #[test] + fn test_withdraw_when_paused_returns_contract_paused() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let to = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.pause(&admin); + let res = c.try_withdraw(&admin, &to, &100u64); + assert_eq!(res, Err(Ok(TreasuryError::ContractPaused))); + } + + #[test] + fn test_add_token_to_allowlist_when_paused_returns_contract_paused() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let token = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.pause(&admin); + let res = c.try_add_token_to_allowlist(&admin, &token); + assert_eq!(res, Err(Ok(TreasuryError::ContractPaused))); + } + + #[test] + fn test_remove_token_from_allowlist_when_paused_returns_contract_paused() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let token = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.add_token_to_allowlist(&admin, &token); + c.pause(&admin); + let res = c.try_remove_token_from_allowlist(&admin, &token); + assert_eq!(res, Err(Ok(TreasuryError::ContractPaused))); + } + + #[test] + fn test_readonly_entrypoints_work_when_paused() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + let token = soroban_sdk::Address::generate(&e); + let merchant = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.propose_settlement(&signer, &token, &100u64, &merchant); + c.pause(&admin); + + let threshold = c.get_threshold(); + assert_eq!(threshold, 1); + + let pending = c.get_pending_settlements(&None, &None); + assert_eq!(pending.len(), 1); + } }