fix: greedy quote=execute mutex, greedy_stop remainder, pool_spot Decimal panic (!480 leftover) #709

Closed
opened 2026-08-30 10:20:07 +00:00 by PlasticDigits · 10 comments
PlasticDigits commented 2026-08-30 10:20:07 +00:00 (Migrated from gitlab.com)

Summary

Must-fix leftovers from !1198 / #708 (review note on !1198). Three related pair/query bugs in the opt-in greedy book-first path:

  1. Quote ≠ execute (G11 / G7 / A11) — pair HybridSimulation silently prefers greedy when both hybrid and greedy are set; execute rejects that payload.
  2. greedy_stop=empty after a real fill — book-then-pool remainder is labeled empty, which the enum documents as “no maker was filled.”
  3. Decimal::from_ratio panic in pool_spot_net — extreme reserve ratios can panic (full gas) on greedy execute and query.

Do not flip hybrid: None to greedy (G1). Do not change Pattern C declared splits. Official dApp stays on GET /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_mode already rejects both fields:

(Some(_), Some(_)) → Err("cannot set both hybrid and greedy; …")
(None, None) → PoolOnly
(None, Some(g)) → Greedy (max_maker_fills == 0 rejects; oversize clamps to 100)
(Some(h), None) → Declared

Execute uses that helper:

  • Pair execute_swap (~L972): resolve_swap_hybrid_mode(hybrid, greedy) → InvalidHybridParams on both.
  • Router execute / query_simulate_swap_operations (~L657–660): explicit hybrid.is_some() && greedy.is_some() error.

Pair query does not. query_hybrid_simulation (~L2674–2679) dummy-outs hybrid whenever greedy is present:

(_, Some(_)) => (&dummy, greedy.as_ref())   // dummy = pool_only_hybrid_params(offer)

simulate_hybrid_swap_with_fee (~L2454–2460) then calls resolve_swap_hybrid_mode with hybrid: None if greedy.is_some(), so the mutex never fires on LCD HybridSimulation. Same JSON can return a greedy quote and fail on Cw20HookMsg::Swap.

QueryMsg::HybridSimulation has #[serde(default)] on both hybrid and greedy (pair.rs ~L568–575). Helper greedy_simulation_undiscounted already sends hybrid: None.

greedy_stop after remainder

GreedyStopReason (~L130–141):

Variant Documented meaning Wire as_attr
WorseThanPool Next live maker does not strictly beat pool worse_than_pool
MaxMakers Hit max_maker_fills with offer left max_makers
ScanCap Hit MAX_SCAN_STEPS scan_cap
Empty No live same-side maker was filled empty
Filled Entire offer consumed on the book filled

greedy_stop_after_walk (~L147–174) priority: worse → scan_cap → (offer_left==0 && makers>0) filled → makers==0 empty → makers>=cap max_makers → fallback Empty.

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_remainder never asserts greedy_stop. Empty-book test correctly expects empty.

Pattern C omits the field via skip_serializing_if (G14).

pool_spot_net panic

G3 compares Decimal rates, not 1-raw-unit CP dumps (greedy.rs header + bid_beats_residual_pool / ask_beats_residual_pool).

pool_spot_net:
  if input_reserve==0 || output_reserve==0 → None (Skip)
  spot = Decimal::from_ratio(output_reserve, input_reserve)  // PANIC on overflow
  * (1 − pool_fee_bps/10000)

cosmwasm-std 1.5.x Decimal::from_ratio panics when numerator * 10^18 overflows Uint128 (~output/input ≳ 3.4×10²⁰). Near-drained 18-dec pools can hit that. Pool-only swaps never call this. Greedy execute and query do (GreedyPoolRef from current RESERVES before the walk).

constant_product_net_out in the same file already uses Uint256 + ceil_div but has no callers (dead). Unpriceable makers (price == 0, no inverse) are Skip (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_better currently asserts price == 0 → Skip, not equal-rate → No.


Why this is needed

!1198 advertises G7 quote = execute, G11 mutex, wasm greedy_stop for indexer/debug, and a panic-free G3 compare. As shipped:

  • A bot that copies a confused LCD payload (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).
  • Integrators/indexers that treat greedy_stop=empty as “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).
  • An unbalanced pool can panic greedy execute/query (full gas) while pool-only still works. That is an opt-in DoS / grief, not fund theft, but it violates “oversize clamps, do not panic” (G5 spirit) and A7 (overflow must skip or error, not abort the VM).

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

