WEB3DEV

Cover image for Tudo o que você precisa saber sobre Contratos Inteligentes Atualizáveis e Proxy
Banana Labs
Banana Labs

Posted on

Tudo o que você precisa saber sobre Contratos Inteligentes Atualizáveis e Proxy

Uma das maiores características da blockchain é que os dados são imutáveis. Isso significa que ninguém pode alterar o conteúdo de qualquer transação sem invalidar toda a blockchain.

Mas, infelizmente, existem vários problemas com este sistema:

  • Ao implantar seu contrato inteligente, você não pode alterar o código posteriormente e, portanto, não pode atualizar o contrato inteligente. Depois que a transação é enviada: ela fica para sempre, não há nada que você possa fazer para reverter o estado anterior da EVM - Ethereum Virtual Machine (Máquina Virtual Ethereum).
  • Se você ainda deseja atualizar o contrato inteligente, é preciso implantar um novo em um novo endereço, pagar as taxas de gás, migrar todos os dados e convencer os usuários atuais a usar o novo contrato V2.

Este é um processo muito complicado, leva muito tempo e há uma grande chance de cometer erros ao migrar os dados, o que pode levar a consequências desastrosas para os usuários.

No entanto, a boa notícia é: você pode contornar esse problema, usando contrato inteligente atualizável e este é o tópico principal deste artigo!

1. Como funciona?

O sistema é bastante simples, você precisará de 2 contratos inteligentes diferentes.

  1. O primeiro é chamado de implementação (ou lógica), contém todas as funções regulares do contrato inteligente. (como transfer(), aprove(), e assim por diante…)
  2. E o segundo é chamado de contrato de proxy, que contém: um endereço de armazenamento que aponta para o endereço do contrato de implementação e uma função que delega todas as chamadas ao contrato de implementação e todo o armazenamento do DApp.

Contrato Inteligente atualizável

Quando você chama (por exemplo) a função transfer no contrato de proxy, o proxy irá “delegatecall()” fazer a transferência no contrato de implementação.

Se você não sabe o que é delegatecall: https://solidity-by-example.org/delegatecall/

Isso significa que a função transfer() (inicialmente escrita no contrato de implementação) será executada no contexto do contrato proxy. (A diferença com os contratos inteligentes regulares é que o armazenamento está situado no contrato de proxy, mas a função no contrato de implementação)

Isto é muito útil porque quando você deseja atualizar o contrato inteligente:

Você só precisa definir o endereço da implementação no contrato de proxy para o novo contrato de implementação/lógica.

Implementação de Contrato

Mas o armazenamento ainda está no proxy, como resultado ainda está aqui e acima de tudo: O armazenamento NÃO ESTÁ PERDIDO (se armazenamos os dados no contrato de implementação, quando atualizarmos para um novo contrato de implementação, o armazenamento será redefinido. )

E… isso é tudo! Quando todas essas operações forem concluídas, você poderá descartar o antigo contrato inteligente e começar a usar o novo como antes.

Qualquer usuário que chamar o contrato inteligente não verá nenhuma diferença além das funções atualizadas. Está feito!

2. Existem 3 padrões de proxy diferentes

Existem várias formas de organizar contratos atualizáveis utilizando sistemas mais ou menos complexos.

2.1 A primeira forma chama-se: Padrão Proxy Transparente.

Este é o padrão de proxy mais simples possível, ele se comporta como descrito acima na primeira seção. Aqui está o código:

contract proxy{   
    address _impl;   // Endereço para implementação.
    function upgradeTo(address newImpl) external {       
        _impl = newImpl;   
    }   
    fallback() {   
        assembly {    
            let ptr := mload(0x40) 
            //(1) copiar os dados da chamada recebida

            calldatacopy(ptr, 0, calldatasize)  
            // (2) encaminhar chamada para contrato lógico 

            let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)     
            let size := returndatasize 
            // (3) recuperar dados retornados  
            returndatacopy(ptr, 0, size) 
            // (4) encaminhar dados de retorno de volta ao chamador 

            switch result     
               case 0 { revert(ptr, size) }     
               default { return(ptr, size) }   
            }  
     }   
     // e outras funções
}
Enter fullscreen mode Exit fullscreen mode

