WEB3DEV

Cover image for Padrões de design para contratos inteligentes
Adriano P. Araujo
Adriano P. Araujo

Posted on

Padrões de design para contratos inteligentes

Padrões de design de contratos inteligentes são essenciais para criar aplicativos de blockchain seguros e eficientes. Seguindo os padrões de design estabelecidos, os desenvolvedores podem garantir que seus contratos inteligentes sejam confiáveis, escaláveis e fáceis de manter.

Neste artigo, exploraremos alguns padrões comuns de design de contratos inteligentes e forneceremos exemplos de suas implementações. Compreender padrões de design de contratos inteligentes é crucial para criar aplicativos descentralizados bem-sucedidos, seja você um  desenvolvedor  experiente ou apenas um iniciante no desenvolvimento blockchain.

Padrões comuns de design

  1. Padrão de fábrica (Factory Pattern) — Um contrato inteligente que cria e implanta outros contratos inteligentes.

  2. Padrão Singleton (Singleton Pattern) — Esse padrão de design garante que uma entidade possa ser instanciada apenas uma vez.

  3. Padrão da máquina de estado (State Machine Pattern)  — Um contrato inteligente que representa uma máquina de estados com um conjunto finito de estados e transições entre eles.

  4. Padrão de Oráculo (Oracle Pattern) — Um contrato inteligente que fornece dados de uma fonte externa.

  5. Padrão de proxy (Proxy Pattern) — Um contrato inteligente que permite atualizações e manutenção — dentro de certos parâmetros — para outro contrato, sem afetar seu estado e fornecer uma interface para seu acesso.

  6. Padrão de verificação de efeitos-interações (Check-Effects-Interactions Pattern) — Esse padrão de design determina a ordem em que certos tipos de instruções devem ser chamados nas funções do contrato inteligente.

Padrão de fábrica

Aqui está um exemplo do padrão de fábrica em um contrato inteligente:


pragma solidity 0.8.4;

Enter fullscreen mode Exit fullscreen mode

contract MyContract {

   uint256 public data;

   constructor(uint256 _data) {

       data = _data;

   }

}

contract MyContractFactory {

   mapping(address => address) public myContracts;

   function createMyContract(uint256 _data) public {

       address newContract = address(new MyContract(_data));

       myContracts[msg.sender] = newContract;

   }

}

Enter fullscreen mode Exit fullscreen mode

Neste exemplo, MyContract é o contrato com a funcionalidade desejada e MyContractFactory é o contrato de fábrica que cria as instâncias de MyContract. Quando createMyContract é chamado com um parâmetro _data, ele cria uma nova instância de MyContract com esses dados e os atribui ao endereço do chamador no mapeamento do myContracts.

Se você deseja criar várias instâncias do mesmo contrato e está procurando uma maneira de acompanhá-las e facilitar o gerenciamento, esse é um padrão útil. Nesse caso, cada instância é monitorada de acordo com o chamador do  createMyContract.

A desvantagem aqui é que qualquer endereço único pode ter, no máximo, uma instância registrada no estado do contrato de fábrica, embora todas as instâncias continuem a existir na blockchain e possam ser eventualmente rastreadas.

Padrão Singleton

O desenvolvimento clássico de software determina que o padrão Singleton garante que apenas uma instância de uma classe existirá e fornecerá um ponto de acesso global a ela. Em Solidity, isso não é realmente possível como é em outras linguagens.

Como a blockchain é pública, ninguém pode realmente controlar quem implanta o quê, bastando apenas ter os fundos necessários. Isso significa que não há uma maneira infalível de garantir que um contrato inteligente específico (que signifique uma sequência exata e específica de bytecode) não seja implantado n vezes.

A coisa mais próxima disso é a EIP-2470, que não deve ser usada, pois está oficialmente marcada como estagnada. O que ela visa fazer, em vez de ter um contrato implantado apenas uma vez por cadeia (que não é realmente possível, como mencionado), é ter o mesmo endereço para um contrato em qualquer cadeia. Em outras palavras, ela tenta tornar o endereço de um contrato — conhecido por um cliente — previsível em qualquer rede.

Padrão da máquina do estado

O Padrão da máquina de estado modela o comportamento com base no estado de um objeto, entidade ou, nesse caso, um contrato inteligente.

Vejamos um exemplo simplificado de sua implementação em Solidity.


pragma solidity 0.8.4;

Enter fullscreen mode Exit fullscreen mode

contract VoteTally {

    enum State { Created, InProgress, Completed }

    address public owner;

    State public state;

    uint256 public votes;

    constructor() {

        owner = msg.sender;

        state = State.Created;

    }

    function start() public {

        require(msg.sender == owner, "must be owner to start");

        require(state == State.Created);

        state = State.InProgress;

    }

    function vote() external {

        require(state == State.InProgress, "must be in progress to vote");

        votes++;

    }

    function finish() public {

        require(msg.sender == owner, "must be owner to finish");

        require(state == State.InProgress, "must be in progress to finish");

        state = State.Completed;

    }

}

Enter fullscreen mode Exit fullscreen mode

Este contrato implementa uma contagem direta de votos. Possui 3 estados:

  1. Criado(Created), que é o seu estado padrão.

  2. Em andamento(In progress), que é o único estado em que os votos — lançados publicamente — podem ser enviados.

  3. Acabado(Finished), que é seu estado final, simbolizando o encerramento da votação.

Somente o proprietário (que é a conta do implantador) pode alterar o estado do contrato. Nesse caso em particular, o contrato não pode voltar para um de seus estados anteriores após ter passado para um novo.

