On-chain: SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR.stableswap-swap-helper-v-1-5 ·
~280 lines ·
Clarity 2 ·
Audited: February 24, 2026
The stableswap-swap-helper-v-1-5 contract is a multi-hop swap router for Charisma DEX's stableswap pools. It provides functions to swap tokens through 1 to 5 consecutive stableswap pools, with matching quote (read-only-equivalent) functions. Each swap deducts an aggregator fee before routing through stableswap-core-v-1-4.
The contract is stateless — it holds no data vars, no maps, no stored balances. It acts purely as a routing layer, delegating all actual swaps to the core stableswap contract and all fee logic to aggregator-core-v-1-1. This is a good architectural pattern that minimizes the helper's attack surface.
Confidence: 🟡 MEDIUM — Source verified on-chain. Some findings depend on behavior of external contracts (stableswap-core-v-1-4, aggregator-core-v-1-1) which were not audited here.
| ID | Severity | Title |
|---|---|---|
| M-01 | Medium | Intermediate swaps use hardcoded u1 min-amount — sandwich vector |
| M-02 | Medium | Pre-Clarity 4 as-contract gives blanket asset authority |
| L-01 | Low | Quote functions are define-public instead of define-read-only |
| L-02 | Low | unwrap-panic in path detection aborts with no error code |
| I-01 | Info | Clarity 2 — recommend migration to Clarity 4 |
| I-02 | Info | Repetitive code structure across swap helpers |
| I-03 | Info | Good: stateless custody-free design |
u1 min-amount — sandwich vectorLocation: stableswap-sa (private function)
Description: Every individual swap call to stableswap-core-v-1-4 passes u1 as the minimum output amount. While the final output is checked against the user's min-received parameter, intermediate hops in multi-pool swaps (swap-helper-b through swap-helper-e) have effectively zero slippage protection.
;; In stableswap-sa:
(swap-a (if (is-eq is-reversed false)
(try! (contract-call?
.stableswap-core-v-1-4 swap-x-for-y
pool
token-in token-out
amount u1)) ;; <-- hardcoded u1
(try! (contract-call?
.stableswap-core-v-1-4 swap-y-for-x
pool
token-out token-in
amount u1)))) ;; <-- hardcoded u1
Impact: A sandwich attacker can manipulate intermediate pool prices between hops. On a 5-hop swap, the attacker gets 4 opportunities to extract value from intermediate pools. The final min-received check catches the total damage but doesn't prevent it at each step — this makes the economics worse for the swapper than necessary.
Recommendation: This is a known trade-off in multi-hop routers — computing per-hop minimums requires knowing each pool's state. The final slippage check is the practical mitigation. Users should set tight min-received values, especially on multi-hop routes. Consider exposing per-hop min-amounts as parameters for advanced users.
as-contract gives blanket asset authorityLocation: get-aggregator-fees and transfer-aggregator-fees (private functions)
Description: The contract uses (as-contract tx-sender) to pass its own principal to the aggregator core. In Clarity 2, as-contract gives the enclosed expression unrestricted authority to move any asset the contract holds. While this contract is stateless and shouldn't hold assets, any tokens accidentally sent to it could be moved by the aggregator call.
(define-private (transfer-aggregator-fees (token <stableswap-ft-trait>) (provider (optional principal)) (amount uint))
(let (
(call-a (try! (contract-call?
'SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR.aggregator-core-v-1-1 transfer-aggregator-fees
token (as-contract tx-sender) provider amount)))
;; ^^^ blanket as-contract authority
(amount-after-fees (- amount (get amount-fees-total call-a)))
)
(ok amount-after-fees)
)
)
Impact: Low in practice because the contract is stateless. However, if any tokens are sent to this contract by mistake, the aggregator (or any contract it calls) could potentially move them under as-contract authority.
Recommendation: Migrate to Clarity 4 and use as-contract? with explicit with-ft allowances to restrict which assets can be moved. This would make the contract provably safe even if it accidentally holds assets.
define-public instead of define-read-onlyLocation: get-quote-a through get-quote-e
Description: All five quote functions are defined as define-public despite performing no state mutations. They only call get-aggregator-fees (which calls a read function on the aggregator) and stableswap-qa (which calls get-dy/get-dx read functions on the core).
Impact: Callers pay unnecessary gas when using quote functions. They also cannot be called from define-read-only contexts in other contracts.
Recommendation: Change to define-read-only if the downstream calls (get-aggregator-fees, get-dy, get-dx) are also read-only. If any downstream call is define-public, this is a Clarity limitation and the current approach is correct. Note: get-aggregator-fees in this contract is private and calls a public function on the aggregator core — if that function is truly read-only but defined as public, this propagates unnecessarily.
unwrap-panic in path detection aborts with no error codeLocation: is-stableswap-path-reversed
Description: The path detection function uses unwrap-panic to extract pool data. If get-pool fails (e.g., invalid pool contract passed), the entire transaction aborts with a runtime error rather than returning a meaningful error code.
(define-private (is-stableswap-path-reversed
(token-in <stableswap-ft-trait>) (token-out <stableswap-ft-trait>)
(pool-contract <stableswap-pool-trait>)
)
(let (
(pool-data (unwrap-panic (contract-call? pool-contract get-pool)))
;; ^^^ panics if get-pool fails
)
(not
(and
(is-eq (contract-of token-in) (get x-token pool-data))
(is-eq (contract-of token-out) (get y-token pool-data))
)
)
)
)
Impact: Users passing an invalid pool contract get an opaque runtime error instead of a descriptive error code. This makes debugging harder but doesn't create a security vulnerability.
Recommendation: Refactor to return a (response bool uint) and propagate the error, or use unwrap! with a descriptive error constant like ERR_INVALID_POOL. Note: since this is a private function called from both stableswap-qa and stableswap-sa, both callers would need to handle the new response type.
Description: The contract uses Clarity 2. Clarity 4 (epoch 3.3) introduced as-contract? with explicit asset allowances, contract-hash? for deployment verification, and other safety improvements. Migrating would address M-02 and improve overall security posture.
Description: The five swap helpers (a through e) and five quote functions follow an identical pattern, differing only in the number of hops. This is ~280 lines of largely duplicated code. While Clarity's lack of loops/recursion makes this necessary, it increases maintenance burden and the risk of inconsistency between variants.
Description: The contract holds no state variables, maps, or persistent data. It acts as a pure routing layer, delegating all swaps to stableswap-core-v-1-4 and fees to aggregator-core-v-1-1. This is an excellent pattern: the helper cannot be drained because it never holds funds, and there's no admin functionality to compromise. The only attack surface is through the external contract calls it makes.
The contract's dependency graph:
User
└─→ stableswap-swap-helper-v-1-5 (this contract)
├─→ aggregator-core-v-1-1 (fee calculation & transfer)
├─→ stableswap-core-v-1-4 (actual swaps: swap-x-for-y, swap-y-for-x)
├─→ stableswap-pool-trait-v-1-4 (pool interface: get-pool)
└─→ sip-010-trait-ft-standard-v-1-1 (token interface)
The helper detects token path direction via is-stableswap-path-reversed, which checks whether token-in matches the pool's x-token. If reversed, it calls swap-y-for-x / get-dx instead of swap-x-for-y / get-dy. This bidirectional routing is correct and well-implemented.
This is a low-risk helper contract with solid design. The stateless architecture means there are no fund custody risks. The two medium findings (intermediate slippage and pre-Clarity 4 as-contract) are common patterns across the Charisma DEX ecosystem and represent known trade-offs rather than exploitable bugs. The final min-received check on each swap helper provides adequate end-to-end slippage protection for users who set appropriate values.