ID Rule
G1 hybrid: None + greedy: None stays pool-only. This ticket does not change the default.
G7 HybridSimulation with a given (hybrid, greedy) must resolve with the same resolve_swap_hybrid_mode as execute. Queries remain read-only (no parks).
G11 Setting both hybrid and greedy rejects on pair query, pair execute, router sim, and router execute. Distinct GreedySwapParams JSON stays; do not overload pool_input=0, book_input=offer.
G14 Pattern C JSON still omits greedy_stop when unused. Existing Pattern C tests stay green. New stop-reason wire names must be snake_case and documented.
L8 Do not resurrect legacy Simulation for greedy quotes.
L17 / G6 Hint fallback unchanged (missing / wrong-side / missing-id → head). Not in scope unless a test needs it.
L18 / L20 Unpriceable makers stay Skip, not stop. Pool-spot overflow must not panic; prefer checked_from_ratio → Skip or a typed ContractError (pick one, test it).
No new hook string Still Cw20HookMsg::Swap. G13 gas maps stay as in !1198.
No solver / dApp switch Official UI stays GET /route/solve.

Out of scope: owner opt-out default; pair wasm migrate; reverse greedy sim (unless you choose to error on reverse-sim greedy instead of silently quoting pool-only — that is a sibling, not required here); pause/blacklist/L7/tax tests (sibling issue).


Relevant files

Path Role
smartcontracts/contracts/pair/src/contract.rs execute_swap mutex (~L972); simulate_hybrid_swap_with_fee dummy-out (~L2454); query_hybrid_simulation (~L2663)
smartcontracts/contracts/pair/src/greedy.rs pool_spot_net, greedy_stop_after_walk, unit tests
smartcontracts/packages/dex-common/src/pair.rs GreedyStopReason, resolve_swap_hybrid_mode, QueryMsg::HybridSimulation
smartcontracts/contracts/router/src/contract.rs Already rejects both on forward sim (~L657); keep aligned
smartcontracts/tests/src/limit_order_tests.rs mod greedy_book_first_708
skills/AGENTS_GREEDY_BOOK_FIRST.md G7 / G11 / stop-reason pin
docs/integrators.md Greedy quote JSON (hybrid: null, greedy)
docs/contracts-security-audit.md G1–G14 / A11 row
scripts/qa/verify-issue-708.sh Extend or add verify-issue-<this>

  1. Mutex on pair query (Fix 1)
    In query_hybrid_simulation (and/or simulate_hybrid_swap_with_fee), pass both hybrid and greedy into resolve_swap_hybrid_mode. Delete the “if greedy then hybrid=None” dummy. Map the String error to InvalidHybridParams / StdError::generic_err the same way execute does. Router forward sim already errors — do not weaken it.

  2. Stop reason for remainder (Fix 2)
    Prefer a new variant (e.g. RemainderToPool / wire remainder_to_pool) when makers_used > 0, offer left, not worse/scan/cap. Keep Empty only when makers_used == 0. Update as_attr, skill, integrators doc, and greedy_better_bid_then_pool_remainder assert. Do not overload empty.

    Alternative (weaker): map that case to filled — rejected; filled means offer fully consumed on the book.

  3. Checked Decimal (Fix 3)
    Replace Decimal::from_ratio in pool_spot_net (and any fee_keep path that can overflow) with Decimal::checked_from_ratio. On overflow: Skip (walk continues, consistent with L18/L20) or ContractError fail-closed. Pin one in the skill. Delete or use constant_product_net_out (do not leave a misleading dead “1-unit dump” helper if unused). Add a unit test with a ratio that would panic from_ratio.

  4. Equal-rate unit test while touching greedy.rs: bid/ask net equal to pool net → GreedyBeats::No (stop), not Skip.


