SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-v1-03 · On-chain source (immutable) · Audited: February 24, 2026
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.
| Metric | Score | Weight | Weighted |
|---|---|---|---|
| Financial risk | 3 — DeFi swap routing | 3 | 9 |
| Deployment likelihood | 3 — Deployed mainnet | 2 | 6 |
| Code complexity | 2 — 457 lines, multi-pool routing | 2 | 4 |
| User exposure | 3 — Major Stacks DEX | 1.5 | 4.5 |
| Novelty | 2 — DEX router category | 1.5 | 3 |
| Total | 26.5 / 30 = 2.65 | ||
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:
Source verified on-chain via Hiro API. Findings verified against source. Some assumptions about underlying pool contract behavior (not in scope).
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.
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.
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.
div-down when pool balance is zeroLocation: 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.
fee-helper returns u0 silently for unknown routesLocation: 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.
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.
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.
route-helper returns raw pair for unknown routes instead of errorLocation: 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.
as-contract usage: The contract never acts with its own authority, so there's no risk of malicious token contracts exploiting as-contract scope.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.