fix(pair): UpdateLimitOrderPrice same-id relink jumps FIFO at the new price #1227
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#1227
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
ExecuteMsg::UpdateLimitOrderPricedetaches a resting limit and relinks it at the new price without minting a neworder_id. Equal-price FIFO on the pair book is the composite key(price, order_id)(DLL insert viabid_before/ask_before; match walks that list). A maker who quoted a different price earlier therefore keeps a globally older id, moves onto a level other makers already occupy, and sorts ahead of those makers.That is a price-time / FIFO violation at the destination level. Price priority across different prices is unchanged and must stay.
Closed #247 shipped price-only Edit →
UpdateLimitOrderPricewith an explicit AC of “sameorder_id” (gas: no cancel+replace, no second maker fee, no CW20). That identity choice is the mechanism. #247 did not specify time-priority at the new level.Closed #424 independently recorded the same fairness quirk (
relink_limit_order_pricekeeps the original id → leapfrog) and closed the audit as docs +Repricefuzz, not a matching-engine fix. This ticket is the matching-engine fix. Do not reopen #424. #42 QA FIFO (fifo_two_bids_same_price_older_filled_first) only covers two new placements at the same price, where id ≈ arrival-at-that-price.Not a pool drain: escrow
remainingandPENDING_ESCROW_*stay put on relink (#247 L1 / #424 reprice note). The defect is fill order among honest makers at one price.Current codebase
smartcontracts/packages/dex-common/src/pair.rs—ExecuteMsg::UpdateLimitOrderPrice { order_id, price, hint_after_order_id, max_adjust_steps, … }smartcontracts/contracts/pair/src/contract.rs—execute_update_limit_order_price: owner-only, pause/expiry/blacklist gates, thenorderbook::relink_limit_order_pricesmartcontracts/contracts/pair/src/orderbook.rs—relink_limit_order_priceunlinks and reinserts with the same id;find_insert_bid/find_insert_askplace by(price, order_id)smartcontracts/tests/src/limit_order_tests.rs—fifo_two_bids_same_price_older_filled_first(new places only); no reprice-then-match FIFO caseupdateLimitOrderPriceinfrontend-dapp/src/services/terraclassic/pair.ts— product path that exercises this msg on every price-only editdocs/limit-orders.md(Edit → update, same id);docs/contracts-security-audit.md(L12 id monotonicity, FIFO notes). #266 requires equal-price batch rungs to resolve ascending id — that rule is for one batch’s new ids, not for a later relink into someone else’s level.Expected vs actual
order_id. Insert at the new price uses that id, so a low id lands at the head of the equal-price run and matches before makers who have been on that price longer.Repro
Given a pair with two makers on the same side: maker A already resting at price
Pwithorder_id = A_id, and maker B resting at a different in-band price withorder_id = B_idwhereB_id < A_id(B placed globally earlier)When B’s owner executes
UpdateLimitOrderPricetoP(valid hint /max_adjust_steps; remaining and side unchanged)Then the book at
Pmust match A before B on the next hybrid/book take that walks from the head of that sideAnd B’s
remaining/PENDING_ESCROW_*are unchanged; no second maker fee; no CW20 on the update txAnd today, without a fix, B sorts before A at
PbecauseB_id < A_idConstraints / guardrails
LimitInsertStepsExceededstill all-or-nothing (order not half-detached).remainingorPENDING_ESCROW_*.ORDER_NEXT_IDonce, never reuse, and keepbatch_placement_order_ids_match_sequential_singlesgreen. If the fix keeps the id, do not punch holes in the id space.simulate_match_*/ indexerdb_orderbook_simmust see the same FIFO after relink as execute.order_idis kept, placements/book pages stay keyed the same; if id changes, update parse/upsert so the old row does not remain as a ghost live order.limitBookPage/limitPlacementsand do not leave the ticket pointed at a canceled id.UpdateLimitOrderPriceon one rung must not jump a level another maker already occupies.grok-high(below). Do not addreadyfrom intake.Relevant files
smartcontracts/contracts/pair/src/orderbook.rsrelink_limit_order_price,find_insert_bid/find_insert_ask, equal-price comparesmartcontracts/contracts/pair/src/contract.rsexecute_update_limit_order_pricesmartcontracts/packages/dex-common/src/pair.rsUpdateLimitOrderPricemsgsmartcontracts/tests/src/limit_order_tests.rsfifo_two_bids_same_price_older_filled_firstmust staydb_orderbook_simPif execute will not fill them first (or vice versa)frontend-dapp/src/services/terraclassic/pair.ts+ Edit ticketdocs/limit-orders.md,docs/contracts-security-audit.md,docs/integrators.mdRecommended direction
Prefer keep
order_idfor cancel/edit identity (preserves #247 UX) and change insert position at equal price for relink:relink_limit_order_price, after unlink, insert as a new arrival at that price: walk/place after every existing node with the same side + same price (tail of the equal-price run), not by comparing the preserved id. Cross-price ordering stays the existing bid/ask price sort.(price, sentinel_last)while storage key /ORDERSmap key stays the original id.order_idon reprice (next_order_id/ oneORDER_NEXT_IDwrite), leave the old id unused, emit attributes the indexer already understands for cancel+place identity. Heavier: indexer + frontend + L12 docs. Use this only if tail-insert cannot be made match-equivalent.Repriceop toprop_escrow_dllas #424 asked, asserting escrow + DLL integrity and equal-price FIFO after random reprices.Do not “fix” this by charging a second maker fee or by forcing cancel+place in the dApp while the chain msg still jumps. The chain is the source of truth; any owner can call the msg.
Acceptance criteria
P, a taker that fills one maker atPfills A, not B. Remaining on B unchanged until A is gone.order_id(fifo_two_bids_same_price_older_filled_firstand the ask twin stay green).new_price == old_pricedoes not move the order ahead of later same-price makers who were already behind it (no free bump).LimitInsertStepsExceeded, expired, pause, non-owner) leaves the order linked at the old price with the same remaining (atomicity).PENDING_ESCROW_*andorder.remainingunchanged on success. No CW20 messages on the update tx.simulate_match_*+ indexer sim agree with execute FIFO after relink.docs/limit-orders.mdstates time-priority at the quoted price (reprice joins the tail). Security matrix notes the #424 quirk is closed by this behavior, not merely documented as “by design.”cargo testfor pair orderbook /cl8y-dex-testslimit FIFO +make test-contracts(or the repo’s documented contract suite) pass.Test plan (functional)
P, B older id atP', B →P, take 1 makerP(no reprice)P; middle one reprices away and back toPhint_after_order_idpointing at A when B joinsPP; hint cannot invert FIFOsimulate_match_*on T1 bookTest plan (attack / hack / abuse)
P' → P → P' → Pto climb a levelPjoins tail; cannot grind to headhint_after_order_idon relinkmax_adjust_stepsmid-relinkMIN_LIMIT_PRICE/ band edgecost ≥ 1at the new priceu64::MAXrevert; no wrap to 0Verification criteria
limit_order_tests(or pairorderbooktests). Name them sofifo_*_after_update_limit_order_priceis grepable.prop_escrow_dllgains aRepriceop that never lets a repriced node sit before a same-price node that was already on that price.docs/limit-orders.mdfor reprice / FIFO-at-level.Out of scope
UpdateLimitOrderPrice).First-pass model recommendation
Recommendation: grok-high
Rationale: CosmWasm pair matching and order-book FIFO (founder-required: contracts / wasm). The change is a protocol time-priority rule in
relink_limit_order_priceplus insert/match/sim/indexer agreement, not a local helper. It collides with shipped #247 “keeporder_id” UX and with #266 composite-key insertion. Composer fails the contracts/wasm criterion and the “no cross-cutting state/protocol change” criterion even if the Rust edit is small. Verify with the FIFO-after-reprice tests above plus existing equal-price FIFO and escrow props — not a mainnet book walk.cl8y-agent-control: queued
implementjobe10fc52c-42fb-4d75-8b0e-154779c3a954(not executed; no Hetzner VM).Merged onto
origin/mainvia #1235. Pair + docs steps ofmake verify-issue-1227were 8/8. Relink joins the equal-price FIFO tail (RELINK_EQUAL_PRICE_SORT_ID); indexer snapshot storeswalk_index.Problems:
indexer/.envexists (VERIFY1227_INDEXERis optional; presence of.envalso enables it). Postgres timed out onresting_book_walk_index_preserves_reprice_fifo. That is host DB reachability, not a missing FIFO fix. Re-run with reachable Postgres, or without.envto skip.indexer/migrations/20260911120000_resting_orders_walk_index.sqlor livedb_orderbook_simcan disagree with execute after reprice.CI: Woodpecker did not post live statuses; merge used local gitleaks + Forgejo status.
Follow-up ops ticket: #1246. Woodpecker enablement: #1247.
columbus-5 wasm for this ticket is not live. Ops tracker: #1246.
Live pairs are 11639 / 1.16.0 (#712). LCD
HybridSimulationbelief_price: "0"still 200 (same output as omitted belief) — #1230 / #1227 / #1231 execute/query wasm still needs a 1.17.0 store+migrate (gitCONTRACT_VERSIONis still 1.15.0). Tax listed pin is 11630 (not 11611/11619); ALPHAterra1x6e64…is 1.0.0 and needs a tax cw2 bump + CMM migrate for #1228 / #1237.columbus-5 pair wasm is live: 11664 / cw2 1.17.0 (store
DB35943A4925059E770A63C9ADDF35622696088A4DFFC8A91110466034995961). Factorypair_code_id=11664,GetPairCount=20. LCD UST1/cUSTCHybridSimulationbelief_price:"0"→Invalid belief_price 0. Ops #1246.