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
| Variable | Type | Default | Mutated by |
|---|---|---|---|
paused |
bool |
true |
pause (DAO/extension only) |
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 balances and supply tracked in the LP token contract. Minted by add-to-position, burned by reduce-position.
Actual token custody. transfer-ft, transfer-ft-two, add-to-reserve called under as-contract to move tokens.
| 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 |
| 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 |
| 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)
| 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.
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.
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).
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 is used only for outbound calls to trusted sibling contracts (registry, vault, LP token). No unexpected escalation.
<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.
| 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. |
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