Multi-hop swap router for StackSwap DEX
Contract: SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-swap-router-v1b
Source: Hiro API (on-chain)
Clarity Version: Pre-Clarity 4 (no epoch 3.3 features)
Confidence: ๐ข HIGH โ complete source reviewed, all findings verified against on-chain code
Date: February 24, 2026
This is the multi-hop swap router for StackSwap, one of the earliest DEXes on Stacks. It performs two-hop swaps through a bridge token: from โ bridge โ to, routing both legs through the StackSwap AMM (stackswap-swap-v5k).
The contract is remarkably simple at ~60 lines โ a single public function router-swap. It uses a balance-delta approach to calculate actual amounts received at each hop, which is the correct pattern for composable DEX routers. The contract is stateless and custody-free โ it holds no funds and relies entirely on the underlying swap contract for asset transfers.
router-swap โ two sequential swaps through a bridge tokenstackswap-swap-v5k for both swap legs (swap-x-for-y / swap-y-for-x)stackswap-security-list-v1a.is-secure-router-or-user for access controlfrom-2-bridge-min-amt, bridge-2-to-min-amt)u1 for intermediate swapscontract-caller โ properly prevents unauthorized contract integrationsThe router has no deadline parameter. A swap transaction can sit in the mempool indefinitely and execute at a time when market conditions have changed dramatically, resulting in the user receiving far less than expected.
How it works: A user submits a swap with min-amounts based on current prices. The transaction gets delayed (congestion, low fee, or deliberate withholding by a miner). Hours or days later, prices have moved significantly. The transaction executes and still passes the min-amount checks (if the user set them loosely), but the user receives much less value than they would have at submission time.
;; No deadline check โ swap can execute at any future block
(define-public (router-swap
(from <sip-010-token>)
(bridge <sip-010-token>)
(to <sip-010-token>)
(from-lp <liquidity-token>)
(to-lp <liquidity-token>)
(from-type bool)
(to-type bool)
(from-amt uint)
(from-2-bridge-min-amt uint)
(bridge-2-to-min-amt uint)
;; Missing: (deadline uint) with (asserts! (<= block-height deadline) ERR_EXPIRED)
)
Impact: Users may unknowingly execute swaps at stale prices. Combined with sandwich attacks (manipulate price โ let delayed tx execute โ reverse), this becomes a MEV extraction vector.
Recommendation: Add a deadline parameter (block height) and assert (<= block-height deadline) at the start of the function. This is standard practice in Uniswap-style routers.
(and from-type true)
Both swap direction checks use (and from-type true) and (and to-type true), which are logically equivalent to just from-type and to-type. The (and x true) pattern is a no-op.
;; Current (redundant):
(if (and from-type true) ...)
(if (and to-type true) ...)
;; Should be:
(if from-type ...)
(if to-type ...)
Impact: No functional impact. This is dead code that suggests the developer may have intended a second condition that was never added, or it's a copy-paste artifact.
Recommendation: Simplify to (if from-type ...) and (if to-type ...).
pre_from_amt Computed but Never Used
The function reads the user's initial from token balance into pre_from_amt but never references it. The return value uses the input parameter from-amt directly instead of computing a delta like it does for bridge and to tokens.
(let
(
(pre_from_amt (unwrap-panic (contract-call? from get-balance tx-sender))) ;; never used
...
)
...
(ok (list from-amt delta_bridge_amt delta_to_amt)) ;; uses from-amt, not delta
)
Impact: Wastes gas on an unnecessary external call. More importantly, the return value reports the requested input amount rather than the actual amount deducted, which could differ if the token has transfer fees or rebasing mechanics.
Recommendation: Either remove pre_from_amt or compute delta_from_amt for an accurate return value: (- pre_from_amt (contract-call? from get-balance tx-sender)).
unwrap-panic on External Trait Calls
All six get-balance calls use unwrap-panic. If any token's get-balance returns an error (e.g., a non-compliant SIP-010 implementation), the entire transaction aborts with a runtime error rather than a descriptive error code.
Impact: Poor error reporting. Users see a generic abort rather than understanding which token failed. Not exploitable, but degrades UX.
Recommendation: Use unwrap! with descriptive error codes (e.g., ERR_BALANCE_CHECK_FAILED).
Access to the router is gated by stackswap-security-list-v1a.is-secure-router-or-user. This external contract controls who can use the router. If the security list is misconfigured or the StackSwap team removes a previously-approved caller, integrations break without warning.
(asserts! (contract-call? .stackswap-security-list-v1a is-secure-router-or-user contract-caller) ERR_INVALID_ROUTER)
Impact: Standard centralization trade-off. The security list provides protection against malicious contract interactions but creates a single point of control. This is a reasonable design choice for a DEX router.
This contract predates Clarity 4 (epoch 3.3). However, since the router never uses as-contract and holds no assets, the absence of Clarity 4's as-contract? with explicit asset allowances is not a security concern here. The contract operates entirely under the user's authority via tx-sender.
Impact: None for this specific contract. Noted for completeness.
| Metric | Score | Weight | Weighted |
|---|---|---|---|
| Financial risk | 3 (DeFi swap router) | 3 | 9 |
| Deployment likelihood | 3 (deployed mainnet) | 2 | 6 |
| Code complexity | 1 (~60 lines) | 2 | 2 |
| User exposure | 3 (StackSwap โ established DEX) | 1.5 | 4.5 |
| Novelty | 1 (similar to other router audits) | 1.5 | 1.5 |
| Total | 2.3 / 3.0 | ||
Clarity version penalty: -0.5 โ Final: 1.8 (meets threshold)
This is a well-designed, minimal router with a small attack surface. The stateless, custody-free architecture means there are no stored funds to steal. Both swap legs have user-specified slippage protection, which is better than many competing routers that pass hardcoded minimums for intermediate hops.
The only actionable finding is the missing swap deadline (M-01), which is a standard DEX router issue. The remaining findings are cosmetic (redundant logic, unused variable) or informational (centralized security list, pre-Clarity 4).
Overall risk: LOW. This contract's simplicity is its strength. The real security surface is in the underlying stackswap-swap-v5k AMM contract (see separate audit).