Segurança na Web3: Desmistificando Imutabilidade e Elevando Padrões de Código
Este artigo foi escrito por Barba.
À medida que avançamos para um novo ciclo de mercado no ecossistema web3, percebemos que, para uma vasta parcela da população mundial, alguns conceitos básicos desse ecossistema ainda são obscuros, confusos e até desconhecidos. Portanto, nesta e na próxima semana, focaremos em artigos mais técnicos. Hoje, abordaremos um ponto chave: a Segurança.
Infelizmente, é comum ouvir adeptos da web3 confundirem imutabilidade com segurança. A imutabilidade garante que o contrato inteligente com o qual você interage não será alterado para beneficiar terceiros. Contudo, isso não assegura que o contrato esteja livre de bugs, erros de lógica ou brechas que possam prejudicar os usuários em momentos oportunos.
É responsabilidade do desenvolvedor empenhar-se ao máximo para elevar a segurança do seu código. E, como bem salientou Patrick Collins, “é responsabilidade do auditor fazer o seu melhor para encontrar e reportar toda e qualquer vulnerabilidade em um código”.
Focando em vulnerabilidades acidentais, existem inúmeros vetores que podem levar a exploits. Um exploit pode não ocorrer sempre de forma maliciosa, como em um ataque. Isso significa que um contrato imutável pode estar suscetível a vulnerabilidades. Portanto, imutabilidade não equivale a segurança, e é por isso que códigos claros e auditorias são cruciais na web3.
Vamos explorar um cenário simples:
- Você está verificando um contrato no Etherscan;
- Uma função chama sua atenção, você interage com essa função, e tudo ocorre normalmente.
- Então você identifica uma função chamada “Kill” e a executa.
- Para sua surpresa, a função também é executada corretamente.
Qual o resultado? Você “deleta” o contrato! Isso parece absurdo, mas aconteceu, e o prejuízo foi significativo. Existe um contexto por trás desse acontecimento, mas o ponto principal é:
Como minimizar o risco e facilitar a identificação de vulnerabilidades durante revisões de código e auditorias?
Auditorias são processos extremamente necessários e têm um alto custo. Logo, precisamos facilitar ao máximo o trabalho dos auditores para que possam se concentrar em encontrar vulnerabilidades em vez de tentar decifrar códigos complexos e obsoletos.
O que veremos a seguir é o resultado de dois princípios básicos: Disciplina e Organização.
Layout
Se você desenvolve, qual é o layout dos seus Contratos Inteligentes? Como desenvolvedores, nossa tarefa é manter códigos limpos, claros, objetivos e bem documentados, facilitando o entendimento por qualquer pessoa que os acesse. Informações adicionais sobre isso podem ser encontradas na documentação do Solidity.
Nesse link você pode acessar o layout que eu uso para desenvolver os projetos da Bellum Galaxy e parceiros.
Nomenclatura
Erros
Erros são uma maneira eficiente de indicar que uma ação não pode ser executada por algum motivo, além de auxiliar na depuração do código. É importante que os erros sejam descritivos para facilitar o entendimento e economizar gás.
Erros são comumente declarados da seguinte forma:
error DeuRuimCara();
Observando esse erro customizado, podemos identificar:
- Deu
- Ruim
- Cara
Isso representa um desperdício de gás e em alguns casos podem não ser tão descritivo.
A maneira recomendada de utilizar erros customizados é:
error NomeDoContrato_OValorInseridoEhMenorQueOValorPermitido(uint256 valorInserido, uint256 valorPermitido);
Agora, as informações que conseguimos identificar são:
- Nome do contrato onde o erro foi disparado;
- O motivo pelo qual o erro foi disparado;
- O valor que você inseriu;
- O valor mínimo permitido.
Variáveis
A nomeação das variáveis deve refletir o seu tipo de armazenamento, adotando padrões claros que facilitem a identificação e organização do código. Vejamos um exemplo que não deve ser seguido:
contract Exemplo {
uint256 public umNumero;
uint256 public immutable doisNumeros;
uint256 public constant tresNumeros = 3;
constructor(uint256 doisNumerosUm){
doisNumeros = doisNumerosUm;
}
function umaFuncao(uint256 umOutroNumero) public returns(uint256){
uint256 maisUmNumero = umOutroNumero + 50;
return maisUmNumero;
}
}
Embora neste contrato seja fácil identificar qual variável é qual, o que aconteceria se ele tivesse 600 linhas de código?
Para facilitar o entendimento, a organização e até mesmo a sua própria vida na revisão do código, por que não adotar este método:
contract Exemplo {
//Inicia com s_, de storage
uint256 public s_umNumero;
//Inicia com i_, de immutable, seguido do nome da variável
uint256 public immutable i_doisNumeros;
//Caixa alta, separado por _
uint256 public constant TRES_NUMEROS = 3;
//Remoção de "número mágico"
uint256 public constant BONUS_DA_FUNCAO = 50;
//Incluímos um _ antes do nome da variável
//indicando que é um parâmetro da função.
constructor(uint256 _doisNumerosUm){
i_doisNumeros = _doisNumerosUm;
}
//Incluímos um _ antes do nome da variável
//indicando que é um parâmetro da função.
function umaFuncao(uint256 _umOutroNumero) public returns(uint256){
//Substituímos o "número mágico", 50, pelo nome descritivo.
uint256 maisUmNumero = _umOutroNumero + BONUS_DA_FUNCAO;
return maisUmNumero;
}
}
Existem inúmeros ajustes que ainda podem ser feitos no que diz respeito à economia de gás, etc. Mas não é o momento oportuno, estamos focando no básico.
Eventos
Assim como os erros, os eventos devem ser descritivos e emitidos sempre após alterações no storage, aumentando a transparência e a rastreabilidade das operações.
contract UmContrato{
uint256 public s_umaVariavel;
event UmContrato_UmaVariavelFoiAtualizada(uint256 valorAnterior, uint256 valorAtual);
function umaFuncao(uint256 _umParametro) public {
uint256 umaVariavelNaMemoria = s_umaVariavel;
s_umaVariavel = _umParametro;
emit UmContrato_UmaVariavelFoiAtualizada(umaVariavelNaMemoria, _umParametro);
}
}
Funções
A nomenclatura das funções deve indicar sua visibilidade, diferenciando claramente aquelas que são acessíveis externamente das que são internas ou privadas.
//Funções que podem ser acessadas externamente, sejam públicas ou externas
function funcaoPublicaOuExterna() public{}
//Funções que são acessadas só internamente, sejam internas ou privadas
function _funcaoInternaOuExterna() private{}
Conclusão
A verificação da segurança de um contrato inteligente não é a última etapa do desenvolvimento, mas um aspecto que deve ser considerado desde a criação do arquivo até a finalização de toda a documentação, auditorias e plano de contingência pós-deploy. Como vimos, com organização e disciplina, é possível seguir padrões básicos que refletem diretamente na segurança do projeto.