Padrões de Desenvolvimento
No desenvolvimento de contratos inteligentes com Solidity, a aplicação de padrões de design comprovados pode melhorar significativamente a segurança, eficiência e manutenção do código. Esses padrões ajudam a resolver problemas recorrentes no desenvolvimento de software e são especialmente importantes no Ethereum, onde erros podem ser custosos e a otimização de gas é essencial. A seguir, apresentamos alguns padrões de design importantes em Solidity:
1. Padrão de Verificação-Efeitos-Interação (Checks-Effects-Interactions)
Este padrão ajuda a prevenir ataques de reentrância ao garantir que chamadas a contratos externos sejam realizadas apenas no final de uma função. Sua estrutura segue três etapas:
Verificação: Primeiro, valide todas as condições e entradas da função.
Efeitos: Em seguida, atualize o estado do contrato.
Interação: Por último, realize interações com outros contratos.
Exemplo deste padrão:
2. Padrão de Retirada (Withdrawal Pattern)
Em vez de enviar Ether diretamente aos usuários (por exemplo, com send
ou transfer
), este padrão permite que os usuários retirem os fundos por conta própria. Isso reduz o risco de erros e ataques de reentrância.
3. Padrão de Acesso Restrito (Access Restriction)
Este padrão é utilizado para restringir o acesso a determinadas funções do contrato a usuários específicos. É comumente implementado por meio de modificadores de função.
Da mesma forma que, neste caso, o acesso é restrito para que apenas o proprietário (Owner) possa acessar a função restrita, é possível criar modificadores para outros perfis, como administradores ou pessoas autorizadas.
4. Padrão de Parada de Emergência (Emergency Stop)
Este padrão, também conhecido como "Circuit Breaker", permite pausar a execução de certas funções críticas em caso de emergência ou quando se detecta um comportamento anômalo. Este padrão é particularmente útil para prevenir danos maiores, como a perda de fundos ou a exploração de vulnerabilidades, até que o problema possa ser investigado e resolvido.
Para implementar esse padrão, normalmente se utiliza uma variável de estado que indica se o contrato está no modo "pausado" ou não. As funções que podem ser vulneráveis a ataques ou falhas são modificadas para que sua execução dependa do estado dessa variável. Além disso, implementam-se funções para alterar o estado dessa variável, permitindo ativar ou desativar o "modo de emergência". Essas funções de controle devem ser restritas a endereços autorizados para evitar o mau uso.
A seguir, um exemplo simples de como implementá-lo:
Considerações:
Autorização: É crucial restringir quem pode ativar ou desativar o modo de emergência, geralmente o proprietário do contrato ou um conjunto de endereços confiáveis.
Transparência: A capacidade de colocar o contrato em modo de emergência deve ser comunicada claramente aos usuários, explicando em quais circunstâncias isso será utilizado.
Recuperação: Deve haver um plano claro sobre como os problemas que levaram à ativação do modo de emergência serão resolvidos e como a normalidade será retomada.
5. Padrão de Fábrica de Contratos (Factory Pattern)
Este padrão permite a criação de novos contratos a partir de outro contrato. Ele é especialmente útil quando é necessário criar várias instâncias de um contrato com configurações semelhantes ou quando se deseja centralizar a lógica de criação de contratos para facilitar a manutenção e a atualização. A "fábrica" atua como um criador centralizado que pode gerar instâncias de contratos sob demanda. A seguir, é mostrado um exemplo simples de como implementar o padrão Factory Contract. Neste exemplo, o contrato ChildContract será o contrato que a fábrica produz, e o FactoryContract atuará como a fábrica que cria instâncias do ChildContract.
Contrato Filho Primeiro, definimos o contrato que queremos produzir. Este contrato pode ser qualquer coisa, mas, para fins deste exemplo, será um contrato simples que armazena um número.
Contrato Fábrica
O contrato fábrica será responsável por criar novas instâncias do contrato filho. Ele manterá um registro de todas as instâncias criadas para poder interagir com elas posteriormente, caso necessário.
Neste exemplo, o FactoryContract tem uma função createChild que implanta uma nova instância do ChildContract com um número específico. Cada nova instância do ChildContract é armazenada no array children, e um evento ChildCreated é emitido, registrando o número atribuído e o endereço do novo contrato filho.
Este padrão é poderoso porque permite a criação de contratos de maneira programática, o que facilita a gestão de múltiplas instâncias de contratos e a centralização da lógica de criação de contratos. Além disso, ao manter um registro de todos os contratos criados, o contrato fábrica pode interagir com eles ou fornecer funcionalidades adicionais, como a gestão ou atualização dos contratos filhos.
6. Padrão de Máquina de Estados (State Machine)
Este padrão é uma forma eficaz de gerenciar o ciclo de vida de um contrato, modelando explicitamente os diferentes estados pelos quais um contrato pode passar e as transições permitidas entre esses estados. Este padrão é particularmente útil para contratos com lógicas de negócios complexas que precisam lidar com diferentes fases ou etapas de maneira clara e segura, como contratos de votação, leilões ou crowdfunding.
Implementação básica do padrão State Machine
Para implementar uma máquina de estados em um contrato inteligente, você pode seguir estes passos:
Definir os estados: Use um
enum
para definir todos os possíveis estados do contrato.Armazenar o estado atual: Use uma variável para armazenar o estado atual do contrato.
Restringir funções a estados específicos: Use modificadores para permitir que certas funções sejam executadas apenas em estados específicos.
Alterar de estado: Implemente funções que alterem o estado do contrato de maneira controlada.
Veja a seguir um exemplo simplificado de um contrato de leilão que utiliza o padrão State Machine:
Neste contrato de leilão, existem três estados definidos: AcceptingBlindBids (aceitando propostas cegas), RevealBids (revelando propostas) e Finished (finalizado). O contrato começa no estado AcceptingBlindBids e avança sequencialmente pelos estados com base no tempo (a cada dia, avança para o próximo estado). São usados modificadores para garantir que certas funções só possam ser executadas em seus respectivos estados, e há uma função nextStage que gerencia a transição entre estados.
Benefícios do padrão Máquina de Estados:
Clareza: Facilita a compreensão do fluxo lógico do contrato e suas diferentes fases.
Segurança: Ajuda a prevenir a execução não autorizada de funções e garante que o contrato execute apenas operações válidas em seu estado atual.
Flexibilidade: Permite gerenciar complexidades em contratos com múltiplas fases ou etapas de maneira estruturada.
7. Patrón para obtener datos de un oráculo
O uso de oráculos em contratos inteligentes do Ethereum permite acessar dados externos à blockchain, o que é essencial para muitas aplicações descentralizadas (DApps) que precisam interagir com o mundo real. No entanto, o Ethereum e outras blockchains semelhantes não podem acessar diretamente dados externos devido à sua natureza isolada e determinista. É aqui que entram os oráculos, atuando como intermediários que trazem informações do exterior para a blockchain.
Um oráculo é um serviço (geralmente operado por um terceiro) que envia dados do mundo real para a blockchain. Esses dados podem ser qualquer coisa, desde preços de ativos e resultados esportivos até a temperatura de uma cidade. Os oráculos desempenham um papel crucial no funcionamento de muitos tipos de DApps, como as financeiras (DeFi), as de seguros e as de jogos de azar, entre outras.
Implementação Básica A implementação de um oráculo em Solidity geralmente segue esses passos:
Solicitação de Dados: O contrato inteligente envia uma solicitação de dados para o oráculo. Essa solicitação pode ser o resultado de uma ação do usuário ou de outra função do contrato.
Obtenção de Dados: O oráculo recebe a solicitação, obtém os dados necessários do mundo real e os envia de volta para a blockchain.
Processamento de Dados: O contrato inteligente recebe os dados e executa a lógica correspondente com essas informações.
Exemplo com Chainlink Chainlink é um dos serviços de oráculo mais populares e amplamente utilizados no ecossistema Ethereum. A seguir, é mostrado um exemplo básico de como um contrato inteligente pode interagir com o Chainlink para obter o preço do ETH em USD.
Primeiro, certifique-se de ter importado as dependências do Chainlink no seu arquivo Solidity, o que geralmente é feito por meio de importações de NPM ou diretamente com URLs.
Neste exemplo, o contrato PriceConsumerV3 utiliza a interface AggregatorV3Interface do Chainlink para interagir com um contrato oráculo do Chainlink que fornece o preço atual do ETH em USD. A função getLatestPrice consulta o último dado de preço disponível e o retorna.
Ao trabalhar com oráculos, é crucial considerar a confiabilidade e a segurança do provedor de dados. Dependendo de um único oráculo, pode-se introduzir um ponto de falha centralizado. Para mitigar isso, algumas aplicações utilizam múltiplos oráculos ou serviços de oráculos descentralizados como o Chainlink, que agregam dados de várias fontes para fornecer uma medida mais confiável e resistente à manipulação.
8. Padrão de Contratos Atualizáveis (Upgradeable Contracts)
Este padrão, comumente conhecido como o padrão 'Proxy' ou 'Contratos Atualizáveis', é uma técnica avançada que permite aos desenvolvedores alterar o código de um contrato inteligente após ele ter sido implantado na blockchain. Dado que o código de um contrato inteligente é imutável após a implantação, esse padrão oferece uma forma flexível de atualizar a lógica do contrato sem perder o estado ou os dados armazenados, nem mudar o endereço do contrato.
Como Funciona o Padrão Proxy
O padrão se baseia em dois componentes principais: o contrato Proxy e o contrato de Implementação (ou lógica).
Contrato Proxy: É o contrato com o qual os usuários interagem diretamente. Ele mantém o estado do contrato e delega chamadas a um contrato de implementação que contém a lógica do negócio. O endereço do contrato proxy permanece constante, mesmo durante as atualizações.
Contrato de Implementação: Contém a lógica do negócio e o código que pode ser atualizado. Quando a lógica do contrato é atualizada, um novo contrato de implementação é implantado, e o proxy é atualizado para apontar para o novo endereço.
Neste exemplo, o Proxy delega todas as chamadas para o LogicContractV1. Se quisermos atualizar a lógica, implantaríamos um novo contrato de implementação (por exemplo, LogicContractV2) e, em seguida, chamaríamos o método upgrade
no Proxy para mudar o endereço da implementação.
Considerações de Segurança:
Armazenamento consistente: É crucial garantir que a estrutura de armazenamento permaneça consistente entre as versões dos contratos de implementação para evitar problemas de corrupção de dados.
Transparência das atualizações: As atualizações devem ser feitas com cautela para manter a confiança dos usuários, idealmente por meio de um processo de governança claro ou um período de aviso antes de realizar mudanças significativas.
Controle de acesso: O contrato Proxy deve ter controles de acesso robustos para garantir que apenas entidades autorizadas possam atualizar o contrato de implementação.
9. Padrão de Mapa Iterável
Implementar um mapa iterável em Solidity permite combinar as vantagens dos mapas (acesso rápido aos dados por chave) com as dos arrays (capacidade de iterar sobre os elementos). Como o Solidity não oferece nativamente uma estrutura de dados que seja ao mesmo tempo um mapa e permita a iteração sobre seus elementos, é necessário projetar uma estrutura que atenda a ambos os requisitos.
A seguir, veremos uma implementação detalhada do padrão de mapa iterável, o qual é composto por um mapa para armazenar os dados e um array para manter a ordem das chaves e permitir a iteração.
Implementação
Características e considerações
Flexibilidade: Este padrão permite tanto a recuperação de valores por chave quanto a iteração sobre todas as entradas.
Gestão de estado: A consistência entre o mapa e o array é mantida, garantindo que as operações de adicionar, atualizar e remover se reflitam em ambos.
Eficiência de Gas: A remoção e adição de elementos, especialmente em um conjunto grande, podem consumir uma quantidade significativa de gas devido às operações sobre o array. A eficiência deve ser considerada ao projetar o contrato, especialmente para operações que modificam o array.
Este padrão é especialmente útil em situações onde é necessário tanto um acesso rápido aos dados por meio de chaves quanto a capacidade de iterar sobre todas as entradas armazenadas.
10. Padrão de Lista de Endereços
Este padrão é utilizado para manter uma lista curada de endereços pelo proprietário. Isso é necessário, por exemplo, quando se precisa de uma lista de endereços em uma whitelist (lista de permissões) que estejam autorizados a executar determinadas funções em um contrato. Neste padrão, apenas o proprietário do contrato pode adicionar e remover endereços da lista.
Vamos ver um exemplo.
11. Padrão de Comparação de Strings
Em Solidity, as strings são um tipo especial de dado que representa sequências de caracteres. No entanto, Solidity não oferece uma maneira direta de comparar strings devido à sua natureza de baixo nível e ao foco na eficiência de gas. Comparar strings envolve comparar o conteúdo dos dados, em vez das localizações de memória, o que exige uma lógica específica dada a forma como os strings são armazenados na EVM (Ethereum Virtual Machine).
Uma maneira comum de comparar strings em Solidity é convertê-las primeiro em bytes e depois comparar esses bytes usando o algoritmo de hash keccak256
. Se duas strings forem idênticas, seus hashes keccak256
também serão idênticos. Este método é eficiente e eficaz para a comparação de strings em contratos inteligentes.
A seguir, mostramos como implementar a comparação de strings usando keccak256
:
abi.encodePacked(...)
: Esta função recebe qualquer número de argumentos de qualquer tipo e os codifica em um único conjunto contínuo debytes
. É usada aqui para converter as strings embytes
.keccak256(...)
: Calcula o hash KECCAK-256 dos dados. Neste contexto, é usada para obter o hash dos bytes resultantes de cadastring
.==
: Compara os hashes das duas strings. Se as strings forem iguais, seus hashes também serão, resultando emtrue
. Caso contrário, o resultado seráfalse
.
Considerações
Eficiência de Gas: Usar keccak256 para comparar strings geralmente é eficiente em termos de gas, especialmente comparado com métodos que implicariam iterar sobre cada caractere das strings.
Colisões: Teoricamente, as funções hash podem ter colisões (entradas diferentes gerando o mesmo hash), embora a probabilidade disso ocorrer seja extremamente baixa com keccak256.
Limitações: Este método compara o conteúdo completo das strings. Se você precisar realizar comparações mais complexas (como verificações de prefixo ou padrões), será necessário uma lógica adicional."
Last updated