1, 2, 3 Testando: Eliminando Surpresas Indesejáveis em Contratos Inteligentes

Bellum Galaxy
bellum-galaxy-community
4 min readApr 2, 2024

Esse artigo foi escrito por Barba.

Essa é a terceira semana de conteúdo técnico sobre desenvolvimento. Até aqui, abordamos as práticas básicas de desenvolvimento, como layout, nomenclatura de variáveis e funções, no artigo “Segurança na Web3: Desmistificando Imutabilidade e Elevando Padrões de Código”, ao mesmo tempo que esclarecemos dúvidas comuns. Também discutimos segurança, ressaltando a importância de bases de código claras e objetivas, e listamos os pré-requisitos necessários para que seu projeto possa ser auditado com sucesso no artigo “Segurança em Blockchain: O Caminho para Auditorias de Contratos Inteligentes Robustas”.

Hoje, vamos abordar um tema que se situa entre os dois primeiros: Testes.

Os testes são cruciais para garantir que o seu código se comporte como esperado e são a primeira etapa de ajustes. Mas, você já parou para pensar se está testando o seu código corretamente?

Na Bellum Galaxy, usamos Foundry e dividimos nossos testes em:

  • Unitários
  • Integração
  • Fork
  • Fuzz

Teríamos mais uma etapa, que é a Verificação Formal, mas isso ainda está sendo aprimorado. Chegaremos lá!

Testes Unitários

Testes unitários verificam a integridade de uma variável, função, etc., em isolamento. Você confirma se ela se comporta como esperado após determinado processo. Vamos usar o contrato abaixo como exemplo:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

contract Example {
//variável que está sendo alterada.
uint256 private s_example;

//evento emitido após alteração de uma variável de estado do contrato.
event Example_StorageVariableExampleUpdated(uint256 previousValue, uint256 currentValue);

//função que atualiza a variável.
function updateVariable(uint256 _newValue) external {
//armazena o valor na memória
uint256 previousValue = s_example;
//atualiza a variável com o novo valor.
s_example = _newValue;

//informa a alteração. Transparência é importante!
emit Example_StorageVariableExampleUpdated(previousValue, s_example);
}

//Função para visualizar o valor da variável
function getStorageVariableValue() external view returns(uint256){
return s_example;
}
}

Com testes unitários, validamos se, quando a função updateVariable é chamada diretamente pelo usuário:

  • O evento é emitido.
  • O evento emite o valor atualizado.

Pode parecer óbvio, mas muitos desenvolvedores não verificam o principal: a variável de estado. Com isso, variáveis ‘inconsistentes’ podem passar despercebidas.

Mas e se essa função for chamada por outro contrato?

Imagem gerada por AI.

Testes de Integração

Seja um contrato externo, de outro projeto, ou um segundo contrato do seu próprio projeto, o fato é que seu projeto receberá os mais diversos tipos de interação assim que estiver em ambiente de produção. Portanto, é imprescindível realizar testes para cobrir esses pontos também.

Então, entramos na área dos testes de integração, que simulam e verificam a resposta de um contrato a interações vindas de outro. Um exemplo comum, responsável por inúmeros vetores de exploit, é um projeto que aceita tokens ERC20 como forma de pagamento. Esse projeto poderá interagir com inúmeros tipos de tokens, como tokens que cobram taxa na transação, tokens com número de decimais diferentes, tokens que podem ser atualizáveis e mudar no futuro, etc.

Os testes de integração servem para verificar como seu projeto reagirá a esses cenários.

Testes de Fork

De forma simples e direta, os testes de fork criam uma cópia local de determinada rede, na qual você deseja realizar os testes, e executa esses testes em um ambiente simulado. Essa cópia considera o estado atual da rede e é uma ferramenta importante para que o desenvolvedor possa entender o comportamento do seu projeto em um ambiente real.

Testes Fuzz

Os testes fuzz devem ser tratatos como um teste base para qualquer projeto. Eles ajudam a verificar cenários humanamente impossíveis de checar manualmente, testando suas funções ao inserir ‘automaticamente’ grandes quantidades de dados aleatórios, tentando quebrar a funcionalidade.

Imagine um cenário onde a função de saque do seu projeto realiza um cálculo para verificar a quantidade que cada usuário pode sacar com base no valor depositado e alguns incentivos. Você realiza os testes com inputs ‘comuns’ como 10, 43, 500, e tudo ocorre bem. Entretanto, com o valor 387, a razão do cálculo é quebrada, e o usuário recebe um saque maior, ou o valor acaba sendo bloqueado dentro do contrato.

Como alguém descobriria isso?

Esse é um exemplo muito simples, mas isso não se limita a tokens ERC20. Os fuzzers cobrem todo esse terreno, ajudando a tornar os contratos mais seguros e consistentes.

Para se aprofundar nesse assunto, inscreva-se nos cursos do Patrick Collins, na Cyfrin Updraft. Abaixo, você pode ver um vídeo específico sobre fuzz e invariantes.

Os testes fuzz são divididos em três categorias, mas abordaremos isso em outro momento. Não vamos atropelar as etapas.

Conclusão

Testes são imprescindíveis para colocar um projeto à prova e verificar se o código funciona como esperado, garantindo o bom funcionamento e ajudando a eliminar possíveis vulnerabilidades que poderiam ser exploradas em ambientes de produção. Entretanto, ter cem por cento de cobertura de testes não significa um projeto sem vulnerabilidades. É por isso que iniciamos este artigo deixando claro que os testes são um passo intermediário essencial entre o desenvolvimento do projeto e os processos de auditoria.

Precisa de orientação para melhorar a qualidade dos seus testes, preparar o seu projeto para uma auditoria, ou está procurando por um auditor independente?

Entre em contato comigo no LinkedIn.

Conecte-se Conosco:

Visite nosso site, junte-se ao nosso Discord, siga-nos no X, Instagram e no LinkedIn para ficar por dentro de nossas aventuras e insights.

--

--

Bellum Galaxy
bellum-galaxy-community

An educational community to bring science and technology to all.