Pular para o conteúdo principal
Change page

Testes de contratos inteligentes

Última edição: @MCreimer(opens in a new tab), 19 de janeiro de 2024

Blockchains públicas como Ethereum são imutáveis, dificultando alterações de código de contratos inteligentes após sua implementação. Existem padrões de atualização de contrato para realizar "atualizações virtuais", mas são difíceis de implementar e requer um consenso social. Além disso, uma atualização só pode corrigir um erro após que é descoberto se um invasor descobrir a vulnerabilidade primeiro, seu contrato inteligente corre o risco sofrer um exploit.

Por estas razões, testar contratos inteligentes antes de implementar à rede principal é o requisito mínimo para segurança. Existem muitas técnicas para testar contratos e avaliar a corretude de código; qual escolher depende de suas necessidades. No entanto, um conjunto de testes feito a partir de diferentes ferramentas e abordagens é ideal para pegar pequenas e grandes falhas de segurança no código do contrato.

Pré-requisitos

Esta página explica como testar contratos inteligentes antes de implantar na rede Ethereum. Pressupõe-se que você esteja familiarizado com contratos inteligentes.

O que é teste de contrato inteligente?

O teste de contrato inteligente é o processo de verificação de que o código de um contrato inteligente funciona conforme o esperado. Testar é útil para verificar se um contrato inteligente específico atende aos requisitos de confiabilidade, usabilidade e segurança.

Embora as abordagens variem, a maioria dos métodos de teste exige a execução de um contrato inteligente com uma pequena amostra dos dados que se espera manipular. Se o contrato produzir resultados corretos para dados da amostra, presume-se que esteja funcionando corretamente. A maioria das ferramentas de teste fornece recursos para escrever e executar casos de teste(opens in a new tab) para verificar se a execução de um contrato corresponde aos resultados esperados.

Por que é importante testar contratos inteligentes?

Como os contratos inteligentes geralmente gerenciam ativos financeiros de alto valor, pequenos erros de programação podem e geralmente levam a perdas massivas para os usuários(opens in a new tab). Rigorous testing can, however, help you discover defects and issues in a smart contract's code early and fix them before launching on Mainnet.

Embora seja possível atualizar um contrato se um bug for descoberto, as atualizações são complexas e podem resultar em erros(opens in a new tab) se tratadas de forma inadequada. A atualização de um contrato vai contra o princípio da imutabilidade e sobrecarrega os usuários com suposições de confiança adicionais. Por outro lado, um plano abrangente para testar seu contrato reduz os riscos de segurança do contrato inteligente e reduz a necessidade de realizar atualizações lógicas complexas após a implantação.

Métodos para testar contratos inteligentes

Methods for testing Ethereum smart contracts fall under two broad categories: automated testing and manual testing. Testes automatizados e testes manuais tem seus prós e contras, mas você pode combinar ambos para criar um plano robusto para analisar seus contratos.

Teste automatizado

O teste automatizado usa ferramentas que verificam automaticamente um código de contratos inteligentes em busca de erros na execução. O benefício do teste automatizado vem do uso de scripts(opens in a new tab) para orientar a avaliação das funcionalidades do contrato. Os scripts de testes podem ser programados para serem executados repetidamente com o mínimo de intervenção humana, tornando o teste automatizado mais eficiente do que as abordagens manuais de teste.

O teste automatizado é particularmente útil quando os testes são repetitivos e demorados; difícil de realizar manualmente; suscetíveis a erro humano; ou envolvem a avaliação de funções contratuais críticas. Mas as ferramentas de teste automatizadas podem ter desvantagens - elas podem perder certos bugs e produzir muitos falsos positivos(opens in a new tab). Portanto, combinar testes automatizados com testes manuais para contratos inteligentes é ideal.

Teste manual

O teste manual é auxiliado por humanos e envolve a execução de cada caso de teste em seu conjunto de testes, um após o outro, ao analisar a corretude de um contrato inteligente. Isso é diferente do teste automatizado, no qual você pode executar simultaneamente vários testes isolados em um contrato e obter um relatório mostrando todos os testes que falharam e os que foram aprovados.

