diff --git a/SECURITY.md b/SECURITY.md index 64ec779..623e230 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -180,11 +180,11 @@ This document provides a structured security audit checklist for reviewing the C | Contract | Files | Lines | |---|---|---| -| `learn-token` | `lib.rs`, `storage.rs`, `events.rs` | ~1400 | -| `credential-nft` | `lib.rs`, `mint.rs`, `verify.rs`, `metadata.rs`, `xcall.rs` | ~750 | -| `progress-tracker` | `lib.rs`, `types.rs`, `rewards.rs` | ~1500 | -| `shared` | `constants.rs`, `lib.rs` | ~60 | -| **Total** | | **~3710** | +| `learn-token` | `lib.rs`, `storage.rs`, `events.rs` | ~5230 | +| `credential-nft` | `lib.rs`, `mint.rs`, `verify.rs`, `metadata.rs`, `xcall.rs`, `events.rs` | ~2660 | +| `progress-tracker` | `lib.rs`, `types.rs`, `rewards.rs`, `events.rs` | ~5050 | +| `shared` | `constants.rs`, `lib.rs` | ~100 | +| **Total** | | **~13040** | ## Known Limitations diff --git a/contracts/progress-tracker/src/lib.rs b/contracts/progress-tracker/src/lib.rs index 8fb92da..1b73228 100644 --- a/contracts/progress-tracker/src/lib.rs +++ b/contracts/progress-tracker/src/lib.rs @@ -2096,7 +2096,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( @@ -2692,6 +2691,21 @@ mod tests { ); } + #[test] + #[should_panic(expected = "contract is paused")] + fn test_enroll_checked_panics_when_paused() { + let env = Env::default(); + let (_admin, contract_id) = setup_contract(&env); + let client = ProgressTrackerClient::new(&env, &contract_id); + + env.mock_all_auths(); + let course_id = create_test_course(&env, &client); + let learner = Address::generate(&env); + + client.emergency_pause(); + client.enroll_checked(&learner, &course_id, &None); + } + // ── Issue #265: course last updated timestamp ─────────────────────── #[test] @@ -4306,6 +4320,43 @@ mod tests { assert_eq!(client.get_quiz_score(&learner, &course_id, &quiz_1), 90); } + #[test] + #[should_panic(expected = "contract is paused")] + fn test_delegate_progress_panics_when_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.emergency_pause(); + client.delegate_progress(&learner, &delegate); + } + + #[test] + fn test_revoke_delegation_succeeds_when_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)); + + client.emergency_pause(); + + let other = Address::generate(&env); + assert!(client.try_delegate_progress(&learner, &other).is_err()); + + client.revoke_delegation(&learner); + assert_eq!(client.delegated_to(&learner), None); + } + // ── Issue #234: quiz retake support ─────────────────────────────────── #[test] diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md index 0936e71..68494e0 100644 --- a/docs/upgrade-guide.md +++ b/docs/upgrade-guide.md @@ -131,11 +131,19 @@ soroban contract invoke \ ### Option C: Using Deploy Script +> [!NOTE] +> `scripts/deploy.sh` enforces a safety guard (lines 62-66) that refuses to run if `deployments-.json` already exists to prevent accidental overwrites. You must back up or remove the existing deployment file before redeploying. + ```bash -# Re-deploy and upgrade all contracts +# 1. Back up existing deployment file (recommended before re-deploying) +mv deployments-testnet.json deployments-testnet.backup.$(date +%s).json +# Alternatively, remove it if no backup is needed: +# rm deployments-testnet.json + +# 2. Re-deploy all contracts ./scripts/deploy.sh testnet -# Re-initialize if needed (only if initialization logic changed) +# 3. Re-initialize if needed (only if initialization logic changed) ./scripts/initialize.sh testnet ``` @@ -307,10 +315,11 @@ soroban contract invoke \ If rollback is not possible: -1. Deploy fresh contracts -2. Re-initialize with same parameters -3. Migrate state (if needed) via custom migration script -4. Update all cross-contract references +1. Back up or remove existing deployment file (`mv deployments-.json deployments-.backup.json`) to satisfy `deploy.sh` guards +2. Deploy fresh contracts (`./scripts/deploy.sh `) +3. Re-initialize with same parameters (`./scripts/initialize.sh `) +4. Migrate state (if needed) via custom migration script +5. Update all cross-contract references ## Upgrade Checklist diff --git a/tests/integration/emergency_pause_flow.rs b/tests/integration/emergency_pause_flow.rs index 4f4d8f4..a7b6ff3 100644 --- a/tests/integration/emergency_pause_flow.rs +++ b/tests/integration/emergency_pause_flow.rs @@ -120,6 +120,9 @@ fn test_progress_tracker_emergency_pause() { assert!(progress_client .try_enroll(&new_learner, &course_id) .is_err()); + assert!(progress_client + .try_enroll_checked(&new_learner, &course_id, &None) + .is_err()); // ── Module completion reverts when paused ── // Enroll a learner before pausing so we can test complete_module. diff --git a/tests/unit/progress_tests.rs b/tests/unit/progress_tests.rs index a434a7f..9dec4c2 100644 --- a/tests/unit/progress_tests.rs +++ b/tests/unit/progress_tests.rs @@ -1702,6 +1702,30 @@ mod progress_unit_tests { assert_eq!(client.delegated_to(&learner), None); } + #[test] + fn test_revoke_delegation_succeeds_when_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)); + + client.emergency_pause(); + + // delegate_progress must fail when paused + let other = Address::generate(&env); + assert!(client.try_delegate_progress(&learner, &other).is_err()); + + // revoke_delegation must succeed when paused + assert!(client.try_revoke_delegation(&learner).is_ok()); + assert_eq!(client.delegated_to(&learner), None); + } + #[test] #[should_panic] fn test_revoke_delegation_requires_learner_auth() {