Como atualizar Smart Contracts em Solidity (Adapter)

Cripto

Como atualizar Smart Contracts em Solidity (Adapter)

Luiz Duarte
Escrito por Luiz Duarte em 31/01/2023
Junte-se a mais de 34 mil devs

Entre para minha lista e receba conteúdos exclusivos e com prioridade

Quando aprendemos os fundamentos da blockchain, que você confere neste artigo aqui, uma das primeiras coisas que aprendemos é que ela é imutável. Isso não impede de novas informações serem adicionadas, mas sim de que informações anteriores não podem ser alteradas, o histórico se mantém eterno. Da mesma forma, quando estamos programando para uma blockchain, os nossos algoritmos/programas que chamamos de Smart Contracts (que você aprende o básico aqui) também não podem ser alterados, apenas podemos publicar novos smart contracts, com novos endereços.

Isso quer dizer que você pode adicionar novos smart contracts na blockchain, incluindo novas versões do mesmo, mas que jamais poderá excluir ou modificar um já existente. E essa frase coloca muito medo nos programadores Solidity pois caso você suba uma versão do contrato com um bug ou vulnerabilidade, isso quer dizer que ela ficará na blockchain para sempre. E sim, isso é verdade.

Apesar disso, claro, existem medidas de contorno que você pode fazer. Uma delas seria avisar todo mundo que usa este smart contract bugado a desconsiderá-lo e passar a usarem somente o novo que você publicou com o bug corrigido. Mas nem sempre isso é viável ou desejável. O ideal é que você, já na concepção do smart contract, crie algum mecanismo que permita a atualização do seu smart contract posteriormente.

#1 – Opções de Atualização

A opção número um é a que citei antes: desconsidere o endereço antigo do contrato e passe a usar somente o novo. Ponto. Simples e direto mas que geralmente causa muitos problemas se seu smart contract é de uso público como uma DEX ou é uma criptomoeda ERC-20 com vários holders. Se for o segundo caso, ainda dá para contornar: você pode publicar um contrato de migração para fornecer tokens da nova moeda na mesma quantidade que os holders já possuíam na moeda antiga.

A opção número dois é você ter variáveis de estado e funções administrativas para permitir ajustes sem a necessidade de um novo deploy. Isso é fácil de fazer e perfeitamente possível em contratos simples. Mas simples mesmo, onde os bugs sejam de valores errados, inconsistências nos dados, etc. Nada que algumas funções sets definidas para o administrador usar não resolvam.

Mas para qualquer outra situação mais complexa o caminho é usar algum padrão de contratos atualizáveis (upgradeable contracts). Existem duas soluções muito famosas no mercado: os proxies e os adapters.

Os proxies talvez sejam os mais famosos, embora não sejam simples de fazer. Eles estão definidos na EIP-1967 e dizem, resumidamente, que você deve criar um contrato de proxy que vai ficar na frente do seu contrato de fato. É ele que vai receber todas as requisições e que vai rotear elas para o outro contrato através de uma função de fallback e de um recurso chamado delegate call.

Mais tarde, caso sua implementação mude, basta que o admin do contrato proxy altere o endereço do contrato chamando alguma função para isso. Assim, todo mundo sempre conhece o endereço do proxy, enquanto que o endereço (e implementação) do contrato final pode variar ao longo do tempo.

Essa é a solução perfeita e a ideal para casos complexos ou que envolvam muitos dados, mas te adianto que lida com bastante código de baixo nível e exige um pouco de assembly para funcionar (pelo menos da última vez que vi). Então a minha proposta de solução vai ser um pouco diferente: adapters.

Curso Node.js e MongoDB

#2 – Proposta de Solução

A proposta que considero mais interessante para boa parte dos cenários é a de Adapter. À primeira vista um proxy e um adapter são idênticos e para o usuário eles são mesmo. A diferença é que um adapter não faz delegate calls e nem usa fallbacks. Ele pega o conceito de proxy e o implementa com o conceito da solução número dois que citei anteriormente, com variáveis de estado e funções administrativas.

Peguemos como exemplo o contrato a abaixo, hipotético.

