This document describes how the Callora smart contracts are deployed, how state is stored, and how to upgrade or migrate each contract. Soroban contract upgradeability is limited: you cannot replace the code of an existing contract instance in place. Migration is done by deploying a new contract and migrating state and traffic.
- Architecture Overview
- Contract Storage Layouts
- Upgrade Choreography
- Backend Coordination
- Admin Key Handling
- Rollback Stance
- Stellar Network Procedures
- Verification Checklist
The Callora workspace consists of three independently deployed Soroban smart contracts:
| Contract | Crate | Purpose |
|---|---|---|
| callora-vault | contracts/vault |
USDC vault for prepaid API calls; tracks per-user balance, min deposits, and authorized callers |
| callora-revenue-pool | contracts/revenue_pool |
Receives USDC from vault deducts and distributes to developers |
| callora-settlement | contracts/settlement |
Developer balance tracking and global pool settlement |
- One WASM per contract type: Each contract is built as a separate Soroban WASM module.
- One instance per logical entity: Each vault/revenue-pool/settlement is a separate contract instance identified by its contract address.
- No in-place upgrades: Soroban does not support replacing contract code in place. To change behavior, you must deploy a new WASM and migrate state.
User deposits USDC
│
▼
┌─────────┐ deduct ┌──────────────┐ distribute ┌─────────────┐
│ Vault │ ───────────────► │ Revenue Pool │ ─────────────────► │ Developer │
│ │ (if revenue_ │ │ │ Wallet │
└─────────┘ pool set) └──────────────┘ └─────────────┘
┌─────────┐ deduct ┌────────────┐
│ Vault │ ───────────────► │ Settlement │ (alternative to revenue pool)
└─────────┘ └────────────┘
The vault uses instance storage with the following keys:
| StorageKey | Type | Description |
|---|---|---|
MetaKey |
VaultMeta |
Owner address, tracked balance, authorized caller, min deposit |
AllowedDepositors |
Vec<Address> |
List of addresses permitted to deposit |
Admin |
Address |
Admin address (defaults to owner at init) |
UsdcToken |
Address |
USDC token contract address |
Settlement |
Option<Address> |
Optional settlement contract for deduct flow |
RevenuePool |
Option<Address> |
Optional revenue pool address for deduct flow |
MaxDeduct |
i128 |
Maximum amount per single deduct (default i128::MAX) |
Metadata(String) |
String |
Per-offering metadata (IPFS CID or URI) |
VaultMeta structure (defined in lib.rs:46-51):
pub struct VaultMeta {
pub owner: Address,
pub balance: i128,
pub authorized_caller: Option<Address>,
pub min_deposit: i128,
}Init signature (lib.rs:93-131):
pub fn init(
env: Env,
owner: Address,
usdc_token: Address,
initial_balance: Option<i128>,
authorized_caller: Option<Address>,
min_deposit: Option<i128>,
revenue_pool: Option<Address>,
max_deduct: Option<i128>,
) -> VaultMetaBehavior note (pause semantics):
pause()is a circuit breaker for deposit-like flows.- While paused,
deposit()is rejected, butdeduct()andbatch_deduct()still execute and still emitdeductevents. - If you rely on “pause stops all balance movement”, you must update your operational assumptions and monitoring.
| Key | Type | Description |
|---|---|---|
admin |
Address |
Admin address; may call distribute and set_admin |
usdc |
Address |
USDC token contract address |
Init signature (lib.rs:28-39):
pub fn init(env: Env, admin: Address, usdc_token: Address)| Key | Type | Description |
|---|---|---|
admin |
Address |
Admin address; may call set_admin, set_vault |
vault |
Address |
Registered vault address |
developer_balances |
Map<Address, i128> |
Per-developer balance tracking |
global_pool |
GlobalPool |
Total balance and last updated timestamp |
GlobalPool structure (lib.rs:16-19):
pub struct GlobalPool {
pub total_balance: i128,
pub last_updated: u64,
}Init signature (lib.rs:51-65):
pub fn init(env: Env, admin: Address, vault_address: Address)Because contracts reference each other by address, upgrades must be sequenced carefully to maintain consistency.
1. Settlement (if changing)
│
▼
2. Revenue Pool (if changing)
│
▼
3. Vault (always upgrade last if updating references)
Rationale: The vault can reference either a settlement contract or a revenue pool. Update the target first, then update the vault's reference.
-
Export state
# Read current state via RPC or CLI soroban contract invoke --contract-id <VAULT_ID> -- get_meta soroban contract invoke --contract-id <VAULT_ID> -- get_admin soroban contract invoke --contract-id <VAULT_ID> -- get_settlement # if set
-
Deploy new vault WASM
cargo build --target wasm32-unknown-unknown --release -p callora-vault soroban contract deploy --wasm target/wasm32-unknown-unknown/release/callora_vault.wasm --source <OWNER_ACCOUNT>
-
Initialize new vault (same owner, same USDC token, migrate balance)
soroban contract invoke --contract-id <NEW_VAULT_ID> -- init \ --owner <OWNER> \ --usdc_token <USDC_TOKEN> \ --initial_balance <CURRENT_BALANCE> \ --authorized_caller <AUTH_CALLER> \ --min_deposit <MIN_DEPOSIT> \ --revenue_pool <REVENUE_POOL_OR_NONE> \ --max_deduct <MAX_DEDUCT>
-
Transfer actual USDC (if balance was real USDC)
# From old vault owner, withdraw to self, then deposit to new vault soroban contract invoke --contract-id <OLD_VAULT_ID> -- withdraw --amount <BALANCE> # Then deposit from owner to new vault soroban contract invoke --contract-id <NEW_VAULT_ID> -- deposit --caller <OWNER> --amount <BALANCE>
-
Update backend config (see Backend Coordination below)
-
Decommission old vault (stop using; do not delete)
-
Export state
soroban contract invoke --contract-id <RP_ID> -- get_admin soroban contract invoke --contract-id <RP_ID> -- balance
-
Deploy new revenue pool WASM
cargo build --target wasm32-unknown-unknown --release -p callora-revenue-pool soroban contract deploy --wasm target/wasm32-unknown-unknown/release/callora_revenue_pool.wasm --source <ADMIN_ACCOUNT>
-
Initialize new revenue pool
soroban contract invoke --contract-id <NEW_RP_ID> -- init \ --admin <ADMIN> \ --usdc_token <USDC_TOKEN>
-
Transfer USDC balance (if applicable)
- Revenue pool holds actual USDC tokens
- Transfer from old contract to new via token
transfer
-
Update vault references (if vault points to this revenue pool)
-
Decommission old revenue pool
-
Export state
soroban contract invoke --contract-id <SETTLE_ID> -- get_admin soroban contract invoke --contract-id <SETTLE_ID> -- get_global_pool soroban contract invoke --contract-id <SETTLE_ID> -- get_all_developer_balances
-
Deploy new settlement WASM
cargo build --target wasm32-unknown-unknown --release -p callora-settlement soroban contract deploy --wasm target/wasm32-unknown-unknown/release/callora_settlement.wasm --source <ADMIN_ACCOUNT>
-
Initialize new settlement
soroban contract invoke --contract-id <NEW_SETTLE_ID> -- init \ --admin <ADMIN> \ --vault_address <VAULT_ADDRESS>
-
Re-credit developer balances
- Call
receive_paymentfor each developer with their balance - Or implement a migration helper contract
- Call
-
Update vault references
-
Decommission old settlement
When any contract is upgraded, the backend must update its configuration:
| Upgrade Type | Backend Config Update |
|---|---|
| New vault instance | Update vault_contract_id per user/API |
| New revenue pool | Update revenue_pool_contract_id in vault (via set_settlement) |
| New settlement | Update settlement_contract_id in vault (via set_settlement) |
| Vault points to new revenue pool | Call vault.set_revenue_pool(new_address) |
| Vault points to new settlement | Call vault.set_settlement(new_address) |
1. Deploy new contract(s)
2. Initialize with migrated state
3. Update backend configuration (new contract addresses)
4. Verify backend can reach new contracts
5. Point traffic to new contract (gradual or atomic switchover)
6. Monitor for 24-48 hours
7. Decommission old contract (stop calls, archive address)
- Verify
get_meta()/get_admin()/balance()return expected values - Run a small test transaction before full traffic switchover
- Monitor error rates and revert if anomalies detected
Admin keys for all three contracts should be managed with care:
| Contract | Admin Role | Key Type Recommendation |
|---|---|---|
| Vault | Sets distribution recipients, authorized callers, min deposits | Hardware wallet or multisig |
| Revenue Pool | Calls distribute, batch_distribute, set_admin |
Hardware wallet or multisig |
| Settlement | Calls set_admin, set_vault, receives payments |
Hardware wallet or multisig |
To rotate an admin key:
- Ensure new admin key is accessible (test in non-production first)
- Call
set_adminon each affected contract:soroban contract invoke --contract-id <CONTRACT_ID> -- set_admin \ --caller <OLD_ADMIN> \ --new_admin <NEW_ADMIN>
- Verify by calling
get_admin()and confirming new address - Update backend to use new admin key for signing transactions
- Archive old admin key (do not delete; retain for audit purposes)
- If using a Stellar multisig account (e.g., 2-of-3), all admin operations require sufficient signers
- Coordinate multisig transactions carefully to avoid being locked out
- Test multisig threshold changes on testnet before mainnet
Rollback is not supported as a first-class operation. Due to Soroban's immutability design, there is no mechanism to revert a contract instance to previous code. Instead, rollback is achieved through redeployment.
If an upgrade causes issues:
- Do not attempt to modify the upgraded contract — it cannot be changed
- Deploy the previous WASM as a new instance:
# Get previous WASM (from git history or artifact store) git checkout <PREVIOUS_COMMIT> cargo build --target wasm32-unknown-unknown --release -p callora-vault soroban contract deploy --wasm target/wasm32-unknown-unknown/release/callora_vault.wasm
- Migrate state back (export from current, import to previous)
- Update backend to point to the previous contract instance
- Investigate the issue in the new contract separately (do not delete the new contract yet)
| Scenario | Rollback Recommended? | Alternative |
|---|---|---|
| Critical bug affecting funds | Yes | Deploy hotfix and migrate |
| Non-critical bug | No | Deploy fix in next release cycle |
| Performance regression | No | Optimize and redeploy |
| Feature removal | No | Communicate to users; deprecate |
- Always test upgrades on testnet first
- Run full test suite (
cargo test) and coverage (./scripts/coverage.sh) before any upgrade - Use gradual traffic switchover (e.g., 5% → 25% → 100%) to catch issues early
Soroban smart contracts on Stellar have the following upgrade characteristics:
- No in-place code replacement: Once a contract is deployed, its WASM code cannot be changed.
- Contract addresses are deterministic: The address is derived from the deployer's public key and sequence number, not from the WASM code hash.
- Storage persists independently: Contract storage exists separately from the WASM code and travels with the contract address.
Soroban enforces a 64 KB WASM size limit. The Callora contracts are optimized to stay under this limit:
# Check WASM size
./scripts/check-wasm-size.sh
# Build optimized WASM
cargo build --target wasm32-unknown-unknown --release -p callora-vaultCurrent optimized sizes should be approximately:
callora-vault: ~17-18 KBcallora-revenue-pool: ~15-16 KBcallora-settlement: ~16-17 KB
-
Build the WASM
cargo build --target wasm32-unknown-unknown --release -p <crate-name>
-
Deploy using Soroban CLI or Stellar Laboratory
soroban contract deploy \ --wasm target/wasm32-unknown-unknown/release/<contract>.wasm \ --source <DEPLOYER_ACCOUNT>
-
Initialize the contract
soroban contract invoke --contract-id <NEW_ID> -- init <args>
-
Verify on-chain
soroban contract invoke --contract-id <NEW_ID> -- get_meta # or other view function
| Network | Use For |
|---|---|
| Testnet | Development, testing upgrades, integration testing |
| Mainnet | Production deployment |
Never deploy experimental code directly to mainnet.
Before and after any upgrade, verify the following:
- All tests pass:
cargo test - Coverage above 95%:
./scripts/coverage.sh - Clippy clean:
cargo clippy --all-targets --all-features -- -D warnings - Format clean:
cargo fmt -- --check - WASM size under limit:
./scripts/check-wasm-size.sh - State export from old contracts completed
- Backend configuration backup taken
- Rollback plan documented and tested (if critical)
-
get_meta()/get_admin()/balance()return expected values - Test transaction executed successfully
- Backend can communicate with new contracts
- Error rates nominal (compare to pre-upgrade baseline)
- Event emissions correct (verify emitted events match expected)
- Monitoring dashboards updated (if applicable)
- Old contract marked as decommissioned (no new traffic)
Per the contribution guidelines:
- Run
cargo fmt,cargo clippy --all-targets --all-features -- -D warnings, andcargo testfrom workspace root - For WASM builds:
cargo build --target wasm32-unknown-unknown --release -p callora-vault(adjust-pas needed) - Run
./scripts/coverage.sh(orcargo tarpaulinpertarpaulin.toml) - Include summarized test output in PR description
| Aspect | Recommendation |
|---|---|
| Upgrade approach | Deploy new contract, migrate state, redirect traffic |
| Upgrade order | Settlement → Revenue Pool → Vault |
| Rollback | Not supported; deploy previous WASM as new instance |
| Admin keys | Hardware wallet or multisig; rotate via set_admin |
| Testing | Testnet first, then gradual mainnet rollout |
| Verification | Run full test suite and coverage before any upgrade |
For detailed storage layouts, see: