From a1de11ab98fb6a07362f7db5432b3eab3128d62d Mon Sep 17 00:00:00 2001 From: mj Date: Fri, 2 Oct 2026 14:59:16 +0700 Subject: [PATCH] fix(progress-tracker): allow revoke_delegation during emergency pause (#492) --- contracts/progress-tracker/src/lib.rs | 4 +++- tests/integration/emergency_pause_flow.rs | 13 ++++++++++- tests/unit/progress_tests.rs | 28 +++++++++++++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/contracts/progress-tracker/src/lib.rs b/contracts/progress-tracker/src/lib.rs index 8fb92da..9189538 100644 --- a/contracts/progress-tracker/src/lib.rs +++ b/contracts/progress-tracker/src/lib.rs @@ -2080,6 +2080,9 @@ impl ProgressTracker { /// Revoke `learner`'s active progress delegation, if any. Learner only. /// + /// Permitted even while the contract is paused so that learners can revoke + /// compromised or malicious delegates during an emergency (#492). + /// /// After this, only `learner` themselves may call the state-changing /// progress functions for their own progress. A no-op is not treated as /// an error: revoking when there is no active delegation simply leaves @@ -2096,7 +2099,6 @@ impl ProgressTracker { /// client.revoke_delegation(&learner); /// ``` pub fn revoke_delegation(env: Env, learner: Address) { - Self::require_not_paused(&env); learner.require_auth(); types::remove_entry( diff --git a/tests/integration/emergency_pause_flow.rs b/tests/integration/emergency_pause_flow.rs index 4f4d8f4..6f960c9 100644 --- a/tests/integration/emergency_pause_flow.rs +++ b/tests/integration/emergency_pause_flow.rs @@ -26,7 +26,7 @@ fn test_token_emergency_pause_prevents_state_changes() { env.mock_all_auths(); let token_client = LearnTokenClient::new(env, &setup.token_contract_id); - let progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id); + let _progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id); // Mint tokens to the learner so we have something to transfer/burn. token_client.mint(admin, learner, &10_000); @@ -150,6 +150,17 @@ fn test_progress_tracker_emergency_pause() { progress_client.complete_module(learner, &course_id, &Symbol::new(env, "mod_basics")); let progress = progress_client.get_progress(learner, &course_id); assert!(progress.overall_progress > 0); + + // ── Delegation pause flow (#492) ── + let delegate = Address::generate(env); + progress_client.delegate_progress(learner, &delegate); + assert_eq!(progress_client.delegated_to(learner), Some(delegate)); + + // Pause again: revoking delegation remains allowed during emergency pause + progress_client.emergency_pause(); + progress_client.revoke_delegation(learner); + assert_eq!(progress_client.delegated_to(learner), None); + progress_client.unpause(); } // ── Issue #281: credential-nft emergency pause ─────────────────────────── diff --git a/tests/unit/progress_tests.rs b/tests/unit/progress_tests.rs index a434a7f..2852d9e 100644 --- a/tests/unit/progress_tests.rs +++ b/tests/unit/progress_tests.rs @@ -760,8 +760,7 @@ mod progress_unit_tests { assert!(result.is_err(), "second initialize call should fail"); let contract_err = result - .err() - .expect("expected an error") + .expect_err("expected an error") .expect("expected a typed contract error, not a host trap"); assert_eq!( contract_err, @@ -1702,6 +1701,31 @@ mod progress_unit_tests { assert_eq!(client.delegated_to(&learner), None); } + #[test] + fn test_revoke_delegation_succeeds_while_paused() { + let env = Env::default(); + let (_admin, contract_id) = setup_contract(&env); + let client = ProgressTrackerClient::new(&env, &contract_id); + + env.mock_all_auths(); + let learner = Address::generate(&env); + let delegate = Address::generate(&env); + + client.delegate_progress(&learner, &delegate); + assert_eq!(client.delegated_to(&learner), Some(delegate)); + + // Emergency pause + client.emergency_pause(); + + // Attempting to delegate while paused must fail + let second_delegate = Address::generate(&env); + assert!(client.try_delegate_progress(&learner, &second_delegate).is_err()); + + // Revoking delegation must succeed even while paused (#492) + client.revoke_delegation(&learner); + assert_eq!(client.delegated_to(&learner), None); + } + #[test] #[should_panic] fn test_revoke_delegation_requires_learner_auth() {