univ2-path2
Velar univ2-path2
Multi-hop swap router for the Velar DEX — supports 2-to-5 token paths via Uniswap V2-style AMM pools
| Contract | SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-path2 |
| Protocol | Velar — Uniswap V2-style DEX on Stacks |
| Source | Verified on-chain via Hiro API (/v2/contracts/source) |
| Clarity Version | Pre-Clarity 4 (publish height 153315) |
| Lines of Code | ~150 |
| Confidence | 🟢 HIGH — simple routing contract, all logic visible; delegates actual swaps to univ2-router |
| Date | February 25, 2026 |
Overview
univ2-path2 is a multi-hop swap routing contract for the Velar DEX. It composes single-hop swaps through univ2-router.swap-exact-tokens-for-tokens to support paths of 2 to 5 tokens. The contract includes its own copy of the constant-product AMM formula (get-amount-out) for computing expected outputs and providing read-only quote functions.
Architecture
- Single swap:
do-swaplooks up the pool viauniv2-core.lookup-pool, computes the expected output using the local AMM formula, and delegates execution touniv2-router. - Multi-hop:
swap-3,swap-4,swap-5chain sequentialdo-swapcalls, piping each hop's output as the next hop's input. A finalamt-out-mincheck protects the entire path. - Quotes:
get-amount-out-3/4/5provide read-only multi-hop output estimates using the same formula. - No admin functions: The contract is stateless and has no owner, no configuration, and no upgradability.
Trust Assumptions
univ2-corecorrectly reports pool reserves and swap feesuniv2-routerexecutes swaps consistently with the AMM formula replicated in this contract- The
share-fee-totrait implementation is trusted (user-supplied)
Findings
M-01: Zero output amount not validated — dust swaps silently lose funds
Location: swap-args → get-amount-out
Description: The get-amount-out function uses integer division, which truncates to zero for very small inputs relative to reserves. When this happens, swap-args sets amt-out-min: u0, and the swap proceeds — the user spends their input tokens and receives nothing in return.
;; get-amount-out can return 0 for dust amounts:
;; (/ (* amt-in-adjusted reserve-out)
;; (+ reserve-in amt-in-adjusted))
;; → 0 when amt-in-adjusted * reserve-out < reserve-in + amt-in-adjusted
;; swap-args only validates input, not output:
(asserts! (and (> amt-in u0)) err-preconditions)
;; Missing: (asserts! (> amt-out u0) err-postconditions)
Impact: Users sending very small amounts (dust) will lose their tokens with zero output. While unlikely for intentional swaps, this can occur in multi-hop paths where intermediate outputs shrink through compounding rounding losses. Bots or contracts composing with this router could trigger this inadvertently.
Recommendation: Add output validation in swap-args:
(asserts! (> amt-out u0) err-postconditions)
L-01: unwrap-panic in swap-args produces unhelpful errors
Location: swap-args function — two unwrap-panic calls
Description: swap-args uses unwrap-panic for both lookup-pool and get-pool-id results. If a pool doesn't exist for the given token pair, the transaction aborts with a runtime panic rather than returning a descriptive error code.
(let ((res (unwrap-panic
(contract-call? .univ2-core lookup-pool
(contract-of token-in) (contract-of token-out))))
(id (unwrap-panic
(contract-call? .univ2-core get-pool-id ...))))
Impact: Users and integrators see an opaque runtime error instead of a meaningful error code when attempting to swap through a non-existent pool. This complicates debugging and error handling for downstream contracts.
Recommendation: Replace unwrap-panic with unwrap! and a descriptive error constant (e.g., (err u2003) for "pool not found").
L-02: Unused ids parameter in get-amount-out-4
Location: get-amount-out-4
Description: The function accepts an (ids (list 4 uint)) parameter that is never referenced in the function body. The other quote functions (get-amount-out-3, get-amount-out-5) do not have this parameter, suggesting it was left in by mistake.
(define-read-only
(get-amount-out-4
(amt-in uint)
(token-a <ft-trait>)
(token-b <ft-trait>)
(token-c <ft-trait>)
(token-d <ft-trait>)
(ids (list 4 uint))) ;; <-- never used
...)
Impact: Callers must supply a meaningless parameter. Since the contract is deployed and immutable, this cannot be changed, but integrators should be aware they can pass any list value.
Recommendation: If redeploying, remove the unused parameter. Document for integrators that ids is ignored.
I-01: Pre-Clarity 4 contract
Description: Deployed before Clarity 4. This contract doesn't directly hold or transfer assets (it delegates to univ2-router), so the lack of as-contract? is not a direct risk here. However, it cannot use contract-hash? for on-chain verification.
Recommendation: If the router is ever redeployed, use Clarity 4.
I-02: No transaction deadline mechanism
Description: Unlike Uniswap V2's router (which accepts a deadline parameter), this contract has no mechanism to expire stale transactions. A signed transaction could sit in the mempool and execute much later when market conditions have changed unfavorably.
Impact: Users relying on the amt-out-min parameter are still protected against slippage, but may receive a worse-than-expected price if market conditions shift between signing and execution.
Recommendation: Add an optional block-height deadline parameter: (asserts! (<= block-height deadline) (err u2004))
I-03: Per-hop slippage uses exact calculated output (zero tolerance)
Description: do-swap computes the expected output via get-amount-out and passes it as amt-out-min to the router — zero slippage tolerance per hop. This works if the router's internal formula is identical. Any discrepancy (rounding, fee calculation differences) would cause every swap to fail.
Impact: If univ2-router uses even a slightly different formula or rounding strategy than the local get-amount-out, all swaps through this contract would revert. In practice, the formula appears to be consistent (the router likely uses the same constant-product formula), but this is a fragile coupling.
Recommendation: This is by design and works as long as the formulas match. Document the dependency for future maintainers.
Positive Observations
- End-to-end slippage protection: Multi-hop swaps (
swap-3/4/5) enforce a user-specifiedamt-out-minon the final output, protecting against sandwich attacks across the full path. - Correct pool direction handling: The
flippedflag fromlookup-poolis properly handled — reserves and token ordering are swapped consistently. - Stateless design: No maps, no variables, no owner. The contract is pure routing logic with no admin surface to attack.
- Clean composition: Each hop's output feeds cleanly into the next via
(get amt-out b), maintaining type safety. - Read-only quotes:
get-amount-out-3/4/5allow UIs and bots to preview multi-hop outputs without executing transactions.
Related Contracts
SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-router— executes actual swapsSP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-core— pool registry and reserve storage