Skip to content

Instantly share code, notes, and snippets.

@Mayjor01
Created June 3, 2026 10:17
Show Gist options
  • Select an option

  • Save Mayjor01/c3ee870360692870b51a98cb10784cf0 to your computer and use it in GitHub Desktop.

Select an option

Save Mayjor01/c3ee870360692870b51a98cb10784cf0 to your computer and use it in GitHub Desktop.
Static Analysis Audit: ALEX AMM pool v2 (amm-pool-v2-01)

Static Analysis Audit: ALEX AMM pool v2 (amm-pool-v2-01)

Auditor: Emerald Castle (bc1qzhlus0nesaphjy5jfd2tnef9te9j4mq57mzd6r)
Contract: SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01
Source: https://api.hiro.so/v2/contracts/source/SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM/amm-pool-v2-01
Protocol: ALEX — primary AMM v2 pool router and math execution contract
Audit Type: Static analysis only (no execution)
Date: 2026-06-03


1. State Model

Data Variables

Variable Type Initial Value Mutated By Authority
paused bool true pause DAO or whitelisted extension

Data Maps

This contract contains no local data maps. All pool state records (balances, fee rates, oracle parameters, thresholds, and total supply) are stored in and retrieved from the companion registry contract:

  • .amm-registry-v2-01

Constants

Constant Value Purpose
ERR-NOT-AUTHORIZED (err u1000) Standard authorization failure
ERR-POOL-ALREADY-EXISTS (err u2000) Attempting to create an existing pool
ERR-INVALID-POOL (err u2001) Provided token parameters do not map to an active pool
ERR-BLOCKLISTED (err u2002) Action attempted by a blocklisted address
ERR-INVALID-LIQUIDITY (err u2003) Provided token amounts are invalid (e.g., zero)
ERR-PERCENT-GREATER-THAN-ONE (err u2004) Withdrawal percentage exceeds 100% (ONE_8)
ERR-EXCEEDS-MAX-SLIPPAGE (err u2005) Resulting output does not meet slippage bounds
ERR-ORACLE-NOT-ENABLED (err u2006) Attempting to fetch TWAP price for a pool without active oracle
ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE (err u2007) Oracle weight exceeds 1.0 (defined but never asserted)
ERR-PAUSED (err u2008) Operations suspended due to emergency contract pause
ERR-SWITCH-THRESHOLD-BIGGER-THAN-ONE (err u2009) Threshold exceeds 1.0 (defined but never asserted)
ERR-NO-LIQUIDITY (err u2010) Pool exists but carries zero active liquidity
ERR-MAX-IN-RATIO (err u2011) Trade size exceeds the configured max in ratio
ERR-MAX-OUT-RATIO (err u2012) Trade output size exceeds the max out ratio
ONE_8 u100000000 Fixed-point scale factor (1.0 in 8 decimals)

2. Function Inventory

Public Functions

create-pool

  • Authority: Open to any caller who is not blocklisted.
  • Pre-conditions: is-blocklisted-or-default checks pass for tx-sender.
  • State mutations: Creates registry entry for pool, transfers initial tokens, mints initial LP shares.
  • External calls: .amm-registry-v2-01.create-pool, add-to-position.

add-to-position

  • Authority: Open (contract must be unpaused).
  • Pre-conditions: not paused, dx > 0, dy > 0, slippage check passes.
  • State mutations: Updates pool balances and supply in registry, mints LP shares.
  • External calls: token-x-trait.transfer-fixed, token-y-trait.transfer-fixed, .amm-registry-v2-01.update-pool, .token-amm-pool-v2-01.mint-fixed.

reduce-position

  • Authority: Open to token holder (must not be blocklisted).
  • Pre-conditions: not paused, caller is not blocklisted, percent <= ONE_8.
  • State mutations: Decreases pool balances and supply in registry, burns LP shares.
  • External calls: .token-amm-pool-v2-01.get-balance-fixed, .amm-vault-v2-01.transfer-ft-two, .amm-registry-v2-01.update-pool, .token-amm-pool-v2-01.burn-fixed.

swap-x-for-y

  • Authority: Open (caller must not be blocklisted).
  • Pre-conditions: not paused, caller not blocklisted, pool status active, dx > 0, slippage checks pass.
  • State mutations: Deducts fees, calculates output, updates balances and oracle state in registry.
  • External calls: token-x-trait.transfer-fixed, .amm-vault-v2-01.transfer-ft, .amm-vault-v2-01.add-to-reserve, .amm-registry-v2-01.update-pool.