Acceptance criteria

  • Pair HybridSimulation with both hybrid and greedy errors (same family as execute / router). No greedy number returned.
  • Pair HybridSimulation with only greedy still matches execute on a live non-expired book (existing greedy_simulation_matches_execute stays green; also assert greedy_stop + limit_book_offer_consumed where useful).
  • Pair HybridSimulation with only hybrid (Pattern C) unchanged; greedy_stop omitted (G14).
  • Pair HybridSimulation with neither stays pool-only (G1).
  • Execute still rejects both; router sim/execute still reject both.
  • Empty book greedy execute/query: greedy_stop=empty, limit_book_offer_consumed=0.
  • Better book + remainder to pool: greedy_stop is not empty; makers were filled; leftover went to AMM (G9).
  • Full book consume: greedy_stop=filled.
  • Worse-than-pool with zero fills: worse_than_pool (or empty-equivalent only if zero makers — pin: prefer worse_than_pool when the walk stopped on G3).
  • max_makers / scan_cap priority unchanged vs today’s stop_reason_priority test.
  • Extreme output/input (overflow from_ratio) on greedy execute and query: no panic; Skip or typed error as designed; pool-only swap on the same reserves still works.
  • Equal Decimal rates → stop (No), not skip.
  • Docs/skill: G11 applies to query; new stop-reason wire name; A11/A7 notes.
  • make verify-issue-<iid> (or extend verify-issue-708) greps + unit/multitest names below.

Test plan (functional paths)

dex-common / pair unit

  1. resolve_swap_hybrid_mode(Some(h), Some(g)) still errors (existing g11_both_hybrid_and_greedy_rejected).
  2. New: pair query both fields → error (not a HybridSimulationResponse).
  3. Query greedy-only / hybrid-only / neither — regression.
  4. greedy_stop_after_walk: makers>0, offer_left>0, not worse/scan/cap → new remainder reason, not Empty.
  5. Existing stop_reason_priority updated for the new variant.
  6. pool_spot_net / beats: ratio that panics from_ratio → Skip or error, no panic.
  7. Equal bid net == pool net → GreedyBeats::No. Ask-side equal → No.
  8. Zero reserves still Skip.

cl8y-dex-tests greedy_book_first_708

  1. LCD-style HybridSimulation { hybrid: Some(declared), greedy: Some(g) } errors; execute of the same payload still errors.
  2. greedy_empty_book_rolls_to_pool still greedy_stop=empty.
  3. greedy_better_bid_then_pool_remainder asserts new stop reason + limit_book_offer_consumed > 0 (and book_return_amount if emitted).
  4. greedy_worse_or_equal_bid_does_not_fill stop reason unchanged.
  5. greedy_max_maker_fills_one_stops → max_makers.
  6. greedy_simulation_matches_execute still equal and same greedy_stop.
  7. Pattern C sim omits greedy_stop (existing pattern_c_sim_omits_greedy_stop).

Router (keep aligned)

  1. SimulateSwapOperations hop with both fields still errors (regression if not already covered — add if missing).

Test plan (attack, hack, abuse)

