Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bitflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,9 @@ Output:
- Price impact is calculated using the XYK constant-product formula across all hops in the route.
- Keeper features enable automated/scheduled swaps. Use `get-keeper-contract` to get started.
- Wallet operations require an unlocked wallet (use `bun run wallet/wallet.ts unlock` first).

## Safety update (2026-07-15 field audit)

- **Direct HODLMM core swaps are refused by default.** `dlmm-core-v-1-1` `swap-x-for-y`/`swap-y-for-x` has no minimum-output parameter, fills at most the single active bin per call, and was previously broadcast in Allow mode with no post-conditions (audit F-1, CRITICAL). When the best route is HODLMM-direct, `swap` now errors unless `--allow-unprotected-hodlmm-swap` is passed — supervised, small-size use only. SDK-routed swaps (Deny mode + SDK post-conditions) are unaffected. Planned replacement: `dlmm-swap-router-v-1-2` `swap-*-simple-range-multi` with `max-steps <= 230` and a real min-out.
- **Production API hosts are now the compiled defaults** (previously TEST gateways; audit F-3). `BITFLOW_API_HOST` / `BITFLOW_KEEPER_API_HOST` still override.
- `--slippage-tolerance` is now threaded into the HODLMM multi-quote request — converted to the quote API's percent unit at the boundary (CLI stays fractional, 0.01 = 1%; previously hardcoded `3` = 3%, audit F-6). When the best route is HODLMM-direct and a protected SDK route exists, `swap` falls back to the SDK route with a warning instead of hard-failing.
11 changes: 9 additions & 2 deletions bitflow/bitflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,10 @@ program
"--confirm-high-impact",
"Set to execute swaps with price impact above 5%"
)
.option(
"--allow-unprotected-hodlmm-swap",
"DANGEROUS: permit direct dlmm-core HODLMM swaps, which carry NO minimum-output bound (audit F-1). Supervised, small-size use only."
)
.action(
async (opts: {
tokenX: string;
Expand All @@ -779,6 +783,7 @@ program
fee?: string;
walletPassword?: string;
confirmHighImpact?: boolean;
allowUnprotectedHodlmmSwap?: boolean;
}) => {
try {
if (NETWORK !== "mainnet") {
Expand All @@ -793,7 +798,8 @@ program
const quote = await bitflowService.getSwapQuote(
opts.tokenX,
opts.tokenY,
Number(opts.amountIn)
Number(opts.amountIn),
slippage
);
const impact = quote.priceImpact;
if (
Expand All @@ -819,7 +825,8 @@ program
opts.tokenY,
Number(opts.amountIn),
slippage,
resolvedFee
resolvedFee,
Boolean(opts.allowUnprotectedHodlmmSwap)
);

printJson({
Expand Down
9 changes: 7 additions & 2 deletions src/lib/config/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,15 @@ export function getBitflowConfig(): BitflowConfig {
const readOnlyCallApiHost = process.env.BITFLOW_READONLY_API_HOST || "https://api.hiro.so";

return {
apiHost: process.env.BITFLOW_API_HOST || "https://bitflowsdk-api-test-7owjsmt8.uk.gateway.dev",
// PRODUCTION host as default (2026-07-15 field audit F-3): the previous default
// was the TEST gateway (bitflow-core-api README: uk.gateway.dev = TEST,
// uc.gateway.dev = PROD), so any deployment that forgot the env override
// quoted and routed mainnet swaps from test-API data. TEST remains reachable
// via BITFLOW_API_HOST.
apiHost: process.env.BITFLOW_API_HOST || "https://bitflow-sdk-api-gateway-7owjsmt8.uc.gateway.dev",
apiKey: process.env.BITFLOW_API_KEY,
readOnlyCallApiHost,
keeperApiHost: process.env.BITFLOW_KEEPER_API_HOST || "https://bitflow-keeper-test-7owjsmt8.uc.gateway.dev",
keeperApiHost: process.env.BITFLOW_KEEPER_API_HOST || "https://keeper.bitflowapis.finance",
keeperApiKey: process.env.BITFLOW_KEEPER_API_KEY,
};
}
Expand Down
66 changes: 57 additions & 9 deletions src/lib/services/bitflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,12 @@ export class BitflowService {
async getUnifiedRouteQuotes(
tokenXId: string,
tokenYId: string,
amount: number
amount: number,
// Fractional slippage tolerance (e.g. 0.01 = 1%), the SDK-wide convention.
// NOTE: the quotes-plane HTTP request field is in PERCENT — converted at the
// request boundary below. 2026-07-15 field audit F-6: previously hardcoded
// to `3` (= 3%) regardless of the user's --slippage-tolerance.
slippageTolerance: number = 0.01
): Promise<UnifiedBitflowRouteQuote[]> {
this.ensureMainnet();
const sdk = this.ensureSdk();
Expand Down Expand Up @@ -933,7 +938,11 @@ export class BitflowService {
output_token: hodlmmTokenYContract,
amount_in: hodlmmAmountIn,
amm_strategy: "best",
slippage_tolerance: 3,
// The quotes-plane REQUEST field is in PERCENT (live-probed
// 2026-07-15: 3 => min_out/out = 0.9701 i.e. 3%; 0.01 => ~0%
// buffer). Our internal convention is fractional (0.01 = 1%),
// matching the SDK — convert at this boundary only.
slippage_tolerance: slippageTolerance * 100,
}),
}
);
Expand Down Expand Up @@ -1237,11 +1246,12 @@ export class BitflowService {
async getAllRoutes(
tokenXId: string,
tokenYId: string,
amount?: number
amount?: number,
slippageTolerance: number = 0.01
): Promise<UnifiedBitflowRouteQuote[]> {
this.ensureMainnet();
if (amount !== undefined) {
return this.getUnifiedRouteQuotes(tokenXId, tokenYId, amount);
return this.getUnifiedRouteQuotes(tokenXId, tokenYId, amount, slippageTolerance);
}

const sdk = this.ensureSdk();
Expand Down Expand Up @@ -1306,9 +1316,10 @@ export class BitflowService {
async getSwapQuote(
tokenXId: string,
tokenYId: string,
amount: number
amount: number,
slippageTolerance: number = 0.01
): Promise<BitflowSwapQuote> {
const rankedRoutes = await this.getUnifiedRouteQuotes(tokenXId, tokenYId, amount);
const rankedRoutes = await this.getUnifiedRouteQuotes(tokenXId, tokenYId, amount, slippageTolerance);
const bestRoute = rankedRoutes[0];
let bestExecutableRoute = rankedRoutes.find((route) => route.executable);

Expand Down Expand Up @@ -1561,18 +1572,55 @@ export class BitflowService {
tokenYId: string,
amountIn: number,
slippageTolerance: number = 0.01,
fee?: bigint
fee?: bigint,
allowUnprotectedHodlmmSwap: boolean = false
): Promise<TransferResult> {
this.ensureMainnet();
const quote = await this.getSwapQuote(tokenXId, tokenYId, amountIn);
const quote = await this.getSwapQuote(tokenXId, tokenYId, amountIn, slippageTolerance);
const executableRoute = quote.bestExecutableRoute;

if (!executableRoute) {
throw new Error(`No route found for ${tokenXId} -> ${tokenYId}`);
}

if (executableRoute.source === "hodlmm") {
return this.executeHodlmmSwap(account, executableRoute, fee);
// 2026-07-15 field audit F-1 (CRITICAL): this path calls dlmm-core-v-1-1
// swap-x-for-y / swap-y-for-x, whose signature has NO minimum-output
// parameter (contract source :1277-1281), fills at most the single active
// bin per call (:1320, :1331), and is broadcast in Allow mode with no
// post-conditions — zero on-chain protection. The Bitflow wiki's own
// security guidance: "route through dlmm-swap-router, not dlmm-core
// directly"; post-conditions are the ONLY slippage/fund guard on the core
// swap path. Observed live 2026-07-14: a garbage quote (implied $107k/BTC
// vs $64.9k market) would have executed unprotected.
// Interim guard until this path targets dlmm-swap-router-v-1-2
// swap-*-simple-range-multi (max-steps <= 230) with a real min-out:
// refuse by default; require an explicit opt-in for supervised use only.
if (!allowUnprotectedHodlmmSwap) {
// If a protected SDK route exists for the same pair, fall back to it
// with a warning instead of hard-failing (reviewer UX suggestion) —
// the caller gets a slightly worse quote but Deny-mode post-conditions.
const sdkFallback = quote.rankedRoutes?.find(
(route) => route.source === "sdk" && route.executable
);
if (sdkFallback) {
console.error(
`WARNING: best route is an unprotected HODLMM core swap (out=${executableRoute.amountOutHuman}); ` +
`falling back to the protected SDK route (out=${sdkFallback.amountOutHuman}). ` +
`Pass --allow-unprotected-hodlmm-swap to take the HODLMM route anyway (supervised use only).`
);
} else {
throw new Error(
"REFUSED: best route is a direct HODLMM core swap, which cannot carry a minimum-output bound " +
"(no min-out parameter; single-bin fills; Allow mode), and no protected SDK route exists for " +
"this pair. Re-run with --allow-unprotected-hodlmm-swap for supervised, small-size use after " +
"verifying the quote against an independent price. " +
`Quoted route: ${executableRoute.label} out=${executableRoute.amountOutHuman}.`
);
}
} else {
return this.executeHodlmmSwap(account, executableRoute, fee);
}
}

const sdk = this.ensureSdk();
Expand Down
Loading