Bitflow swap-helper-v1-03 — Security Audit

SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-v1-03 · On-chain source (immutable) · Audited: February 24, 2026

0
Critical
1
High
2
Medium
2
Low
3
Info

Overview

swap-helper-v1-03 is Bitflow/ALEX's DEX routing contract deployed on Stacks mainnet. It acts as a stateless swap router that finds the optimal path between two tokens across two pool types: fixed-weight-pool-v1-01 (50/50 weighted pools with wSTX as base) and simple-weight-pool-alex (ALEX governance token as base).

The contract supports direct swaps, 2-hop routes (through wSTX or ALEX), and 3-hop cross-pool routes (e.g., token → wSTX → ALEX → token). It also provides oracle price feeds (instant and EMA-resilient), fee calculation helpers, and route discovery.

457 lines · Pre-Clarity 4 · No as-contract usage · No token custody · All tokens flow through underlying pool contracts.

Priority Score: 2.65 / 3.0

MetricScoreWeightWeighted
Financial risk3 — DeFi swap routing39
Deployment likelihood3 — Deployed mainnet26
Code complexity2 — 457 lines, multi-pool routing24
User exposure3 — Major Stacks DEX1.54.5
Novelty2 — DEX router category1.53
Total26.5 / 30 = 2.65

Architecture

The contract is a pure routing layer — it holds no tokens and has no admin functions. The only mutable state is two oracle price maps (fwp-oracle-resilient-map and simple-oracle-resilient-map) updated as a side effect of swaps.

Route resolution follows a priority cascade:

  1. Fixed-weight pool — direct or 2-hop through wSTX
  2. Simple-weight pool — direct or 2-hop through ALEX
  3. Fixed → Simple — cross-pool via wSTX → ALEX bridge
  4. Simple → Fixed — cross-pool via ALEX → wSTX bridge (implicit fallthrough)

Audit Confidence: Medium

Source verified on-chain via Hiro API. Findings verified against source. Some assumptions about underlying pool contract behavior (not in scope).

Findings

High H-01: Intermediate swaps in multi-hop routes lack slippage protection

Location: swap-helper function — all multi-hop branches

Description: When routing through 2 or 3 hops, intermediate swap calls pass none for the min-dy parameter. Only the final output is checked against the user's slippage tolerance. This means an attacker can sandwich the intermediate swap(s) to extract value, as long as the final output still meets the user's minimum.

;; Example: fixed→simple 3-hop route (token-x → wSTX → ALEX → token-y)
;; First swap: no slippage check
(get dx (try! (contract-call? .fixed-weight-pool-v1-01 swap-y-for-wstx
  token-x-trait u50000000 dx none)))             ;; ← none = no min-dy

;; Second swap: no slippage check
(get dy (try! (contract-call? .fixed-weight-pool-v1-01 swap-wstx-for-y
  .age000-governance-token u50000000 ... none)))  ;; ← none = no min-dy

;; Third swap: only this one checks min-dy
(get dy (try! (contract-call? .simple-weight-pool-alex swap-alex-for-y
  token-y-trait ... min-dy)))                     ;; ← user's slippage check

Impact: In a 3-hop route, a sandwich attacker can manipulate intermediate pool prices to extract MEV. The attacker front-runs with a large trade on an intermediate pool, the victim's intermediate swap executes at a worse price, and the attacker back-runs to profit. The final output may still meet the user's min-dy if the slippage tolerance is generous, but the user receives less than they would have without the attack. With tight slippage, the tx reverts entirely (griefing).

Recommendation: Pass calculated intermediate minimums based on expected rates with a tolerance band, or use a cumulative slippage check approach. Alternatively, document that callers should set tight min-dy values to limit total extractable value across all hops.

Medium M-01: Oracle resilient maps are writable by any swapper — potential price manipulation

Location: fwp-oracle-resilient-map, simple-oracle-resilient-map — updated in swap-helper

Description: The resilient oracle maps are updated by every swap-helper call with an exponential moving average (EMA). While the EMA smoothing provides some resistance, any user can influence the oracle price by executing swaps. A series of directional swaps can gradually shift the resilient oracle toward a target price.

;; EMA update formula in fwp-oracle-resilient-internal:
(ok (+ (mul-down (- ONE_8 (get oracle-average pool))
         (try! (fwp-oracle-instant-internal token-x token-y)))
       (mul-down (get oracle-average pool) value)))

Impact: If other contracts on Stacks use oracle-resilient-helper as a price feed (e.g., for liquidations or collateral valuation), an attacker could manipulate the oracle over multiple blocks. The cost is the swap fees plus any slippage incurred. The oracle-average parameter from the pool determines the smoothing factor — a high value makes manipulation harder but slower to react to real price changes.

