feat: opt-in greedy book-first swap (#708, leftovers #709 #710) #1198

Merged
PlasticDigits merged 4 commits from feat/708-greedy-book-first into main 2026-08-31 05:07:56 +00:00
PlasticDigits commented 2026-08-30 06:40:16 +00:00 (Migrated from gitlab.com)

Summary

  • Adds opt-in pair/router greedy book-first swap: caller sends only max_maker_fills + optional book_start_hint (GreedySwapParams), not a Pattern C split.
  • Fills live same-side makers whose net Decimal rate strictly beats the pool spot after fees, then dumps leftover offer into the AMM. hybrid: None stays pool-only (G1).
  • Quote (HybridSimulation + greedy) uses the same resolve_swap_hybrid_mode as execute on both fields (G7 / G11 / #709). Official dApp stays on GET /route/solve.
  • Leftovers from review: pair query mutex (no silent greedy quote when both fields set), greedy_stop=remainder_to_pool after a real book fill + pool remainder, pool-spot overflow is Skip not a VM panic (A7), plus tax/pause/blacklist/AfterSwap coverage (#710).

Closes #708
Closes #709
Closes #710

Design notes

  • New serde shape (greedy) so declared HybridSwapParams cannot deserialize as greedy (G11 / A10).
  • G3 beats: Decimal rates, not 1-raw-unit CP dumps. Equal is a stop. Pool spot uses Decimal::checked_from_ratio (overflow → Skip).
  • Owner note asked for opt-out default. This MR keeps G1 (opt-in).
  • Wasm attrs include greedy_stop: worse_than_pool | max_makers | scan_cap | empty (no maker filled) | filled | remainder_to_pool (makers filled, leftover → AMM).

Invariants (G1–G14)

  • G1 — hybrid: None + greedy: None is pool-only
  • G2 — no caller pool_input / book_input on greedy
  • G3 — stop when next priceable maker does not strictly beat pool (rate compare; skip unpriceable)
  • G4 — not the solver; docs say greedy ≤ /route/solve, ≥ pool-only when the book is strictly better
  • G5 — max_maker_fills == 0 rejects; oversize clamps to 100
  • G6 — hint is caller-supplied; missing/stale/wrong-side → head (L17)
  • G7 — pair query and execute both call resolve_swap_hybrid_mode(hybrid, greedy)
  • G8 — greedy execute / router hops require belief_price or min_return
  • G9 — leftover offer after the walk goes to the pool; stop reason remainder_to_pool
  • G10 — reuse match_bids / match_asks / simulate_match_*
  • G11 — distinct GreedySwapParams; both hybrid+greedy rejected on pair query, pair execute, router sim, router execute
  • G12 — router greedy is optional and explicit per hop
  • G13 — getGasLimitForTx + swarm gas.ts map greedy (hook string stays swap)
  • G14 — Pattern C JSON omits greedy_stop; existing Pattern C tests stay green

Leftovers #709 (correctness)

  • Pair HybridSimulation with both hybrid and greedy errors (same family as execute)
  • Greedy-only query still matches execute; Pattern C / neither unchanged (G14 / G1)
  • Empty book → greedy_stop=empty; full consume → filled; remainder → remainder_to_pool (not empty)
  • Equal Decimal rates → stop (No); pool-spot overflow → Skip, no panic (A7)
  • make verify-issue-709

Leftovers #710 (coverage)

  • Community-tax greedy sell extra-debits CW20 from; router hop extra-debits original trader (T592-13)
  • Pause rejects greedy execute; unpause can fill (L6); G1 pool-only also rejects while paused
  • Blacklisted maker not filled; blacklisted taker rejects (L19)
  • Pair-direct trader spoof does not steal fee tier or change tax subject (A9 / A14)
  • AfterSwap return / commission = book net + pool net (L7); query does not park
  • make verify-issue-710

Docs / skills

  • skills/AGENTS_GREEDY_BOOK_FIRST.md (G1–G14, #709/#710)
  • ADR 0001, docs/limit-orders.md, docs/integrators.md, docs/contracts-security-audit.md, docs/testing.md, docs/README.md agent-player index
  • Crosslinks: AGENTS_HYBRID_QUOTING.md, AGENTS_BOOK_MATCH_HINT_SECURITY.md, AGENTS_TERRACLASSIC_GAS.md, AGENTS_COMMUNITY_TAX_ROUTER.md
  • make verify-issue-708 · make verify-issue-709 · make verify-issue-710

Test plan

  • make verify-issue-709 (dex-common + pair greedy + greedy_book_first_708 + docs grep + retest)
  • make verify-issue-710 (community-tax greedy + pause/L7/A14 + greedy_blacklist + docs grep + retest)
  • Pattern C: cargo test -p cl8y-dex-tests bid_and_hybrid_swap_partially_fills_book -- --test-threads=1
  • Greedy gas vitest (G13) via frontend vitest run hybridSwapGas + terraGas.greedy
  • make verify-issue-708 on a checkout with frontend-dapp/node_modules (worktree without install fails vitest PATH; tests themselves passed)
  • LocalTerra canary (optional): greedy hop vs pool-only on a pair with a strictly better live ask

Not in this MR

  • Pair wasm migrate on columbus-5 (G14 ops follow-up)
  • Flipping default to greedy (owner opt-out note)
  • Official dApp off /route/solve
  • Reverse greedy simulation (HybridReverseSimulation unchanged — pool-only / declared hybrid only)
  • Indexer replacing GET best-execution with greedy
  • Auto-deriving book_start_hint on-chain
  • AfterSwap re-entrancy (A13) greedy-specific test (optional on #710; Pattern C allowlist unchanged)
  • Community-tax greedy buy analogue; empty-book tax extra-debit named test (sell extra-debit does not depend on book contact; covered by existing pool-only tax tests + greedy sell)
  • Router G8 dummy book_input=1 error string; G6 live same-side stale hint (should-fix from !1198 review, not #709/#710)
## Summary - Adds opt-in pair/router greedy book-first swap: caller sends only `max_maker_fills` + optional `book_start_hint` (`GreedySwapParams`), not a Pattern C split. - Fills live same-side makers whose net Decimal rate strictly beats the pool spot after fees, then dumps leftover offer into the AMM. `hybrid: None` stays pool-only (**G1**). - Quote (`HybridSimulation` + `greedy`) uses the same `resolve_swap_hybrid_mode` as execute on **both** fields (**G7** / **G11** / #709). Official dApp stays on `GET /route/solve`. - Leftovers from review: pair query mutex (no silent greedy quote when both fields set), `greedy_stop=remainder_to_pool` after a real book fill + pool remainder, pool-spot overflow is **Skip** not a VM panic (**A7**), plus tax/pause/blacklist/AfterSwap coverage (#710). Closes #708 Closes #709 Closes #710 ## Design notes - New serde shape (`greedy`) so declared `HybridSwapParams` cannot deserialize as greedy (**G11** / **A10**). - **G3 beats:** Decimal rates, not 1-raw-unit CP dumps. Equal is a stop. Pool spot uses `Decimal::checked_from_ratio` (overflow → Skip). - Owner note asked for **opt-out default**. This MR keeps **G1** (opt-in). - Wasm attrs include `greedy_stop`: `worse_than_pool` | `max_makers` | `scan_cap` | `empty` (no maker filled) | `filled` | `remainder_to_pool` (makers filled, leftover → AMM). ## Invariants (G1–G14) - [x] **G1** — `hybrid: None` + `greedy: None` is pool-only - [x] **G2** — no caller `pool_input` / `book_input` on greedy - [x] **G3** — stop when next priceable maker does not strictly beat pool (rate compare; skip unpriceable) - [x] **G4** — not the solver; docs say greedy ≤ `/route/solve`, ≥ pool-only when the book is strictly better - [x] **G5** — `max_maker_fills == 0` rejects; oversize clamps to 100 - [x] **G6** — hint is caller-supplied; missing/stale/wrong-side → head (**L17**) - [x] **G7** — pair query and execute both call `resolve_swap_hybrid_mode(hybrid, greedy)` - [x] **G8** — greedy execute / router hops require `belief_price` or `min_return` - [x] **G9** — leftover offer after the walk goes to the pool; stop reason `remainder_to_pool` - [x] **G10** — reuse `match_bids` / `match_asks` / `simulate_match_*` - [x] **G11** — distinct `GreedySwapParams`; both hybrid+greedy rejected on pair query, pair execute, router sim, router execute - [x] **G12** — router greedy is optional and explicit per hop - [x] **G13** — `getGasLimitForTx` + swarm `gas.ts` map greedy (hook string stays `swap`) - [x] **G14** — Pattern C JSON omits `greedy_stop`; existing Pattern C tests stay green ## Leftovers #709 (correctness) - [x] Pair `HybridSimulation` with both `hybrid` and `greedy` errors (same family as execute) - [x] Greedy-only query still matches execute; Pattern C / neither unchanged (**G14** / **G1**) - [x] Empty book → `greedy_stop=empty`; full consume → `filled`; remainder → `remainder_to_pool` (not `empty`) - [x] Equal Decimal rates → stop (`No`); pool-spot overflow → Skip, no panic (**A7**) - [x] `make verify-issue-709` ## Leftovers #710 (coverage) - [x] Community-tax greedy sell extra-debits CW20 `from`; router hop extra-debits original trader (**T592-13**) - [x] Pause rejects greedy execute; unpause can fill (**L6**); G1 pool-only also rejects while paused - [x] Blacklisted maker not filled; blacklisted taker rejects (**L19**) - [x] Pair-direct `trader` spoof does not steal fee tier or change tax subject (**A9** / **A14**) - [x] AfterSwap return / commission = book net + pool net (**L7**); query does not park - [x] `make verify-issue-710` ## Docs / skills - `skills/AGENTS_GREEDY_BOOK_FIRST.md` (G1–G14, #709/#710) - ADR 0001, `docs/limit-orders.md`, `docs/integrators.md`, `docs/contracts-security-audit.md`, `docs/testing.md`, `docs/README.md` agent-player index - Crosslinks: `AGENTS_HYBRID_QUOTING.md`, `AGENTS_BOOK_MATCH_HINT_SECURITY.md`, `AGENTS_TERRACLASSIC_GAS.md`, `AGENTS_COMMUNITY_TAX_ROUTER.md` - `make verify-issue-708` · `make verify-issue-709` · `make verify-issue-710` ## Test plan - [x] `make verify-issue-709` (dex-common + pair greedy + `greedy_book_first_708` + docs grep + retest) - [x] `make verify-issue-710` (community-tax greedy + pause/L7/A14 + `greedy_blacklist` + docs grep + retest) - [x] Pattern C: `cargo test -p cl8y-dex-tests bid_and_hybrid_swap_partially_fills_book -- --test-threads=1` - [x] Greedy gas vitest (G13) via frontend `vitest run` hybridSwapGas + terraGas.greedy - [ ] `make verify-issue-708` on a checkout with `frontend-dapp/node_modules` (worktree without install fails vitest PATH; tests themselves passed) - [ ] LocalTerra canary (optional): greedy hop vs pool-only on a pair with a strictly better live ask ## Not in this MR - Pair wasm migrate on columbus-5 (**G14** ops follow-up) - Flipping default to greedy (owner opt-out note) - Official dApp off `/route/solve` - Reverse greedy simulation (`HybridReverseSimulation` unchanged — pool-only / declared hybrid only) - Indexer replacing GET best-execution with greedy - Auto-deriving `book_start_hint` on-chain - AfterSwap re-entrancy (A13) greedy-specific test (optional on #710; Pattern C allowlist unchanged) - Community-tax greedy **buy** analogue; empty-book tax extra-debit named test (sell extra-debit does not depend on book contact; covered by existing pool-only tax tests + greedy sell) - Router G8 dummy `book_input=1` error string; G6 live same-side stale hint (should-fix from !1198 review, not #709/#710)
PlasticDigits commented 2026-08-30 06:40:27 +00:00 (Migrated from gitlab.com)

marked this merge request as ready

marked this merge request as **ready**
PlasticDigits commented 2026-08-30 06:41:06 +00:00 (Migrated from gitlab.com)

added 1 commit

  • f0d0c2d5 - Document greedy G4 (not an on-chain split solver).

Compare with previous version

added 1 commit <ul><li>f0d0c2d5 - Document greedy G4 (not an on-chain split solver).</li></ul> [Compare with previous version](/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/480/diffs?diff_id=1992525287&start_sha=e3ab31400a824f7e02feea8beb2266ebfb8eb792)
PlasticDigits commented 2026-08-30 07:29:26 +00:00 (Migrated from gitlab.com)

RECOMMEND: FIX

Reviewed !1198 (feat/708-greedy-book-first, f0d0c2d5) as the MR that implements #708. GitLab has no MR iid 708; this is the open greedy book-first change.

The opt-in contract path is real and mostly matches G1–G13 on execute: distinct GreedySwapParams, hybrid: None stays pool-only, Decimal-rate G3 stop, G5 0-reject / 100-clamp, G8 floor, G9 remainder to AMM, matcher reuse, frontend/swarm hybrid gas envelope (not silent 600k). No fund-theft / privilege-escalation / prompt-injection path turned up on the new walk.

Do not merge until the quote/execute mutex, greedy_stop attr, and pool-spot panic are fixed. Closes #708 also overclaims relative to the issue’s open acceptance checkboxes, owner opt-out note, and columbus-5 migrate.


Must-fix (blocking)

1. Pair HybridSimulation accepts hybrid + greedy; execute rejects (G11 / G7 / A11)

query_hybrid_simulation prefers greedy and drops hybrid:

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

simulate_hybrid_swap_with_fee then calls resolve_swap_hybrid_mode with hybrid: None whenever greedy is set. Execute uses resolve_swap_hybrid_mode(hybrid, greedy) and errors. Router simulate already rejects both (cannot set both hybrid and greedy on a hop).

Same LCD JSON can quote greedy and fail on Cw20HookMsg::Swap. Fix: call resolve_swap_hybrid_mode on the pair query with both fields; do not dummy-out hybrid.

  • smartcontracts/contracts/pair/src/contract.rs (~2454–2460, ~2674–2679)

2. greedy_stop=empty after a real book fill + pool remainder

GreedyStopReason::Empty is documented as “no live same-side maker was filled.” greedy_stop_after_walk falls through to Empty when makers_used > 0, offer remains, and the book is exhausted (not worse-than-pool, not cap). That is the common success path (greedy_better_bid_then_pool_remainder). Indexers/bots keying empty as “no book contact” will mis-read fills.

Add a distinct reason (e.g. remainder_to_pool / book_exhausted) or map that case to something other than empty. Assert greedy_stop on the remainder test.

  • smartcontracts/contracts/pair/src/greedy.rs (~147–174)
  • smartcontracts/packages/dex-common/src/pair.rs (~137–138)

3. Decimal::from_ratio can panic on greedy pool spot (DoS, opt-in only)

pool_spot_net uses Decimal::from_ratio(output_reserve, input_reserve). In cosmwasm-std 1.5 this panics when numerator * 10^18 overflows Uint128 (~ratio ≳ 3.4e20). Near-drained 18-dec pools can hit that. Pool-only swaps never take this path; greedy execute and query do. constant_product_net_out already uses Uint256 but is unused.

Use Decimal::checked_from_ratio and treat overflow as Skip or a contract error (fail closed, full gas panic).

  • smartcontracts/contracts/pair/src/greedy.rs (~73–82)

Should-fix (same MR if cheap)

Item Why
Router ReverseSimulateSwapOperations ignores greedy (TerraSwap { .. }) Reverse-quotes a greedy hop as pool-only (A11). Pair HybridReverseSimulation has no greedy field (listed as not-in-MR — either implement or document as unsupported, not silent pool-only).
Router G8 error uses dummy book_input = 1 LCD: “Hybrid swap with book_input 1 requires per-hop min_return”. Greedy hops have no book_input.
Skill G6 “stale → head” Missing / wrong-side / missing-id → head (L17). A live same-side stale hint starts there; greedy stop can then dump the whole offer to the AMM and skip a better head. Document as integrator risk; optional greedy-only fallback if hinted row fails G3.
equal_is_not_strictly_better Asserts price == 0 → Skip, not equal-rate → No. Add a real equal-rate unit test.
ask_beats_residual_pool No pair unit test (ask covered only in multitest).

Security audit

Area Result
Access control No new admin. Direct ExecuteMsg::Swap still rejected. trader still router-stamped only (A9 / A14).
G1 default Pool-only unless greedy is set. TerraSwap callers unchanged.
A1 wrong-side hint L17 fallback; multitest greedy_wrong_side_hint_falls_back_to_head.
A4 worse-than-pool Strict Decimal net; equal stops. Better than Pattern C book_input = offer.
A5 sandwich / oracle No external oracle. Compare uses this-tx reserves. G8 required. max_spread is inert on pure-book (declared_pool_input = 0); tight min_return is the real floor. Residual MEV, not a new trust boundary.
A6 / A7 L18 cost == 0 skip before greedy; overflow maker skip. Residual: Decimal pool overflow (must-fix 3).
A8 0 reject, clamp 100. Dust can still exhaust cap; G8 if tight.
A10 schema Distinct struct + deny_unknown_fields; Pattern C JSON does not decode as greedy.
A12 / A13 Pause / blacklist / AfterSwap ordering unchanged. Greedy does not add Send-to-maker. Untested on the greedy path.
A13 re-entrancy Same settlement-then-hooks.
Community tax Same Cw20HookMsg::Swap + router trader. Extra-debit should still hit the original trader. No greedy sell test (issue path 18).
k / fees / TWAP Book does not touch RESERVES; remainder uses existing CP + new_k >= k. TWAP still before reserve change.
Tokenomic / interior split Greedy can lose to /route/solve (G4, documented). Official dApp stays on the solver.
Database / indexer leaks Indexer unchanged. No new SQL, no greedy_stop ingest, no secret/log change. N/A for DB leaks.
Gas grief Caps 100 / 500 / 15 parks. Unmapped greedy in this dApp/swarm uses hybrid 15M, not 600k (G13). Third-party wallets without the map can still OOG (pre-#475 class).
Prompt injection None.

Not BLOCK: no validated escrow theft, maker drain below pool via greedy, or auth bypass.


#708 acceptance / verification

Issue checkboxes are still unchecked. Mapping against this diff:

Criterion Verdict
hybrid: None never reads the book Pass — g1_pool_only_skips_better_bid
No pool_input / book_input on greedy Pass — G2 / A10 serde tests
Better book → fill + remainder; return_amount ≥ pool-only Partial — fill+remainder tested; no ≥ pool-only, k, or treasury = pool + book half
Worse book → 100% pool Pass
Mixed prefix, stop before worse tail Pass
Caps; OOG not the bound Partial — fills=1 / 0 / clamp; no greedy MAX_SCAN_STEPS / park-cap
Wrong-side / missing hint → head Partial — wrong-side + None; no stale/cancelled-id
Quote = execute; expired-head drift documented Partial — net return_amount only; query mutex hole; drift not documented
G8 neither floor reverts Pass on pair; router hop untested
Pause / blacklist / fee-tier / tax extra-debit / AfterSwap L7 Missing on greedy
Pattern C still works Partial — greedy: None compile-fixed; verify-issue-708 does not run hybrid suites
Gas mapped; unmapped throws Partial — dApp vitest; swarm gas.ts wired with no greedy gas.test.ts; unmapped still warn/200k fallback, not greedy-specific throw
ADR / skill / make verify-issue-708 Pass (grep + scoped tests)
LocalTerra / limit_order_fill + book_return_amount Missing (explicit follow-up)

Attack plan: A1, A4, A8, A10 tested. A2, A3, A5, A7, A9, A12–A14 missing. A6 partial (price=0 skip only). A11 broken by must-fix 1.

Functional paths 1–18: 2, 5, 7, 9 strongest. 11, 12, 14, 16, 17, 18 missing. Router greedy hop has no contract test.

make verify-issue-708 is dex-common + pair greedy + greedy_book_first_708 + two Vitest files + docs greps. It does not run make test-contracts, Pattern C hybrid, router, community-tax, swarm, or e2e.

Locally: cargo test -p dex-common greedy_swap (9) and cargo test -p cl8y-dex-pair greedy (4) pass. GitLab pipeline 2802987818 failed with ci_quota_exceeded on every job (including gitleaks) — infra, not a test signal.


Gap analysis

Features vs #708

Shipped: opt-in pair/router greedy, G3 Decimal pin, wasm greedy_stop, G13 gas, skill + ADR/docs. Honored out-of-scope: do not flip hybrid: None; do not take Swap/Trade off /route/solve; no on-chain split search; no auto hint.

Deferred (MR “Not in this MR”, but issue still wants them before calling #708 done):

  • Owner opt-out default (PlasticDigits note). Keeping G1 is the right security call (15M gas + expired-head grief + L8). Do not close #708 as “bots get greedy without changing JSON” — they still pool-dump until they opt in and pair wasm is migrated.
  • G14 ops: factory pair_code_id migrate, LocalTerra canary, columbus-5 canary. Until then live greedy fails deserialize on old pair wasm.
  • Reverse greedy simulation.
  • Indexer greedy_stop / metric “share of pair-direct txs with book leg” cannot tell greedy from Pattern C.

UI/UX

No retail greedy toggle. Types + gas only. Integrators get one paragraph in docs/integrators.md — no LCD JSON cookbook, no simulateGreedySwap in pair.ts. Fine vs “no retail requirement”; thin for the integrators label.

DRY / quality

Greedy gate copied 4× in match_* / simulate_match_* (issue asked for a pre-pass then existing matcher — G10 reuse is good, duplication is not). constant_product_net_out is dead. Pair InvalidHybridParams Display drops {reason} — greedy max_maker_fills == 0 shows as generic “Invalid hybrid parameters”.

Gas

Mapping is conservative (always makers + pool leg + scan overhead). Correct vs 600k; not tight. No wasm-size check.

Packages

36 files. Community-tax / blacklist / migration greedy: None literals are serde/struct fallout, not tax-behavior changes. AutoLP skim stays pool-only.

What a user might still expect

Copy-paste LCD execute/query; SDK helper; reverse quote; makers_used on sim (only limit_book_offer_consumed + greedy_stop); indexer attr; LocalTerra proof; dApp badge (correctly out of scope).


Suggested patch order

  1. Pair query: resolve_swap_hybrid_mode(hybrid, greedy) — reject both (align with execute + router).
  2. Fix greedy_stop enum/fallback + remainder test assert.
  3. checked_from_ratio in pool_spot_net.
  4. Tests: router greedy hop vs hybrid: null; hybrid+greedy query error; community-tax greedy sell extra-debit; pause/blacklist; expired-prefix scan cap; return_amount ≥ pool-only; equal-rate G3 unit test.
  5. Either implement reverse greedy or make reverse-sim error on greedy (do not quote pool-only).
  6. Split follow-ups from Closes #708: owner default, pair migrate/canary, indexer greedy_stop, reverse sim if deferred.

Happy-path design (G1 opt-in, G3 rate stop, G8, G11 serde shape, G13 gas) is in good shape. The mutex/attr/panic issues plus the open #708 checklist are why this is FIX, not ACCEPT.

RECOMMEND: FIX Reviewed **!1198** (`feat/708-greedy-book-first`, `f0d0c2d5`) as the MR that implements **#708**. GitLab has no MR iid 708; this is the open greedy book-first change. The opt-in contract path is real and mostly matches **G1–G13** on execute: distinct `GreedySwapParams`, `hybrid: None` stays pool-only, Decimal-rate G3 stop, G5 0-reject / 100-clamp, G8 floor, G9 remainder to AMM, matcher reuse, frontend/swarm hybrid gas envelope (not silent 600k). **No fund-theft / privilege-escalation / prompt-injection path** turned up on the new walk. Do **not** merge until the quote/execute mutex, `greedy_stop` attr, and pool-spot panic are fixed. `Closes #708` also overclaims relative to the issue’s open acceptance checkboxes, owner opt-out note, and columbus-5 migrate. --- ## Must-fix (blocking) ### 1. Pair `HybridSimulation` accepts `hybrid` + `greedy`; execute rejects (**G11** / **G7** / **A11**) `query_hybrid_simulation` prefers greedy and drops hybrid: ```rust (_, Some(_)) => (&dummy, greedy.as_ref()), ``` `simulate_hybrid_swap_with_fee` then calls `resolve_swap_hybrid_mode` with `hybrid: None` whenever `greedy` is set. Execute uses `resolve_swap_hybrid_mode(hybrid, greedy)` and **errors**. Router simulate already rejects both (`cannot set both hybrid and greedy on a hop`). Same LCD JSON can quote greedy and fail on `Cw20HookMsg::Swap`. Fix: call `resolve_swap_hybrid_mode` on the pair query with both fields; do not dummy-out hybrid. - `smartcontracts/contracts/pair/src/contract.rs` (~2454–2460, ~2674–2679) ### 2. `greedy_stop=empty` after a real book fill + pool remainder `GreedyStopReason::Empty` is documented as “no live same-side maker was filled.” `greedy_stop_after_walk` falls through to `Empty` when `makers_used > 0`, offer remains, and the book is exhausted (not worse-than-pool, not cap). That is the **common success path** (`greedy_better_bid_then_pool_remainder`). Indexers/bots keying `empty` as “no book contact” will mis-read fills. Add a distinct reason (e.g. `remainder_to_pool` / `book_exhausted`) or map that case to something other than `empty`. Assert `greedy_stop` on the remainder test. - `smartcontracts/contracts/pair/src/greedy.rs` (~147–174) - `smartcontracts/packages/dex-common/src/pair.rs` (~137–138) ### 3. `Decimal::from_ratio` can panic on greedy pool spot (**DoS**, opt-in only) `pool_spot_net` uses `Decimal::from_ratio(output_reserve, input_reserve)`. In cosmwasm-std 1.5 this **panics** when `numerator * 10^18` overflows `Uint128` (~ratio ≳ 3.4e20). Near-drained 18-dec pools can hit that. Pool-only swaps never take this path; greedy execute **and** query do. `constant_product_net_out` already uses `Uint256` but is unused. Use `Decimal::checked_from_ratio` and treat overflow as `Skip` or a contract error (fail closed, full gas panic). - `smartcontracts/contracts/pair/src/greedy.rs` (~73–82) --- ## Should-fix (same MR if cheap) | Item | Why | |------|-----| | Router `ReverseSimulateSwapOperations` ignores `greedy` (`TerraSwap { .. }`) | Reverse-quotes a greedy hop as pool-only (**A11**). Pair `HybridReverseSimulation` has no `greedy` field (listed as not-in-MR — either implement or document as unsupported, not silent pool-only). | | Router G8 error uses dummy `book_input = 1` | LCD: “Hybrid swap with book_input 1 requires per-hop min_return”. Greedy hops have no `book_input`. | | Skill **G6** “stale → head” | Missing / wrong-side / missing-id → head (**L17**). A **live same-side** stale hint starts there; greedy **stop** can then dump the whole offer to the AMM and skip a better head. Document as integrator risk; optional greedy-only fallback if hinted row fails G3. | | `equal_is_not_strictly_better` | Asserts `price == 0` → `Skip`, not equal-rate → `No`. Add a real equal-rate unit test. | | `ask_beats_residual_pool` | No pair unit test (ask covered only in multitest). | --- ## Security audit | Area | Result | |------|--------| | **Access control** | No new admin. Direct `ExecuteMsg::Swap` still rejected. `trader` still router-stamped only (**A9** / **A14**). | | **G1 default** | Pool-only unless `greedy` is set. TerraSwap callers unchanged. | | **A1 wrong-side hint** | L17 fallback; multitest `greedy_wrong_side_hint_falls_back_to_head`. | | **A4 worse-than-pool** | Strict Decimal net; equal stops. Better than Pattern C `book_input = offer`. | | **A5 sandwich / oracle** | No external oracle. Compare uses this-tx reserves. G8 required. `max_spread` is inert on pure-book (`declared_pool_input = 0`); tight `min_return` is the real floor. Residual MEV, not a new trust boundary. | | **A6 / A7** | L18 `cost == 0` skip before greedy; overflow maker skip. Residual: Decimal **pool** overflow (must-fix 3). | | **A8** | 0 reject, clamp 100. Dust can still exhaust cap; G8 if tight. | | **A10 schema** | Distinct struct + `deny_unknown_fields`; Pattern C JSON does not decode as greedy. | | **A12 / A13** | Pause / blacklist / AfterSwap ordering unchanged. Greedy does not add Send-to-maker. **Untested** on the greedy path. | | **A13 re-entrancy** | Same settlement-then-hooks. | | **Community tax** | Same `Cw20HookMsg::Swap` + router `trader`. Extra-debit should still hit the original trader. **No greedy sell test** (issue path 18). | | **k / fees / TWAP** | Book does not touch `RESERVES`; remainder uses existing CP + `new_k >= k`. TWAP still before reserve change. | | **Tokenomic / interior split** | Greedy can lose to `/route/solve` (**G4**, documented). Official dApp stays on the solver. | | **Database / indexer leaks** | Indexer unchanged. No new SQL, no `greedy_stop` ingest, no secret/log change. N/A for DB leaks. | | **Gas grief** | Caps 100 / 500 / 15 parks. Unmapped greedy in **this** dApp/swarm uses hybrid 15M, not 600k (**G13**). Third-party wallets without the map can still OOG (pre-#475 class). | | **Prompt injection** | None. | Not **BLOCK**: no validated escrow theft, maker drain below pool via greedy, or auth bypass. --- ## #708 acceptance / verification Issue checkboxes are still unchecked. Mapping against this diff: | Criterion | Verdict | |-----------|---------| | `hybrid: None` never reads the book | **Pass** — `g1_pool_only_skips_better_bid` | | No `pool_input` / `book_input` on greedy | **Pass** — G2 / A10 serde tests | | Better book → fill + remainder; `return_amount` ≥ pool-only | **Partial** — fill+remainder tested; **no** ≥ pool-only, k, or treasury = pool + book half | | Worse book → 100% pool | **Pass** | | Mixed prefix, stop before worse tail | **Pass** | | Caps; OOG not the bound | **Partial** — fills=1 / 0 / clamp; **no** greedy `MAX_SCAN_STEPS` / park-cap | | Wrong-side / missing hint → head | **Partial** — wrong-side + `None`; **no** stale/cancelled-id | | Quote = execute; expired-head drift documented | **Partial** — net `return_amount` only; query mutex hole; drift not documented | | G8 neither floor reverts | **Pass** on pair; router hop untested | | Pause / blacklist / fee-tier / tax extra-debit / AfterSwap L7 | **Missing** on greedy | | Pattern C still works | **Partial** — `greedy: None` compile-fixed; `verify-issue-708` does **not** run hybrid suites | | Gas mapped; unmapped throws | **Partial** — dApp vitest; swarm `gas.ts` wired with **no** greedy `gas.test.ts`; unmapped still warn/200k fallback, not greedy-specific throw | | ADR / skill / `make verify-issue-708` | **Pass** (grep + scoped tests) | | LocalTerra / `limit_order_fill` + `book_return_amount` | **Missing** (explicit follow-up) | **Attack plan:** A1, A4, A8, A10 tested. **A2, A3, A5, A7, A9, A12–A14 missing.** A6 partial (`price=0` skip only). A11 broken by must-fix 1. **Functional paths 1–18:** 2, 5, 7, 9 strongest. **11, 12, 14, 16, 17, 18 missing.** Router greedy hop has **no** contract test. `make verify-issue-708` is dex-common + pair `greedy` + `greedy_book_first_708` + two Vitest files + docs greps. It does not run `make test-contracts`, Pattern C hybrid, router, community-tax, swarm, or e2e. Locally: `cargo test -p dex-common greedy_swap` (9) and `cargo test -p cl8y-dex-pair greedy` (4) **pass**. GitLab pipeline **2802987818** failed with **`ci_quota_exceeded`** on every job (including gitleaks) — infra, not a test signal. --- ## Gap analysis **Features vs #708** Shipped: opt-in pair/router greedy, G3 Decimal pin, wasm `greedy_stop`, G13 gas, skill + ADR/docs. Honored out-of-scope: do not flip `hybrid: None`; do not take Swap/Trade off `/route/solve`; no on-chain split search; no auto hint. Deferred (MR “Not in this MR”, but issue still wants them before calling #708 done): - Owner **opt-out default** (PlasticDigits note). Keeping G1 is the right security call (15M gas + expired-head grief + L8). **Do not close #708 as “bots get greedy without changing JSON”** — they still pool-dump until they opt in **and** pair wasm is migrated. - **G14 ops:** factory `pair_code_id` migrate, LocalTerra canary, columbus-5 canary. Until then live `greedy` **fails deserialize** on old pair wasm. - Reverse greedy simulation. - Indexer `greedy_stop` / metric “share of pair-direct txs with book leg” cannot tell greedy from Pattern C. **UI/UX** No retail greedy toggle. Types + gas only. Integrators get one paragraph in `docs/integrators.md` — no LCD JSON cookbook, no `simulateGreedySwap` in `pair.ts`. Fine vs “no retail requirement”; thin for the integrators label. **DRY / quality** Greedy gate copied 4× in `match_*` / `simulate_match_*` (issue asked for a pre-pass then existing matcher — G10 reuse is good, duplication is not). `constant_product_net_out` is dead. Pair `InvalidHybridParams` Display drops `{reason}` — greedy `max_maker_fills == 0` shows as generic “Invalid hybrid parameters”. **Gas** Mapping is conservative (always makers + pool leg + scan overhead). Correct vs 600k; not tight. No wasm-size check. **Packages** 36 files. Community-tax / blacklist / migration `greedy: None` literals are **serde/struct fallout**, not tax-behavior changes. AutoLP skim stays pool-only. **What a user might still expect** Copy-paste LCD execute/query; SDK helper; reverse quote; `makers_used` on sim (only `limit_book_offer_consumed` + `greedy_stop`); indexer attr; LocalTerra proof; dApp badge (correctly out of scope). --- ## Suggested patch order 1. Pair query: `resolve_swap_hybrid_mode(hybrid, greedy)` — reject both (align with execute + router). 2. Fix `greedy_stop` enum/fallback + remainder test assert. 3. `checked_from_ratio` in `pool_spot_net`. 4. Tests: router greedy hop vs `hybrid: null`; `hybrid`+`greedy` query error; community-tax greedy sell extra-debit; pause/blacklist; expired-prefix scan cap; `return_amount ≥ pool-only`; equal-rate G3 unit test. 5. Either implement reverse greedy or make reverse-sim **error** on `greedy` (do not quote pool-only). 6. Split follow-ups from `Closes #708`: owner default, pair migrate/canary, indexer `greedy_stop`, reverse sim if deferred. Happy-path design (G1 opt-in, G3 rate stop, G8, G11 serde shape, G13 gas) is in good shape. The mutex/attr/panic issues plus the open #708 checklist are why this is **FIX**, not **ACCEPT**.
PlasticDigits commented 2026-08-30 10:20:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #709

mentioned in issue #709
PlasticDigits commented 2026-08-30 10:20:12 +00:00 (Migrated from gitlab.com)

mentioned in issue #710

mentioned in issue #710
PlasticDigits commented 2026-08-30 11:08:18 +00:00 (Migrated from gitlab.com)

added 1 commit

  • 83bccc28 - Fix greedy query mutex, remainder_to_pool, and leftover tests (#709 #710).

Compare with previous version

added 1 commit <ul><li>83bccc28 - Fix greedy query mutex, remainder_to_pool, and leftover tests (#709 #710).</li></ul> [Compare with previous version](/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/480/diffs?diff_id=1992627670&start_sha=f0d0c2d5e36157fb9767099ecd5a5c4bb8d02adb)
PlasticDigits commented 2026-08-30 11:09:50 +00:00 (Migrated from gitlab.com)

changed title from feat: opt-in greedy book-first swap (#708) to feat: opt-in greedy book-first swap (#708, leftovers #709 #710)

<p>changed title from <code class="idiff">feat: opt-in greedy book-first swap (#708)</code> to <code class="idiff">feat: opt-in greedy book-first swap (#708<span class="idiff left right addition">, leftovers #709 #710</span>)</code></p>
PlasticDigits commented 2026-08-30 11:09:50 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-31 04:17:47 +00:00 (Migrated from gitlab.com)

RECOMMEND: ACCEPT

Reviewed !1198 (feat/708-greedy-book-first, 83bccc28) as the MR that implements #708 plus leftovers #709 / #710. The prior !1198 review (RECOMMEND: FIX on f0d0c2d5) listed three merge blockers; those are fixed in this HEAD. Opt-in greedy book-first is real, mutex-aligned on quote and execute, and covered by pause / blacklist / tax / AfterSwap tests. No fund-theft, privilege-escalation, or prompt-injection path turned up.

This is not a “flip default to greedy” or columbus-5 pair migrate. Official dApp stays on GET /route/solve. Remaining items below are should-fix / follow-up, not merge blockers.


Prior must-fixes (status)

Prior blocker Status Evidence
Pair HybridSimulation accepted hybrid + greedy; execute rejected (G11 / A11) Fixed simulate_hybrid_swap_with_fee calls resolve_swap_hybrid_mode(hybrid, greedy) on both fields (pair/src/contract.rs). Tests: greedy_query_both_hybrid_and_greedy_errors, greedy_and_hybrid_mutex, greedy_router_sim_both_hybrid_and_greedy_errors.
greedy_stop=empty after a real book fill + pool remainder Fixed GreedyStopReason::RemainderToPool / wire remainder_to_pool. Test: greedy_better_bid_then_pool_remainder.
Decimal::from_ratio panic on greedy pool spot (A7) Fixed pool_spot_net uses checked_from_ratio; overflow → Skip. Test: pool_spot_overflow_is_skip_not_panic. Dead constant_product_net_out is gone.

Ran on this checkout (all green):

  • cargo test -p dex-common greedy_swap — 9 passed
  • cargo test -p cl8y-dex-pair greedy — 9 passed
  • cargo test -p cl8y-dex-tests greedy_book_first_708 — 21 passed
  • cargo test -p cl8y-dex-tests greedy_blacklist — 2 passed
  • cargo test -p cl8y-community-tax-token greedy — 3 passed

Frontend greedy vitest was not re-run here (this worktree has no frontend-dapp/node_modules). Tests exist: terraGas.greedy.test.ts, hybridSwapGas.test.ts.


Security audit

Area Result
Access control No new admin. Direct ExecuteMsg::Swap still rejected. trader still router-stamped only (A9 / A14). Pair-direct spoof tested (greedy_pair_direct_trader_spoof_does_not_steal_fee_tier, tax greedy_pair_direct_trader_spoof_extra_debits_from).
G1 default hybrid: None + greedy: None → PoolOnly. TerraSwap callers unchanged. Test: g1_pool_only_skips_better_bid.
A1 wrong-side hint L17 fallback. Test: greedy_wrong_side_hint_falls_back_to_head.
A2 stale / cancelled hint Missing / wrong-side / missing-id → head. A live same-side stale hint still starts there; greedy stop can dump the whole offer to the AMM (documented G6 integrator risk). No greedy multitest for live-stale / cancelled-id.
A3 expired-prefix grief Caps reused (MAX_SCAN_STEPS / park 15). No greedy-specific scan_cap / expired-head multitest (unit priority only).
A4 worse-than-pool Strict Decimal net; equal stops. Better than Pattern C book_input = offer. Tests: greedy_worse_or_equal_bid_does_not_fill, greedy_mixed_prefix_stops_at_worse, greedy_does_not_match_declared_full_book_on_equal_price.
A5 sandwich / oracle No external oracle. Compare uses this-tx RESERVES before the walk. Book fills do not move AMM reserves, so the pool spot is stable for the walk (G3 / G4: not a solver). G8 required. max_spread is inert on pure-book (declared_pool_input = 0); greedy starts pool_leg = 0 then remainder hits the AMM, so residual pool still goes through existing CP + assert_max_spread when a pool leg exists. Tight min_return is the real floor on a full-book consume. Residual MEV, not a new trust boundary.
A6 / A7 L18 cost == 0 skip before greedy compare. Maker overflow skip unchanged. Pool-spot overflow → Skip, no VM panic. Residual: Skip-all-makers on unpriceable pool spot can walk to scan cap then remainder to pool (fail-closed vs filling worse makers).
A8 0 reject, clamp 100. Dust can still exhaust cap; G8 if tight.
A10 schema Distinct GreedySwapParams + tests that Pattern C JSON does not decode as greedy.
A11 quote vs execute Pair query mutex fixed. Router forward sim rejects both. Router reverse sim still ignores greedy (quotes pool-only / declared hybrid) — documented G12, A11-adjacent footgun (see should-fix).
A12 pause / blacklist Pause on Receive (assert_not_paused). Tests: greedy_paused_execute_rejects_then_unpause_fills; greedy_blacklist_maker_better_bid_not_filled; greedy_blacklist_taker_rejects.
A13 re-entrancy Same settlement-then-hooks as Pattern C. AfterSwap amounts covered; no greedy AfterSwap re-enter PoC (adversarial_token still greedy: None). Optional on #710.
A14 fee-tier Pair-direct ignores spoofed trader. Covered.
Community tax is_swap_send_hook matches any Cw20HookMsg::Swap. Pair-direct extra-debits from; router extra-debits authenticated trader (T592-13). Tests: greedy_sell_extra_debit_on_swap_send, greedy_router_sell_extra_debits_authenticated_trader. No greedy buy analogue (listed as not-in-MR).
k / fees / TWAP Book does not touch RESERVES; remainder uses existing CP + new_k >= k. TWAP still before reserve change. AfterSwap return_asset = book net + pool net (wasm attrs + treasury; not a hook-mock).
Tokenomic / interior split Greedy can lose to /route/solve (G4, documented). Official dApp stays on the solver.
Database / indexer leaks No indexer changes. greedy_stop is not ingested. No new SQL, migrations, or secret/log surface. Unknown wasm attrs ignored. Not a leak; bots must parse raw events if they want stop reasons.
Rust server Indexer / API unchanged. N/A except that solver is not replaced by greedy.
Gas grief Caps 100 / 500 / 15 parks. dApp + swarm map greedy to the hybrid book-walk envelope, not silent 600k (G13). Third-party wallets without the map can still OOG (pre-#475 class).
Prompt injection None.

Not BLOCK: no validated escrow theft, maker drain below pool via greedy, tax-subject = router, pause bypass, or auth bypass.


#708 / #709 / #710 acceptance

GitLab issue checkboxes are still unchecked. Mapping against this diff + tests:

#708

Criterion Verdict
hybrid: None never reads the book Pass
No pool_input / book_input on greedy Pass
Better book → fill + remainder; return_amount ≥ pool-only Partial — fill + remainder_to_pool tested; no ≥ pool-only control, k, or treasury = pool + book half as a dedicated assert (AfterSwap/treasury covers the split loosely)
Worse book → 100% pool Pass
Mixed prefix, stop before worse tail Pass (stop attr not asserted on mixed)
Caps; OOG not the bound Partial — fills=1 / 0 / clamp; no greedy MAX_SCAN_STEPS / park-cap multitest
Wrong-side / missing hint → head Partial — wrong-side + None; no stale/cancelled-id
Quote = execute; expired-head drift documented Partial — live parity + matching greedy_stop; drift is L8/hybrid docs, not a greedy expired-prefix test
G8 neither floor reverts Pass on pair (greedy_requires_slippage_floor)
Pause / blacklist / fee-tier / tax / AfterSwap L7 Pass via #710
Pattern C still works Pass (pattern_c_sim_omits_greedy_stop; mutex tests)
Gas mapped; unmapped throws Partial — dApp vitest + swarm gas.ts; unmapped throw is the existing #475 path, not a greedy-specific throw
ADR / skill / make verify-issue-708 Pass
LocalTerra canary Missing (explicit follow-up; verify scripts skip chain)

Attack plan: A1, A4, A8, A10, A11 (forward), A12, A14 tested. A2, A3, A13 missing on greedy. A5 residual by design. A7 unit-only. A11 reverse still silent.

Functional paths 1–18: 1–9, 14–16, 18 strongest. 10 partial, 11 / 12 / 17 thin (no plain router execute greedy-vs-null hop; tax router covers execute tax only).

#709

Query both-fields error, remainder_to_pool, filled, empty, worse_than_pool, equal-rate stop, overflow Skip: Pass. Router execute mutex has no dedicated multitest (sim + pair execute covered). Overflow has no execute/query multitest on absurd reserves.

#710

Tax pair-direct + router trader, pause, blacklist maker/taker, trader spoof: Pass. AfterSwap L7: Partial (wasm return_amount / commissions / treasury / no park — not a hook-mock like Pattern C hybrid_hook_commission_includes_pool_and_book). G1 on tax fixture omitted-greedy: Partial (generic G1 only). A13 optional: Missing.

Tick the GitLab boxes that this MR actually landed so Closes #708/#709/#710 does not auto-close tickets whose remaining rows (LocalTerra, migrate, opt-out default, reverse greedy, buy-tax analogue) are still open follow-ups.


Gap analysis

Features / UX

  • Integrators get a simple opt-in (max_maker_fills + hint) without inventing a Pattern C split — matches the #708 design.
  • Official Swap/Trade does not expose greedy (correct per G4 / #596).
  • Reverse / exact-out greedy is unsupported. Pair HybridReverseSimulation has no greedy field. Router ReverseSimulateSwapOperations discards greedy (TerraSwap { hybrid, .. }) and quotes pool-only when hybrid is unset. Docs (integrators.md, skill G12) say this; docs/limit-orders.md router paragraph still only describes hybrid unset → pool-only. A bot that reverse-sims a greedy hop gets a number (pool-only offer), then execute greedy can oversize the offer. Inefficiency, not theft.
  • greedy_stop=worse_than_pool can still dump remainder to the AMM after fills — stop reason ≠ “no remainder”. Bots should key remainder_to_pool vs worse_than_pool carefully.
  • Indexer will not surface greedy_stop / book-first attribution until a follow-up parser change.

Should-fix (cheap; not blocking)

Item Why
Router reverse-sim: error if greedy.is_some() Same family as forward mutex. Avoid silent pool-only numbers on a greedy-tagged hop.
Router G8 LCD copy uses dummy book_input = 1 validate_greedy_hop_execute_slippage_floor → “Hybrid swap with book_input 1 requires per-hop min_return”. Greedy hops have no book_input. Pair path uses real input_amount.
limit-orders.md router reverse paragraph Mention greedy is ignored / unsupported so readers of that section alone do not miss it.
Assert return ≥ pool-only on remainder test #708 AC wording; cheap control swap.
Greedy scan_cap / live-stale hint / router execute hop Paths 10, 11, 17 / A2 / A3.

Nice-to-have

  • DRY the four identical greedy_pool match arms in orderbook.rs.
  • AfterSwap hook-mock (parity with Pattern C L7 test).
  • A13 greedy re-enter; community-tax greedy buy.
  • Swarm gas.ts unit test (mapping is present).
  • Indexer ingest of greedy_stop (optional per #708).
  • Pair wasm migrate on columbus-5 (G14 ops).
  • Owner note “opt-out default” — still a follow-up, correctly not this MR.

Testing / e2e

  • Unit + multitest: strong for the new path (see runs above). Verify scripts: make verify-issue-708 / 709 / 710 (docs grep + cargo; no LocalTerra).
  • E2E / Playwright: none for greedy (out of #710 scope; official UI does not send greedy).
  • Happy path: empty book, better bid/ask, remainder, mixed prefix, full consume, caps, G8, quote=execute.
  • Bad path: mutex both fields, zero fills, pause, blacklist maker/taker, trader spoof, tax extra-debit, overflow Skip.

Packages / DRY / readability / gas / best practices

  • Reuses match_bids / match_asks / simulate_match_* (G10) — right CosmWasm approach vs a second walker.
  • Decimal rates avoid 1-raw-unit CP floor-to-zero on large reserves (G3). checked_* on pool spot.
  • fee_keep still uses from_ratio for bps ∈ [0, 10000] — safe.
  • Gas: worst-case stays in the existing hybrid 15M envelope; dApp maps greedy so it does not fall through to 600k.
  • Pattern C JSON omits greedy_stop (skip_serializing_if) — G14.
  • Router first hop saves SWAP_STATE then validates greedy; CosmWasm atomicity rolls back on error (hop N validates before save — slightly cleaner).

What a user might expect that is missing

  • Exact-out / reverse greedy.
  • Retail UI toggle (intentionally omitted).
  • Indexer dashboard of “book-first vs pool-only” fills.
  • Guarantee of solver-quality fills (docs correctly say greedy ≤ /route/solve).
  • Auto-derived book_start_hint (forbidden by G6).

Verdict

Merge-ready for the opt-in wasm + tests + docs scope. Prior quote/execute mutex, empty stop-reason, and pool-spot panic are gone. Tick issue checkboxes (or leave LocalTerra / migrate / opt-out / reverse-reject as open children) so GitLab auto-close matches reality. Land reverse-sim reject + G8 error copy in a small follow-up if not squeezed in here.

RECOMMEND: ACCEPT Reviewed **!1198** (`feat/708-greedy-book-first`, `83bccc28`) as the MR that implements **#708** plus leftovers **#709** / **#710**. The prior !1198 review (`RECOMMEND: FIX` on `f0d0c2d5`) listed three merge blockers; those are **fixed** in this HEAD. Opt-in greedy book-first is real, mutex-aligned on quote and execute, and covered by pause / blacklist / tax / AfterSwap tests. **No fund-theft, privilege-escalation, or prompt-injection path** turned up. This is **not** a “flip default to greedy” or columbus-5 pair migrate. Official dApp stays on `GET /route/solve`. Remaining items below are **should-fix / follow-up**, not merge blockers. --- ## Prior must-fixes (status) | Prior blocker | Status | Evidence | |---------------|--------|----------| | Pair `HybridSimulation` accepted `hybrid` + `greedy`; execute rejected (**G11** / **A11**) | **Fixed** | `simulate_hybrid_swap_with_fee` calls `resolve_swap_hybrid_mode(hybrid, greedy)` on **both** fields (`pair/src/contract.rs`). Tests: `greedy_query_both_hybrid_and_greedy_errors`, `greedy_and_hybrid_mutex`, `greedy_router_sim_both_hybrid_and_greedy_errors`. | | `greedy_stop=empty` after a real book fill + pool remainder | **Fixed** | `GreedyStopReason::RemainderToPool` / wire `remainder_to_pool`. Test: `greedy_better_bid_then_pool_remainder`. | | `Decimal::from_ratio` panic on greedy pool spot (**A7**) | **Fixed** | `pool_spot_net` uses `checked_from_ratio`; overflow → `Skip`. Test: `pool_spot_overflow_is_skip_not_panic`. Dead `constant_product_net_out` is **gone**. | Ran on this checkout (all green): - `cargo test -p dex-common greedy_swap` — 9 passed - `cargo test -p cl8y-dex-pair greedy` — 9 passed - `cargo test -p cl8y-dex-tests greedy_book_first_708` — 21 passed - `cargo test -p cl8y-dex-tests greedy_blacklist` — 2 passed - `cargo test -p cl8y-community-tax-token greedy` — 3 passed Frontend greedy vitest was **not** re-run here (this worktree has no `frontend-dapp/node_modules`). Tests exist: `terraGas.greedy.test.ts`, `hybridSwapGas.test.ts`. --- ## Security audit | Area | Result | |------|--------| | **Access control** | No new admin. Direct `ExecuteMsg::Swap` still rejected. `trader` still router-stamped only (**A9** / **A14**). Pair-direct spoof tested (`greedy_pair_direct_trader_spoof_does_not_steal_fee_tier`, tax `greedy_pair_direct_trader_spoof_extra_debits_from`). | | **G1 default** | `hybrid: None` + `greedy: None` → `PoolOnly`. TerraSwap callers unchanged. Test: `g1_pool_only_skips_better_bid`. | | **A1 wrong-side hint** | L17 fallback. Test: `greedy_wrong_side_hint_falls_back_to_head`. | | **A2 stale / cancelled hint** | Missing / wrong-side / missing-id → head. A **live same-side** stale hint still starts there; greedy **stop** can dump the whole offer to the AMM (documented **G6** integrator risk). **No greedy multitest** for live-stale / cancelled-id. | | **A3 expired-prefix grief** | Caps reused (`MAX_SCAN_STEPS` / park 15). **No greedy-specific** `scan_cap` / expired-head multitest (unit priority only). | | **A4 worse-than-pool** | Strict Decimal net; equal stops. Better than Pattern C `book_input = offer`. Tests: `greedy_worse_or_equal_bid_does_not_fill`, `greedy_mixed_prefix_stops_at_worse`, `greedy_does_not_match_declared_full_book_on_equal_price`. | | **A5 sandwich / oracle** | **No external oracle.** Compare uses this-tx `RESERVES` before the walk. Book fills do not move AMM reserves, so the pool spot is stable for the walk (G3 / G4: not a solver). G8 required. `max_spread` is inert on **pure-book** (`declared_pool_input = 0`); greedy starts `pool_leg = 0` then remainder hits the AMM, so residual pool still goes through existing CP + `assert_max_spread` when a pool leg exists. Tight `min_return` is the real floor on a full-book consume. Residual MEV, not a new trust boundary. | | **A6 / A7** | L18 `cost == 0` skip before greedy compare. Maker overflow skip unchanged. Pool-spot overflow → Skip, no VM panic. Residual: Skip-all-makers on unpriceable pool spot can walk to scan cap then remainder to pool (fail-closed vs filling worse makers). | | **A8** | 0 reject, clamp 100. Dust can still exhaust cap; G8 if tight. | | **A10 schema** | Distinct `GreedySwapParams` + tests that Pattern C JSON does not decode as greedy. | | **A11 quote vs execute** | Pair query mutex **fixed**. Router **forward** sim rejects both. Router **reverse** sim still **ignores** `greedy` (quotes pool-only / declared hybrid) — documented **G12**, A11-adjacent footgun (see should-fix). | | **A12 pause / blacklist** | Pause on `Receive` (`assert_not_paused`). Tests: `greedy_paused_execute_rejects_then_unpause_fills`; `greedy_blacklist_maker_better_bid_not_filled`; `greedy_blacklist_taker_rejects`. | | **A13 re-entrancy** | Same settlement-then-hooks as Pattern C. AfterSwap amounts covered; **no** greedy AfterSwap re-enter PoC (`adversarial_token` still `greedy: None`). Optional on #710. | | **A14 fee-tier** | Pair-direct ignores spoofed `trader`. Covered. | | **Community tax** | `is_swap_send_hook` matches any `Cw20HookMsg::Swap`. Pair-direct extra-debits `from`; router extra-debits authenticated `trader` (**T592-13**). Tests: `greedy_sell_extra_debit_on_swap_send`, `greedy_router_sell_extra_debits_authenticated_trader`. No greedy **buy** analogue (listed as not-in-MR). | | **k / fees / TWAP** | Book does not touch `RESERVES`; remainder uses existing CP + `new_k >= k`. TWAP still before reserve change. AfterSwap `return_asset` = book net + pool net (wasm attrs + treasury; not a hook-mock). | | **Tokenomic / interior split** | Greedy can lose to `/route/solve` (**G4**, documented). Official dApp stays on the solver. | | **Database / indexer leaks** | **No indexer changes.** `greedy_stop` is **not** ingested. No new SQL, migrations, or secret/log surface. Unknown wasm attrs ignored. Not a leak; bots must parse raw events if they want stop reasons. | | **Rust server** | Indexer / API unchanged. N/A except that solver is **not** replaced by greedy. | | **Gas grief** | Caps 100 / 500 / 15 parks. dApp + swarm map greedy to the hybrid book-walk envelope, not silent 600k (**G13**). Third-party wallets without the map can still OOG (pre-#475 class). | | **Prompt injection** | None. | Not **BLOCK**: no validated escrow theft, maker drain below pool via greedy, tax-subject = router, pause bypass, or auth bypass. --- ## #708 / #709 / #710 acceptance GitLab issue **checkboxes are still unchecked**. Mapping against this diff + tests: ### #708 | Criterion | Verdict | |-----------|---------| | `hybrid: None` never reads the book | **Pass** | | No `pool_input` / `book_input` on greedy | **Pass** | | Better book → fill + remainder; `return_amount` ≥ pool-only | **Partial** — fill + `remainder_to_pool` tested; **no** ≥ pool-only control, k, or treasury = pool + book half as a dedicated assert (AfterSwap/treasury covers the split loosely) | | Worse book → 100% pool | **Pass** | | Mixed prefix, stop before worse tail | **Pass** (stop attr not asserted on mixed) | | Caps; OOG not the bound | **Partial** — fills=1 / 0 / clamp; **no** greedy `MAX_SCAN_STEPS` / park-cap multitest | | Wrong-side / missing hint → head | **Partial** — wrong-side + `None`; **no** stale/cancelled-id | | Quote = execute; expired-head drift documented | **Partial** — live parity + matching `greedy_stop`; drift is L8/hybrid docs, not a greedy expired-prefix test | | G8 neither floor reverts | **Pass** on pair (`greedy_requires_slippage_floor`) | | Pause / blacklist / fee-tier / tax / AfterSwap L7 | **Pass** via #710 | | Pattern C still works | **Pass** (`pattern_c_sim_omits_greedy_stop`; mutex tests) | | Gas mapped; unmapped throws | **Partial** — dApp vitest + swarm `gas.ts`; unmapped throw is the existing #475 path, not a greedy-specific throw | | ADR / skill / `make verify-issue-708` | **Pass** | | LocalTerra canary | **Missing** (explicit follow-up; verify scripts skip chain) | **Attack plan:** A1, A4, A8, A10, A11 (forward), A12, A14 tested. **A2, A3, A13 missing** on greedy. A5 residual by design. A7 unit-only. A11 reverse still silent. **Functional paths 1–18:** 1–9, 14–16, 18 strongest. **10 partial, 11 / 12 / 17 thin** (no plain router execute greedy-vs-null hop; tax router covers execute tax only). ### #709 Query both-fields error, remainder_to_pool, filled, empty, worse_than_pool, equal-rate stop, overflow Skip: **Pass**. Router **execute** mutex has no dedicated multitest (sim + pair execute covered). Overflow has **no** execute/query multitest on absurd reserves. ### #710 Tax pair-direct + router trader, pause, blacklist maker/taker, trader spoof: **Pass**. AfterSwap L7: **Partial** (wasm `return_amount` / commissions / treasury / no park — not a hook-mock like Pattern C `hybrid_hook_commission_includes_pool_and_book`). G1 on tax fixture omitted-greedy: **Partial** (generic G1 only). A13 optional: **Missing**. Tick the GitLab boxes that this MR actually landed so `Closes #708/#709/#710` does not auto-close tickets whose remaining rows (LocalTerra, migrate, opt-out default, reverse greedy, buy-tax analogue) are still open follow-ups. --- ## Gap analysis ### Features / UX - Integrators get a simple opt-in (`max_maker_fills` + hint) without inventing a Pattern C split — matches the #708 design. - Official Swap/Trade **does not** expose greedy (correct per **G4** / **#596**). - Reverse / exact-out greedy is **unsupported**. Pair `HybridReverseSimulation` has no `greedy` field. Router `ReverseSimulateSwapOperations` **discards** `greedy` (`TerraSwap { hybrid, .. }`) and quotes pool-only when `hybrid` is unset. Docs (`integrators.md`, skill G12) say this; `docs/limit-orders.md` router paragraph still only describes `hybrid` unset → pool-only. A bot that reverse-sims a greedy hop **gets a number** (pool-only offer), then execute greedy can oversize the offer. Inefficiency, not theft. - `greedy_stop=worse_than_pool` can still dump remainder to the AMM after fills — stop reason ≠ “no remainder”. Bots should key `remainder_to_pool` vs `worse_than_pool` carefully. - Indexer will not surface `greedy_stop` / book-first attribution until a follow-up parser change. ### Should-fix (cheap; not blocking) | Item | Why | |------|-----| | Router reverse-sim: **error** if `greedy.is_some()` | Same family as forward mutex. Avoid silent pool-only numbers on a greedy-tagged hop. | | Router G8 LCD copy uses dummy `book_input = 1` | `validate_greedy_hop_execute_slippage_floor` → “Hybrid swap with book_input 1 requires per-hop min_return”. Greedy hops have no `book_input`. Pair path uses real `input_amount`. | | `limit-orders.md` router reverse paragraph | Mention greedy is ignored / unsupported so readers of that section alone do not miss it. | | Assert `return ≥ pool-only` on remainder test | #708 AC wording; cheap control swap. | | Greedy `scan_cap` / live-stale hint / router execute hop | Paths 10, 11, 17 / A2 / A3. | ### Nice-to-have - DRY the four identical `greedy_pool` match arms in `orderbook.rs`. - AfterSwap hook-mock (parity with Pattern C L7 test). - A13 greedy re-enter; community-tax greedy **buy**. - Swarm `gas.ts` unit test (mapping is present). - Indexer ingest of `greedy_stop` (optional per #708). - Pair wasm migrate on columbus-5 (**G14** ops). - Owner note “opt-out default” — still a **follow-up**, correctly not this MR. ### Testing / e2e - **Unit + multitest:** strong for the new path (see runs above). Verify scripts: `make verify-issue-708` / `709` / `710` (docs grep + cargo; **no LocalTerra**). - **E2E / Playwright:** none for greedy (out of #710 scope; official UI does not send greedy). - **Happy path:** empty book, better bid/ask, remainder, mixed prefix, full consume, caps, G8, quote=execute. - **Bad path:** mutex both fields, zero fills, pause, blacklist maker/taker, trader spoof, tax extra-debit, overflow Skip. ### Packages / DRY / readability / gas / best practices - Reuses `match_bids` / `match_asks` / `simulate_match_*` (**G10**) — right CosmWasm approach vs a second walker. - Decimal rates avoid 1-raw-unit CP floor-to-zero on large reserves (**G3**). `checked_*` on pool spot. - `fee_keep` still uses `from_ratio` for bps ∈ [0, 10000] — safe. - Gas: worst-case stays in the existing hybrid 15M envelope; dApp maps greedy so it does not fall through to 600k. - Pattern C JSON omits `greedy_stop` (`skip_serializing_if`) — **G14**. - Router first hop saves `SWAP_STATE` then validates greedy; CosmWasm atomicity rolls back on error (hop N validates before save — slightly cleaner). ### What a user might expect that is missing - Exact-out / reverse greedy. - Retail UI toggle (intentionally omitted). - Indexer dashboard of “book-first vs pool-only” fills. - Guarantee of solver-quality fills (docs correctly say greedy ≤ `/route/solve`). - Auto-derived `book_start_hint` (forbidden by **G6**). --- ## Verdict Merge-ready for the **opt-in wasm + tests + docs** scope. Prior quote/execute mutex, `empty` stop-reason, and pool-spot panic are gone. Tick issue checkboxes (or leave LocalTerra / migrate / opt-out / reverse-reject as open children) so GitLab auto-close matches reality. Land reverse-sim reject + G8 error copy in a small follow-up if not squeezed in here.
PlasticDigits commented 2026-08-31 05:07:34 +00:00 (Migrated from gitlab.com)

added 5 commits

  • 83bccc28...8491aad6 - 4 commits from branch main
  • 56e4c4f5 - Merge origin/main into feat/708-greedy-book-first.

Compare with previous version

added 5 commits <ul><li>83bccc28...8491aad6 - 4 commits from branch <code>main</code></li><li>56e4c4f5 - Merge origin/main into feat/708-greedy-book-first.</li></ul> [Compare with previous version](/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/480/diffs?diff_id=1993204658&start_sha=83bccc28386660222aebde56a127f4403f834669)
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) merged commit f54de4fd4b into main 2026-08-31 05:07:56 +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:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #708

mentioned in issue #708
Sign in to join this conversation.
No reviewers
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!1198
No description provided.