Skip to content
Merged
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
10 changes: 5 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 52 additions & 1 deletion contracts/progress-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
21 changes: 15 additions & 6 deletions docs/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<network>.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
```

Expand Down Expand Up @@ -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-<network>.json deployments-<network>.backup.json`) to satisfy `deploy.sh` guards
2. Deploy fresh contracts (`./scripts/deploy.sh <network>`)
3. Re-initialize with same parameters (`./scripts/initialize.sh <network>`)
4. Migrate state (if needed) via custom migration script
5. Update all cross-contract references

## Upgrade Checklist

Expand Down
3 changes: 3 additions & 0 deletions tests/integration/emergency_pause_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/progress_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down