swap-y-for-x

  • Authority: Open (caller must not be blocklisted).
  • Pre-conditions: not paused, caller not blocklisted, pool status active, dy > 0, slippage checks pass.
  • State mutations: Deducts fees, calculates output, updates balances and oracle state in registry.
  • External calls: token-y-trait.transfer-fixed, .amm-vault-v2-01.transfer-ft, .amm-vault-v2-01.add-to-reserve, .amm-registry-v2-01.update-pool.

swap-helper / swap-helper-a / swap-helper-b / swap-helper-c

  • Authority: Open (inherited from individual swap functions).
  • Pre-conditions: Inherited; executes single or multi-hop token routes.
  • External calls: swap-x-for-y / swap-y-for-x.

pause

  • Authority: DAO or whitelisted extension.
  • Pre-conditions: is-dao-or-extension checks pass.
  • State mutations: Updates paused variable.

Parameter Setters (set-start-block, set-end-block, set-max-in-ratio, set-max-out-ratio, set-oracle-enabled, set-oracle-average, set-threshold-x, set-threshold-y, set-fee-rate-x, set-fee-rate-y)

  • Authority: Pool owner or DAO.
  • Pre-conditions: tx-sender == pool-owner or is-dao-or-extension.
  • State mutations: Updates corresponding parameter in registry.
  • External calls: Companion functions in .amm-registry-v2-01.

3. Post-Condition Coverage Matrix

Function Asset Movements Recommended Post-Conditions
create-pool User sends dx of X and dy of Y; user receives LP shares ft-transfer of X (== dx), ft-transfer of Y (== dy), ft-mint of LP token (> 0)
add-to-position User sends dx of X and dy of Y; user receives LP shares ft-transfer of X (== dx), ft-transfer of Y (<= max-dy), ft-mint of LP token (>= min-lp)
reduce-position User burns LP shares; receives X and Y tokens ft-burn of LP shares (== shares), ft-receive of X (>= dx), ft-receive of Y (>= dy)
swap-x-for-y User sends dx of X; receives dy of Y ft-transfer of X (== dx), ft-receive of Y (>= min-dy)
swap-y-for-x User sends dy of Y; receives dx of X ft-transfer of Y (== dy), ft-receive of X (>= min-dx)

4. Authority / Access-Control Matrix

Operation Whitelisted Roles Check Implementation Impact of Compromise
Emergency Pause DAO / Extensions is-dao-or-extension Pause/unpause all pool activity globally
Parameter Updates Pool Owner / DAO tx-sender == pool-owner or is-dao-or-extension Arbitrary fee rate changes, oracle state manipulation, trading ratio blocks
Swap / Liquidity Open Blocklist status checking Denial of service for blocklisted addresses
  • Oracle Dependency: Pure mathematical TWAP oracle calculated internally based on pool trade balances. No external chainlink or pyth feed dependency.
  • Pause Mechanism: Global paused variable. Swaps and liquidity addition checks (asserts! (not (is-paused)) ERR-PAUSED).

5. Clarity Best-Practice Review

  • tx-sender vs contract-caller: Correctly captures tx-sender as the user (sender / who) when doing token transfers. Captures contract-caller for extension validation.
  • Integer Math: Uses customized mathematical helpers (mul-down, mul-up, div-down, div-up, pow-down, pow-up, and fixed-point natural exponential/log functions). Scale factor is 10^8 (ONE_8).
  • Panic propagation: Multiple unwrap-panic calls exist inside mathematical operations (e.g. in pow-down, pow-up, and reduce-position). These will trigger transactional reverts without clear failure codes.

6. Findings Table

ID Severity Function Line Finding Recommended Fix
A-01 High get-oracle-resilient 53–60 Mathematical logic error in reverse-order TWAP price calculation Reciprocate the stored resilient oracle price when querying in the non-canonical direction
A-02 Medium swap-x-for-y / swap-y-for-x 309–310, 338–339 Silent token swallowing when fee rate is configured to >= 100% Add assertions to validate fee rate inputs are strictly < ONE_8
A-03 Medium add-to-position 257–278 Missing blocklist validation permits blocklisted accounts to add liquidity Enforce is-blocklisted-or-default assertion matching other state-mutating paths
A-04 Low set-oracle-average 227–231 Missing upper-bound validation enables denial of service via TWAP calculations Assert (asserts! (<= new-oracle-average ONE_8) ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE)
A-05 Low pow-down / pow-up 494–505 Internal unwrap-panics in exponentiation helper discard context error codes Propagate math boundary errors via response types instead of panicking

A-01 Detail — Mathematical logic error in reverse-order TWAP price calculation (High)

