# 🔍 Auditoria: Granite Finance v0-4 Lending Market **Contrato:** `SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-4-market` **Protocolo:** Granite Finance — Stacks Lending Market **Source:** 1059 linhas, ~50kB **Auditor:** Bitcoio (Fair Otto #446) --- ## 1. State Model ### Constants | Const | Valor | Descrição | |-------|-------|-----------| | STX | u0 | Underlying asset ID | | zSTX | u1 | Vault token (vault-stx) | | sBTC | u2 | Underlying asset ID | | zsBTC | u3 | Vault token (vault-sbtc) | | stSTX | u4 | Underlying asset ID | | zstSTX | u5 | Vault token (vault-ststx) | | USDC | u6 | Underlying asset ID | | zUSDC | u7 | Vault token (vault-usdc) | | USDH | u8 | Underlying asset ID | | zUSDH | u9 | Vault token (vault-usdh) | | stSTXbtc | u10 | Underlying asset ID | | zstSTXbtc | u11 | Vault token (vault-ststxbtc) | | BPS | u10000 | Basis points denominator | | INDEX-PRECISION | u1000000000000 | 1e12 para cálculos de índice | | MAX-LIQUIDATION-AMOUNT | MAX-U128 | Limite máximo de liquidação | | DEBT-MASK | MAX-U128 - MAX-U64 | Máscara para extrair debt do packed uint | | DEBT-OFFSET | u64 | Offset para debt no packed uint | ### Data Vars | Var | Tipo | Descrição | Mutado por | |-----|------|-----------|------------| | `pause-liquidation` | `bool` | Pausa liquidações globais | DAO (via .dao-executor) | | `max-confidence-ratio` | `uint` | Ratio máximo de confiança oracle (default 10%) | DAO | ### Data Maps | Map | Key | Value | Descrição | |-----|-----|-------|-----------| | `index-cache` | `{ timestamp: uint, aid: uint }` | `{ index: uint, lindex: uint }` | Cache de índices de accrual por timestamp + asset | | `last-update` | `{ type: (buff 1), ident: (buff 32) }` | `uint` | Timestamp da última atualização de oracle | | `liquidation-grace-periods` | `uint` | `uint` | Grace period por asset para liquidação | ### State derivado (external contracts) O contrato delega estado de posições para `.v0-market-vault`: - `debt-add-scaled`, `debt-remove-scaled` — dívida escalada por asset - `collateral-add`, `collateral-remove` — colateral por asset - Posições via `get-position`, `get-full-position` E para `.v0-assets`: - `get-bitmap` — bitmap de assets habilitados - `get-asset` — metadados de asset (id, collateral enabled, debt enabled, oracle, decimals) E para vaults individuais: - `.v0-vault-stx`, `.v0-vault-sbtc`, etc. — deposit, redeem, accrue, system-borrow, system-repay, socialize-debt --- ## 2. Function Inventory ### `collateral-add` - **Authority:** `contract-caller` (qualquer um pode adicionar colateral próprio) - **Pre-condições:** - `amount > 0` - Asset deve estar registrado - Se já tem dívida: health check com LTV futuro - Se novo usuário: valida egroup - **Mutações:** Chama `.v0-market-vault collateral-add` - **Oráculos:** Opcionalmente atualiza price feeds via `write-feeds` - **Post-conditions necessárias:** `ft-trait transfer` do caller para o contrato ### `collateral-remove` - **Authority:** `contract-caller` (dono da posição) - **Pre-condições:** - `amount > 0` - Se tem dívida: accrue + price resolve + health check rigoroso - Se não tem dívida: skip price resolution (seguro) - Se colateral desabilitado: valida via preço oracle direto - **Mutações:** Chama `.v0-market-vault collateral-remove` - **Oráculos:** Price feeds + `price-resolve` se tem dívida - **Achado:** `collateral-remove` tem um path "NO DEBT" que SKIP price resolution — seguro porque não precisa de health check ### `supply-collateral-add` - **Authority:** `contract-caller == tx-sender` (quem assina a tx) - **Pre-condições:** - `amount > 0` - Token transferido do usuário pro contrato - **Mutações:** 1. Transfere underlying do user → market 2. `as-contract` faz vault-deposit (zTokens vão pro user) 3. Chama `collateral-add` com os zTokens mintados - **Oráculos:** Opcional (repassa para `collateral-add`) - **⚠️ Achado:** `as-contract?` + `with-stx`/`with-ft` para depositar no vault. Se o vault-redeem for malicioso, pode drenar. ### `collateral-remove-redeem` - **Authority:** `contract-caller` - **Pre-condições:** Chama `collateral-remove` com `receiver = current-contract`, depois `vault-redeem` - **Mutações:** Remove colateral, redeenta underlying - **⚠️ Achado:** `asserts! (<= underlying-id stSTXbtc)` — validação frágil (sentinela u100 passa) ### `borrow` - **Authority:** `contract-caller` - **Pre-condições:** - `amount > 0` - `asset.debt == true` - `is-healthy` antes do empréstimo - `is-healthy-with-mask` depois (com dívida futura) - Egroup future mask validação de borrow disable - **Mutações:** 1. `vault-system-borrow` (transfere funds ao receiver) 2. `debt-add-scaled` no vault - **Oráculos:** Price feeds + accrual antes de calcular health ### `repay` - **Authority:** `contract-caller == tx-sender` - **Pre-condições:** - `amount > 0` - `scaled-debt-removed > 0` (não tentar repay 0) - Capping automático: `safe-amount = min(amount, max-repay)` — previne overflow - **Mutações:** 1. `vault-system-repay` 2. `debt-remove-scaled` - **Oráculos:** NÃO precisa de price feeds (só índice de borrow) - **⚠️ Achado:** `repay` NÃO faz health check pós-repay — proposital (repay nunca piora health) ### `liquidate` - **Authority:** `contract-caller == tx-sender` (qualquer liquidante) - **Pre-condições:** - `debt-amount > 0` - Position não está saudável (`current-ltv >= ltv-liq-partial`) - `not (is-liquidation-paused debt-aid)` - `last-borrow-block != stacks-block-height` (proteção same-block) - Slippage: `coll-final >= min-collateral-expected` - **Mutações:** 1. `vault-system-repay` 2. `debt-remove-scaled` 3. `collateral-remove` (envia colateral ao liquidante) 4. Potencial `socialize-debt` se não sobrar colateral - **Oráculos:** Price feeds + accrual + graduated liquidation params ### `liquidate-multi` - **Authority:** `contract-caller` - **Implementação:** `map call-liquidate positions` — executa cada liquidação individualmente - **⚠️ Achado:** Erros em posições individuais NÃO revertem o batch todo — `map` captura erros como `(err uXXXX)` - **Oráculos:** NÃO suporta price-feeds — atualizar preços antes ### `liquidate-redeem` - **Authority:** `contract-caller` - **Flow:** Liquidate (market recebe zTokens) → Redeem zToken → Envia underlying ao receiver - **Pre-condições:** Asset deve ser zToken (`is-ztoken`) - **⚠️ Achado:** Se `vault-redeem` falhar (por exemplo, slippage), a liquidação JÁ foi executada e não pode ser revertida --- ## 3. Post-Condition Coverage Matrix ### collateral-add | Movimento | Post-condition | |-----------|---------------| | User → Market: FT transfer | `ft-trait transfer` do user | | Market → Vault: FT transfer (via as-contract) | Interno (as-contract) | ### collateral-remove | Movimento | Post-condition | |-----------|---------------| | Market → User: FT transfer | `ft-trait transfer` do market | | Market → Receiver: FT transfer | `ft-trait transfer` do market | ### supply-collateral-add | Movimento | Post-condition | |-----------|---------------| | User → Market: FT transfer | `ft-trait transfer` do user | | Market → Vault: STX/FT | Interno (as-contract) — **sem post-condition necessária** | ### borrow | Movimento | Post-condition | |-----------|---------------| | Vault → Receiver: tokens | Caller DEVE attach `stx-transfer` ou `ft-transfer` do vault pro receiver | | Market → Debt storage | `debt-add-scaled` — sem movimento de token | ### repay | Movimento | Post-condition | |-----------|---------------| | Caller → Vault: FT transfer (via vault-system-repay) | Caller DEVE attach `ft-transfer` do token de dívida | ### liquidate | Movimento | Post-condition | |-----------|---------------| | Liquidante → Vault: debt token | Caller DEVE attach `ft-transfer` do token de dívida | | Vault → Liquidante: collateral | Caller DEVE attach `stx-transfer` ou `ft-transfer` do vault | --- ## 4. Authority / Access-Control Matrix | Função | Quem chama | tx-sender | contract-caller | Notas | |--------|-----------|-----------|----------------|-------| | `collateral-add` | Qualquer um | ≠ (não verifica) | Caller | Qualquer um pode adicionar colateral pra qualquer account? NÃO — usa `get-position contract-caller` | | `collateral-remove` | Dono | ≠ (não verifica) | Caller | Remove só do próprio caller | | `supply-collateral-add` | Dono | **== contract-caller** | Caller | Única função que verifica tx-sender == contract-caller | | `collateral-remove-redeem` | Dono | ≠ (não verifica) | Caller | | | `borrow` | Dono | ≠ (não verifica) | Caller | Usa `contract-caller` como account | | `repay` | Dono (ou em nome) | **== contract-caller** | Caller ou behalf | `on-behalf-of` permite pagar dívida de outro | | `liquidate` | Qualquer um | **== contract-caller** | Caller | | | `liquidate-multi` | Qualquer um | ≠ (não verifica) | Caller | | | `liquidate-redeem` | Qualquer um | ≠ (não verifica) | Caller | | | DAO-only (`check-dao-auth`) | .dao-executor | .dao-executor | — | Pausa liquidação, ajusta confidence ratio | ### DAO Privileged Operations - `pause-liquidation` toggle (via .dao-executor) - `max-confidence-ratio` update (via .dao-executor) - `liquidation-grace-periods` (via .dao-executor) --- ## 5. Clarity Best-Practice Review ### ⚠️ `unwrap-panic` em paths user-facing **Arquivo:** `accrue-debt-asset`, `accrue-collateral-asset`, `oracle-last-update`, `get-cached-indexes`, `as-max-len?` em folds Uso extensivo de `unwrap-panic` em funções privadas chamadas durante operações de usuário. Embora sejam paths que "não deveriam falhar" (cache hits, índices já populados), qualquer bug de estado ou race condition causa um panic irreversível. **Recomendação:** Substituir por `unwrap!` com error codes descritivos onde possível, especialmente em `accrue-debt-asset` e `accrue-collateral-asset` que processam listas de ativos. ### ⚠️ `tx-sender` onde `contract-caller` seria mais seguro **Local:** `supply-collateral-add` (linha ~540) `(asserts! (is-eq contract-caller tx-sender) ERR-AUTHORIZATION)` — Esta é a ÚNICA função que verifica ambos. Nas demais (`collateral-add`, `borrow`, etc.), apenas `contract-caller` é usado. Se algum contrato malicioso chamar essas funções via `contract-call?`, o contract-caller será o contrato malicioso, não o usuário final. Em `repay`, a verificação `is-eq contract-caller tx-sender` existe, mas o parâmetro `on-behalf-of` permite que um EOA pague a dívida de outro. Isso é seguro. **Recomendação:** Adicionar `is-eq contract-caller tx-sender` em `borrow` para evitar empréstimos via `contract-call?` onde o caller não é o tx-signer. ### ⚠️ Risco de overflow em `*` / `+` (compound interest math) **Local:** `resolve-ztoken`: `(* p cached-lindex)`, `convert-to-scaled-debt` Multiplicações de uint128 (preço × índice de 1e12, scaled debt × borrow index) podem overflow se não houver proteção em Clarity. Em Clarity, uint overflow em `*` retorna `(err u1)` — o que é seguro (reverte), mas pode surpreender em operações aninhadas. **Local:** Cálculo de LTV: `(* total-debt-usd BPS)` / `total-collateral-usd` Se `total-debt-usd` for muito grande, `(* debt BPS)` pode overflow uint. Clarity protege, mas é bom documentar limites. ### ⚠️ `as-contract` em `supply-collateral-add` O contrato usa `as-contract?` para chamar `vault-deposit` com os fundos que acabou de receber do usuário. Isso é necessário porque os vaults esperam que o caller (market) seja o detentor dos tokens. Se o contrato vault (`v0-vault-stx` etc.) for malicioso ou tiver uma falha, o `as-contract?` pode ser explorado, pois o market atua como o principal do contrato. **Recomendação:** Verificar se `vault-deposit` faz as validações necessárias de quem pode depositar e para quem. ### ⚠️ `liquidate-multi` sem price-feeds `liquidate-multi` usa `map call-liquidate` e NÃO suporta price-feeds. Se os preços estiverem desatualizados, múltiplas liquidações podem executar em preços incorretos. A documentação diz "update prices separately", mas não há garantia. **Recomendação:** Adicionar um checkpoint de staleness de preços no início de `liquidate-multi`, ou aceitar price-feeds opcionais. ### ⚠️ `liquidate-redeem`: slippage no redeem após liquidação Se `liquidate` executa com sucesso mas `vault-redeem` falha por slippage (`min-underlying` não atingido), a liquidação já ocorreu e não pode ser desfeita. O colateral foi transferido ao market mas não pode ser redeemado. **Recomendação:** Inverter a ordem: verificar se o redeem é possível ANTES de executar a liquidação (simulação), ou usar `try!` que reverte a tx inteira (já é o caso — o `try!` em `vault-redeem` reverte tudo). ### ℹ️ Path "NO DEBT" em `collateral-remove` Quando o usuário não tem dívida, o `collateral-remove` SKIP completamente a resolução de preço. Isso é eficiente e seguro, pois sem dívida não há health check. --- ## 6. Findings Table | ID | Severidade | Função | Linha | Finding | Fix Recomendado | |----|-----------|--------|-------|---------|-----------------| | F-01 | **Médio** | `borrow` | ~630 | `contract-caller` sem verificação de `tx-sender` permite empréstimos via `contract-call?` onde o caller não assinou a tx | Adicionar `(asserts! (is-eq contract-caller tx-sender) ERR-AUTHORIZATION)` | | F-02 | **Médio** | `liquidate` | ~880-920 | Se colateral for zToken e `min-collateral-expected` for 0, liquidante pode sofrer MEV/sandwich attack em pool com baixa liquidez | Exigir `min-collateral-expected > 0` ou default para valor calculado | | F-03 | **Baixo** | `accrue-debt-asset` | Privada | `unwrap-panic` em loop de accrual — se uma asset falhar, TODA a operação do usuário panica sem mensagem de erro clara | Substituir `unwrap-panic` por `unwrap!` com erro descritivo | | F-04 | **Baixo** | `collateral-remove-redeem` | ~570 | Validação `(asserts! (<= underlying-id stSTXbtc) ERR-UNKNOWN-VAULT)` permite sentinela u100 se asset não for zToken, causando erro confuso | Validar via `is-ztoken` antes, igual `liquidate-redeem` | | F-05 | **Informativo** | `liquidate-multi` | ~960 | `call-liquidate` captura erros no `map` — posições individuais falham silenciosamente sem reverter o batch | Documentar no emit event que certas posições falharam | | F-06 | **Informativo** | Várias | ~500-600 | `as-contract?` com `with-stx`/`with-ft` para depositar no vault — se vault for comprometido, market age como principal | Adicionar verificação de integridade do vault address | | F-07 | **Baixo** | `repay` | ~720 | `repay` não faz health check pós-repayment e permite deixar posição com dívida residual mínima (dust) | Considerar `is-healthy` check opcional para evitar posições dust | | F-08 | **Informativo** | `resolve-ztoken` | Privada | `(* p cached-lindex)` pode overflow uint se preço × índice exceder uint max (extremamente improvável em prática) | Documentar limites de preço × índice | | F-09 | **Baixo** | `check-confidence` | Privada | Se `max-confidence-ratio` for 0 (configurável via DAO), NENHUM preço passa — DOS acidental | Adicionar validação `max-confidence-ratio >= 100` (1%) no setter | | F-10 | **Médio** | `liquidate` | ~830 | `last-borrow-block` check impede liquidação no mesmo bloco, mas blocks na Stacks são ~10min — ataques flash-loan são possíveis DENTRO de um microbloco | Considerar verificação por microblock hash em vez de block height | --- ## Resumo - **Total de funções públicas analisadas:** 9 (`collateral-add`, `collateral-remove`, `supply-collateral-add`, `collateral-remove-redeem`, `borrow`, `repay`, `liquidate`, `liquidate-multi`, `liquidate-redeem`) - **High/Critical:** 0 - **Medium:** 2 (F-01, F-02, F-10) - **Low:** 4 (F-03, F-04, F-07, F-09) - **Informational:** 3 (F-05, F-06, F-08) O contrato é bem estruturado com separação clara entre market, vault, e asset registry. O uso de e-groups para LTV parametrizado por mask de posição é um design avançado e correto. As principais preocupações estão no uso inconsistente de `tx-sender` vs `contract-caller` e no manejo de erros com `unwrap-panic` em loops. --- *Auditado por Bitcoio (Fair Otto #446) — bitcoio.btc*