diff --git a/contracts/rewards/src/lib.rs b/contracts/rewards/src/lib.rs index d88cbe1..4c07708 100644 --- a/contracts/rewards/src/lib.rs +++ b/contracts/rewards/src/lib.rs @@ -10,7 +10,7 @@ #![no_std] use soroban_sdk::{ - contract, contracterror, contractimpl, contractmeta, symbol_short, Address, Env, Symbol, Vec, + contract, contracterror, contractimpl, contractmeta, symbol_short, Address, BytesN, Env, Symbol, Vec, }; #[contracterror] @@ -217,6 +217,22 @@ impl RewardsContract { pub fn is_paused(env: Env) -> bool { env.storage().instance().get(&PAUSED).unwrap_or(false) } + + /// Upgrade the contract WASM to a new version (admin only). + /// + /// The new WASM must have been uploaded to the network first using + /// `stellar contract install`. The `new_wasm_hash` is obtained from that + /// installation step. + /// + /// **State preservation**: Soroban upgrades preserve all instance and + /// persistent storage. Only the logic (WASM bytecode) changes. New code + /// MUST read the existing storage layout — adding new storage keys is safe, + /// but reordering, removing, or repurposing existing keys will corrupt data. + pub fn upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) -> Result<(), Error> { + require_admin(&env, &admin)?; + env.deployer().update_current_contract_wasm(new_wasm_hash); + Ok(()) + } } #[cfg(test)] diff --git a/docs/upgradeability.md b/docs/upgradeability.md index 3441cb1..09a59d8 100644 --- a/docs/upgradeability.md +++ b/docs/upgradeability.md @@ -1,31 +1,219 @@ # Soroban Contract Upgradeability -Trivela contracts are designed with a standard Soroban upgradeability pattern, allowing for logic updates while preserving contract address and state. +This document describes the upgradeability patterns used in Trivela smart contracts, covering the mechanism, upgrade process, storage considerations, and security model. -## Upgrade Pattern +--- -Soroban provides a built-in mechanism for upgrading contract code. This is achieved by updating the Wasm byte-code associated with a contract ID. +## Overview -### 1. Requirements -- The contract must implement an admin-only function that calls `env.deployer().update_current_contract_wasm(new_wasm_hash)`. -- The new WASM file must already be uploaded to the network (obtaining a `wasm_hash`). +Trivela contracts are upgradeable using Soroban's built-in **in-place WASM upgrade** mechanism. Unlike EVM proxy patterns (EIP-1967), Soroban upgrades replace the contract's bytecode directly while preserving all on-chain storage. There is no separate proxy contract or fallback mechanism. -### 2. Implementation Example +**Key properties:** +- Contract address remains unchanged after an upgrade +- All persistent storage (`instance()` and `persistent()`) is preserved automatically +- Only the logic (WASM bytecode) changes +- Admin authority is required for all upgrades -In a future iteration, we can add this to `RewardsContract`: +--- + +## Mechanism: `update_current_contract_wasm` + +Soroban provides a first-class upgrade API via the `deployer`: + +```rust +env.deployer().update_current_contract_wasm(new_wasm_hash); +``` + +Calling this function replaces the WASM bytecode associated with the current contract ID. The call requires `require_auth()` from the admin, ensuring only the designated administrator can trigger upgrades. + +### Requirements + +1. **Admin authentication** — the caller must be the stored admin address. +2. **WASM hash** — the new WASM must already be installed on the network (not uploaded as part of the upgrade transaction). The hash is a `BytesN<32>` value obtained during the `stellar contract install` step. +3. **State compatibility** — the new contract code must be able to read all existing storage keys. Adding new keys is safe; removing, reordering, or changing the type of existing keys will corrupt data. + +--- + +## Storage Compatibility Rules + +When upgrading a contract, persistent state lives in Soroban storage regions: + +| Storage region | Persisted across upgrade? | Upgrade risk | +|---|---|---| +| `instance()` | ✅ Yes | Changing existing key types or order breaks reads | +| `persistent()` | ✅ Yes | Same as above | +| `temporary()` | ❌ No (cleared) | Not used in Trivela contracts | + +### Safe changes in new contract versions + +- ✅ Adding new storage keys +- ✅ Adding new public or private functions +- ✅ Extending event schemas (adding new topics) +- ✅ Relaxing validation (e.g., accepting wider input ranges) +- ✅ Adding new error variants (append only to `Error` enum) + +### Breaking changes (must avoid) + +- ❌ Removing or renaming existing storage keys +- ❌ Changing the type of an existing stored value +- ❌ Reordering fields in a stored struct without migration logic +- ❌ Removing public functions that integrators depend on +- ❌ Tightening validation that would reject previously valid states + +### Storage layout example (RewardsContract) + +The rewards contract stores these keys in `instance()`: + +| Key | Type | Purpose | +|---|---|---| +| `admin` (Symbol) | `Address` | Admin identity for upgrade, pause, credit | +| `balance:
` (tuple) | `u64` | Per-user points balance | +| `claimed` (Symbol) | `u64` | Total cumulative claims | +| `metadata` (Symbol) | `(Symbol, Symbol)` | Token name and symbol | +| `paused` (Symbol) | `bool` | Pause state | +| `mxcredit` (Symbol) | `u64` | Max credit per single call (0 = unlimited) | + +New contract versions MUST preserve all of the above with identical types. New keys can be added freely. + +--- + +## Upgrade Process + +### Prerequisites + +- Stellar CLI (`stellar`) installed +- Admin keypair for the deployed contract +- New WASM file (built from the updated Rust source) + +### Step 1: Build the new WASM + +```bash +cd contracts/rewards +cargo build --target wasm32-unknown-unknown --release +``` + +Output: `contracts/rewards/target/wasm32-unknown-unknown/release/rewards.wasm` + +### Step 2: Install the new WASM on the network + +```bash +stellar contract install \ + --source