Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save pamorgan01/1ce4d1e5a9b587dcbdf609e588c737af to your computer and use it in GitHub Desktop.

Select an option

Save pamorgan01/1ce4d1e5a9b587dcbdf609e588c737af to your computer and use it in GitHub Desktop.
AIBTC ALEX AMM pool v2 static-analysis report - repaired public gist

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

Submission target: AIBTC bounty "Audit: ALEX AMM pool v2 (amm-pool-v2-01) -- static-analysis"
Contract: SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01
Source: https://api.hiro.so/v2/contracts/source/SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM/amm-pool-v2-01
Review type: static analysis only
Reviewed source size: 40,659 characters

Executive Summary

I reviewed the AMM pool contract surface for state ownership, public entry points, token movement, authorization, pause handling, oracle controls, and Clarity footguns.

No critical issue is reported in this public submission. The highest items are medium severity correctness and access-control hardening findings:

  1. add-to-position lacks the blocklist/default-account guard used by create-pool, reduce-position, and both direct swap functions.
  2. set-oracle-average does not enforce new-oracle-average <= ONE_8, even though ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE exists and get-oracle-resilient subtracts the stored value from ONE_8.
  3. set-fee-rate-x and set-fee-rate-y do not cap fee rates, allowing privileged configuration that can make swaps abort through zero-net-input/zero-net-output arithmetic.

State Model

Local State

Name Type Line Mutated by Notes
paused bool 16 pause Contract-level pause flag. Initial value is true, so the pool begins paused until DAO/extension action unpauses it.

Constants

Name Line Purpose
ERR-NOT-AUTHORIZED 2 Authorization or invalid active-window failure.
ERR-POOL-ALREADY-EXISTS 3 Declared locally; pool creation is delegated to registry.
ERR-INVALID-POOL 4 Declared locally; registry handles pool lookups.
ERR-BLOCKLISTED 5 Blocklist/default-account guard.
ERR-INVALID-LIQUIDITY 6 Invalid liquidity or swap-ratio guard.
ERR-PERCENT-GREATER-THAN-ONE 7 Liquidity burn percent exceeds fixed-point 1.0.
ERR-EXCEEDS-MAX-SLIPPAGE 8 Slippage bound violation.
ERR-ORACLE-NOT-ENABLED 9 Oracle read attempted while disabled.
ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE 10 Declared locally; set delegated to registry.
ERR-PAUSED 11 Mutating liquidity/swap call while paused.
ERR-SWITCH-THRESHOLD-BIGGER-THAN-ONE 12 Declared locally; threshold source delegated to registry.
ERR-NO-LIQUIDITY 13 Empty pool or impossible price/position query.
ERR-MAX-IN-RATIO 14 Swap input ratio exceeds configured limit.
ERR-MAX-OUT-RATIO 15 Swap output ratio exceeds configured limit.
ONE_8 481 Fixed-point one (100000000).
MAX_POW_RELATIVE_ERROR 482 Fixed-point exponent math tolerance.
Math bounds/errors 506-527 Bounds for logarithm/exponent helper functions.

External State Dependencies

The contract stores almost all pool state externally via registry and vault contracts:

External contract Used for Evidence
.amm-registry-v2-01 Pool metadata, balances, fee rates, oracle parameters, max ratio limits, blocklist checks, and pool updates. Lines 19-28, 40-53, 75-85, 196-197, 202-256, 275, 298, 328, 357.
.amm-vault-v2-01 Custody and token transfers out of the vault; reserve accounting. Lines 273-274, 297, 325-327, 354-356.
.token-amm-pool-v2-01 Pool share mint/burn and balance reads. Lines 276, 286, 299.
.executor-dao DAO and extension authorization. Lines 17-18, 198-201.
FT trait tokens User deposits and AMM withdrawals/swaps. Lines 273-274, 297, 325-327, 354-356.

Function Inventory

Read-Only Functions