O teste manual pode ser realizado por um único indivíduo seguindo um plano de teste escrito que cobre diferentes cenários de teste. Você também pode ter vários indivíduos ou grupos interagindo com um contrato inteligente durante um período especificado como parte do teste manual. Os testadores compararão o comportamento real do contrato com o comportamento esperado, sinalizando qualquer diferença como um bug.

Um teste manual eficaz requer recursos consideráveis ​​(habilidade, tempo, dinheiro e esforço) e é possível, devido a erro humano, perder certos erros durante a execução dos testes. Mas o teste manual também pode ser benéfico - por exemplo, um testador humano (por exemplo, um auditor) pode usar a intuição para detectar casos extremos que uma ferramenta de teste automatizada perderia.

Teste automatizado para contratos inteligentes

Teste unitário

O teste unitário avalia as funções do contrato separadamente e verifica se cada componente funciona corretamente. Testes unitários bons devem ser simples, rápidos de executar e fornecer uma ideia clara do que deu errado se os testes falharem.

Os testes unitários são úteis para verificar se as funções retornam os valores esperados e se o armazenamento do contrato é atualizado corretamente após a execução da função. Além disso, a execução de testes unitários após fazer alterações em uma base de código de contratos garante que a adição de nova lógica não introduza erros. Abaixo estão algumas diretrizes para executar testes unitários eficazes:

Diretrizes para testes unitários de contratos inteligentes

1. Entenda a lógica de negócios e o fluxo de trabalho de seus contratos

Antes de escrever testes unitários, é bom saber quais funcionalidades um contrato inteligente oferece e como os usuários acessarão e usarão essas funções. Isso é particularmente útil para executar testes de caminho feliz(opens in a new tab) que determinam se as funções em um contrato retornam a saída correta para entradas válidas do usuário. Explicaremos esse conceito usando este exemplo (resumido) de um contrato de leilão(opens in a new tab)

1constructor(
2 uint biddingTime,
3 address payable beneficiaryAddress
4 ) {
5 beneficiary = beneficiaryAddress;
6 auctionEndTime = block.timestamp + biddingTime;
7 }
8
9function bid() external payable {
10
11 if (block.timestamp > auctionEndTime)
12 revert AuctionAlreadyEnded();
13
14 if (msg.value <= highestBid)
15 revert BidNotHighEnough(highestBid);
16
17 if (highestBid != 0) {
18 pendingReturns[highestBidder] += highestBid;
19 }
20 highestBidder = msg.sender;
21 highestBid = msg.value;
22 emit HighestBidIncreased(msg.sender, msg.value);
23 }
24
25 function withdraw() external returns (bool) {
26 uint amount = pendingReturns[msg.sender];
27 if (amount > 0) {
28 pendingReturns[msg.sender] = 0;
29
30 if (!payable(msg.sender).send(amount)) {
31 pendingReturns[msg.sender] = amount;
32 return false;
33 }
34 }
35 return true;
36 }
37
38function auctionEnd() external {
39 if (block.timestamp < auctionEndTime)
40 revert AuctionNotYetEnded();
41 if (ended)
42 revert AuctionEndAlreadyCalled();
43
44 ended = true;
45 emit AuctionEnded(highestBidder, highestBid);
46
47 beneficiary.transfer(highestBid);
48 }
49}
Exibir tudo

Este é um contrato de leilão simples projetado para receber lances durante o período de submissão de ofertas. Se a variável highestBid aumentar, o licitante anterior mais alto receberá seu dinheiro; uma vez terminado o período de licitação, o objeto beneficiary aciona o contrato para obter seu dinheiro.

Testes unitários para um contrato como este cobriria diferentes funções que um usuário poderia chamar quando interagindo com o contrato. Um exemplo seria um teste unitário que checa se o usuário pode colocar uma ordem enquanto o leilão está em andamento (ou seja, chamadas para bid() com sucesso) ou checar se um usuário pode colocar uma ordem mais alta que o atual highestBid.

