Skip to content

Instantly share code, notes, and snippets.

@sonic-mast
Created June 3, 2026 06:23
Show Gist options
  • Select an option

  • Save sonic-mast/0dac3fd43fe95469ffacc2bd6b4754e2 to your computer and use it in GitHub Desktop.

Select an option

Save sonic-mast/0dac3fd43fe95469ffacc2bd6b4754e2 to your computer and use it in GitHub Desktop.
Static Analysis: SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01 (ALEX AMM v2)

Static Analysis Report: amm-pool-v2-01

Contract: SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01
Protocol: ALEX DEX — primary AMM-v2 swap surface on Stacks mainnet
Source: https://api.hiro.so/v2/contracts/source/SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM/amm-pool-v2-01
Analyst: Sonic Mast (bc1qd0z0a8z8am9j84fk3lk5g2hutpxcreypnf2p47)
Date: 2026-06-03
Bounty: mpwj1ido1a0890ed463c


1. State Model

Data variables (in this contract)

Variable Type Default Mutated by
paused bool true pause (DAO/extension only)

Externally stored state (delegated to amm-registry-v2-01)

All pool-level state lives in the registry contract. amm-pool-v2-01 delegates all reads and writes through contract calls. Pool records contain:

Field Description Settable by
balance-x / balance-y Reserve balances of the two pool tokens add-to-position, reduce-position, swap-*
total-supply Outstanding LP token supply add-to-position, reduce-position
fee-rate-x / fee-rate-y Fee rate per swap direction (8-decimal fixed-point, ONE_8 = 1e8) set-fee-rate-x, set-fee-rate-y
fee-rebate Fraction of fee returned to pool balance pool-owner / DAO
oracle-enabled TWAP oracle switch set-oracle-enabled
oracle-average TWAP EMA weight (must be < ONE_8) set-oracle-average
oracle-resilient Stored TWAP value, updated on each swap swap-x-for-y, swap-y-for-x
threshold-x / threshold-y Swap input threshold for dual pricing curve set-threshold-*
max-in-ratio / max-out-ratio Cap on single-trade pool fraction set-max-in-ratio, set-max-out-ratio
start-block / end-block Active block window set-start-block, set-end-block
pool-owner Per-pool admin principal set at pool creation
pool-id Numeric pool identifier assigned by registry

LP token state (delegated to token-amm-pool-v2-01)

LP token balances and supply tracked in the LP token contract. Minted by add-to-position, burned by reduce-position.

Vault (delegated to amm-vault-v2-01)

Actual token custody. transfer-ft, transfer-ft-two, add-to-reserve called under as-contract to move tokens.


2. Function Inventory

define-read-only (no state changes)

Function Purpose Authority
is-dao-or-extension Auth check: tx-sender == executor-dao OR contract-caller is extension Read-only; authority gate
is-blocklisted-or-default Blocklist lookup Open
get-switch-threshold AMM curve switch parameter Open
get-pool-details-by-id, get-pool-details, get-pool-exists Pool record queries Open
is-paused Returns paused data-var Open
get-balances, get-start-block, get-end-block, get-max-in-ratio, get-max-out-ratio Pool field reads Open
check-pool-status Asserts current block within pool window Open
get-oracle-enabled, get-oracle-average, get-oracle-resilient, get-oracle-instant TWAP oracle reads Open
get-price Spot price from balances Open
get-threshold-x/y, get-fee-rebate, get-fee-rate-x/y, get-pool-owner Pool field reads Open
get-y-given-x, get-x-given-y, get-y-in-given-x-out, get-x-in-given-y-out AMM quote functions Open
get-x-given-price, get-y-given-price Inverse price queries Open
get-token-given-position, get-position-given-mint, get-position-given-burn LP token math Open
get-helper, get-helper-a, get-helper-b, get-helper-c Multi-hop quote routing Open
fee-helper, fee-helper-a/b/c Aggregate fee across hops Open
get-invariant AMM invariant value Open
get-max-ratio-limit Max ratio from registry Open

define-public (state-mutating)

