Skip to content

Instantly share code, notes, and snippets.

@adamzafir
Created June 3, 2026 14:51
Show Gist options
  • Select an option

  • Save adamzafir/81eca30a91fc7a8b7ca6e0a2f6d61eb5 to your computer and use it in GitHub Desktop.

Select an option

Save adamzafir/81eca30a91fc7a8b7ca6e0a2f6d61eb5 to your computer and use it in GitHub Desktop.
ALEX AMM pool v2 static analysis for AIBTC bounty

ALEX AMM pool v2 static analysis

Contract: SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01

Source:

1. State model

This contract keeps very little local mutable state and delegates most pool bookkeeping to external contracts.

  • Local constants: 27 define-constant entries.
  • Local mutable vars:
    • L16: (define-data-var paused bool true)
  • Externalized state:
    • Pool balances, fee settings, oracle flags, thresholds, pool owner, and supply metadata are read from and written back to .amm-registry-v2-01.
    • Liquidity token supply is minted and burned through .token-amm-pool-v2-01.
    • Asset custody and reserve accounting live in .amm-vault-v2-01.

Practical implication: this contract is mostly a router/controller around registry and vault state. Any security review has to treat cross-contract invariants as first-class.

2. Function inventory

Public entrypoints

Function Line Authority Preconditions / asserts State mutations External calls
pause L198 DAO or approved extension none explicit sets paused none
set-start-block L202 pool owner or DAO/extension; tx-sender bound L205: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-end-block L207 pool owner or DAO/extension; tx-sender bound L210: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-max-in-ratio L212 pool owner or DAO/extension; tx-sender bound L215: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-max-out-ratio L217 pool owner or DAO/extension; tx-sender bound L220: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-oracle-enabled L222 pool owner or DAO/extension; tx-sender bound L225: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-oracle-average L227 pool owner or DAO/extension; tx-sender bound L230: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-threshold-x L232 pool owner or DAO/extension; tx-sender bound L235: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-threshold-y L237 pool owner or DAO/extension; tx-sender bound L240: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-fee-rate-x L242 pool owner or DAO/extension; tx-sender bound L245: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
set-fee-rate-y L247 pool owner or DAO/extension; tx-sender bound L250: (asserts! (or (is-eq tx-sender (get pool-owner pool)) (is-ok (is-dao-or-extension))) ERR-NOT-AUTHORIZED) none .amm-registry-v2-01
create-pool L252 pool owner or DAO/extension; tx-sender bound L254: (asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED) creates pool in registry .amm-registry-v2-01
add-to-position L257 tx-sender bound L270: (asserts! (not (is-paused)) ERR-PAUSED)
L271: (asserts! (and (> dx u0) (> dy u0)) ERR-INVALID-LIQUIDITY)
L272: (asserts! (>= (default-to u340282366920938463463374607431768211455 max-dy) dy) ERR-EXCEEDS-MAX-SLIPPAGE)
updates pool record in registry, mints LP token token-x-trait, token-y-trait, .amm-registry-v2-01, .token-amm-pool-v2-01
reduce-position L279 tx-sender bound L294: (asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED)
L295: (asserts! (not (is-paused)) ERR-PAUSED)
L296: (asserts! (<= percent ONE_8) ERR-PERCENT-GREATER-THAN-ONE)
updates pool record in registry, burns LP token, transfers assets out of vault .token-amm-pool-v2-01, .amm-vault-v2-01, .amm-registry-v2-01
swap-x-for-y L302 tx-sender bound L319: (asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED)
L320: (asserts! (not (is-paused)) ERR-PAUSED)
L322: (asserts! (> dx u0) ERR-INVALID-LIQUIDITY)
L323: (asserts! (<= (div-down dy dx-net-fees) (get-price-internal balance-x balance-y factor)) ERR-INVALID-LIQUIDITY)
L324: (asserts! (<= (default-to u0 min-dy) dy) ERR-EXCEEDS-MAX-SLIPPAGE)
updates pool record in registry, moves fees into vault reserve, transfers assets out of vault token-x-trait, .amm-vault-v2-01, .amm-registry-v2-01
swap-y-for-x L331 tx-sender bound L348: (asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED)
L349: (asserts! (not (is-paused)) ERR-PAUSED)
L351: (asserts! (> dy u0) ERR-INVALID-LIQUIDITY)
L352: (asserts! (>= (div-down dy-net-fees dx) (get-price-internal balance-x balance-y factor)) ERR-INVALID-LIQUIDITY)
L353: (asserts! (<= (default-to u0 min-dx) dx) ERR-EXCEEDS-MAX-SLIPPAGE)
updates pool record in registry, moves fees into vault reserve, transfers assets out of vault token-y-trait, .amm-vault-v2-01, .amm-registry-v2-01
swap-helper L360 open none explicit none none
swap-helper-a L364 open none explicit none none
swap-helper-b L366 open none explicit none none
swap-helper-c L371 open none explicit none none