Function Lines Authority Reads/mutations Notes
is-dao-or-extension 17-18 tx-sender == .executor-dao or contract-caller registered extension Reads .executor-dao extension status Returns an ok wrapping asserts!; callers use try! or is-ok.
is-blocklisted-or-default 19-20 Open Registry read Used for tx-sender checks.
get-switch-threshold 21-22 Open Registry read Controls formula branch.
get-pool-details-by-id 23-24 Open Registry read Pool lookup by ID.
get-pool-details 25-26 Open Registry read Main pool lookup.
get-pool-exists 27-28 Open Registry read Used to infer route direction.
is-paused 29-30 Open Reads local paused No mutation.
get-balances 31-35 Open Registry read Returns balance-x and balance-y.
get-start-block / get-end-block 36-39 Open Registry read Pool active window.
get-max-in-ratio / get-max-out-ratio 40-43 Open Registry read Ratio controls.
check-pool-status 44-48 Open Registry read Requires block-height inside pool window.
get-oracle-enabled / get-oracle-average 49-52 Open Registry read Oracle settings.
get-oracle-resilient 53-60 Open Registry read Requires oracle enabled, combines instant and prior resilient oracle.
get-oracle-instant 61-69 Open Registry read Supports inverse pair lookup.
get-price 70-74 Open Registry read Calls internal price math.
Threshold/fee/owner getters 75-85 Open Registry read Pool configuration.
Swap quote functions 87-130 Open Registry read + math Enforce max in/out ratios.
Price-position quote functions 131-161 Open Registry read + math Enforce nonzero liquidity where needed.
Multi-hop helpers 162-177 Open Registry read + quote math Infer pair direction and chain quotes.
Fee helpers 178-191 Open Registry read Sum configured fee rates across routes.
get-invariant 192-195 Open Pure math + registry threshold read Computes invariant branch by t.
get-max-ratio-limit 196-197 Open Registry read Delegated limit.

Public Mutating Functions

Function Lines Caller authority State mutations External calls / transfers
pause 198-201 DAO or DAO extension only Sets local paused None
set-start-block 202-206 Pool owner or DAO/extension Registry updates start block .amm-registry-v2-01 set-start-block via as-contract
set-end-block 207-211 Pool owner or DAO/extension Registry updates end block Registry via as-contract
set-max-in-ratio 212-216 Pool owner or DAO/extension Registry updates max input ratio Registry via as-contract
set-max-out-ratio 217-221 Pool owner or DAO/extension Registry updates max output ratio Registry via as-contract
set-oracle-enabled 222-226 Pool owner or DAO/extension Registry updates oracle flag Registry via as-contract
set-oracle-average 227-231 Pool owner or DAO/extension Registry updates oracle weighting Registry via as-contract
set-threshold-x 232-236 Pool owner or DAO/extension Registry updates X threshold Registry via as-contract
set-threshold-y 237-241 Pool owner or DAO/extension Registry updates Y threshold Registry via as-contract
set-fee-rate-x 242-246 Pool owner or DAO/extension Registry updates X fee Registry via as-contract
set-fee-rate-y 247-251 Pool owner or DAO/extension Registry updates Y fee Registry via as-contract
create-pool 252-256 Any non-blocklisted/default tx-sender Registry creates pool, then liquidity added Registry create via as-contract; then add-to-position
add-to-position 257-278 Any caller while unpaused; no blocklist/default-account guard Registry pool balances/supply update; pool-share mint Pulls token X/Y from caller into vault; mints pool tokens
reduce-position 279-301 Non-blocklisted/default caller while unpaused Registry pool balances/supply update; pool-share burn Vault transfers token X/Y to caller; burns pool tokens
swap-x-for-y 302-330 Non-blocklisted/default caller while unpaused and active pool window Registry balances/oracle update; reserve update Pulls token X, transfers token Y, records reserve
swap-y-for-x 331-359 Non-blocklisted/default caller while unpaused and active pool window Registry balances/oracle update; reserve update Pulls token Y, transfers token X, records reserve
swap-helper 360-363 Same as called swap leg Same as one swap leg Calls swap-x-for-y or inverse swap-y-for-x
swap-helper-a/b/c 364-375 Same as all called swap legs Same as multi-hop route Chains nested swap helpers

Post-Condition Coverage Matrix

Users should attach post-conditions that cover actual token movement, not only route intent.

