Segurança de um Contrato Inteligente
Os contratos inteligentes da Ethereum são extremamente flexíveis, capaz de armazenar grandes quantidades de tokens (muitas vezes superior a $1B) e executar uma lógica imutável baseada no código do contrato inteligente previamente implantado. Embora isto tenha criado um ecossistema criativo e vibrante de contratos inteligentes confiáveis e interconectados, é também o ecossistema perfeito para atrair atacantes que buscam lucrar, explorando vulnerabilidades em contratos inteligentes e comportamento inesperado na Ethereum. O código do contrato inteligente geralmente não pode ser alterado para correção de falhas de segurança. Os bens que foram roubados a contratos inteligentes são irrecuperáveis e os bens roubados são extremamente difíceis de rastrear. The total of amount of value stolen or lost due to smart contract issues is easily over $1B. Alguns dos maiores devido a erros de código de contrato inteligentes incluem:
- Problema multi-sig de paridade #1 - $30M perdidos(opens in a new tab)
- Problema multi-sig de paridade #2 - $300M bloqueados(opens in a new tab)
- O hack DAO, 3.6M ETH! Mais de $1B nos preços de hoje do ETH(opens in a new tab)
Pré-requisitos
Isto abrangerá a segurança de contratos inteligentes, por isso, certifique-se de que você está familiarizado com contratos inteligentes antes de resolver a segurança.
Como escrever um código de Contrato Inteligente mais seguro
Antes de lançar qualquer código no mainnet, é importante tomar precaução suficiente para proteger qualquer coisa de valor que seu contrato inteligente tenha sido confiado. Neste artigo, discutiremos alguns ataques específicos, forneceremos recursos para aprender sobre mais tipos de ataques, e deixá-lo com algumas ferramentas básicas e melhores práticas para garantir que seus contratos funcionem de forma correta e segura.
Auditorias não são uma arma mágica
Anos antes, as ferramentas para escrita, compilação, testes e implantação de contratos inteligentes eram muito imaturas, levando muitos projetos a escrever código Solidity de maneiras aleatórias, e depois encaminha-los a um auditor que investigaria o código para garantir que ele funcione de forma segura e conforme esperado. Em 2020, os processos de desenvolvimento e ferramentas que apoiam a linguagem Solidity são significativamente melhores; se aproveitar destas melhores práticas não só garante que o seu projeto seja mais fácil de gerenciar, como é uma parte vital da segurança do seu projeto. Uma auditoria no final da escrita do seu contrato inteligente não é mais suficiente como a única consideração de segurança que seu projeto faz. A segurança começa antes de escrever sua primeira linha de código do contrato inteligente, a segurança começa com um design adequado e processos de desenvolvimento.
Processo de desenvolvimento de um Contrato Inteligente
No mínimo:
- Todo o código armazenado em um sistema de controle de versão, como o git
- Todas as modificações de código feitas via Pull Requests
- Todos os Pull Requests têm pelo menos um revisor. Se você for um projeto individual, considere encontrar outro autor individual e trocar avaliações de código!
- Um único comando compila, implementa e executa um conjunto de testes contra o seu código usando um ambiente de desenvolvimento de Ethereum (Veja: Truffle)
- Você executou seu código através de ferramentas de análise de código básicas como Mythril e Slither, idealmente antes de cada pull request ser mesclado, comparando diferenças na saída
- Solidity não emite QUALQUER aviso do compilador
- Seu código está bem documentado
Há muito mais a dizer em relação ao processo de desenvolvimento, mas estes itens são um bom ponto de partida. Para mais itens e explicações detalhadas, veja a lista de verificação de qualidade fornecida pelo DeFiSafety(opens in a new tab). DefiSafety(opens in a new tab) é um serviço público não oficial de publicação de várias grandes avaliações públicas de Ethereum dApps. Parte do sistema de classificação DeFiSafety inclui quão bem o projeto adere a esta lista de verificação de qualidade do processo. Ao seguir estes processos:
- Você produzirá um código mais seguro, através de testes automáticos reprodutíveis
- Os auditores serão capazes de rever seu projeto com mais eficácia
- Integração mais fácil de novos desenvolvedores
- Permite aos desenvolvedores rapidamente iterar, testar e receber feedback sobre modificações
- Menos provável que seu projeto tenha regressões
Ataques e vulnerabilidades
Agora que você está escrevendo código Solidity usando um processo de desenvolvimento eficiente, vamos analisar algumas vulnerabilidades comuns de Solidity para ver o que pode dar errado.
Reentrada
A Reentrada é uma das maiores e mais significativas questões de segurança a considerar ao desenvolver Contratos Inteligentes. Enquanto o EVM não pode executar múltiplos contratos ao mesmo tempo, um contrato que chame um contrato diferente pausa a execução do contrato e o estado de memória até que a chamada retorne, e neste ponto a execução prossegue normalmente. Esta pausa e reinicialização pode criar uma vulnerabilidade conhecida como "reentrada".
Esta é uma versão simples de um contrato que é vulnerável a reentrada:
1// ESTE CONTRATO É VULNERÁVEL INTENCIONALMENTE, NÃO COPIAR2contract Victim {3 mapping (address => uint256) public balances;45 function deposit() external payable {6 balances[msg.sender] += msg.value;7 }89 function withdraw() external {10 uint256 amount = balances[msg.sender];11 (bool success, ) = msg.sender.call.value(amount)("");12 require(success);13 balances[msg.sender] = 0;14 }15}16Exibir tudoCopiar
Para permitir que um usuário retire o ETH que tenha armazenado anteriormente no contrato, esta função
- Lê quanto saldo o usuário tem
- Envia o valor de saldo em ETH
- Redefine o seu saldo para 0, então eles não podem sacar do seu saldo novamente.
If called from a regular account (such as your own MetaMask account), this functions as expected: msg.sender.call.value() simply sends your account ETH. Contudo, os contratos inteligentes também podem fazer chamadas. Se um contrato personalizado malicioso é aquele que chama withdraw()
, msg.sender.call.value() não só enviará o valor
de ETH, como também chamará implicitamente o contrato para começar a executar o código. Imagine este contrato malicioso:
1contract 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}13Exibir tudoCopiar
Chamando Attacker.beginAttack() começará um ciclo que parece algo como:
10.) Ataque EOA chama Attacker.beginAttack() com 1 ETH20.) Attacker.beginAttack() deposita 1 ETH na vítima34 1.) Atacante -> Victim.withdraw()5 1.) Victim reads balances[msg.sender]6 1.) Vítima envia ETH para o Atacante (que executa a função padrão)7 2.) Atacante -> Victim.withdraw()8 2.) Victim reads balances[msg.sender]9 2.) Vítima envia ETH para o Atacante (que executa a função padrão)10 3.) Atacante -> Victim.withdraw()11 3.) Victim reads balances[msg.sender]12 3.) Vítima envia ETH para o Atacante (que executa a função padrão)13 4.) Atacante não tem mais gás suficiente, retorna sem chamar a função novamente14 3.) balances[msg.sender] = 0;15 2.) balances[msg.sender] = 0; (já era 0)16 1.) balances[msg.sender] = 0; (já era 0)17Exibir tudo
Chamando Attacker.beginAttack com 1 ETH irá atacar a vítima com reentrada, sacando mais ETH do que o que foi provido (retirado dos saldos de outros usuários, fazendo com que o contrato da vítima fique sub-colateralizado)
Como lidar com a reentrada (da maneira errada)
Alguém poderia considerar vencer a reentrada, simplesmente impedindo qualquer contrato inteligente de interagir com o seu código. Você busca no stackoverflow, e encontra esse trecho de código com toneladas de votos:
1function isContract(address addr) internal returns (bool) {2 uint size;3 assembly { size := extcodesize(addr) }4 return size > 0;5}6Copiar
Parece fazer sentido: os contratos têm código, se o autor da chamada tiver qualquer código, não permita que ele faça um depósito. Vamos adicionar:
1// ESTE CONTRATO TEM VULNERABILIDADE INTENCIONALMENTE, NÃO COPIE2contract ContractCheckVictim {3 mapping (address => uint256) public balances;45 function isContract(address addr) internal returns (bool) {6 uint size;7 assembly { size := extcodesize(addr) }8 return size > 0;9 }1011 function deposit() external payable {12 require(!isContract(msg.sender)); // <- NEW LINE13 balances[msg.sender] += msg.value;14 }1516 function withdraw() external {17 uint256 amount = balances[msg.sender];18 (bool success, ) = msg.sender.call.value(amount)("");19 require(success);20 balances[msg.sender] = 0;21 }22}23Exibir tudoCopiar
Agora, para depositar ETH, você não deve ter o código do contrato inteligente no seu endereço. No entanto, isso é facilmente derrotado com o seguinte contrato do Atacante:
1contract ContractCheckAttacker {2 constructor() public payable {3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- New line4 }56 function beginAttack() external payable {7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();8 }910 function() external payable {11 if (gasleft() > 40000) {12 Victim(VICTIM_ADDRESS).withdraw();13 }14 }15}16Exibir tudoCopiar
Enquanto o primeiro ataque foi um ataque na lógica do contrato, isso é um ataque ao comportamento de implantação do contrato Ethereum. Durante a construção, um contrato ainda não devolveu seu código para ser implantado em seu endereço, mas mantém o controle total da EVM DURANTE este processo.
É tecnicamente possível prevenir que contratos inteligentes chamem seu código, usando esta linha:
1require(tx.origin == msg.sender)2Copiar
Contudo, esta ainda não é uma boa solução. Um dos aspectos mais empolgantes do Ethereum é a sua compostabilidade, os contratos inteligentes se integram e constroem entre si. Ao usar a linha acima, você está limitando a utilidade do seu projeto.
Como lidar com a reentrada (da maneira correta)
Ao simplesmente mudar a ordem da atualização do armazenamento e a chamada externa, evitamos a condição de reentrada, que permitiu o ataque. Chamando de volta a função sacar, quando possível, não vai beneficiar o atacante, já que os saldos
do armazenamento já estarão definidos para 0.
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}9Copiar
O código acima segue o padrão de design "Verificações-Efeitos-Interações", que ajuda a se proteger contra reentrada. Você pode ler mais sobre Verificações-Efeitos-Interações aqui(opens in a new tab)
Como lidar com a reentrada (a opção nuclear)
Sempre que você estiver enviando ETH para um endereço não confiável ou interagindo com um contrato desconhecido (como chamar transfer()
de um endereço de token fornecido pelo usuário), se abre à possibilidade de reentrada. Ao projetar contratos que nem enviam ETH nem chamam contratos não confiáveis, você impede a possibilidade de reentrada!
Mais tipos de ataque
Os tipos de ataque acima cobrem problemas de codificação de contrato inteligente (reentrada) e peculiaridades do Ethereum (executando código dentro dos construtores de contratos, antes que o código esteja disponível no endereço do contrato). Há muito, muito mais de tipos de ataque para conhecer, tais como:
- Front-running
- Rejeição do envio de ETH
- Integer overflow/underflow
Leitura adicional:
- Consensys Smart Contract Known Attacks(opens in a new tab) - Uma explicação muito legível das vulnerabilidades mais significativas, com código de amostra para a maioria.
- Registro SWC(opens in a new tab) - Lista dos CWE que se aplicam à Ethereum e a contratos inteligentes
Ferramentas de Segurança
Embora não haja um substituto para entender os conceitos básicos de segurança do Ethereum e contatar uma empresa de auditoria profissional para revisar seu código, existem muitas ferramentas disponíveis para ajudar a destacar potenciais problemas em seu código.
Segurança de um Contrato Inteligente
Slither - Estrutura de análise estática Solidity escrita em Python 3.
MythX - API de análise de segurança para contratos inteligentes na Ethereum.
Mythril - Ferramente de análise de segurança para bytecode EVM.
Manticore - Uma interface de linha de comando que usa uma ferramenta de execução simbólica em contratos inteligentes e binários.
Securify - Scanner de segurança para contratos inteligentes Ethereum.
ERC20 Verifier - Uma ferramenta de verificação usada para conferir se um contrato está de acordo com o padrão ERC20.
Verificação formal
Informações sobre verificação formal
- Como a verificação formal dos contatos inteligentes funciona(opens in a new tab) 20 de julho de 2018 - Brian Marick
- Como a verificação formal pode garantir contratos inteligentes sem falhas(opens in a new tab) 29 de janeiro de 2018 - Bernard Mueller
Utilizando ferramentas
Duas das ferramentas mais populares para a análise de segurança de contratos inteligentes são:
- Slither(opens in a new tab) por Rastro de Bits(opens in a new tab) (versão hospedada: Critica(opens in a new tab))
- Mythril(opens in a new tab) por ConsenSys(opens in a new tab) (versão hospedada: MythX(opens in a new tab))
Ambas são ferramentas úteis que analisam seu código e reportam problemas. Cada uma possui uma versão hospedada [comercial], mas também está disponível para ser executada localmente. O exemplo a seguir é um rápido exemplo de como executar Slither, que é disponibilizado em uma imagem conveniente do Docker trailofbits/eth-security-toolbox
. Você precisará instalar o Docker se você ainda não o tiver instalado(opens in a new tab).
$ mkdir test-slither$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract. ol$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolboxdocker$ cd /sharedocker$ solc-select 0,11docker$ slither bad-contract.sol
Irá gerar esta saída:
ethsec@1435b241ca60:/share$ slither bad-contract.solINFO:Detectors:Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):External calls:- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)State variables written after the call(s):- balances[msg.sender] = 0 (bad-contract.sol#15)Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilitiesINFO:Detectors:Low level call in Victim.withdraw() (bad-contract.sol#11-16):- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-callsINFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) foundINFO:Slither:Use https://crytic.io/ to get access to additional detectors and GitHub integrationExibir tudo
Slither identificou o potencial de reentrada aqui, identificando as linhas chave onde o problema pode ocorrer e nos dando um link para mais detalhes sobre o problema:
Referência: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities(opens in a new tab)
permitindo que você aprenda rapidamente sobre potenciais problemas com seu código. Como todas as ferramentas de testes automatizadas, o Slither não é perfeito, e erra no fato de relatar demais. Pode avisar sobre uma possível reentrada, mesmo quando não existe qualquer vulnerabilidade explorável. Muitas vezes, rever a DIFERENÇA na saída de Slither entre alterações de código é extremamente esclarecedora, ajudando a descobrir vulnerabilidades que foram introduzidas muito mais cedo do que esperar que o seu projeto esteja completo em termos de código.
Leituras adicionais
Guias sobre melhores práticas de segurança para um Contrato Inteligente
- consensys.github.io/smart-contract-best-practices/(opens in a new tab)
- GitHub(opens in a new tab)
- Coleção agregada de recomendações de segurança e melhores práticas(opens in a new tab)
Padrão de verificação de segurança para um Contrato Inteligente (SCSVS)
Conhece algum recurso da comunidade que o ajudou? Edite essa página e adicione!
Tutoriais relacionados
- Fluxo de desenvolvimento seguro
- Como utilizar o Slither para encontrar bugs nos contratos inteligentes
- Como usar o Manticore para encontrar bugs em contratos inteligentes
- Diretrizes de segurança
- Segurança de tokens