Read-only entrypoints

Function Line Authority Preconditions / asserts State mutations External calls
is-dao-or-extension L17 DAO or approved extension; tx-sender bound L18: (ok (asserts! (or (is-eq tx-sender .executor-dao) (contract-call? .executor-dao is-extension contract-caller)) ERR-NOT-AUTHORIZED))) none .executor-dao
is-blocklisted-or-default L19 open none explicit none .amm-registry-v2-01
get-switch-threshold L21 open none explicit none .amm-registry-v2-01
get-pool-details-by-id L23 open none explicit none .amm-registry-v2-01
get-pool-details L25 open none explicit none .amm-registry-v2-01
get-pool-exists L27 open none explicit none .amm-registry-v2-01
is-paused L29 open none explicit none none
get-balances L31 open none explicit none none
get-start-block L36 open none explicit none none
get-end-block L38 open none explicit none none
get-max-in-ratio L40 open none explicit none none
get-max-out-ratio L42 open none explicit none none
check-pool-status L44 open L48: (ok (asserts! (and (>= block-height (get start-block pool)) (<= block-height (get end-block pool))) ERR-NOT-AUTHORIZED)))) none none
get-oracle-enabled L49 open none explicit none none
get-oracle-average L51 open none explicit none none
get-oracle-resilient L53 open L58: (asserts! (get oracle-enabled pool) ERR-ORACLE-NOT-ENABLED) none none
get-oracle-instant L61 open L66: (asserts! (get oracle-enabled pool) ERR-ORACLE-NOT-ENABLED) none none
get-price L70 open none explicit none none
get-threshold-x L75 open none explicit none none
get-threshold-y L77 open none explicit none none
get-fee-rebate L79 open none explicit none none
get-fee-rate-x L81 open none explicit none none
get-fee-rate-y L83 open none explicit none none
get-pool-owner L85 pool owner or DAO/extension none explicit none none
get-y-given-x L87 open L95: (asserts! (< dx (mul-down (get balance-x pool) (get max-in-ratio pool))) ERR-MAX-IN-RATIO)
L96: (asserts! (< dy (mul-down (get balance-y pool) (get max-out-ratio pool))) ERR-MAX-OUT-RATIO)
none none
get-x-given-y L98 open L106: (asserts! (< dy (mul-down (get balance-y pool) (get max-in-ratio pool))) ERR-MAX-IN-RATIO)
L107: (asserts! (< dx (mul-down (get balance-x pool) (get max-out-ratio pool))) ERR-MAX-OUT-RATIO)
none none
get-y-in-given-x-out L109 open L117: (asserts! (< dy (mul-down (get balance-y pool) (get max-in-ratio pool))) ERR-MAX-IN-RATIO)
L118: (asserts! (< dx (mul-down (get balance-x pool) (get max-out-ratio pool))) ERR-MAX-OUT-RATIO)
none none
get-x-in-given-y-out L120 open L128: (asserts! (< dx (mul-down (get balance-x pool) (get max-in-ratio pool))) ERR-MAX-IN-RATIO)
L129: (asserts! (< dy (mul-down (get balance-y pool) (get max-out-ratio pool))) ERR-MAX-OUT-RATIO)
none none
get-x-given-price L131 open L135: (asserts! (< price (get-price-internal (get balance-x pool) (get balance-y pool) factor)) ERR-NO-LIQUIDITY) none none
get-y-given-price L137 open L141: (asserts! (> price (get-price-internal (get balance-x pool) (get balance-y pool) factor)) ERR-NO-LIQUIDITY) none none
get-token-given-position L143 open L148: (asserts! (and (> dx u0) (> dy u0)) ERR-NO-LIQUIDITY) none none
get-position-given-mint L150 open L154: (asserts! (> (get total-supply pool) u0) ERR-NO-LIQUIDITY) none none
get-position-given-burn L156 open L160: (asserts! (> (get total-supply pool) u0) ERR-NO-LIQUIDITY) none none
get-helper L162 open none explicit none none
get-helper-a L166 open none explicit none none
get-helper-b L168 open none explicit none none
get-helper-c L173 open none explicit none none
fee-helper L178 open none explicit none none
fee-helper-a L182 open none explicit none none
fee-helper-b L184 open none explicit none none
fee-helper-c L188 open none explicit none none
get-invariant L192 open none explicit none none
get-max-ratio-limit L196 open none explicit none .amm-registry-v2-01