Outros estados arbitrários e suas transições podem ser modelados após diferentes quantidades de enumerações, bem como privilégios.

Padrão de Oráculo

Em algumas ocasiões, existem dados que não podem ser deduzidos, calculados ou obtidos diretamente da blockchain. Em outros, é uma boa ideia em termos de segurança não deduzir esses dados na cadeia. Nesses casos, o padrão de Oráculo pode ser aplicado.

Esse padrão é baseado na delegação da fonte de algumas informações ou dados específicos a um contrato inteligente, que é confiável pelos chamadores e que pode recuperar e definir arbitrariamente informações de fontes fora da cadeia.

Normalmente, os Oráculos fornecerão algum tipo de prova fora da cadeia de sua autoridade ou confiança, mas, em última análise, cabe aos chamadores confiar neles ou não. Alguns exemplos de oráculos populares são ChainLink, Witnet e UMA.

Aqui está um exemplo simples de como isso pode ser implementado no Solidity:


pragma solidity 0.8.4;

Enter fullscreen mode Exit fullscreen mode

interface IOracle {

   function getPrice() external view returns (uint);

}

contract MyContract {

   IOracle private oracle;

   constructor(address oracleAddress) {

       oracle = IOracle(oracleAddress);

   }

   function doSomething() external {

       uint price = oracle.getPrice();

       // Faz alguma coisa com price (preço)

   }

}

contract MyOracle is IOracle {

   address public admin;

   uint256 private price;

   constructor(){

       admin = msg.sender;

   }

   function getPrice() external view override returns (uint) {

       return price;

   }

   function setPrice(uint256 _price) external {

       require(msg.sender == admin, "only admin can set price");

       // define um preço arbitrário, mas de fonte confiável representado pelo endereço admin

       price = _price;

   }

}

Enter fullscreen mode Exit fullscreen mode

Neste exemplo, a conta do implantador do contrato do oráculo é a conta que pode definir arbitrariamente a variável price. Então, contratos como MyContract depositam sua confiança no contrato do oráculo e usa qualquer valor que tenha sido definido para essa variável.

Padrão de proxy

Contratos inteligentes são imutáveis, mas a qualidade do software geralmente requer atualizações de código-fonte para produzir versões melhores. Embora a tecnologia blockchain se beneficie da imutabilidade, é necessário algum nível de mutabilidade para corrigir erros e melhorar produtos.

O padrão de proxy em Solidity permite que os desenvolvedores modifiquem contratos inteligentes sem comprometer a integridade da rede blockchain. Um contrato de proxy serve como intermediário entre os usuários e o contrato real, permitindo atualizações sem interromper o endereço do contrato existente.

Existe uma complexidade técnica significativa em termos de implementação de tal padrão no desenvolvimento  blockchain. Portanto, o uso de soluções comprovadas e amplamente adotadas é recomendado aqui, em vez de codificar sua própria implementação.

As atualizações do OpenZeppelin fornecem um “mecanismo de atualização fácil de usar, simples, robusto e opcional para contratos inteligentes que podem ser controlados por qualquer tipo de governança, seja uma carteira multi-sig, um endereço simples ou uma DAO complexa“.

Sua solução é amplamente utilizada e sua documentação aprofundada está disponível em seu site de Padrões de Atualização de Proxy.

Verificações-efeitos-interações

Esse padrão realiza verificações primeiro (requisições, permissões), depois atualiza as variáveis de estado para o contrato (como um saldo) e finalmente interage com elementos externos como outros contratos.

Aqui está um exemplo:


function withdraw() public {

   // primeiro, verificações

   require(balances[msg.sender] > 0, "insufficient balance");

   // segundo, efeitos

   balances[msg.sender] = 0;

   // por último, interações

   (bool success, ) = msg.sender.call{value: balances[msg.sender]}("");

   require(success, "transfer failed");

}

Enter fullscreen mode Exit fullscreen mode

O objetivo desse padrão é evitar vulnerabilidades de reentrância. Você pode encontrar mais informações sobre esse padrão, vetor de ataque e como mitigar seu impacto em nosso artigo Prevenção de ataques de reentrância em Solidity.

Melhores práticas para implementar padrões

  1. Use padrões estabelecidos: não reinvente a roda. Padrões de design estabelecidos que foram testados e comprovados para funcionar geralmente devem ser favorecidos em relação a outro código.

  2. Mantenha-o simples: não complique demais o seu contrato inteligente. Mantenha-o simples e fácil de ler.

  3. Teste seu código: sempre teste seu código completamente para garantir que ele funcione como pretendido. Isso inclui testes unitários ou de integração, bem como testes de usuário final.

  4. Use práticas de codificação seguras: siga as práticas de codificação seguras para garantir que seu contrato inteligente não seja vulnerável a ataques. Auditorias de segurança e ferramentas de análise estática, como o Slither, serão úteis para isso.

  5. Documente seu código: documentar seu código facilitará a compreensão e a colaboração de outros desenvolvedores.

Conclusão

Em resumo, os padrões de design são ferramentas úteis para desenvolvedores de blockchain que pretendem escrever código de contrato inteligente. Usando padrões estabelecidos e seguindo as melhores práticas, os desenvolvedores podem criar contratos inteligentes seguros e eficientes que automatizam os processos de negócios e reduzem custos. Seja você um iniciante ou um desenvolvedor experiente em blockchain, entender os padrões de design é crucial para o sucesso no desenvolvimento inteligente de contratos.


Este artigo foi escrito por Infuy e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Oldest comments (0)