bug(frontend): one-sided zap Add reverts provide_liquidity when swap fill is below quote (Overflow Cannot Sub) #559
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#559
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 one-sided Add (automatic zap) on production
dex.cl8y.comreverts with CosmWasmOverflow: Cannot Subduringprovide_liquidity. The swap leg is allowed to return any amount ≥min_return(default 5% below the quote), but the following provide stillTransferFroms the quoted ask amount. Any fill in(min_return, quote)succeeds the swap and then fails the provide.Reported on columbus-5, pair UST1 / cUSTC (
terra1ceprjsxp86ggftf5e38wwt34l83e5gq7penkdnv4wsatkwcs8v6qccw55f).Evidence (mobile wallet, Pool → Add):
200 in + min swap 500571Zap. Est. LP ~0.000000000007259failed to execute message; message index: 3: Overflow: Cannot Sub with 525495 and 526916: execute wasm contract failed500571 ≈ 526916 × 0.95(Settings 5% slippage).525495is ~0.27% below the quote and abovemin_return, so this is not a swap slippage reject.Related: #533 (one-sided zap), #342 (historical
Cannot Subon wrap amounts — different cause), #508 (UST1/cUSTC secondary AMM), #537 (I14 discount gating), #147 (allowance rollback is for sequential 3-tx provide, not this multi-msg).Do not split zap-in provide, zap-out unwrap/swap, and the Add quote/pre-sign display into separate issues. Same multi-msg “next leg sized to quote” class.
Current codebase
#533 shipped retail
/poolone-sided zap as frontend orchestration on existing messages (Z533-10: no pair/routerZapexecute).quoteOneSidedAddruns localzapInSpliton LCDgetPool(React QuerystaleTime: 15_000). It does not call pairsimulateHybridSwap.swapMinReturn = applySlippagePercentFloor(swapOut, slippage%).provideOffer/provideAskaresplit.provideIn/split.provideOutfrom the quoted net, not the floor.effectivePoolFeeBps(fee_bps, advertisedDiscountBps). I14 gates discount on pairDISCOUNT_REGISTRY(#537). Local math can still disagree with execute (stale reserves, rounding, hook skim, mempool).buildZapInMessages:[0] CW20 send + swap (min_return)→[1] increase_allowance offer→[2] increase_allowance ask→[3] provide_liquidity. Matches message index: 3.provide_liquidity.assets[].amount= quotedprovideAsk. Pair then CW20TransferFromthat exact amount (pair/src/contract.rsexecute_provide_liquidity). CW20checked_subon a balance of525495vs amount526916is the observedOverflow: Cannot Sub with 525495 and 526916.min_returnis the 5% floor. Provideslippage_toleranceis set (Z533-7). That does not shrink the TransferFrom amounts.terraGasRetailInventory.ts). No LCD simulate-before-broadcast. A structurally failing zap is still signed and pays gas. Cosmos multi-msg is atomic (swap+allowances revert with provide); user loses gas only, not a half-zap.OneSidedWithdrawCardsetsswapAmountto quotedsplit.swapInandunwrap.amountto quotedsplit.totalWantedCw20(wanted + quoted swap out), while the swap leg only guaranteesswapMinReturnand withdraw only guaranteesmin_assets. A worse-than-quote fill canCannot Subon the swap send or unwrap send.oneSidedLiquidityTx.test.tsA18 fixture already usesprovideAsk === swapMinReturn.quoteOneSidedAddnever assertsprovideAsk <= swapMinReturn. No unit covers “fill in(min_return, quote)must still provide”.${amount} innext to rawmin swap ${swapMinReturn}(500571instead of0.500571at 6 decimals). Est. LP uses LP CW20 18 decimals (Z533-10), so a small mint looks like~0.000000000007259.Pair mint/burn math is fine. This is not a wrap-tax #342 miss: the UI showed
Zap.notWrap + zap., and the failing msg is provide.Why this is needed
min_returnis lying. Settings 5% is applied to the swap, then ignored when sizing the provide. The only fills that work are those ≥ the optimistic quote (a ~0% band), not the 5% the UI sold.TransferFrom(quoted)can succeed by dipping pre-existing ask balance to cover a shortfall. That violates Z533-4 (no silent donate / no unsolicited other-side spend). Atomic revert currently hides this; a future “retry provide” or split-tx would expose it. Size to floors so it cannot happen.totalWantedCw20).min swap 500571raw; dust-looking 18-dec LP) so users cannot tell a thin/skewed pool from a solver bug.Constraints / guardrails
Zapexecute unless LocalTerra rehearsal proves conservative floors still cannot work (Z533-10). Default fix is quote/tx sizing, not a contract upgrade.min_return/slippage_tolerance/min_assets). Do not setslippage_tolerance: null. Default 5% from Settings (#497).useFeeDiscountRegistryStatus(pairAddr). Unwired pair →discountBps = 0. Do not apply a walletget_discountto the split when the pair registry is unset.poolOnlyHybridParams, A13). No book leg.Relevant files
frontend-dapp/src/utils/oneSidedLiquidityQuote.tsprovideAskfrom quotedsplit.provideOut;swapMinReturnunused for providefrontend-dapp/src/utils/oneSidedLiquidity.tszapInSplit/trimProvideToRatio/zapOutSplitfrontend-dapp/src/utils/oneSidedLiquidityTx.tsfrontend-dapp/src/components/pool/OneSidedAddCard.tsxmin swap ${swapMinReturn}raw; Est. LP 18-decfrontend-dapp/src/components/pool/OneSidedWithdrawCard.tsxswapAmount+ unwrap of quotedtotalWantedCw20frontend-dapp/src/utils/provideLiquidityEstimate.tsfrontend-dapp/src/utils/rawAmountMath.tsapplySlippagePercentFloorfrontend-dapp/src/hooks/useFeeDiscountRegistryStatus.tsdiscountBpssmartcontracts/contracts/pair/src/contract.rsexecute_provide_liquidityTransferFrom (do not change mint math)smartcontracts/contracts/pair/src/hybrid_reverse.rspool_net_output_for_input(solver should keep matching)frontend-dapp/src/utils/__tests__/oneSidedLiquidityQuote.test.tsfrontend-dapp/src/utils/__tests__/oneSidedLiquidityTx.test.tsprovideAsk === min_returnfrontend-dapp/e2e/pool-one-sided-533-tx.spec.tsdocs/frontend.md§ One-sided poolskills/AGENTS_FRONTEND_POOL_ONE_SIDED.mddeployments/mainnet-ust1-wrap/REGISTRY.mdRecommended direction
Size every subsequent multi-msg leg to the previous leg’s floor, then trim to the conservative post-swap ratio.
Zap-in
zapInSpliton current pool + effective fee (quoted split for Est. LP / impact).swapMinReturnstaysfloor(swapOut × (1 − slippage%)).provideAsk = min(split.provideOut, swapMinReturn)(typically the floor).trimProvideToRatiowithswapOut = provideAskand post-swap reserves consistent with that worse output (offer reserve still+ swapIn; ask reserve higher if less ask left the pool).provideOffershrinks; extra offer stays in the wallet.simulateHybridSwap(pool-only) to refreshswapOutbefore flooring — not a substitute for (2).Zap-out
swapAmount≤ withdrawmin_assetsof the sold leg (not the 0% pro-rata quote).min(withdrawn wanted, min_assets[wanted]) + swapMinReturn(never quotedtotalWantedCw20if that exceeds the floor chain).Quote / pre-sign UX (same cards)
min swapmust be human (formatTokenAmount(swapMinReturn, askDecimals)), not raw.Amount too small(existing dust copy) rather than submitting a tx that can only mint~1e-14LP.Zap./Wrap + zap./Route + zap.prefixes.Out of scope
Zapexecute on pair/router.provide_liquidityto take max assets and refund (would be a contract change and donation foot-gun).Acceptance criteria
provideAsk≤swapMinReturnand is satisfiable if the swap returns exactlymin_returnand the user had zero pre-existing ask balance.provideOfferis ratio-trimmed to the conservative post-swap reserves (Z533-4). Leftover offer/ask stays in the wallet; no donate.(min_return, quote)(e.g. quote526916, fill525495, min500571) must not produceOverflow: Cannot Subon provide. Tx either succeeds or fails at swapmin_return/ provideslippage_tolerancewith those errors — never CW20 underflow.swapAmountare sized to withdraw/swap floors, not optimistic quotes. SameCannot Subban.min_return.discountBps = 0) still floors provide; a wallet with a largeget_discountmust not inflateprovideAskabove on-chain output.500571must not appear next to a human200 in.Amount too small); CTA blocked. Empty pool stillEmpty pool. Use Advanced.slippage_toleranceset. Pool-only hybrid (book_input = 0).make verify-issue-533still passes; this issue getsmake verify-issue-559.min_return(fee mismatch or reserve nudge). UST1/cUSTC-shaped 6-dec/6-dec pair included.Test plan (all paths)
Use invariant ids T-Z1–T-Z14 in unit tests.
provideAsk <= swapMinReturn;provideOffer + leftoverIn = amountIn - swapIn; ratio trim vs conservative post-swap reservesquote - 1but>= min_return(fixture balances)TransferFromamount ≤ fill; noCannot SubprovideAsk == swapOut(floors collapse to quote); still ratio-trimmedfee_wrap_bps; provide floors after wrap net; no burn tax on wrapminimum_receivefloors the zapamountIn; zap provide uses zapmin_returnEmpty pool. Use Advanced.swapAmount <= min_assets[sold]; leftover sold token OKswapMinReturn500571Unit files:
oneSidedLiquidity.test.ts,oneSidedLiquidityQuote.test.ts,oneSidedLiquidityTx.test.ts, OneSidedAddCard / copy if needed.Playwright: 5 workers for UI smoke; 1 worker
e2e-txfor chain (pool-one-sided-533-tx.spec.ts+ new case).PLAYWRIGHT_SKIP_CHAIN=1must not be used to skip P9.Test plan (attack, hack, abuse)
min_returnand provide succeeds at conservative amounts, or swap revertsmin_return/max_spread. NeverCannot Subon provide.− (quote − fill)from old inventory.fee_bpswhen registry unset. No inflated TransferFrom.trimProvideToRatio+ slippage_tolerance; excess offer not transferred.Cannot Subbanned; leftover CW20 stays.min_return0 stillRetailZapFloorError/ dust disable. No wrap of unsigned sub in JS (bigintonly).Verification criteria
Close when all of the following are true:
make verify-issue-559(new script) passes: unit T-Z1–T-Z12, grep thatquoteOneSidedAddprovide amounts are floor-trimmed, docs/skill invariant listed,make verify-issue-533still green.make test-frontendgreen for the touched files.make verify-issue-539/sg docker -c 'CI=1 make test-e2e'path including P9 (worse-than-quote fill still adds LP, or swap fails cleanly). Cloud Agent: provision viamake setup-cloud-localterra— do not report skip.Overflow: Cannot Sub with <fill> and <quote>at message index 3./poolAdd shows human min-swap; Est. LP is either a readable 18-dec amount or a dust disable.Zapexecute landed unless a linked issue records LocalTerra proof that multi-msg floors cannot work.Severity: production retail LP add on columbus-5. Priority: high.
marked as related to #533
changed the description
Follow-up from code review of the same report: quote–execute coupling is confirmed. Additional related issues to keep in scope as hardening, not a second ticket:
useSubmitAlignedSimQuote; one-sided Add only guardssnapshot.payRaw !== rawAmount. Optional LCDhybrid_simulation/simulateSwap(..., { trader })on the zap swap leg belongs under AC5 / recommended direction item 4, and must not replace floor-sizedprovideAsk(AC1–AC3).hybrid_simulationvs indexer drift; same class.traderonhybrid_simulationso fee-discount matches execute.Zap-out floors (AC4), human min-swap (AC7), and dust LP disable (AC8) stay in this issue. Do not split a P1 “quote hardening” work item unless floors-only still fails LocalTerra P9.
mentioned in commit
cb834f3069mentioned in merge request !1084
mentioned in commit
da133bd380Merged in !1084 (Makefile conflict only vs !1080–!1083).
Shipped vs #559 AC: zap-in
provideAskfollowsswapMinReturnthen conservative reserve trim; zap-out usesmin_assets+swapMinReturn; leftover stays in wallet; pre-sign min-swap is human units; Z559-1–Z559-4 +make verify-issue-559.Still open: production manual on UST1/cUSTC (Settings 5%) must not show
Cannot Subat message index 3. Coolify/CImake test-frontendwas not waited (quota-exceeded). Local frontend unit tests for zap + portfolio/volume/hub helpers passed after the merge stack.Tracked in the post-merge follow-up after !1080–!1084.
mentioned in issue #560
marked as related to #560
Post-merge follow-up: #560
Post-merge review extra: LocalTerra P9 names “fill can be below quote” but only asserts human min-swap copy + a happy-path add. Worse-than-quote is covered by unit fixtures (
525495/526916/500571), not an on-chain reserve nudge. Optional follow-up: e2e that forces fill ∈(min_return, quote). Production UST1/cUSTC manual QA remains the AC that matters; tracked on #560.mentioned in merge request !1090
mentioned in issue #622
mentioned in merge request !1135
mentioned in issue #660
marked as related to #660