WEB3DEV

Cover image for Padrões de design para contratos inteligentes - Segurança
Diogo Jorge
Diogo Jorge

Posted on

Padrões de design para contratos inteligentes - Segurança

Image description

Prefácio

Esta é a seção final da série de 5 partes sobre como resolver falhas de projeto recorrentes por meio de padrões de projeto convencionais e reutilizáveis. Por meio disso, vamos dissecar em Security, um conjunto de padrões que introduzem medidas de segurança para mitigar danos e garantir uma execução confiável do contrato.

Verificações-Efeitos-Interação

Problema

Quando um contrato chama outro contrato, ele entrega o controle a esse outro contrato. O contrato chamado pode, por sua vez, reinserir o contrato pelo qual foi chamado e tentar

manipular seu estado ou sequestrar o fluxo de controle por meio de código malicioso.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

// ESTE CONTRATO CONTÉM UM BUG - NÃO USE
contract Fund {
/// Mapeamento @dev das partes ether do contrato.
   mapping(address => uint256) shares;

/// Saque a sua parte.
   function withdraw() public {
// o código do chamador é executado e pode reinserir a retirada novamente
       (bool success,) = msg.sender.call{value: shares[msg.sender]}("");

// INSEGURO - os compartilhamentos do usuário devem ser redefinidos antes da chamada externa
       if (success)
           shares[msg.sender] = 0;
   }
}
Enter fullscreen mode Exit fullscreen mode

A transferência de Ether sempre pode incluir a execução de código, portanto, o destinatário pode ser um contrato que chama de volta retirar. Isso permitiria obter vários reembolsos e, basicamente, recuperar todo o Ether do contrato.

Solução

O padrão verificação-efeitos-interação é fundamental para funções de codificação e descreve como o código de função deve ser estruturado para evitar efeitos colaterais e comportamento de execução indesejado.

O padrão verificação-efeitos-interação garante que todos os caminhos de código através de um contrato completem todas as verificações necessárias dos parâmetros fornecidos antes de modificar o estado do contrato (Checks), só então ele faz qualquer alteração no estado (Effects), pode fazer chamadas para funções em outros contratos depois que todas as alterações de estado planejadas sejam gravadas no armazenamento (interações).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract Fund {
/// Mapeamento @dev das partes ether do contrato.
   mapping(address => uint256) shares;

/// Saque sua parte.
   function withdraw() public {
       uint256 share = shares[msg.sender];
       // 1. Verificações
       require(share > 0);


       // 2. Efeitos
       shares[msg.sender] = 0;

       // 3. Interação
       payable(msg.sender).transfer(share);
   }
}
Enter fullscreen mode Exit fullscreen mode

O ataque de reentrância é especialmente prejudicial quando usa address.call de baixo nível, que encaminha todo o gás restante por padrão, dando ao contrato chamado mais espaço para ações potencialmente maliciosas. Portanto, o uso de address.call de baixo nível deve ser evitado sempre que possível.

Para envio de fundos address.send() e address.transfer() devem ser preferidos, essas funções minimizam o risco de reentrância por meio de encaminhamento de gás limitado (o contrato chamado recebe apenas um estipêndio de 2.300 gás, que atualmente é suficiente apenas para registrar um evento).

Parada de Emergência (Disjuntor)

Problema

Uma vez que um contrato implantado é executado de forma autônoma na rede Ethereum, não há opção de interromper sua execução em caso de um grande bug ou problema de segurança.

Solução

Uma contramedida e uma resposta rápida a ataques desconhecidos são paradas de emergência ou disjuntores. Eles interrompem a execução de um contrato ou de suas partes quando certas condições são atendidas.