Recommendation: Contracts consuming these oracle values should apply their own sanity checks (e.g., max deviation from external price feeds, TWAP over multiple blocks). Consider adding a separate oracle update mechanism with access controls rather than coupling oracle updates to swap execution.

Medium M-02: Swap fallthrough assumes simple→fixed route without validation

Location: swap-helper — final else branch

Description: After checking fixed-weight, simple-weight, and fixed→simple routes, the swap-helper function falls through to an implicit simple→fixed route without first validating that is-from-simple-alex-to-fixed returns a non-zero value. If a token pair doesn't match any known route, the function will attempt the simple→fixed swap anyway, resulting in a confusing error from the underlying pool contract rather than a clear "no route found" error.

;; In swap-helper: the final else branch handles simple→fixed
;; but never checks is-from-simple-alex-to-fixed first
(if (> (is-from-fixed-to-simple-alex token-x token-y) u0)
    ;; ... handle fixed→simple ...
    ;; ELSE: assumes simple→fixed without validation ↓
    (if (is-eq token-y .token-wstx)
        ;; attempts swap-y-for-alex then swap-alex-for-wstx
        ...

Impact: Users passing unsupported token pairs will get opaque errors from downstream pool contracts instead of a clear routing error. This complicates debugging and could cause front-end integration issues. No funds at risk — the underlying calls will simply fail.

Recommendation: Add an explicit check: (asserts! (> (is-from-simple-alex-to-fixed token-x token-y) u0) ERR-NO-ROUTE) before the fallthrough branch.

Low L-01: Division by zero in div-down when pool balance is zero

Location: div-down private function

Description: The div-down function checks for a = 0 but not b = 0. If a pool has a zero balance for one token (e.g., drained pool), any oracle query against that pool will panic with a runtime division-by-zero error.

(define-private (div-down (a uint) (b uint))
  (if (is-eq a u0)
    u0
    (/ (* a ONE_8) b)  ;; panics if b = 0
  )
)

Impact: Low — a pool with zero balance would already be non-functional. However, the panic aborts the entire transaction rather than returning a graceful error, which could affect multi-step transactions that query oracle prices.

Recommendation: Add a check: (if (is-eq b u0) u0 (/ (* a ONE_8) b)) or return an error response.

Low L-02: fee-helper returns u0 silently for unknown routes

Location: fee-helper — final else branch

Description: When a token pair doesn't match any known route configuration, fee-helper returns (ok u0) instead of an error. A caller relying on this function to display fees to users would show "0% fee" for unsupported pairs, which is misleading.

;; Final else in fee-helper:
u0  ;; silently returns 0 fee for unknown routes

Impact: Front-end or integrating contracts may display incorrect fee information. No direct funds at risk since swap-helper would fail independently for unsupported pairs.

Recommendation: Return an error (e.g., (err u7001)) for unrecognized routes instead of u0.

Info I-01: Hardcoded 50/50 pool weights limit routing flexibility

Location: All fixed-weight-pool-v1-01 calls

Description: Every call to the fixed-weight pool uses hardcoded weight parameters u50000000 u50000000 (50%/50%). This limits the router to only 50/50 balanced pools and cannot route through pools with different weight configurations (e.g., 80/20).

Impact: Design limitation — not a vulnerability. Pools with non-50/50 weights are unreachable through this helper.

Info I-02: Pre-Clarity 4 deployment

Location: Entire contract

Description: The contract was deployed before Clarity 4 (epoch 3.3). It does not use as-contract and holds no tokens, so the absence of Clarity 4's as-contract? with explicit asset allowances is not a direct risk here. However, any future version that needs to custody tokens should use Clarity 4.

Impact: Informational. The contract's stateless routing design mitigates the usual pre-Clarity 4 risks.

Info I-03: route-helper returns raw pair for unknown routes instead of error

Location: route-helper — final else branch

Description: When no known route exists between two tokens, route-helper returns (list token-x token-y) as if a direct route exists. This is misleading — callers may attempt a swap that will fail.

;; Final else in route-helper:
(list token-x token-y)  ;; implies direct route exists

Impact: Informational — front-ends may show a route that doesn't actually work.

Positive Observations

Summary

The swap-helper-v1-03 contract is a well-designed stateless DEX routing layer. Its primary strength is that it never holds tokens — it delegates all custody and transfer logic to the underlying pool contracts, significantly reducing the attack surface.

The most significant finding is H-01: intermediate swaps in multi-hop routes have no slippage protection, making users vulnerable to sandwich attacks on intermediate legs. This is a common pattern in DEX routers but should be mitigated with tighter slippage controls or documented prominently for integrators.

The oracle manipulation risk (M-01) depends on how widely the resilient oracle values are consumed by other protocols. If used as a price feed for lending/liquidation, it should be hardened with additional checks.

Overall, the contract's immutable, stateless, and custody-free design makes it relatively low-risk for a DeFi routing contract.