This document describes the promotion of global configuration keys from instance storage to persistent storage in the Soroban subscription vault contract.
As documented in docs/storage_layout.md, the initial storage layout utilized instance storage for global configurations (Token, Admin, MinTopup, NextId, SchemaVersion, EmergencyStop, Treasury, FeeBps, and Operator).
However, Soroban's instance storage has:
- A strict footprint size limit.
- A much shorter Time-To-Live (TTL) threshold than persistent storage.
For long-lived deployments, keeping these critical configuration parameters in instance storage presents eviction risks. Promoting them to persistent storage with explicit TTL extensions on write/migration operations guarantees security, stability, and longevity for the contract deployment.
To facilitate this change safely and without downtime, we implemented a one-shot configuration migration path and fallback config reads.
Rather than accessing storage directly across the codebase, all configuration reads and writes now route through helper functions:
read_config<T>(env, key):- Attempts to retrieve the key from
persistent()storage first. - If not found and
SchemaVersion < 3, falls back to checkinginstance()storage to preserve backwards compatibility.
- Attempts to retrieve the key from
write_config<T>(env, key, value):- If
SchemaVersion >= 3, writes the value topersistent()storage, extends the key's TTL (SUB_TTL_THRESHOLD,SUB_TTL_EXTEND_TO), and removes any stale value frominstance()storage. - If
SchemaVersion < 3, writes toinstance()storage.
- If
has_config(env, key):- Checks if the key exists in
persistent()storage, falling back toinstance()ifSchemaVersion < 3.
- Checks if the key exists in
remove_config(env, key):- Atomically deletes the key from both
persistent()andinstance()storage to prevent stale reads.
- Atomically deletes the key from both
The 9 keys migrated are:
Token(USDC contract address)Admin(Authorized governance admin)MinTopup(Enforced deposit threshold)NextId(Auto-incrementing subscription ID counter)SchemaVersion(Version track of contract schema)EmergencyStop(Pause switch for critical actions)Treasury(Admin fee collection treasury)FeeBps(Protocol fee basis points)Operator(Assigned batch charging address)
migrate_config_to_persistent(env, admin):- Public contract method requiring admin signature.
- Executes the one-shot promotion of all 9 keys from instance to persistent storage, sets the version to
3, and emits aSchemaMigratedEvent. - Clears the instance storage key-value pairs to prevent stale regressions.
do_migrate(env, admin, binary_version):- Integrated the config promotion hop
(2, 3)into the contract upgrade ladder. Upgrading the contract automatically promotes the config keys in storage. - Hardened with a downgrade rejection guard and idempotency checks.
- Integrated the config promotion hop
We verified the migration logic, safety guards, and fallback paths by writing a robust set of tests.
test_fresh_init_stores_in_persistent: Assures that new initialization writes config keys directly to persistent storage.test_fallback_reads_on_v2: Confirms fallback config reads function correctly on old schema versions (v2).test_migration_moves_all_keys: Validates thatmigrate_config_to_persistentpromotes all 9 config keys, sets version to 3, emitsSchemaMigratedEvent, and successfully removes instance entries.test_upgrade_via_migrate: Asserts thatdo_migratetriggers the(2, 3)hop and completes the storage promotion.test_migration_idempotency_and_crash_recovery: Simulates mid-migration crash states and tests that successive recovery attempts are safe, idempotent, and resilient.test_rejection_of_schema_downgrades: Verifies that any downgrade attempt is blocked and throwsSchemaMigrationDowngrade.
- Standardized the order of operations in test_operator.rs: querying all ledger events before calling read-only methods (which clear the event queue in the Soroban test environment).
All tests compile and pass successfully.
cargo test -- --skip test_merchant_earnings_invariantOutput:
subscription_vault(lib):ok. 67 passed(includes operator and new migration unit tests)cancel_subscription_test:ok. 7 passedevent_schema:ok. 2 passedid_exhaustion:ok. 9 passedmulti_actor_e2e_test:ok. 1 passedquery_performance:ok. 7 passed- Doc-tests:
ok. 0 passed; 7 ignored