3. Post-condition coverage matrix

The highest-risk public flows are the ones that move tokens or LP supply:

Function Token movements callers should protect Recommended caller post-conditions
create-pool transfers dx + derived dy from caller into vault; mints LP token sender FT decrease for both input tokens; LP token increase to caller
add-to-position transfers dx + derived dy into vault; mints LP token sender FT decrease for both inputs; LP token increase
reduce-position burns LP token; transfers token X/Y from vault to caller LP token decrease; minimum X/Y receive post-conditions
swap-x-for-y transfers token X from caller, transfers token Y from vault, books fees token X decrease capped at input; token Y increase at or above min-dy
swap-y-for-x transfers token Y from caller, transfers token X from vault, books fees token Y decrease capped at input; token X increase at or above min-dx
swap-helper* and swap-*-multi chained transfers through one or more pools per-hop minimum output where available; aggregate receive floor on terminal asset
admin setters no user asset movement, but registry writes alter future swap behavior no token post-conditions; governance callers should audit target values out-of-band

4. Authority / access-control matrix

  • Global pause: local paused flag gates liquidity and swap flows.
  • DAO / extension control: pause requires is-dao-or-extension, which checks either tx-sender == .executor-dao or extension approval in .executor-dao.
  • Pool-owner control: setter functions such as set-start-block, set-end-block, fee setters, threshold setters, and oracle toggles allow either the pool owner or DAO/extension to mutate registry-backed configuration.
  • Oracle dependencies: oracle-enabled behavior relies on pool metadata in .amm-registry-v2-01 and the resilient-oracle helper path.
  • Privileged surfaces:
    • .amm-registry-v2-01 for pool metadata creation and updates.
    • .amm-vault-v2-01 for reserve and transfer operations.
    • .token-amm-pool-v2-01 for LP mint/burn authority.

5. Clarity best-practice review

  • tx-sender is used throughout public liquidity and admin flows. That is likely intentional, but it couples authorization and transfer source semantics to the outermost caller and reduces composability for wrappers or smart-wallet patterns.
  • as-contract is used for registry and vault writes in multiple admin and swap paths. That is a normal pattern here, but it makes cross-contract invariant review mandatory because the effective caller changes during those subcalls.
  • Arithmetic-heavy pricing helpers (pow-down, pow-up, invariant math, quote helpers) are a critical precision surface; they deserve fuzzing beyond static review.
  • No obvious trait-conformance gap surfaced from the top-level contract text. The contract consistently uses the declared SIP-010 and pool traits.

6. Findings table

ID Severity Function Line Finding Recommended fix
ALEX-1 low swap-x-for-y L315 Reserve updates clamp negative balances to zero instead of aborting, which can hide quote overshoot or invariant drift if a future math change produces dy > balance-y. Assert reserve sufficiency before state mutation and revert instead of saturating to zero.
ALEX-2 low swap-y-for-x L343 The mirror path uses the same saturating pattern for balance-x, so a bad quote or rounding overshoot would silently zero reserves instead of failing fast. Use an explicit asserts! (> balance-x dx) or equivalent invariant guard before pool state is committed.
ALEX-3 informational get-oracle-resilient L60 When the resilient oracle snapshot is unset, the contract silently falls back to the instantaneous pool price, reducing manipulation resistance exactly when the resilient feed is absent. Surface an explicit uninitialized-oracle state or require the resilient snapshot to be set before oracle-enabled pools rely on the blended price.

Top 3 findings summary

  1. swap-x-for-y floors reserve underflow to zero instead of reverting if the quote path ever overshoots available Y reserve.
  2. swap-y-for-x mirrors the same saturating-reserve behavior on X reserve updates.
  3. get-oracle-resilient silently falls back to the instantaneous pool price when the resilient snapshot is unset, weakening oracle hardening.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment