Target bounty: mpwj1ido1a0890ed463c
Contract: SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01
Source reviewed: https://api.hiro.so/v2/contracts/source/SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM/amm-pool-v2-01
Review type: static source review only. I did not execute mainnet swaps.
Responsible disclosure note: I found no high or critical severity issue in this pass. No private disclosure was required before this public report.
Local router/pool state:
| Store | Type | Initial | Mutated by | Notes |
|---|---|---|---|---|
paused |
bool data-var |
true |
pause |
Gates add/reduce/swap flows, but not read-only quote helpers |
Registry-backed pool state is read and updated through .amm-registry-v2-01. The reviewed contract treats the registry as canonical for:
- pool existence and
pool-id balance-x,balance-ytotal-supplystart-block,end-blockmax-in-ratio,max-out-ratiooracle-enabled,oracle-average,oracle-resilientthreshold-x,threshold-yfee-rebate,fee-rate-x,fee-rate-ypool-owner
Asset state is held outside this contract:
- SIP-010 token balances are moved through user-supplied token traits.
- Pool reserves are held in
.amm-vault-v2-01. - LP token balances are tracked by
.token-amm-pool-v2-01.
Important constants:
| Name | Value / role |
|---|---|
ONE_8 |
u100000000, fixed-point scale |
MAX_POW_RELATIVE_ERROR |
u4, exponentiation rounding guard |
ERR-* u1000..u2012 |
pool/control/swap errors |
ERR-* u5009..u5013 |
fixed-point math bounds errors |
MAX_NATURAL_EXPONENT, MIN_NATURAL_EXPONENT, MILD_EXPONENT_BOUND |
exponent/log bounds |
Read-only functions:
| Group | Functions | Notes |
|---|---|---|
| Authority/status | is-dao-or-extension, is-blocklisted-or-default, is-paused, check-pool-status |
is-dao-or-extension authorizes DAO or extension callers |
| Pool metadata | get-pool-details*, get-pool-exists, get-balances, get-start-block, get-end-block, ratio/fee/oracle/threshold getters |
Registry passthroughs |
| Quote/oracle | get-oracle-resilient, get-oracle-instant, get-price, get-y-given-x, get-x-given-y, exact-out helpers, price-target helpers |
Uses registry balances and fixed-point math |
| LP math | get-token-given-position, get-position-given-mint, get-position-given-burn |
Converts between deposits/burns and LP supply |
| Multi-hop helpers | get-helper, get-helper-a, get-helper-b, get-helper-c, fee-helper* |
Quote-only path helpers |
| Math | get-invariant plus private fixed-point log/exp/pow helpers |
No storage mutation |
Public functions:
| Function | Authority | Preconditions / asserts | State mutations / external calls |
|---|---|---|---|
pause |
DAO or registered DAO extension | is-dao-or-extension |
var-set paused |
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 |
pool owner or DAO/extension | pool exists; caller authorized | as-contract registry updates |
create-pool |
any non-blocklisted caller | blocklist check | registry pool creation, then initial add-to-position |
add-to-position |
open | not paused; positive liquidity; dy <= max-dy |
transfers both tokens to vault, registry update, LP mint |
reduce-position |
open | not blocklisted; not paused; percent <= ONE_8 |
vault transfers both tokens, registry update, LP burn |
swap-x-for-y |
open | not blocklisted; not paused; pool active; dx > 0; output >= optional min |
transfers X to vault, transfers Y out, reserve/registry update |
swap-y-for-x |
open | not blocklisted; not paused; pool active; dy > 0; output >= optional min |
transfers Y to vault, transfers X out, reserve/registry update |
swap-helper, swap-helper-a/b/c |
open | delegated to swap functions | chained swaps; only final helper call receives the optional min |
| Public function | Token movement surface | Suggested postconditions |
|---|---|---|
create-pool |
User deposits initial X and Y; LP minted to caller | At-most X spend dx; at-most Y spend dy; at-least/expected LP mint if supported by the LP token contract |
add-to-position |
User deposits X plus computed Y; LP minted | At-most X spend dx; at-most Y spend supplied max-dy; at-least LP mint based on quote |
reduce-position |
LP burned; X and Y returned | At-most/exact LP burn; at-least X and Y receives from vault |
swap-x-for-y |
X in, Y out | At-most X spend dx; at-least Y receive min-dy; bind token principals |
swap-y-for-x |
Y in, X out | At-most Y spend dy; at-least X receive min-dx; bind token principals |
swap-helper-a/b/c |
Multiple swaps in sequence | Final output minimum is not enough for user intent in all cases; attach postconditions for total initial input and final output, and prefer explicit single-hop minimums where available |
| Setter functions | No user token movement | No token postconditions needed; governance monitoring should track registry writes |
| Surface | Authority model | Notes |
|---|---|---|
| Pause switch | DAO or DAO extension | Starts paused; swaps/liquidity require unpaused |
| Pool scheduling/ratios/oracle/fees/thresholds | Pool owner or DAO/extension | Per-pool owner is powerful and can alter fee, oracle, threshold, and max ratio behavior |
| Pool creation | Any non-blocklisted caller | Registry determines uniqueness/validity |
| Add liquidity | Any caller while unpaused | See finding ALEX-M01: blocklist is not checked here |
| Reduce liquidity | Any non-blocklisted caller while unpaused | Uses caller LP balance |
| Swaps | Any non-blocklisted caller while unpaused and pool active | Optional min values default to zero |
| Vault / registry / LP token calls | as-contract |
Intended principal escalation to mutate protocol-owned registry/vault/LP state |
tx-sender: used for user identity, blocklist checks, pool-owner checks, and transfer sender. This is expected for direct user calls, but integrators should consider proxy/contract-caller behavior.contract-caller: used in DAO extension authorization.as-contract: used for registry, vault, and LP token operations. This is intentional but should remain tightly scoped.unwrap-panic: present inreduce-positionfor LP balance and inpow-down/pow-upfor fixed-point math. The LP balance case is user-facing and could be converted to a normal error.- Arithmetic: extensive fixed-point math uses guarded helpers, but some public swap paths compute price-ratio divisions before proving the output amount is non-zero.
- Trait conformance: token traits are SIP-010 typed via
.trait-sip-010.sip-010-trait.
| ID | Severity | Function | Line | Finding | Recommended fix |
|---|---|---|---|---|---|
| ALEX-M01 | Medium | add-to-position |
257-278 | Add-liquidity does not check is-blocklisted-or-default, while create-pool, reduce-position, and both direct swap functions do. A blocklisted address cannot create, reduce, or swap, but can still add liquidity to an existing pool when the protocol is unpaused. |
Add the same blocklist assert used in reduce-position and swaps before accepting liquidity. |
| ALEX-M02 | Medium | set-oracle-average, get-oracle-resilient |
227-231, 53-60 | set-oracle-average does not enforce new-oracle-average <= ONE_8, even though ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE exists. If a pool owner/DAO sets a larger value, get-oracle-resilient can underflow at (- ONE_8 oracle-average), disabling resilient oracle reads for that pool. |
Add asserts! (<= new-oracle-average ONE_8) ERR-ORACLE-AVERAGE-BIGGER-THAN-ONE in the setter. |
| ALEX-L01 | Low | swap-y-for-x |
341, 352 | If a very small positive dy-net-fees rounds to dx = 0, the price-ratio guard computes (div-down dy-net-fees dx). With dy-net-fees > 0 and dx = 0, this can panic/abort before returning a domain-specific liquidity error. |
Assert dx > u0 before the division and return ERR-INVALID-LIQUIDITY or ERR-EXCEEDS-MAX-SLIPPAGE. |
| ALEX-L02 | Low | swap-x-for-y |
312, 323-326 | The X->Y path can accept a positive dx with dy = 0 when the caller omits min-dy or passes zero. The function will transfer X, skip the zero Y transfer, update reserves, and return {dy: u0}. This is caller-protectable with min-dy, but the default behavior is sharp. |
Consider asserting dy > u0, or document that callers must always pass a non-zero minimum output. |
| ALEX-L03 | Low | swap-helper-a/b/c |
364-375 | Multi-hop public helpers pass none as the intermediate minimum and only apply the optional min to the final hop. A final min can still protect final output, but callers have no per-hop slippage controls through these helpers. |
Add helper variants that accept per-hop minimums, or document these helpers as convenience functions requiring strong transaction postconditions. |
| ALEX-I01 | Informational | reduce-position, pow-down, pow-up |
286, 496, 502 | unwrap-panic is used in user-reachable paths. The LP balance unwrap in reduce-position is the most visible case; if the LP token read unexpectedly errors, callers receive a panic rather than a protocol error. |
Replace user-facing unwrap-panic with unwrap! and a dedicated error code. |
Overall assessment: the contract has clear registry/vault separation and many explicit ratio/slippage checks, but the admin parameter setters should consistently enforce fixed-point bounds, and swap paths should guard zero-output/zero-denominator edge cases before price-ratio divisions.