Auditor: Emerald Castle (bc1qzhlus0nesaphjy5jfd2tnef9te9j4mq57mzd6r)
Contract: SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01
Source: https://api.hiro.so/v2/contracts/source/SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM/amm-pool-v2-01
Protocol: ALEX — primary AMM v2 pool router and math execution contract
Audit Type: Static analysis only (no execution)
Date: 2026-06-03
| Variable | Type | Initial Value | Mutated By | Authority |
|---|---|---|---|---|
paused |
bool | true | pause |
DAO or whitelisted extension |
This contract contains no local data maps. All pool state records (balances, fee rates, oracle parameters, thresholds, and total supply) are stored in and retrieved from the companion registry contract:
.amm-registry-v2-01
| Constant | Value | Purpose |
|---|---|---|
ERR-NOT-AUTHORIZED |
(err u1000) |
Standard authorization failure |
ERR-POOL-ALREADY-EXISTS |
(err u2000) |
Attempting to create an existing pool |
ERR-INVALID-POOL |
(err u2001) |
Provided token parameters do not map to an active pool |
ERR-BLOCKLISTED |
(err u2002) |
Action attempted by a blocklisted address |
ERR-INVALID-LIQUIDITY |
(err u2003) |
Provided token amounts are invalid (e.g., zero) |
ERR-PERCENT-GREATER-THAN-ONE |
(err u2004) |
Withdrawal percentage exceeds 100% (ONE_8) |
ERR-EXCEEDS-MAX-SLIPPAGE |
(err u2005) |
Resulting output does not meet slippage bounds |
ERR-ORACLE-NOT-ENABLED |
(err u2006) |
Attempting to fetch TWAP price for a pool without active oracle |
ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE |
(err u2007) |
Oracle weight exceeds 1.0 (defined but never asserted) |
ERR-PAUSED |
(err u2008) |
Operations suspended due to emergency contract pause |
ERR-SWITCH-THRESHOLD-BIGGER-THAN-ONE |
(err u2009) |
Threshold exceeds 1.0 (defined but never asserted) |
ERR-NO-LIQUIDITY |
(err u2010) |
Pool exists but carries zero active liquidity |
ERR-MAX-IN-RATIO |
(err u2011) |
Trade size exceeds the configured max in ratio |
ERR-MAX-OUT-RATIO |
(err u2012) |
Trade output size exceeds the max out ratio |
ONE_8 |
u100000000 |
Fixed-point scale factor (1.0 in 8 decimals) |
- Authority: Open to any caller who is not blocklisted.
- Pre-conditions:
is-blocklisted-or-defaultchecks pass fortx-sender. - State mutations: Creates registry entry for pool, transfers initial tokens, mints initial LP shares.
- External calls:
.amm-registry-v2-01.create-pool,add-to-position.
- Authority: Open (contract must be unpaused).
- Pre-conditions:
not paused,dx > 0,dy > 0, slippage check passes. - State mutations: Updates pool balances and supply in registry, mints LP shares.
- External calls:
token-x-trait.transfer-fixed,token-y-trait.transfer-fixed,.amm-registry-v2-01.update-pool,.token-amm-pool-v2-01.mint-fixed.
- Authority: Open to token holder (must not be blocklisted).
- Pre-conditions:
not paused, caller is not blocklisted,percent <= ONE_8. - State mutations: Decreases pool balances and supply in registry, burns LP shares.
- External calls:
.token-amm-pool-v2-01.get-balance-fixed,.amm-vault-v2-01.transfer-ft-two,.amm-registry-v2-01.update-pool,.token-amm-pool-v2-01.burn-fixed.
- Authority: Open (caller must not be blocklisted).
- Pre-conditions:
not paused, caller not blocklisted, pool status active,dx > 0, slippage checks pass. - State mutations: Deducts fees, calculates output, updates balances and oracle state in registry.
- External calls:
token-x-trait.transfer-fixed,.amm-vault-v2-01.transfer-ft,.amm-vault-v2-01.add-to-reserve,.amm-registry-v2-01.update-pool.
- Authority: Open (caller must not be blocklisted).
- Pre-conditions:
not paused, caller not blocklisted, pool status active,dy > 0, slippage checks pass. - State mutations: Deducts fees, calculates output, updates balances and oracle state in registry.
- External calls:
token-y-trait.transfer-fixed,.amm-vault-v2-01.transfer-ft,.amm-vault-v2-01.add-to-reserve,.amm-registry-v2-01.update-pool.
- Authority: Open (inherited from individual swap functions).
- Pre-conditions: Inherited; executes single or multi-hop token routes.
- External calls:
swap-x-for-y/swap-y-for-x.
- Authority: DAO or whitelisted extension.
- Pre-conditions:
is-dao-or-extensionchecks pass. - State mutations: Updates
pausedvariable.
Parameter Setters (set-start-block, set-end-block, set-max-in-ratio, set-max-out-ratio, set-oracle-enabled, set-oracle-average, set-threshold-x, set-threshold-y, set-fee-rate-x, set-fee-rate-y)
- Authority: Pool owner or DAO.
- Pre-conditions:
tx-sender == pool-owneroris-dao-or-extension. - State mutations: Updates corresponding parameter in registry.
- External calls: Companion functions in
.amm-registry-v2-01.
| Function | Asset Movements | Recommended Post-Conditions |
|---|---|---|
create-pool |
User sends dx of X and dy of Y; user receives LP shares |
ft-transfer of X (== dx), ft-transfer of Y (== dy), ft-mint of LP token (> 0) |
add-to-position |
User sends dx of X and dy of Y; user receives LP shares |
ft-transfer of X (== dx), ft-transfer of Y (<= max-dy), ft-mint of LP token (>= min-lp) |
reduce-position |
User burns LP shares; receives X and Y tokens | ft-burn of LP shares (== shares), ft-receive of X (>= dx), ft-receive of Y (>= dy) |
swap-x-for-y |
User sends dx of X; receives dy of Y |
ft-transfer of X (== dx), ft-receive of Y (>= min-dy) |
swap-y-for-x |
User sends dy of Y; receives dx of X |
ft-transfer of Y (== dy), ft-receive of X (>= min-dx) |
| Operation | Whitelisted Roles | Check Implementation | Impact of Compromise |
|---|---|---|---|
| Emergency Pause | DAO / Extensions | is-dao-or-extension |
Pause/unpause all pool activity globally |
| Parameter Updates | Pool Owner / DAO | tx-sender == pool-owner or is-dao-or-extension |
Arbitrary fee rate changes, oracle state manipulation, trading ratio blocks |
| Swap / Liquidity | Open | Blocklist status checking | Denial of service for blocklisted addresses |
- Oracle Dependency: Pure mathematical TWAP oracle calculated internally based on pool trade balances. No external chainlink or pyth feed dependency.
- Pause Mechanism: Global
pausedvariable. Swaps and liquidity addition checks(asserts! (not (is-paused)) ERR-PAUSED).
- tx-sender vs contract-caller: Correctly captures
tx-senderas the user (sender/who) when doing token transfers. Capturescontract-callerfor extension validation. - Integer Math: Uses customized mathematical helpers (
mul-down,mul-up,div-down,div-up,pow-down,pow-up, and fixed-point natural exponential/log functions). Scale factor is10^8(ONE_8). - Panic propagation: Multiple
unwrap-paniccalls exist inside mathematical operations (e.g. inpow-down,pow-up, andreduce-position). These will trigger transactional reverts without clear failure codes.
| ID | Severity | Function | Line | Finding | Recommended Fix |
|---|---|---|---|---|---|
| A-01 | High | get-oracle-resilient |
53–60 | Mathematical logic error in reverse-order TWAP price calculation | Reciprocate the stored resilient oracle price when querying in the non-canonical direction |
| A-02 | Medium | swap-x-for-y / swap-y-for-x |
309–310, 338–339 | Silent token swallowing when fee rate is configured to >= 100% | Add assertions to validate fee rate inputs are strictly < ONE_8 |
| A-03 | Medium | add-to-position |
257–278 | Missing blocklist validation permits blocklisted accounts to add liquidity | Enforce is-blocklisted-or-default assertion matching other state-mutating paths |
| A-04 | Low | set-oracle-average |
227–231 | Missing upper-bound validation enables denial of service via TWAP calculations | Assert (asserts! (<= new-oracle-average ONE_8) ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE) |
| A-05 | Low | pow-down / pow-up |
494–505 | Internal unwrap-panics in exponentiation helper discard context error codes | Propagate math boundary errors via response types instead of panicking |
Location: Lines 53–60 (get-oracle-resilient), Lines 61–69 (get-oracle-instant)
(define-read-only (get-oracle-resilient (token-x principal) (token-y principal) (factor uint))
(let (
(exists (is-some (get-pool-exists token-x token-y factor)))
(pool (if exists (try! (get-pool-details token-x token-y factor)) (try! (get-pool-details token-y token-x factor))))
(oracle-instant (try! (get-oracle-instant token-x token-y factor))))
(asserts! (get oracle-enabled pool) ERR-ORACLE-NOT-ENABLED)
(ok (+ (mul-down (- ONE_8 (get oracle-average pool)) oracle-instant)
(mul-down (get oracle-average pool) (if (is-eq (get oracle-resilient pool) u0) oracle-instant (get oracle-resilient pool)))))))When get-oracle-resilient is queried in the reverse direction (exists = false):
- The
oracle-instantis correctly computed in reverse (balance-y / balance-x) using theelsebranch ofget-oracle-instant. - However, the calculation of the resilient price multiplies the weight
oracle-averageby the stored(get oracle-resilient pool)value. - The value stored in the registry pool state (
oracle-resilient) is ALWAYS recorded in the canonical direction (balance-x / balance-y) during swap execution. - This results in the function averaging
oracle-instant(expressed as1/P) with the stored resilient price (expressed asP).
Impact: Integrations querying the oracle in the non-canonical direction will receive mathematically corrupted price feeds. This corrupted oracle state breaks downstream protocols (such as lending platforms or synthetic assets) relying on the non-canonical price queries.
Recommended Fix:
When exists is false, the contract must reciprocate the stored resilient oracle price before performing the weighted average:
(let (
(exists (is-some (get-pool-exists token-x token-y factor)))
(pool (if exists (try! (get-pool-details token-x token-y factor)) (try! (get-pool-details token-y token-x factor))))
(oracle-instant (try! (get-oracle-instant token-x token-y factor)))
(stored-resilient (get oracle-resilient pool))
(resilient-dir (if exists
stored-resilient
(if (is-eq stored-resilient u0) u0 (div-down ONE_8 stored-resilient)))))
;; Use resilient-dir instead of (get oracle-resilient pool) in the weighted sum
)Location: Lines 242–251 (set-fee-rate-x/y), Lines 309–310 / 338–339 (swap-x-for-y/y-for-x)
The pool fee setters do not restrict the fee-rate-x/y parameters to be strictly less than 100% (ONE_8). If a pool owner sets the fee rate to ONE_8 (100%) or larger:
- In
swap-x-for-y,feeis computed asmul-up dx fee-rate-x(which makesfee >= dx). dx-net-feesevaluates tou0.dyevaluates tou0.- The swap proceeds without error, transferring the user's entire input
dxtoamm-vault-v2-01and registering the difference in the reserve. - The user receives
0output tokens without any runtime revert.
Impact: Loss of user funds. If a pool's fee parameter is misconfigured or updated maliciously, any swap attempt will consume the user's full input amount without returning any trade output.
Recommended Fix: Enforce fee validation limits in parameter updates:
(define-public (set-fee-rate-x (token-x principal) (token-y principal) (factor uint) (fee-rate-x uint))
(begin
(asserts! (< fee-rate-x ONE_8) ERR-PERCENT-GREATER-THAN-ONE)
;; Proceed with update
))Location: Lines 257–278 (add-to-position)
All mutating paths that alter pool balances check and reject blacklisted actors using is-blocklisted-or-default:
create-pool(line 254)reduce-position(line 294)swap-x-for-y(line 319)swap-y-for-x(line 348)
However, add-to-position does not include this assertion.
Impact: Blocklisted addresses can continue to deposit liquidity into active AMM pools. Once their liquidity is added, it becomes permanently locked because they cannot retrieve it (due to the active blocklist check inside reduce-position). This creates compliance leaks and permanently traps user assets without a formal legal/procedural confiscation path.
Recommended Fix:
Add the blocklist validation check at the entry of add-to-position:
(define-public (add-to-position (token-x-trait <ft-trait>) (token-y-trait <ft-trait>) (factor uint) (dx uint) (max-dy (optional uint)))
(begin
(asserts! (not (is-blocklisted-or-default tx-sender)) ERR-BLOCKLISTED)
;; Proceed with execution
))