This guide covers the upgrade path for the standalone contracts/nova-rewards Soroban contract. The contract exposes two admin-only entrypoints for this process:
The nova-rewards Soroban contract supports in-place WASM upgrades via
env.deployer().update_current_contract_wasm(). All instance storage
(balances, admin, version counters) persists across upgrades.
Two storage keys track upgrade state:
| Key | Description |
|---|---|
MigrationVersion |
Target version — incremented by upgrade() |
MigratedVersion |
Last completed migration — incremented by migrate() |
migrate() is gated: it only runs when migrated_version < migration_version,
so it is safe to call exactly once per upgrade and will panic if called again.
Because the contract stores its operational state in instance storage, balances, staking records, swap configuration, and the saved migration version remain available after a successful code swap.
- Install Rust and the
wasm32-unknown-unknowntarget. - Install the Stellar CLI used by the repository deployment workflow.
- Have the admin secret, or another signer authorized to act for the current admin.
- Know the deployed contract ID for the
nova-rewardsinstance you are updating.
# Rust + wasm32 target
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
# Soroban CLI
cargo install --locked soroban-cliIn contracts/nova-rewards/src/lib.rs, inside migrate(), add a versioned
block for any data transformations needed by the new release:
// Example: backfill a new field for version 2
if migration_version == 2 {
// ... transform storage ...
}The build artifact is:
cd contracts/nova-rewards
cargo build --release --target wasm32-unknown-unknown
# Output: ../../target/wasm32-unknown-unknown/release/nova_rewards.wasmUse the optimized artifact if wasm-opt is available; otherwise use the raw release WASM.
Upload the artifact to the target network and capture the returned hash:
stellar contract upload \
--wasm contracts/nova-rewards/target/wasm32-unknown-unknown/release/nova_rewards.optimized.wasm \
--network-passphrase "Test SDF Network ; September 2015" \
--rpc-url https://soroban-testnet.stellar.org \
--source <ADMIN_SECRET>This prints the 64-character hex WASM hash, e.g. abc123...def456.
stellar contract invoke \
--network testnet \
--source-account alice \
--id <CONTRACT_ID> \
-- upgrade \
--new_wasm_hash <WASM_HASH_FROM_STEP_3>What happens internally:
MigrationVersionis incremented.- The new WASM hash is stored under
PendingWasmHash. env.deployer().update_current_contract_wasm(new_wasm_hash)swaps the bytecode.
stellar contract invoke \
--network testnet \
--source-account alice \
--id <CONTRACT_ID> \
-- migrateWhat happens internally:
- Checks
migrated_version < migration_version; panics with"migration already applied"if already done. - Runs version-specific data transformations.
- Sets
MigratedVersion = MigrationVersion. - Emits
upgradedevent with(wasm_hash, migration_version).
# Both counters should match after a successful migrate()
soroban contract invoke --network testnet --id <CONTRACT_ID> -- get_migration_version
soroban contract invoke --network testnet --id <CONTRACT_ID> -- get_migrated_versionConfirm the upgraded event appears in the transaction record on the Stellar explorer.
- Invoke
get_migrated_versionand confirm it matchesget_migration_version(both counters equal means the migration for this upgrade completed). - Query representative balances with
get_balanceto confirm state survived the code swap. - Re-run
contracts/nova-rewards/tests/upgrade.rs. - If the release touched swaps or staking, also run the swap and staking test suites.
- Only the
adminaddress set duringinitializemay callupgradeormigrate. migrate()panics with"migration already applied"if called more than once per version.- All instance storage (balances, admin, version counters) survives the WASM swap.
Soroban does not support automatic rollback. To revert:
- Install the previous WASM and note its hash.
- Call
upgrade()with the old hash. - Call
migrate()— add compensating data transformations if needed.
The following contracts use a M-of-N multisig approval pattern instead of the two-step upgrade()/migrate() flow:
campaignescrowdistributiongovernanceadmin_rolescontract_state
- Each contract is initialized with a
signerslist and athreshold(M-of-N). - Each authorized signer calls
approve_upgrade(signer, new_wasm_hash). - When the approval count reaches
threshold, the upgrade executes automatically:- A
ContractUpgradedevent is emitted with the new WASM hash. env.deployer().update_current_contract_wasm(new_wasm_hash)is called.- All instance storage (state, admin, config) is preserved.
- A
- Unauthorized callers (not in the signer set) are rejected with
"not an authorized signer". - Duplicate approvals from the same signer are rejected with
"already approved". - The upgrade fires exactly once per hash — approvals are cleared before the WASM swap.
# Signer 1 approves
stellar contract invoke --network testnet --source signer1 \
--id <CAMPAIGN_CONTRACT_ID> \
-- approve_upgrade \
--signer <SIGNER1_ADDRESS> \
--new_wasm_hash <NEW_WASM_HASH>
# Signer 2 approves — threshold reached, upgrade fires
stellar contract invoke --network testnet --source signer2 \
--id <CAMPAIGN_CONTRACT_ID> \
-- approve_upgrade \
--signer <SIGNER2_ADDRESS> \
--new_wasm_hash <NEW_WASM_HASH>stellar contract invoke --network testnet --id <CONTRACT_ID> \
-- get_upgrade_approvals \
--new_wasm_hash <NEW_WASM_HASH>
# Returns: current approval count (u32)
stellar contract invoke --network testnet --id <CONTRACT_ID> \
-- get_threshold
# Returns: required approvals (u32)All contracts emit this event when the upgrade executes:
| Contract | topics | data |
|---|---|---|
| campaign | ("camp", "upgraded") |
(schema_version: 1, new_wasm_hash: BytesN<32>) |
| escrow | ("escrow", "upgraded") |
(schema_version: 1, new_wasm_hash: BytesN<32>) |
| distribution | ("dist", "upgraded") |
(schema_version: 1, new_wasm_hash: BytesN<32>) |
| governance | ("gov", "upgraded") |
(schema_version: 1, new_wasm_hash: BytesN<32>) |
| admin_roles | ("adm_roles", "upgraded") |
(schema_version: 1, new_wasm_hash: BytesN<32>) |
| contract_state | ("state", "upgraded") |
(schema_version: 1, new_wasm_hash: BytesN<32>) |
The nova-rewards contract emits ("nova_rwd", "upgraded") with an additional migration_version: u32 field from its migrate() function.
All contracts store operational state in Soroban instance storage, which persists across WASM upgrades. After an upgrade:
- Admin addresses, signer sets, and thresholds are unchanged.
- All contract-specific state (campaigns, escrows, distributions, proposals, etc.) is preserved.
- The new WASM code takes effect immediately for all subsequent invocations.
This section walks through a complete upgrade of the campaign contract from
start to finish using a 3-of-5 signer set. Substitute distribution or
governance as needed; the procedure is identical.
- Stellar CLI ≥ 21 installed (
stellar --version). - The five signer key-pairs available as named profiles in
~/.config/stellar/identity/. - The deployed contract ID (visible in
docs/contracts.mdor your deploy log).
# Build the optimised release artifact
cd contracts/campaign
cargo build --release --target wasm32v1-none
cd ../..
# Upload to the target network and capture the returned hash
NEW_WASM_HASH=$(stellar contract upload \
--wasm target/wasm32v1-none/release/campaign.wasm \
--network testnet \
--source admin \
| tr -d '[:space:]')
echo "New WASM hash: $NEW_WASM_HASH"The hash is a 64-character hex string, e.g.
a3f1b2c4d5e6... — keep it in a shared document so all five signers can
verify they are approving the same artifact.
Before approving, each signer should verify the uploaded WASM matches the locally-built artifact:
# Download and compare (requires stellar contract fetch or soroban CLI)
stellar contract fetch \
--wasm-hash "$NEW_WASM_HASH" \
--network testnet \
--out /tmp/fetched.wasm
sha256sum target/wasm32v1-none/release/campaign.wasm /tmp/fetched.wasm
# Both lines must show the same checksumstellar contract invoke \
--network testnet \
--source signer1 \
--id "$CAMPAIGN_CONTRACT_ID" \
-- approve_upgrade \
--signer "$SIGNER1_ADDRESS" \
--new_wasm_hash "$NEW_WASM_HASH"Poll the approval counter after each invocation:
stellar contract invoke \
--network testnet \
--id "$CAMPAIGN_CONTRACT_ID" \
-- get_upgrade_approvals \
--new_wasm_hash "$NEW_WASM_HASH"
# Returns: 1stellar contract invoke \
--network testnet \
--source signer2 \
--id "$CAMPAIGN_CONTRACT_ID" \
-- approve_upgrade \
--signer "$SIGNER2_ADDRESS" \
--new_wasm_hash "$NEW_WASM_HASH"
# Counter: 2stellar contract invoke \
--network testnet \
--source signer3 \
--id "$CAMPAIGN_CONTRACT_ID" \
-- approve_upgrade \
--signer "$SIGNER3_ADDRESS" \
--new_wasm_hash "$NEW_WASM_HASH"
# Approval count reaches 3 == threshold → upgrade executes automatically.
# The ("camp", "upgraded") event is emitted with (schema_version=1, new_wasm_hash).
# The approval key is removed from storage.# Approval counter should now return 0 (key cleared)
stellar contract invoke \
--network testnet \
--id "$CAMPAIGN_CONTRACT_ID" \
-- get_upgrade_approvals \
--new_wasm_hash "$NEW_WASM_HASH"
# Expected: 0
# Confirm the threshold config is still intact
stellar contract invoke \
--network testnet \
--id "$CAMPAIGN_CONTRACT_ID" \
-- get_threshold
# Expected: 3
# Check the upgraded event in the transaction record
stellar tx view --network testnet --hash <TX_HASH_OF_SIGNER3_INVOCATION>A WASM hash mismatch means signers are not all voting on the same upgrade. This is treated as two completely independent ballots — each unique hash has its own approval counter.
get_upgrade_approvalsreturns a count for one hash but0for another.- The upgrade never fires even though enough signers have called
approve_upgrade.
Signer 1 approves hash A → counter(A) = 1
Signer 2 approves hash B → counter(B) = 1 ← different hash!
Signer 3 approves hash A → counter(A) = 2 (threshold is 3 — still not enough)
Neither hash reaches the 3-of-5 threshold.
- All signers must agree on the canonical hash before any invocation.
- Use the verification step in Step 2 above to confirm everyone's local build produces the same hash.
- If some signers already approved the wrong hash, those approvals are harmless — they sit in storage against the wrong key and will never fire an upgrade (because no one will complete that ballot).
- Simply coordinate on the correct hash and repeat Steps 3–5 using the agreed hash.
There is no penalty for an orphaned ballot. The stale approval entries under the wrong hash consume a small amount of instance storage but cause no security risk. They are removed automatically if someone ever completes that ballot (which would require the threshold to be reached on the wrong hash — practically impossible without coordination).
The integration tests for all three contracts are located in:
contracts/integration_tests/tests/upgrade_approval_tests.rs
Run them with:
cd contracts
cargo test -p integration_tests upgrade_approval| Test name | Scenario |
|---|---|
*_single_signer_below_threshold_upgrade_blocked |
1 of 2 signers approves; upgrade does NOT fire; counter stays at 1 |
*_threshold_reached_upgrade_executes_and_emits_event |
2nd signer completes 2-of-2; upgrade fires; counter reset to 0; event emitted |
*_duplicate_approval_rejected |
Same signer approves twice; panics with "already approved" |
*_approval_state_cleared_after_upgrade |
After upgrade the approval key is absent from storage |
*_unauthorized_signer_rejected |
Address not in signer set; panics with "not an authorized signer" |
*_different_hash_is_independent_ballot |
Two different hashes each get 1 approval; neither fires |
*_three_of_five_accumulates_then_upgrades |
Progressive 3-of-5 accumulation; count 1 → 2 → upgrade |
governance_upgrade_preserves_existing_proposal_state |
Pre-upgrade governance proposals survive the WASM swap |
These tests cover every acceptance criterion from the issue:
- Upgrade is blocked until M-of-N threshold is reached.
- A signer cannot cast a second vote on the same hash.
- The
upgradedevent is emitted with the correct WASM hash once threshold is met. - Approval state is cleared after a successful upgrade, preventing re-use.