Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 (fractional; previously hardcoded `3`, audit F-6).
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
44 changes: 36 additions & 8 deletions src/lib/services/bitflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,11 @@ export class BitflowService {
async getUnifiedRouteQuotes(
tokenXId: string,
tokenYId: string,
amount: number
amount: number,
// Fractional slippage tolerance (e.g. 0.01 = 1%), matching the quotes-plane
// response-field semantics. 2026-07-15 field audit F-6: previously hardcoded
// to `3` (ambiguous scale) 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 +937,7 @@ export class BitflowService {
output_token: hodlmmTokenYContract,
amount_in: hodlmmAmountIn,
amm_strategy: "best",
slippage_tolerance: 3,
slippage_tolerance: slippageTolerance,
}),
}
);
Expand Down Expand Up @@ -1237,11 +1241,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 +1311,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,17 +1567,39 @@ 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") {
// 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) {
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). This path is unsafe for unattended use. " +
"Use an SDK-routed pair instead, or 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}.`
);
}
return this.executeHodlmmSwap(account, executableRoute, fee);
}

Expand Down
Loading