ALEX AMM Swap Pool v1-1

Core AMM pool contract for the ALEX DEX — weighted constant-product/constant-sum hybrid with oracle integration, multi-hop routing, and LP token management

ContractSP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v1-1
ProtocolALEX — leading DEX on Stacks
SourceVerified on-chain via Hiro API (/v2/contracts/source)
Clarity VersionPre-Clarity 4 (uses as-contract, not as-contract?)
Lines of Code1,071
Audit Date2026-02-25
Confidence🟡 MEDIUM — complex math library, multi-contract system (vault, LP tokens)
0
Critical
1
High
4
Medium
2
Low
2
Info

Overview

This is the core AMM swap pool contract for the ALEX DEX, the largest decentralized exchange on Stacks. The contract implements a hybrid constant-product / constant-sum bonding curve parameterized by a factor weight, supporting:

The contract delegates actual token custody to .alex-vault-v1-1 and LP token accounting to .token-amm-swap-pool-v1-1. It includes a full fixed-point math library (~400 lines) implementing exp, ln, pow for the weighted curve calculations.

Architecture

Pools are identified by a (token-x, token-y, factor) triple. The factor parameter controls the bonding curve shape:

This dual-mode approach allows pools to range from constant-sum-like (low slippage, concentrated) to constant-product-like behavior depending on the weight parameter.

Findings

HIGH

H-01: Multi-Hop Swaps Have No Intermediate Slippage Protection

Location: swap-helper-a, swap-helper-b, swap-helper-c

Description: The multi-hop swap helper functions pass none for the min-dy / min-dx parameter on all intermediate hops. Only the final hop in swap-helper-a respects the user's slippage parameter. The higher-order helpers (swap-helper-b, swap-helper-c) don't even pass the user's min-dw / min-dv to the final hop.

;; swap-helper-b: user's min-dw is IGNORED
(define-public (swap-helper-b 
    (token-x-trait <ft-trait>) (token-y-trait <ft-trait>) 
    (token-z-trait <ft-trait>) (token-w-trait <ft-trait>) 
    (factor-x uint) (factor-y uint) (factor-z uint)
    (dx uint) (min-dw (optional uint)))
    (swap-helper token-z-trait token-w-trait factor-z 
        (try! (swap-helper-a token-x-trait token-y-trait 
               token-z-trait factor-x factor-y dx none)) 
        none)  ;; <-- min-dw never used!
)

Impact: Users executing 3-hop or 4-hop swaps via swap-helper-b or swap-helper-c have zero slippage protection. A sandwich attacker can extract maximum value from these trades. Even swap-helper-a (2-hop) lacks protection on the first hop, allowing partial sandwich extraction.

Recommendation: Pass the user's minimum output parameter to the final swap-helper call. Consider also computing intermediate minimum amounts based on expected rates to protect each hop individually.

MEDIUM

M-01: Fee Can Consume Entire Swap Amount

Location: swap-x-for-y, swap-y-for-x

Description: When the fee rate is set high enough that fee >= dx, the net amount becomes zero via saturating subtraction:

(fee (mul-up dx (get fee-rate-x pool)))
(dx-net-fees (if (<= dx fee) u0 (- dx fee)))

The swap proceeds with dx-net-fees = 0, which passes the (asserts! (> dx u0)) check (since dx is the gross amount). The user sends tokens but receives nothing. The fee accounting also breaks: (- fee fee-rebate) sends the full fee to reserves while the pool balance increases by zero.

Impact: If the pool owner or contract owner sets an extremely high fee rate (≥ 100%), users lose their entire swap input to fees. While this requires malicious admin action, the contract doesn't bound fee rates.

Recommendation: Add a maximum fee rate constant (e.g., 10%) enforced in set-fee-rate-x and set-fee-rate-y. Alternatively, assert (> dx-net-fees u0) after fee deduction.

MEDIUM

M-02: Saturating Balance Subtraction Can Desync Pool State from Vault

Location: swap-x-for-y, swap-y-for-x, reduce-position

Description: Balance updates use saturating subtraction patterns like:

balance-y: (if (<= balance-y dy) u0 (- balance-y dy))

If due to rounding or edge conditions dy > balance-y, the pool's recorded balance goes to zero while the vault still holds the real tokens. This creates a permanent desync — the pool's internal accounting no longer reflects actual vault holdings.

Impact: Desynced accounting means pool pricing becomes incorrect. Remaining LPs may not be able to withdraw their full share. In the worst case, tokens become permanently locked in the vault with no pool balance to claim them against.

Recommendation: Replace saturating subtraction with strict subtraction that reverts the transaction if the balance would underflow. This ensures accounting always stays consistent with actual transfers.

MEDIUM

M-03: Oracle Price Manipulation via Large Trades

Location: swap-x-for-y, swap-y-for-x, get-oracle-resilient

Description: The oracle updates oracle-resilient on every swap using an EWMA formula:

oracle-resilient: (+ (mul-down (- ONE_8 oracle-average) instant-price) 
                     (mul-down oracle-average oracle-resilient))

The issue is that oracle-resilient is updated before the pool balances are modified in the map-set, but the get-oracle-instant call reads from the pre-swap pool state. This means the oracle always lags by one trade. More critically, a large trade can significantly shift the instant price, and repeated trades in the same block can compound the oracle shift.

Impact: An attacker with sufficient capital can manipulate the oracle price by executing a series of large swaps in a single block. Any external contract relying on get-oracle-resilient for pricing decisions (liquidations, collateral valuation) could be exploited.

Recommendation: Consider updating the oracle based on the pre-trade balances explicitly, or use a time-weighted mechanism that limits per-block oracle movement. Document the oracle's security properties so downstream consumers understand the manipulation bounds.

MEDIUM

M-04: Pre-Clarity 4 as-contract — Blanket Asset Access

Location: add-to-position, reduce-position, swap-x-for-y, swap-y-for-x

Description: The contract uses as-contract (pre-Clarity 4) for all vault interactions and LP token operations. This grants blanket access to all assets the contract holds or can access during those calls.

(as-contract (try! (contract-call? .alex-vault-v1-1 transfer-ft token-y-trait dy sender)))
(as-contract (try! (contract-call? .token-amm-swap-pool-v1-1 mint-fixed ...)))

Impact: If any of the called contracts (vault, LP token) have vulnerabilities or if a malicious token trait is passed, the as-contract scope gives broader access than necessary. Clarity 4's as-contract? with explicit with-ft/with-stx allowances would limit the blast radius.

Recommendation: When migrating to Clarity 4, replace as-contract with as-contract? using explicit asset allowances (e.g., (with-ft .token-amm-swap-pool-v1-1)).

LOW

L-01: No Fee Rate Bounds Validation

Location: set-fee-rate-x, set-fee-rate-y

Description: Fee rates can be set to any uint value, including values exceeding ONE_8 (100%). The pool owner or contract owner can set arbitrarily high fees without any upper bound check.

Impact: A compromised or malicious pool owner/admin could set fees to extract all value from swaps. This is partially mitigated by the fact that users can check fee rates before trading, and post-conditions can limit token outflow.

Recommendation: Add (asserts! (<= fee-rate-x MAX-FEE-RATE) ERR-...) with a reasonable maximum (e.g., u10000000 = 10%).

LOW

L-02: Pool Created with Zero Max-In/Out Ratios Blocks All Trading

Location: create-pool

Description: New pools are created with max-in-ratio: u0 and max-out-ratio: u0, as well as start-block: u340282366920938463463374607431768211455 (uint max). The swap functions assert (< dx (mul-down balance-x max-in-ratio)), which with max-in-ratio = 0 means dx < 0 — always false.

Impact: Newly created pools cannot process any swaps until the pool owner or contract owner explicitly sets ratios and adjusts the start/end blocks. This is likely intentional (pool setup phase), but it's a silent failure — the pool appears created but is non-functional.

Recommendation: Document this setup requirement clearly. Consider requiring non-zero ratios in create-pool or adding a separate activate-pool function.

INFO

I-01: Centralized Admin Powers

Location: set-contract-owner, pause, set-switch-threshold

Description: The contract owner (deployer) has broad unilateral powers: pause all trading, change the global switch threshold (affecting all pools' bonding curve behavior), transfer ownership, set fee rebates, and change pool owners. There is no timelock, multisig, or governance mechanism.

Impact: Standard centralization risk. The owner can effectively halt the entire DEX or change economic parameters instantly. Users must trust the ALEX team's key management practices.

Recommendation: Consider governance-controlled ownership, timelocked parameter changes, or at minimum a multisig for the owner role.

INFO

I-02: Permissionless Pool Creation with Arbitrary Token Traits

Location: create-pool

Description: Anyone can create a pool with any two tokens implementing the SIP-010 trait. There's no allowlist for token contracts. Combined with the trait-based as-contract calls, a malicious token contract could potentially execute unexpected logic during transfer-fixed calls within the swap/liquidity functions.

Impact: Low practical risk since Clarity doesn't have reentrancy, but malicious tokens could revert transfers selectively, manipulate return values, or emit misleading events. Pool creation spam is also possible.

Recommendation: Consider a token allowlist for production pools, or flag unverified pools in the UI layer.

Positive Observations

Score Rationale

MetricScoreRationale
Financial Risk3Core DEX contract holding significant TVL via vault
Deployment3Deployed on mainnet, actively used
Complexity31,071 lines, advanced math, multi-contract interaction
User Exposure3ALEX is the largest DEX on Stacks
Novelty1AMM pattern well-studied, but hybrid curve is unique
Final Score2.7 (threshold: 1.8) — pre-Clarity 4 penalty: 2.2