Entendendo o fluxo operacional do contrato também ajuda a escrever testes unitários que checam se a execução atende os requisitos. Por exemplo, o contrato de leilão especifica que os usuários não podem colocar ordens quando o leilão terminou (ou seja, quando auctionEndTime é menor que block.timestamp). Portanto, o desenvolvedor deve rodar um teste unitário que checa se chamadas para a função bid() tiveram sucesso ou falharam quando o leilão terminou (ou seja, quando auctionEndTime > block.timestamp).

2. Avalie todas as suposições relacionadas à execução do contrato

É importante documentar quaisquer suposições sobre a execução do contrato e escrever testes unitários para verificar a validade destas suposições. Além de oferecer proteção contra execução inesperada, testar afirmações força você a pensar sobre operações que poderiam quebrar o modelo de segurança do contrato inteligente. Uma dica útil é ir além dos "testes do usuário feliz" e escrever testes negativos que checam se a função falha com as entradas erradas.

Muitos frameworks de teste unitário permitem você criar afirmações - simples declarações que declaram o que um contrato pode e não pode fazer - e rodar testes para ver se estas afirmações funcionam durante a execução. Um desenvolvedor trabalhando no contrato de leilão descrito anteriormente poderia fazer as seguintes afirmações sobre o seu comportamento antes de rodar testes negativos:

  • Usuários não podem colocar ordens quando o leilão acabou ou não começou.

  • O contrato de leilão reverte se a ordem é abaixo do limite aceitável.

  • Usuários que falham em vencer o leilão são creditados com seus fundos

Nota: Uma outra maneira de testar suposições é escrever testes que disparam modificadores de função(opens in a new tab) em um contrato, especialmente declaraçõesrequire, assert, e if…else.

3. Medida de cobertura do código

Cobertura de código(opens in a new tab) é uma métrica de teste que rastreia o número de ramificações, linhas e comandos no seu código executados durante os testes. Testes devem ter boa cobertura de código, caso contrário você pode ter "falsos negativos" que acontecem quando um contrato passa todos os testes, mas vulnerabilidades ainda existem no código. Obtendo alta cobertura de código, entretanto, dá a segurança que todos os comandos/funções em um contrato inteligente foram suficientemente testados por exatidão.

4. Use frameworks de teste bem desenvolvidos

A qualidade das ferramentas usada para rodar testes unitários para o seu contrato inteligente é crucial. Um framework de teste ideal é aquele que é regularmente mantido; fornece recursos úteis (por exemplo, capacidades de log e relatórios); e tem de ter sido extensivamente usado por outros desenvolvedores.

Frameworks de teste unitário para contratos inteligentes em Solidity vêm em diferentes linguagens (geralmente JavaScript, Python e Rust). Veja alguns dos guias abaixo para informações sobre como começar a rodar testes unitários com diferentes frameworks de teste:

Teste de Integração

Enquanto o teste unitário depura funções de contrato isoladamente, testes integrados avaliam os componentes de um contrato inteligente como um todo. Teste de integração pode detectar defeitos vindos de chamadas entre contratos ou interações entre diferentes funções no mesmo contrato inteligente. Por exemplo, testes de integração podem ajudar a checar se coisas como herança(opens in a new tab) e injeção de dependência funcionam devidamente.

Teste de integração é útil se o seu contrato adota uma arquitetura modular ou interfaces com outros contratos on-chain durante a execução. One way of running integration tests is to

