Segurança de um Contrato Inteligente
Última atualização da página: 26 de fevereiro de 2026
Os contratos inteligentes são extremamente flexíveis e capazes de controlar grandes quantidades de valor e dados, enquanto executam lógica imutável com base no código implantado na blockchain. Isto criou um vibrante ecossistema de aplicações descentralizadas e sem confiança que oferecem muitas vantagens sobre os sistemas legados. Eles também representam oportunidades para os invasores que procuram lucrar explorando vulnerabilidades em contratos inteligentes.
Blockchains públicas, como a Ethereum, complicam ainda mais a questão de proteger contratos inteligentes. O código do contrato implantado geralmente não pode ser alterado para corrigir falhas de segurança, enquanto os ativos roubados de contratos inteligentes são extremamente difíceis de rastrear e, em sua maioria, irrecuperáveis devido à imutabilidade.
Embora os números variem, estima-se que o valor total roubado ou perdido devido a defeitos de segurança em contratos inteligentes é facilmente superior a 1 bilhão de dólares. Isso inclui incidentes de grande repercussão, como o hack da DAO (opens in a new tab) (3,6M de ETH roubados, valendo mais de US$ 1 bilhão nos preços de hoje), o hack da carteira multi-assinatura da Parity (opens in a new tab) (US$ 30 milhões perdidos para hackers) e o problema da carteira congelada da Parity (opens in a new tab) (mais de US$ 300 milhões em ETH bloqueados para sempre).
As questões mencionadas tornam imperativo para os desenvolvedores investirem esforços na construção de contratos inteligentes seguros, sólidos e resistentes. Segurança dos contratos inteligentes é um assunto sério, e todo desenvolvedor deve aprender. Este guia abrangerá considerações de segurança para desenvolvedores de Ethereum e explorará recursos para melhorar a segurança dos contratos inteligentes.
Pré-requisitos
Certifique-se de estar familiarizado com os fundamentos do desenvolvimento de contratos inteligentes antes de abordar a segurança.
Diretrizes para a criação de contratos inteligentes Ethereum seguros
1. Projete controles de acesso adequados
Em contratos inteligentes, as funções marcadas como public ou external podem ser chamadas por quaisquer contas de propriedade externa (EOAs) ou contas de contrato. Especificar a visibilidade pública para funções é necessária se você quiser que outras pessoas interajam com seu contrato. No entanto, as funções marcadas como private só podem ser chamadas por funções dentro do contrato inteligente, e não por contas externas. Dar a cada participantes da rede o acesso às funções do contrato pode causar problemas, especialmente se isso significar que qualquer pessoa pode realizar operações confidenciais (por exemplo, cunhar novos tokens).
Para evitar o uso não autorizado de funções do contrato inteligente, é necessário implementar controles de acesso seguros. Os mecanismos de controle de acesso restringem a capacidade de usar determinadas funções em um contrato inteligente para entidades aprovadas, como contas responsáveis pelo gerenciamento do contrato. O padrão Ownable e o controle baseado em papéis são dois padrões úteis para a implementação do controle de acesso em contratos inteligentes:
Padrão Ownable
No padrão Proprietário, um endereço é definido como o “dono” do contrato durante o processo de criação do contrato. As funções protegidas recebem um modificador OnlyOwner, que garante que o contrato autentique a identidade do endereço de chamada antes de executar a função. Chamadas para funções protegidas de outros endereços além do proprietário do contrato sempre revertem, impedindo o acesso indesejado.
Controle de acesso baseado em papéis
Registrar um único endereço como Owner em um contrato inteligente introduz o risco de centralização e representa um único ponto de falha. Se as chaves da conta do proprietário forem comprometidas, os invasores podem invadir o contrato de propriedade. É por isso que usar um padrão de controle de acesso baseado em funções com várias contas administrativas pode ser uma opção melhor.
No controle de acesso baseado em funções, o acesso a funções confidenciais é distribuído entre um conjunto de participantes confiáveis. Por exemplo, uma conta pode ser responsável por cunhar tokens (transformar um ativo digital na blockchain), enquanto outra conta realiza atualizações ou pausa o contrato. Descentralizar o controle de acesso dessa forma elimina pontos únicos de falha e reduz as suposições de confiança para os usuários.
Usando carteiras multi-assinatura
Outra abordagem para implementar um controle de acesso seguro é usar uma conta de múltiplas assinaturas para gerenciar um contrato. Ao contrário de um EOA (conta de propriedade externa) regular, as contas com várias assinaturas são de propriedade de várias entidades e exigem assinaturas de um número mínimo de contas - digamos 3 de 5 - para executar transações.
O uso de multisig (múltiplas assinaturas) para controle de acesso introduz uma camada extra de segurança, pois as ações no contrato de destino exigem o consentimento de várias partes. Isso é particularmente útil se usar o padrão Proprietário, pois torna mais difícil para um invasor ou malfeitor interno de manipular funções de contrato confidenciais para fins maliciosos.
2. Use as instruções require(), assert() e revert() para proteger as operações do contrato
Como mencionado, qualquer pessoa pode chamar funções públicas em seu contrato inteligente uma vez que ele é implantado na blockchain. Como você não pode saber com antecedência como as contas externas (EOA) vão interagir com um contrato, é ideal implementar proteções internas contra operações problemáticas antes da implantação. Você pode impor o comportamento correto em contratos inteligentes usando as instruções require(), assert() e revert() para acionar exceções e reverter alterações de estado se a execução falhar em satisfazer certos requisitos.
require(): As instruções require são definidas no início das funções e garantem que as condições predefinidas sejam atendidas antes que a função chamada seja executada. Uma instrução require pode ser usada para validar entradas do usuário, verificar variáveis de estado ou autenticar a identidade da conta chamadora antes de prosseguir com uma função.
assert(): assert() é usado para detectar erros internos e verificar violações de “invariantes” em seu código. Uma invariável é uma asserção lógica sobre o estado de um contrato que deve ser verdadeira para todas as execuções de função. Um exemplo invariável é a oferta total máxima ou saldo de um contrato de token. O uso de assert() garante que seu contrato nunca atinja um estado vulnerável e, se isso acontecer, todas as alterações nas variáveis de estado serão revertidas.
revert(): revert() pode ser usado em uma instrução if-else que aciona uma exceção se a condição necessária não for satisfeita. O contrato de exemplo abaixo usa revert() para proteger a execução das funções:
1pragma solidity ^0.8.4;23contract VendingMachine {4 address owner;5 error NaoAutorizado();6 function buy(uint amount) public payable {7 if (amount > msg.value / 2 ether)8 revert("Ether insuficiente fornecido.");9 // Realizar a compra.10 }11 function withdraw() public {12 if (msg.sender != owner)13 revert NaoAutorizado();1415 payable(msg.sender).transfer(address(this).balance);16 }17}Exibir tudo3. Teste contratos inteligentes e verifique a exatidão do código
A imutabilidade do código em execução na Máquina Virtual Ethereum significa que os contratos inteligentes exigem um nível mais alto de avaliação de qualidade durante a fase de desenvolvimento. Testar seu contrato extensivamente e observá-lo para quaisquer resultados inesperados irão melhorar muito a segurança e proteger os seus usuários a longo prazo.
O método habitual é escrever pequenos testes unitários utilizando dados simulados que o contrato deverá receber dos usuários. Teste de unidade é bom para testar a funcionalidade de certas funções e garantir que um contrato inteligente funcione como esperado.
Infelizmente, o teste unitário é minimamente eficaz para melhorar a segurança do contrato inteligente quando usado isoladamente. Um teste unitário pode provar que uma função é executada corretamente para dados simulados (mock), mas os testes unitários são tão eficazes quanto os testes que são escritos. Isso torna difícil detectar casos perdidos de falha e vulnerabilidades que poderiam quebrar a segurança de seu contrato inteligente.
Uma abordagem melhor é combinar testes de unidade com testes baseados em propriedades, realizados usando análise estática e dinâmica. A análise estática se baseia em representações de baixo nível, como gráficos de fluxo de controle (opens in a new tab) e árvores de sintaxe abstrata (opens in a new tab), para analisar os estados do programa e os caminhos de execução alcançáveis. Enquanto isso, as técnicas de análise dinâmica, como o fuzzing de contratos inteligentes (opens in a new tab), executam o código do contrato com valores de entrada aleatórios para detectar operações que violam as propriedades de segurança.
A verificação formal é outra técnica para verificar as propriedades de segurança em contratos inteligentes. Ao contrário dos testes regulares, a verificação formal pode comprovar conclusivamente a ausência de erros em um contrato inteligente. Isso é alcançado criando uma especificação formal que captura as propriedades de segurança desejadas e provando que um modelo formal dos contratos adere a esta especificação.
4. Solicite uma revisão independente do seu código
Depois de testar seu contrato, é bom pedir aos outros que verifiquem o código-fonte para quaisquer problemas de segurança. O teste não revelará todas as falhas de um contrato inteligente, mas realizar uma revisão independente aumenta a possibilidade de detectar vulnerabilidades.
Auditorias
A comissão de uma auditoria de contrato inteligente é uma forma de realizar uma revisão de código independente. Os auditores desempenham um papel importante na garantia de que os contratos inteligentes sejam seguros e livres de falhas de qualidade e erros de concepção.
Com isto em mente, há que evitar tratar as auditorias como uma bala de prata. Auditorias de contratos inteligentes não irão detectar todos os bugs e são concebidas principalmente para fornecer uma rodada adicional de revisões, o qual pode ajudar a detectar problemas perdidos pelos desenvolvedores durante o desenvolvimento e testes iniciais. Você também deve seguir as práticas recomendadas para trabalhar com auditores, como documentar o código apropriadamente e adicionar comentários em linha, para maximizar o benefício de uma auditoria de contrato inteligente.
- Dicas e truques de auditoria de contratos inteligentes (opens in a new tab) - @tinchoabbate
- Aproveite ao máximo sua auditoria (opens in a new tab) - Inference
Recompensas por bugs
A criação de um programa de recompensas por bugs é outra abordagem para implementar revisões de código externas. Uma recompensa por bugs é uma recompensa financeira dada a indivíduos (geralmente hackers de chapéu branco) que descobrem vulnerabilidades em um aplicativo.
Quando usadas corretamente, as recompensas por bugs dão aos membros da comunidade hacker incentivo para inspecionar seu código em busca de falhas críticas. Um exemplo da vida real é o “bug do dinheiro infinito” que teria permitido a um invasor criar uma quantidade ilimitada de ether no Optimism (opens in a new tab), um protocolo de Camada 2 em execução no Ethereum. Felizmente, um hacker white hat descobriu a falha (opens in a new tab) e notificou a equipe, ganhando um grande pagamento no processo (opens in a new tab).
Uma estratégia útil é definir o pagamento de um programa de recompensas por bugs proporcionalmente à quantidade de fundos em jogo. Descrita como a “recompensa por bug de escalabilidade (opens in a new tab)”, essa abordagem fornece incentivos financeiros para que indivíduos divulguem vulnerabilidades de forma responsável, em vez de explorá-las.
5. Siga as melhores práticas durante o desenvolvimento de contratos inteligentes
A existência de auditorias e recompensas por bugs não dispensa sua responsabilidade de escrever código de alta qualidade. Uma boa segurança em contrato inteligente começa com os seguintes processos de concepção e desenvolvimento adequados:
-
Guarde todo o código em um sistema de controle de versão, como git
-
Faça todas as modificações de código por meio de solicitações de pull (conhecido como pull request, da sigla PR)
-
Garanta que as solicitações de pull (PR) tenham pelo menos um revisor independente - se você estiver trabalhando sozinho(a) em um projeto, considere encontrar outros desenvolvedores e negociar revisões de código
-
Use um ambiente de desenvolvimento para testar, compilar e implantar contratos inteligentes
-
Execute seu código através de ferramentas básicas de análise de código, como Cyfrin Aderyn (opens in a new tab), Mythril e Slither. Idealmente, você deve fazer isso antes de cada solicitação de pull ser mesclado (merge) e comparar as diferenças na saída
-
Garanta que seu código seja compilado sem erros e que o compilador Solidity não emita alertas
-
Documente seu código adequadamente (usando NatSpec (opens in a new tab)) e descreva detalhes sobre a arquitetura do contrato em uma linguagem de fácil compreensão. Isso facilitará para outras pessoas auditarem e revisarem seu código.
6. Implemente planos robustos de recuperação de desastres
Conceber controles de acesso seguros, implementar modificadores de função e outras sugestões podem melhorar a segurança do contrato inteligente, mas não podem excluir a possibilidade de explorações maliciosas. Construir contratos inteligentes seguros requer “preparar-se para falhas” e ter um plano de retorno para responder de forma eficaz a ataques. Um plano de recuperação de desastres adequado incorporará alguns ou todos os seguintes componentes:
Atualizações de contrato
Embora os contratos inteligentes Ethereum sejam imutáveis por padrão, é possível alcançar algum grau de mutabilidade usando padrões de atualização. A atualização de contratos é necessária nos casos em que uma falha crítica torna seu contrato antigo inutilizável e a implantação de uma nova lógica é a opção mais viável.
Os mecanismos de atualização de contrato funcionam de forma diferente, mas o “padrão de proxy” é uma das abordagens mais populares para atualizar contratos inteligentes. Padrões de proxy (opens in a new tab) dividem o estado e a lógica de um aplicativo entre dois contratos. O primeiro contrato (chamado de 'contrato de proxy') armazena variáveis de estado (por exemplo, saldos de usuários), enquanto o segundo contrato (chamado de 'contrato lógico') contém o código para executar funções de contrato.
As contas interagem com o contrato de proxy, que encaminha todas as chamadas de função para o contrato de lógica usando a chamada de baixo nível delegatecall() (opens in a new tab). Diferente de uma chamada de mensagem regular, delegatecall() garante que o código em execução no endereço do contrato de lógica seja executado no contexto do contrato chamador. Isso significa que o contrato de lógica sempre gravará no armazenamento do proxy (em vez de em seu próprio armazenamento) e os valores originais de msg.sender e msg.value serão preservados.
Delegar chamadas para o contrato lógico requer armazenar seu endereço no armazenamento do contrato de proxy. Portanto, atualizar a lógica do contrato é apenas uma questão de implantar outro contrato lógico e armazenar o novo endereço no contrato de proxy. Como as chamadas subsequentes para o contrato de proxy são roteadas automaticamente para o novo contrato lógico, você teria “atualizado” o contrato sem realmente modificar o código.
Mais sobre a atualização de contratos.
Paradas de emergência
Como mencionado, auditorias e testes extensivos não podem descobrir todos os bugs em um contrato inteligente. Se uma vulnerabilidade aparecer em seu código após a implantação, corrigi-la é impossível, pois você não pode alterar o código em execução no endereço do contrato. Além disso, mecanismos de atualização (por exemplo, padrões de proxy) podem levar tempo para serem implementados (eles geralmente exigem aprovação de diferentes partes), o que só dá aos invasores mais tempo para causar mais danos.
A opção nuclear é implementar uma função de “interrupção de emergência” que bloqueia chamadas para funções vulneráveis em um contrato. As interrupções ou paradas de emergência normalmente compreendem os seguintes componentes:
-
Uma variável global booleana indicando se o contrato inteligente está em um estado interrompido ou não. Esta variável é definida como
falseao configurar o contrato, mas reverterá paratrueassim que o contrato for interrompido. -
Funções que referenciam a variável booleana em sua execução. Essas funções são acessíveis quando o contrato inteligente não é interrompido e tornam-se inacessíveis quando o recurso da interrupção de emergência é acionado.
-
Uma entidade que tenha acesso à função de parada de emergência, que define a variável booleana como
true. Para evitar ações maliciosas, as chamadas para essa função podem ser restritas a um endereço confiável (por exemplo, o proprietário do contrato).
Uma vez que o contrato ative a parada ou interrupção de emergência, determinadas funções não poderão ser chamadas. Isso é alcançado envolvendo funções de seleção em um modificador que faz referência à variável global. Abaixo está um exemplo (opens in a new tab) que descreve uma implementação desse padrão em contratos:
1// Este código não foi auditado profissionalmente e não faz promessas sobre segurança ou exatidão. Use por sua conta e risco.23contract EmergencyStop {45 bool isStopped = false;67 modifier stoppedInEmergency {8 require(!isStopped);9 _;10 }1112 modifier onlyWhenStopped {13 require(isStopped);14 _;15 }1617 modifier onlyAuthorized {18 // Verifique a autorização do msg.sender aqui19 _;20 }2122 function stopContract() public onlyAuthorized {23 isStopped = true;24 }2526 function resumeContract() public onlyAuthorized {27 isStopped = false;28 }2930 function deposit() public payable stoppedInEmergency {31 // Lógica de depósito aqui32 }3334 function emergencyWithdraw() public onlyWhenStopped {35 // Saque de emergência aqui36 }37}Exibir tudoEste exemplo mostra as características básicas das interrupções de emergência:
-
isStoppedé uma variável booleana que avalia parafalseno início etruequando o contrato entra no modo de emergência. -
Os modificadores de função
onlyWhenStoppedestoppedInEmergencyverificam a variávelisStopped.stoppedInEmergencyé usado para controlar funções que devem estar inacessíveis quando o contrato está vulnerável (por exemplo,deposit()). As chamadas para essas funções simplesmente serão revertidas.
onlyWhenStopped é usado para funções que devem ser chamadas durante uma emergência (por exemplo, emergencyWithdraw()). Essas funções podem ajudar a resolver a situação, daí a sua exclusão da lista de “funções restritas”.
Usar uma funcionalidade de interrupção de emergência fornece um paliativo eficaz para lidar com vulnerabilidades graves em seu contrato inteligente. No entanto, aumenta a necessidade dos usuários confiarem nos desenvolvedores para não ativá-lo por razões egoístas. Para este fim, descentralizar o controle da interrupção de emergência sujeitando-o a um mecanismo de votação on-chain, como o timelock (bloqueio de tempo para transações) ou a aprovação de uma carteira de assinatura múltipla são soluções possíveis.
Monitoramento de eventos
Eventos (opens in a new tab) permitem que você rastreie chamadas para funções de contrato inteligente e monitore alterações nas variáveis de estado. É ideal programar seu contrato inteligente para emitir um evento sempre que alguma parte tomar uma ação crítica de segurança (por exemplo, retirar fundos).
Registrar eventos e monitorá-los off-chain fornece informações sobre as operações do contrato e auxilia na descoberta mais rápida de ações maliciosas. Isso significa que sua equipe pode responder mais rapidamente a hacks e tomar medidas para mitigar o impacto sobre os usuários, como pausar funções ou realizar uma atualização.
Você também pode optar por uma ferramenta de monitoramento pronta para uso, que encaminha alertas automaticamente, sempre que alguém interage com seus contratos. Essas ferramentas permitirão que você crie alertas personalizados com base em diferentes gatilhos, como volume de transações, frequência de chamadas de função ou funções específicas envolvidas. Por exemplo, você poderia programar um alerta que chega quando a quantia retirada em uma única transação ultrapassa determinado limite.
7. Projete sistemas de governança seguros
Você pode querer descentralizar sua aplicação, transferindo o controle dos principais contratos inteligentes para os membros da comunidade. Neste caso, o sistema de contrato inteligente incluirá um módulo de governança — um mecanismo que permite que os membros da comunidade aprovem ações administrativas, por meio de um sistema de governança on-chain. Por exemplo, uma proposta para atualizar um contrato de proxy para uma nova implementação, que pode ser votada pelos detentores do token.
A governança descentralizada pode ser benéfica, especialmente porque alinha os interesses dos desenvolvedores e usuários finais. No entanto, os mecanismos de governança de contratos inteligentes podem apresentar novos riscos se implementados incorretamente. Um cenário plausível é se um invasor adquirir um enorme poder de voto (medido pelo número de tokens detidos) ao fazer um empréstimo relâmpago e aprovar uma proposta maliciosa.
Uma forma de evitar problemas relacionados à governança on-chain é usar um timelock (opens in a new tab). Um timelock impede que um contrato inteligente execute certas ações até que um período específico passe. Outras estratégias incluem atribuir um “peso de voto” a cada token com base em quanto tempo ele foi bloqueado ou medir o poder de voto de um endereço em um período histórico (por exemplo, 2-3 blocos no passado) em vez do bloco atual. Ambos os métodos reduzem a possibilidade de acumular rapidamente o poder de voto para influenciar os votos on-chain.
Mais sobre como projetar sistemas de governança seguros (opens in a new tab), diferentes mecanismos de votação em DAOs (opens in a new tab) e os vetores comuns de ataque a DAOs que se aproveitam do DeFi (opens in a new tab) nos links compartilhados.
8. Reduza a complexidade do código ao mínimo
Os desenvolvedores de software tradicionais estão familiarizados com o princípio KISS (“Não complique, estúpido!”), o qual aconselha a não introdução complexidade desnecessária na concepção de software. Isso segue o pensamento de longa data, de que “sistemas complexos falham de maneiras complexas” e são mais suscetíveis a erros dispendiosos.
Não complicar é de particular importância ao escrever contratos inteligentes, visto que os contratos inteligentes estão potencialmente controlando grandes quantidades de valor. Uma dica para alcançar a simplicidade ao escrever contratos inteligentes é reutilizar bibliotecas existentes, como os Contratos OpenZeppelin (opens in a new tab), sempre que possível. Como essas bibliotecas foram extensivamente auditadas e testadas pelos desenvolvedores, usá-las reduz as chances de introduzir bugs ao escrever novas funcionalidades do zero.
Outro conselho comum é escrever funções pequenas e manter contratos modulares, dividindo a lógica do negócio por vários contratos. Não só escrever um código simples reduz a superfície de ataque em um contrato inteligente, também facilita argumentar sobre a exatidão do sistema por inteiro e detectar possíveis erros de concepção mais cedo.
9. Defenda-se contra vulnerabilidades comuns de contratos inteligentes
Reentrância
A EVM (Ethereum Virtual Machine) não permite concorrência (paralelismo), o que significa que dois contratos envolvidos em uma chamada de mensagem não podem ser executados simultaneamente. Uma chamada externa pausa a execução e a memória do contrato de chamada até que a chamada retorne, momento em que a execução prossegue normalmente. Este processo pode ser formalmente descrito como a transferência do fluxo de controle (opens in a new tab) para outro contrato.
Embora a maioria seja inofensiva, a transferência de fluxo de controle para contratos não confiáveis pode causar problemas, tais como a reentrância. Um ataque de reentrância ocorre quando um contrato malicioso volta a chamar um contrato vulnerável antes que a invocação da função original ser completa. Este tipo de ataque é melhor explicado com um exemplo.
Considere um contrato inteligente ("vítima") que permite que qualquer pessoa deposite e saque Ether:
1// Este contrato é vulnerável. Não use em produção23contract Victim {4 mapping (address => uint256) public balances;56 function deposit() external payable {7 balances[msg.sender] += msg.value;8 }910 function withdraw() external {11 uint256 amount = balances[msg.sender];12 (bool success, ) = msg.sender.call.value(amount)("");13 require(success);14 balances[msg.sender] = 0;15 }16}Exibir tudoEste contrato expõe uma função withdraw() para permitir que os usuários saquem ETH previamente depositado no contrato. Ao processar uma retirada, o contrato realiza as seguintes operações:
- Verifica o saldo de ETH do usuário
- Envia fundos para o endereço de chamada
- Redefine seu saldo para 0, evitando saques adicionais do usuário
A função withdraw() no contrato Victim segue um padrão de “verificações-interações-efeitos”. Ele verifica se as condições necessárias para a execução são satisfeitas (ou seja, o usuário tem um saldo de ETH positivo) e realiza a interação enviando ETH para o endereço do chamador, antes de aplicar os efeitos da transação (ou seja, reduzindo o saldo do usuário).
Se withdraw() for chamada a partir de uma conta de propriedade externa (EOA), a função será executada como esperado: msg.sender.call.value() envia ETH para o chamador. No entanto, se msg.sender for uma conta de contrato inteligente que chama withdraw(), o envio de fundos usando msg.sender.call.value() também acionará a execução do código armazenado nesse endereço.
Imagine que este é o código implantado no endereço do contrato:
1 contract Attacker {2 function beginAttack() external payable {3 Victim(victim_address).deposit.value(1 ether)();4 Victim(victim_address).withdraw();5 }67 function() external payable {8 if (gasleft() > 40000) {9 Victim(victim_address).withdraw();10 }11 }12}Exibir tudoEste contrato foi concebido para fazer três coisas:
- Aceite um depósito de outra conta (provavelmente o EOA do atacante)
- Deposite 1 ETH no contrato Victim
- Retire o 1 ETH armazenado no contrato inteligente
Não há nada de errado aqui, exceto que Attacker tem outra função que chama withdraw() em Victim novamente se o gás restante do msg.sender.call.value recebido for maior que 40.000. Isso dá a Attacker a capacidade de reentrar em Victim e sacar mais fundos antes que a primeira invocação de withdraw seja concluída. O ciclo fica assim:
1- EOA do invasor chama `Attacker.beginAttack()` com 1 ETH2- `Attacker.beginAttack()` deposita 1 ETH em `Victim`3- `Attacker` chama `withdraw() em `Victim`4- `Victim` verifica o saldo do `Attacker` (1 ETH)5- `Victim` envia 1 ETH para o `Attacker` (que aciona a função padrão)6- `Attacker` chama `Victim.withdraw()` novamente (observe que `Victim` não reduziu o saldo do `Attacker` desde a primeira retirada)7- `Victim` verifica o saldo do `Attacker` (que ainda é 1 ETH porque não tem aplicado os efeitos da primeira chamada)8- `Victim` envia 1 ETH para `Attacker` (que aciona a função padrão e permite que `Attacker` entre novamente na função `withdraw`)9- O processo se repete até que `Attacker` fique sem gás, ponto em que `msg.sender.call.value` retorna sem acionar retiradas adicionais10- `Victim` finalmente aplica os resultados da primeira transação (e as subsequentes) ao seu estado, então o saldo do `Attacker` é definido para 0 (zero)Exibir tudoO resumo é que, como o saldo do chamador não é definido como 0 até que a execução da função termine, as invocações subsequentes serão bem-sucedidas e permitirão que o chamador retire seu saldo várias vezes. Esse tipo de ataque pode ser usado para drenar os fundos de um contrato inteligente, como o que aconteceu no hack da DAO em 2016 (opens in a new tab). Os ataques de reentrância ainda são um problema crítico para os contratos inteligentes hoje, como mostram as listas públicas de explorações de reentrância (opens in a new tab).
Como prevenir ataques de reentrância
Uma abordagem para lidar com a reentrância é seguir o padrão de verificações-efeitos-interações (opens in a new tab). Este padrão ordena a execução de funções de forma que o código que realiza as verificações necessárias antes de prosseguir com a execução chegar primeiro, seguido pelo código que manipula o estado do contrato, com o código que interage com outros contratos ou EOAs chegando por último.
O padrão de interação de verificação-efeito é usado em uma versão revisada do contrato Victim mostrado abaixo:
1contract NoLongerAVictim {2 function withdraw() external {3 uint256 amount = balances[msg.sender];4 balances[msg.sender] = 0;5 (bool success, ) = msg.sender.call.value(amount)("");6 require(success);7 }8}Este contrato realiza uma verificação no saldo do usuário, aplica os efeitos da função withdraw() (redefinindo o saldo do usuário para 0) e prossegue para realizar a interação (enviando ETH para o endereço do usuário). Isso garante que o contrato atualize seu armazenamento antes da chamada externa, eliminando a condição de reentrância que permitiu o primeiro ataque. O contrato Attacker ainda poderia chamar de volta NoLongerAVictim, mas como balances[msg.sender] foi definido como 0, saques adicionais gerarão um erro.
Outra opção é usar um bloqueio de exclusão mútua (comumente descrito como "mutex") que bloqueia uma porção do estado de um contrato até que a invocação de uma função seja concluída. Isso é implementado usando uma variável booleana que é definida como true antes que a função seja executada e reverte para false após a conclusão da invocação. Como visto no exemplo abaixo, usar um mutex protege uma função contra chamadas recursivas enquanto a invocação original ainda está sendo processada, efetivamente interrompendo a reentrada.
1pragma solidity ^0.7.0;23contract MutexPattern {4 bool locked = false;5 mapping(address => uint256) public balances;67 modifier noReentrancy() {8 require(!locked, "Bloqueado contra reentrância.");9 locked = true;10 _;11 locked = false;12 }13 // Esta função está protegida por um mutex, portanto, chamadas reentrantes de dentro de `msg.sender.call` não podem chamar `withdraw` novamente.14 // A instrução `return` é avaliada como `true`, mas ainda avalia a instrução `locked = false` no modificador15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {16 require(balances[msg.sender] >= _amount, "Nenhum saldo para sacar.");1718 balances[msg.sender] -= _amount;19 (bool success, ) = msg.sender.call{value: _amount}("");20 require(success);2122 return true;23 }24}Exibir tudoVocê também pode usar um sistema de pagamentos pull (opens in a new tab) que exige que os usuários retirem fundos dos contratos inteligentes, em vez de um sistema de "pagamentos push" que envia fundos para as contas. Isso elimina a possibilidade de acionar código inadvertidamente em endereços desconhecidos (e também pode impedir determinados ataques de negação de serviço).
Underflows e overflows de números inteiros
Um extravasamento (overflow) de números inteiros ocorre quando os resultados de uma operação aritmética ficam fora do intervalo aceitável de valores, fazendo com que ela "role" para o menor valor representável. Por exemplo, um uint8 só pode armazenar valores até 2^8-1=255. As operações aritméticas que resultam em valores superiores a 255 sofrerão overflow e redefinirão uint para 0, semelhante a como o hodômetro de um carro é redefinido para 0 quando atinge a quilometragem máxima (999999).
Underflows de inteiros acontecem por razões semelhantes: os resultados de uma operação aritmética ficam abaixo do intervalo aceitável. Digamos que você tentou decrementar 0 em um uint8, o resultado simplesmente passaria para o valor máximo representável (255).
Tanto overflows quanto underflows de inteiros podem levar a mudanças inesperadas nas variáveis de estado de um contrato e resultar em execução não planejada. Veja abaixo um exemplo mostrando como um invasor pode explorar o extravasamento aritmético em um contrato inteligente para executar uma operação inválida:
1pragma solidity ^0.7.6;23// Este contrato foi projetado para atuar como um cofre de tempo.4// O usuário pode depositar neste contrato, mas não pode sacar por pelo menos uma semana.5// O usuário também pode estender o tempo de espera para além do período de espera de 1 semana.67/*81. Implante o TimeLock92. Implante o Attack com o endereço do TimeLock103. Chame Attack.attack enviando 1 ether. Você poderá imediatamente11 sacar seu ether.1213O que aconteceu?14O ataque causou o overflow de TimeLock.lockTime e foi capaz de sacar15antes do período de espera de 1 semana.16*/1718contract TimeLock {19 mapping(address => uint) public balances;20 mapping(address => uint) public lockTime;2122 function deposit() external payable {23 balances[msg.sender] += msg.value;24 lockTime[msg.sender] = block.timestamp + 1 weeks;25 }2627 function increaseLockTime(uint _secondsToIncrease) public {28 lockTime[msg.sender] += _secondsToIncrease;29 }3031 function withdraw() public {32 require(balances[msg.sender] > 0, "Fundos insuficientes");33 require(block.timestamp > lockTime[msg.sender], "O tempo de bloqueio não expirou");3435 uint amount = balances[msg.sender];36 balances[msg.sender] = 0;3738 (bool sent, ) = msg.sender.call{value: amount}("");39 require(sent, "Falha ao enviar Ether");40 }41}4243contract Attack {44 TimeLock timeLock;4546 constructor(TimeLock _timeLock) {47 timeLock = TimeLock(_timeLock);48 }4950 fallback() external payable {}5152 function attack() public payable {53 timeLock.deposit{value: msg.value}();54 /*55 se t = tempo de bloqueio atual, então precisamos encontrar x tal que56 x + t = 2**256 = 057 então x = -t58 2**256 = type(uint).max + 159 então x = type(uint).max + 1 - t60 */61 timeLock.increaseLockTime(62 type(uint).max + 1 - timeLock.lockTime(address(this))63 );64 timeLock.withdraw();65 }66}Exibir tudoComo evitar overflows e underflows de números inteiros
A partir da versão 0.8.0, o compilador Solidity rejeita código que resulta em underflows e overflows de números inteiros. No entanto, os contratos compilados com uma versão inferior do compilador devem realizar verificações em funções que envolvam operações aritméticas ou usar uma biblioteca (por exemplo, SafeMath (opens in a new tab)) que verifica a ocorrência de underflow/overflow.
Manipulação de oráculos
Oráculos obtêm informações off-chain e as enviam on-chain para uso em contratos inteligentes. Com oráculos, você pode criar contratos inteligentes que interagem com sistemas off-chain, como mercados capitais, expandindo muito sua aplicação.
Mas se o oráculo estiver corrompido e enviar informações incorretas on-chain, os contratos inteligentes serão executados com base em entradas erradas, o que pode causar problemas. Essa é a base do “problema do oráculo” (paradoxo), que diz respeito à tarefa de garantir que as informações de um oráculo da blockchain sejam precisas, atualizadas e pontuais.
Uma preocupação de segurança relacionada está usando um oráculo on-chain, como uma troca descentralizada, para obter o preço de ponto por um ativo. As plataformas de empréstimo no setor de finanças descentralizadas (DeFi) costumam fazer isso para determinar o valor da garantia de um usuário e, assim, definir o quanto ele pode tomar emprestado.
Os preços dos DEX são muitas vezes exatos, em grande parte devido aos árbitros que restauram a paridade nos mercados. Porém, eles estão abertos à manipulação, especialmente se o oráculo on-chain calcular os preços dos ativos com base em padrões históricos de negociação (como geralmente é o caso).
Por exemplo, um invasor pode explodir artificialmente o preço de um ativo fazendo um empréstimo rápido antes de interagir com seu contrato de empréstimo. Consultar o DEX pelo preço do ativo retornaria um valor mais alto do que normal (devido à grande demanda de inclinação do atacante de "ordem de compra" pelo ativo), permitir que emprestem mais do que deveriam. Esses "ataques de empréstimos rápidos" foram utilizados para explorar a dependência de preços nos oráculos de aplicações DeFi, custando protocolos milhões em fundos perdidos.
Como evitar manipulação de oráculos
O requisito mínimo para evitar a manipulação de oráculos (opens in a new tab) é usar uma rede de oráculos descentralizada que consulta informações de várias fontes para evitar pontos únicos de falha. Na maioria dos casos, oráculos descentralizados tem incentivos criptoeconômicos incorporados para incentivar nós oráculos a relatar informações corretas, tornando-os mais seguros do que os oráculos centralizados.
Se você planeja consultar um oráculo on-chain sobre preços de ativos, considere usar um que implemente um mecanismo de preço médio ponderado por tempo (TWAP). Um oráculo TWAP (opens in a new tab) consulta o preço de um ativo em dois momentos diferentes (que você pode modificar) e calcula o preço à vista com base na média obtida. Escolher períodos mais longos protege seu protocolo contra a manipulação de preços uma vez que grandes ordens executadas recentemente não podem afetar os preços dos ativos.
Recursos de segurança de contratos inteligentes para desenvolvedores
Ferramentas para analisar contratos inteligentes e verificar a correção do código
-
Ferramentas e bibliotecas de teste - Coleção de ferramentas e bibliotecas padrão da indústria para realizar testes de unidade, análise estática e análise dinâmica em contratos inteligentes.
-
Ferramentas de verificação formal - Ferramentas para verificar a correção funcional em contratos inteligentes e checar invariantes.
-
Serviços de auditoria de contratos inteligentes - Listagem de organizações que fornecem serviços de auditoria de contratos inteligentes para projetos de desenvolvimento Ethereum.
-
Plataformas de recompensa por bugs - Plataformas para coordenar recompensas por bugs e premiar a divulgação responsável de vulnerabilidades críticas em contratos inteligentes.
-
Fork Checker (opens in a new tab) - Uma ferramenta on-line gratuita para verificar todas as informações disponíveis sobre um contrato bifurcado.
-
ABI Encoder (opens in a new tab) - Um serviço online gratuito para codificar as funções e os argumentos do construtor do seu contrato Solidity.
-
Aderyn (opens in a new tab) - Analisador estático do Solidity, percorrendo as Árvores de Sintaxe Abstrata (AST) para identificar vulnerabilidades suspeitas e imprimir problemas em um formato markdown de fácil consumo.
Ferramentas para monitorar contratos inteligentes
- Tenderly Real-Time Alerting (opens in a new tab) - Uma ferramenta para receber notificações em tempo real quando eventos incomuns ou inesperados acontecem em seus contratos inteligentes ou carteiras.
Ferramentas para administração segura de contratos inteligentes
-
Safe (opens in a new tab) - Carteira de contrato inteligente executada na Ethereum que requer um número mínimo de pessoas para aprovar uma transação antes que ela possa ocorrer (M-de-N).
-
OpenZeppelin Contracts (opens in a new tab) - Bibliotecas de contratos para implementar recursos administrativos, incluindo propriedade de contrato, atualizações, controles de acesso, governança, pausabilidade e muito mais.
Serviços de auditoria de contratos inteligentes
-
ConsenSys Diligence (opens in a new tab) - Serviço de auditoria de contratos inteligentes que ajuda projetos em todo o ecossistema de blockchain a garantir que seus protocolos estejam prontos para o lançamento e construídos para proteger os usuários.
-
CertiK (opens in a new tab) - Empresa de segurança de blockchain pioneira no uso de tecnologia de verificação formal de ponta em contratos inteligentes e redes de blockchain.
-
Trail of Bits (opens in a new tab) - Empresa de segurança cibernética que combina pesquisa de segurança com uma mentalidade de invasor para reduzir riscos e fortalecer códigos.
-
PeckShield (opens in a new tab) - Empresa de segurança de blockchain que oferece produtos e serviços para a segurança, privacidade e usabilidade de todo o ecossistema de blockchain.
-
QuantStamp (opens in a new tab) - Serviço de auditoria que facilita a adoção em massa da tecnologia blockchain por meio de serviços de segurança e avaliação de riscos.
-
OpenZeppelin (opens in a new tab) - Empresa de segurança de contratos inteligentes que fornece auditorias de segurança para sistemas distribuídos.
-
Runtime Verification (opens in a new tab) - Empresa de segurança especializada em modelagem e verificação formal de contratos inteligentes.
-
Hacken (opens in a new tab) - Auditor de segurança cibernética da Web3 que traz a abordagem de 360 graus para a segurança da blockchain.
-
Nethermind (opens in a new tab) - Serviços de auditoria em Solidity e Cairo que garantem a integridade dos contratos inteligentes e a segurança dos usuários em toda a Ethereum e Starknet.
-
HashEx (opens in a new tab) - A HashEx se concentra na auditoria de blockchain e contratos inteligentes para garantir a segurança de criptomoedas, fornecendo serviços como desenvolvimento de contratos inteligentes, testes de penetração e consultoria em blockchain.
-
Code4rena (opens in a new tab) - Plataforma de auditoria competitiva que incentiva especialistas em segurança de contratos inteligentes a encontrar vulnerabilidades e ajudar a tornar a web3 mais segura.
-
CodeHawks (opens in a new tab) - Plataforma de auditorias competitivas que hospeda competições de auditoria de contratos inteligentes para pesquisadores de segurança.
-
Cyfrin (opens in a new tab) - Potência em segurança Web3, incubando a segurança de criptoativos através de produtos e serviços de auditoria de contratos inteligentes.
-
ImmuneBytes (opens in a new tab) - Empresa de segurança Web3 que oferece auditorias de segurança para sistemas de blockchain por meio de uma equipe de auditores experientes e as melhores ferramentas da categoria.
-
Oxorio (opens in a new tab) - Auditorias de contratos inteligentes e serviços de segurança de blockchain com experiência em EVM, Solidity, ZK e tecnologia cross-chain para empresas de criptoativos e projetos de DeFi.
-
Inference (opens in a new tab) - Empresa de auditoria de segurança especializada em auditoria de contratos inteligentes para blockchains baseadas em EVM. É graças a seus auditores especializados que eles identificam possíveis problemas e sugerem soluções úteis para corrigi-los antes da implementação.
Plataformas de recompensa por bugs
-
Immunefi (opens in a new tab) - Plataforma de recompensa por bugs para contratos inteligentes e projetos de DeFi, onde pesquisadores de segurança revisam códigos, divulgam vulnerabilidades, são pagos e tornam as criptomoedas mais seguras.
-
HackerOne (opens in a new tab) - Plataforma de coordenação de vulnerabilidades e recompensa por bugs que conecta empresas com testadores de penetração e pesquisadores de segurança cibernética.
-
HackenProof (opens in a new tab) - Plataforma de recompensa por bugs especializada para projetos de cripto (DeFi, contratos inteligentes, carteiras, CEX e mais), onde profissionais de segurança fornecem serviços de triagem e pesquisadores são pagos por relatórios de bugs relevantes e verificados.
-
Sherlock (opens in a new tab) - Subscritor na Web3 para segurança de contratos inteligentes, com pagamentos para auditores gerenciados por meio de contratos inteligentes para garantir que os bugs relevantes sejam pagos de forma justa.
-
CodeHawks (opens in a new tab) - Plataforma de recompensa por bugs competitiva, onde os auditores participam de concursos e desafios de segurança e (em breve) em suas próprias auditorias privadas.
Publicações de vulnerabilidades e ataques (exploits) conhecidos de contratos inteligentes
-
ConsenSys: ataques conhecidos em contratos inteligentes (opens in a new tab) - Explicação amigável para iniciantes sobre as vulnerabilidades de contrato mais significativas, com código de exemplo para a maioria dos casos.
-
Registro SWC (opens in a new tab) - Lista com curadoria de itens da Enumeração de Fraquezas Comuns (CWE) que se aplicam a contratos inteligentes Ethereum.
-
Rekt (opens in a new tab) - Publicação atualizada regularmente sobre hacks e explorações de criptoativos de grande repercussão, juntamente com relatórios post-mortem detalhados.
Desafios para aprender sobre segurança de contratos inteligentes
-
Awesome BlockSec CTF (opens in a new tab) - Lista com curadoria de wargames de segurança de blockchain, desafios e competições de Capture The Flag (opens in a new tab) e artigos de soluções.
-
Damn Vulnerable DeFi (opens in a new tab) - Wargame para aprender segurança ofensiva de contratos inteligentes de DeFi e desenvolver habilidades em caça a bugs e auditoria de segurança.
-
Ethernaut (opens in a new tab) - Wargame baseado em Web3/Solidity onde cada nível é um contrato inteligente que precisa ser 'hackeado'.
-
HackenProof x HackTheBox (opens in a new tab) - Desafio de hacking de contrato inteligente, ambientado em uma aventura de fantasia. A conclusão bem-sucedida do desafio também dá acesso a um programa privado de recompensa por bugs.
Melhores práticas para proteger contratos inteligentes
-
ConsenSys: melhores práticas de segurança de contratos inteligentes Ethereum (opens in a new tab) - Lista abrangente de diretrizes para proteger contratos inteligentes Ethereum.
-
Nascent: Simple Security Toolkit (opens in a new tab) - Coleção de guias práticos focados em segurança e listas de verificação para o desenvolvimento de contratos inteligentes.
-
Padrões do Solidity (opens in a new tab) - Compilação útil de padrões seguros e melhores práticas para a linguagem de programação de contratos inteligentes Solidity.
-
Documentação do Solidity: Considerações de segurança (opens in a new tab) - Diretrizes para escrever contratos inteligentes seguros com Solidity.
-
Padrão de Verificação de Segurança de Contratos Inteligentes (opens in a new tab) - Lista de verificação de quatorze partes criada para padronizar a segurança de contratos inteligentes para desenvolvedores, arquitetos, revisores de segurança e fornecedores.
-
Aprenda sobre segurança e auditoria de contratos inteligentes (opens in a new tab) - Curso definitivo de segurança e auditoria de contratos inteligentes, criado para desenvolvedores de contratos inteligentes que desejam aprimorar suas melhores práticas de segurança e se tornar pesquisadores de segurança.