Skip to content

Commit 2dfc690

Browse files
committed
feat(contracts): add contract upgradeability via WASM hash upgrade
Implement Soroban's built-in upgrade mechanism that replaces contract bytecode while preserving all storage (persistent + instance). Changes: - Add `upgrade(admin, new_wasm_hash)` function, admin-only, uses `env.deployer().update_current_contract_wasm()` - Add `version()` query returning current contract version (1) for tracking upgrades across deployments - Add tests: version query and non-admin upgrade rejection Closes #42
1 parent c6cba46 commit 2dfc690

1 file changed

Lines changed: 56 additions & 1 deletion

File tree

contracts/src/lib.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![no_std]
22

3-
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, String};
3+
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, String};
44

55
#[contracttype]
66
#[derive(Clone, Debug, PartialEq)]
@@ -265,6 +265,33 @@ impl AgenticPayContract {
265265
.get(&DataKey::ProjectCount)
266266
.unwrap_or(0)
267267
}
268+
269+
/// Upgrade the contract WASM code. Admin-only.
270+
///
271+
/// Uses Soroban's built-in upgrade mechanism which replaces the contract
272+
/// bytecode while preserving all persistent and instance storage. This
273+
/// allows the contract to be upgraded without redeploying or migrating data.
274+
///
275+
/// # Arguments
276+
/// * `admin` - Must match the stored admin address
277+
/// * `new_wasm_hash` - SHA-256 hash of the new WASM binary (uploaded via `soroban contract install`)
278+
pub fn upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) {
279+
admin.require_auth();
280+
281+
let stored_admin: Address = env
282+
.storage()
283+
.instance()
284+
.get(&DataKey::Admin)
285+
.expect("Not initialized");
286+
assert!(admin == stored_admin, "Only admin can upgrade");
287+
288+
env.deployer().update_current_contract_wasm(new_wasm_hash);
289+
}
290+
291+
/// Return the contract version for tracking upgrades.
292+
pub fn version(_env: Env) -> u32 {
293+
1
294+
}
268295
}
269296

270297
#[cfg(test)]
@@ -295,4 +322,32 @@ mod test {
295322
assert_eq!(project.amount, 1000);
296323
assert_eq!(project.status, ProjectStatus::Created);
297324
}
325+
326+
#[test]
327+
fn test_version_returns_current() {
328+
let env = Env::default();
329+
let contract_id = env.register_contract(None, AgenticPayContract);
330+
let client = AgenticPayContractClient::new(&env, &contract_id);
331+
332+
assert_eq!(client.version(), 1);
333+
}
334+
335+
#[test]
336+
#[should_panic(expected = "Only admin can upgrade")]
337+
fn test_upgrade_rejects_non_admin() {
338+
let env = Env::default();
339+
env.mock_all_auths();
340+
341+
let contract_id = env.register_contract(None, AgenticPayContract);
342+
let client = AgenticPayContractClient::new(&env, &contract_id);
343+
344+
let admin = Address::generate(&env);
345+
let non_admin = Address::generate(&env);
346+
347+
client.initialize(&admin);
348+
349+
// Non-admin attempting upgrade should panic
350+
let fake_hash = BytesN::from_array(&env, &[0u8; 32]);
351+
client.upgrade(&non_admin, &fake_hash);
352+
}
298353
}

0 commit comments

Comments
 (0)