at a specific height (using a tool like Forge(opens in a new tab) or Hardhat(opens in a new tab) and simulate interactions between your contract and deployed contracts.

O blockchain que sofreu fork irá se comportar similarmente à Mainnet e ter contas com estados e saldos associados. Mas ele age somente como um ambiente de área local de desenvolvimento restrita, significando que você não precisará de ETH real para transações, por exemplo, nem suas modificações irão afetar o protocolo Ethereum real.

Teste baseado em propriedade

Teste baseado em propriedade é o processo de checar que um contrato inteligente satisfaz algumas propriedades definidas. Propriedades afirmam fatos sobre o comportamento de um contrato esperado continuar verdadeiro em diferentes cenários - um exemplo de propriedade de contrato inteligente poderia ser "Operações aritméticas no contrato nunca sofrem overflow ou underflow."

Análise estática e análise dinâmica são duas técnicas comuns de execução de teste baseado em propriedade, e ambas podem verificar que o código para um programa (um contrato inteligente no caso) satisfaz algumas propriedades pré-definidas. Algumas ferramentas de teste baseadas em propriedade vem com regras pré-definidas sobre propriedades esperadas de contratos e checam o código contra estas regras, enquanto outras permitem você criar propriedades customizadas para um contrato inteligente.

Análise estática

Um analisador estático pega como entrada o código-fonte de um contrato inteligente e retorna resultados declarando se o contrato satisfaz a propriedade ou não. Diferente da análise dinâmica, análise estática não envolve executar um contrato para analisá-lo por exatidão. Análise estática gera razões alternativas sobre todos os caminhos possíveis que um contrato inteligente poderia tomar durante a execução (ou seja, examinando a estrutura do código-fonte para determinar o que significaria para a operação do contrato em tempo de execução).

Testes Linting(opens in a new tab) e estático(opens in a new tab) são métodos comuns de rodar análise estática em contratos. Ambos requerem analisar representações de baixo nível da execução de um contrato, como árvores de sintaxe abstrata(opens in a new tab) e gráficos de controle de fluxo(opens in a new tab) retornados pelo compilador.

Na maioria dos casos, análise estática é útil para detectar problemas de segurança como uso de construtores inseguros, erros de sintaxe, ou violações de padrões de código no código de contratos. Entretanto, analisadores estáticos são conhecidos por geralmente serem instáveis em detectar vulnerabilidades mais profundas, e podem produzir excessivos falsos positivos.

Análise dinâmica

Análise dinâmica gera entradas simbólicas (por exemplo, em execução simbólica(opens in a new tab)) ou entradas concretas (por exemplo, em fuzzing(opens in a new tab)) para funções de contratos inteligentes para ver se qualquer trace de execução violou propriedades específicas. Esta forma de teste baseado em propriedades difere dos testes unitários no tocante a casos de teste cobrem múltiplos cenários e um programa manipula a geração de casos de teste.

Fuzzing(opens in a new tab) é um exemplo de análise técnica dinâmica para verificar propriedades arbitrárias em contratos inteligentes. Um fuzzer invoca funções em um contrato alvo com variações randômicas ou mal formadas de um valor de entrada definido. Se um contrato inteligente entra em estado de erro (por exemplo, uma afirmação 'where' falha), o problema é indicado e as entradas que geraram esta execução para o caminho da vulnerabilidade são produzidas em um relatório.

Fuzzing é útil para avaliação de um mecanismo de validação de entrada de contratos inteligentes, já que manipulação imprópria de entradas inesperadas pode resultar em execução não pretendida e produzir efeitos perigosos. Esta forma de teste baseado em propriedade pode ser ideal por muitas razões:

  1. Escrever casos de teste para cobrir muitos cenários é difícil. Um teste de propriedade somente requer que você defina o comportamento e faixa de dados com a qual testar o comportamento - o programa automaticamente gera casos de teste baseados na propriedade definida.

  2. Sua suíte de testes pode não cobrir suficientemente todos os caminhos possíveis dentro do programa. Até com 100% de cobertura, é possível perder alguns casos limítrofes.

  3. Testes unitários provam que um contrato executa corretamente para dados de amostra, mas se o contrato executa corretamente para entradas fora das amostras, permanece desconhecido. Testes de propriedade executam um contrato alvo com múltiplas variações de uma dado valor de entrada para encontrar traços de execução que causaram falhas de afirmação. Por isso, um teste proprietário fornece mais garantias que um contrato execute corretamente para uma larga classe de dados de entrada.

Orientações para rodar teste baseado em propriedade para contratos inteligentes

Executar testes baseados em propriedade geralmente começa com a definição da propriedade (por exemplo, ausência de overflows de inteiro(opens in a new tab)) ou com coleções de propriedades que você quer verificar em um contrato inteligente. Você pode também precisar definir uma faixa de valores dentro da qual o programa pode gerar dados para entradas de transação quando escrevendo os testes de propriedade.

Uma vez configurado propriamente, a ferramenta de teste de propriedade irá executar as suas funções do contrato inteligente com entradas aleatoriamente geradas. Se houver quaisquer violações de afirmações, você deve receber um relatório com os dados de entrada concretos que violaram a propriedade sendo avaliada. Veja alguns dos guias abaixo para começar com testes baseados em propriedade com diferentes ferramentas:

Testes manuais para contratos inteligentes

Teste manual de contratos inteligentes frequentemente vêm mais tarde no ciclo de desenvolvimento, após rodar testes automatizados. Essa forma de teste avalia o contrato inteligente como um produto totalmente integrado para ver se ele executa conforme especificado nos requisitos técnicos.

Testando contratos no blockchain local

Enquanto testes automatizados realizados em um ambiente local de desenvolvimento podem fornecer informações úteis de depuração, você irá querer saber como seus contrato inteligente se comporta em um ambiente de produção. Entretanto, implantar na cadeia principal do Ethereum incorre em taxas de gas - sem mencionar que você ou seus usuários podem perder dinheiro real se o seu contrato inteligente ainda tem falhas.

Testar seu contrato em um blockchain local (também conhecido como uma rede de desenvolvimento) é uma alternativa recomendada em relação a testar na Mainnet. Um blockchain local é uma cópia do blockchain Ethereum rodando localmente no seu computador que simula o comportamento da camada de execução do Ethereum. Como tal, você pode programar transações para interagir com um contrato sem incorrer em custo significante.

Rodar contratos em blockchain local pode ser útil como forma de teste de integração manual. Contratos inteligentes são altamente combináveis, permitindo você integrar com protocolos existentes - mais você ainda precisará garantir que interações on-chain assim tão complexas produzam os resultados corretos.

Mais sobre redes de desenvolvimento.

Testando contratos nas redes de teste

Uma rede de teste ou testnet funciona exatamente como o Ethereum Mainnet, exceto que ela usa Ether (ETH) sem valor no mundo real. Implantar seu contrato em uma testnet significa que qualquer um pode interagir com ele (por exemplo, via o front-end do dapp) sem colocar fundos em risco.

Esta forma de teste manual é útil para avaliação do fluxo fim-a-fim da sua aplicação do ponto de vista do usuário. Aqui, testadores beta podem também realizar execuções experimentais e reportar qualquer problema com a lógica de negócios do contrato e funcionalidade geral.

Implantar na testnet depois de testar no blockchain local é ideal desde que o primeiro é mais perto do comportamento da Máquina Virtual Ethereum. Portanto, é comum para muitos projetos nativos do Ethereum implantar dapps nas testnets para avaliar a operação dos contratos inteligentes em condições de vida real.

Mais sobre redes de teste do Ethereum.

Testes vs. Verificação formal

Ao passo que testar ajuda a confirmar se um contrato retorna os resultados esperados para algumas entradas de dados, isso não pode comprovar de forma conclusiva o mesmo para entradas não utilizadas durante os testes. Testar um contrato inteligente, portanto, não pode garantir "correção funcional" (o que significa que não pode mostrar que um programa se comporta conforme necessário para todos os conjuntos de valores de entrada).

Verificação formal é uma abordagem para avaliação da correção do software checando se um modelo formal do programa bate com a especificação formal. Um modelo formal é uma representação matemática abstrata de um programa, enquanto uma especificação formal define as propriedades de um programa (por exemplo, afirmações lógicas sobre a execução do programa).

Pelo fato de propriedades serem escritas em termos matemáticos, é possível verificar que um modelo formal (matemático) do sistema satisfaz uma especificação usando regras lógicas de inferência. Por isso, ferramentas de verificação formal são ditas produzir 'provas matemáticas' da correção de um sistema.

Diferente de testar, verificações formais podem ser usadas para verificar se a execução de um contrato inteligente satisfaz uma especificação formal para todas as execuções (por exemplo, não ter falhas) sem necessitar executá-lo com dados de amostra. Não apenas isto reduz tempo gasto em rodar dezenas de testes unitários, mas é também mais efetivo na caça por vulnerabilidades escondidas. Dito isto, técnicas de verificação formal se baseiam em um espectro dependendo da sua dificuldade de implementação e utilidade.

Saiba mais sobre verificação formal de contratos inteligentes.

Testes vs auditorias e recompensas por bugs

Como mencionado, testes rigorosos raramente podem garantir a ausência de bugs em um contrato; abordagens de verificação formal podem fornecer garantias mais fortes da correção, mas atualmente são difíceis de usar e incorrem em custos consideráveis.

Ainda assim, você pode aumentar a possibilidade de encontrar vulnerabilidades de contrato pegando uma revisão independente de código. Auditorias de contratos inteligentes(opens in a new tab) e recompensas por bugs(opens in a new tab) são duas maneiras de ter outros analisando os seus contratos.

Auditorias são realizadas por auditores experientes em encontrar casos de falhas de segurança e práticas pobres de desenvolvimento em contratos inteligentes. Uma auditoria irá geralmente incluir testes (e possivelmente verificação formal) assim como revisão manual de todo o código.

Por outro lado, um programa de recompensas por bug geralmente envolve oferta de recompensa financeira para um indivíduo (geralmente descrito como um whitehat hackers(opens in a new tab)) que descobre uma vulnerabilidade em um contrato inteligente e divulga-a para os desenvolvedores. As recompensas por bugs são semelhantes às auditorias, uma vez que envolve pedir que outras pessoas ajudem a encontrar defeitos em contratos inteligentes.

A maior diferença é que programas de recompensa por bug são abertos a uma maior comunidade de desenvolvedores/hackers e atraem uma vasta classe de hackers éticos e profissionais de segurança independentes com habilidades únicas e experiência. Isso pode ser uma vantagem em relação às auditorias de contratos inteligentes, que dependem principalmente de equipes que podem possuir conhecimentos especializados limitados ou estreitos.

Testando ferramentas e bibliotecas

Ferramentas de testes unitários

Ferramentas de teste baseadas em propriedades

Ferramentas de análise estática

  • Slither(opens in a new tab) - Framework com base no Python de análise estática estabelecida no Solidity para encontrar vulnerabilidades, aprimorar a compreensão do código e escrever análises personalizadas para contratos inteligentes.

  • Ethlint(opens in a new tab) - Analisador (linter) para garantir as práticas recomendadas de estilo e segurança para a linguagem de programação de contrato inteligente Solidity.

Ferramentas de análise dinâmica

  • Echidna(opens in a new tab) - Fuzzer (analisador) de contrato para detectar vulnerabilidades em contratos inteligentes por meio de testes baseados em propriedade.

  • Diligence Fuzzing(opens in a new tab) - Ferramenta de análise automatizada útil para detectar violações de propriedade no código de contrato inteligente.

  • Manticore(opens in a new tab) - Framework de execução simbólica dinâmica para análise de bytecode na EVM.

  • Mithril(opens in a new tab) - Ferramenta para diagnóstico de bytecode na EVM para detectar vulnerabilidades de contrato usando análise de contaminação, análise simbólica e verificação de fluxo de controle.

  • Diligence Scribble(opens in a new tab) - Scribble é uma linguagem de especificação e ferramenta de verificação do tempo de execução, que possibilita anotar contratos inteligentes com propriedades, o que permite testar automaticamente os contratos com ferramentas como Diligence Fuzzing ou MythX.

Leitura adicional

Este artigo foi útil?