fix(pair): migrate must backfill DISCOUNT_REGISTRY and ORACLE_STATE #1232
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#1232
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Pair
migratealready backfills later-addedItems (ORDER_NEXT_ID, escrow, limit configs,ASSET_CODE_IDS) when they are absent. It does not backfillDISCOUNT_REGISTRYorORACLE_STATE. Instantiation writes both. Runtime execute, simulation, and several queries then call hard.load()on those keys.A pair whose storage predates those items (older wasm, or a migrate test fixture that only seeds
PAIR_INFO/RESERVES) therefore migrates “successfully” (cw2version bump +action=migrate) and then bricks:Swap,ProvideLiquidity,WithdrawLiquidity) callsoracle_update→ORACLE_STATE.load.DISCOUNT_REGISTRY.load.ObserveandOracleInfoalsoORACLE_STATE.load.GetDiscountRegistryis the exception: it usesmay_loadand reportsNone. That hides the execute-path panic.PAUSEDalready usesmay_loadand is out of scope.This is not #535 / #536 / #538 (factory pointer snapshot and wiring addresses into existing listings). Those tickets assume the
DISCOUNT_REGISTRYkey exists. Do not auto-wire a registry address here.This is not #465 / #1224 / #1231 (TWAP arithmetic). Those paths assume
ORACLE_STATEalready exists.Factory already has the right pattern:
migrate_from_1_0_0_backfills_pair_addr_registered_and_pair_key_indexinsmartcontracts/contracts/factory/src/contract.rs. PairASSET_CODE_IDSbackfill (commit43173579, #582) is the local precedent that these two keys missed.Missing keys vs instantiate defaults
migratetoday.load()DISCOUNT_REGISTRYmsg.discount_registryorNoneORACLE_STATE{ cardinality: DEFAULT_OBSERVATION_CARDINALITY (360), index: 0, cardinality_initialized: 0 }oracle_update,IncreaseObservationCardinality,Observe,OracleInfoASSET_CODE_IDScw-storage-plusItem::loadisStdErrorwhen the key is absent (not a typedContractError). Callers use?, so the tx/query aborts.IncreaseObservationCardinalitycannot rescue a missing oracle item: it also.load()s first. There is no execute path that createsORACLE_STATEexcept instantiate (and this migrate, once fixed).SetDiscountRegistrycan create the discount key (factory-onlysave), but it does not run on migrate and does not create oracle state.Why the new implementation is needed
CreatePairis fine (instantiate writes them). Upgrade of old instances is not.may_loadinto silent wrong fees or a missing ring. The fix is to initialize storage inmigrate, matching instantiate, not to weaken every.load().DISCOUNT_REGISTRY→None(full pair fee, unwired). MissingORACLE_STATE→ empty ring with default cardinality; first lateroracle_updateseedsOBSERVATIONSthe same way instantiate+first swap does. Do not invent TWAP samples. Do not copy factoryconfig.discount_registry(F5 / #535: existing pairs are not retroactively wired).Some(registry)and a live observation ring, must keep both.Constraints / guardrails
ready.may_loadisNone. Never overwriteSome(Addr)or a writtenOracleState.OBSERVATIONSin migrate. Empty ring +cardinality_initialized: 0is correct;oracle_updatealready seeds the first slot whenmay_load(index)isNone.SetDiscountRegistry/ factory All/Batch from pair migrate. Unwired (None) is the documented post-migrate default for listings that never had the key (#535).GetDiscountRegistryJSON.CONTRACT_VERSION(today1.15.0insmartcontracts/contracts/pair/src/contract.rs) only if this repo’s wasm-admin convention requires a new cw2 version for the backfill to run on already-1.15.0 instances. If live instances are already 1.15.0 without these keys, a version bump is required forensure_from_older_versionto accept a second migrate. Confirm againstdocs/runbooks/wasm-admin-migration.md/ existing #582 pin flow — do not invent a mainnet schedule in this ticket.MigrateMsgstays empty unless a documented extra field is required. Prefer zero-arg migrate like currentpair::migrate.CreatePairchanges in this issue.Relevant files
smartcontracts/contracts/pair/src/contract.rs(migrate, instantiate oracle/discount saves,oracle_update, execute/sim.load()sites)may_load→saveblocks; keep existing backfillssmartcontracts/contracts/pair/src/state.rsDISCOUNT_REGISTRY,ORACLE_STATEItemkeyssmartcontracts/contracts/pair/src/limit_placement.rsDISCOUNT_REGISTRY.loadon placesmartcontracts/packages/dex-common/src/oracle.rsDEFAULT_OBSERVATION_CARDINALITY(360) — use the same default, do not forkoracle_overflow_tests/ factory migrate testsdocs/contracts-terraclassic.md/skills/AGENTS_FACTORY_DISCOUNT_REGISTRY.md(one sentence)None, not the factory pointerRecommended direction
In
pair::migrate, after the existingASSET_CODE_IDSbackfill:DISCOUNT_REGISTRY.may_loadisNone,save(&None).ORACLE_STATE.may_loadisNone,savethe instantiate default (cardinality: DEFAULT_OBSERVATION_CARDINALITY,index: 0,cardinality_initialized: 0).Unit test (mock storage, no chain): set
cw2to an older pair version, seedPAIR_INFO+RESERVES(and whateverensure_from_older_versionneeds), omit the two keys, callmigrate. Assert both keys exist with the defaults above. Thenoracle_updatewith non-zero reserves returnsOk.query_discount_registry/GetDiscountRegistryreturnsregistry: null. Hybrid simulate /DISCOUNT_REGISTRY.loadsucceeds.Idempotence: seed
DISCOUNT_REGISTRY = Some(addr)and a non-defaultOracleState(e.g.cardinality_initialized: 1,index: 3). Migrate. Both unchanged.Optional: one integration test that instantiates an old-layout fixture or runs migrate then
Swap/ProvideLiquidity/WithdrawLiquiditywithoutnot found/type: oracle_stateStdError. Do not require LocalTerra for AC.Acceptance criteria
migrateon storage that lacksdiscount_registrywritesDISCOUNT_REGISTRY = None. Pairs that already haveSome(addr)or explicitNoneare unchanged.migrateon storage that lacksoracle_statewrites{ cardinality: 360, index: 0, cardinality_initialized: 0 }(orDEFAULT_OBSERVATION_CARDINALITYif that constant changes in the same PR). ExistingOracleStateandOBSERVATIONSare not rewritten.oracle_update/Swap/ProvideLiquidity/WithdrawLiquidity/ hybrid simulate / reverse-sim / limit place do not fail with missing-keyStdErroron those two items. First oracle write may seed observation slot 0.GetDiscountRegistrystill returns storedOption<Addr>. Unwired after migrate remainsNone(fullfee_bps). FactorySetDiscountRegistryremains the only wiring path.ORDER_NEXT_ID, escrow, limit configs,ASSET_CODE_IDS) stay.make test-contracts(orcargo test -p cl8y-dex-pair+ documented pair integration) passes.#465oracle_overflow_testsstay green.Test plan (functional paths)
PAIR_INFO+RESERVES, no discount/oracle keys →migrateaction=migrateGetDiscountRegistryregistry: nullOracleInfooracle_updatethenObserveOk; first observation seeded; no missing-key errorDISCOUNT_REGISTRY.loadsucceeds; full feeDISCOUNT_REGISTRY = Some(factory-set addr)→migrateOracleState+ observation atindex→migratemigrateafter T1 (if cw2 bump allows)None+ empty ring#582ASSET_CODE_IDSbackfill still runs when that key is also missingTest plan (attack, hack, and abuse)
NoneSome(registry)withNonecardinality/indexon a live ringIncreaseObservationCardinalityon missing key).loadStdError swallowed into wrong fee (zero bps)fee_bpsonly afterNonesaveGetDiscountRegistrymay_loadused as proof execute is safe.load(), not only the queryVerification criteria
cargo test -p cl8y-dex-pair migrate(name the modulemigrate_backfill_testsor similar).cargo test -p cl8y-dex-pair oracle_overflow_testsstill passes.make test-contractsor the repo’s documented contract suite.Out of scope
CreatePairinherit (#536 / #538, closed).from_ratio/price_times_dtoverflow (#465, #1224, #1231).GetDiscountRegistryfrommay_loadtoload(optional later hardening after this backfill; not required).First-pass model recommendation
Recommendation: grok-high
Rationale: CosmWasm pair
migratewrites persisted contract state. Founder-required surface (contracts, wasm, migrate). Cross-cutting: twoItems used from execute, limit place, hybrid simulate, and oracle queries. Composer is disallowed for wasm/migrations even if the production edit is a fewmay_load/savelines plus tests. Verify with the missing-key fixture and idempotence tests above, plus existing#465oracle overflow tests — not a live-chain migrate.Related (do not retarget): #535, #536, #538, #582 (ASSET_CODE_IDS backfill precedent), #465, #1224.
Verification on origin/main at
54c4868e: #1232 remains open and cannot close. pair::migrate backfills order, escrow, limit configuration, and asset-code items, but omits missing DISCOUNT_REGISTRY and ORACLE_STATE; current hard .load() paths can still fail on an older layout. Existing migration_tests::pair_migration_preserves_fee_registry_lp_admin_and_limit_book passes, but covers populated-state preservation only, not missing keys.Remaining before closure:
Related: #1324 is the ops migration and explicitly excludes this fix; #582 is the prior asset-key backfill precedent. #535/#536/#538 cover registry wiring, while #465/#1224/#1231 cover oracle arithmetic/ratio behavior. No newer issue tracks the #1232 backfill verification.