Function User sends User receives Recommended caller post-conditions
create-pool dx of token X and computed dy of token Y AMM pool-share tokens Limit outgoing token X to dx; limit outgoing token Y to chosen max dy; require incoming pool-share token amount if tooling supports it.
add-to-position dx of token X and computed dy of token Y AMM pool-share tokens Limit outgoing X to dx; limit outgoing Y to max-dy when provided; require no unrelated token outflows.
reduce-position AMM pool-share tokens burned Token X and token Y from vault Limit outgoing pool-share amount based on percent; require minimum incoming X/Y if caller needs slippage protection.
swap-x-for-y Token X amount dx Token Y amount dy Limit outgoing X to dx; require incoming Y at least min-dy.
swap-y-for-x Token Y amount dy Token X amount dx Limit outgoing Y to dy; require incoming X at least min-dx.
swap-helper First leg input token, direction inferred by pool existence Opposite token for selected pair Include post-conditions for the inferred route direction. Do not assume parameter order equals transfer direction.
swap-helper-a/b/c First route token Final route token Include strict max-out for initial token and min-in for final token. For conservative wallets, also restrict intermediate token movement because nested helpers perform real transfers between legs.

Authority / Access-Control Matrix

Surface Guard Assessment
Global pause pause uses try! (is-dao-or-extension) at lines 198-201. Strong DAO/extension-only guard.
Pool parameter setters Lines 202-251 allow tx-sender == pool-owner or DAO/extension. Clear split between pool owner and DAO control.
Pool creation Line 254 rejects blocklisted/default tx-sender; line 255 creates pool through registry. Open creation model, but see finding ALEX-LOW-001 about unchecked pool-owner.
Liquidity add Lines 270-272 enforce unpaused, positive liquidity, and max dy, but no blocklist/default-account check. Missing parity with create/reduce/swap guards.
Liquidity reduce Lines 294-296 enforce blocklist/default, unpaused, and percent <= ONE_8. Good, but see finding ALEX-INFO-002 on unwrap-panic.
Swaps Lines 319-324 and 348-353 enforce blocklist/default, unpaused, active pool window, positive input, price-ratio sanity, and slippage. Good surface coverage.
Helper swaps Lines 360-375 delegate to guarded swap functions. Guards execute per leg; post-condition complexity increases for users.
Oracle controls Pool owner or DAO/extension can enable and tune oracle average, thresholds, fees. Privileged by design; users should not treat oracle settings as immutable.

Clarity Best-Practice Review

tx-sender vs contract-caller

The contract intentionally uses tx-sender for end-user identity in liquidity and swap operations. DAO extension authorization checks both .executor-dao as tx-sender and .executor-dao is-extension contract-caller at lines 17-18.

No clear instance was found where contract-caller appears intended but tx-sender is used accidentally. However, pool-owner setters authorize by tx-sender, so delegated pool-owner management through another contract would not work unless routed through DAO extension authorization.

unwrap-panic

unwrap-panic appears at line 286 when reading AMM pool-token balance. This is not directly exploitable by itself because a panic reverts the transaction, but it is less graceful than the surrounding try!/domain-error style.

Arithmetic Safety

Clarity checked arithmetic prevents silent overflow/underflow. The contract also uses explicit saturating patterns in several places, for example:

  • balance-y floor to zero when dy >= balance-y at line 315.
  • balance-x floor to zero when dx >= balance-x at line 343.
  • burn/update floor-to-zero logic at line 292.

The main arithmetic risk is not unchecked overflow, but precision and rounding behavior in the invariant and exponent helpers. The contract uses mul-up, mul-down, div-up, div-down, pow-up, and pow-down to make rounding direction explicit.

as-contract

The contract uses as-contract for registry, vault, reserve, mint, and burn calls. This is expected because external systems likely authorize this AMM contract as the mutator. The key operational requirement is that registry and vault contracts must restrict calls to this pool contract and compatible versions.

Trait Conformance

Token inputs are constrained through <ft-trait>. The contract relies on transfer-fixed rather than raw SIP-010 transfer?, so compatible assets must expose the expected fixed transfer wrapper. This is a design dependency, not a direct vulnerability.

