Skip to content
Open
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
4 changes: 3 additions & 1 deletion contracts/progress-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
13 changes: 12 additions & 1 deletion tests/integration/emergency_pause_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 ───────────────────────────
Expand Down
28 changes: 26 additions & 2 deletions tests/unit/progress_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down