UniswapV2-style router for the Velar DEX on Stacks โ handles add/remove liquidity and token swaps
| Contract | SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-router |
| Protocol | Velar โ UniswapV2 fork DEX on Stacks |
| Source | Verified on-chain via Hiro API (/v2/contracts/source) |
| Clarity Version | Pre-Clarity 4 (uses as-contract paradigm, no asset allowances) |
| Lines of Code | 174 |
| Confidence | ๐ก MEDIUM โ router-only audit; depends on univ2-core, univ2-library, and trait contracts not reviewed here |
| Date | February 24, 2026 |
This contract is a thin routing layer modeled after Uniswap V2's Router02. It computes optimal liquidity amounts, delegates to univ2-core for mint/burn/swap, and enforces user-specified slippage bounds. At 174 lines it is compact, but several design choices create real risk for users.
univ2-library.quote, then calls univ2-core.mintuniv2-core.burn, then checks output against user minimumsget-amount-out, executes swap, checks slippageget-amount-in, executes swap, checks max inputAll core logic (token transfers, reserve updates, fee collection) lives in univ2-core โ this contract is a convenience wrapper with validation.
Location: swap-exact-tokens-for-tokens, swap-tokens-for-exact-tokens
Description: The original Uniswap V2 Router includes a deadline parameter that reverts the transaction if it is mined after a user-specified block. This router omits it entirely. A swap transaction can sit in the mempool for an extended period, then be mined when market conditions have changed dramatically โ the user's amt-out-min may still be satisfied but at a much worse rate than intended.
Impact: MEV bots and validators can delay inclusion of swap transactions to extract value. Combined with Stacks' block timing variability, users may receive significantly worse execution than expected. This is the primary sandwich vector on any AMM.
;; No deadline parameter โ tx can be mined at any future block
(define-public
(swap-exact-tokens-for-tokens
(id uint)
(token0 <ft-trait>)
(token1 <ft-trait>)
(token-in <ft-trait>)
(token-out <ft-trait>)
(share-fee-to <share-fee-to-trait>)
(amt-in uint)
(amt-out-min uint))
;; ... no block-height check ...
Recommendation: Add a deadline parameter (block height) and assert (<= block-height deadline) at the start of each swap function. Alternatively, accept a max-block-delay parameter.
Location: swap-exact-tokens-for-tokens (lines ~88โ115), swap-tokens-for-exact-tokens (lines ~118โ160)
Description: Both swap functions execute the swap via contract-call? .univ2-core swap in the let bindings, before the asserts! that verify token0/token1 match the pool and that amounts are valid. If a user passes mismatched token traits, the swap is attempted against univ2-core before the router rejects it.
;; Swap happens here in the let-binding...
(event (try! (contract-call? .univ2-core swap
id token-in token-out share-fee-to amt-in amt-out)))
;; ...but validation happens AFTER
(asserts!
(and (is-eq (get token0 pool) (contract-of token0))
(is-eq (get token1 pool) (contract-of token1))
(> amt-in u0)
(> amt-out-min u0))
err-router-preconditions)
Impact: If univ2-core does not independently validate token trait matching, a malicious or incorrect token contract could be passed. Even if core validates correctly, the code wastes gas on reverted calls and violates the checks-effects-interactions pattern. The revert from try! at least prevents state changes, but the pattern is fragile.
Recommendation: Move all asserts! precondition checks before the let bindings that execute the swap. Validate inputs first, then execute.
Location: All public functions
Description: The router accepts token0, token1, lp-token, and share-fee-to as trait parameters from the caller. While the swap functions verify token0/token1 against the pool (after execution โ see M-01), the add-liquidity and remove-liquidity functions perform no token matching at all. They pass whatever traits the caller provides directly to univ2-core.
;; add-liquidity passes caller-supplied traits directly to core โ no validation
(contract-call? .univ2-core mint
id token0 token1 lp-token
(get amt0 amts) (get amt1 amts))
Impact: If univ2-core.mint or univ2-core.burn do not validate that the passed token traits match the pool's registered tokens, a caller could substitute a malicious token contract that implements the SIP-010 trait but behaves differently (e.g., stealing funds via transfer). The share-fee-to trait in swaps is similarly unvalidated โ a malicious fee contract could siphon fees.
Recommendation: Validate all trait parameters against the pool's registered addresses before calling core. For example: (asserts! (is-eq (contract-of token0) (get token0 pool)) err-router-preconditions) in every function.
Location: add-liquidity (lines ~49โ54)
Description: The assertions (>= amt0-min u0), (>= amt1-min u0), (>= amt0-desired u0), and (>= amt1-desired u0) are always true because all parameters are uint โ unsigned integers cannot be negative in Clarity.
(asserts!
(and (<= amt0-min amt0-desired)
(<= amt1-min amt1-desired)
(>= amt0-min u0) ;; always true
(>= amt1-min u0) ;; always true
(>= amt0-desired u0) ;; always true
(>= amt1-desired u0)) ;; always true
err-router-preconditions)
Impact: No security impact. Code noise that may mislead auditors into thinking these checks provide meaningful validation.
Recommendation: Remove the four redundant >= u0 checks. The meaningful checks are <= amt0-min amt0-desired and <= amt1-min amt1-desired.
Location: add-liquidity, remove-liquidity
Description: Unlike the swap functions which check (> amt-in u0), the liquidity functions do not prevent zero-amount operations. A user can call add-liquidity with amt0-desired = u0, amt1-desired = u0 or remove-liquidity with liquidity = u0.
Impact: Zero-amount operations waste gas and may create misleading events. On first liquidity provision (empty pool), zero amounts would create a pool with zero reserves.
Recommendation: Add (> amt0-desired u0), (> amt1-desired u0), and (> liquidity u0) guards.
Location: Entire contract
Description: This contract was deployed before Clarity 4 (epoch 3.3). While the router itself does not use as-contract, the downstream univ2-core likely does for token transfers. Clarity 4 introduced as-contract? with explicit with-ft/with-nft/with-stx allowances that enforce at the language level which assets a contract can move.
Impact: The entire Velar protocol stack operates without language-level asset allowances, relying on application-level checks in univ2-core.
Recommendation: When upgrading the protocol, migrate to Clarity 4 and use as-contract? with explicit asset allowances in the core contract.
Location: Entire contract
Description: Unlike Uniswap V2's Router02, this router only supports single-pool swaps. There is no multi-hop routing (e.g., AโBโC). Users who need multi-hop must compose multiple transactions, which exposes them to intermediate slippage and higher total fees.
Impact: Reduced capital efficiency and worse execution for users trading tokens without a direct pair. Each hop is a separate transaction with independent slippage risk.
Recommendation: Consider adding multi-hop swap functions that accept a path of pool IDs and enforce slippage only on the final output.
The Velar univ2-router is a minimal, clean implementation of a UniswapV2-style router at only 174 lines. Its simplicity is a strength โ less code means less attack surface. However, the missing deadline protection (H-01) is a real risk for users on a live DEX, and the execute-before-validate pattern (M-01) is a code smell that could become exploitable if the core contract's validation is ever weakened. The unverified trait parameters (M-02) mean the router's security posture depends entirely on univ2-core's validation โ a fragile assumption for a contract handling real user funds.
No critical findings were identified. The contract delegates all token custody to univ2-core and holds no assets itself, which limits the blast radius of any router-level bug.