Findings

ALEX-MED-001: add-to-position is missing the blocklist/default-account guard used by sibling mutating functions

Severity: Medium
Function: add-to-position
Lines: 257-278

create-pool rejects blocklisted/default tx-sender at line 254. reduce-position rejects blocklisted/default tx-sender at line 294. swap-x-for-y and swap-y-for-x reject blocklisted/default tx-sender at lines 319 and 348.

add-to-position is the outlier: it checks pause state, positive liquidity, and slippage at lines 270-272, then transfers both tokens from tx-sender to the vault at lines 273-274. It never calls is-blocklisted-or-default.

Impact: a blocklisted/default principal that cannot create a pool, remove liquidity, or swap directly can still add liquidity directly to an existing pool if it holds both assets. This weakens the registry blocklist policy and can create operational edge cases where blocked liquidity remains in the pool until some other policy path handles it.

Recommendation:

  • Add the same guard used by the other user-facing mutating functions:
(asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED)
  • Add a regression test proving a blocklisted/default account cannot add liquidity.

ALEX-MED-002: set-oracle-average lacks an upper bound and can configure oracle arithmetic to abort

Severity: Medium
Function: set-oracle-average
Lines: 227-231, 53-60

The contract defines ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE at line 10, but set-oracle-average does not use it. The setter only checks pool-owner or DAO/extension authorization at line 230, then delegates the new value to the registry at line 231.

get-oracle-resilient later computes:

(- ONE_8 (get oracle-average pool))

at line 59. If oracle-average is set above ONE_8, this subtraction underflows and aborts the read. Because swaps update oracle-resilient through try! (get-oracle-resilient ...) at lines 316 and 345 when oracle is enabled, the misconfiguration can make swaps for the pool fail.

Impact: privileged misconfiguration by pool owner or DAO/extension can break oracle-enabled swaps for the pool. This is not an arbitrary external attacker path, but it is a real production safety issue because the contract already anticipates the invalid value with a dedicated error constant.

Recommendation:

  • Add a local bound check before delegating to the registry:
(asserts! (<= new-oracle-average ONE_8) ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE)
  • Add tests for ONE_8, ONE_8 + 1, and normal weighted-average values.

ALEX-MED-003: Fee-rate setters allow values that can make swaps abort through zero-net arithmetic

Severity: Medium
Functions: set-fee-rate-x, set-fee-rate-y, swap-x-for-y, swap-y-for-x
Lines: 242-251, 309-323, 338-352

set-fee-rate-x and set-fee-rate-y only check pool-owner or DAO/extension authorization, then delegate the fee rates to the registry. They do not cap fee-rate-x or fee-rate-y.

In swap-x-for-y, the contract computes:

(fee (mul-up dx (get fee-rate-x pool)))
(dx-net-fees (if (<= dx fee) u0 (- dx fee)))

at lines 309-310, then uses dx-net-fees in quote and price-ratio logic. If the configured fee is greater than or equal to the input, dx-net-fees becomes zero. The later assertion at line 323 divides by dx-net-fees; this can abort before token transfer. The Y-to-X path mirrors the issue with dy-net-fees at lines 338-352.

Impact: privileged fee misconfiguration can make swaps fail for the affected side of a pool. Because this can be introduced by pool owner configuration, it is best treated as a safety/control-plane bug rather than a user-exploitable theft path.

Recommendation:

  • Cap fee rates below ONE_8, and preferably at a much lower protocol maximum.
  • Add explicit assertions that net input is nonzero before quote/ratio arithmetic:
(asserts! (> dx-net-fees u0) ERR-INVALID-LIQUIDITY)

and the equivalent for dy-net-fees.

ALEX-LOW-004: create-pool validates creator blocklist status but not the supplied pool-owner

Severity: Low
Function: create-pool
Lines: 252-256

create-pool rejects blocklisted/default tx-sender at line 254, then passes the caller-supplied pool-owner to the registry at line 255. If the registry does not independently validate pool-owner, a permitted creator can create a pool owned by a blocklisted/default principal or by an unintended governance principal.

