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
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:
add-to-positionlacks the blocklist/default-account guard used bycreate-pool,reduce-position, and both direct swap functions.set-oracle-averagedoes not enforcenew-oracle-average <= ONE_8, even thoughERR-ORACLE-AVERAGE-BIGGER-THAN-ONEexists andget-oracle-resilientsubtracts the stored value fromONE_8.set-fee-rate-xandset-fee-rate-ydo not cap fee rates, allowing privileged configuration that can make swaps abort through zero-net-input/zero-net-output arithmetic.
| 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. |
| 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. |
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 | 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. |
| 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 |
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. |
| 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. |
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 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.
Clarity checked arithmetic prevents silent overflow/underflow. The contract also uses explicit saturating patterns in several places, for example:
balance-yfloor to zero whendy >= balance-yat line 315.balance-xfloor to zero whendx >= balance-xat 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.
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.
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.
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.
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.
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.
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-ownerlocally with the same registry helper before pool creation, or document and test that.amm-registry-v2-01 create-poolperforms this check.
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.
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.
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.
- 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.
add-to-positionshould reject a blocklisted/defaulttx-sender.set-oracle-averageshould reject values greater thanONE_8.set-fee-rate-xandset-fee-rate-yshould reject protocol-invalid fee rates; swaps should explicitly reject zero net input.create-poolwith a blocklisted/defaultpool-ownershould fail or be proven to fail in registry tests.reduce-positionshould handle pool-token balance read failure with a deterministic error.- 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.
- Active-window failures should be covered for
swap-x-for-y,swap-y-for-x, and helper routes. - Oracle-enabled swaps should verify
oracle-resilientupdate behavior for both pair orderings.
This report contains no high or critical severity finding. No private disclosure was required before public submission under the bounty rules.
- Medium:
add-to-positionlacks the blocklist/default-account guard present in create/reduce/swap entry points. - Medium:
set-oracle-averagedoes not enforce<= ONE_8, enabling privileged misconfiguration that can abort oracle-enabled swaps. - Medium: fee-rate setters do not cap rates, allowing privileged configuration that can make swap net-input arithmetic hit zero.