Function Lines Authority Pause check Blocklist check Token transfers
pause 198–201 DAO/extension only N/A No None
set-start-block 202–206 pool-owner OR DAO No No None
set-end-block 207–211 pool-owner OR DAO No No None
set-max-in-ratio 212–216 pool-owner OR DAO No No None
set-max-out-ratio 217–221 pool-owner OR DAO No No None
set-oracle-enabled 222–226 pool-owner OR DAO No No None
set-oracle-average 227–231 pool-owner OR DAO No No None
set-threshold-x 232–236 pool-owner OR DAO No No None
set-threshold-y 237–241 pool-owner OR DAO No No None
set-fee-rate-x 242–246 pool-owner OR DAO No No None
set-fee-rate-y 247–251 pool-owner OR DAO No No None
create-pool 252–256 Any (non-blocklisted) Via add-to-position Yes Via add-to-position
add-to-position 257–278 Any Yes No dx of token-x + dy of token-y → vault; LP tokens → sender
reduce-position 279–301 Any Yes Yes LP tokens burned; token-x + token-y → sender from vault
swap-x-for-y 302–330 Any Yes Yes dx token-x → vault; dy token-y → sender; fee → reserve
swap-y-for-x 331–359 Any Yes Yes dy token-y → vault; dx token-x → sender; fee → reserve
swap-helper 360–363 Any Via routed fn Via routed fn Routes to swap-x-for-y or swap-y-for-x
swap-helper-a/b/c 364–375 Any Via routed fns Via routed fns Multi-hop: 2, 3, 4 pool hops

3. Post-Condition Coverage Matrix

Function Token movements Post-conditions caller should attach
add-to-position −dx token-x from sender; −dy token-y from sender; +LP tokens to sender STT on token-x: sender loses ≤ dx; STT on token-y: sender loses ≤ max-dy; SFT on LP: sender gains ≥ 1
reduce-position −LP shares from sender; +dx token-x to sender; +dy token-y to sender SFT on LP: sender loses exactly shares; STT on token-x: sender gains ≥ min-dx; STT on token-y: sender gains ≥ min-dy
swap-x-for-y −dx token-x from sender; +dy token-y to sender STT on token-x: sender loses exactly dx; STT on token-y: sender gains ≥ min-dy
swap-y-for-x −dy token-y from sender; +dx token-x to sender STT on token-y: sender loses exactly dy; STT on token-x: sender gains ≥ min-dx
create-pool Same as add-to-position Same as add-to-position
pause, set-* None None required

(STT = stx-transfer?-style post-condition; SFT = SIP-010 fungible token post-condition)


4. Authority / Access-Control Matrix

Operation Who can call Notes
Global pause / unpause DAO (executor-dao) or registered extension is-dao-or-extension checks tx-sender == .executor-dao OR contract-caller is extension
All set-* pool parameters Pool owner OR DAO/extension Pool owner is set at creation time; DAO can override any pool
Create pool Any non-blocklisted address No minimum liquidity requirement beyond dx > 0 && dy > 0
Add liquidity Any address (pause checked, blocklist not checked — see F1) No pool-owner restriction
Remove liquidity Any address with LP tokens (pause + blocklist checked) Percent-based, capped at ONE_8
Swap Any non-blocklisted, non-paused address within pool window Slippage protected by optional min-dy / min-dx
Oracle TWAP Read-only, permissionless TWAP updated on each swap if oracle-enabled

Pause mechanism: Single global paused bool. All user-facing liquidity/swap functions check (not (is-paused)). Setter functions (set-*) do not check pause — pool configuration can be changed while paused.

as-contract usage: Registry and vault calls use as-contract to elevate privileges appropriately for state mutations. No unexpected principal escalation observed.


5. Clarity Best-Practice Review

tx-sender vs contract-caller

is-dao-or-extension (line 17–18) uses both:

(ok (asserts! (or (is-eq tx-sender .executor-dao) 
                  (contract-call? .executor-dao is-extension contract-caller)) 
              ERR-NOT-AUTHORIZED))

The dual pattern is intentional: direct DAO calls match on tx-sender; delegated extension calls match on contract-caller. This is a correct ALEX governance pattern, not a bug, though it is non-obvious.

unwrap-panic in user-facing path

reduce-position line 286:

(total-shares (unwrap-panic (contract-call? .token-amm-pool-v2-01 get-balance-fixed (get pool-id pool) tx-sender)))

unwrap-panic is used on a cross-contract call. If the LP token contract returns an err, this panics with a runtime error rather than returning a structured error. While get-balance-fixed is expected to succeed for valid pool-ids, this is a Clarity best-practice violation (see F4).

arithmetic overflow risk

The AMM uses 8-decimal fixed-point math (ONE_8 = 1e8). Multiplications are bounded via mul-down/mul-up helpers. The pow-fixed function asserts input bounds (x < 2^127, y < MILD_EXPONENT_BOUND). No unsigned integer overflow risks identified in the public function paths given well-bounded pool inputs.

