Práticas recomendadas de solidez para segurança inteligente de contratos

blog 1NewsDevelopersEnterpriseBlockchain ExplainedEvents and ConferencesPressboletins informativos

Assine a nossa newsletter.

Endereço de email

Nós respeitamos sua privacidade

HomeBlogBlockchain Development

Práticas recomendadas de solidez para segurança inteligente de contratos

Do monitoramento às considerações de carimbo de data / hora, aqui estão algumas dicas profissionais para garantir que seus contratos inteligentes Ethereum sejam fortalecidos. por ConsenSysAugust 21, 2020Posted on August 21, 2020

herói de boas práticas de solidez

Por ConsenSys Diligence, nossa equipe de especialistas em segurança de blockchain.

Se você levou a sério a mentalidade de segurança de contrato inteligente e está entendendo as idiossincrasias do EVM, é hora de considerar alguns padrões de segurança que são específicos para a linguagem de programação Solidity. Neste resumo, vamos nos concentrar em recomendações de desenvolvimento seguro para Solidity que também podem ser instrutivas para o desenvolvimento de contratos inteligentes em outras línguas. 

Ok, vamos começar.

Use assert (), require (), revert () corretamente

As funções de conveniência afirmar e exigir pode ser usado para verificar as condições e lançar uma exceção se a condição não for atendida.

afirmar função só deve ser usada para testar erros internos e verificar invariantes.

exigir A função deve ser usada para garantir que condições válidas, como entradas ou variáveis ​​de estado do contrato sejam atendidas, ou para validar valores de retorno de chamadas para contratos externos. 

Seguir este paradigma permite que ferramentas de análise formal verifiquem se o opcode inválido nunca pode ser alcançado: o que significa que nenhuma invariância no código é violada e que o código é verificado formalmente.

