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
6 changes: 6 additions & 0 deletions app/backend/src/common/soroban-errors/soroban-error.codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ var SorobanErrorCode;
SorobanErrorCode["VERSION_MISMATCH"] = "CONTRACT_VERSION_MISMATCH";
/** WASM hash provided for upgrade is invalid. */
SorobanErrorCode["INVALID_WASM_HASH"] = "CONTRACT_INVALID_WASM_HASH";
/** Upgrade window is not currently active; start_upgrade is blocked. */
SorobanErrorCode["UPGRADE_WINDOW_NOT_ACTIVE"] = "CONTRACT_UPGRADE_WINDOW_NOT_ACTIVE";
/** An upgrade is already in progress; start_upgrade cannot be called again. */
SorobanErrorCode["UPGRADE_ALREADY_IN_PROGRESS"] = "CONTRACT_UPGRADE_ALREADY_IN_PROGRESS";
/** No upgrade is currently in progress; upgrade or complete_upgrade cannot proceed. */
SorobanErrorCode["UPGRADE_NOT_IN_PROGRESS"] = "CONTRACT_UPGRADE_NOT_IN_PROGRESS";
// ── Admin ─────────────────────────────────────────────────────────────────
/** Caller is not the contract admin. */
SorobanErrorCode["NOT_ADMIN"] = "CONTRACT_NOT_ADMIN";
Expand Down
16 changes: 11 additions & 5 deletions app/backend/src/common/soroban-errors/soroban-error.codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,17 @@
/** Amount provided is zero or negative. */
INVALID_AMOUNT = 'CONTRACT_INVALID_AMOUNT',

// ── Version / upgrade ─────────────────────────────────────────────────────
/** Contract schema version is not supported by this client. */
VERSION_MISMATCH = 'CONTRACT_VERSION_MISMATCH',
/** WASM hash provided for upgrade is invalid. */
INVALID_WASM_HASH = 'CONTRACT_INVALID_WASM_HASH',
// ── Version / upgrade ─────────────────────────────────────────────────────
/** Contract schema version is not supported by this client. */
VERSION_MISMATCH = 'CONTRACT_VERSION_MISMATCH',
/** WASM hash provided for upgrade is invalid. */
INVALID_WASM_HASH = 'CONTRACT_INVALID_WASM_HASH',
/** Upgrade window is not currently active; start_upgrade is blocked. */
UPGRADE_WINDOW_NOT_ACTIVE = 'CONTRACT_UPGRADE_WINDOW_NOT_ACTIVE',
/** An upgrade is already in progress; start_upgrade cannot be called again. */
UPGRADE_ALREADY_IN_PROGRESS = 'CONTRACT_UPGRADE_ALREADY_IN_PROGRESS',
/** No upgrade is currently in progress; upgrade or complete_upgrade cannot proceed. */
UPGRADE_NOT_IN_PROGRESS = 'CONTRACT_UPGRADE_NOT_IN_PROGRESS',

