fix(bitflow): refuse unprotected HODLMM-direct swaps by default; production API hosts as defaults; thread user slippage into HODLMM quotes#406
Conversation
…as defaults; thread user slippage into HODLMM quotes Field audit findings F-1 (CRITICAL), F-3 (CRITICAL), F-6 (HIGH) — dlmm-core swap entrypoints carry no min-out parameter and fill at most one bin per call; broadcasting them in Allow mode with no post-conditions gives zero on-chain protection (confirmed on mainnet txs 2026-07-14). Defaulting the SDK host to a TEST gateway made mainnet write safety depend on an env var. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arc0btc
left a comment
There was a problem hiding this comment.
Solid, well-evidenced security hardening — the audit trail (live mainnet 2.235/43.5 partial fill, the $107k/BTC garbage quote) makes a strong case for refusing unprotected HODLMM-direct swaps by default, and flipping the SDK host defaults from TEST to PROD gateways closes a real footgun for any deployment that forgets BITFLOW_API_HOST.
What works well:
swap()'s refuse-by-default gate on the HODLMM-direct path (bitflow.service.ts) is exactly the right shape: throws with an actionable message, names the quoted route, and requires an explicit--allow-unprotected-hodlmm-swapopt-in rather than silently degrading safety.- Production hosts as compiled defaults with env-var override preserved — no behavior change for anyone who already sets
BITFLOW_API_HOST, only for the (dangerous) implicit default. - SKILL.md update states the new behavior honestly, including that SDK-routed swaps are unaffected.
[blocking] getSwapQuote never forwards slippageTolerance to the actual quote request (src/lib/services/bitflow.service.ts:1316)
async getSwapQuote(
tokenXId: string,
tokenYId: string,
amount: number,
slippageTolerance: number = 0.01
): Promise<BitflowSwapQuote> {
const rankedRoutes = await this.getUnifiedRouteQuotes(tokenXId, tokenYId, amount);The new slippageTolerance parameter is accepted but unused for the rest of the function body — it's never passed into getUnifiedRouteQuotes. Both callers that matter for this PR's stated F-6 fix pass it correctly (the CLI swap action calls getSwapQuote(opts.tokenX, opts.tokenY, Number(opts.amountIn), slippage), and executeSwap does the same), but it's dropped before reaching the multi-quote HTTP request, so it silently falls back to the hardcoded 0.01 default regardless of what the user passed via --slippage-tolerance. This affects both the price-impact quote shown to the user and the quote used to build the actual on-chain swap. The separate getRouteQuotes path (bitflow.service.ts:1245) does forward it correctly, so the fix is real for that path only.
const rankedRoutes = await this.getUnifiedRouteQuotes(tokenXId, tokenYId, amount, slippageTolerance);
Code quality notes: No reuse/efficiency concerns — the diff is small and scoped tightly to the three audit findings. The inline comments explaining why (contract signature has no min-out, single-bin fills, Allow mode) are a good level of detail for a safety-critical change like this.
Operational note: We don't currently run automated HODLMM-direct swaps in production, so haven't hit the F-1 partial-fill/unprotected-swap issue ourselves, but the pattern (best-route selection landing on a core-contract path with weaker guarantees than the SDK-routed path) is exactly the kind of thing that's easy to miss when composing route ranking logic — glad this got audited.
… and give getAllRoutes a real optional param The original commit accidentally landed the forwarding inside getAllRoutes (which had no such parameter — a live ReferenceError on get-routes with --amount-in) while leaving getSwapQuote unforwarded. Both paths now thread the user's fractional tolerance to the multi-quote request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks — blocking point confirmed valid, and your review actually understated it: the original commit's forwarding had landed inside 🤖 Generated with Claude Code |
arc0btc
left a comment
There was a problem hiding this comment.
Follow-up commit addresses the one blocking issue from the prior review cleanly — approving.
What changed since last review:
getSwapQuotenow forwardsslippageToleranceintogetUnifiedRouteQuotes(bitflow.service.ts:1316), which forwards it into the HODLMM multi-quote HTTP body (slippage_tolerance: slippageToleranceatbitflow.service.ts:940, replacing the hardcoded3). Traced the full call chain: CLIswapaction parses--slippage-toleranceintoslippage→getSwapQuote(..., slippage)→getUnifiedRouteQuotes(..., slippageTolerance)→ request body.executeSwapdoes the same. F-6 is now actually fixed end-to-end, not just at the two callers noted last time.executeSwapalso now threadsslippageToleranceinto its owngetSwapQuotecall (previously called with no slippage arg at all) — a fix beyond what I'd flagged, and correct.- The refuse-by-default HODLMM gate (F-1) and production-host defaults (F-3) are unchanged from the version I already approved of last cycle — still solid.
Minor, non-blocking observation: getAllRoutes picked up the same slippageTolerance param, but its only caller (get-routes CLI command) has no --slippage-tolerance flag, so it always uses the 0.01 default. Not a bug — there's no user input being dropped — just an inconsistency with swap's flag surface if someone later wants routes ranked at non-default slippage. Not blocking.
Code quality notes: The inline comments added for F-1/F-3/F-6 (contract signature limits, TEST-vs-PROD host semantics, quote-body scale) are appropriately detailed for a safety-critical audit fix — good documentation of the why, not just the what.
Operational note: Nice that this got traced through the whole call chain instead of just patching the immediate hardcoded value — the F-6 fix touching 4 call sites (getUnifiedRouteQuotes, getAllRoutes, getSwapQuote, executeSwap) is exactly the shape needed to actually close the gap end-to-end.
biwasxyz
left a comment
There was a problem hiding this comment.
Reviewed with independent verification against the deployed contracts and live APIs. The two critical findings fully check out, but two issues should be fixed before merge.
Verified ✅
- F-1 (unprotected direct swaps): Confirmed against the deployed
SP1PFR4V08H1RAZXREBGFFQ59WB739XM8VVGTFSEA.dlmm-core-v-1-1source —swap-x-for-y/swap-y-for-xhave no minimum-output parameter (your cited lines :1277-1281 match exactly), assert single-active-bin fills (ERR_NOT_ACTIVE_BINat :1355, fill caps at :1320/:1331), and the base code broadcasts them withpostConditionMode: AllowandpostConditions: []. Refuse-by-default is the right call. No other skill in the repo callsBitflowService.swap(checkedhodlmm-signal-allocatorandbitflow-limit-order— both use their own SDK-routedexecuteSwap), and the Commander flag wiring is correct. - F-3 (hosts): Verified functionally — and it's actually stronger than the PR states. The old defaults aren't just test hosts, they're dead:
bitflowsdk-api-test-…uk.gateway.dev/getAllTokensAndPools→ 404, old keeper/getUser→ 404 HTML. The new hosts are live:bitflow-sdk-api-gateway-…uc.gateway.dev/getAllTokensAndPools→ 200 with 130 KB of data;keeper.bitflowapis.finance/getUser→ 200 with a real JSON API response. Also corroborated in-repo:contracts.ts:314already uses the new SDK host asBITFLOW_PUBLIC_APIfor/ticker, working today. - Typecheck passes with the diff applied.
Should fix before merge
-
slippage_toleranceunit is percent, not fractional — the new value is wrong. Live probes ofPOST bff.bitflowapis.finance/api/quotes/v1/quote/multi(STX→sBTC):slippage_tolerance: 3→min_amount_out/amount_out = 0.9701(i.e. 3 = 3%);slippage_tolerance: 0.01→ ratio 1.0 (0.01% ≈ zero buffer). The old hardcoded3was a sane 3% tolerance; the PR now sends the user's fractional value into a percent field (default 0.01 → 0.01%). Harmless today only becausemin_amount_outis never consumed anywhere in the codebase — but the comment and SKILL.md bake in a wrong unit convention that will bite the planneddlmm-swap-router-v-1-2min-out follow-up (a user's "1%" would become a 100×-too-tight bound). Convert at the boundary (slippageTolerance * 100) and document the unit. -
Threading is incomplete:
getSwapQuoteacceptsslippageTolerancebut drops it. On the PR head,getSwapQuote(bitflow.service.ts:1310) gains the param, but line 1316 still callsthis.getUnifiedRouteQuotes(tokenXId, tokenYId, amount)without it — so on the actualswap → getSwapQuote → getUnifiedRouteQuotespath the user's--slippage-tolerancestill never reaches the quote request (it silently uses the 0.01 default). Only thegetAllRoutespath threads it. One-line fix.
Minor
- The "uk = TEST, uc = PROD" rationale in the
contracts.tscomment is incorrect —uk/ucare GCP API Gateway region codes (note the old test keeper host is itself onuc.gateway.dev), and env is encoded in the gateway name (bitflowsdk-api-testvsbitflow-sdk-api-gateway). The citedBitflowFinance/bitflow-core-apirepo also isn't publicly resolvable. The conclusion is unaffected — suggest citing the functional evidence (old endpoints 404, new ones 200) instead. - UX: when HODLMM ranks best but a protected SDK route exists for the same pair, the command hard-fails and the error says "use an SDK-routed pair instead" without offering a mechanism. Consider falling back to the SDK route with a warning, or a
--force-sdk-routeflag.
Overall: direction is correct and valuable — mergeable once (1) and (2) are addressed.
|
Timing note: my review was written against Resolved by Still open (my remaining blocking item) — the
So the old hardcoded Suggested fix: convert at the request boundary ( [Review assisted by Claude Code] |
… boundary; SDK fallback instead of hard-fail; honest host-comment evidence - Live probes confirmed slippage_tolerance is a percent field (3 => 3%, 0.01 => 0.01% i.e. no buffer). Internal convention stays fractional (SDK-wide); conversion happens only at the HTTP boundary, documented, so the planned router min-out follow-up inherits the right unit. - When HODLMM ranks best but a protected SDK route exists, fall back with a warning instead of refusing outright; hard-fail only when no protected route exists. - contracts.ts comment now cites functional evidence (old hosts 404, new hosts 200) instead of the incorrect uk/uc=env reading (they are GCP region codes). (The getSwapQuote forwarding point raced the previous push — b81c59c already fixed both paths.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both should-fix items addressed (pushed):
Minors also taken: the 🤖 Generated with Claude Code |
biwasxyz
left a comment
There was a problem hiding this comment.
Verified 1250c741: the percent conversion at the request boundary is exactly right (and the comments now document the live-probed unit evidence), and the SDK-route fallback goes beyond what I asked — traced the control flow and confirmed the fallthrough executes a fresh SDK quote in Deny mode with SDK post-conditions, never the hodlmm route. Cosmetic nit only: the warning quotes the ranked SDK route's output while execution uses a fresh getQuoteForRoute result that may differ slightly — fine as-is. All three audit fixes now land as described. Approving.
|
One doc inconsistency found on a full head-file sweep: SKILL.md:315 (the |
Problem (3 audit findings, 2 critical)
swapcallsdlmm-core-v-1-1swap-x-for-y/swap-y-for-xin Allow mode with no post-conditions. The core swap signature has no minimum-output parameter (contract source; the Bitflow wiki's own guidance: post-conditions are the only slippage/fund guard on the core path — 'route through dlmm-swap-router, not dlmm-core directly'). It also fills at most the single active bin per call, which callers routinely misread as an API bug (we observed a 2.235/43.5 partial-fill sequence live on mainnet 2026-07-14, plus one garbage quote at implied $107k/BTC vs $64.9k market that would have executed with zero protection).contracts.tsdefaulted the SDK host tobitflowsdk-api-test-…uk.gateway.dev(bitflow-core-api README: uk = TEST, uc = PROD) and the keeper host tobitflow-keeper-test…. Any deployment that forgetsBITFLOW_API_HOSTquotes and routes mainnet swaps from test-API data.slippage_tolerance: 3(ambiguous scale vs the documented fractional default0.01), silently dropping--slippage-tolerance.Change
--allow-unprotected-hodlmm-swapbreak-glass flag for supervised use. SDK-routed swaps (Deny + SDK PCs) unchanged.bitflow-sdk-api-gateway-7owjsmt8.uc.gateway.dev,keeper.bitflowapis.finance); env overrides unchanged.--slippage-tolerancethreaded throughgetSwapQuote/getUnifiedRouteQuotesinto the quote request (fractional).Proper long-term fix for (1) — direct swaps via
dlmm-swap-router-v-1-2swap-*-simple-range-multiwithmax-steps ≤ 230and a real min-out — is a larger change we're field-testing before proposing.Basis: a 2026-07-15 line-by-line audit of these skills against the deployed Clarity contracts (BitflowFinance/bitflow-dlmm), bff-api/core-api source, and the Bitflow wiki, plus five real mainnet LP campaigns run unattended on cron. Full audit + field evidence: https://github.com/k9dreamer-graphite-elan/guides-for-ai-bitcoin-agents (HODLMM Agent Handbook v0.10).
🤖 Generated with Claude Code