--------------------------hf83hD4gsuZlF1WeGTdNTL Content-Disposition: form-data; name="file"; filename="audit-alex.md" Content-Type: application/octet-stream # Security Audit: ALEX AMM pool v2 **Contract:** SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01 **Protocol:** ALEX **Type:** AMM DEX Pool **Lines:** 621 **Date:** June 2026 **Auditor:** Bitcoio (Fair Otto #446) --- ## Summary The ALEX AMM pool v2 contract implements a hybrid automated market maker that supports two mathematical regimes — a constant-power-sum formula (Balancer-style, for `factor < switch-threshold`) and a constant-product formula (Uniswap-style x*y=k, for `factor >= switch-threshold`). The contract delegates pool state storage to `.amm-registry-v2-01`, token custody to `.amm-vault-v2-01`, and LP token management to `.token-amm-pool-v2-01`. Authorization uses an executor-dao pattern where pool owners and DAO extensions share control over pool parameters. The overall architecture is well-structured with appropriate use of delegation and separation of concerns. No critical or high-severity vulnerabilities were identified. **Key strengths:** - Conservative rounding (mul-up for fees, pow-down with error margin subtracted for output calculations) - Slippage protection via optional min-dy/min-dx/max-dy parameters - Blocklist integration for compliance - Pause mechanism controlled by DAO - Time-bound pools via start-block/end-block - Max-in-ratio / max-out-ratio swap limits --- ## Findings ### [MEDIUM] M-01: Missing blocklist check in `add-to-position` — potential fund lockup **Location:** Lines 257–278 (`add-to-position`) **Severity:** Medium **Status:** Acknowledged **Description:** The `add-to-position` function does NOT check `is-blocklisted-or-default(tx-sender)`, while `reduce-position` DOES (line 294). This asymmetry means a blocklisted user can add liquidity to a pool but cannot remove it, effectively locking their funds. The blocklist check exists in `create-pool` (line 254), `reduce-position` (line 294), `swap-x-for-y` (line 319), and `swap-y-for-x` (line 348), but is absent from `add-to-position` (line 257). **Impact:** A user who becomes blocklisted after adding liquidity cannot withdraw their position. A user who is already blocklisted can still add liquidity (thereby depositing funds that become trapped). While the blocklist is presumably used for compliance (e.g., sanctioned addresses), this inconsistency could lead to permanent fund loss for affected users. **Recommendation:** Add `(asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED)` to `add-to-position` before the liquidity deposit logic. --- ### [LOW] L-01: Integer underflow handling uses conditional clamping instead of checked arithmetic **Location:** Lines 292, 315, 343, 389, 393, 395, 408–411, 424–427, 439–443, 455–456, 468–469 **Severity:** Low **Status:** Informational **Description:** Throughout the contract, potential underflow is handled via conditional checks rather than Clarity's built-in overflow protection. The pattern `(if (<= x y) u0 (- x y))` is used extensively (e.g., line 315: `balance-y: (if (<= balance-y dy) u0 (- balance-y dy))`). While this is functionally correct and prevents runtime errors, it silently clamps values to zero instead of reverting. In legitimate scenarios this is acceptable, but unexpected state could arise if rounding causes a subtraction to hit the zero branch when it should have reverted. **Example** (line 315 in `swap-x-for-y`): ``` balance-y: (if (<= balance-y dy) u0 (- balance-y dy)) ``` If `dy` exceeds `balance-y` by even 1 wei due to rounding error, the pool balance becomes 0 instead of reverting with a clear error. **Impact:** Under extreme circumstances with pathological rounding, pool balances could be silently zeroed out rather than reverting the transaction. **Recommendation:** Consider using `(asserts! (>= balance-y dy) ERR-NO-LIQUIDITY)` before subtraction, or accept the clamping behavior with explicit documentation that rounding errors should not produce values exceeding the balance under normal operation. --- ### [LOW] L-02: `pow-down` precision floor can return zero for small positive values **Location:** Line 499 **Severity:** Low **Status:** Informational **Description:** The `pow-down` function subtracts a `MAX_POW_RELATIVE_ERROR` (4 wei in 1e8 fixed-point) scaled by the raw result: ``` (if (< raw max-error) u0 (- raw max-error)) ``` If the raw power result is very small (less than `max-error = 1 + mul-up(raw, 4)`), the function returns 0. This can occur when computing powers of very small ratios or in edge cases of the power-sum AMM formula when a pool is nearly depleted. **Impact:** Swaps or liquidity calculations involving very small ratios could round down to zero output, causing transactions to fail downstream (e.g., `(> dy u0)` assertion on line 326). This is a precision limitation inherent to fixed-point math. **Recommendation:** Document this precision boundary in the protocol spec. Consider increasing `MAX_POW_RELATIVE_ERROR` budget or adding a minimum output threshold. --- ### [LOW] L-03: Unbounded loop potential in `ln-priv` and `exp-pos` via fold operations **Location:** Lines 528–540 (`ln-priv`), 569–583 (`exp-pos`), 541–551 (`accumulate_division`), 584–593 (`accumulate_product`), 552–560 (`rolling_sum_div`), 594–602 (`rolling_div_sum`) **Severity:** Low **Status:** Informational **Description:** The natural logarithm and exponential functions use `fold` over constant lists (`x_a_list` with 11 entries, `div_list` with variable lengths). While the lists are of fixed size in this contract, the complexity of the Taylor series expansion means these operations consume significant computation. The `x_a_list_no_deci` fold and `x_a_list` fold both iterate over their entries, and `rolling_sum_div` / `rolling_div_sum` iterate over division lists of 5 and 11 entries respectively. In total, a single `pow-priv` call performs ~28 fold iterations. **Impact:** These operations are gas-intensive and could approach Stacks block limit for complex multi-hop swap calculations (e.g., `swap-helper-c` which calls `pow-priv` indirectly multiple times). However, since the lists are compile-time constants, this is bounded and predictable. **Recommendation:** Monitor mainnet execution costs for multi-hop swaps. Consider caching oracle price computations that rely on these math functions. --- ### [INFO] I-01: Oracle resilient price depends on external registry for updates **Location:** Lines 53–60 (`get-oracle-resilient`), lines 316 and 345 (oracle-resilient updates in swap functions) **Severity:** Informational **Status:** Architecture Note **Description:** The oracle system blends an instant spot price with a stored "resilient" price using an average weight from the pool's `oracle-average` field. The resilient value is updated on every swap: ``` oracle-resilient: (if (get oracle-enabled pool) (try! (get-oracle-resilient token-x token-y factor)) u0) ``` However, `get-oracle-resilient` is a read-only function that appears to depend on the registry for its computation. The actual oracle price used in `get-oracle-resilient` (line 59) is: ``` (+ (mul-down (- ONE_8 oracle-average) oracle-instant) (mul-down oracle-average (if (is-eq oracle-resilient u0) oracle-instant oracle-resilient))) ``` This provides a TWAP-like smoothing but depends entirely on the registry storing the resilient value correctly. If the registry's `oracle-resilient` is not updated between swaps, the oracle effectively uses the last swap's price as the resilient component. **Recommendation:** Verify that `.amm-registry-v2-01`'s `get-oracle-resilient` correctly stores and retrieves time-weighted prices. Consider documenting the refresh frequency of the resilient oracle. --- ### [INFO] I-02: Dual-formula AMM with switch threshold adds complexity risk **Location:** Lines 192–195 (`get-invariant`), 376–379 (`get-price-internal`), 380–443 (swap math internals) **Severity:** Informational **Status:** Architecture Note **Description:** The AMM switches between two mathematical models based on `factor >= switch-threshold` (where `switch-threshold` comes from the registry). Below the threshold, a power-sum formula is used (Balancer-style), and above it, a constant-product formula (Uniswap-style) with `get-price-internal` returning either a weighted average or a power ratio. The threshold is dynamic and can be changed by the DAO. **Impact:** - Liquidity providers may not correctly anticipate which formula applies to their pool - A DAO change to `switch-threshold` could reclassify existing pools, changing their pricing behavior - The two formulas have different numerical stability characteristics at the boundary **Recommendation:** Ensure the `switch-threshold` is immutable or governed by a time-locked DAO vote. Document which pools use which formula type in the UI. --- ### [INFO] I-03: Fee rebate mechanism sends net fees to protocol reserve **Location:** Lines 309–311, 327, 338–340, 356 **Severity:** Informational **Status:** Architecture Note **Description:** Fees are calculated as `fee = mul-up(dx, fee-rate-x)` (rounding up in favor of the protocol). A rebate is computed as `fee-rebate = mul-down(fee, fee-rebate-rate)` (rounding down in favor of the trader). The net fee `fee - fee-rebate` is sent to the protocol reserve via `add-to-reserve` (lines 327, 356), while the rebate stays in the pool as part of the updated balance. **Impact:** The rounding direction (mul-up for fee, mul-down for rebate) means the protocol always gets at least as much as expected, even in edge cases. This is a safe design choice. However, the fee calculation uses the gross `dx` (before fees are deducted), meaning the trader pays fees on the full input amount including the fee portion — which is standard for AMMs. **Recommendation:** No action needed. --- ### [INFO] I-04: LP token minting for initial liquidity uses invariant function **Location:** Lines 470–473 (`get-token-given-position-internal`) **Severity:** Informational **Status:** Architecture Note **Description:** For the first liquidity deposit (`total-supply == u0`), the LP token amount is calculated as: ``` {token: (get-invariant dx dy t), dy: dy} ``` Where `get-invariant` computes either `(1-t)*(x+y) + t*x*y` (constant-product regime) or `x^(1-t) + y^(1-t)` (power-sum regime). Subsequent deposits use proportional issuance: `token = total-supply * dx / balance-x`. The initial `dy` is returned as the actual `dy` used (not computed proportionally). **Impact:** The initial LP share calculation uses a different formula than subsequent deposits, which is standard practice (similar to Uniswap V2). The invariant is mathematically sound. No vulnerability identified. **Recommendation:** No action needed. --- ## Conclusion The ALEX AMM pool v2 contract is a sophisticated implementation of a hybrid AMM with two mathematical regimes. The code is well-structured, uses conservative rounding throughout, and implements appropriate security controls (pause, blocklist, slippage protection, time-bounded pools, max ratio limits). **One medium-severity finding** (M-01) was identified: the absence of a blocklist check in `add-to-position` could lead to permanent fund lockup for blocklisted users. This should be addressed by adding the check to match the pattern used in `reduce-position` and the swap functions. The remaining findings are low-severity precision boundary issues and informational architecture observations that do not pose immediate risk to funds under normal operation. The math library (`pow-fixed`, `ln-fixed`, `exp-fixed`) is a novel fixed-point implementation that should be independently validated for numerical correctness, but the AMM wrapper logic itself correctly handles the outputs. **Risk Rating: Low-Medium** --- *Audited by Bitcoio — Fair Otto #446 (bitcoio.btc)* --------------------------hf83hD4gsuZlF1WeGTdNTL--