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
208 changes: 192 additions & 16 deletions app/contract/UPGRADE_SAFETY_GATE_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
# Issue #432 + #318: Upgrade Safety Gate Implementation Summary
# Issue #432 + #318 + #554: Upgrade Safety Gate Implementation Summary

**Status**: Complete
**Complexity**: High (200 points)
**Wave**: 5 – Lifecycle Management
**Date**: May 29, 2026
**Updated**: July 20, 2026 (Issue #318 – gate master switch + regression coverage)
**Updated**: August 19, 2026 (Issue #554 – re-entry protection, invariant drift detection, repeated-init prevention)

---

## Overview

Added contract-level safeguards and comprehensive invariant enforcement for safe, controlled upgrades in RustAcademy. This implementation ensures upgrades only occur during admin-configured time windows and validates state machine consistency post-migration.

**Issue #554 Enhancements** (August 19, 2026):
- **Re-entry protection**: All upgrade lifecycle functions now check reentrancy guard to prevent callback-based attacks
- **Invariant drift detection**: Pre-upgrade invariant snapshotting detects unexpected state changes during migration
- **Repeated-init prevention**: Upgrade operations blocked on uninitialized contracts
- **Strengthened window validation**: Window checks enforced at all upgrade lifecycle stages

---

## Acceptance Criteria – All Met ✅

### Issue #554 Acceptance Criteria ✅

**AC1: Upgrade operations blocked when gate checks fail**
- ✅ Re-entry protection added to `start_upgrade`, `upgrade`, `complete_upgrade`, `cancel_upgrade`
- ✅ Window validation strengthened across all upgrade lifecycle functions
- ✅ Invariant drift detection in `complete_upgrade` blocks completion on state corruption

**AC2: Repeated or malicious initializer paths prevented**
- ✅ `start_upgrade` checks contract initialization before proceeding
- ✅ Upgrade operations fail on uninitialized contracts
- ✅ Legitimate upgrade flows remain functional after proper initialization

**AC3: Team documentation and test coverage**
- ✅ Comprehensive test coverage for re-entry protection (4 new tests)
- ✅ Invariant drift detection tests (3 new tests)
- ✅ Repeated-init prevention tests (2 new tests)
- ✅ Strengthened window validation tests (1 new test)
- ✅ Documentation updated with safe deployment and rollback expectations

### AC1: Upgrades Blocked Outside Window ✅

**What**: `start_upgrade()` and `upgrade()` deterministically fail if called outside the admin-configured time window or if no upgrade is in progress.
Expand Down Expand Up @@ -133,39 +159,46 @@ ORDER BY timestamp

### Files Modified

1. **storage.rs** (+66 lines)
- New `DataKey` variants: `UpgradeWindowStart`, `UpgradeWindowEnd`, `UpgradeInProgress`
1. **storage.rs** (+66 lines, +95 lines for Issue #554)
- New `DataKey` variants: `UpgradeWindowStart`, `UpgradeWindowEnd`, `UpgradeInProgress`, `PreUpgradeInvariantSnapshot`
- Functions: `set_upgrade_window()`, `get_upgrade_window()`, `is_upgrade_window_active()`, `set_upgrade_in_progress()`, `is_upgrade_in_progress()`, `assert_post_upgrade_invariants()`
- **Issue #554 additions**: `InvariantSnapshot` struct, `snapshot_pre_upgrade_invariants()`, `check_invariant_drift()`, `clear_invariant_snapshot()`

2. **events.rs** (+56 lines)
- New event structs: `UpgradeStartedEvent`, `UpgradeCompletedEvent`
- Functions: `publish_upgrade_started()`, `publish_upgrade_completed()`

3. **admin.rs** (+102 lines)
3. **admin.rs** (+102 lines, +20 lines for Issue #554)
- New functions: `set_upgrade_window()`, `start_upgrade()`, `complete_upgrade()`
- Modified `migrate()` to call `assert_post_upgrade_invariants()` before returning
- Added imports for new event publishers
- **Issue #554 additions**: Re-entry protection in all upgrade functions, initialization check in `start_upgrade`, invariant drift check in `complete_upgrade`

4. **lib.rs** (+114 lines)
4. **lib.rs** (+114 lines, +10 lines for Issue #554)
- Public entrypoints: `set_upgrade_window()`, `get_upgrade_window()`, `start_upgrade()`, `complete_upgrade()`
- Full docstrings with error codes and usage examples
- **Issue #554 additions**: `get_invariant_snapshot()` public view function

5. **upgrade_test.rs** (+155 lines)
5. **upgrade_test.rs** (+155 lines, +250 lines for Issue #554)
- Updated header comment to reference Issue #432
- Added 5 new test functions (safety gate tests)
- Tests cover all ACs and edge cases
- **Issue #554 additions**: 10 new test functions covering re-entry protection, invariant drift detection, repeated-init prevention, and strengthened window validation

### New Public Entrypoints

| Function | Admin-Only | Window-Gated | Emits Event |
| -------------------------------- | ---------- | ------------ | --------------------- |
| `set_upgrade_window(start, end)` | ✅ | ❌ | ✅ `UpgradeWindowSet` |
| `get_upgrade_window()` | ❌ | ❌ | ❌ |
| `start_upgrade(new_version)` | ✅ | ✅ | ✅ `UpgradeStarted` |
| `complete_upgrade(new_version)` | ✅ | ❌ | ✅ `UpgradeCompleted` |
| `set_upgrade_gate(enabled)` | ✅ | ❌ | ❌ |
| `check_upgrade_safety()` | ❌ | ❌ | ❌ (view) |
| `get_upgrade_status()` | ❌ | ❌ | ❌ (view) |
| Function | Admin-Only | Window-Gated | Emits Event | Re-entry Protected |
| -------------------------------- | ---------- | ------------ | --------------------- | ------------------ |
| `set_upgrade_window(start, end)` | ✅ | ❌ | ✅ `UpgradeWindowSet` | ❌ |
| `get_upgrade_window()` | ❌ | ❌ | ❌ | ❌ |
| `start_upgrade(new_version)` | ✅ | ✅ | ✅ `UpgradeStarted` | ✅ (Issue #554) |
| `complete_upgrade(new_version)` | ✅ | ❌ | ✅ `UpgradeCompleted` | ✅ (Issue #554) |
| `upgrade(new_wasm_hash)` | ✅ | ✅ | ✅ `ContractUpgraded` | ✅ (Issue #554) |
| `cancel_upgrade()` | ✅ | ❌ | ❌ | ✅ (Issue #554) |
| `set_upgrade_gate(enabled)` | ✅ | ❌ | ❌ | ❌ |
| `check_upgrade_safety()` | ❌ | ❌ | ❌ (view) | ❌ |
| `get_upgrade_status()` | ❌ | ❌ | ❌ (view) | ❌ |
| `get_invariant_snapshot()` | ❌ | ❌ | ❌ (view) | ❌ (Issue #554) |

---

Expand Down Expand Up @@ -309,25 +342,168 @@ test result: ok. 5 passed; 0 failed; 0 ignored

- [ ] Code reviewed and merged to `main`
- [ ] All tests passing: `cargo test upgrade_safety_gate_`
- [ ] Issue #554 tests passing: `cargo test upgrade_safety_gate_blocks_reentry upgrade_safety_gate_detects upgrade_safety_gate_blocks_start_on_uninitialized`
- [ ] Documentation complete: `UPGRADE_SAFETY_GATE.md` in `app/contract/docs/`
- [ ] Regression suite passing: `cargo test test_deposit test_successful_withdrawal test_refund_successful`
- [ ] Contract deployed with new WASM hash
- [ ] Set first upgrade window via admin TX
- [ ] Indexer configured to monitor `UpgradeStarted` / `UpgradeCompleted` events
- [ ] Monitoring/alerting on `InternalError` during `complete_upgrade()`
- [ ] Release notes include upgrade ceremony steps
- [ ] Monitoring/alerting on `ReentrancyDetected` during upgrade operations
- [ ] Release notes include upgrade ceremony steps and Issue #554 safety enhancements

---

## References

- **Issue #310**: Upgrade simulation test harness (foundational; now extended)
- **Issue #432**: This issue (safety gate + invariants)
- **Issue #554**: Re-entry protection, invariant drift detection, repeated-init prevention
- **Acceptance Criteria**: See `UPGRADE_SAFETY_GATE.md` for detailed validation
- **Events Schema**: `event_schema_version = 2` (consistent with #157, #305)

---

## Safe Deployment and Rollback Expectations (Issue #554)

### Pre-Deployment Safety Checks

Before initiating any upgrade, administrators should:

1. **Verify Contract Initialization**
- Ensure `is_initialized()` returns `true`
- Confirm admin role is properly set
- Check that no upgrade is currently in progress

2. **Validate Upgrade Window**
- Set appropriate `[start, end]` window for the upgrade
- Ensure sufficient time for the complete upgrade ceremony
- Consider network conditions and validator participation

3. **Check Invariant Snapshot**
- Call `check_upgrade_safety()` to verify all preconditions
- Review the `UpgradeSafetyReport` for any warnings
- Ensure fee config and other invariants are within bounds

### Upgrade Ceremony Steps

**Step 1: Preparation**
```bash
# Check current state
client.check_upgrade_safety()
client.get_upgrade_status()
client.get_invariant_snapshot() # Should be None before start_upgrade
```

**Step 2: Start Upgrade**
```bash
# Set window (if not already configured)
client.set_upgrade_window(admin, start_timestamp, end_timestamp)

# Start the upgrade (creates invariant snapshot)
client.start_upgrade(admin, new_version, new_wasm_hash)
# → Emits UpgradeStarted event
# → Creates pre-upgrade invariant snapshot
# → Sets UpgradeInProgress flag
```

**Step 3: Deploy WASM**
```bash
# Deploy the new WASM hash to the network
# This is done outside the contract via Stellar deployment tools
```

**Step 4: Complete Upgrade**
```bash
# Complete the upgrade (validates invariants and checks drift)
client.complete_upgrade(admin, new_version)
# → Calls migrate() internally
# → Validates post-upgrade invariants
# → Checks for invariant drift against snapshot
# → Emits UpgradeCompleted event
# → Clears UpgradeInProgress flag
```

### Rollback Procedures

**Automatic Rollback on Failure**

If any of the following occur, the upgrade automatically rolls back:

1. **Invariant Violation**: `assert_post_upgrade_invariants()` fails
- Contract state is atomically reverted
- `InternalError` is returned
- Upgrade remains in progress for manual intervention

2. **Invariant Drift Detection**: `check_invariant_drift()` fails
- Pending upgrade state is cleared
- `InternalError` is returned
- Admin must investigate and retry

3. **Re-entry Attack Detected**: `assert_not_reentrant()` fails
- Operation is blocked immediately
- `ReentrancyDetected` error is returned
- No state changes occur

**Manual Rollback Procedure**

```bash
# If upgrade fails and needs manual rollback:
client.cancel_upgrade(admin)
# → Restores previous WASM hash
# → Clears all pending upgrade state
# → Clears invariant snapshot
```

### Post-Upgrade Validation

After successful upgrade completion:

1. **Verify Version**
```bash
assert_eq!(client.get_version(), expected_version)
```

2. **Check Invariants**
```bash
client.check_upgrade_safety() # Should report safe
```

3. **Test Critical Operations**
- Deposit and withdraw test transactions
- Verify fee configuration
- Check admin functionality

4. **Monitor Events**
- Verify `UpgradeCompleted` event was emitted
- Check indexer logs for upgrade tracking

### Emergency Recovery

If the contract enters an invalid state:

1. **Emergency Mode Activation**
```bash
client.activate_emergency_mode(admin)
```
- Blocks most mutating operations
- Allows withdrawals to continue
- Irreversible once activated

2. **Investigate State**
```bash
client.get_contract_health()
client.get_upgrade_status()
client.get_invariant_snapshot()
```

3. **Recovery Options**
- Use `cancel_upgrade()` if upgrade is in progress
- Deploy emergency fix if critical bug discovered
- Coordinate with team for coordinated response

---

## Sign-Off

**Implementation**: ✅ Complete
Expand Down
41 changes: 41 additions & 0 deletions app/contract/contracts/Folder/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ pub fn set_upgrade_window(
///
/// **Admin only**. Emits `UpgradeStarted` event with old/new versions.
/// Blocks if window is not active or upgrade already in progress.
/// Protected against re-entry attacks (Issue #554).
pub fn start_upgrade(
env: &Env,
caller: &Address,
Expand All @@ -373,6 +374,9 @@ pub fn start_upgrade(
) -> Result<(), RustAcademyError> {
require_admin(env, caller)?;

// Re-entry protection (Issue #554)
crate::hook::assert_not_reentrant(env)?;

// Check upgrade gate master switch (Issue #318)
if !storage::is_upgrade_gate_enabled(env) {
return Err(RustAcademyError::UpgradeWindowNotActive);
Expand All @@ -387,8 +391,17 @@ pub fn start_upgrade(
return Err(RustAcademyError::UpgradeAlreadyInProgress);
}

// Prevent repeated-init misuse (Issue #554)
if !storage::is_initialized(env) {
return Err(RustAcademyError::Unauthorized);
}

let old_version = get_version(env);
let (window_start, window_end) = storage::get_upgrade_window(env);

// Snapshot pre-upgrade invariants for drift detection (Issue #554)
storage::snapshot_pre_upgrade_invariants(env)?;

if let Some(current_hash) = storage::get_wasm_hash(env) {
storage::set_pending_upgrade_rollback_wasm_hash(env, &current_hash);
} else {
Expand Down Expand Up @@ -416,17 +429,22 @@ pub fn start_upgrade(
///
/// Must be called during an active upgrade window and while an upgrade is in progress.
/// The provided WASM hash must match the one recorded during `start_upgrade`.
/// Protected against re-entry attacks (Issue #554).
pub fn upgrade(
env: &Env,
caller: &Address,
new_wasm_hash: BytesN<32>,
) -> Result<(), RustAcademyError> {
require_admin(env, caller)?;

// Re-entry protection (Issue #554)
crate::hook::assert_not_reentrant(env)?;

if !storage::is_upgrade_in_progress(env) {
return Err(RustAcademyError::UpgradeNotInProgress);
}

// Strengthened window validation (Issue #554)
if !storage::is_upgrade_window_active(env) {
return Err(RustAcademyError::UpgradeWindowNotActive);
}
Expand All @@ -451,8 +469,13 @@ pub fn upgrade(
}

/// Cancel a pending upgrade and clear gating state (**Admin only**).
/// Protected against re-entry attacks (Issue #554).
pub fn cancel_upgrade(env: &Env, caller: &Address) -> Result<(), RustAcademyError> {
require_admin(env, caller)?;

// Re-entry protection (Issue #554)
crate::hook::assert_not_reentrant(env)?;

if let Some(rollback_hash) = storage::get_pending_upgrade_rollback_wasm_hash(env) {
storage::set_wasm_hash(env, &rollback_hash);

Expand All @@ -468,15 +491,25 @@ pub fn cancel_upgrade(env: &Env, caller: &Address) -> Result<(), RustAcademyErro
///
/// **Admin only**. Must be called after `start_upgrade` and `upgrade` to finalize.
/// Calls `migrate()` internally and re-checks invariants.
/// Protected against re-entry attacks (Issue #554).
pub fn complete_upgrade(
env: &Env,
caller: &Address,
new_version: u32,
) -> Result<u32, RustAcademyError> {
// Re-entry protection (Issue #554)
crate::hook::assert_not_reentrant(env)?;

if !storage::is_upgrade_in_progress(env) {
return Err(RustAcademyError::UpgradeNotInProgress);
}

// Check upgrade window is still active (Issue #554)
// Prevent completing upgrades after the window expires
if !storage::is_upgrade_window_active(env) {
return Err(RustAcademyError::UpgradeWindowNotActive);
}

// Verify version and hash (Issue #432 AC2)
let pending_version =
storage::get_pending_upgrade_version(env).ok_or(RustAcademyError::InternalError)?;
Expand All @@ -497,6 +530,14 @@ pub fn complete_upgrade(

let old_version = get_version(env);

// Check for invariant drift BEFORE migrate (Issue #554)
// This ensures we detect drift caused by the upgrade itself
if let Err(_drift_error) = storage::check_invariant_drift(env) {
// Clear pending state on drift detection to force rollback
storage::clear_pending_upgrade(env);
return Err(RustAcademyError::InternalError);
}

// Run migration
let migrated_version = migrate(env, caller)?;

Expand Down
Loading
Loading