design: greedy book-first swap variant (hint + max_maker_fills, no caller split) #708
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#708
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add an opt-in pair swap variant that fills greedy book-first-then-pool in-contract: the caller supplies only
book_start_hint+max_maker_fills(nopool_input/book_input). The pair walks the top of book while each live level beats the pool's current marginal price, capped atmax_maker_fills(and existing scan/park caps), then routes the remainder to the AMM.This is the gas-efficient on-chain hybrid approved from #704. It is not a change to the default
hybrid: Nonepath (that stays pool-only). Full split optimization stays off-chain (#501 / indexerGET /route/solve).Sibling (docs now, no wasm): integrator guidance that direct
pair.swapis pool-only — see the linked docs issue from #704.Current codebase
Pattern C is caller-declared. The pair never chooses a pool/book split.
Pair execute
smartcontracts/contracts/pair/src/contract.rsexecute_swap(~L968):When
book_leg > 0, the pair callsorderbook::match_bidsormatch_askswith the declaredbook_input, then rolls unfilled book remainder into the pool:pool_input_amount = pool_leg + (book_leg − offer_consumed_by_book)(~L1131).That roll-forward is not “stop when the next maker is worse than the pool.” A caller who sets
pool_input = 0,book_input = offerwill take worse-than-pool resting orders until the budget,max_maker_fills, orMAX_SCAN_STEPSis exhausted. #307 also requires a material pool leg (orbelief_price/min_return) for declared hybrid.Types
smartcontracts/packages/dex-common/src/pair.rs—HybridSwapParams { pool_input, book_input, max_maker_fills, book_start_hint }. Caps:MAX_MAKER_FILLS_HARD_CAP = 100,MAX_SCAN_STEPS = 500.Cw20HookMsg::Swap.hybrid: Option<HybridSwapParams>. Quotes:HybridSimulation/HybridReverseSimulationonly (legacySimulationremoved, L8 / #190).Book walk
smartcontracts/contracts/pair/src/orderbook.rs—match_bids/match_asks/simulate_match_*.book_start_hintis permissionless; wrong-side / stale / missing → fall back to head (L17 / #272). Do not auto-derive the hint on-chain (contrast insert-thread #266). Indexerglobal_v2already emits the first live same-side id when the mirror is fresh (#332).Router
smartcontracts/contracts/router/src/contract.rs—SwapOperation::TerraSwap { hybrid: Option<HybridSwapParams> }.hybrid: Nonemaps topool_only_hybrid_paramsfor quotes; execute still pool-only on that hop.Off-chain best execution (already exists)
GET /api/v1/route/solve+amount_in= global path + joint hybrid grid (#209, ADR 0002). Interior splits can beat naive book-first (~+0.07% / ~+0.4% on observed EMBER sizes in #704 discussion).send→swapwith those params (swapRouting.ts).Observed gap
Bots that omit
hybrid(columbus-5 CL8Y-cb/cLUNC ~85 swaps/day in #704) fill 100% against the pool even when the best ask is strictly better. Arb may correct later; that taker still overpays and the maker never sees the flow.Why this is needed
Not every integrator can (or will) run the indexer solver. Asking them to invent
pool_input/book_inputis the hard part. A bounded greedy walk:order-book-head/ indexer hint);hybrid: Noneunchanged);This is not global best execution. Greedy book-first can lose to an interior split. The indexer remains the price-optimal path. This variant is a better-than-pool-only floor for simple bots.
Constraints / guardrails
Invariants G1–G14. Do not ship a variant that cannot satisfy them.
hybrid: None/ omitted onCw20HookMsg::Swapand routerTerraSwapmust remain pool-only (book_leg = 0). No silent book walk on TerraSwap-compatible callers.pool_input/book_input. Caller fields: offer (CW20amount),max_maker_fills, optionalbook_start_hint, plus existing slippage (belief_price/max_spread/min_return),to,deadline,trader.effective_fee_bps). Next worse-or-equal level → stop book, remainder → pool. Empty book / all worse → 100% pool. Design must pin “beats” (gross vs net of taker commission; 1-unit marginal vs this order’s fill).max_maker_fillsclamped withMAX_MAKER_FILLS_HARD_CAP(100). Walk still bounded byMAX_SCAN_STEPS(500) andMAX_EXPIRED_PARKS_PER_SWAP(15).max_maker_fills == 0→ reject (or treat as pool-only — pick one, test it). Oversize fills clamp, do not panic.book_start_hintin the pair (no #266 insert-thread on swap). Missing/stale/wrong-side → head fallback (L17). Integrators should pass first live same-side id (#289 expired prefix).HybridSimulationor a sibling) must use the same greedy rule as execute. Queries remain read-only (no parks). Execute may still park expired/dust; document sim vs exec drift for expired heads.belief_priceormin_return(same family as #307 / #334). Do not invent a path that walks the book with neither.max_spreadL9 applies to the realized pool+book total.trader(router-stamped only), community-tax extra-debit (T592-13) stay on the existingmatch_bids/match_askspath. Do not fork a second matcher.HybridSwapParamsJSON cannot be misread as greedy. Do not overloadpool_input=0, book_input=offer(that already means “take book even if worse”).hybrid: nullstays pool-only. Do not auto-upgrade router hops to greedy. Official dApp does not have to switch off the solver.HYBRID_SWAP_GAS_LIMIT(15M) and existing book-walk overhead. MapgetGasLimitForTx/ swarm gas if a new send-hook string appears (#475). Unmapped hooks must throw, not silent 600k.pair_code_id). No instantiate-msg break. Existing Pattern C execute/sim tests must stay green. Metric: share of pair-direct txs withbook_leg_volume > 0after adoption (indexer columns already exist).Out of scope: changing
hybrid: None; forcing greedy on the official UI; on-chain split optimizer; auto hint from storage; other-DEX hops (#690).Relevant files
Contracts:
smartcontracts/packages/dex-common/src/pair.rs·smartcontracts/contracts/pair/src/{contract,orderbook,error,lib}.rs·smartcontracts/packages/dex-common/src/max_spread.rs·smartcontracts/contracts/router/src/{msg,contract}.rs·smartcontracts/tests/src/limit_order_tests.rs(+ hybrid / hint tests)Indexer (optional follow-through, same issue if small):
indexer/src/api/{route_solver,hybrid_route_opt}.rs— do not replace GET best-execution with greedy; optional document/flag only.Frontend: no retail requirement. If a new hook string exists, gas maps in
frontend-dapp/src/services/terraclassic/{terraGas,hybridSwapGas}.ts.Docs / skills:
docs/limit-orders.md·docs/integrators.md·docs/contracts-security-audit.md(new G* / L* row) ·docs/adr/0001-hybrid-quoting-and-routing.md·skills/AGENTS_HYBRID_QUOTING.md·skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md·skills/AGENTS_TERRACLASSIC_GAS.mdRelated: #704 (parent) · #501 / #596 (solver UI) · #262 / #254 (caps) · #272 (hint side) · #307 (pool floor) · #190 (L8)
Recommended direction
Cw20HookMsgsibling vsenum Hybrid { Declared(HybridSwapParams), Greedy { max_maker_fills, book_start_hint } }.makers_used,offer_consumed_by_book, stop reason).book_input_effective/ stop, then calls existingmatch_*with that budget — avoid duplicating fill accounting.greedy_stop=worse_than_pool|max_makers|scan_cap|empty|filled) for indexer/debug.greedyonTerraSwap; null remains pool-only./route/solve.Acceptance criteria
hybrid: Nonestill never reads the book (regression tests).pool_input/book_input(not in schema, or explicit error if a confused payload is sent).max_maker_fills; remainder goes to pool; takerreturn_amount≥ pool-only on the same offer (after fees; equality only if book not used).hybrid: Noneeconomically).max_maker_fills/ hard cap /MAX_SCAN_STEPS/ park cap bound the walk; OOG not used as the bound.belief_pricenormin_returnreverts.pool_input/book_inputunchanged).make verify-issue-<iid>.Test plan (functional paths)
Pair multitest (
cl8y-dex-pair/cl8y-dex-tests):hybrid: None+ live better ask → still pool-only (G1).book_input = offeron a mixed book: declared may take worse tail; greedy must not.match_bids; token1 in →match_asks.max_maker_fills = 1with three better makers → one fill, rest pool.max_maker_fills = 0and> 100(clamp vs reject — as designed).book_start_hintlive same-side; wrong-side; stale;None.Nonehitting scan cap (#289).min_return/belief_price/max_spreadpass and fail.traderonly when CW20 sender is trusted router.return_asset= book net + pool net (L7).hybrid: nullhop unchanged.Indexer / dApp (only if hook string or attrs change): parser still maps
pool_leg_volume/book_leg_volume; gas map unit tests.Test plan (attack, hack, abuse)
book_input = offer).max_spread/min_return. No extra MEV toggle.cost == 0 && fill > 0; do not brick the walk.continue), do not revert whole swap (L20).max_maker_fillsgrief (1 vs 10_000)tradertraderunless sender is trusted router (existing). Greedy must not loosen this.{pool_input, book_input, …}must not deserialize as greedy and silently ignore the split.Simulationpool-only.traderon pair-direct to steal a tier.Verification criteria
make test-contracts(or scopedcargo test -p cl8y-dex-pair+cl8y-dex-testsgreedy / hint / hybrid suites) green.book_return_amountabsent / zero.limit_order_fill+ positivebook_return_amountwhen the book is better; pool-only control does not.make verify-issue-<iid>covers message-shape grep, G1 comment, ADR/skill links, and the contract test names above./route/solve) unless a later ticket says so.marked as related to #704
marked as related to #707
mentioned in issue #704
mentioned in issue #707
Sibling docs/awareness (can merge first, no wasm): #707. Parent: #704.
Should be opt-out instead of opt-in, so by default bots that are doing normal swaps use this unless they have a reason not to, as its a sensible default.
mentioned in commit
e3ab31400amentioned in merge request !1198
mentioned in issue #709
marked as related to #709
mentioned in issue #710
marked as related to #710
mentioned in commit
56e4c4f513mentioned in commit
f54de4fd4bmentioned in issue #712
marked as related to #712
!1198 merged to
main(f54de4fd; conflict resolution56e4c4f5).Merge-time AC check vs this issue:
hybrid: Nonenever reads the bookg1_pool_only_skips_better_bid)pool_input/book_inputon greedyreturn_amount≥ pool-onlyremainder_to_pooltested; no named ≥ pool-only / k / treasury = pool + book half assertMAX_SCAN_STEPS/ park-cap emit testNone; live same-side stale hint still should-fix)greedy_simulation_matches_execute; ADR L8 note). Query mutex was the #709 fix.make verify-issue-708gas vitest needsfrontend-dapp/node_modulesmake verify-issue-708Do not reopen this issue for ops. Tracking leftover: #712.
mentioned in issue #718