Um cenário recomendado seria que, assim que um bug fosse detectado, todas as funções críticas seriam interrompidas, deixando apenas a possibilidade de retirar fundos.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Pausable {
   bool private paused;

   event Paused(address account);
   event Unpaused(address account);

   error EnforcedPause();
   error ExpectedPause();

   modifier whenNotPaused() {
       if (paused) {
           revert EnforcedPause();
       }
       _;
   }

   modifier whenPaused() {
       if (!paused) {
           revert ExpectedPause();
       }
       _;
   }

   constructor() {
       paused = false;
   }

   function _pause() internal virtual whenNotPaused {
       paused = true;
       emit Paused(msg.sender);
   }

   function _unpause() internal virtual whenPaused {
       paused = false;
       emit Unpaused(msg.sender);
   }
}
Enter fullscreen mode Exit fullscreen mode

Implemente a parada de emergência no contrato de Stake.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./Pausable.sol";

contract Staking is Pausable, Ownable {
   /** declarar variáveis de estado */


   function pause() external onlyOwner {
       _pause();
   }

   function unpause() external onlyOwner {
      _unpause();
   }

   function deposit() external payable whenNotPaused {
       // algum código
   }

   function withdraw() external whenNotPaused {
       // algum código
   }

   function emergencyWithdraw() external whenPaused {
       // algum código
   }
}
Enter fullscreen mode Exit fullscreen mode

Speedbump (Redutor de Velocidade)

Problema

A execução simultânea de tarefas delicadas por um grande número de partes pode acarretar a ruína de um contrato.

Solução