Vector Expect
A10 / A11 confused payload {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.
A11 sim greedy / exec pool-only Distinct msgs; omitted greedy on execute remains G1 pool-only (pre-existing). Not a contract bug; query must not hide the mutex.
A7 overflow pool spot No VM panic. Skip maker-compare or fail with typed error. Remainder/pool-only path still available. Do not treat overflow as “beats pool” (that would drain worse makers).
A7 overflow maker price Unchanged L20 skip.
Stop-reason spoof / indexer Only the contract emits greedy_stop. New remainder value must not collide with empty. Pattern C still omits the field so old parsers do not see a bogus empty.
DoS via tiny reserve Greedy on a 1-wei vs huge-reserve pool must not panic the pair for other users’ pool-only txs.
A8 max_maker_fills Unchanged 0-reject / 100-clamp; not this ticket unless tests regress.

Verification criteria

  • cd smartcontracts && cargo test -p dex-common greedy_swap -- --test-threads=1 green.
  • cd smartcontracts && cargo test -p cl8y-dex-pair greedy -- --test-threads=1 green (includes overflow + equal-rate + remainder stop reason).
  • cd smartcontracts && cargo test -p cl8y-dex-tests greedy_book_first_708 -- --test-threads=1 green, including new both-fields query test and remainder greedy_stop assert.
  • Explicit: pair query both fields errors; execute both fields still errors.
  • Explicit: better-bid remainder greedy_stop != empty.
  • Explicit: Decimal::from_ratio-overflow ratio does not panic in pool_spot_net / greedy execute.
  • make verify-issue-<iid> (or extended verify-issue-708) covers the greps + test names.
  • Skill G7/G11 state that pair query uses resolve_swap_hybrid_mode with both fields.

Refs: !1198 review, #708 G7 / G11 / A7 / A11, skills/AGENTS_GREEDY_BOOK_FIRST.md.

## Summary Must-fix leftovers from [!1198](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/480) / [#708](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/708) (review note on !1198). Three related pair/query bugs in the **opt-in greedy book-first** path: 1. **Quote ≠ execute (G11 / G7 / A11)** — pair `HybridSimulation` silently prefers `greedy` when both `hybrid` and `greedy` are set; execute rejects that payload. 2. **`greedy_stop=empty` after a real fill** — book-then-pool remainder is labeled `empty`, which the enum documents as “no maker was filled.” 3. **`Decimal::from_ratio` panic in `pool_spot_net`** — extreme reserve ratios can panic (full gas) on greedy execute and query. Do **not** flip `hybrid: None` to greedy (**G1**). Do **not** change Pattern C declared splits. Official dApp stays on `GET /route/solve` (**G4**). Related: [#708](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/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_mode`](smartcontracts/packages/dex-common/src/pair.rs) already rejects both fields: ```text (Some(_), Some(_)) → Err("cannot set both hybrid and greedy; …") (None, None) → PoolOnly (None, Some(g)) → Greedy (max_maker_fills == 0 rejects; oversize clamps to 100) (Some(h), None) → Declared ``` **Execute** uses that helper: - Pair `execute_swap` (~L972): `resolve_swap_hybrid_mode(hybrid, greedy)` → `InvalidHybridParams` on both. - Router execute / `query_simulate_swap_operations` (~L657–660): explicit `hybrid.is_some() && greedy.is_some()` error. **Pair query does not.** `query_hybrid_simulation` (~L2674–2679) dummy-outs hybrid whenever greedy is present: ```text (_, Some(_)) => (&dummy, greedy.as_ref()) // dummy = pool_only_hybrid_params(offer) ``` `simulate_hybrid_swap_with_fee` (~L2454–2460) then calls `resolve_swap_hybrid_mode` with `hybrid: None` if `greedy.is_some()`, so the mutex never fires on LCD `HybridSimulation`. Same JSON can return a **greedy** quote and fail on `Cw20HookMsg::Swap`. `QueryMsg::HybridSimulation` has `#[serde(default)]` on both `hybrid` and `greedy` ([`pair.rs`](smartcontracts/packages/dex-common/src/pair.rs) ~L568–575). Helper `greedy_simulation_undiscounted` already sends `hybrid: None`. ### `greedy_stop` after remainder [`GreedyStopReason`](smartcontracts/packages/dex-common/src/pair.rs) (~L130–141): | Variant | Documented meaning | Wire `as_attr` | |---------|--------------------|----------------| | `WorseThanPool` | Next live maker does not strictly beat pool | `worse_than_pool` | | `MaxMakers` | Hit `max_maker_fills` with offer left | `max_makers` | | `ScanCap` | Hit `MAX_SCAN_STEPS` | `scan_cap` | | `Empty` | **No live same-side maker was filled** | `empty` | | `Filled` | Entire offer consumed on the book | `filled` | [`greedy_stop_after_walk`](smartcontracts/contracts/pair/src/greedy.rs) (~L147–174) priority: worse → scan_cap → (offer_left==0 && makers>0) filled → makers==0 empty → makers>=cap max_makers → **fallback `Empty`**. 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_remainder` never asserts `greedy_stop`. Empty-book test correctly expects `empty`. Pattern C omits the field via `skip_serializing_if` (**G14**). ### `pool_spot_net` panic G3 compares **Decimal rates**, not 1-raw-unit CP dumps ([`greedy.rs`](smartcontracts/contracts/pair/src/greedy.rs) header + `bid_beats_residual_pool` / `ask_beats_residual_pool`). ```text pool_spot_net: if input_reserve==0 || output_reserve==0 → None (Skip) spot = Decimal::from_ratio(output_reserve, input_reserve) // PANIC on overflow * (1 − pool_fee_bps/10000) ``` cosmwasm-std **1.5.x** `Decimal::from_ratio` panics when `numerator * 10^18` overflows `Uint128` (~output/input ≳ 3.4×10²⁰). Near-drained 18-dec pools can hit that. Pool-only swaps never call this. Greedy execute **and** query do (`GreedyPoolRef` from current `RESERVES` before the walk). `constant_product_net_out` in the same file already uses `Uint256` + `ceil_div` but has **no callers** (dead). Unpriceable makers (`price == 0`, no inverse) are `Skip` (**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_better` currently asserts `price == 0` → `Skip`, **not** equal-rate → `No`. --- ## Why this is needed !1198 advertises **G7 quote = execute**, **G11 mutex**, wasm `greedy_stop` for indexer/debug, and a panic-free G3 compare. As shipped: - A bot that copies a confused LCD payload (`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). - Integrators/indexers that treat `greedy_stop=empty` as “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`). - An unbalanced pool can **panic greedy execute/query** (full gas) while pool-only still works. That is an opt-in DoS / grief, not fund theft, but it violates “oversize clamps, do not panic” (**G5** spirit) and **A7** (overflow must skip or error, not abort the VM). 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 | ID | Rule | |----|------| | **G1** | `hybrid: None` + `greedy: None` stays pool-only. This ticket does **not** change the default. | | **G7** | `HybridSimulation` with a given `(hybrid, greedy)` must resolve with the **same** `resolve_swap_hybrid_mode` as execute. Queries remain read-only (no parks). | | **G11** | Setting **both** `hybrid` and `greedy` **rejects** on pair query, pair execute, router sim, and router execute. Distinct `GreedySwapParams` JSON stays; do **not** overload `pool_input=0, book_input=offer`. | | **G14** | Pattern C JSON still **omits** `greedy_stop` when unused. Existing Pattern C tests stay green. New stop-reason wire names must be snake_case and documented. | | **L8** | Do not resurrect legacy `Simulation` for greedy quotes. | | **L17 / G6** | Hint fallback unchanged (missing / wrong-side / missing-id → head). Not in scope unless a test needs it. | | **L18 / L20** | Unpriceable makers stay `Skip`, not stop. Pool-spot overflow must **not** panic; prefer `checked_from_ratio` → `Skip` or a typed `ContractError` (pick one, test it). | | **No new hook string** | Still `Cw20HookMsg::Swap`. G13 gas maps stay as in !1198. | | **No solver / dApp switch** | Official UI stays `GET /route/solve`. | **Out of scope:** owner opt-out default; pair wasm migrate; reverse greedy sim (unless you choose to **error** on reverse-sim `greedy` instead of silently quoting pool-only — that is a sibling, not required here); pause/blacklist/L7/tax tests (sibling issue). --- ## Relevant files | Path | Role | |------|------| | [`smartcontracts/contracts/pair/src/contract.rs`](smartcontracts/contracts/pair/src/contract.rs) | `execute_swap` mutex (~L972); `simulate_hybrid_swap_with_fee` dummy-out (~L2454); `query_hybrid_simulation` (~L2663) | | [`smartcontracts/contracts/pair/src/greedy.rs`](smartcontracts/contracts/pair/src/greedy.rs) | `pool_spot_net`, `greedy_stop_after_walk`, unit tests | | [`smartcontracts/packages/dex-common/src/pair.rs`](smartcontracts/packages/dex-common/src/pair.rs) | `GreedyStopReason`, `resolve_swap_hybrid_mode`, `QueryMsg::HybridSimulation` | | [`smartcontracts/contracts/router/src/contract.rs`](smartcontracts/contracts/router/src/contract.rs) | Already rejects both on forward sim (~L657); keep aligned | | [`smartcontracts/tests/src/limit_order_tests.rs`](smartcontracts/tests/src/limit_order_tests.rs) | `mod greedy_book_first_708` | | [`skills/AGENTS_GREEDY_BOOK_FIRST.md`](skills/AGENTS_GREEDY_BOOK_FIRST.md) | G7 / G11 / stop-reason pin | | [`docs/integrators.md`](docs/integrators.md) | Greedy quote JSON (`hybrid: null, greedy`) | | [`docs/contracts-security-audit.md`](docs/contracts-security-audit.md) | G1–G14 / A11 row | | [`scripts/qa/verify-issue-708.sh`](scripts/qa/verify-issue-708.sh) | Extend or add `verify-issue-<this>` | --- ## Recommended direction 1. **Mutex on pair query (Fix 1)** In `query_hybrid_simulation` (and/or `simulate_hybrid_swap_with_fee`), pass **both** `hybrid` and `greedy` into `resolve_swap_hybrid_mode`. Delete the “if greedy then hybrid=None” dummy. Map the `String` error to `InvalidHybridParams` / `StdError::generic_err` the same way execute does. Router forward sim already errors — do not weaken it. 2. **Stop reason for remainder (Fix 2)** Prefer a **new** variant (e.g. `RemainderToPool` / wire `remainder_to_pool`) when `makers_used > 0`, offer left, not worse/scan/cap. Keep `Empty` only when `makers_used == 0`. Update `as_attr`, skill, integrators doc, and `greedy_better_bid_then_pool_remainder` assert. Do **not** overload `empty`. Alternative (weaker): map that case to `filled` — rejected; filled means offer fully consumed on the book. 3. **Checked Decimal (Fix 3)** Replace `Decimal::from_ratio` in `pool_spot_net` (and any fee_keep path that can overflow) with `Decimal::checked_from_ratio`. On overflow: **`Skip`** (walk continues, consistent with L18/L20) **or** `ContractError` fail-closed. Pin one in the skill. Delete or use `constant_product_net_out` (do not leave a misleading dead “1-unit dump” helper if unused). Add a unit test with a ratio that would panic `from_ratio`. 4. **Equal-rate unit test** while touching `greedy.rs`: bid/ask net **equal** to pool net → `GreedyBeats::No` (stop), not `Skip`. --- ## Acceptance criteria - [ ] Pair `HybridSimulation` with **both** `hybrid` and `greedy` **errors** (same family as execute / router). No greedy number returned. - [ ] Pair `HybridSimulation` with **only** `greedy` still matches execute on a live non-expired book (existing `greedy_simulation_matches_execute` stays green; also assert `greedy_stop` + `limit_book_offer_consumed` where useful). - [ ] Pair `HybridSimulation` with **only** `hybrid` (Pattern C) unchanged; `greedy_stop` omitted (**G14**). - [ ] Pair `HybridSimulation` with neither stays pool-only (**G1**). - [ ] Execute still rejects both; router sim/execute still reject both. - [ ] Empty book greedy execute/query: `greedy_stop=empty`, `limit_book_offer_consumed=0`. - [ ] Better book + remainder to pool: `greedy_stop` is **not** `empty`; `makers` were filled; leftover went to AMM (**G9**). - [ ] Full book consume: `greedy_stop=filled`. - [ ] Worse-than-pool with zero fills: `worse_than_pool` (or empty-equivalent only if zero makers — pin: prefer `worse_than_pool` when the walk stopped on G3). - [ ] `max_makers` / `scan_cap` priority unchanged vs today’s `stop_reason_priority` test. - [ ] Extreme `output/input` (overflow `from_ratio`) on greedy execute **and** query: **no panic**; Skip or typed error as designed; pool-only swap on the same reserves still works. - [ ] Equal Decimal rates → stop (`No`), not skip. - [ ] Docs/skill: G11 applies to **query**; new stop-reason wire name; A11/A7 notes. - [ ] `make verify-issue-<iid>` (or extend `verify-issue-708`) greps + unit/multitest names below. --- ## Test plan (functional paths) **dex-common / pair unit** 1. `resolve_swap_hybrid_mode(Some(h), Some(g))` still errors (existing `g11_both_hybrid_and_greedy_rejected`). 2. **New:** pair query both fields → error (not a `HybridSimulationResponse`). 3. Query greedy-only / hybrid-only / neither — regression. 4. `greedy_stop_after_walk`: makers>0, offer_left>0, not worse/scan/cap → **new remainder reason**, not `Empty`. 5. Existing `stop_reason_priority` updated for the new variant. 6. `pool_spot_net` / beats: ratio that panics `from_ratio` → `Skip` or error, no panic. 7. Equal bid net == pool net → `GreedyBeats::No`. Ask-side equal → `No`. 8. Zero reserves still `Skip`. **cl8y-dex-tests `greedy_book_first_708`** 9. LCD-style `HybridSimulation { hybrid: Some(declared), greedy: Some(g) }` errors; execute of the same payload still errors. 10. `greedy_empty_book_rolls_to_pool` still `greedy_stop=empty`. 11. `greedy_better_bid_then_pool_remainder` asserts new stop reason + `limit_book_offer_consumed > 0` (and `book_return_amount` if emitted). 12. `greedy_worse_or_equal_bid_does_not_fill` stop reason unchanged. 13. `greedy_max_maker_fills_one_stops` → `max_makers`. 14. `greedy_simulation_matches_execute` still equal **and** same `greedy_stop`. 15. Pattern C sim omits `greedy_stop` (existing `pattern_c_sim_omits_greedy_stop`). **Router (keep aligned)** 16. `SimulateSwapOperations` hop with both fields still errors (regression if not already covered — add if missing). --- ## Test plan (attack, hack, abuse) | Vector | Expect | |--------|--------| | **A10 / A11 confused payload** | `{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. | | **A11 sim greedy / exec pool-only** | Distinct msgs; omitted `greedy` on execute remains G1 pool-only (pre-existing). Not a contract bug; query must not hide the mutex. | | **A7 overflow pool spot** | No VM panic. Skip maker-compare or fail with typed error. Remainder/pool-only path still available. Do not treat overflow as “beats pool” (that would drain worse makers). | | **A7 overflow maker price** | Unchanged L20 skip. | | **Stop-reason spoof / indexer** | Only the contract emits `greedy_stop`. New remainder value must not collide with `empty`. Pattern C still omits the field so old parsers do not see a bogus empty. | | **DoS via tiny reserve** | Greedy on a 1-wei vs huge-reserve pool must not panic the pair for other users’ pool-only txs. | | **A8 max_maker_fills** | Unchanged 0-reject / 100-clamp; not this ticket unless tests regress. | --- ## Verification criteria - [ ] `cd smartcontracts && cargo test -p dex-common greedy_swap -- --test-threads=1` green. - [ ] `cd smartcontracts && cargo test -p cl8y-dex-pair greedy -- --test-threads=1` green (includes overflow + equal-rate + remainder stop reason). - [ ] `cd smartcontracts && cargo test -p cl8y-dex-tests greedy_book_first_708 -- --test-threads=1` green, including **new** both-fields query test and remainder `greedy_stop` assert. - [ ] Explicit: pair query both fields errors; execute both fields still errors. - [ ] Explicit: better-bid remainder `greedy_stop != empty`. - [ ] Explicit: `Decimal::from_ratio`-overflow ratio does not panic in `pool_spot_net` / greedy execute. - [ ] `make verify-issue-<iid>` (or extended `verify-issue-708`) covers the greps + test names. - [ ] Skill **G7/G11** state that pair **query** uses `resolve_swap_hybrid_mode` with both fields. Refs: !1198 review, [#708](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/708) G7 / G11 / A7 / A11, [`skills/AGENTS_GREEDY_BOOK_FIRST.md`](skills/AGENTS_GREEDY_BOOK_FIRST.md).
PlasticDigits commented 2026-08-30 10:20:09 +00:00 (Migrated from gitlab.com)

marked as related to #708

marked as related to #708
PlasticDigits commented 2026-08-30 11:08:14 +00:00 (Migrated from gitlab.com)

mentioned in commit 83bccc2838

mentioned in commit 83bccc28386660222aebde56a127f4403f834669
PlasticDigits commented 2026-08-30 11:09:51 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1198

mentioned in merge request !1198
PlasticDigits commented 2026-08-31 05:07:34 +00:00 (Migrated from gitlab.com)

mentioned in commit 56e4c4f513

mentioned in commit 56e4c4f51339b9bc20422a7783bf94af748d7044
PlasticDigits commented 2026-08-31 05:07:56 +00:00 (Migrated from gitlab.com)

mentioned in commit f54de4fd4b

mentioned in commit f54de4fd4b2373b8f482791c620ff34e6d47d6fa
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-31 05:07:57 +00:00
PlasticDigits commented 2026-08-31 05:09:18 +00:00 (Migrated from gitlab.com)

mentioned in issue #712

mentioned in issue #712
PlasticDigits commented 2026-08-31 05:09:18 +00:00 (Migrated from gitlab.com)

marked as related to #712

marked as related to #712
PlasticDigits commented 2026-08-31 05:09:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #708

mentioned in issue #708
PlasticDigits commented 2026-08-31 05:09:38 +00:00 (Migrated from gitlab.com)

!1198 merged to main (f54de4fd). Merge-time verify: make verify-issue-709 PASS (mutex, remainder_to_pool, checked_from_ratio Skip).

All #709 acceptance items landed in 83bccc28 and 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 dummy book_input=1 error string; G6 live same-side stale hint. Tracked on #712. Do not reopen this issue for those.

!1198 merged to `main` (`f54de4fd`). Merge-time verify: `make verify-issue-709` **PASS** (mutex, `remainder_to_pool`, `checked_from_ratio` Skip). All #709 acceptance items landed in `83bccc28` and 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 dummy `book_input=1` error string; G6 live same-side stale hint. Tracked on #712. Do **not** reopen this issue for those.
PlasticDigits commented 2026-09-01 08:15:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #718

mentioned in issue #718
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#709
No description provided.