20 de Julho de 2022
Foto por Olav Ahrens Rotne no Unsplash
Por favor, note que: daqui em diante, eu migrei para o Hashnode como meu blog de tecnologia oficial e publico semanalmente. Se você quiser mais desse conteúdo sobre todas as coisas da Web3, sinta-se à vontade para se inscrever no meu blog Bored on The Edge.
Por favor, note que: daqui em diante, eu migrei para o Hashnode como meu blog de tecnologia oficial e publico semanalmente. Se você quiser mais desse conteúdo sobre todas as coisas da Web3, sinta-se à vontade para se inscrever no meu blog Bored on The Edge.
Faz um bom tempo desde que postei a última vez no Medium. Se você vem me seguindo no Dev.to ou Hashnode, este é o quarto artigo na minha série sobre contratos inteligentes atualizáveis. Já escrevemos sobre contratos inteligentes atualizáveis, testando-os e, aí então, implantando a versão 1 seguida da versão 2 na rede de teste Mumbai da Polygon. Você pode encontrá-los em qualquer um dos blogs de tecnologia mencionados acima.
Embora o ciclo de desenvolvimento esteja completo neste ponto, ainda há coisas a serem discutidas. Mais especificamente, pontos a serem observados de todos esses exercícios e recapitulá-los todos de uma só vez. Este artigo terá como objetivo fazer isso. Passaremos pela maioria das coisas que é preciso lembrar ao desenvolver um contrato inteligente atualizável. Lembre-se de que o paradigma da “atualização” como um todo é comparativamente novo, então esses pontos ainda não são fatos. Se você acha que há pontos que eu possa ter deixado passar ou que gostaria de iniciar uma discussão sobre o contrário, sinta-se à vontade para deixar um comentário.
UUPS vs Transparent
Os padrões UUPS e Transparent são dois dos métodos de atualização de contratos inteligentes. Eles foram adotados pelos EIPs 1538 e 1822, respectivamente. O Openzeppelin oferece ambas opções em contratos inteligentes atualizáveis. Mas, com o tempo, o 1822, ou o padrão UPPS, cresceu até ser adotado pela comunidade (e com boas razões).
Se você deseja entender a diferença entre os dois, então considere isso - ambos padrões têm um proxy na frente de outro contrato. Porém, enquanto o padrão Transparent está alterando o proxy para cada atualização, o UUPS altera a implementação registrada e mantém o proxy do contrato constante.
Embora o proxy do padrão Transparent consuma menos gás para implantação, o UUPS provou ser mais eficiente em termos de gás a longo prazo. É também mais fácil de desenvolver ambos contratos inteligentes e front-end. Se você está considerando contratos inteligentes atualizáveis, o proxy do padrão UUPS é, na minha opinião, o caminho.
Entenda a fase de inicialização
A fase de inicialização de um contrato inteligente atualizável é uma das fases mais importantes. Se não for tratada corretamente, pode transformar um contrato inteligente com implementação de lógica de negócios perfeita em um brinquedo de hacker.
Várias vezes nesta série, eu enfatizei que a fase de inicialização de um contrato inteligente atualizável é diferente de um normal. Não existem construtores na maioria dos contratos inteligentes atualizáveis por causa do design do proxy.
A implementação não pode armazenar nenhum dado. É apenas pura lógica de negócios. É por isso que a maior parte das coisas que vão para um construtor de um contrato normal precisa ir para a função especial “inicializadora”. Mas, ao contrário do construtor, que só é chamado durante a implantação do contrato inteligente, a função inicializadora precisa de uma camada extra de segurança, porque, em seu núcleo, é apenas mais uma função normal de contrato inteligente. Daí toda a confusão.
Inicializador
A maioria dos contratos inteligentes usa o Openzeppelin/contracts. Aqueles desenvolvedores que se interessam por contratos inteligentes atualizáveis usam seu projeto irmão Openzeppelin/contracts-upgradeable. As duas são iguais, embora o construtor no contrato seja substituído pelo inicializador.
O Initializable.sol do Openzeppelin/contract-upgradeable é um contrato inteligente que eu recomendaria a todos os desenvolvedores de contrato inteligente a entender em profundidade. Contém, basicamente, toda a lógica necessária para a inicialização de contratos inteligentes atualizáveis. O modificador inicializador é aquele que discutiremos nesta subseção.
Se você quiser, pode simplesmente ir em frente e implementar a mesma coisa com uma variável booleana simples. Essa variável será definida como verdadeira quando o contrato inteligente atualizável for implantado. A lógica é a função inicializadora somente poder ser invocada se essa variável Booleana for falsa. E é isso. Isso pode realmente evitar um ataque de sequestro de nicho em seu contrato inteligente atualizável.
O modificador fornecido pelo Openzeppelin, ainda assim, vai além do seu dever. Ele verifica o contrato que está sendo implantado e fornece uma funcionalidade especial - invocando funções apenas DURANTE a fase de inicialização.
A função inicializadora no seu contrato é onde você inicializa o método _init() de outro contrato atualizável, como diz o __UUPSUpgradeable_init(). Tudo o que você precisa para proteger seu inicializador é usar o modificador inicializador do Initializable.sol do Openzeppelin.
Reinicializador
Um reinicializador é invocado durante as atualizações. Este é o inicializador para suas próximas versões. O modificador inicializador só pode ser usado uma vez durante toda a vida útil do seu contrato em todas as versões. Então, o que acontece se você quiser adicionar uma funcionalidade específica na segunda função?
É aí que entra o modificador reinicializador (versão do tipo inteiro). Ele também é fornecido pelo Initializable.sol e pode ser considerado uma versão generalizada do inicializador. Na verdade, passar a versão número 1 no modificador é o mesmo que usar o modificador inicializador.
Mantenha em mente que isso é apenas incremental. Você pode tentar usar o reinicializador(2) depois de usar o reinicializador(3) e não vai funcionar. Isso ocorre porque, ao herdar o Initializable.sol, você também herda o mecanismo de atualização incremental e as verificações associadas a isso. Portanto, se você quiser reverter, precisará alterar o registro do contrato de implementação do proxy ou implantar o contrato de versão mais antiga como a próxima versão.
Além disso, você precisa observar que, ao invocar o reinicializador, seu contrato passará por uma fase de “inicialização” e isso também é fornecido por esse modificador.
Desativar inicializador
A função _disableInitializer() impede qualquer inicialização adicional. Lembre-se de que, ao atualizar seu contrato, você precisa seguir uma convenção comum de herdar a versão anterior e então adicionar quaisquer modificações. Não é garantido que você precisará inicializar algo todas as vezes. Portanto, você não deve confundir isso como prevenção para qualquer atualização.
Convencionalmente, esta função é usada dentro do construtor do contrato de implementação. Essa linha toca um alarme? Afinal, contratos inteligentes atualizáveis não podem ter construtores, certo?
O problema é que os contratos inteligentes atualizáveis não podem ter nenhum construtor que inicialize nenhum dado variável. Você pode ter um construtor que inicializa um imutável (as constantes são inicializadas em linha). Isso é bem inseguro, então você precisa do
/// @custom:oz-upgrades-unsafe-allow constructor
Nesse ponto, você também precisa notar que, ao herdar uma versão mais antiga e então criar uma nova versão, todas as funções do inicializador nas versões mais antigas serão executadas durante a implantação do contrato. Um jeito rápido de testar isso é colocar eventos dentro das funções do inicializador sobre as versões e, em seguida, verificar os eventos que foram emitidos logo após a implantação do novo contrato de implementação. Discutiremos isso em uma seção mais posterior.
Isso significa que, se você tiver usado a função _disableInitializer() em qualquer uma das versões anteriores, o estado de armazenamento será inicializado para aquela versão específica no contrato de implementação atual. Use-o com sabedoria.
EIP de armazenamento Transparent
O EIP-1967 é o que trouxe o padrão de armazenamento Transparent para o ecossistema. Até agora você deve estar ciente de que, embora possa atualizar seu contrato de implementação, o espaço de armazenamento compartilhado por todas as versões permanece o mesmo.
É como o backup dos discos de armazenamento anexados à nuvem de VMs (máquinas virtuais). O espaço de armazenamento é alterado de forma incremental à medida que as versões são implantadas. Isso significa que se você tinha um certo número de variáveis já declaradas em versões anteriores, o espaço persistirá na próxima versão também e você precisa definir essas variáveis exatas na mesma ordem nas versões mais recentes.
Embora não seja obrigatório entender o EIP completo, recomendaria passar por ele pelo menos uma vez porque pode influenciar imensamente em como você projeta seus contratos inteligentes.
Herdar dos teus ancestrais
Isso nos leva ao ponto atual - por que você deve praticar a herança em contratos inteligentes. A resposta é bastante simples - porque você precisa declarar as variáveis na versão anterior exatamente do mesmo jeito antes de declarar as novas.
Às vezes pode parecer simples apenas declarar essas variáveis e pronto. Isso pode ser uma nuance se você estiver herdado de um novo contrato na versão 2 (por exemplo, para adicionar funcionalidades). Isso não é tão incomum. Na contrapartida não atualizável, você precisaria herdar do contrato da Chainlink para usar seus serviços de oráculo.
A razão pela qual é complicado é que o espaço é alocado primeiro para os contratos herdados e depois para quaisquer outras variáveis em seu contrato principal. Portanto, embora você possa pensar que ainda está declarando as variáveis na mesma ordem da versão anterior, na verdade, você está re-declarando novas variáveis e as variáveis do contrato herdado sobrepostas ao espaço de armazenamento variável do contrato antigo.
É por isso que herdar a versão anterior é a coisa mais fácil de fazer. Lembre-se de que é recomendável herdar a versão anterior primeiro, seguida de qualquer outro(s) contrato(s). Caso contrário, poderá levar a problemas na árvore de herança (como linhagem). Além disso, qualquer funcionalidade na versão anterior é herdada na nova versão. Isso inclui qualquer funcionalidade do contrato Initialization.sol ou contratos OwnableUpgradable.sol.
Uma mistura saudável de Virtual e Sobreposição
A capacidade de atualização não visa apenas introduzir novas funcionalidades no contrato inteligente, mas também alterar as existentes. A herança também facilita isso. Ao projetar a versão inicial de seu contrato inteligente, você precisa ter em mente que a lógica de negócios pode mudar em versões futuras.
Uma boa convenção de projeto aqui, na minha opinião, seria criar um documento para listar todos os seus métodos de contrato inteligente e, em seguida, repassá-los pelo menos duas vezes. Isso lhe dará tempo para discutir qual deles pode mudar no futuro. Aqueles que podem mudar no futuro - marque-os como funções virtuais.
Marcar qualquer função como virtual permitirá que você use a palavra-chave de sobreposição para sobrepor aquela função em versões futuras. Um ponto a ser observado aqui é que ambas as palavras-chave podem ser usadas ao mesmo tempo no protótipo da função. Isso significaria que você está sobrepondo a função da versão anterior, mas também está mantendo o escopo para que ela seja alterada em versões futuras.
Você só pode atualizar tanto…
Atualizar seus contratos inteligentes não resolverá todos os seus problemas. Por um lado, ele não pode completar péssimos projetos de contrato inteligente. Isso acontece eventualmente. A única maneira de melhorar seu projeto é revisá-lo antes de realmente implementá-lo. Se você é parte de uma equipe, certifique-se de discutir cada funcionalidade antes de ir para o código.
Além disso, o contrato Initializable.sol coloca um limite máximo no número de versões que um contrato pode ser atualizado para 255. Você pode implementar o seu próprio para aumentar esse limite. Mas, honestamente, se você precisar atualizar seu contrato tantas vezes, pode ser melhor escrever um novo. Afinal, se você continuar adicionado ao contrato ao longo do tempo e continuar alterando-o minimamente… Será o mesmo contrato no final?
Um ovo de páscoa da Wanda Vision para quem gosta…
Conclusão
Isso nos leva ao fim. Honestamente, não foi um dos meus artigos mais longos, mas eu queria obter o máximo de coisas que pudesse neste pequeno artigo. Se você acha que eu possa ter perdido alguma coisa, sinta-se à vontade para iniciar uma discussão abaixo. É sempre encorajador ver esse engajamento.
Além disso, lembre-se de que todo o contrato inteligente “atualizável” é ainda uma coisa nova no momento da escrita e este artigo pode crescer à medida que as coisas se alinharem no futuro. Se você gosta, dê um like e um alô no Twitter ou LinkedIn, faria o meu dia!
Isso também nos leva ao fim da minha série de artigos sobre contratos inteligentes atualizáveis. Acho que vou dar início a outro tópico interessante na próxima semana. Mas, até lá, continue construindo coisas incríveis na Web3 e WAGMI (todos nós vamos conseguir)!
Esse artigo foi escrito por Abhik Banerjee, freelancer e ávido leitor de artigos de pesquisa, e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Oldest comments (0)