Location: Lines 53–60 (get-oracle-resilient), Lines 61–69 (get-oracle-instant)

(define-read-only (get-oracle-resilient (token-x principal) (token-y principal) (factor uint))
    (let (
            (exists (is-some (get-pool-exists token-x token-y factor)))
            (pool (if exists (try! (get-pool-details token-x token-y factor)) (try! (get-pool-details token-y token-x factor))))
            (oracle-instant (try! (get-oracle-instant token-x token-y factor))))
        (asserts! (get oracle-enabled pool) ERR-ORACLE-NOT-ENABLED)
        (ok (+ (mul-down (- ONE_8 (get oracle-average pool)) oracle-instant) 
            (mul-down (get oracle-average pool) (if (is-eq (get oracle-resilient pool) u0) oracle-instant (get oracle-resilient pool)))))))

When get-oracle-resilient is queried in the reverse direction (exists = false):

  1. The oracle-instant is correctly computed in reverse (balance-y / balance-x) using the else branch of get-oracle-instant.
  2. However, the calculation of the resilient price multiplies the weight oracle-average by the stored (get oracle-resilient pool) value.
  3. The value stored in the registry pool state (oracle-resilient) is ALWAYS recorded in the canonical direction (balance-x / balance-y) during swap execution.
  4. This results in the function averaging oracle-instant (expressed as 1/P) with the stored resilient price (expressed as P).

Impact: Integrations querying the oracle in the non-canonical direction will receive mathematically corrupted price feeds. This corrupted oracle state breaks downstream protocols (such as lending platforms or synthetic assets) relying on the non-canonical price queries.

Recommended Fix: When exists is false, the contract must reciprocate the stored resilient oracle price before performing the weighted average:

(let (
        (exists (is-some (get-pool-exists token-x token-y factor)))
        (pool (if exists (try! (get-pool-details token-x token-y factor)) (try! (get-pool-details token-y token-x factor))))
        (oracle-instant (try! (get-oracle-instant token-x token-y factor)))
        (stored-resilient (get oracle-resilient pool))
        (resilient-dir (if exists 
            stored-resilient 
            (if (is-eq stored-resilient u0) u0 (div-down ONE_8 stored-resilient)))))
    ;; Use resilient-dir instead of (get oracle-resilient pool) in the weighted sum
)

A-02 Detail — Silent token swallowing when fee rate is configured to >= 100% (Medium)

Location: Lines 242–251 (set-fee-rate-x/y), Lines 309–310 / 338–339 (swap-x-for-y/y-for-x)

The pool fee setters do not restrict the fee-rate-x/y parameters to be strictly less than 100% (ONE_8). If a pool owner sets the fee rate to ONE_8 (100%) or larger:

  1. In swap-x-for-y, fee is computed as mul-up dx fee-rate-x (which makes fee >= dx).
  2. dx-net-fees evaluates to u0.
  3. dy evaluates to u0.
  4. The swap proceeds without error, transferring the user's entire input dx to amm-vault-v2-01 and registering the difference in the reserve.
  5. The user receives 0 output tokens without any runtime revert.

Impact: Loss of user funds. If a pool's fee parameter is misconfigured or updated maliciously, any swap attempt will consume the user's full input amount without returning any trade output.

Recommended Fix: Enforce fee validation limits in parameter updates:

(define-public (set-fee-rate-x (token-x principal) (token-y principal) (factor uint) (fee-rate-x uint))
    (begin
        (asserts! (< fee-rate-x ONE_8) ERR-PERCENT-GREATER-THAN-ONE)
        ;; Proceed with update
    ))

A-03 Detail — Missing blocklist validation in add-to-position (Medium)

Location: Lines 257–278 (add-to-position)

All mutating paths that alter pool balances check and reject blacklisted actors using is-blocklisted-or-default:

  • create-pool (line 254)
  • reduce-position (line 294)
  • swap-x-for-y (line 319)
  • swap-y-for-x (line 348)

However, add-to-position does not include this assertion.

Impact: Blocklisted addresses can continue to deposit liquidity into active AMM pools. Once their liquidity is added, it becomes permanently locked because they cannot retrieve it (due to the active blocklist check inside reduce-position). This creates compliance leaks and permanently traps user assets without a formal legal/procedural confiscation path.

Recommended Fix: Add the blocklist validation check at the entry of add-to-position:

(define-public (add-to-position (token-x-trait <ft-trait>) (token-y-trait <ft-trait>) (factor uint) (dx uint) (max-dy (optional uint)))
    (begin
        (asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED)
        ;; Proceed with execution
    ))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment