diff --git a/UPGRADE.md b/UPGRADE.md index a205e460..a4b10739 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -120,7 +120,7 @@ pub fn upgrade(env: Env, caller: Address, new_wasm_hash: BytesN<32>) **Version Function Signature:** ```rust -pub fn version(env: Env) -> Option> +pub fn get_version(env: Env) -> Option> ``` **Behavior note (pause semantics):** @@ -135,6 +135,7 @@ pub fn version(env: Env) -> Option> |-----|------|-------------| | `admin` | `Address` | Admin address; may call `distribute` and `set_admin` | | `usdc` | `Address` | USDC token contract address | +| `ContractVersion` | `BytesN<32>` | WASM hash set by `upgrade` function | #### Upgradeability @@ -151,7 +152,7 @@ soroban contract invoke --contract-id -- upgrade \ --caller --new_wasm_hash <32-byte-hex> ``` -The `version()` view returns the stored WASM hash; the contract emits an `upgraded` +The `get_version()` view returns the stored WASM hash; the contract emits an `upgraded` event with the admin as a topic and the new version as data. **Init signature** (`lib.rs:28-39`): @@ -168,6 +169,25 @@ pub fn init(env: Env, admin: Address, usdc_token: Address) | `vault` | `Address` | Registered vault address | | `developer_balances` | `Map` | Per-developer balance tracking | | `global_pool` | `GlobalPool` | Total balance and last updated timestamp | +| `ContractVersion` | `BytesN<32>` | WASM hash set by `upgrade` function | + +#### Upgradeability + +The settlement contract supports in-place upgrades via an admin-gated `upgrade` function. +This method calls the host deployer to update the contract WASM code while +preserving existing instance storage. + +```bash +# Build new WASM +cargo build --target wasm32-unknown-unknown --release -p callora-settlement + +# Compute WASM hash and call upgrade via RPC or tooling +soroban contract invoke --contract-id -- upgrade \ + --caller --new_wasm_hash <32-byte-hex> +``` + +The `get_version()` view returns the stored WASM hash (as an `Option>`); the contract emits an `upgraded` +event with the admin as a topic and the new version as data. **GlobalPool structure** (`lib.rs:16-19`): @@ -236,7 +256,7 @@ The vault now supports admin-gated in-place upgrades that preserve all existing 4. **Verify upgrade** ```bash # Check version marker - soroban contract invoke --contract-id -- version + soroban contract invoke --contract-id -- get_version # Should return # Verify state preserved @@ -264,7 +284,7 @@ The `upgrade` function emits an `upgraded` event with: - Data: New WASM hash (BytesN<32>) **Version Tracking:** -- Call `version()` to retrieve the current WASM hash +- Call `get_version()` to retrieve the current WASM hash - Returns `None` for contracts deployed before upgrade functionality - Returns `Some(BytesN<32>)` after first upgrade diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index a833c858..26a7a394 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -582,12 +582,11 @@ impl RevenuePool { /// Read the stored contract version (WASM hash) as last set by `upgrade`. /// - /// Panics if no version has been stored yet. - pub fn version(env: Env) -> BytesN<32> { + /// Returns `None` if no version has been stored yet. + pub fn get_version(env: Env) -> Option> { env.storage() .instance() .get(&Symbol::new(&env, VERSION_KEY)) - .expect("version not set") } } diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index 005e3bf0..b329c684 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -1883,8 +1883,8 @@ fn upgrade_sets_version_and_emits_event() { client.upgrade(&admin, &new_hash); // version() should return stored value - let readback: BytesN<32> = client.version(); - assert_eq!(readback, new_hash); + let readback: Option> = client.get_version(); + assert_eq!(readback, Some(new_hash)); // An `upgraded` event should have been emitted let events = env.events().all(); diff --git a/contracts/settlement/src/lib.rs b/contracts/settlement/src/lib.rs index 9376db40..45f4b314 100644 --- a/contracts/settlement/src/lib.rs +++ b/contracts/settlement/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use soroban_sdk::{contract, contracterror, contractimpl, contracttype, token, Address, Env, Symbol, Vec}; +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, token, Address, BytesN, Env, Symbol, Vec}; /// Maximum number of items allowed in a single `batch_receive_payment` call. pub const MAX_BATCH_SIZE: u32 = 50; @@ -58,6 +58,7 @@ pub enum StorageKey { DeveloperBalance(Address), GlobalPool, Usdc, + ContractVersion, } /// Developer balance record in settlement contract @@ -791,6 +792,40 @@ impl CalloraSettlement { env.panic_with_error(SettlementError::Unauthorized); } } + + /// Admin-gated contract upgrade. + /// + /// Only the current admin may call. This will instruct the host to update + /// the current contract WASM to `new_wasm_hash` and persist the version marker. + /// Emits an `upgraded` event with the admin as topic and the new version as data. + pub fn upgrade(env: Env, caller: Address, new_wasm_hash: BytesN<32>) { + caller.require_auth(); + let admin = Self::get_admin(env.clone()); + if caller != admin { + env.panic_with_error(SettlementError::Unauthorized); + } + + // Perform the on-chain upgrade via the deployer interface. + env.deployer().update_current_contract_wasm(new_wasm_hash.clone()); + + // Persist the version marker for on-chain queries. + env.storage() + .instance() + .set(&StorageKey::ContractVersion, &new_wasm_hash); + + // Emit an event for indexers / audit logs. + env.events() + .publish((Symbol::new(&env, "upgraded"), admin), new_wasm_hash); + } + + /// Read the stored contract version (WASM hash) as last set by `upgrade`. + /// + /// Returns `None` if no upgrade has been performed yet (initial deployment). + pub fn get_version(env: Env) -> Option> { + env.storage() + .instance() + .get(&StorageKey::ContractVersion) + } } #[cfg(test)] diff --git a/contracts/settlement/src/test.rs b/contracts/settlement/src/test.rs index 51661258..ee1dbdd8 100644 --- a/contracts/settlement/src/test.rs +++ b/contracts/settlement/src/test.rs @@ -1679,5 +1679,22 @@ mod settlement_tests { pool.total_balance + sum_dev_balances, "Conservation invariant violated: total credits must equal pool + developer balances" ); + #[test] + fn test_upgrade_and_get_version() { + let (env, addr, admin, _vault, _third_party) = setup_contract(); + let client = CalloraSettlementClient::new(&env, &addr); + + assert_eq!(client.get_version(), None); + + let new_hash = BytesN::from_array(&env, &[1u8; 32]); + client.upgrade(&admin, &new_hash); + + assert_eq!(client.get_version(), Some(new_hash.clone())); + + // An `upgraded` event should have been emitted + let events = env.events().all(); + let ev = events.last().unwrap(); + let name = soroban_sdk::Symbol::try_from_val(&env, &ev.1.get(0).unwrap()).unwrap(); + assert_eq!(name, soroban_sdk::Symbol::new(&env, "upgraded")); } } diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index 43d2363d..c5337531 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -1148,7 +1148,7 @@ impl CalloraVault { /// Read the stored contract version (WASM hash) as last set by `upgrade`. /// /// Returns `None` if no upgrade has been performed yet (initial deployment). - pub fn version(env: Env) -> Option> { + pub fn get_version(env: Env) -> Option> { env.storage() .instance() .get(&StorageKey::ContractVersion) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 57c88236..c5e7afbd 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -5880,14 +5880,14 @@ fn upgrade_sets_version_and_emits_event() { client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); // Version should be None before any upgrade - assert_eq!(client.version(), None); + assert_eq!(client.get_version(), None); let new_hash = BytesN::from_array(&env, &[2u8; 32]); client.upgrade(&owner, &new_hash); // version() should return stored value - let readback = client.version(); + let readback = client.get_version(); assert_eq!(readback, Some(new_hash.clone())); // An `upgraded` event should have been emitted @@ -5927,7 +5927,7 @@ fn upgrade_non_owner_admin_succeeds() { // new_admin should be able to upgrade client.upgrade(&new_admin, &new_hash); - let readback = client.version(); + let readback = client.get_version(); assert_eq!(readback, Some(new_hash)); } @@ -5966,7 +5966,7 @@ fn version_returns_none_before_first_upgrade() { fund_vault(&usdc_admin, &vault_address, 100); client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); - assert_eq!(client.version(), None); + assert_eq!(client.get_version(), None); } #[test] @@ -5982,15 +5982,15 @@ fn upgrade_multiple_times_updates_version() { let hash1 = BytesN::from_array(&env, &[5u8; 32]); client.upgrade(&owner, &hash1); - assert_eq!(client.version(), Some(hash1.clone())); + assert_eq!(client.get_version(), Some(hash1.clone())); let hash2 = BytesN::from_array(&env, &[6u8; 32]); client.upgrade(&owner, &hash2); - assert_eq!(client.version(), Some(hash2.clone())); + assert_eq!(client.get_version(), Some(hash2.clone())); let hash3 = BytesN::from_array(&env, &[7u8; 32]); client.upgrade(&owner, &hash3); - assert_eq!(client.version(), Some(hash3)); + assert_eq!(client.get_version(), Some(hash3)); } // ---------------------------------------------------------------------------