fix: greedy quote=execute mutex, greedy_stop remainder, pool_spot Decimal panic (!480 leftover) #709
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#709
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
Must-fix leftovers from !1198 / #708 (review note on !1198). Three related pair/query bugs in the opt-in greedy book-first path:
HybridSimulationsilently prefersgreedywhen bothhybridandgreedyare set; execute rejects that payload.greedy_stop=emptyafter a real fill — book-then-pool remainder is labeledempty, which the enum documents as “no maker was filled.”Decimal::from_ratiopanic inpool_spot_net— extreme reserve ratios can panic (full gas) on greedy execute and query.Do not flip
hybrid: Noneto greedy (G1). Do not change Pattern C declared splits. Official dApp stays onGET /route/solve(G4).Related: #708. Sibling test coverage (pause / blacklist / AfterSwap L7 / community-tax extra-debit) is a separate issue.
Current codebase
All of this lives on
feat/708-greedy-book-first(!1198) until merge; line numbers are that branch.Quote vs execute mutex (G11)
resolve_swap_hybrid_modealready rejects both fields:Execute uses that helper:
execute_swap(~L972):resolve_swap_hybrid_mode(hybrid, greedy)→InvalidHybridParamson both.query_simulate_swap_operations(~L657–660): explicithybrid.is_some() && greedy.is_some()error.Pair query does not.
query_hybrid_simulation(~L2674–2679) dummy-outs hybrid whenever greedy is present:simulate_hybrid_swap_with_fee(~L2454–2460) then callsresolve_swap_hybrid_modewithhybrid: Noneifgreedy.is_some(), so the mutex never fires on LCDHybridSimulation. Same JSON can return a greedy quote and fail onCw20HookMsg::Swap.QueryMsg::HybridSimulationhas#[serde(default)]on bothhybridandgreedy(pair.rs~L568–575). Helpergreedy_simulation_undiscountedalready sendshybrid: None.greedy_stopafter remainderGreedyStopReason(~L130–141):as_attrWorseThanPoolworse_than_poolMaxMakersmax_maker_fillswith offer leftmax_makersScanCapMAX_SCAN_STEPSscan_capEmptyemptyFilledfilledgreedy_stop_after_walk(~L147–174) priority: worse → scan_cap → (offer_left==0 && makers>0) filled → makers==0 empty → makers>=cap max_makers → fallbackEmpty.That fallback is the common success path: greedy fills a better prefix, leftover offer goes to the AMM (G9). Test
greedy_better_bid_then_pool_remaindernever assertsgreedy_stop. Empty-book test correctly expectsempty.Pattern C omits the field via
skip_serializing_if(G14).pool_spot_netpanicG3 compares Decimal rates, not 1-raw-unit CP dumps (
greedy.rsheader +bid_beats_residual_pool/ask_beats_residual_pool).cosmwasm-std 1.5.x
Decimal::from_ratiopanics whennumerator * 10^18overflowsUint128(~output/input ≳ 3.4×10²⁰). Near-drained 18-dec pools can hit that. Pool-only swaps never call this. Greedy execute and query do (GreedyPoolReffrom currentRESERVESbefore the walk).constant_product_net_outin the same file already usesUint256+ceil_divbut has no callers (dead). Unpriceable makers (price == 0, no inverse) areSkip(L18 / L20), not a stop — overflow of the pool spot should follow the same fail-closed / skip policy, not a panic.Unit test
equal_is_not_strictly_bettercurrently assertsprice == 0→Skip, not equal-rate →No.Why this is needed
!1198 advertises G7 quote = execute, G11 mutex, wasm
greedy_stopfor indexer/debug, and a panic-free G3 compare. As shipped:hybrid+greedy) will size from a greedy number then revert on execute (or, if they strip one field inconsistently, fill a different path). That is A11 (query vs execute drift) and a G11 hole unique to the pair query (router already rejects).greedy_stop=emptyas “no book contact” will mis-attribute the common book-then-pool fill. The issue #708 design spike asked for stable stop reasons (worse_than_pool | max_makers | scan_cap | empty | filled).These block treating !1198 as merge-ready. They do not require flipping the TerraSwap default or a columbus-5 migrate by themselves (migrate remains #708 G14 ops).
Constraints / guardrails
hybrid: None+greedy: Nonestays pool-only. This ticket does not change the default.HybridSimulationwith a given(hybrid, greedy)must resolve with the sameresolve_swap_hybrid_modeas execute. Queries remain read-only (no parks).hybridandgreedyrejects on pair query, pair execute, router sim, and router execute. DistinctGreedySwapParamsJSON stays; do not overloadpool_input=0, book_input=offer.greedy_stopwhen unused. Existing Pattern C tests stay green. New stop-reason wire names must be snake_case and documented.Simulationfor greedy quotes.Skip, not stop. Pool-spot overflow must not panic; preferchecked_from_ratio→Skipor a typedContractError(pick one, test it).Cw20HookMsg::Swap. G13 gas maps stay as in !1198.GET /route/solve.Out of scope: owner opt-out default; pair wasm migrate; reverse greedy sim (unless you choose to error on reverse-sim
greedyinstead of silently quoting pool-only — that is a sibling, not required here); pause/blacklist/L7/tax tests (sibling issue).Relevant files
smartcontracts/contracts/pair/src/contract.rsexecute_swapmutex (~L972);simulate_hybrid_swap_with_feedummy-out (~L2454);query_hybrid_simulation(~L2663)smartcontracts/contracts/pair/src/greedy.rspool_spot_net,greedy_stop_after_walk, unit testssmartcontracts/packages/dex-common/src/pair.rsGreedyStopReason,resolve_swap_hybrid_mode,QueryMsg::HybridSimulationsmartcontracts/contracts/router/src/contract.rssmartcontracts/tests/src/limit_order_tests.rsmod greedy_book_first_708skills/AGENTS_GREEDY_BOOK_FIRST.mddocs/integrators.mdhybrid: null, greedy)docs/contracts-security-audit.mdscripts/qa/verify-issue-708.shverify-issue-<this>Recommended direction
Mutex on pair query (Fix 1)
In
query_hybrid_simulation(and/orsimulate_hybrid_swap_with_fee), pass bothhybridandgreedyintoresolve_swap_hybrid_mode. Delete the “if greedy then hybrid=None” dummy. Map theStringerror toInvalidHybridParams/StdError::generic_errthe same way execute does. Router forward sim already errors — do not weaken it.Stop reason for remainder (Fix 2)
Prefer a new variant (e.g.
RemainderToPool/ wireremainder_to_pool) whenmakers_used > 0, offer left, not worse/scan/cap. KeepEmptyonly whenmakers_used == 0. Updateas_attr, skill, integrators doc, andgreedy_better_bid_then_pool_remainderassert. Do not overloadempty.Alternative (weaker): map that case to
filled— rejected; filled means offer fully consumed on the book.Checked Decimal (Fix 3)
Replace
Decimal::from_ratioinpool_spot_net(and any fee_keep path that can overflow) withDecimal::checked_from_ratio. On overflow:Skip(walk continues, consistent with L18/L20) orContractErrorfail-closed. Pin one in the skill. Delete or useconstant_product_net_out(do not leave a misleading dead “1-unit dump” helper if unused). Add a unit test with a ratio that would panicfrom_ratio.Equal-rate unit test while touching
greedy.rs: bid/ask net equal to pool net →GreedyBeats::No(stop), notSkip.Acceptance criteria
HybridSimulationwith bothhybridandgreedyerrors (same family as execute / router). No greedy number returned.HybridSimulationwith onlygreedystill matches execute on a live non-expired book (existinggreedy_simulation_matches_executestays green; also assertgreedy_stop+limit_book_offer_consumedwhere useful).HybridSimulationwith onlyhybrid(Pattern C) unchanged;greedy_stopomitted (G14).HybridSimulationwith neither stays pool-only (G1).greedy_stop=empty,limit_book_offer_consumed=0.greedy_stopis notempty;makerswere filled; leftover went to AMM (G9).greedy_stop=filled.worse_than_pool(or empty-equivalent only if zero makers — pin: preferworse_than_poolwhen the walk stopped on G3).max_makers/scan_cappriority unchanged vs today’sstop_reason_prioritytest.output/input(overflowfrom_ratio) on greedy execute and query: no panic; Skip or typed error as designed; pool-only swap on the same reserves still works.No), not skip.make verify-issue-<iid>(or extendverify-issue-708) greps + unit/multitest names below.Test plan (functional paths)
dex-common / pair unit
resolve_swap_hybrid_mode(Some(h), Some(g))still errors (existingg11_both_hybrid_and_greedy_rejected).HybridSimulationResponse).greedy_stop_after_walk: makers>0, offer_left>0, not worse/scan/cap → new remainder reason, notEmpty.stop_reason_priorityupdated for the new variant.pool_spot_net/ beats: ratio that panicsfrom_ratio→Skipor error, no panic.GreedyBeats::No. Ask-side equal →No.Skip.cl8y-dex-tests
greedy_book_first_708HybridSimulation { hybrid: Some(declared), greedy: Some(g) }errors; execute of the same payload still errors.greedy_empty_book_rolls_to_poolstillgreedy_stop=empty.greedy_better_bid_then_pool_remainderasserts new stop reason +limit_book_offer_consumed > 0(andbook_return_amountif emitted).greedy_worse_or_equal_bid_does_not_fillstop reason unchanged.greedy_max_maker_fills_one_stops→max_makers.greedy_simulation_matches_executestill equal and samegreedy_stop.greedy_stop(existingpattern_c_sim_omits_greedy_stop).Router (keep aligned)
SimulateSwapOperationshop with both fields still errors (regression if not already covered — add if missing).Test plan (attack, hack, abuse)
{hybrid, greedy}on pair query errors. Attacker cannot obtain a greedy quote then execute Pattern C (or vice versa) from the same JSON. Docs: mismatched execute is still user-signed if they change fields.greedyon execute remains G1 pool-only (pre-existing). Not a contract bug; query must not hide the mutex.greedy_stop. New remainder value must not collide withempty. Pattern C still omits the field so old parsers do not see a bogus empty.Verification criteria
cd smartcontracts && cargo test -p dex-common greedy_swap -- --test-threads=1green.cd smartcontracts && cargo test -p cl8y-dex-pair greedy -- --test-threads=1green (includes overflow + equal-rate + remainder stop reason).cd smartcontracts && cargo test -p cl8y-dex-tests greedy_book_first_708 -- --test-threads=1green, including new both-fields query test and remaindergreedy_stopassert.greedy_stop != empty.Decimal::from_ratio-overflow ratio does not panic inpool_spot_net/ greedy execute.make verify-issue-<iid>(or extendedverify-issue-708) covers the greps + test names.resolve_swap_hybrid_modewith both fields.Refs: !1198 review, #708 G7 / G11 / A7 / A11,
skills/AGENTS_GREEDY_BOOK_FIRST.md.marked as related to #708
mentioned in commit
83bccc2838mentioned in merge request !1198
mentioned in commit
56e4c4f513mentioned in commit
f54de4fd4bmentioned in issue #712
marked as related to #712
mentioned in issue #708
!1198 merged to
main(f54de4fd). Merge-time verify:make verify-issue-709PASS (mutex,remainder_to_pool,checked_from_ratioSkip).All #709 acceptance items landed in
83bccc28and survived the main merge. No product hole found on the three must-fixes.Remaining should-fix (not this ticket): router reverse-sim ignores
greedy; G8 dummybook_input=1error string; G6 live same-side stale hint. Tracked on #712. Do not reopen this issue for those.mentioned in issue #718