Toda vez que o usuário chama uma função no proxy, a função de fallback delega todos os parâmetros ao contrato de implementação e retorna o resultado. (reverta se o resultado for zero.)

Se quisermos atualizar o contrato, precisamos apenas implantar o novo contrato inteligente e alterar a implementação no proxy para o contrato inteligente chamando a função upgradeTo que define a implementação. (não se esqueça de adicionar um proprietário ao contrato, caso contrário, todos poderão atualizar o contrato com seu próprio código).

2.2 O segundo é o mais popular: chama-se proxy UUPS. (ou proxy ERC1967)

UUPS Proxy

Um terceiro contrato inteligente é adicionado: o contrato ProxyAdmin que controla o “proxy público”. (O proxy que delega para a implementação.)

Ele contém algumas funções de administrador, para garantir que apenas um pequeno conjunto de endereços possa atualizar o proxy.

2.3 O terceiro é o proxy Diamond, que é bem mais complexo, mas acaba com quase todas as limitações dos proxies atualizáveis UUPS (limitação que veremos um pouco mais adiante)

  • O armazenamento e as funções são divididos entre 2 contratos diferentes. (mínimo)
  • Você pode ter vários contratos inteligentes para um contrato inteligente. (para contornar o limite de 24kb)

Este é mais adequado para aplicativos descentralizados muito grandes, mas no nosso caso é muito complicado; portanto, nas próximas seções, estaremos mais focados nos proxys UUPS, adequados para 98% dos DApps.

3. Tutorial de proxy atualizável

Nesta seção, mostrarei como usar o proxy atualizável UUPS com o Hardhat.

Presumo que você já saiba como criar e implantar um contrato inteligente com esta ferramenta, caso contrário visite https://hardhat.org/.

Há 2 etapas rápidas para a instalação:

1.Instalação

npm install — save-dev @openzeppelin/hardhat-upgrades npm install — save-dev @nomiclabs/hardhat-ethers ethers
Enter fullscreen mode Exit fullscreen mode

2.Registrando o novo plugin no hardhat.config.js

require(‘@openzeppelin/hardhat-upgrades’);
Enter fullscreen mode Exit fullscreen mode

Agora, como exemplo, escreveremos um contrato inteligente de teste:

//SPDX-License-Identifier: Unlicense 
pragma solidity ^0.8.0;  
contract MyContract {     
     function returnValue() public view returns(uint) { 
        return 1;
     } 
}
Enter fullscreen mode Exit fullscreen mode

O contrato contém apenas uma função que retorna sempre 1.

Nosso objetivo é atualizar este contrato que sempre retornará 2.

Existem 2 etapas.

1) Crie o script para implantar o proxy e a primeira versão do contrato inteligente (em scripts/deploy.js)

const { ethers, upgrades } = require("hardhat");  async function main() {   
    const Box = await ethers.getContractFactory("MyContract"); 
    // obter MyContract que retorna 1    
    const box = await upgrades.deployProxy(Box); 
    // implantar o proxy e o contrato    
     await box.deployed();   
     console.log("Box deployed to:", box.address); 
     // mostre o endereço do contrato assim que for implantado 
}
Enter fullscreen mode Exit fullscreen mode

Não se esqueça de executar scripts/deploy.js

npx hardhat run --network [your_network] scripts/deploy.js
Enter fullscreen mode Exit fullscreen mode

2) Crie o script para atualizar o contrato inteligente (scripts/update.js)

const { ethers, upgrades } = require("hardhat");  
let BOX_ADDRESS = "..."
// defina BOX_ADDRESS como box.address, o endereço da função proxy.async main() {   
    const BoxV2 = await ethers.getContractFactory("MyContractV2"); // obter o novo contrato (versão 2)    
const box = await upgrades.upgradeProxy(BOX_ADDRESS, BoxV2); 
// definir implementação de proxy para BOX V2    
console.log("Box upgraded"); 
// quando terminar, imprime "box upgraded".
}
Enter fullscreen mode Exit fullscreen mode

