Balancer-style weighted AMM pool — supports unequal token weights, TWAP oracle, multi-hop routing via wSTX, and LP token management
| Contract | SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.fixed-weight-pool-v1-01 |
| 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,320 |
| Audit Date | 2026-02-25 |
| Confidence | 🟡 MEDIUM — complex math library inlined, multi-contract system (vault, weighted-equation, reserve-pool) |
fixed-weight-pool-v1-01 is ALEX's original Balancer-style AMM. Unlike equal-weight (50/50) pools, this contract allows arbitrary weight ratios (e.g. 80/20), enabling pools where one token dominates price exposure. The contract:
weighted-equation-v1-01 and custody to alex-vaultKey dependencies:
.alex-vault — token custody (holds all pool assets).alex-reserve-pool — fee collection.weighted-equation-v1-01 — Balancer weighted math (get-y-given-x, etc.).token-wstx — wrapped STX used as routing intermediarymint-fixed/burn-fixed interfacesLocation: reduce-position, swap-wstx-for-y, swap-y-for-wstx
Description: Throughout the contract, balance updates use a pattern that silently floors to zero instead of reverting on underflow:
balance-x: (if (<= balance-x dx) u0 (- balance-x dx)),
balance-y: (if (<= balance-y dy) u0 (- balance-y dy))
Impact: If the pool's internal accounting ever drifts from actual vault holdings (due to rounding, external vault interactions, or bugs in weighted-equation), the pool will silently become insolvent — setting balances to zero rather than reverting. This masks the accounting error and allows subsequent LPs or swappers to absorb the loss. A pool with balance-x = 0 and balance-y = 0 but nonzero total-supply would strand all LP tokens.
Recommendation: Replace the underflow guard with a hard assertion: (asserts! (> balance-x dx) ERR-INSUFFICIENT-BALANCE). If the pool truly doesn't have enough tokens, the transaction should revert, not silently corrupt state.
Location: set-fee-rate-x, set-fee-rate-y
(define-public (set-fee-rate-x (token-x principal) ... (fee-rate-x uint))
(let ((pool ...))
(asserts! (or (is-eq tx-sender (get fee-to-address pool))
(is-ok (check-is-owner))) ERR-NOT-AUTHORIZED)
(map-set pools-data-map ... (merge pool { fee-rate-x: fee-rate-x }))
(ok true)))
Impact: The fee-to-address (multisig/DAO) can set fee rates to any value including 100% (ONE_8) or higher. A fee rate of ONE_8 means dx-net-fees becomes zero (or the fee exceeds the input), effectively bricking the pool for swappers. There's also no cap on fee-rebate — it could exceed 100%, causing the pool to retain more fees than collected.
Recommendation: Add upper-bound validation: (asserts! (< fee-rate-x MAX-FEE-RATE) ERR-FEE-TOO-HIGH) where MAX-FEE-RATE is a reasonable cap (e.g., 10% = u10000000). Similarly cap fee-rebate to <= ONE_8.
Location: swap-wstx-for-y, swap-y-for-wstx — oracle update
oracle-resilient: (if (get oracle-enabled pool)
(try! (get-oracle-resilient .token-wstx token-y weight-x weight-y))
u0)
Description: The oracle-resilient price is an EMA that blends the instant spot price (post-swap) with the previous resilient price. However, get-oracle-resilient is called after the pool balances have been updated in the current swap's pool-updated merge. Since the map is set after the oracle read, the oracle actually reads the pre-swap balances (the map hasn't been updated yet). This is actually safer than it appears — but the oracle-average weighting still means a single large swap shifts the resilient price by (1 - oracle-average) fraction of the price impact. With a low oracle-average (e.g., 0.05), a single swap moves the resilient price by 95% of the spot move.
Impact: If other contracts rely on get-oracle-resilient for lending or liquidation decisions, a flash-loan-style large swap can significantly shift the oracle in a single transaction. Stacks doesn't have flash loans natively, but multi-call transactions can approximate this.
Recommendation: Consider enforcing a minimum oracle-average (e.g., ≥ 50%) to limit single-block manipulation. Alternatively, use a block-height-gated TWAP that only updates once per block.
Location: swap-wstx-for-y (line ~485), swap-y-for-wstx (line ~530)
;; swap-wstx-for-y:
(asserts! (< (div-down dy dx-net-fees)
(div-down (mul-down balance-y weight-x)
(mul-down balance-x weight-y)))
ERR-INVALID-LIQUIDITY)
;; swap-y-for-wstx:
(asserts! (> (div-down dy-net-fees dx)
(div-down (mul-down balance-y weight-x)
(mul-down balance-x weight-y)))
ERR-INVALID-LIQUIDITY)
Description: These checks verify the effective swap price is "better" than the marginal price, which should always hold for constant-product math. However, they don't limit the size of the swap relative to pool liquidity. A swap that drains 99% of one side is allowed as long as the price check passes. Combined with the balance-floor-to-zero pattern (H-01), this could leave a pool in a degenerate state.
Impact: Very large swaps relative to pool size cause extreme slippage and can leave the pool nearly empty on one side, making subsequent swaps extremely expensive. This is standard AMM behavior but combined with H-01 creates additional risk.
Recommendation: Consider adding a maximum swap size relative to pool balance (e.g., no single swap can drain more than 50% of either token balance).
Location: create-pool
Description: The create-pool function stores the pool token principal but only validates token matching in add-to-position and reduce-position. If the contract owner creates a pool with a malicious pool token that has unrestricted mint-fixed, anyone with that token contract's mint authority could mint LP tokens without depositing, then call reduce-position to drain pool funds.
Impact: Since pool creation is owner-restricted, this is a trust assumption on the owner. However, if ownership transfers to a DAO or multisig, the risk surface expands.
Recommendation: Document this trust assumption clearly. Consider adding a validation that the pool token's owner/minter is set to the fixed-weight-pool contract itself at creation time.
reduce-position uses unwrap-panic for balance queryLocation: reduce-position
(total-shares (unwrap-panic (contract-call? pool-token-trait get-balance-fixed tx-sender)))
Description: If the pool token contract's get-balance-fixed returns an error (e.g., the trait implementation is buggy or the contract is upgraded), this will abort the entire transaction with a runtime panic rather than returning a graceful error.
Recommendation: Use (unwrap! ... ERR-TRANSFER-FAILED) for graceful error propagation.
Location: swap-wstx-for-y, swap-y-for-wstx
(fee (mul-up dx (get fee-rate-x pool)))
(dx-net-fees (if (<= dx fee) u0 (- dx fee)))
Description: If fee ≥ dx (possible with high fee rates or small swaps due to mul-up rounding), dx-net-fees becomes zero. A zero input to the weighted equation would produce zero output, and the user loses their entire input as fees.
Recommendation: Add (asserts! (> dx-net-fees u0) ERR-INVALID-LIQUIDITY) after the fee calculation.
as-contract gives blanket asset accessLocation: Multiple — all as-contract calls
Description: The contract uses pre-Clarity 4 as-contract which gives unrestricted asset access within the expression scope. While Clarity's execution model prevents re-entrancy (no callbacks during contract-call?), the blanket access means any future code changes or upgrades within the as-contract block could accidentally authorize unintended asset transfers.
Recommendation: When migrating to Clarity 4, replace as-contract with as-contract? using explicit with-ft/with-stx allowances for each token being transferred.
Description: The entire Balancer-style log/exp/pow math library (~320 lines) is inlined in this contract. The same code appears in amm-swap-pool-v1-1 and likely other ALEX pool contracts. This increases deployment cost and makes bug fixes require redeploying every pool contract.
Recommendation: The math is already delegated for some functions via weighted-equation-v1-01. Consider moving all math to a shared library contract.
Location: swap-x-for-y, swap-y-for-x
Description: When neither token is wSTX, swaps route through two pools (token-x → wSTX → token-y). Each hop applies its own fee. Users may not realize they're paying fees twice. The intermediate swap in swap-x-for-y passes none for min-dy, meaning the intermediate hop has no slippage protection.
Recommendation: Document the double-fee behavior. Consider adding end-to-end slippage protection that covers both hops (the outer min-dy partially addresses this but only on the final output).
Location: create-pool
(var-set pools-list (unwrap! (as-max-len? (append (var-get pools-list) pool-id) u500) ERR-TOO-MANY-POOLS))
Description: The pools-list is limited to 500 entries. Once reached, no new pools can be created. This is a deployment constraint, not a bug, but worth noting for long-term scalability.
Location: Last line of contract
(set-contract-owner .executor-dao)
Description: The contract owner is initialized to .executor-dao at deploy time. This is the correct pattern — governance via DAO rather than an EOA. The set-contract-owner function allows ownership transfer, which is appropriate for DAO upgrades.
create-pool checks both token orderings, preventing the same pair from being created twice with swapped positions.add-to-position and reduce-position verify the passed pool token trait matches the stored pool token principal (CR-01 comment suggests this was a patched issue).min-dy/min-dx on swaps and max-dy on liquidity adds protect users from sandwich attacks.executor-dao, not an EOA.
The ALEX Fixed Weight Pool is a mature, deployed contract implementing Balancer-style weighted AMM mechanics. The core swap and liquidity logic is sound, delegating complex math to a dedicated equation contract. The main concerns are operational: unbounded fee rates (H-02) could brick pools if governance is compromised, and the silent underflow protection (H-01) could mask insolvency rather than failing safely. The oracle implementation (M-01) is a standard EMA design but could be manipulated with low smoothing factors. As a pre-Clarity 4 contract, it uses blanket as-contract — migration to as-contract? with explicit allowances would improve safety.
Overall, this is a well-structured DeFi contract with appropriate access controls and slippage protections. The findings are primarily defense-in-depth improvements rather than exploitable vulnerabilities in the current deployment context.