// ── Admin ─────────────────────────────────────────────────────────────────
/** Caller is not the contract admin. */
Expand Down
15 changes: 15 additions & 0 deletions app/backend/src/common/soroban-errors/soroban-error.mapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@ var ERROR_MAPPINGS = [
code: soroban_error_codes_1.SorobanErrorCode.INVALID_WASM_HASH,
message: 'The WASM hash provided for the contract upgrade is invalid.',
},
{
pattern: /upgrade.*window.*not.*active|upgrade.*window.*inactive/i,
code: soroban_error_codes_1.SorobanErrorCode.UPGRADE_WINDOW_NOT_ACTIVE,
message: 'The upgrade window is not currently active. Please try again during the scheduled window.',
},
{
pattern: /upgrade.*already.*in.*progress/i,
code: soroban_error_codes_1.SorobanErrorCode.UPGRADE_ALREADY_IN_PROGRESS,
message: 'An upgrade is already in progress. Please wait for it to complete before starting a new one.',
},
{
pattern: /upgrade.*not.*in.*progress|no.*upgrade.*in.*progress/i,
code: soroban_error_codes_1.SorobanErrorCode.UPGRADE_NOT_IN_PROGRESS,
message: 'No upgrade is currently in progress. Start an upgrade first before performing this operation.',
},
// ── Input / params ────────────────────────────────────────────────────────
{
pattern: /account.*does not exist|account.*not.*found/i,
Expand Down
15 changes: 15 additions & 0 deletions app/backend/src/common/soroban-errors/soroban-error.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,21 @@ const ERROR_MAPPINGS: Array<{
code: SorobanErrorCode.INVALID_WASM_HASH,
message: 'The WASM hash provided for the contract upgrade is invalid.',
},
{
pattern: /upgrade.*window.*not.*active|upgrade.*window.*inactive/i,
code: SorobanErrorCode.UPGRADE_WINDOW_NOT_ACTIVE,
message: 'The upgrade window is not currently active. Please try again during the scheduled window.',
},
{
pattern: /upgrade.*already.*in.*progress/i,
code: SorobanErrorCode.UPGRADE_ALREADY_IN_PROGRESS,
message: 'An upgrade is already in progress. Please wait for it to complete before starting a new one.',
},
{
pattern: /upgrade.*not.*in.*progress|no.*upgrade.*in.*progress/i,
code: SorobanErrorCode.UPGRADE_NOT_IN_PROGRESS,
message: 'No upgrade is currently in progress. Start an upgrade first before performing this operation.',
},

// ── Input / params ────────────────────────────────────────────────────────
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@ describe('mapSorobanError', () => {
expect(result.code).toBe(SorobanErrorCode.INVALID_WASM_HASH);
});

it('maps "upgrade window not active" to UPGRADE_WINDOW_NOT_ACTIVE', () => {
const result = mapSorobanError('upgrade window not active');
expect(result.code).toBe(SorobanErrorCode.UPGRADE_WINDOW_NOT_ACTIVE);
});

it('maps "upgrade already in progress" to UPGRADE_ALREADY_IN_PROGRESS', () => {
const result = mapSorobanError('upgrade already in progress');
expect(result.code).toBe(SorobanErrorCode.UPGRADE_ALREADY_IN_PROGRESS);
});

it('maps "no upgrade in progress" to UPGRADE_NOT_IN_PROGRESS', () => {
const result = mapSorobanError('no upgrade in progress');
expect(result.code).toBe(SorobanErrorCode.UPGRADE_NOT_IN_PROGRESS);
});

// ── Admin ───────────────────────────────────────────────────────────────

it('maps "not admin" to NOT_ADMIN', () => {
Expand Down
3 changes: 3 additions & 0 deletions app/contract/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ target
# Local settings
.soroban
.stellar

# Test snapshots (generated by property-based tests)
contracts/Folder/test_snapshots/
12 changes: 6 additions & 6 deletions app/contract/UPGRADE_SAFETY_GATE_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Added contract-level safeguards and comprehensive invariant enforcement for safe
- Function: `storage::is_upgrade_window_active(env)` checks ledger timestamp against `[start, end)`
- Gating: `upgrade()` now requires `UpgradeInProgress` and active window.
- Verification: `upgrade()` verifies the WASM hash matches the one stored in `start_upgrade()`.
- Error: Returns `InvalidAmount` (repurposed as "upgrade window not active") or `InternalError` (not in progress).
- Error: Returns `UpgradeWindowNotActive` when window is not active or `UpgradeAlreadyInProgress` when already started.

**Test**: `upgrade_safety_gate_blocks_upgrade_outside_window`, `upgrade_safety_gate_blocks_direct_upgrade_without_start`

Expand Down Expand Up @@ -71,7 +71,7 @@ admin::upgrade(hash)

```rust
pub fn complete_upgrade(env, caller, new_version) {
if !storage::is_upgrade_in_progress(env) { return Err(InternalError); }
if !storage::is_upgrade_in_progress(env) { return Err(UpgradeNotInProgress); }
if new_version != storage::get_pending_upgrade_version(env) { return Err(InvalidContractVersion); }
if storage::get_wasm_hash(env) != storage::get_pending_upgrade_wasm_hash(env) { return Err(InternalError); }
// ... run migrate() ...
Expand Down Expand Up @@ -177,7 +177,7 @@ Step 1: Admin calls set_upgrade_window(start, end)
Step 2: Admin calls start_upgrade(new_version)
→ Check: is_upgrade_window_active()
→ If yes: set UpgradeInProgress = true, emit UpgradeStarted
→ If no: return Err(InvalidAmount)
→ If no: return Err(UpgradeWindowNotActive)

Step 3a: (Deploy) update_current_contract_wasm(new_wasm_hash)
→ Caller publishes new WASM; contract code swaps
Expand All @@ -191,9 +191,9 @@ Step 3b: Admin calls complete_upgrade(new_version)

**Error Handling**:

- `InvalidAmount`: Used to signal "upgrade window not active"
- `ContractPaused`: Used to signal "upgrade already in progress" (repurposed)
- `InternalError`: Used when post-upgrade invariants fail (repurposed)
- `UpgradeWindowNotActive`: Used to signal "upgrade window not active"
- `UpgradeAlreadyInProgress`: Used to signal "upgrade already in progress"
- `InternalError`: Used when post-upgrade invariants fail

---

Expand Down
39 changes: 22 additions & 17 deletions app/contract/UPGRADE_SAFETY_GATE_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,23 @@ start_upgrade(env, caller, new_version) -> Result<(), RustAcademyError>
- **Must be called during active window** (AC1)
- Sets `UpgradeInProgress = true`
- Emits: `UpgradeStarted { admin, old_version, new_version, ... }`
- **Errors**:
- `InvalidAmount`: Window not active
- `ContractPaused`: Already in-progress
- `InsufficientRole`: Not admin
- **Errors**:
- `UpgradeWindowNotActive`: Window not active
- `UpgradeAlreadyInProgress`: Already in-progress
- `InsufficientRole`: Not admin

### 4. Update WASM

```rust
upgrade(env, caller, new_wasm_hash) -> Result<(), RustAcademyError>
```

- Swaps contract code (no storage changes)
- Emits: `ContractUpgraded { admin, new_wasm_hash, ... }`
- **Error**: `InsufficientRole` if not admin
- Swaps contract code (no storage changes)
- Emits: `ContractUpgraded { admin, new_wasm_hash, ... }`
- **Errors**:
- `UpgradeNotInProgress`: No upgrade in progress
- `UpgradeWindowNotActive`: Window not active
- `InsufficientRole`: Not admin

### 5. Complete Upgrade (Admin)

Expand All @@ -57,9 +60,10 @@ complete_upgrade(env, caller, new_version) -> Result<u32, RustAcademyError>
- Sets `UpgradeInProgress = false`
- Emits: `UpgradeCompleted { admin, old_version, new_version, ... }`
- **Returns**: New contract version
- **Errors**:
- `InternalError`: Not in-progress, or invariants failed
- `InvalidContractVersion`: Version mismatch
- **Errors**:
- `UpgradeNotInProgress`: No upgrade in progress
- `InternalError`: Invariants failed post-upgrade
- `InvalidContractVersion`: Version mismatch

---

Expand All @@ -79,13 +83,14 @@ Day 2 @ 14:15: admin.complete_upgrade(2u32)

---

## Error Codes (Repurposed)
## Error Codes (Semantic)

| Error | Code | Meaning |
| ---------------- | ---- | ------------------------------ |
| `InvalidAmount` | 100 | Upgrade window not active |
| `ContractPaused` | 300 | Upgrade already in-progress |
| `InternalError` | 900 | Invariants failed post-upgrade |
| Error | Code | Meaning |
| ------------------------- | ---- | ------------------------------ |
| `UpgradeWindowNotActive` | 502 | Upgrade window not active |
| `UpgradeAlreadyInProgress`| 503 | Upgrade already in-progress |
| `UpgradeNotInProgress` | 504 | No upgrade in progress |
| `InternalError` | 900 | Invariants failed post-upgrade |

---

Expand Down Expand Up @@ -168,7 +173,7 @@ Post-upgrade checks that **must** hold:
A: No. The original `migrate()` still works standalone. These are optional extra guards.

**Q: Can I call `complete_upgrade()` without `start_upgrade()`?**
A: No. `complete_upgrade()` checks `UpgradeInProgress` flag first → returns `InternalError`.
A: No. `complete_upgrade()` checks `UpgradeInProgress` flag first → returns `UpgradeNotInProgress`.

**Q: What if invariants fail during `complete_upgrade()`?**
A: Contract panics → all storage changes rolled back → upgrade aborted. Retry after fixing.
Expand Down
10 changes: 5 additions & 5 deletions app/contract/UPGRADE_SAFETY_GATE_TEST_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
assert!(result.is_err(), "start_upgrade must fail when no window is set");
```

- Expected: `InvalidAmount` ("upgrade window not active")
- Expected: `UpgradeWindowNotActive` ("upgrade window not active")

2. **Before Window**

Expand All @@ -45,9 +45,9 @@
assert!(result.is_err(), "start_upgrade must fail before window start");
```

- Expected: `InvalidAmount`
- Expected: `UpgradeWindowNotActive`

3. **Within Window**
3. **Within Window**

```rust
gs.env.ledger().with_mut(|li| {
Expand All @@ -71,7 +71,7 @@
assert!(result.is_err(), "start_upgrade must fail after window end");
```

- Expected: `InvalidAmount`
- Expected: `UpgradeWindowNotActive`

**Acceptance Criterion**: ✅ AC1 – Upgrades are blocked outside the allowed window.

Expand Down Expand Up @@ -301,7 +301,7 @@ LIMIT 1;
```rust
let result = client.try_start_upgrade(&gs.admin, &3u32);
assert!(result.is_err(), "start_upgrade must fail when already in progress");
// → Expected: ContractPaused ("upgrade already in progress")
// → Expected: `UpgradeAlreadyInProgress` ("upgrade already in progress")
```

3. **Complete & Retry**
Expand Down
10 changes: 5 additions & 5 deletions app/contract/contracts/Folder/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,11 +354,11 @@ pub fn start_upgrade(

// Check upgrade window is active (Issue #432 AC1)
if !storage::is_upgrade_window_active(env) {
return Err(RustAcademyError::InvalidAmount); // Repurpose for "upgrade window not active"
return Err(RustAcademyError::UpgradeWindowNotActive);
}

if storage::is_upgrade_in_progress(env) {
return Err(RustAcademyError::ContractPaused); // Reuse for "upgrade in progress"
return Err(RustAcademyError::UpgradeAlreadyInProgress);
}

let old_version = get_version(env);
Expand Down Expand Up @@ -398,11 +398,11 @@ pub fn upgrade(
require_admin(env, caller)?;

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

if !storage::is_upgrade_window_active(env) {
return Err(RustAcademyError::InvalidAmount);
return Err(RustAcademyError::UpgradeWindowNotActive);
}

let pending_hash =
Expand Down Expand Up @@ -448,7 +448,7 @@ pub fn complete_upgrade(
new_version: u32,
) -> Result<u32, RustAcademyError> {
if !storage::is_upgrade_in_progress(env) {
return Err(RustAcademyError::InternalError); // Not in upgrade state
return Err(RustAcademyError::UpgradeNotInProgress);
}

// Verify version and hash (Issue #432 AC2)
Expand Down
7 changes: 7 additions & 0 deletions app/contract/contracts/Folder/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,11 @@ pub enum RustAcademyError {
NonceAlreadyUsed = 500,
/// The signature's valid_until timestamp has passed; signature expired.
SignatureExpired = 501,
// Upgrade gating (502-504)
/// The upgrade window is not currently active; start_upgrade is blocked.
UpgradeWindowNotActive = 502,
/// An upgrade is already in progress; start_upgrade cannot be called again.
UpgradeAlreadyInProgress = 503,
/// No upgrade is currently in progress; upgrade or complete_upgrade cannot proceed.
UpgradeNotInProgress = 504,
}
9 changes: 6 additions & 3 deletions app/contract/contracts/Folder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,8 @@ impl RustAcademyContract {
///
/// # Errors
/// * `Unauthorized` - Caller is not the admin, or admin not set
/// * `UpgradeNotInProgress` - no upgrade is currently in progress
/// * `UpgradeWindowNotActive` - upgrade window is not currently active
///
/// # Security
/// Updates the contract's executable code. Call [`migrate`]( RustAcademyContract::migrate)
Expand Down Expand Up @@ -1267,8 +1269,8 @@ impl RustAcademyContract {
/// * `new_wasm_hash` - The target WASM hash
///
/// # Errors
/// * `InvalidAmount` - (repurposed) upgrade window not active
/// * `ContractPaused` - (repurposed) upgrade already in progress
/// * `UpgradeWindowNotActive` - upgrade window is not currently active
/// * `UpgradeAlreadyInProgress` - an upgrade is already in progress
pub fn start_upgrade(
env: Env,
caller: Address,
Expand Down Expand Up @@ -1297,7 +1299,8 @@ impl RustAcademyContract {
/// The actual new contract version
///
/// # Errors
/// * `InternalError` - no upgrade in progress, or post-upgrade invariants violated
/// * `UpgradeNotInProgress` - no upgrade is currently in progress
/// * `InternalError` - post-upgrade invariants violated
pub fn complete_upgrade(
env: Env,
caller: Address,
Expand Down
18 changes: 10 additions & 8 deletions app/contract/docs/UPGRADE_SAFETY_GATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ admin_account.complete_upgrade(contract, 2u32)?;
- ✅ `start_upgrade()` fails after window end time
- ✅ `start_upgrade()` succeeds within window `[start, end)`

**Error Code**: `InvalidAmount` (repurposed as "upgrade window not active")
**Error Code**: `UpgradeWindowNotActive`

### AC2: Post-Upgrade Invariants Enforced Deterministically

Expand Down Expand Up @@ -235,13 +235,15 @@ pub struct UpgradeCompletedEvent {

## Error Codes

New or repurposed errors:
New upgrade-specific errors:

| Error | Code | Trigger | Context |
| ---------------- | ---- | ------------------------------ | ------------------------------------------- |
| `InvalidAmount` | 100 | Upgrade window not active | `start_upgrade()` outside `[start, end)` |
| `ContractPaused` | 300 | Upgrade already in progress | `start_upgrade()` called twice |
| `InternalError` | 900 | Post-upgrade invariants failed | `complete_upgrade()` after failed migration |
| Error | Code | Trigger | Context |
| ------------------------- | ---- | ------------------------------ | ------------------------------------------- |
| `UpgradeWindowNotActive` | 502 | Upgrade window not active | `start_upgrade()` outside `[start, end)` |
| `UpgradeAlreadyInProgress`| 503 | Upgrade already in progress | `start_upgrade()` called twice |
| `UpgradeNotInProgress` | 504 | No upgrade in progress | `upgrade()` / `complete_upgrade()` without `start_upgrade()` |

Legacy repurposed codes removed; backend now maps these to stable API codes.

---

Expand Down Expand Up @@ -291,7 +293,7 @@ let new_version = admin.complete_upgrade(contract, 2u32)?;
```rust
// Same setup as above; but now it's Day 1, 13:00 UTC (before window)
admin.start_upgrade(contract, 2u32)?;
// → Err: InvalidAmount ("upgrade window not active")
// → Err: UpgradeWindowNotActive ("upgrade window not active")
```

---
Expand Down
Loading