Core AMM pool contract for the ALEX DEX — weighted constant-product/constant-sum hybrid with oracle integration, multi-hop routing, and LP token management
| Contract | SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v1-1 |
| Protocol | ALEX — leading DEX on Stacks |
| Source | Verified on-chain via Hiro API (/v2/contracts/source) |
| Clarity Version | Pre-Clarity 4 (uses as-contract, not as-contract?) |
| Lines of Code | 1,071 |
| Audit Date | 2026-02-25 |
| Confidence | 🟡 MEDIUM — complex math library, multi-contract system (vault, LP tokens) |
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.
Pools are identified by a (token-x, token-y, factor) triple. The factor parameter controls the bonding curve shape:
factor >= switch-threshold (default 0.8): uses a linear-product hybrid formulafactor < switch-threshold: uses power-law curve x^(1-t) + y^(1-t) = kThis dual-mode approach allows pools to range from constant-sum-like (low slippage, concentrated) to constant-product-like behavior depending on the weight parameter.
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.
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.
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.
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.
as-contract — Blanket Asset AccessLocation: 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)).
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%).
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.
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.
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.
exp/ln/pow implementation uses established algorithms with proper rounding direction (mul-up/mul-down, pow-up/pow-down) for conservative bounds| Metric | Score | Rationale |
|---|---|---|
| Financial Risk | 3 | Core DEX contract holding significant TVL via vault |
| Deployment | 3 | Deployed on mainnet, actively used |
| Complexity | 3 | 1,071 lines, advanced math, multi-contract interaction |
| User Exposure | 3 | ALEX is the largest DEX on Stacks |
| Novelty | 1 | AMM pattern well-studied, but hybrid curve is unique |
| Final Score | 2.7 (threshold: 1.8) — pre-Clarity 4 penalty: 2.2 | |