Aqui está o contrato MycontractV2, a única diferença com a primeira versão, é que a função retorna sempre 2.

//SPDX-License-Identifier: Unlicense 
pragma solidity ^0.8.0;  
contract MyContract {     
    function returnValue() public view returns(uint) { 
        return 2;
    } 
}
Enter fullscreen mode Exit fullscreen mode

E pronto, toda vez que você quiser atualizar seu contrato inteligente, basta executar o script update.js com o contrato de notícias na variável BOX_ADDRESS. (que precisa ser o endereço do proxy)

npx hardhat run — testnet testnet scripts/update.js
Enter fullscreen mode Exit fullscreen mode

Hardhat fará o resto, escrever contrato inteligente atualizável, nunca foi tão fácil!

Regras Importantes.

Aviso: Esteja ciente de que o uso de proxy tem um certo custo, na verdade, há algumas pequenas limitações para contratos atualizáveis UUPS:

1) O construtor deve estar vazio

Nas linguagens de programação, o construtor é sempre executado uma vez para iniciar os parâmetros, mas quando atualizamos os contratos inteligentes, o construtor é executado outra vez, o que pode levar a um comportamento indefinido. Precisamos garantir que todos os parâmetros sejam inicializados uma única vez.

No lugar do construtor, usaremos uma função inicializadora com um modificador inicializador.

//construtor perigoso: 
   constructor(uint256 _x,_y) {  x = _x  y = _y }  
//deve ficar assim: 
   function initialize(uint256 _x,uint256 _y) public initializer {            x = _x   y = _y  }  
modifier initializer() {  
    require(!initialized, "A instância do contrato já foi inicializada");   
    initialized = true;  _; 
}
Enter fullscreen mode Exit fullscreen mode

Quando a função initialize é chamada uma vez, o modificador inicializador marca “initialized” como “true” e evita que initialize seja chamada uma segunda vez.

Info: quando você implanta o proxy e o contrato pela primeira vez, chame o hardhat initialize() diretamente, para que você não precise fazer isso por conta própria.

2) Você não pode excluir variáveis, não pode alterar o tipo e a ordem das variáveis também.

Por que? Porque isso reordenará os slots no armazenamento EVM e tornará o contrato inteligente inutilizável, por exemplo:

uint a = 11 //slot 1
uint b = 22 //slot 2
uint c = 33 //slot 3
Enter fullscreen mode Exit fullscreen mode

Se reordenarmos b e c na atualização (V2 do contrato inteligente), obteremos:

uint a // slot 1
uint c // slot 2
uint b // slot 3
Enter fullscreen mode Exit fullscreen mode

O slot 2 e o slot 3 não serão reordenados no proxy, onde os dados são armazenados. O slot 2 ainda será igual a 22 em vez de 33, portanto, c será igual a 22.

O slot 3 ainda será igual a 33 em vez de 22, portanto, b será igual a 33.

Problemas semelhantes ocorrem quando uma variável é excluída. (Os slots no EVM não são liberados.)

Na verdade, qualquer atualização que altere a ordem no armazenamento EVM pode comprometer os dados.

Isso é conhecido como vulnerabilidade de conflito de armazenamento.

3) Você não pode usar bibliotecas externas.

Essas limitações impõem que muitas bibliotecas não serão compatíveis com contratos atualizáveis.

Para solucionar esse problema, o Openzeppelin reescreveu muitas de suas bibliotecas para serem “seguras para atualização” e compatíveis com as regras definidas acima.

Portanto, é possível escrever contratos atualizáveis ERC20, ERC721, ERC1155….

Para isso, você precisa instalar adicionalmente: @openzeppelin/contracts-upgradable

Os contratos estão escritos aqui: https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable

Existem outras limitações, você pode aprender aqui: https://docs.openzeppelin.com/upgrades-plugins/1.x/faq

4. Conclusão

Espero que você tenha gostado do meu tutorial sobre atualizações de contratos inteligentes. Vejo você em um próximo tutorial!


Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui

Oldest comments (0)