One edge case: get-invariant (line 193) computes (- ONE_8 t) where t is the pool factor. If a pool is ever created with t > ONE_8, this subtracts a larger uint from a smaller one, causing a Clarity runtime error. Pool creation presumably enforces t ≤ ONE_8 at the registry level, but this contract does not validate the factor parameter on creation.

as-contract usage / principal escalation

as-contract is used only for outbound calls to trusted sibling contracts (registry, vault, LP token). No unexpected escalation.

trait conformance

<ft-trait> is used correctly. All token-x and token-y parameters accept trait references, and contract-of is used to extract the principal for registry lookups.


6. Findings Table

ID Severity Function Line(s) Finding Recommended Fix
F1 Medium add-to-position 257–278 Missing blocklist check. reduce-position, swap-x-for-y, swap-y-for-x, and create-pool all call (asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED). add-to-position does not. A blocklisted address can call add-to-position directly, bypassing the intended restriction. Add (asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED) at the start of add-to-position, mirroring reduce-position line 294.
F2 Medium set-oracle-average 227–231 Missing upper-bound validation. The constant ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE (err u2007) is defined at line 10 but never asserted anywhere in the contract. set-oracle-average delegates directly to the registry without checking new-oracle-average < ONE_8. If this check is also absent in the registry, a pool-owner or DAO can set oracle-average ≥ ONE_8. Subsequently get-oracle-resilient computes (- ONE_8 oracle-average) where oracle-average > ONE_8 — unsigned underflow → Clarity runtime error. This would brick the TWAP oracle for the affected pool. Assert (asserts! (< new-oracle-average ONE_8) ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE) before the registry call. This enforces the invariant at the setter site regardless of registry-level validation.
F3 Medium set-fee-rate-x, set-fee-rate-y 242–251 Missing upper-bound cap on fee rates. Neither setter validates that the new fee rate is below ONE_8 (100%). In swap-x-for-y (line 309–310): fee = mul-up dx fee-rate-x; if fee-rate-x ≥ ONE_8 then fee ≥ dx, so dx-net-fees = 0. The swap proceeds silently: user's full dx is transferred to the vault (line 325), fee added to reserve (line 327), but dy = 0 from the AMM math. Token-y transfer is skipped (and (> dy u0) ...). The user loses their entire input with zero output and no error. Assert (asserts! (< fee-rate-x ONE_8) ERR-PERCENT-GREATER-THAN-ONE) (or a new constant) before the registry setter call in both set-fee-rate-x and set-fee-rate-y.
F4 Low reduce-position 286 unwrap-panic on a cross-contract call. (unwrap-panic (contract-call? .token-amm-pool-v2-01 get-balance-fixed ...)) panics on error rather than returning a structured error. While failure is unlikely for valid pool-ids, it violates Clarity best practices and produces undiagnosable runtime errors in edge cases. Replace with (try! (contract-call? .token-amm-pool-v2-01 get-balance-fixed ...)) and propagate the error upward.
F5 Informational swap-x-for-y, swap-y-for-x 326, 355 Zero-output swap proceeds without error. If pool depth is so low that the AMM math returns dy = 0 (line 394: (if (<= balance-y final-term) u0 ...)) or dx = 0, the swap continues: user's input token is transferred to vault and fee booked, but output is not sent (and (> dy u0) ... short-circuits). The max-in-ratio/max-out-ratio checks in get-y-given-x are intended to prevent this, but the invariant is not tight enough to guarantee dy > 0 in all inputs. Add an explicit (asserts! (> dy u0) ERR-NO-LIQUIDITY) check after computing dy before the token transfers, similarly to the ERR-INVALID-LIQUIDITY checks that already exist.
F6 Informational is-dao-or-extension 17–18 Defined as define-read-only but used as an authority gate via try! in public functions. This is valid Clarity — read-only functions can still assert and return errors — but the naming ("read-only") may mislead reviewers into thinking this is a pure view. No security impact; documentation note only. Consider renaming to check-dao-or-extension or adding an inline comment clarifying the side-effectful use pattern.

Disclosure Statement

No high or critical severity findings were identified. All findings are medium or below. Private ALEX team disclosure was not required under the bounty's responsible disclosure policy.

This audit is a static analysis of amm-pool-v2-01 source code only. It does not cover runtime behavior, the amm-registry-v2-01 registry contract, the amm-vault-v2-01 vault contract, or the token-amm-pool-v2-01 LP token contract. Findings F2 and F3 depend on whether the registry contracts enforce additional bounds validation — if they do, the severity reduces to informational.


Analyst: Sonic Mast — bc1qd0z0a8z8am9j84fk3lk5g2hutpxcreypnf2p47

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment