Skip to content
Open
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
12 changes: 11 additions & 1 deletion contracts/payment-vault-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::error::VaultError;
use crate::events;
use crate::storage;
use crate::types::{BookingRecord, BookingStatus};
use soroban_sdk::{token, Address, Env, Symbol};
use soroban_sdk::{token, Address, BytesN, Env, Symbol};

pub fn initialize_vault(
env: &Env,
Expand Down Expand Up @@ -370,6 +370,16 @@ pub fn transfer_admin(env: &Env, new_admin: &Address) -> Result<(), VaultError>
Ok(())
}

pub fn upgrade_contract(env: &Env, new_wasm_hash: BytesN<32>) -> Result<(), VaultError> {
let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?;
admin.require_auth();

env.deployer().update_current_contract_wasm(new_wasm_hash.clone());
events::contract_upgraded(env, new_wasm_hash);

Ok(())
}

pub fn set_oracle(env: &Env, new_oracle: &Address) -> Result<(), VaultError> {
let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?;
admin.require_auth();
Expand Down
9 changes: 8 additions & 1 deletion contracts/payment-vault-contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![allow(deprecated)]
use soroban_sdk::{symbol_short, Address, Env};
use soroban_sdk::{symbol_short, Address, BytesN, Env};

/// Emitted when a new booking is created
pub fn booking_created(
Expand Down Expand Up @@ -87,3 +87,10 @@ pub fn disputed_remainder_recovered(env: &Env, booking_id: u64, amount: i128) {
let topics = (symbol_short!("dsp_rcvr"), booking_id);
env.events().publish(topics, amount);
}


/// Emitted when the contract WASM is upgraded
pub fn contract_upgraded(env: &Env, new_wasm_hash: BytesN<32>) {
let topics = (symbol_short!("upgraded"),);
env.events().publish(topics, new_wasm_hash);
}
8 changes: 8 additions & 0 deletions contracts/payment-vault-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ impl PaymentVaultContract {
contract::finalize_session(&env, booking_id, actual_duration)
}

/// Allows hot-swapping contract logic without migrating on-chain state.
pub fn upgrade_contract(
env: Env,
new_wasm_hash: soroban_sdk::BytesN<32>,
) -> Result<(), VaultError> {
contract::upgrade_contract(&env, new_wasm_hash)
}

/// Reclaim funds from a stale booking (User-only).
/// Users can reclaim their deposit if the booking has been pending for more than 24 hours.
pub fn reclaim_stale_session(
Expand Down
84 changes: 83 additions & 1 deletion contracts/payment-vault-contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::types::BookingStatus;
use crate::{PaymentVaultContract, PaymentVaultContractClient};
use soroban_sdk::{
testutils::{Address as _, Ledger},
token, Address, Env,
token, Address, BytesN, Env,
};

extern crate std;
Expand Down Expand Up @@ -1857,3 +1857,85 @@ fn test_resolve_dispute_nonexistent_booking() {
let result = client.try_resolve_dispute(&999, &100, &100);
assert!(result.is_err());
}


// Upgradability Tests for Issue #35
// Note: Tests that attempt to call update_current_contract_wasm() are marked #[ignore]
// because this function doesn't work end-to-end in Soroban SDK unit tests (GitHub issue #1089).
// In production, the WASM hash must come from Deployer::upload_contract_wasm(), not an arbitrary BytesN<32>.
#[test]
#[ignore = "update_current_contract_wasm() doesn't work end-to-end in Soroban SDK unit tests (GitHub issue #1089). This test would require actual WASM hash from Deployer::upload_contract_wasm()."]
fn test_admin_can_upgrade_contract() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let token = Address::generate(&env);
let oracle = Address::generate(&env);
let registry = create_mock_registry(&env);

let client = create_client(&env);
client.init(&admin, &token, &oracle, &registry);

// Create a dummy WASM hash for testing (32 bytes of zeros)
// In production, this would be the actual hash from Deployer::upload_contract_wasm()
let new_wasm_hash = BytesN::from_array(&env, &[0u8; 32]);

let result = client.try_upgrade_contract(&new_wasm_hash);
if let Err(e) = &result {
std::println!("Upgrade error: {:?}", e);
}
assert!(result.is_ok());
Comment thread
cyber-excel10 marked this conversation as resolved.
}

#[test]
fn test_non_admin_cannot_upgrade_contract() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let token = Address::generate(&env);
let oracle = Address::generate(&env);
let registry = create_mock_registry(&env);

let client = create_client(&env);
client.init(&admin, &token, &oracle, &registry);

// Create a dummy WASM hash for testing (32 bytes of zeros)
// In production, this would be the actual hash from Deployer::upload_contract_wasm()
let new_wasm_hash = BytesN::from_array(&env, &[0u8; 32]);

// Strip all mocked auths upgrade must fail without admin auth
env.set_auths(&[]);

let result = client.try_upgrade_contract(&new_wasm_hash);
assert!(result.is_err());
}

#[test]
#[ignore = "update_current_contract_wasm() doesn't work end-to-end in Soroban SDK unit tests (GitHub issue #1089). This test would require actual WASM hash from Deployer::upload_contract_wasm()."]
fn test_upgrade_blocked_when_paused() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let token = Address::generate(&env);
let oracle = Address::generate(&env);
let registry = create_mock_registry(&env);

let client = create_client(&env);
client.init(&admin, &token, &oracle, &registry);

// Create a dummy WASM hash for testing (32 bytes of zeros)
// In production, this would be the actual hash from Deployer::upload_contract_wasm()
let new_wasm_hash = BytesN::from_array(&env, &[0u8; 32]);

client.pause();

// Upgrade should still succeed even when paused — it's an admin
// infrastructure operation, not a user-facing state-changing op.
// If your team decides upgrades SHOULD be blocked when paused,
// add the paused check to upgrade_contract and flip this assertion.
let result = client.try_upgrade_contract(&new_wasm_hash);
assert!(result.is_ok());
}