Pair-direct swaps bypass the limit book entirely — makers are invisible to any integrator that doesn't pass HybridSwapParams #704
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#704
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
#596made the frontend's swaps always consider the book, but the pair contract itself still only consults the limit book when the caller suppliesHybridSwapParams:Any wallet, bot, or integrator that calls the pair contract directly gets pool-only execution — even when a resting order is strictly better. Observed on columbus-5: an automated trader running ~85 swaps/day on CL8Y-cb/cLUNC crossed the best resting ask by 1.7% and still filled 100% against the pool (it never passes
HybridSwapParams, so the book cannot participate).Consequences:
Expected
The pair's plain
swappath computes the price-optimal pool/book split itself (or defaults to book-first) when noHybridSwapParamsis provided — so best execution is a property of the contract, not of which client the taker happens to use. Callers who explicitly want pool-only could pass a flag for it.Actual
hybrid: None→book_leg = 0unconditionally; the default path never reads the book.Notes
pool_leg_volume/book_leg_volumefields make the fix measurable: share of pair-direct trades with a non-zero book leg.This is expected behavior, because hybrid orders require complex offchain routing. Onchain behavior must allow bots that dont have access to offchain routing to do direct v2 swaps, then allow other bots to arbitrage between the pair and the limit orders because not all bots can do the complex price routing, so in effect the bot is paying arbitraguers to do price routing for it. Open to additional feedback and ideas - if there is a gas efficient and affordable way to do onchain hybrid orders, an upgrade is fine. Or if this is an issue where bot operators arent aware of the need for offchain routing, perhaps changes to documentation or onchain behavior could help guide bot creators and operators towards better routing algorithms, such as the ones in the indexer in this repo. @leonardocolucci
(leaving open for a few days for ideas/feedback, then will close as expected unless other actions are approved)
Thank you!
...
From: gitlab@mg.gitlab.com gitlab@mg.gitlab.com on behalf of Plastic Digits (@PlasticDigits) gitlab@mg.gitlab.com
Sent: 29 August 2026 15:27
To: leonardo_colucci@hotmail.com leonardo_colucci@hotmail.com
Subject: Re: cl8y-dex-terraclassic | Pair-direct swaps bypass the limit book entirely — makers are invisible to any integrator that doesn't pass HybridSwapParams (#704)
Plastic Digitshttps://gitlab.com/PlasticDigits commentedhttps://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/704#note_3756599362:
(leaving open for a few days for ideas/feedback, then will close as expected unless other actions are approved)
—
Reply to this email directly or view it on GitLabhttps://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/704#note_3756599362.
You're receiving this email because of your account on gitlab.comhttps://gitlab.com/. Unsubscribehttps://gitlab.com/-/sent_notifications/REDACTED/unsubscribe from this thread · Manage all notificationshttps://gitlab.com/-/profile/notifications · Helphttps://gitlab.com/help Notification message regarding https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/704#note_3756599362 at 1788013667
Thanks — that's a fair framing, and I agree with the core of it: full multi-hop split optimization does belong off-chain (the indexer solver, per #501), and cheap pool-only direct swaps must stay possible. I'm not arguing the default should force a book walk onto every caller.
One gap in the arb-layer model, though: arbitrage corrects the price over time, but it doesn't give the direct taker best execution on their fill — the taker still pays the worse pool price, and the surplus goes to arbitrageurs, not to that taker or to the maker who was quoting better. On a venue where arb flow is still thin, that correction is slow and partial, so a class of integrators systematically overpays and the maker never sees that flow. That's the piece I don't think "the arb layer handles it" fully answers.
On your open door for a gas-efficient on-chain hybrid — I think there's a bounded middle path that sidesteps the "not all bots can do complex routing" problem:
A swap variant that takes only
book_start_hint+max_maker_fills(no caller-suppliedpool_input/book_input), and does a greedy book-first-then-pool fill in-contract: walk the top of book while each level beats the pool's marginal price, capped atmax_maker_fills, then route the remainder to the pool.The key point: the hard offchain part is the split optimization, and greedy book-first doesn't need it. The caller's job shrinks from "run the solver" to "point at top-of-book" — one LCD/indexer query any bot can do. Gas stays bounded and opt-in (only walks when the flag is set, only for levels that beat the pool, capped at N); pure pool-only stays the cheap default. And it reuses the book-walk that already runs when
HybridSwapParamsis passed.The one thing for you to weigh: whether the contract can cheaply start the walk from a caller-supplied hint (vs the #266 anchor threading) — keeping
book_start_hintas a cheap caller input rather than auto-deriving it is probably the pragmatic form.Fully agree the docs/awareness path is worth doing now regardless — clear integrator guidance that direct
pair.swapis pool-only and best execution goes through the router/indexer would help immediately. I'd just suggest keeping this open as a tracked enhancement (thebook_start_hint+max_maker_fillsvariant) rather than closing as expected, and using the per-tradepool_leg_volume/book_leg_volumefields to measure the share of direct trades leaking past a better book — so the call stays data-driven. Happy to help spec the variant if useful."A swap variant that takes only
book_start_hint+max_maker_fills(no caller-suppliedpool_input/book_input), and does a greedy book-first-then-pool fill in-contract: walk the top of book while each level beats the pool's marginal price, capped atmax_maker_fills, then route the remainder to the pool."Interesting idea, Ill open an issue to investigate the design potential. Plus the docs update.
mentioned in issue #707
marked as related to #707
mentioned in issue #708
marked as related to #708
Follow-ups from the approved paths on this thread:
pair.swapwithout hybrid is pool-only; best execution is indexerGET /route/solve/ declared Pattern C.hybrid: Nonestays pool-only): #708 — greedy book-first-then-pool variant taking onlybook_start_hint+max_maker_fills(no callerpool_input/book_input).Default TerraSwap
Swap {}is unchanged. Closing this issue as expected can wait until those two are triaged.@leonardocolucci follow-up on the columbus-5 pair-direct flow you reported (CL8Y-cb/cLUNC bot ~85 swaps/day, never passing
HybridSwapParams, crossing a better ask and filling 100% pool).The greedy variant you sketched is live as opt-in, not as the default
Swap {}path.What migrated (#712, closed): pair 11639 / cw2 1.16.0 on all 20 factory pairs (
config.pair_code_id11639). Router 11640 as well (same cw2 1.1.0; greedy hops now decode). Official dApp still usesGET /route/solve.Still true after that migrate (G1): omitted
hybrid+ omittedgreedyis pool-only. LCD smoke on EMBER/CORAL: omitted and Pattern Cbook_input=0both return929574; greedy with an empty/worse book isworse_than_pooland the same amount. So a TerraSwap-shaped pair-direct bot that does not sendgreedy: { max_maker_fills, book_start_hint }will still skip the book — same class of miss as your CL8Y-cb/cLUNC observation.When greedy is passed, the book does participate. CL8Y/cUSTC query canary (offer
1e15CL8Y): pool-only98538vs greedy99440(book_return_amount=99440,greedy_stop=filled).#718 is the next wasm: omitted params default to greedy (opt-out; pool-only only if the caller sends Pattern C
book_input=0). That would cover misconfigured / TerraSwap-clone bots without a client change. It needs pair 1.17.0 + a new router bump; do not treat 11639/11640 as that.Question for you: given the bot you watched, is 712 (opt-in greedy — they must start sending
greedy) enough, or do we still need 718 (default greedy on plainSwap {}) so that flow hits the book with no integrator change? Happy to go either way off what you are still seeing on-chain (pool_leg_volume/book_leg_volumeon those pair-direct fills).Thanks for turning this around so fast — the opt-in greedy variant is exactly the shape I had in mind, and your CL8Y/cUSTC canary (99440 vs 98538 pool,
greedy_stop=filled) confirms the book participates whengreedyis passed. 🙌On your question — is #712 enough, or do we need #718 — I think the on-chain answer is #718, and it comes straight from the flow this issue was opened about:
The CL8Y-cb/cLUNC bot (
terra18xu97…, a third party) is still running today, after the 11639 migrate: ~85 swaps/day, both directions, and its recent fills are 100% pool-only (book_leg_volume = 0). It's a TerraSwap-shaped integrator, so it will never send CL8Y'sgreedyparam — which means opt-in (#712) can't capture it. That's the crux: opt-in still requires the integrator to pass a special param, so makers stay invisible to any bot that doesn't adopt it — the exact problem here, one param-name over.Opt-in is the right tool for cooperating integrators, and the dApp already best-executes via
GET /route/solve. But closing the leak this issue is about — non-cooperating, TerraSwap-clone pair-direct flow — needs omitted-params default greedy (#718), so plainSwap {}fills a strictly-better book with no client change. That also lines up with the best-execution-by-default direction #596 set for the UI, one layer down. Callers who truly want pool-only keep the explicit Pattern Cbook_input=0escape hatch, and gas stays bounded/opt-out.Fully acknowledge the trade-offs are yours to weigh (default-greedy adds a bounded book scan to the common swap path + needs pair 1.17.0 / router bump). Happy to keep watching
pool_leg_volume/book_leg_volumeon that pair to quantify the captured flow once a maker is quoting against it.Backlog sweep note (2026-09-06) — comment only
Thread suggests an opt-in greedy / HybridSwapParams path may already have landed (founder thank-you on canary). Not closing overnight.
Founder: if ACs are on
main, close; else leave unlabeled (needs product call on default-on vs opt-in — not autoready/agent:implement).