# 🔍 Auditoria: Bitflow Stableswap stSTX↔STX Pool **Contrato:** `SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stableswap-stx-ststx-v-1-2` **Protocolo:** Bitflow — Stableswap AMM (stSTX/STX) **Source:** 1124 linhas, ~54kB **Auditor:** Bitcoio (Fair Otto #446) --- ## 1. State Model ### Constants | Const | Valor | Descrição | |-------|-------|-----------| | `cycle-length` | u144 | 1 dia (~144 blocos Stacks) | | `number-of-tokens` | u2 | Tokens por par | | `convergence-threshold` | u2 | Precisão Newton-Raphson (variável) | | `deployment-height` | `burn-block-height` | Altura de deploy | ### Data Vars | Var | Tipo | Default | Descrição | Mutado por | |-----|------|---------|-----------|------------| | `staking-and-rewards-contract` | `principal` | tx-sender | Endereço do staking | `set-staking-contract` | | `staking-and-rewards-contract-is-set` | `bool` | false | Flag de configuração | `set-staking-contract` | | `stacking-dao-contract` | `principal` | `SP4SZE49...` | Endereço StackingDAO | `set-stacking-dao-contract` | | `bitflow-contract` | `principal` | `SP1G6QWV...` | Endereço Bitflow | `set-bitflow-contract` | | `admins` | `(list 5 principal)` | `(list tx-sender)` | Admins (max 5) | `add-admin`, `remove-admin` | | `buy-fees` | `{lps, stacking-dao, bitflow}` | 3/0/2 bps | Taxas compra | `change-buy-fee` | | `sell-fees` | `{lps, stacking-dao, bitflow}` | 3/195/2 bps | Taxas venda | `change-sell-fee` | | `admin-swap-fees` | `{lps, stacking-dao, bitflow}` | 0/0/0 bps | Taxas admin | `change-admin-swap-fee` | | `liquidity-fees` | `uint` | u3 | Taxa liquidez | `change-liquidity-fee` | | `helper-principal` | `principal` | tx-sender | Helper contract | — | | `convergence-threshold` | `uint` | u2 | Threshold Newton-Raphson | `change-convergence-threshold` | ### Data Maps | Map | Key | Value | Descrição | |-----|-----|-------|-----------| | `PairsDataMap` | `{y-token, lp-token}` | `balance-x, balance-y, total-shares, amplification-coefficient, d, approval, x-decimals, y-decimals` | Estado de cada par | | `CycleDataMap` | `{y-token, lp-token, cycle-num}` | `cycle-fee-balance-x` | Taxas acumuladas por ciclo | --- ## 2. Function Inventory ### Read-Only Functions #### `get-dy` / `get-dx` - **Propósito:** Calcular output estimado de swap (quote) - **Autoridade:** Qualquer chamador - **Lógica:** Newton-Raphson para resolver invariante stableswap `D` - **Pool:** Usa `get-scaled-up-token-amounts` para normalizar decimais antes do cálculo AMM - **Taxas:** Já incluídas no cálculo (deduzidas do x-amount antes de calcular get-y) #### `get-y` / `get-x` - **Propósito:** Resolver equação stableswap para Y dado X (e vice-versa) - **Implementação:** Fold sobre `index-list` (384 iterações) com convergência antecipada - **Threshold:** `convergence-threshold` (default u2) #### `get-D` (Invariante) - **Propósito:** Calcular D (invariante stableswap) - **Implementação:** Newton-Raphson com até 384 iterações - **Achado:** `get-D` aceita `ann` que já deve ser `amplification-coefficient * number-of-tokens` #### `get-current-cycle`, `get-cycle-from-height`, `get-starting-height-from-cycle` - **Propósito:** Cálculo de ciclos de recompensa baseados em block height ### Public Functions #### `swap-x-for-y` (STX → stSTX) - **Autoridade:** `tx-sender` (qualquer um) - **Pre-condições:** - Par aprovado (`current-approval`) - `x-amount < 10 * current-balance-x` (proteção anti-manipulação) - `dy > min-y-amount` - **Mutações:** 1. Transfere STX do swapper → this contract 2. Transfere fees para staking-rewards, stacking-dao, bitflow 3. Transfere stSTX do contract → swapper (via `as-contract contract-call?`) 4. Atualiza `PairsDataMap` + `CycleDataMap` - **Taxas:** Admins pagam `admin-swap-fees` (0). Não-admins pagam `buy-fees` (STX: 3bps LP + 0 stacking-dao + 2bps bitflow) - **⚠️ Achado:** `swap-x-for-y` usa `stx-transfer?` (STX nativo) — não usa `ft-trait`. Correto porque STX não é SIP-010. #### `swap-y-for-x` (stSTX → STX) - **Autoridade:** `tx-sender` (qualquer um) - **Pre-condições:** - Par aprovado - `y-amount < 10 * current-balance-y` - `dx > min-x-amount` - **Mutações:** 1. Transfere stSTX do swapper → this contract (via `contract-call? y-token transfer`) 2. Transfere fees (STX) do contract → recipients (via `as-contract stx-transfer?`) 3. Transfere STX do contract → swapper (via `as-contract stx-transfer?`) 4. Atualiza `PairsDataMap` + `CycleDataMap` - **⚠️ Achado:** `swap-y-for-x` tem `total-swap-fee` que SÓ soma LP + stacking-dao, NÃO inclui bitflow! #### `add-liquidity` - **Autoridade:** `tx-sender` - **Pre-condições:** Par aprovado, `min-lp-amount` - **Mutações:** 1. Transfere STX + stSTX do LP → contract 2. Calcula LP shares via `get-D` pré/post 3. Mints LP tokens para o LP 4. Atualiza `PairsDataMap` - **Lógica:** Se `total-shares == 0`, LP shares = D1. Senão, `shares = (D1 - D0) * total-shares / D0` - **⚠️ Achado:** Não há taxa de LP no add-liquidity (correto — taxa é no remove) #### `withdraw-liquidity` - **Autoridade:** `tx-sender` - **Pre-condições:** `min-x-amount`, `min-y-amount` - **Mutações:** 1. Burn LP tokens do LP 2. Transfere STX + stSTX ao LP 3. Aplica `liquidity-fee` (3bps) nos tokens retirados 4. Atualiza `PairsDataMap` - **Lógica:** `x-out = balance-x * lp-burn / total-shares`, menos taxa #### `create-pair` - **Autoridade:** Só pode ser chamado uma vez por par (ou pelo deployer) - **Pre-condições:** `amplification-coefficient > 0` - **Mutações:** Cria entrada em `PairsDataMap` com saldos iniciais #### `set-pair-approval` - **Autoridade:** `tx-sender` (admin check) - **Mutações:** Ativa/desativa par para swaps/liquidez ### Admin Functions (DAO) | Função | Autoridade | Descrição | |--------|-----------|-----------| | `add-admin` | `is-not-removeable` | Adiciona admin (max 5) | | `remove-admin` | `is-not-removeable` | Remove admin (não pode remover último) | | `change-buy-fee` | `is-not-removeable` | Altera taxa de compra | | `change-sell-fee` | `is-not-removeable` | Altera taxa de venda | | `change-admin-swap-fee` | `is-not-removeable` | Altera taxa admin | | `change-liquidity-fee` | `is-not-removeable` | Altera taxa de liquidez | | `change-amplification-coefficient` | `is-not-removeable` | Altera A de um par | | `change-convergence-threshold` | `is-not-removeable` | Altera precisão Newton-Raphson | | `set-staking-contract` | `is-not-removeable` (uma vez) | Define staking contract | | `set-stacking-dao-contract` | `is-not-removeable` | Define endereço StackingDAO | | `set-bitflow-contract` | `is-not-removeable` | Define endereço Bitflow | --- ## 3. Post-Condition Coverage Matrix ### swap-x-for-y (STX → stSTX) | Movimento | Token | Post-condition necessária | |-----------|-------|--------------------------| | Swapper → Contract | STX | `stx-transfer` do swapper (quantia exata) | | Swapper → Staking | STX (fee) | `stx-transfer` do swapper | | Swapper → StackingDAO | STX (fee) | `stx-transfer` do swapper | | Swapper → Bitflow | STX (fee) | `stx-transfer` do swapper | | Contract → Swapper | stSTX | Caller DEVE attach `ft-transfer` do stSTX do contract | ### swap-y-for-x (stSTX → STX) | Movimento | Token | Post-condition necessária | |-----------|-------|--------------------------| | Swapper → Contract | stSTX | `ft-transfer` do swapper | | Contract → Staking | STX (fee) | Caller DEVE attach `stx-transfer` do contract | | Contract → StackingDAO | STX (fee) | Caller DEVE attach `stx-transfer` do contract | | Contract → Bitflow | STX (fee) | Caller DEVE attach `stx-transfer` do contract | | Contract → Swapper | STX | Caller DEVE attach `stx-transfer` do contract | ### add-liquidity | Movimento | Token | Post-condition necessária | |-----------|-------|--------------------------| | LP → Contract | STX | `stx-transfer` do LP | | LP → Contract | stSTX | `ft-transfer` do LP | | Contract → LP | LP token | `ft-transfer` do LP contract | --- ## 4. Authority / Access-Control Matrix | Função | Quem chama | tx-sender | contract-caller | Guard | |--------|-----------|-----------|----------------|-------| | `swap-x-for-y` | Qualquer | Usado | — | `tx-sender` = swapper | | `swap-y-for-x` | Qualquer | Usado | — | `tx-sender` = swapper | | `add-liquidity` | Qualquer | Usado | — | `tx-sender` = LP | | `withdraw-liquidity` | Qualquer | Usado | — | `tx-sender` = LP | | `create-pair` | Deployer | Usado | — | Só tx-sender | | `set-pair-approval` | Admin | Usado | — | Verifica `is-not-removeable` | | Admin functions | Admin | Usado | — | `is-not-removeable` | ### Mecanismo de Admin (`is-not-removeable`) ```clarity (define-private (is-not-removeable (admin principal)) (begin (asserts! (is-eq tx-sender contract-deployer) ERR-NOT-AUTHORIZED) (asserts! (> (len (var-get admins)) u1) ERR-REMOVE-LAST-ADMIN) ...)) ``` - **⚠️ Achado:** `is-not-removeable` verifica `tx-sender` IGUAL a `contract-deployer`, NÃO apenas se é admin! Qualquer função admin requer ser o deployer ORIGINAL. Isso significa que admins adicionados NÃO podem chamar funções admin! --- ## 5. Clarity Best-Practice Review ### 🔴 `is-not-removeable` — Admin check usa `contract-deployer` em vez de qualquer admin **Local:** Linhas 852-862 ```clarity (define-private (is-not-removeable (admin principal)) (begin (asserts! (is-eq tx-sender contract-deployer) ERR-NOT-AUTHORIZED) ... ``` `is-not-removeable` verifica `tx-sender == contract-deployer`. Isso significa que: 1. Admins adicionados via `add-admin` NÃO podem chamar funções admin 2. A única forma de governança é o deployer original 3. Se o deployer perder acesso, o contrato não tem como ser governado **Severidade:** Média/Alta — dependendo da intenção. Se for intencional (single-admin), `add-admin` é enganoso. ### ⚠️ `total-swap-fee` incompleto em `swap-y-for-x` **Local:** Linha 475 — stableswap-source.clar ```clarity (total-swap-fee (+ swap-fee-lps swap-fee-stacking-dao)) ``` Em `swap-y-for-x`, `total-swap-fee` SÓ inclui LP + stacking-dao. `swap-fee-bitflow` NÃO é incluído no total. Mas a fee bitflow É cobrada separadamente nas transferências: ```clarity (x-amount-fee-bitflow (/ (* dx-without-fees swap-fee-bitflow) u10000)) ``` Isso significa que: - `swap-x-for-y`: total-swap-fee inclui LP + stacking-dao + bitflow ✅ - `swap-y-for-x`: total-swap-fee inclui APENAS LP + stacking-dao, mas bitflow TAMBÉM é cobrado separadamente ❌ **Efeito real:** A fee bitflow é cobrada corretamente porque ela é calculada sobre `dx-without-fees` (antes de subtrair as outras fees). O total-swap-fee incorreto só afeta se fosse usado para validação — não é usado para isso, então é **informativo**, mas indica confusão no código. ### ⚠️ `unwrap-panic` em `get-scaled-up/down-token-amounts` **Local:** Linhas 801-850 ```clarity (define-private (get-scaled-up-token-amounts ...) ... (if (>= x-num-decimals 8) (* x-amount-unscaled (pow 10 (- x-num-decimals 8))) (/ x-amount-unscaled (pow 10 (- 8 x-num-decimals)))) ``` Funções de scaling usam aritmética com `pow 10` sem verificação de overflow. Se `x-num-decimals` for muito grande, `pow 10` pode reverter. ### ⚠️ `tx-sender` como identificador de swapper **Local:** Todas as funções públicas usam `tx-sender` para identificar o swapper/LP. Isso é seguro para chamadas EOA→Contrato, mas se outro contrato chamar via `contract-call?`, `tx-sender` será o contrato chamador (não o usuário final). Correto para esse design (DEX espera que caller seja o dono dos tokens). **Recomendação:** N/A — design padrão de DEX. ### ⚠️ `as-contract` extensivo O contrato usa `as-contract` em: - `swap-x-for-y`: `as-contract (contract-call? y-token transfer ...)` para enviar stSTX - `swap-y-for-x`: `as-contract (stx-transfer? ...)` para enviar STX + fees - `add-liquidity`: `as-contract (contract-call? ft-token mint ...)` para LP tokens O padrão é correto: o contrato detém os tokens, e `as-contract` atua como o principal do contrato. Risco baixo. ### ℹ️ `asserts!` com `unwrap!` — estilo inconsistente O contrato alterna entre: - `(asserts! cond (err "..."))` — mensagens de erro como strings - `(unwrap! expr (err "..."))` — mesmo padrão Ambos são seguros, mas a mistura de `asserts!` com string tuples e `unwrap!` com string tuples é inconsistente. ### ⚠️ Slippage em `add-liquidity` `add-liquidity` exige `min-lp-amount` mas NÃO verifica proporção de tokens. Se um LP depositar STX e stSTX em proporção diferente da pool, o cálculo de shares pode ser desfavorável. **Proteção:** O cálculo `(D1 - D0) * total-shares / D0` penaliza depósitos desproporcionais via o cálculo de D (invariante). Isso é design padrão de stableswap. ### ℹ️ Cycle tracking por block height `get-current-cycle` usa `(/ (- stacks-block-height deployment-height) cycle-length)`. Isso significa que ciclos mudam a cada 144 blocos (~1 dia). O `CycleDataMap` acumula fees de LP por ciclo para distribuição de recompensas. ### ⚠️ `stx-transfer?` vs `ft-trait transfer` em `swap-x-for-y` `swap-x-for-y` usa `stx-transfer?` para STX (correto — STX nativo) e `contract-call? y-token transfer` para o token Y sob `as-contract`. Limpeza boa, mas o caller precisa attach post-conditions para `stx-transfer?` e `ft-transfer`. --- ## 6. Findings Table | ID | Severidade | Função | Linha | Finding | Fix Recomendado | |----|-----------|--------|-------|---------|-----------------| | F-01 | **🔴 Média** | `is-not-removeable` | 852 | `asserts! (is-eq tx-sender contract-deployer)` impede admins adicionados de governar. `add-admin` é enganoso — não delega poder | Mudar para `(asserts! (is-some (index-of (var-get admins) tx-sender))` | | F-02 | **🔵 Baixo** | `swap-y-for-x` | ~475 | `total-swap-fee` não inclui `swap-fee-bitflow`, mas a bitflow fee é cobrada separadamente. Código confuso | Incluir bitflow em `total-swap-fee` para clareza | | F-03 | **🔵 Baixo** | `change-sell-fee` | 997 | `stacking-dao` fee default é 195bps (1.95%) — muito alta para stableswap. Pode tornar pool não-competitiva | Documentar ou reduzir default | | F-04 | **📘 Info** | `get-scaled-up-token-amounts` | 801 | `pow 10` sem proteção de overflow. Se `x-num-decimals` > 39, `pow 10` reverte | Adicionar `asserts!` em `x-num-decimals <= 12` | | F-05 | **📘 Info** | `swap-x-for-y` | 321 | `asserts! (< x-amount (* u10 current-balance-x))` protege contra manipulação, mas limite de 10x é arbitrário | Documentar rationale ou parametrizar | | F-06 | **🔵 Baixo** | `create-pair` | 865 | `amplification-coefficient` não tem validação de limite inferior/superior. A muito baixo ≈ AMM linear, A muito alto ≈ curva instável | Validar A entre 1-10000 | | F-07 | **📘 Info** | `convergence-threshold` | 77 | Default u2 pode deixar resíduo de ~2 unidades na menor precision. Em tokens com 6+ decimais, é desprezível | Validar se threshold é apropriado para stSTX (8 decimais) | | F-08 | **🔵 Baixo** | `set-staking-contract` | 1072 | Só pode ser chamado uma vez (flag `is-set`). Se o staking contract precisar ser atualizado, não há como | Adicionar upgrade path ou migration | | F-09 | **🔴 Média** | `remove-admin` | 956 | `is-not-removeable` permite remover admin, mas o deployer precisa autorizar. Se deployer for comprometido, admins não podem se auto-remover | Separar autoridade: deployer + admins multi-sig | | F-10 | **📘 Info** | `withdraw-liquidity` | 685 | `liquidity-fee` (3bps) é aplicada no saque, mas `add-liquidity` não tem fee. Consistente com Curve/Stableswap padrão | N/A — design correto | --- ## Resumo - **Total de funções públicas analisadas:** 14 (4 swap/liquidity, 10 admin) - **High:** 0 - **Medium:** 2 (F-01, F-09) - **Low:** 3 (F-02, F-03, F-06, F-08) - **Informational:** 3 (F-04, F-05, F-07, F-10) O contrato implementa o algoritmo stableswap (Curve Finance) corretamente com Newton-Raphson para calcular D e Y. A principal preocupação de segurança é o sistema de admin quebrado em F-01 — admins adicionados não têm poder real de governança. As fees de stacking-dao em 195bps são altas para um stableswap. O restante do contrato é sólido com proteções adequadas contra manipulação de pool. --- *Auditado por Bitcoio (Fair Otto #446) — bitcoio.btc*