solidez do pragma ^ 0,5,0; Sharer de contrato {function sendHalf (address payable addr) devoluções públicas a pagar (saldo uint) {require (msg.value% 2 == 0, "Valor uniforme obrigatório."); // Require () pode ter uma string de mensagem opcional uint balanceBeforeTransfer = address (this) .balance; (sucesso de bool,) = addr.call.value (msg.value / 2) (""); requer (sucesso); // Uma vez que revertemos se a transferência falhou, não deve haver // nenhuma maneira de ainda ficarmos com metade do dinheiro. assert (endereço (this) .balance == balanceBeforeTransfer – msg.value / 2); // usado para verificação de erro interno endereço de retorno (this) .balance; }} Linguagem de código: JavaScript (javascript)

Ver SWC-110 & SWC-123

Use modificadores apenas para verificações

O código dentro de um modificador é geralmente executado antes do corpo da função, portanto, qualquer mudança de estado ou chamadas externas violarão o Verificações-efeitos-interações padronizar. Além disso, essas declarações também podem passar despercebidas pelo desenvolvedor, pois o código do modificador pode estar longe da declaração da função. Por exemplo, uma chamada externa no modificador pode levar ao ataque de reentrada:

registro do contrato {proprietário do endereço; function isVoter (address _addr) external return (bool) {// Code}} contrato Election {Registry registry; modificador isEligible (endereço _addr) {require (registry.isVoter (_addr)); _; } function vote () isEligible (msg.sender) public {// Code}} Linguagem de código: JavaScript (javascript)

Nesse caso, o contrato do Registro pode fazer um ataque de reentrada chamando Election.vote () dentro de isVoter ().

Observação: Usar modificadores para substituir verificações de condição duplicadas em várias funções, como isOwner (), caso contrário, use require ou revert dentro da função. Isso torna seu código de contrato inteligente mais legível e fácil de auditar.

Cuidado com o arredondamento com divisão inteira

Todas as divisões inteiras são arredondadas para baixo para o número inteiro mais próximo. Se precisar de mais precisão, considere usar um multiplicador ou armazene o numerador e o denominador.

(No futuro, o Solidity terá um ponto fixo tipo, o que tornará isso mais fácil.)

// uint ruim x = 5/2; // O resultado é 2, todas as divisões inteiras arredondam para BAIXO para a linguagem de código inteiro mais próxima: JavaScript (javascript)

O uso de um multiplicador evita o arredondamento para baixo. Esse multiplicador precisa ser considerado ao trabalhar com x no futuro:

// bom multiplicador uint = 10; uint x = (5 * multiplicador) / 2; Linguagem de código: JavaScript (javascript)

Armazenar o numerador e denominador significa que você pode calcular o resultado do numerador / denominador fora da cadeia:

// numerador uint bom = 5; denominador uint = 2; Linguagem de código: JavaScript (javascript)

Esteja ciente das vantagens e desvantagens entre contratos abstratos e interfaces

Ambas as interfaces e contratos abstratos fornecem uma abordagem personalizável e reutilizável para contratos inteligentes. As interfaces, que foram introduzidas no Solidity 0.4.11, são semelhantes aos contratos abstratos, mas não podem ter nenhuma função implementada. As interfaces também têm limitações, como não ser capaz de acessar o armazenamento ou herdar de outras interfaces, o que geralmente torna os contratos abstratos mais práticos. Embora, as interfaces sejam certamente úteis para projetar contratos antes da implementação. Além disso, é importante ter em mente que, se um contrato herda de um contrato abstrato, ele deve implementar todas as funções não implementadas por meio de substituição ou também será abstrato.

Funções alternativas

Mantenha as funções de fallback simples

Funções alternativas são chamados quando um contrato é enviado uma mensagem sem argumentos (ou quando nenhuma função corresponde) e só tem acesso a 2.300 gases quando chamados de um .send () ou .transfer (). Se você deseja receber Ether de um .send () ou .transfer (), o máximo que você pode fazer em uma função de fallback é registrar um evento. Use uma função adequada se um cálculo de mais gás for necessário.

// função ruim () a pagar {saldos [msg.sender] + = msg.value; } // boa função deposit () a pagar externo {saldos [msg.sender] + = msg.value; } function () a pagar {require (msg.data.length == 0); emitir LogDepositReceived (msg.sender); } Linguagem de código: JavaScript (javascript)

Verifique o comprimento dos dados em funções de fallback

Desde o funções alternativas não é apenas chamado para transferências de ether simples (sem dados), mas também quando nenhuma outra função corresponde, você deve verificar se os dados estão vazios se a função de fallback for destinada a ser usada apenas com o propósito de registrar o Ether recebido. Caso contrário, os chamadores não perceberão se seu contrato for usado incorretamente e funções que não existem forem chamadas.

// função ruim () a pagar {emit LogDepositReceived (msg.sender); } // good function () payable {require (msg.data.length == 0); emitir LogDepositReceived (msg.sender); } Linguagem de código: JavaScript (javascript)

Marque explicitamente funções a pagar e variáveis ​​de estado

A partir do Solidity 0.4.0, cada função que está recebendo ether deve usar o modificador a pagar, caso contrário, se a transação tiver msg.value > 0 irá reverter (exceto quando forçado).

Observação: Algo que pode não ser óbvio: o modificador a pagar só se aplica a chamadas de contratos externos. Se eu chamar uma função não pagável na função pagável no mesmo contrato, a função não pagável não falhará, embora msg.value ainda esteja definido.

Marque explicitamente a visibilidade em funções e variáveis ​​de estado

Rotule explicitamente a visibilidade das funções e variáveis ​​de estado. As funções podem ser especificadas como externas, públicas, internas ou privadas. Por favor, entenda as diferenças entre eles, por exemplo, externo pode ser suficiente em vez de público. Para variáveis ​​de estado, externo não é possível. Rotular a visibilidade explicitamente tornará mais fácil pegar suposições incorretas sobre quem pode chamar a função ou acessar a variável.

  • As funções externas fazem parte da interface do contrato. Uma função externa f não pode ser chamada internamente (ou seja, f () não funciona, mas this.f () funciona). Funções externas às vezes são mais eficientes quando recebem grandes matrizes de dados.
  • As funções públicas fazem parte da interface do contrato e podem ser chamadas internamente ou por meio de mensagens. Para variáveis ​​de estado públicas, uma função getter automática (veja abaixo) é gerada.
  • Funções internas e variáveis ​​de estado só podem ser acessadas internamente, sem usar este.
  • Funções privadas e variáveis ​​de estado são visíveis apenas para o contrato em que são definidas e não em contratos derivados. Observação: Tudo o que está dentro de um contrato é visível para todos os observadores externos ao blockchain, até mesmo variáveis ​​privadas.

// uint x ruim; // o padrão é interno para variáveis ​​de estado, mas deve ser feito explicitamente function buy () {// o padrão é público // código público} // good uint private y; function buy () external {// apenas chamável externamente ou usando this.buy ()} function utility () public {// chamável externamente, bem como internamente: alterar este código requer pensar em ambos os casos. } function internalAction () internal {// código interno} Linguagem de código: PHP (php)

Ver SWC-100 e SWC-108

Bloqueie pragmas para uma versão específica do compilador

Os contratos devem ser implantados com a mesma versão do compilador e sinalizadores com os quais eles foram mais testados. Bloquear o pragma ajuda a garantir que os contratos não sejam implantados acidentalmente usando, por exemplo, o compilador mais recente, que pode ter riscos maiores de bugs não descobertos. Os contratos também podem ser implantados por terceiros e o pragma indica a versão do compilador pretendida pelos autores originais.

// solidez do pragma ruim ^ 0.4.4; // boa solidez do pragma 0.4.4; Linguagem de código: JavaScript (javascript)

Nota: uma versão de pragma flutuante (ou seja, ^ 0.4.25) compilará bem com 0.4.26-nightly.2018.9.25, no entanto, nightly builds nunca devem ser usados ​​para compilar o código para produção.

Aviso: As declarações de pragma podem flutuar quando um contrato se destina ao consumo por outros desenvolvedores, como no caso de contratos em uma biblioteca ou pacote EthPM. Caso contrário, o desenvolvedor precisaria atualizar manualmente o pragma para compilar localmente.

Ver SWC-103

Use eventos para monitorar a atividade do contrato

Pode ser útil ter uma maneira de monitorar a atividade do contrato após sua implantação. Uma maneira de fazer isso é examinar todas as transações do contrato, embora isso possa ser insuficiente, pois as chamadas de mensagens entre os contratos não são gravadas no blockchain. Além disso, mostra apenas os parâmetros de entrada, não as mudanças reais que estão sendo feitas no estado. Além disso, os eventos podem ser usados ​​para acionar funções na interface do usuário.

contrato de caridade {mapeamento (endereço => uint) saldos; função donate () a pagar público {saldos [msg.sender] + = msg.value; }} jogo de contrato {function buyCoins () pagável public {// 5% vai para a caridade charity.donate.value (msg.value / 20) (); }} Linguagem de código: JavaScript (javascript)

Aqui, o contrato do jogo fará uma chamada interna para Charity.donate (). Esta transação não aparecerá na lista de transações externas da instituição beneficente, mas apenas visível nas transações internas.

Um evento é uma maneira conveniente de registrar algo que aconteceu no contrato. Os eventos que foram emitidos permanecem no blockchain junto com os outros dados do contrato e estão disponíveis para auditoria futura. Aqui está uma melhoria do exemplo acima, usando eventos para fornecer um histórico das doações da instituição de caridade.

contrato Caridade {// define evento de evento LogDonate (uint _amount); mapeamento (endereço => uint) saldos; função donate () a pagar público {saldos [msg.sender] + = msg.value; // emite o evento emit LogDonate (msg.value); }} jogo de contrato {function buyCoins () pagável public {// 5% vai para a caridade charity.donate.value (msg.value / 20) (); }} Linguagem de código: JavaScript (javascript)

Aqui, todas as transações que passam pelo contrato de caridade, direta ou não, aparecerão na lista de eventos desse contrato junto com o valor do dinheiro doado.

Nota: Prefira construções mais recentes de Solidity. Prefira construções / aliases, como autodestruição (ao invés de suicídio) e keccak256 (ao invés de sha3). Padrões como require (msg.sender.send (1 ether)) também podem ser simplificados para usar transfer (), como em msg.sender.transfer (1 ether). Verificação de saída Log de mudança de solidez para mais mudanças semelhantes.

Esteja ciente de que ‘integrados’ podem ser obscurecidos

Atualmente é possível sombra globais integrados no Solidity. Isso permite que os contratos substituam a funcionalidade de integrados, como msg e revert (). Embora isso é pretendido, pode enganar os usuários de um contrato quanto ao verdadeiro comportamento do contrato.

contrato PretendingToRevert {função revert () constante interna {}} contrato ExampleContract is PretendingToRevert {function somethingBad () public {revert (); }}

Os usuários do contrato (e auditores) devem estar cientes do código-fonte do contrato inteligente completo de qualquer aplicativo que pretendam usar.

Evite usar tx.origin

Nunca use tx.origin para autorização, outro contrato pode ter um método que chamará seu contrato (onde o usuário tem alguns fundos, por exemplo) e seu contrato irá autorizar essa transação, pois seu endereço está em tx.origin.

contrato MyContract {proprietário do endereço; function MyContract () public {owner = msg.sender; } função sendTo (destinatário do endereço, quantidade uint) public {require (tx.origin == owner); (sucesso de bool,) = receiver.call.value (amount) (""); requer (sucesso); }} contrato AttackingContract {MyContract myContract; atacante de endereço; função AttackingContract (endereço myContractAddress) public {myContract = MyContract (myContractAddress); atacante = msg.sender; } function () public {myContract.sendTo (atacante, msg.sender.balance); }} Linguagem de código: JavaScript (javascript)

Você deve usar msg.sender para autorização (se outro contrato chamar o seu contrato, msg.sender será o endereço do contrato e não o endereço do usuário que chamou o contrato).

Você pode ler mais sobre isso aqui: Solidity docs

Aviso: Além do problema com a autorização, há uma chance de que tx.origin seja removido do protocolo Ethereum no futuro, então o código que usa tx.origin não será compatível com versões futuras Vitalik: ‘NÃO presuma que tx.origin continuará a ser utilizável ou significativo’.

Também vale a pena mencionar que, ao usar tx.origin, você está limitando a interoperabilidade entre os contratos porque o contrato que usa tx.origin não pode ser usado por outro contrato, pois um contrato não pode ser o tx.origin.

Ver SWC-115

Dependência de carimbo de data / hora

Existem três considerações principais ao usar um carimbo de data / hora para executar uma função crítica em um contrato, especialmente quando as ações envolvem transferência de fundos.

Manipulação de carimbo de data / hora

Esteja ciente de que o carimbo de data / hora do bloco pode ser manipulado por um minerador. Considere isto contrato:

uint256 sal privado constante = block.timestamp; function random (uint Max) constante privado retorna (uint256 result) {// obter a melhor semente para aleatoriedade uint256 x = salt * 100 / Max; uint256 y = sal * block.number / (sal% 5); uint256 seed = block.number / 3 + (salt% 300) + Last_Payout + y; uint256 h = uint256 (block.blockhash (semente)); retornar uint256 ((h / x))% Máx + 1; // número aleatório entre 1 e Max} Linguagem de código: PHP (php)

Quando o contrato usa o carimbo de data / hora para semear um número aleatório, o minerador pode realmente postar um carimbo de data / hora dentro de 15 segundos após o bloco ser validado, permitindo que o minerador pré-calcule uma opção mais favorável às suas chances na loteria. Os carimbos de data / hora não são aleatórios e não devem ser usados ​​nesse contexto.

A regra dos 15 segundos

Papel Amarelo (Especificação de referência da Ethereum) não especifica uma restrição sobre a quantidade de blocos que podem derivar no tempo, mas especifica que cada carimbo de data / hora deve ser maior do que o carimbo de data / hora de seu pai. Implementações populares do protocolo Ethereum Geth e Paridade ambos rejeitam blocos com carimbo de data / hora com mais de 15 segundos no futuro. Portanto, uma boa regra para avaliar o uso de timestamp é: se a escala de seu evento dependente do tempo pode variar em 15 segundos e manter a integridade, é seguro usar um block.timestamp.

Evite usar block.number como um carimbo de data / hora

É possível estimar um delta de tempo usando a propriedade block.number e tempo médio de bloqueio, no entanto, isso não é compatível com o futuro, pois os tempos de bloqueio podem mudar (como reorganizações de garfo e a bomba de dificuldade) Em uma venda que abrange dias, a regra dos 15 segundos permite obter uma estimativa de tempo mais confiável.

Ver SWC-116

Cuidado com herança múltipla

Ao utilizar herança múltipla no Solidity, é importante entender como o compilador compõe o gráfico de herança.

contrato final {uint public a; função Final (uint f) public {a = f; }} o contrato B é Final {int public fee; função B (uint f) Final (f) public {} function setFee () public {fee = 3; }} o contrato C é Final {int public fee; função C (uint f) Final (f) public {} function setFee () public {fee = 5; }} o contrato A é B, C {função A () public B (3) C (5) {setFee (); }} Linguagem de código: PHP (php)

Quando um contrato é implantado, o compilador irá linearizar a herança da direita para a esquerda (depois que a palavra-chave for os pais são listados do mais básico ao mais derivado). Aqui está a linearização do contrato A:

Final <- B <- C <- UMA

A consequência da linearização renderá um valor de taxa de 5, uma vez que C é o contrato mais derivado. Isso pode parecer óbvio, mas imagine cenários onde C é capaz de ocultar funções cruciais, reordenar cláusulas booleanas e fazer com que o desenvolvedor escreva contratos exploráveis. A análise estática atualmente não levanta problemas com funções obscurecidas, portanto, deve ser inspecionada manualmente.

Para ajudar a contribuir, o Github da Solidity tem um projeto com todos os problemas relacionados à herança.

Ver SWC-125

Use o tipo de interface em vez do endereço para segurança de tipo

Quando uma função recebe um endereço de contrato como argumento, é melhor passar uma interface ou tipo de contrato em vez de endereço bruto. Se a função for chamada em outro lugar no código-fonte, o compilador fornecerá garantias adicionais de segurança de tipo.

Aqui vemos duas alternativas:

validador de contrato {função validar (uint) retornos externos (bool); } contrato TypeSafeAuction {// boa função validateBet (Validator _validator, uint _value) retornos internos (bool) {bool valid = _validator.validate (_value); retorno válido; }} contrato TypeUnsafeAuction {// função inválida validateBet (endereço _addr, uint _value) retornos internos (bool) {Validator validator = Validator (_addr); bool valid = validator.validate (_value); retorno válido; }} Linguagem de código: JavaScript (javascript)

Os benefícios de usar o contrato TypeSafeAuction acima podem ser vistos no exemplo a seguir. Se validateBet () for chamado com um argumento de endereço ou um tipo de contrato diferente de Validator, o compilador gerará este erro:

contrato NonValidator {} leilão de contrato é TypeSafeAuction {NonValidator nonValidator; função aposta (uint _value) {bool valid = validateBet (nonValidator, _value); // TypeError: Tipo inválido para argumento na chamada de função. // Conversão implícita inválida de NonValidator de contrato // para validador de contrato solicitado. }} Linguagem de código: JavaScript (javascript)

Evite usar extcodesize para verificar contas de propriedade externa

O seguinte modificador (ou uma verificação semelhante) é frequentemente usado para verificar se uma chamada foi feita de uma conta de propriedade externa (EOA) ou de uma conta de contrato:

// modificador incorreto isNotContract (address _a) {uint size; assembly {size: = extcodesize (_a)} require (size == 0); _; } Linguagem de código: JavaScript (javascript)

A ideia é direta: se um endereço contém código, não é um EOA, mas uma conta de contrato. Contudo, um contrato não tem código-fonte disponível durante a construção. Isso significa que, enquanto o construtor está em execução, ele pode fazer chamadas para outros contratos, mas extcodesize para seu endereço retorna zero. Abaixo está um exemplo mínimo que mostra como essa verificação pode ser contornada:

contrato OnlyForEOA {uint public flag; // modificador ruim isNotContract (address _a) {uint len; assembly {len: = extcodesize (_a)} require (len == 0); _; } função setFlag (uint i) public isNotContract (msg.sender) {flag = i; }} contrato FakeEOA {construtor (endereço _a) public {OnlyForEOA c = OnlyForEOA (_a); c.setFlag (1); }} Linguagem de código: JavaScript (javascript)

Como os endereços de contrato podem ser pré-calculados, esta verificação também pode falhar se verificar um endereço que está vazio no bloco n, mas que tem um contrato implantado nele em algum bloco maior que n.

Aviso: Este problema tem nuances. Se o seu objetivo é impedir que outros contratos cancelem o seu contrato, a verificação extcodesize provavelmente é suficiente. Uma abordagem alternativa é verificar o valor de (tx.origin == msg.sender), embora isso também tem desvantagens.

Pode haver outras situações em que a verificação extcodesize atenda ao seu propósito. Descrever todos eles aqui está fora do escopo. Entenda os comportamentos subjacentes do EVM e use seu julgamento.

Seu código de blockchain é seguro??

Reserve uma verificação no local de 1 dia com nossos especialistas em segurança. Reserve o seu hoje DiligenceSecuritySmart ContractsSolidityNewsletterSubscreva a nossa newsletter para obter as últimas notícias da Ethereum, soluções empresariais, recursos para desenvolvedores e muito mais. Endereço de e-mailConteúdo exclusivoComo construir um produto blockchain de sucessoWebinar

Como construir um produto blockchain de sucesso

Como configurar e executar um nó EthereumWebinar

Como configurar e executar um nó Ethereum

Como construir sua própria API EthereumWebinar

Como construir sua própria API Ethereum

Como criar um token socialWebinar

Como criar um token social

Usando ferramentas de segurança no desenvolvimento de contrato inteligenteWebinar

Usando ferramentas de segurança no desenvolvimento de contrato inteligente

O Futuro dos Ativos Digitais Financeiros e DeFiWebinar

O futuro das finanças: ativos digitais e DeFi