Impact depends on registry behavior. If registry validates owners, impact is only redundant defense. If registry does not, the pool could be configured with an owner that should not be allowed to control fee, oracle, threshold, or active-window settings.

Recommendation:

  • Validate pool-owner locally with the same registry helper before pool creation, or document and test that .amm-registry-v2-01 create-pool performs this check.

ALEX-INFO-005: reduce-position uses unwrap-panic for pool-token balance read

Severity: Informational
Function: reduce-position
Line: 286

reduce-position reads the caller's pool-token balance with:

(unwrap-panic (contract-call? .token-amm-pool-v2-01 get-balance-fixed ...))

Most of the contract uses try! to preserve structured error propagation. Here, any unexpected error from the pool-token contract becomes a panic. This is not a fund-loss path by itself because the transaction reverts, but it reduces diagnosability and makes behavior less consistent.

Recommendation:

  • Replace with try! if the return type allows it, or map the failure to a domain error.
  • Add a regression test for failure of the pool-token balance read.

ALEX-INFO-006: Multi-hop swap helpers increase post-condition complexity

Severity: Informational
Functions: swap-helper, swap-helper-a, swap-helper-b, swap-helper-c
Lines: 360-375

The helper functions chain real swap calls, and swap-helper infers direction based on whether the pool exists in the provided order. This is convenient, but it means users and wallet tooling must reason about intermediate transfers and inferred route direction.

For example, swap-helper-a at lines 364-365 first swaps token X to token Y, then token Y to token Z. The final min-dz protects the last output, but callers should still attach post-conditions that bound the first outgoing token and prevent unexpected extra token movement along the route.

Recommendation:

  • Publish route-specific post-condition examples for helper swaps.
  • Consider exposing a read-only route preview that returns all intermediate legs, token principals, and expected transfer directions for wallet UI preflight.

ALEX-INFO-007: Error code ERR-NOT-AUTHORIZED is reused for inactive pool windows

Severity: Informational
Function: check-pool-status
Lines: 44-48

check-pool-status returns ERR-NOT-AUTHORIZED when the current block height is outside a pool's active window. This is not a security issue, but it can mislead clients and operators because active-window failures are not authorization failures.

Recommendation:

  • Introduce a dedicated error such as ERR-POOL-NOT-ACTIVE.
  • If changing error codes is too disruptive, document the current meaning in SDK/client code.

Additional Observations

  • The contract starts paused at line 16, which is a safe deployment default.
  • Liquidity add and swaps reject paused state before token transfers.
  • Swap functions update registry balances after token transfer and vault transfer steps; failed downstream calls revert the transaction.
  • Fee collection separates net input, rebate, and reserve updates in lines 309-327 and 338-356.
  • Max input/output ratio checks occur in read-only quote helpers before swap state updates.
  • The contract does not expose direct admin functions for registry/vault ownership; those concerns are external to this review.

Suggested Tests

  1. add-to-position should reject a blocklisted/default tx-sender.
  2. set-oracle-average should reject values greater than ONE_8.
  3. set-fee-rate-x and set-fee-rate-y should reject protocol-invalid fee rates; swaps should explicitly reject zero net input.
  4. create-pool with a blocklisted/default pool-owner should fail or be proven to fail in registry tests.
  5. reduce-position should handle pool-token balance read failure with a deterministic error.
  6. Multi-hop helpers should have tests that assert:
    • first leg input is bounded,
    • final min-output is enforced,
    • intermediate route direction is correct when only the inverse pool exists.
  7. Active-window failures should be covered for swap-x-for-y, swap-y-for-x, and helper routes.
  8. Oracle-enabled swaps should verify oracle-resilient update behavior for both pair orderings.

Disclosure Statement

This report contains no high or critical severity finding. No private disclosure was required before public submission under the bounty rules.

Three-Line Submission Summary

  1. Medium: add-to-position lacks the blocklist/default-account guard present in create/reduce/swap entry points.
  2. Medium: set-oracle-average does not enforce <= ONE_8, enabling privileged misconfiguration that can abort oracle-enabled swaps.
  3. Medium: fee-rate setters do not cap rates, allowing privileged configuration that can make swap net-input arithmetic hit zero.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment