diff --git a/app/contract/Cargo.lock b/app/contract/Cargo.lock index d36c03a78..52e49599d 100644 --- a/app/contract/Cargo.lock +++ b/app/contract/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "RustAcademy" -version = "0.1.0" -dependencies = [ - "blake3", - "hex", - "proptest", - "serde_json", - "soroban-sdk", -] - [[package]] name = "ahash" version = "0.8.12" @@ -628,9 +617,9 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "ff" @@ -1154,6 +1143,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "rust_academy" +version = "0.1.0" +dependencies = [ + "blake3", + "hex", + "proptest", + "serde_json", + "soroban-sdk", +] + [[package]] name = "rustc_version" version = "0.4.1" diff --git a/app/contract/contracts/Folder/src/coverage_test.rs b/app/contract/contracts/Folder/src/coverage_test.rs index 2d4f12701..05d0ad3e9 100644 --- a/app/contract/contracts/Folder/src/coverage_test.rs +++ b/app/contract/contracts/Folder/src/coverage_test.rs @@ -368,7 +368,7 @@ fn test_demo_expiry_and_refund_under_10_lines() { /// Privacy toggle and balance privacy using TestContext in ≤ 10 lines. #[test] fn test_demo_privacy_toggle_under_10_lines() { - let ctx = TestContext::new(); // 1 + let ctx = TestContext::with_admin(); // 1 — set_privacy requires initialization assert!(!ctx.client.get_privacy(&ctx.alice)); // 2 ctx.client.set_privacy(&ctx.alice, &true); // 3 assert!(ctx.client.get_privacy(&ctx.alice)); // 4 diff --git a/app/contract/contracts/Folder/src/lib.rs b/app/contract/contracts/Folder/src/lib.rs index ec49c2f89..5bc07fb77 100644 --- a/app/contract/contracts/Folder/src/lib.rs +++ b/app/contract/contracts/Folder/src/lib.rs @@ -219,17 +219,24 @@ impl RustAcademyContract { /// Enable or disable privacy for an account. /// + /// Access: **owner** — `owner` must authorize the call. Gated by the + /// [`PauseFlag::SetPrivacy`] feature flag. The auth check is enforced + /// inside [`privacy::set_privacy`]. + /// /// # Arguments /// * `env` - The contract environment - /// * `owner` - The account address to configure + /// * `owner` - The account address to configure (must authorize) /// * `enabled` - `true` to enable privacy, `false` to disable /// /// # Errors - /// * `ContractPaused` - Contract is currently paused + /// * `Unauthorized` - Contract is not initialized + /// * `OperationPaused` - The `SetPrivacy` feature is paused /// * `PrivacyAlreadySet` - Privacy state is already at the requested value pub fn set_privacy(env: Env, owner: Address, enabled: bool) -> Result<(), RustAcademyError> { + // guard_initialized ensures the contract is properly set up before any + // state is mutated. Owner auth is enforced inside privacy::set_privacy. admin::guard_initialized(&env)?; - if is_feature_paused(&env, PauseFlag::SetPrivacy) { + if storage::is_feature_paused(&env, PauseFlag::SetPrivacy) { return Err(RustAcademyError::OperationPaused); } privacy::set_privacy(&env, owner, enabled) @@ -359,13 +366,20 @@ impl RustAcademyContract { /// Returns the new counter value. Parameters `_from`, `_to`, `_amount` are reserved for /// future use; the implementation only increments the counter. /// + /// Access: requires initialized contract. Gated by `guard_initialized` to prevent + /// counter manipulation on an uninitialized deployment. + /// /// # Arguments /// * `env` - The contract environment /// * `_from` - Reserved (depositor address for future use) /// * `_to` - Reserved (recipient address for future use) /// * `_amount` - Reserved (amount for future use) - pub fn create_escrow(env: Env, _from: Address, _to: Address, _amount: u64) -> u64 { - increment_escrow_counter(&env) + /// + /// # Errors + /// * `Unauthorized` - Contract has not been initialized + pub fn create_escrow(env: Env, _from: Address, _to: Address, _amount: u64) -> Result { + admin::guard_initialized(&env)?; + Ok(increment_escrow_counter(&env)) } /// Health check for deployment and monitoring. @@ -572,7 +586,7 @@ impl RustAcademyContract { env: Env, stealth_address: BytesN<32>, ) -> Result<(), RustAcademyError> { - admin::require_initialized(&env)?; + admin::guard_initialized(&env)?; stealth::cleanup_stealth_escrow(&env, stealth_address) } @@ -873,13 +887,6 @@ impl RustAcademyContract { admin::set_paused(&env, caller, new_state) } - /// Check if the function is currently paused. - /// - /// Returns `true` if paused, `false` otherwise. - pub fn is_feature_paused(env: &Env, flag: PauseFlag) -> bool { - storage::is_feature_paused(env, flag) - } - /// Pause a function in the contract (**Admin only**). /// /// When paused, the particular operations is blocked. Caller must equal the stored admin. diff --git a/app/contract/contracts/Folder/src/role_test.rs b/app/contract/contracts/Folder/src/role_test.rs index 49b124ada..70ba1e6dd 100644 --- a/app/contract/contracts/Folder/src/role_test.rs +++ b/app/contract/contracts/Folder/src/role_test.rs @@ -234,4 +234,205 @@ fn test_gated_privacy_and_commitment_work_when_unpaused() { let _ = ctx .client .create_amount_commitment(&ctx.alice, &1_000i128, &salt); +} + +// ============================================================================ +// Issue #53 — standardized guard helper coverage +// ============================================================================ + +/// `set_privacy` requires the owner to authorize — succeeds when called by the owner. +#[test] +fn test_set_privacy_requires_owner_auth() { + let ctx = TestContext::with_admin(); + // In tests all auths are mocked; calling set_privacy with alice means alice + // authorizes her own address, which must succeed. + let res = ctx.client.try_set_privacy(&ctx.alice, &true); + assert!(res.is_ok(), "owner should be allowed to set their own privacy"); +} + +/// `set_privacy` is rejected when the contract has not been initialized. +#[test] +fn test_set_privacy_blocked_when_uninitialized() { + // TestContext::new() does NOT call initialize() + let ctx = TestContext::new(); + let res = ctx.client.try_set_privacy(&ctx.alice, &true); + assert!( + res.is_err(), + "set_privacy must be blocked before contract is initialized" + ); +} + +/// `create_escrow` is gated by `guard_initialized` and fails before initialization. +#[test] +fn test_create_escrow_blocked_when_uninitialized() { + let ctx = TestContext::new(); + let res = ctx.client.try_create_escrow(&ctx.alice, &ctx.bob, &100u64); + assert!( + res.is_err(), + "create_escrow must fail when the contract is not initialized" + ); +} + +/// `create_escrow` succeeds after the contract is initialized and increments the counter. +#[test] +fn test_create_escrow_works_when_initialized() { + let ctx = TestContext::with_admin(); + let counter = ctx.client.create_escrow(&ctx.alice, &ctx.bob, &0u64); + assert_eq!(counter, 1u64, "first create_escrow call should return counter = 1"); +} + +/// `cleanup_stealth_escrow` uses `guard_initialized` — blocked before init. +#[test] +fn test_cleanup_stealth_escrow_blocked_when_uninitialized() { + let ctx = TestContext::new(); + let dummy: soroban_sdk::BytesN<32> = soroban_sdk::BytesN::from_array(&ctx.env, &[0u8; 32]); + let res = ctx.client.try_cleanup_stealth_escrow(&dummy); + assert!( + res.is_err(), + "cleanup_stealth_escrow must fail when contract is not initialized" + ); +} + +/// Deposit is blocked by the global pause flag (tests `guard_deposit`). +#[test] +fn test_deposit_blocked_when_globally_paused() { + let ctx = TestContext::with_admin(); + ctx.client.set_paused(&ctx.admin, &true); + + ctx.mint(&ctx.alice, 1000); + let res = ctx.client.try_deposit( + &ctx.token, + &1000i128, + &ctx.alice, + &ctx.salt(b"paused"), + &0u64, + &None, + ); + assert!( + res.is_err(), + "deposit must be blocked when the contract is globally paused" + ); +} + +/// Deposit is blocked by the feature-level pause flag (tests `guard_deposit` feature gate). +#[test] +fn test_deposit_blocked_when_deposit_feature_paused() { + let ctx = TestContext::with_admin(); + ctx.client + .pause_features(&ctx.admin, &(storage::PauseFlag::Deposit as u64)); + + ctx.mint(&ctx.alice, 1000); + let res = ctx.client.try_deposit( + &ctx.token, + &1000i128, + &ctx.alice, + &ctx.salt(b"feat_paused"), + &0u64, + &None, + ); + assert!( + res.is_err(), + "deposit must be blocked when the Deposit feature flag is paused" + ); +} + +/// Deposit is blocked in emergency mode (tests that `guard_deposit` includes emergency check). +#[test] +fn test_deposit_blocked_in_emergency_mode() { + let ctx = TestContext::with_admin(); + ctx.client.activate_emergency_mode(&ctx.admin); + + ctx.mint(&ctx.alice, 1000); + let res = ctx.client.try_deposit( + &ctx.token, + &1000i128, + &ctx.alice, + &ctx.salt(b"emergency_deposit"), + &0u64, + &None, + ); + assert!( + res.is_err(), + "deposit must be blocked in emergency mode" + ); +} + +/// Refund is blocked when the global pause flag is set (tests `guard_refund`). +#[test] +fn test_refund_blocked_when_globally_paused() { + let ctx = TestContext::with_admin(); + + ctx.mint(&ctx.alice, 500); + let commitment = ctx.client.deposit( + &ctx.token, + &500i128, + &ctx.alice, + &ctx.salt(b"refund_pause"), + &1u64, // 1 second timeout + &None, + ); + + // Advance time past expiry + ctx.advance_time(100); + + // Now pause + ctx.client.set_paused(&ctx.admin, &true); + + let res = ctx.client.try_refund(&commitment, &ctx.alice); + assert!( + res.is_err(), + "refund must be blocked when the contract is globally paused" + ); +} + +/// Dispute operations are blocked when the contract is globally paused (tests `guard_dispute`). +#[test] +fn test_dispute_blocked_when_globally_paused() { + let ctx = TestContext::with_admin(); + let arbiter = Address::generate(&ctx.env); + + ctx.mint(&ctx.alice, 1000); + let commitment = ctx.client.deposit( + &ctx.token, + &1000i128, + &ctx.alice, + &ctx.salt(b"dispute_pause"), + &0u64, + &Some(arbiter.clone()), + ); + + ctx.client.set_paused(&ctx.admin, &true); + + let res = ctx.client.try_dispute(&commitment); + assert!( + res.is_err(), + "dispute must be blocked when the contract is globally paused" + ); +} + +/// Admin configuration calls are blocked in emergency mode (tests `guard_admin_config`). +#[test] +fn test_set_paused_blocked_in_emergency_mode() { + let ctx = TestContext::with_admin(); + ctx.client.activate_emergency_mode(&ctx.admin); + + let res = ctx.client.try_set_paused(&ctx.admin, &false); + assert!( + res.is_err(), + "set_paused must be blocked once emergency mode is active" + ); +} + +/// `guard_initialized` returns Unauthorized when the contract is not yet initialized. +#[test] +fn test_guard_initialized_blocks_uninitialized_ops() { + let ctx = TestContext::new(); + + // cleanup_escrow uses guard_initialized + let dummy: soroban_sdk::BytesN<32> = soroban_sdk::BytesN::from_array(&ctx.env, &[1u8; 32]); + let res = ctx.client.try_cleanup_escrow(&dummy); + assert!( + res.is_err(), + "cleanup_escrow must fail on an uninitialized contract" + ); } \ No newline at end of file diff --git a/app/contract/contracts/Folder/src/test.rs b/app/contract/contracts/Folder/src/test.rs index 50c232962..bbc1872ed 100644 --- a/app/contract/contracts/Folder/src/test.rs +++ b/app/contract/contracts/Folder/src/test.rs @@ -729,6 +729,8 @@ fn test_commitment_cycle() { #[test] fn test_create_escrow() { let (env, client) = setup(); + let admin = Address::generate(&env); + client.initialize(&admin); let from = Address::generate(&env); let to = Address::generate(&env); let amount = 1_000;