Auditoria de Segurança em DApps: Checklist Prático
O que muda numa auditoria de DApp em relação a um code review comum
Um smart contract é um programa que roda na Ethereum Virtual Machine (EVM) ou em uma VM compatível, e que, uma vez implantado, tem seu código imutável na blockchain. Essa imutabilidade é a diferença central entre revisar um contrato e revisar uma API REST qualquer.
Num backend tradicional, um bug vira um hotfix e um deploy de correção. Num contrato já publicado, o bug pode ser permanente — e se ele expõe fundos, a perda é irreversível. Não existe rollback de transação confirmada na blockchain.
Cada linha de código auditada também tem um custo de execução medido em gas fee. Um loop mal projetado ou uma leitura de storage desnecessária não é só "código feio": é dinheiro real gasto pelo usuário a cada chamada, e pode até inviabilizar a função em situações de rede congestionada.
Por isso, auditoria de segurança em DApp não é sinônimo de revisão funcional de código. Revisão funcional pergunta "isso faz o que deveria fazer?". Auditoria de segurança pergunta "existe algum caminho de execução, incluindo os que o time não imaginou, que permite drenar fundos, travar o contrato ou burlar controle de acesso?". Se você ainda não domina os conceitos básicos de contratos, vale revisar o Guia Definitivo de Smart Contracts para Devs antes de seguir com o checklist.
Reentrancy e falhas de controle de fluxo
Reentrancy acontece quando um contrato externo é chamado antes do estado interno ser atualizado, e esse contrato externo chama de volta a função original, repetindo uma operação — geralmente um saque — antes que o saldo seja zerado.
O padrão de defesa é checks-effects-interactions: primeiro valide as condições, depois atualize o estado interno, só então faça a chamada externa.
// Vulnerável
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount, "saldo insuficiente");
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "falha no envio");
balances[msg.sender] -= amount; // estado atualizado depois da chamada externa
}
// Corrigido
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount, "saldo insuficiente");
balances[msg.sender] -= amount; // estado atualizado antes da chamada externa
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "falha no envio");
}
O caso histórico mais citado quando se fala em reentrancy é o hack da The DAO, em 2016, que expôs justamente esse tipo de falha em um contrato de governança na Ethereum e motivou boa parte das boas práticas que hoje são padrão de mercado.
Na prática, a maioria dos projetos evita reescrever essa lógica do zero e usa o modificador nonReentrant da biblioteca OpenZeppelin, que trava a reentrada por meio de uma variável de estado de mutex.
Controle de acesso e gerenciamento de permissões
Revise toda função que altera estado sensível — mint, burn, pause, alteração de parâmetros de taxa, transferência de fundos do contrato. Cada uma precisa de um controle de acesso explícito: onlyOwner, um sistema de roles (AccessControl da OpenZeppelin) ou aprovação multisig.
Contratos upgradeable baseados em padrões de proxy adicionam uma camada extra de risco: quem controla a chave de upgrade pode, em tese, trocar toda a lógica do contrato mantendo o mesmo endereço e o mesmo storage. Isso significa que a segurança do contrato depende tanto do código quanto de quem guarda essa chave.
Para mitigar isso, mudanças administrativas críticas — trocar o admin, atualizar a implementação de um proxy, alterar parâmetros econômicos — devem passar por um timelock, um contrato que impõe um atraso obrigatório entre a proposta e a execução, dando à comunidade ou ao time de segurança uma janela para reagir.
Checklist mínimo de controle de acesso:
- Toda função de mint/burn tem restrição de acesso?
- A chave de owner é uma EOA única ou uma multisig?
- Existe timelock para upgrades de proxy e mudanças de parâmetro?
- Funções de emergência (pause/unpause) estão documentadas e testadas?
Dependência de oráculos e manipulação de preço
Um oráculo (oracle) é o mecanismo que traz dado externo — normalmente preço de ativo — para dentro da blockchain, que por natureza não tem acesso a informação fora de si mesma.
Em protocolos DeFi, o risco mais comum é o de manipulação de preço via oráculo mal projetado. Se o contrato lê o preço diretamente de um único pool de liquidez on-chain, um atacante pode distorcer temporariamente esse preço, executar a operação que lhe interessa, e reverter a posição — tudo dentro de uma única transação, frequentemente financiada por um flash loan, empréstimo sem colateral que precisa ser quitado no mesmo bloco.
Boas práticas de mitigação:
- Preferir oráculos descentralizados com múltiplas fontes de dado agregadas, em vez de ler o preço de um único pool.
- Verificar staleness: o contrato consumidor deve rejeitar preços desatualizados além de um limite de tempo.
- Validar desvio brusco de preço entre leituras consecutivas antes de usar o valor em uma operação financeira crítica.
Gas, negação de serviço e limites de bloco
Loops sem limite superior são um dos vetores de negação de serviço mais comuns em contratos. Se uma função itera sobre um array que cresce indefinidamente — uma lista de todos os investidores, por exemplo — chega um ponto em que a execução ultrapassa o gas limit do bloco e a função fica permanentemente inexecutável.
Outro vetor é o gas griefing: uma função que depende de uma chamada externa para terceiros (um call para um endereço arbitrário) pode ser travada de propósito por um contrato malicioso que consome todo o gas disponível e força a reversão.
A defesa estrutural para pagamentos é o padrão pull sobre push: em vez do contrato enviar fundos ativamente para uma lista de destinatários, cada destinatário deve chamar uma função de saque para retirar o próprio saldo. Isso isola a falha de um destinatário problemático do restante do sistema.
Antes do deploy, vale simular cenários de alta carga — muitos usuários interagindo simultaneamente, arrays no tamanho máximo esperado — para verificar se o custo de gas se mantém previsível.
Ferramentas de análise estática e dinâmica
Nenhuma ferramenta automatizada substitui leitura humana de código, mas todas aceleram a detecção de padrões conhecidos de vulnerabilidade.
| Ferramenta | Tipo de análise | Bom para | Limitação |
|---|---|---|---|
| Slither | Análise estática | Detectar padrões conhecidos (reentrancy, uso incorreto de tx.origin, etc.) rapidamente |
Gera falsos positivos, não entende lógica de negócio |
| MythX | Análise estática + simbólica | Integração em pipeline com relatório mais detalhado | Depende de serviço externo, tem custo |
| Echidna | Fuzzing baseado em propriedades | Encontrar invariantes quebradas com inputs aleatórios | Exige escrever propriedades específicas do contrato |
| Foundry (forge fuzz) | Testes de fuzzing integrados ao framework | Fuzzing rápido no mesmo fluxo de teste unitário | Cobertura depende da qualidade dos testes escritos |
A diferença essencial entre análise estática e fuzzing: a estática lê o código-fonte e compara com padrões conhecidos, sem executar nada; o fuzzing efetivamente executa o contrato com uma quantidade grande de entradas aleatórias ou guiadas, tentando quebrar invariantes definidas pelo time.
O limite comum a todas essas ferramentas é que nenhuma entende a intenção de negócio do contrato. Um scanner não sabe se a regra "só o dono pode resgatar após 30 dias" está correta — ele só sabe se existe ou não uma verificação de acesso ali.
Integrar Slither e fuzzing (Foundry ou Echidna) no pipeline de CI, rodando a cada pull request, é prática recomendada e barata de manter — mas é a etapa inicial da auditoria, não a etapa final.
Checklist prático pré-deploy
Antes de subir um contrato para mainnet, confira:
- Todas as funções que alteram estado sensível têm controle de acesso explícito
- Padrão checks-effects-interactions aplicado em toda função com chamada externa
- Nenhum loop sem limite superior sobre estrutura que cresce com o uso
- Pagamentos seguem padrão pull, não push
- Oráculos de preço validam staleness e desvio
- Slither e fuzzing (Foundry/Echidna) rodando no CI sem findings críticos abertos
- Testes em testnet pública cobrindo os fluxos principais
- Simulação em mainnet fork reproduzindo o estado real antes do deploy definitivo
- Plano de monitoramento pós-deploy (alertas de saldo, eventos anômalos)
- Avaliação de programa de bug bounty como camada adicional de defesa contínua
Testnet e mainnet fork resolvem problemas diferentes: a testnet valida comportamento funcional num ambiente aberto; o fork de mainnet permite testar a interação do contrato com protocolos reais já implantados (DEXs, oráculos, outros contratos) sem risco financeiro. Para aprofundar a arquitetura por trás desses contratos, veja o Guia Definitivo de Smart Contracts para Devs.
Auditoria interna vs auditoria externa contratada
Uma revisão interna, com pares do próprio time rodando o checklist acima e as ferramentas automatizadas, costuma ser suficiente para contratos simples, com escopo pequeno e sem grande valor financeiro em risco.
Os sinais de que vale contratar uma auditoria externa especializada aparecem quando o contrato lida com valor de terceiros de forma direta, quando há lógica financeira complexa (staking, empréstimo, liquidação), ou quando o projeto pretende operar publicamente e precisa de credibilidade perante usuários e investidores.
O custo-benefício de uma auditoria externa deve ser pensado em relação ao valor total travado (TVL) esperado no contrato: quanto maior o TVL projetado, maior a perda potencial de um bug não detectado, e menor, proporcionalmente, o custo da auditoria frente ao risco.
Se o seu projeto Web3 está próximo do deploy em mainnet e precisa de uma revisão de segurança estruturada, a Hack Tech Farm apoia times técnicos nessa etapa — conheça nossos produtos e serviços.
Perguntas Frequentes
O que é reentrancy e como evitar esse tipo de ataque?
Reentrancy é quando um contrato externo chamado antes da atualização de estado consegue chamar de volta a função original e repetir uma operação, como um saque. Evita-se aplicando o padrão checks-effects-interactions e usando o modificador nonReentrant da OpenZeppelin.
Ferramentas automatizadas de análise substituem uma auditoria manual?
Não. Slither, MythX, Echidna e Foundry detectam padrões conhecidos e quebram invariantes com fuzzing, mas nenhuma entende a intenção de negócio do contrato. Elas aceleram a etapa inicial da auditoria, não substituem a leitura humana crítica do código.
Quanto tempo leva uma auditoria completa de smart contract?
O prazo varia conforme a complexidade do contrato, a quantidade de integrações externas (oráculos, outros protocolos) e o escopo definido com a equipe de auditoria. Contratos simples levam menos tempo que sistemas com lógica financeira complexa e múltiplos módulos.
Preciso auditar um contrato que já foi auditado por outra empresa?
Sim, se houve qualquer mudança de código após a auditoria anterior — inclusão de função, upgrade de proxy, alteração de parâmetro. Uma auditoria cobre exatamente o código analisado naquele momento, não versões futuras do contrato.
Auditoria de segurança garante que o contrato está livre de bugs?
Não. Auditoria reduz a probabilidade de vulnerabilidades conhecidas e comuns passarem despercebidas, mas não é uma prova formal de ausência de bugs. Por isso, monitoramento pós-deploy e programas de bug bounty seguem sendo recomendados mesmo após uma auditoria completa.