Tarefas sensíveis ao contrato são desaceleradas de propósito, portanto, quando ocorrem ações maliciosas, o dano é restrito e há mais tempo disponível para neutralizar.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract SpeedBump {
    struct Withdrawal {
        uint256 amount;
        uint256 requestedAt;
    }

    uint256 constant WAIT_PERIOD = 7 days;

    mapping (address => uint256) private balances;
    mapping (address => Withdrawal) private withdrawals;

   // O endereço de cada usuário pode depositar apenas 1 vez até ser totalmente retirado
    function deposit() public payable {
        bool hasDeposited = withdrawals[msg.sender].amount > 0;
        if(!hasDeposited)
            balances[msg.sender] += msg.value;
    }

    function requestWithdrawal() public {
        if (balances[msg.sender] > 0) {
            uint256 amountToWithdraw = balances[msg.sender];
            balances[msg.sender] = 0;

            withdrawals[msg.sender] = Withdrawal({
                amount: amountToWithdraw,
                requestedAt: block.timestamp
            });
        }
    }

   // Somente retirado totalmente quando o WAIT_PERIOD expirou
    function withdraw() public {
        if(withdrawals[msg.sender].amount > 0 && 
            block.timestamp > withdrawals[msg.sender].requestedAt + WAIT_PERIOD)
        {
            uint256 amount = withdrawals[msg.sender].amount;
            withdrawals[msg.sender].amount = 0;
            payable(msg.sender).transfer(amount);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Você pode fazer referência ao contrato TimelockController implementado pelo OpenZeppelin para a versão do contrato baseado em produção.Um timelock é um contrato inteligente que atrasa as chamadas de função de outro contrato inteligente após um período de tempo predeterminado. Os prazos são usados ​​principalmente no contexto da governança para adicionar um atraso nas ações administrativas e geralmente são considerados um forte indicador de que um projeto é legítimo e demonstra o compromisso com o projeto por parte dos proprietários do projeto.

Rate Limit (Taxa Limite)

Problema

Uma solicitação apressada em uma determinada tarefa não é desejada e pode prejudicar a correta execução operacional de um contrato.

Solução

Um limite de taxa regula a frequência com que uma função pode ser chamada consecutivamente dentro de um intervalo de tempo especificado.

Um cenário de uso para contratos inteligentes pode ser fundamentado em considerações operacionais, a fim de controlar o impacto do comportamento (coletivo) do usuário.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract RateLimit {
   uint enabledAt = block.timestamp;

   modifier enabledEvery(uint256 t) {
       if (block.timestamp >= enabledAt) {
           enabledAt = block.timestamp + t;
           _;
       }
   }

   function withdraw() public enabledEvery(1 minutes) {
       // algum código
   }
}
Enter fullscreen mode Exit fullscreen mode

O exemplo acima demonstra a limitação da taxa de execução de retirada de um contrato para evitar uma rápida drenagem de fundos.

mutex

Problema

Os ataques de reentrância podem manipular o estado de um contrato e sequestrar o fluxo de controle.

Solução

Um mutex (de exclusão mútua) é conhecido como um mecanismo de sincronização na ciência da computação para restringir o acesso simultâneo a um recurso. Após o surgimento de cenários de ataque de reentrância, esse padrão encontrou sua aplicação em contratos inteligentes para proteger contra chamadas de função recursivas de contratos externos.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

abstract contract ReentrancyGuard {
   uint256 private constant _NOT_ENTERED = 1;
   uint256 private constant _ENTERED = 2;

   uint256 private _status;

   error ReentrancyGuardReentrantCall();

   constructor() {
       _status = _NOT_ENTERED;
   }

   modifier nonReentrant() {
       _nonReentrantBefore();
       _;
       _nonReentrantAfter();
   }

   function _nonReentrantBefore() private {
       // Na primeira chamada para nonReentrant, _status será _NOT_ENTERED
       if (_status == _ENTERED) {
           revert ReentrancyGuardReentrantCall();
       }
       // Quaisquer chamadas para nonReentrant após este ponto falharão
       _status = _ENTERED;
   }

   function _nonReentrantAfter() private {
       // Ao armazenar novamente o valor original, um reembolso é acionado (consulte
       // https://eips.ethereum.org/EIPS/eip-2200)
       _status = _NOT_ENTERED;
   }
}

contract Mutex is ReentrancyGuard {
   /** declara variáveis de estado */

   // f é protegido por um mutex, portanto chamadas reentrantes
   // de dentro de msg.sender.call não pode chamar f novamente
   function f() external nonReentrant {
       // algum código
   }
}
Enter fullscreen mode Exit fullscreen mode

Limite de Saldo

Problema

Sempre existe o risco de um contrato ser comprometido devido a bugs no código ou ainda problemas de segurança desconhecidos na plataforma do contrato.

Solução

Geralmente, é uma boa ideia gerenciar a quantia de dinheiro em risco ao codificar contratos inteligentes. Isso pode ser alcançado limitando o saldo total mantido em um contrato.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract LimitBalance {
   uint256 public limit;

   modifier limitedPayable() {
       require(address(this).balance < limit);
       _;
   }

   constructor(uint256 value) {
       limit = value;
   }

   function deposit() external payable limitedPayable {
       // algum código
   }
}
Enter fullscreen mode Exit fullscreen mode

O padrão monitora o saldo do contrato e rejeita pagamentos enviados ao longo de uma chamada de função após exceder uma cota _limite _predefinido.

Deve-se notar que esta abordagem não pode impedir a admissão de Ether enviado à força, por exemplo, como beneficiário de uma chamada selfdestruct(endereço), ou como recipient (destinatário) de recompensas de deveres de validador.

Conclusão

Descrevi o grupo de padrões de segurança em detalhes e forneci um código como exemplo para melhor ilustrar. Eu recomendo que você use pelo menos um desses padrões em seu próximo projeto do Solidity para testar sua compreensão deste tópico.

Lembre-se de que, mesmo que seu código de contrato inteligente esteja livre de bugs, mesmo que você cumpra estritamente os padrões que mencionei, o compilador ou a própria plataforma pode ter um bug. Uma lista de alguns bugs relevantes à segurança conhecidos publicamente do compilador pode ser encontrada aqui e considerações de segurança esplêndidas podem ser encontradas aqui. Eu sugiro que dê uma olhada nesses documentos, encontre mais artigos e blogs para melhorar a segurança e é uma prática recomendada sempre pedir às pessoas que revisem seu código.

Siga-me no Linkedin para ficar conectado

https://www.linkedin.com/in/ninh-kim-927571149/

Este artigo foi escrito por BrianKim e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.

Latest comments (0)