This document describes how to migrate all contract state from one deployed instance
to another using export_migration_snapshot and import_migration_batch.
The migration system works in two phases:
-
Export — call
export_migration_snapshoton the source contract.
This locks the source contract (blockscreate_remittanceandconfirm_payout) and returns aMigrationSnapshotcontaining all state plus a SHA-256 verification hash. -
Import — call
import_migration_batchon the destination contract one batch at a time.
Each batch is hash-verified before any data is written. After the final batch the destination contract is unlocked and ready for normal use.
- You must hold the Admin role on both the source and destination contracts.
- The destination contract must already be initialized (call
initializefirst). - Keep the source contract locked (do not call
unpauseor clear the migration flag manually) until the import is fully verified.
Deploy a new contract and call initialize with the same parameters as the source:
soroban contract invoke \
--id <DEST_CONTRACT_ID> \
-- initialize \
--admin <ADMIN_ADDRESS> \
--usdc_token <USDC_TOKEN_ADDRESS> \
--fee_bps 250 \
--rate_limit_cooldown 3600 \
--protocol_fee_bps 0 \
--treasury <TREASURY_ADDRESS>soroban contract invoke \
--id <SOURCE_CONTRACT_ID> \
-- export_migration_snapshot \
--caller <ADMIN_ADDRESS>Save the returned MigrationSnapshot JSON. The source contract is now locked —
create_remittance and confirm_payout will return MigrationInProgress (error 30).
Use the MigrationSnapshot.persistent_data.remittances array. Split it into chunks
of at most MAX_MIGRATION_BATCH_SIZE (100) items. For each chunk compute the
batch_hash using the same algorithm as compute_batch_hash in src/migration.rs:
SHA-256( batch_number_be32 || for each remittance { id_be64 || sender_xdr || agent_xdr || amount_be128 || fee_be128 || status_u8 || expiry_be64? } )
Call import_migration_batch for batch 0, then 1, then 2, … in order:
soroban contract invoke \
--id <DEST_CONTRACT_ID> \
-- import_migration_batch \
--caller <ADMIN_ADDRESS> \
--batch '{ "batch_number": 0, "total_batches": N, "remittances": [...], "batch_hash": "..." }'After the final batch (batch_number == total_batches - 1) the destination
contract automatically clears the MigrationInProgress flag and resumes normal
operations.
Query a sample of remittances on the destination contract and compare with the source:
soroban contract invoke --id <DEST_CONTRACT_ID> -- get_remittance --remittance_id 1
soroban contract invoke --id <DEST_CONTRACT_ID> -- get_remittance --remittance_id 2Also confirm the counters match:
soroban contract invoke --id <DEST_CONTRACT_ID> -- get_platform_fee_bpsUpdate your off-chain services (backend, API, frontend) to point to
<DEST_CONTRACT_ID>. The source contract remains locked as an audit record.
| Error | Code | Meaning |
|---|---|---|
MigrationInProgress |
30 | Export already called; or normal op blocked during migration |
InvalidMigrationHash |
29 | Batch hash mismatch — data was tampered or corrupted |
InvalidMigrationBatch |
31 | batch_number >= total_batches |
Unauthorized |
23 | Caller does not have Admin role |
- The
verification_hashinMigrationSnapshotcovers all instance and persistent data plus the timestamp and ledger sequence. Any tampering will causeInvalidMigrationHashon import. - Each
MigrationBatchcarries its ownbatch_hashverified independently. - The source contract stays locked until you explicitly clear the flag (or redeploy), preventing new state from being created after the snapshot was taken.