Document Version: 1.0
Date: May 2024
Status: Pre-Audit
The Invoice Liquidity Network (ILN) smart contract supports upgrades via WASM hash replacement. This guide documents the complete upgrade procedure, including access control, state migration, rollback procedures, and verification steps.
A Soroban contract upgrade replaces the executable code (WASM binary) while preserving all persistent state. The contract's storage and data remain intact, allowing a seamless transition to new functionality.
Contract State Preserved During Upgrade:
- All invoice records (created invoices, status, funding history)
- Reputation scores (payer and LP reputations)
- Configuration parameters (decay rates, fee rates)
- Admin address and control settings
- Fund queues, appeals, and dispute records
What Can Be Changed:
- Contract logic and behavior
- New functions and features
- Bug fixes and security patches
- Event signatures (within limitations)
What Cannot Be Changed:
- Storage layout of existing data structures (unless data migration is performed)
- Data type sizes in struct fields (e.g.,
u32→u64) - Persistent storage keys (must remain compatible)
Before initiating an upgrade, the following checklist must be completed:
- New WASM binary has passed security audit
- All test suites pass (unit, integration, property-based tests)
- Regression tests confirm backward compatibility
- No breaking changes to external contract interfaces
- Gas costs validated for critical operations
- Current state dump captured from blockchain
- If schema changes required: migration logic designed and tested
- Rollback state snapshot prepared
- Data compatibility verified (e.g., no truncation of numbers)
- Upgrade notes published (what changed, why, benefits)
- LP and freelancer notifications sent (upgrade window, any user action required)
- Governance decision recorded (if governance-controlled upgrade)
- Change log entry added to repository
- Testnet upgrade performed and validated
- Rollback procedure tested
- Admin team trained on upgrade process
- Communication channels ready for support (Discord, forums)
- Monitoring and alerting configured for post-upgrade
- Upgrade approved by governance (if applicable)
- Terms of service updated (if user-facing changes)
- Tax implications reviewed (if economic parameters change)
Goal: Verify the upgrade works correctly before touching mainnet.
Process:
# 1. Build the new WASM binary
cargo build --release --target wasm32-unknown-unknown
# 2. Compute WASM hash
WASM_HASH=$(sha256sum target/wasm32-unknown-unknown/release/invoice_liquidity.wasm | cut -d' ' -f1)
echo "New WASM Hash: $WASM_HASH"
# 3. Deploy to testnet
soroban contract deploy --network testnet \
--source-account ADMIN_KEY \
--wasm target/wasm32-unknown-unknown/release/invoice_liquidity.wasm
# 4. Call upgrade function on testnet
soroban contract invoke \
--id CONTRACT_ID \
--network testnet \
--source-account ADMIN_KEY \
-- upgrade \
--new_wasm_hash "$WASM_HASH"
# 5. Verify state integrity after upgrade
# - Check invoice counts match pre-upgrade
# - Verify sample invoice data is intact
# - Confirm reputation scores unchanged
# - Test new functionality (if applicable)
# 6. Perform smoke tests
# - Submit new invoice
# - Fund existing invoice
# - Mark invoice as paid
# - Query contract statsSuccess Criteria:
- ✅ All invoices readable and unchanged
- ✅ Reputation scores intact
- ✅ New functions work as expected
- ✅ Events are emitted correctly
- ✅ No state inconsistencies detected
If the contract is controlled by a governance token or multi-sig admin:
-
Publish Upgrade Proposal
- Title: "ILN Contract Upgrade: [Brief Description]"
- Description: Link to detailed upgrade notes
- WASM Hash: Include for verification
- Implementation: Call
upgrade(new_wasm_hash)after approval
-
Voting Period
- Vote duration: Typically 3-7 days
- Quorum: As specified in governance contract
- Approval threshold: As specified in governance contract
-
Approval Confirmation
- Record governance decision on-chain
- Document voting results
- Publish rationale for transparency
# 1. Build final release binary
cargo build --release --target wasm32-unknown-unknown
# 2. Compute WASM hash (MUST match governance proposal)
WASM_HASH=$(sha256sum target/wasm32-unknown-unknown/release/invoice_liquidity.wasm | cut -d' ' -f1)
echo "Final WASM Hash: $WASM_HASH"
# Verify against governance proposal
# WASM_HASH should match the one voters approved
# 3. Call upgrade on mainnet
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
--source-account ADMIN_KEY \
-- upgrade \
--new_wasm_hash "$WASM_HASH"
# 4. Confirm event emission
soroban events \
--network mainnet \
--start-ledger CURRENT_LEDGER | grep "ContractUpgraded"Immediately after upgrade completes:
# 1. Verify contract state integrity
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
-- get_contract_stats
# Expected output:
# - total_invoices should match pre-upgrade value
# - total_funded should match pre-upgrade value
# - total_paid should match pre-upgrade value
# 2. Spot-check sample invoices
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
-- get_invoice \
--invoice_id 1
# 3. Monitor on-chain events
# Watch for errors in ContractUpgraded event
# Confirm no unexpected error events
# 4. Test new functionality (if applicable)
# Submit test invoice
# Fund test invoice
# Verify new features work
# 5. Check application/wallet status
# Ensure all downstream systems (frontends, indexers) handle upgradeWhy: Ensure the uploaded WASM binary matches the approved code.
Process:
# 1. Download compiled binary from source repository
git clone https://github.com/drips-network/ILN-Smart-Contract.git
cd ILN-Smart-Contract
git checkout <RELEASE_TAG> # e.g., v1.2.0
# 2. Rebuild locally
cargo build --release --target wasm32-unknown-unknown
# 3. Compute local hash
LOCAL_HASH=$(sha256sum target/wasm32-unknown-unknown/release/invoice_liquidity.wasm | cut -d' ' -f1)
# 4. Fetch on-chain hash from ContractUpgraded event
CHAIN_HASH=$(soroban events \
--network mainnet \
--topic "upgraded" | jq -r '.new_wasm_hash')
# 5. Verify match
if [ "$LOCAL_HASH" == "$CHAIN_HASH" ]; then
echo "✅ WASM hash verified!"
else
echo "❌ WASM hash mismatch!"
echo "Local: $LOCAL_HASH"
echo "Chain: $CHAIN_HASH"
exit 1
fiData Type Changes:
If the upgrade includes changes to struct field types, manual state migration is required:
// ❌ BREAKING: This change requires migration
// pub discount_rate: u32, // Old
pub discount_rate: u64, // New (larger type)
// ✅ SAFE: This change is backward compatible
// pub amount_funded: i128,
pub total_amount_funded: i128, // Different field name, old field still existsSafe Changes (No Migration Needed):
- ✅ Adding new fields (with default values for existing records)
- ✅ Removing unused fields (old data is ignored)
- ✅ Changing function implementations (not called during storage reads)
- ✅ Adding new event types
Unsafe Changes (Migration Required):
- ❌ Changing struct field types (e.g.,
u32→u64) - ❌ Reordering struct fields
- ❌ Changing field names (existing data uses old keys)
- ❌ Changing enum variants
Migration Pattern (if needed):
// During initialization or first-call-after-upgrade:
pub fn migrate_state(env: Env) -> Result<(), ContractError> {
require_admin(&env)?;
// Read old state
let old_score = env.storage()
.persistent()
.get::<_, u32>(&StorageKey::OldPayerScore(payer.clone()))
.unwrap_or(50);
// Transform and write new state
let new_rep = ReputationScore {
score: old_score,
last_activity_ledger: env.ledger().sequence(),
};
env.storage()
.persistent()
.set(&StorageKey::PayerScore(payer.clone()), &new_rep);
Ok(())
}Use Case: If post-upgrade issues are discovered and a rollback is necessary.
Decision Tree:
├─ Data Corruption?
│ ├─ YES → Rollback required (cannot fix without reverting)
│ └─ NO → Continue to Step 2
├─ Critical Functionality Broken?
│ ├─ YES → Rollback recommended
│ └─ NO → Continue to Step 2
└─ Can Issue Be Fixed with Hotfix?
├─ YES → Deploy hotfix (new upgrade)
└─ NO → Proceed with rollback
# 1. Identify the last stable WASM hash
STABLE_HASH="abc123..." # From pre-upgrade records
# 2. Rebuild the stable version from git tag
git checkout v1.1.0 # The previous stable version
cargo build --release --target wasm32-unknown-unknown
VERIFY_HASH=$(sha256sum target/wasm32-unknown-unknown/release/invoice_liquidity.wasm | cut -d' ' -f1)
# Verify hash matches records
if [ "$VERIFY_HASH" != "$STABLE_HASH" ]; then
echo "❌ Rollback hash verification failed!"
exit 1
fi# Call upgrade with the previous WASM hash
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
--source-account ADMIN_KEY \
-- upgrade \
--new_wasm_hash "$STABLE_HASH"
# Verify rollback
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
-- get_contract_stats| Data Element | Impact | Mitigation |
|---|---|---|
| New Invoices (created after failed upgrade) | Lost (no longer compatible) | Require users to re-submit |
| Existing Invoices | Recovered (state intact) | No action needed |
| Reputation Scores | Recovered | No action needed |
| Fund Queues | Recovered (may be stale) | May need to refresh |
Communication After Rollback:
- Notify all users via Discord/Twitter
- Publish post-mortem analysis (what failed, root cause)
- Commit to fixed timeline for re-attempt
- Offer support for affected transactions
# Run before upgrade execution
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
-- get_contract_stats > pre_upgrade_stats.json
# Dump first 100 invoices
for i in {1..100}; do
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
-- get_invoice \
--invoice_id $i >> pre_upgrade_invoices.json 2>/dev/null || true
done
# Save to version control
git add pre_upgrade_stats.json pre_upgrade_invoices.json
git commit -m "Pre-upgrade snapshot at ledger $LEDGER_HEIGHT"# Rerun same queries
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
-- get_contract_stats > post_upgrade_stats.json
# Compare (should be identical)
diff pre_upgrade_stats.json post_upgrade_stats.json
# If diff is empty: ✅ State intact
# If diff found: ❌ Data loss or corruption- Do Not Panic — State remains unchanged until confirmed on-chain
- Wait for Confirmation — Soroban network may take time to process
- Check Event Logs
soroban events --network mainnet --topic "upgraded" | tail -20
- If No Event After 5 Minutes:
- Network may have rejected the upgrade (not authorized, invalid signature)
- Retry with correct admin credentials
- If Event Shows Error:
- Check admin status and auth
- Verify WASM hash format (must be 32 bytes)
- Check contract balance for fees
- Stop all operations — Call
pause()immediately - Notify community — Publish incident notice
- Assess scope:
soroban contract invoke \ --id CONTRACT_ID \ --network mainnet \ -- get_contract_stats
- Decision:
- Minor issue → Deploy hotfix upgrade
- Major issue → Rollback to previous version
- Unknown issue → Fork to testnet for diagnosis
The following parameters can be safely updated post-upgrade without breaking contract:
// Can be updated without upgrade
pub fn update_fee_rate(env: Env, rate: u32)
pub fn update_max_discount(env: Env, rate: u32)
pub fn update_config(env: Env, high_rep_threshold: u32, ...)
// Requires upgrade if logic needs to change
pub fn submit_invoice(...) // Business logic
pub fn fund_invoice(...) // Core mechanicsBest Practice: Use governance to change parameters, reserve upgrades for code logic changes.
| Metric | Target | Frequency | Alert Threshold |
|---|---|---|---|
| Invoice Submission Rate | No sudden drop | 1 hour | <50% of baseline |
| Fund Success Rate | >99% | 1 hour | <98% |
| Default Rate | Baseline ±5% | 1 day | >10% deviation |
| Transaction Latency | <2s | 5 minutes | >5s |
| Event Emission | 100% | 1 hour | Missing events |
# Monitor invoice submissions
while true; do
soroban events \
--network mainnet \
--topic "submitted" \
--start-ledger $(date +%s) | wc -l
sleep 3600 # Every hour
done
# Monitor errors
soroban events \
--network mainnet \
--type "error" \
--start-ledger UPGRADE_LEDGER | jq .
# Monitor fund queue resolutions
soroban events \
--network mainnet \
--topic "fund_queue_resolved" | tail -20For mainnet deployments, use a multi-sig admin:
# Create upgrade proposal (2-of-3 multi-sig)
soroban contract invoke \
--id MULTISIG_CONTRACT \
-- propose_upgrade \
--target_contract CONTRACT_ID \
--new_wasm_hash "$WASM_HASH" \
--description "Upgrade ILN to v1.2: Bug fixes and new features"
# Signers review and vote
# After 2+ approvals:
soroban contract invoke \
--id MULTISIG_CONTRACT \
--source-account SIGNER1 \
-- execute_upgrade \
--proposal_id $PROPOSAL_IDOnce governance DAO is deployed:
- Create governance proposal (Snapshot voting)
- Warm-up period (discussion)
- Voting period (3-7 days)
- Time-lock (24-48 hours)
- Execute on-chain
Error: invalid_wasm_hash
Cause: WASM hash is not 32 bytes (256 bits)
Solution:
# Verify hash is correct format
echo "$WASM_HASH" | wc -c # Should be 65 (64 hex chars + newline)
# If wrong, recompute:
sha256sum target/wasm32-unknown-unknown/release/invoice_liquidity.wasmError: Unauthorized
Cause: Caller is not the contract admin
Solution:
# Verify admin address
soroban contract invoke \
--id CONTRACT_ID \
--network mainnet \
-- get_admin
# If using multi-sig, ensure all signers have signed
# If using single admin, verify private key is correctError: ContractNotFound
Cause: Network hasn't finalized upgrade yet or wrong contract ID
Solution:
# Wait 30 seconds and retry
sleep 30
# Verify contract ID
echo "Contract ID: $CONTRACT_ID"
# Check contract exists
soroban contract info --id $CONTRACT_ID --network mainnet
# If still failing, contact Stellar support- ✅ Always test on testnet first before mainnet upgrades
- ✅ Capture pre-upgrade state snapshot for rollback
- ✅ Publish upgrade notes with clear explanation of changes
- ✅ Use multi-sig admin for mainnet to prevent unauthorized upgrades
- ✅ Implement time-locks for governance-controlled upgrades
- ✅ Monitor metrics post-upgrade for anomalies
- ✅ Maintain rollback readiness for 48 hours after upgrade
- ✅ Document all upgrades with reason, date, WASM hash
- ❌ Don't upgrade without testing on testnet first
- ❌ Don't change struct field types without data migration
- ❌ Don't upgrade during peak usage (e.g., end-of-month invoicing rush)
- ❌ Don't skip governance approval if DAO-controlled
- ❌ Don't forget communication to users about upgrades
- ❌ Don't delete monitoring until 1 week post-upgrade stability
- ❌ Don't assume rollback won't be needed — always prepare
For upgrade-related questions or issues:
- GitHub Issues: https://github.com/drips-network/ILN-Smart-Contract/issues
- Discord: [Link to community Discord]
- Email: security@drips.network (for security issues)
# ILN Contract Upgrade Checklist v1.0
## Pre-Upgrade (Mainnet)
- [ ] Security audit completed
- [ ] All tests passing (unit, integration, fuzzing)
- [ ] Testnet upgrade successful
- [ ] State snapshot captured
- [ ] Governance approval obtained (if required)
- [ ] Admin credentials secured
- [ ] Stakeholder communication sent
- [ ] Rollback procedure tested
## Upgrade Execution
- [ ] WASM binary built and verified
- [ ] WASM hash computed and confirmed
- [ ] Admin auth signatures collected
- [ ] Upgrade transaction signed
- [ ] Upgrade transaction submitted
- [ ] Upgrade event confirmed on-chain
## Post-Upgrade (First Hour)
- [ ] Contract state integrity verified
- [ ] Sample invoices spot-checked
- [ ] New functionality tested (if applicable)
- [ ] No error events observed
- [ ] Monitoring and alerts active
- [ ] Team standing by for support
## Post-Upgrade (First 24 Hours)
- [ ] All downstream systems operational (frontends, indexers, APIs)
- [ ] User transactions processed normally
- [ ] Reputation scores stable
- [ ] Fund queue resolutions working
- [ ] Dispute/appeal system functional
- [ ] No unusual contract behavior
## Post-Upgrade (Stabilization)
- [ ] 1 week of stable operation confirmed
- [ ] Monitoring reduced to normal levels
- [ ] Rollback standby ended
- [ ] Upgrade documentation complete
- [ ] Post-mortem (if any issues) published
- [ ] Archive pre-upgrade snapshot
The v1 → v2 upgrade adds three fields to the persisted Invoice schema
(submitter_reputation, allowed_lps, is_auction). Because Soroban preserves
raw storage across a WASM-hash swap, existing Invoice records must be read and
re-written with the new fields populated (defaulted) or they will fail to decode
under v2. The migration is automated and verified by
scripts/migrate-v1-v2.ts.
The single source of truth is the pure migrateInvoice() function:
| v2 field | Default applied |
|---|---|
submitter_reputation |
the freelancer's current reputation score, else 50 |
allowed_lps |
null (invoice stays public) |
is_auction |
false |
All v1 fields are carried over byte-for-byte; the migration never drops or mutates existing data.
# Default: in-memory simulation — deterministic, no network, no deps.
# Proves the transform is lossless across all invoice statuses. Exits 0.
npx tsx scripts/migrate-v1-v2.ts
# Real testnet run (requires @stellar/stellar-sdk + funded keys):
npx tsx scripts/migrate-v1-v2.ts --testnetThe --simulate path is suitable for CI so a regression in the migration logic
fails the build before any on-chain run.
| Variable | Purpose |
|---|---|
ADMIN_SECRET |
Secret key of the contract admin (signs upgrade + migrate). |
SOROBAN_RPC_URL |
RPC endpoint (default https://soroban-testnet.stellar.org). |
NETWORK_PASSPHRASE |
Network passphrase (default Testnet). |
V1_WASM / V2_WASM |
Paths to the built v1 / v2 WASM artifacts. |
V2_WASM_HASH |
Hash passed to upgrade(new_wasm_hash). |
- Install + deploy v1 WASM;
initializethe contract. - Seed sample state:
submit_invoiceacross all statuses. - Snapshot v1 state (
get_invoice,get_invoice_count). - Install v2 WASM; call
upgrade(V2_WASM_HASH). - Invoke the v2
migrateentrypoint to re-writeInvoicerecords viamigrateInvoice(). - Verify: counts match the snapshot, new fields carry their defaults, and v2-only
calls (e.g.
get_invoice_count(&Some(status))) succeed.
The on-chain
migrateentrypoint applies exactly the transform validated by--simulate; keep the two in lockstep when the schema evolves further.
Document Prepared By: DevOps & Security Team
Last Updated: May 2024 (migration script added — Issue #114)
Next Review: Upon next upgrade