fix(router): declared hybrid split must equal hop offer on execute #1280
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#1280
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
Retail Swap/Trade hybrid execute can revert in the router with a CosmWasm generic error whose payload is
hybrid pool_input + book_input must equal hop offer amount. Wallets wrap that as execute wasm contract failed. The pair has the same invariant (HybridSplitMismatch:pool_input + book_inputmust equal the CW20 hookamount).This is a construction / hop-amount drift bug, not a request to drop the invariant and not greedy-default work.
Declared Pattern C splits are frozen in
SwapOperation::TerraSwap.hybridat quote/submit time. Router execute then requires that sum to equal:Sendamount intoexecute_swap_operations.hop_outputfrom the router balance delta inreply_swap_hop).If indexer/dApp splits were sized for a simulated intermediate (or a different pay raw), hop 1+ fails even when hop 0 was exact.
Related, not duplicates (keyword overlap on hybrid /
pool_input/book_inputis not enough):hybrid: Some(_)stays unchanged.book_input.Current codebase
Router (error the wallet shows)
smartcontracts/contracts/router/src/contract.rs:validate_hybrid_declared_split_for_no_belief(hop_offer, hybrid)— ifhybridisSome,pool_input.checked_add(book_input)must equalhop_offerorStdError::generic_err("hybrid pool_input + book_input must equal hop offer amount").execute_swap_operationsprechecks hop 0 against the incomingamount(~L213).reply_swap_hopprechecks the next hop against this hop’shop_output(~L457) beforeSend { amount: hop_output, msg: Swap { hybrid } }.must equal simulated offer amount for this hop(~L674–676) ascurrent_amountwalks simulated returns.Router
belief_priceis alwaysNoneon these hops, so the no-belief pool-leg /min_returnfloors from #307 / #334 still apply after the sum check.Pair
smartcontracts/contracts/pair/src/contract.rsexecute_swap/HybridSimulation:SwapHybridMode::Declaredrequirespool_input + book_input == input_amountelseHybridSplitMismatch(must equal offer amount). Direct 1-hopsend → pair.swaphits this string instead of the router hop wording.smartcontracts/packages/dex-common/src/pair.rs—HybridSwapParams { pool_input, book_input, max_maker_fills, book_start_hint }; comment: sum must equal the CW20 hook amount.pool_only_hybrid_params(offer)isbook_input = 0.Unfilled book still rolls remaining book budget into the pool after a matching split (
pool_input_amount = pool_leg + (book_leg − offer_consumed_by_book)). The revert is before that roll: the declared integers must already sum.Indexer
indexer/src/api/hybrid_route_opt.rs— per-hop grid;pool_input = offer_amount.saturating_sub(best_book)so a single hop’s strings should sum to that hop’srunningoffer. Joint optimize walksrunningfrom simulated hop outs (#209).GET /api/v1/route/solvereturnsrouter_operationswith thosehybridobjects.Gaps the sum check will catch:
running, while execute uses realizedhop_output(book fill, fee, tax, dust).pool_input/book_inputindependently without remainder-to-pool.Frontend
frontend-dapp/src/utils/cw20RouteSolveQuote.ts— wallet sim + submit must use indexerrouter_operations(including per-hophybrid).frontend-dapp/src/services/terraclassic/router.ts— copiespool_input/book_inputinto execute msgs; wrap/native BFS hops stay pool-only (#587 / #599 / #1264).frontend-dapp/src/services/terraclassic/swapRouting.ts— 1-hop useshybridFromSingleHopIndexerOps; ≥2 hops go router.frontend-dapp/src/utils/humanizeTerraTxError.ts/humanizeUserFacingError.ts— LCDgeneric error: … : execute wasm contract failed. No preflight that the hop sums match the offer about to be sent.Why the new implementation is needed
GET /route/solve). Hop 1+ will keep reverting whenever realized intermediate ≠ quote-time intermediate and the next hop still carries a frozenhybrid.Constraints / guardrails
hybrid: Some(_). Do not acceptpool_input + book_input != hop offer. Do not overflow-wrap.hybrid: Nonepool-only vs #718 greedy default). Do not overloadpool_input=0, book_input=offeras “rescale for me.”min_return/max_spread/ material pool-leg / greedy mutex (G8 / G11 / #307 / #334).pool = offer - book) so strings always sum; no independent rounding of both legs.hybridfrom a CW20 solve onto a wrap hop.Relevant files
smartcontracts/contracts/router/src/contract.rsreply_swap_hopsum checks; sim walksmartcontracts/contracts/pair/src/contract.rs+error.rsHybridSplitMismatchsmartcontracts/packages/dex-common/src/pair.rsHybridSwapParamsschemasmartcontracts/tests/src/limit_order_tests.rsindexer/src/api/hybrid_route_opt.rspool = offer - book; running offerindexer/src/api/route_solver.rsrouter_operationshybrid attachfrontend-dapp/src/utils/cw20RouteSolveQuote.tsfrontend-dapp/src/services/terraclassic/router.tsfrontend-dapp/src/services/terraclassic/swapRouting.tsfrontend-dapp/src/utils/humanizeTerraTxError.tsdocs/route-solver.md/docs/integrators.mdRecommended direction
runningused to size hop 2.hybrid,Uint128(pool)+Uint128(book) === hopOffer. Hop 0 offer = submit pay raw (post-tax/wrap-net). Later hop offers = walletsimulate_swap_operationsrunning amounts, not stale indexer strings. Fail closed with a specific UI string; do not broadcast.hybrid: null(pool-only) unless the simulated running amount is wired into execute (it cannot be — execute uses realized output). Retail then only declares hybrid on hop 0, or on 1-hop pair-direct.reply_swap_hop, if declared sum ≠hop_output, rescalebook_inputtomin(book, hop_output)(or proportional) andpool_input = hop_output - book, with dust-to-pool; reject if rescaling would violate material pool-leg /min_return.generic_errwith a typed error that includes expected vs actual sums (still no wallet addresses).pool_input + book_input == hop_offer(decimal integer strings, no float). Joint optimizer must rebuild hop i+1 splits from that hop’s simulated offer, never reuse hop 0’s pair of integers.Repro (Given / When / Then)
Hop 0 mismatch (pair or router)
hybridwhosepool_input + book_inputis not equal to the CW20 amount sent into the hop (off by 1 unit, or a stale book leg vs current pay raw).HybridSplitMismatch. No funds should remain stuck in the router (SWAP_STATEcleared on error).Hop 1+ quote/execute drift (router)
hybrid: Some(_)on hop 2 sized to simulated hop-1 outputS.hop_output ≠ S(book fill or fee), and the router forwards hop 2 with the frozen split.reply_swap_hophitsmust equal hop offer amountbefore hop 2Send.Expected vs actual
generic error: hybrid pool_input + book_input must equal hop offer amount(paraphrased in wallets as hop amount) +execute wasm contract failed. Simulate may have passed on quote-timecurrent_amount.Acceptance criteria
hop_output, or preflight refuses broadcast. Pick the policy in Recommended direction and lock it in the test name.hybridonrouter_operationssatisfiespool + book == hop offerfor that hop’s running amount (including 0 book / all pool).sign; wrap/native hops still havehybridomitted (#1264).hybrid: Nonebehavior unchanged. Pattern C interior splits that do sum still work.ready/ implement labels invented outside intake typebug. No secrets in fixtures.Test plan (functional paths)
pool+book == offeroffer-1HybridSplitMismatchoffer-1hybridsized to sim out, execute out differsSWAP_STATEhybrid: nullhybridcopied onto wrap hopbook_input = 0,pool_input = offerTest plan (attack, hack, and abuse)
pool_input + book_input > hop offer< hop offerchecked_addbook_input = hop_output + 1after rescalemin_returnomitted with book leghybridandgreedyVerification criteria
cargo testrouter/pair/limit-order hybrid split cases (locked workspace as the repo already runs).multihop-hybrid-tx.spec.ts) — do not add live columbus-5.hybrid(#1264).First-pass model recommendation
Recommendation: grok-high
Rationale: Founder-required wasm / router execute state (
SWAP_STATE, hop reply) plus indexer split emission and dApp preflight. More than three production files and a protocol-level hop-amount invariant; a wrong rescale could oversizebook_inputor skip slippage floors. Verification is cw-multi-test + indexer unit + Vitest, not a single local helper. Composer criteria fail on wasm, cross-subsystem scope, and uncertain hop-0 vs hop-1+ root cause until T4 exists.Merge complete: PR #1281 landed on main (
94ea0c91).Met: Policy A retail GET hop-0-only hybrid; wasm still fail-closes
pool_input + book_input == hop offer; POST interiors remain; wrap hops stay pool-only (H596-7). DB fidelity now recomputesgrid_outon the stripped plan so hop-0-only quotes are not markedindexer_hybrid_db_degraded.make verify-issue-1280passed on the fidelity SHA; Woodpecker on the PR was green. Push CI on main tip94ea0c91is success (ci/woodpecker/push/woodpecker).Leftover:
VERIFY_ISSUE_1280_CHAIN=1LocalTerra Playwright was not run in this merge. Columbus-5 wrap+2hop measurement stays on #1264 (envelope not raised).Follow-up for columbus-5 wrap+2hop measurement remains #1264. Git hook leftover from this land: #1286.