Vamos dizer que eu queira ter a possibilidade de mudar a implementação deste contrato no futuro. Como eu crio um Adapter para ele?

O primeiro passo é criar uma interface que define os comportamentos e assinaturas padrões que existirão em todo Contrato, hoje e no futuro. Importante entender que interfaces em Solidity somente podem ser feitas com funções external, então repare que ao invés de usar public eu usei external como modificador de acesso na função getResult. Outro ponto a salientar é que interfaces não possuem variáveis de estado, somente assinaturas de funções mesmo.

Abaixo um exemplo de interface para o mesmo contrato acima:

Essa interface define então que todas as variações de Contrato que eu tiver, hoje ou no futuro, terão uma função getResult com aquela assinatura. Agora ajustamos nosso contrato para dizer que ele “é” um IContrato.

Se no futuro eu quiser gerar uma nova versão do Contrato, sem problema, desde que siga respeitando a interface IContrato.

Mas isso é apenas preparação de terreno para construção do adapter em si, o que faremos abaixo.

Repare que começamos declarando a variável de estado que aponta para um IContrato. Na prática, IContrato vai guardar um Contrato dentro, mas o Adapter não sabe qual ainda. Depois definimos a variável para sabermos quem é o owner do Adapter, já que não é qualquer um que poderá chamar algumas funções mais administrativas depois. Esse owner é inicializado no constructor, ou seja, quando o deploy é realizado.

Depois, para cada função existente na IContrato eu devo ter uma versão aqui no Adapter que chama a sua contraparte no Contrato. Pode parecer algo tolo ficar repassando chamadas assim, que não está agregando valor, mas ledo engano e a conclusão está na função seguinte: upgrade.

A função upgrade é responsável por atualizar o Adapter, dizendo qual a implementação de Contrato que ele deve usar. Ele espera o endereço do contrato já provisionado na blockchain e carrega um IContrato a partir dele, fazendo com que a variável de estado contrato esteja apontando para um contrato real e com isso as demais funções (como getResult) irão funcionar conforme aquela implementação de Contrato. Por questões de segurança, coloquei uma verificação para que somente o owner possa chamar essa função.

Agora, quando você fizer deploy de uma nova versão de Contrato, basta ir no Adapter e chamar a função upgrade passando o novo endereço. Se você manteve o padrão da interface, vai tudo continuar funcionando usando o novo Contrato ao invés do antigo.

E é claro, para que o Adapter funcione como esperado é importante que seus dapps e demais usuários deste smart contract chamem sempre o Adapter e nunca o Contrato diretamente.

#3 – Avisos Gerais

Valem destacar aqui alguns avisos importantes com relação à esta solução.

Primeiro que ela não resolve o problema de dados nas variáveis de estado de contratos antigos. Ou seja, se você for lançar uma nova versão do contrato e não quer perder os dados existentes no contrato antigo (como tokens dos seus usuários, por exemplo) terá de criar algum mecanismo de migração ou incluir os dados nos fontes da nova versão o que pode tornar seu deploy bem caro.

Segundo, que se você usar o objeto global msg no seu Contrato esperando pegar informações da mensagem gerada pelo usuário você terá comportamentos estranhos. Isso porque como o usuário chama o Adapter e o Adapter chama o Contrato, existe a msg original somente na primeira chamada. A chamada interna entre os contratos gera uma segunda mensagem diferente, feita pelo Adapter para o Contrato. Então tome cuidado com isso e priorize tx.origin ao invés de msg.sender, por exemplo.

Terceiro, se você tiver funções payable na interface é importante frisar que por causa do mesmo problema acima, o valor pago pelo usuário irá parar no Adapter e deve ser repassado ao Contrato. Isso é feito usando a notação abaixo, onde repasso toda a quantia recebida para a segunda chamada.

E por último, é importante frisar que essa solução permite que você atualize a implementação do Contrato, desde que respeite a interface, mas não permite que você atualize a interface ou mesmo o Adapter, caso contrário cairia no mesmo problema antigo. Tenha isso em mente quando estiver projetando seu Adapter e sua interface.

E se quiser aprender a reduzir o consumo de gás nos seus smart contracts, este é o artigo certo.

Espero que tenha ajudado!

Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *