Last updated: 2026-08-05
The corridor aggregate changed shape. This is the runbook for executing that migration against a deployed contract, and the compatibility guarantee for anyone already reading v1.
The guarantee first, because it is the question most readers have:
v1 keys are never deleted. migrate_corridor writes a new key and leaves the
old one untouched, so a contract or script reading get_corridor_aggregate keeps
working indefinitely, before and after migration, with no cutover window and
nothing to coordinate. There is no deprecation date on the v1 read path.
| Version | Data key | Tuple |
|---|---|---|
| v1 | DataKey::Corridor(anchor, corr) |
(fill_rate_bps, slippage_bps, settle_seconds_p50, n) |
| v2 | DataKey::CorridorV2(anchor, corr) |
(fill_rate_bps, slippage_bps, **composite_bps**, settle_seconds_p50, n) |
The only addition is composite_bps, and it is derived, not new data:
migrate_corridor recomputes it from the v1 metrics via
score::compute_composite_bps(fill_rate_bps, slippage_bps, settle_seconds_p50).
Nothing is read from off-chain and nothing is lost.
Read entrypoints, all of which coexist:
| v1 | v2 |
|---|---|
get_corridor_aggregate(...) |
get_corridor_aggregate_v2(...) |
get_score_for_corridor(...) |
get_score_for_corridor_v2(...) |
The TypeScript readers in lib/oracle/read.ts call the v2 entrypoint and fall
back to v1 on error, so the app works against a contract in either state.
Admin-gated. migrate_corridor and migrate_all both call
admin::require_admin, which calls require_auth on the admin address. If the
admin is an M-of-N multisig account (see GOVERNANCE.md), the
threshold is enforced by the host — the contract needs no multisig awareness.
Idempotent. The first thing migrate_corridor_unchecked does is
if env.storage().persistent().has(&v2_key) { return; }. Re-running is a no-op,
so a migration interrupted by a resource limit can be re-invoked safely and only
the unmigrated pairs do work.
Safe on missing data. A pair with no v1 metrics migrates to
(0, 0, composite_of_zeros, 0, 0) rather than failing — get
unwrap_ors a zero tuple. This means migrate_all over a fleet where only some
anchors have history does not need the caller to know which.
Not atomic across pairs. migrate_all loops; there is no transaction
boundary around the set. A run that exhausts its resource budget leaves some
pairs migrated and some not. That state is legal, not corrupt: v1 readers
are unaffected, v2 readers see the pairs that landed, and re-running finishes
the rest.
migrate_all walks the registered anchors × a hardcoded corridor list in
contracts/reputation/src/migration.rs:
const V2_CORRIDORS: &[&str] = &[
"usdc-ngn", "usdc-kes", "usdc-mxn", "usdc-php", "usdc-brl", "usdc-ars",
];Adding a corridor to the app does not add it here. This constant is compiled
into the contract, so extending it requires a contract change and a redeploy or
upgrade. Until that happens, a new corridor's aggregate is reachable via
migrate_corridor per pair, which takes the corridor as an argument and is not
bound by the list.
If you add a corridor to constants/corridors.ts, add it here in the same PR or
the fleet migration will silently skip it.
-
Confirm the deployed contract actually has the v2 entrypoints:
npx tsx scripts/verify-oracle-read.mts
The script prints
Contract versionand warns when the deployed bytecode predates the current source. If it warns, stop — migrating a contract withoutmigrate_all_v2compiled in will simply fail, and the deployment needs refreshing first. -
Note the current admin:
stellar contract invoke --id <CONTRACT_ID> -- admin
-
Record the pre-migration read for at least one pair, so the post-check is a comparison rather than an assertion of plausibility:
stellar contract invoke --id <CONTRACT_ID> \ -- get_corridor_aggregate --anchor_id cowrie --corridor usdc-ngn
Whole fleet, one invocation:
stellar contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
-- migrate_all_v2 \
--caller <ADMIN_ADDRESS>If the admin is a multisig account, compose and co-sign the transaction the same
way as accept_admin — see GOVERNANCE.md § "Accept with the
multisig account".
One pair at a time, for a corridor outside V2_CORRIDORS or to bound resource
usage:
stellar contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
-- migrate_corridor_v2 \
--caller <ADMIN_ADDRESS> \
--anchor_id cowrie \
--corridor usdc-ngn-
Read both paths for the pair recorded above and confirm the first four fields match, with
composite_bpsinserted third:stellar contract invoke --id <CONTRACT_ID> \ -- get_corridor_aggregate --anchor_id cowrie --corridor usdc-ngn stellar contract invoke --id <CONTRACT_ID> \ -- get_corridor_aggregate_v2 --anchor_id cowrie --corridor usdc-ngn
-
Confirm the v1 path still answers — that is the compatibility guarantee, and it should be verified rather than assumed.
-
Re-run
migrate_all_v2. It must be a no-op. If anything changes on the second run, the idempotency guard is not doing its job and that is a bug worth stopping for.
Re-run the same invocation. Already-migrated pairs short-circuit on the has
check, so the retry costs only the remainder. There is no rollback and none is
needed: v1 keys were never touched.
A compatibility shim, and a cutover window. Both were in the original scope for #872 and both are omitted.
The premise for them was "third-party contracts already reading v1." As of
2026-08-05 that set is empty and structurally so: the deployment is testnet-only
(CCZ54NTE…), its anchor registry is empty, and no corridor rate has ever been
published — publish_corridor_rate exists on the contract but nothing in the
publisher calls it. Designing a shim against zero readers is speculative work,
and the property that actually protects a future reader — v1 keys are never
deleted — already holds and is now written down.
If a real third-party reader appears and a v1 removal is ever contemplated, that
is a new decision and belongs in VERSIONING.md's deprecation
process, not here.
ORACLE_SPEC.md— the contract interfaceGOVERNANCE.md— admin custody and the multisig runbookVERSIONING.md— the deprecation policy a future v1 removal would followcontracts/reputation/src/migration.rs— the implementationcontracts/reputation/tests/corridor.rs— migration tests