WEB3DEV

Cover image for Tudo o que você precisa saber sobre Contratos Inteligentes atualizáveis
Lorenzo Battistela
Lorenzo Battistela

Posted on

Tudo o que você precisa saber sobre Contratos Inteligentes atualizáveis

Uma das melhores características das blockchains é que os dados são imutáveis. Isso significa que ninguém pode mudar o conteúdo de qualquer uma das transações sem invalidar toda a blockchain.

Mas infelizmente, existem alguns problemas com esse sistema:

  • Quando você implanta seu contrato inteligente, você não pode mudar o código mais tarde e assim não pode atualizá-lo. Uma transação é enviada e permanece para sempre, e não há nada que você possa fazer para reverter o estado da EVM.
  • Se você quiser atualizar seu contrato inteligente, precisaria implantar um novo contrato em um novo endereço, pagar as taxas de gas, migrar todos os dados e convencer seus usuários a migrar para a versão 2 do contrato.

Esse é um processo bem desajeitado, toma muito tempo e tem uma grande chance de cometer algum erro na migração dos dados, o que pode levar a consequências desastrosas para os usuários.

Entretanto, a boa notícia é que: você pode contornar esse problema utilizando um contrato inteligente atualizável, e esse é o tópico principal deste artigo!

1. Como funciona?

O sistema é bem simples, você precisará de dois contratos inteligentess diferentes.

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

Image description

Quando você chama (por exemplo) transfer em um contrato de proxy, o proxy vai “delegatecall()” transfer na implementação do contrato.

Se você não souber 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 de proxy. (A diferença de contratos regulares é que o armazenamento está situado no contrato de proxy mas a função no contrato de implementação).

É muito útil, pois quando você precisar atualizar um contrato inteligente:

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

Mas o armazenamento ainda está no proxy. Ele não está perdido (se armazenássemos os dados no contrato de implementação então quando fizéssemos a atualização para uma nova implementação do contrato, o armazenamento seria reiniciado).

E é isso! Quando todas essas operações forem feitas, você pode descartar o contrato inteligente anterior e começar a usar o novo como antes.

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

2. Existem 3 padrões de proxy diferentes

Existem diferentes maneiras de organizar contratos inteligentes organizáveis, usando sistemas mais ou menos complexos.

2.1 Padrão de Proxy Transparente

Esse é o padrão de proxy mais simples possível, e 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) copia os dados vindos da chamada

           calldatacopy(ptr, 0, calldatasize) 
           // (2) chamada para a frente para a lógica do contrato

           let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)   
           let size := returndatasize
           // (3) recupera os dados retornados 
           returndatacopy(ptr, 0, size)
           // (4) retorna os dados de volta para o 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 um usuário chama uma função no proxy, a função fallback delega todos os parâmetros para o contrato de implementação e retorna o resultado. (reverte se o resultado for zero).

Se quisermos atualizar o contrato, apenas precisamos implantar o novo contrato inteligente e mudar a implementação no proxy para o contrato inteligente chamando a função upgradeTo que configura a implementação. (não esqueça de adicionar um dono do contrato, de outra maneira todo o mundo estará apto a atualizar o contrato com seu próprio código).

2.2 Proxy UUPS (ou proxy ERC1967)

Image description

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

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

2.3 O terceiro é o Proxy diamante, que é bem mais complexo mas passa todas as limitações do modelo UUPS (que veremos daqui a pouco)

  • O armazenamento e as funções são divididas em dois diferentes contratos (no mínimo).
  • Você pode ter vários contratos para um contrato (para contornar o limite de 24kb).

Isso serve para aplicações grandes e descentralizadas, mas no nosso caso é bem complicado, então nas próximas seções estaremos mais focados nos proxies UUPS, que serve para 98% dos DApps.

3. Tutorial de proxy atualizável

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

Estou assumindo que você já sabe como criar e implantar um contrato inteligente com essa ferramenta. Caso não, visite https://hardhat.org/

Existem 2 passos rápidos para a instalação:

  1. Instalação

    npm install -- save-dev @openzeppelin/hardhat-upgrades npm install -- save-dev @nomiclabs/hardhat-ethers ethers
    
  2. Registrar o novo plugin no hardhat.config.js

    require('@openzeppelin/hardhat-upgrades');
    

Agora para o 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 sempre retornará 1.

Nosso objetivo é atualizar esse contrato para sempre retornar 2.

São 2 passos.

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

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

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

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

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

const { ethers, upgrades } = require("hardhat"); 
let BOX_ADDRESS = "..."
// configura BOX_ADDRESS para box.address, o endereço do proxy
async function main() {  
   const BoxV2 = await ethers.getContractFactory("MyContractV2"); 
// pega o novo contrato (versão 2)
   const box = await upgrades.upgradeProxy(BOX_ADDRESS, BoxV2);
// configura a implementação do proxy para BOXV2   
   console.log("Box upgraded");
// quando terminar, escreva box upgraded.
}
Enter fullscreen mode Exit fullscreen mode

Aqui está o contrato MycontractV2, e a única diferença da primeira versão é que a função sempre retorna 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, execute o script update.js com o novo contrato 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

O hardhat fará o resto. Escrever contratos atualizáveis nunca foi tão fácil!

Regras importantes.

Aviso: lembre-se de que usar proxy vem a certo custo. Na verdade, existem algumas limitações dos contratos atualizáveis UUPS

1) O construtor deve estar vazio

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

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

//  construtor perigoso
  constructor(uint256 _x,_y) {  x = _x  y = _y } 
 // deve se tornar
 function initialize(uint256 _x,uint256 _y) public initializer {            x = _x   y = _y  }  modifier initializer() { 
   require(!initialized, "Contract instance has already been initialized");  
   initialized = true;  _;
}
Enter fullscreen mode Exit fullscreen mode

Quando o inicializador é chamado uma vez, o modificador é marcado com: inicializado = true, e previne segundas chamadas à inicialização.

Info: quando você implantar o proxy e o contrato pela primeira vez, o hardhat chama initialize() diretamente, então você não precisa fazer isso manualmente.

2) Você não pode deletar, mudar o tipo e a ordem das variáveis.

Por quê? Pois isso irá reordenar os espaços no armazenamento EVM e deixar o contrato inutilizável. 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 e atualizarmos para uma versão 2, teremos:

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

O slot (espaço) 2 e o 3 não serão reordenados no proxy, onde os dados são armazenados. O espaço 2 será igual a 22 ao invés de 33, e c será igual a 22.

O espaço 3 será igual a 33 ao invés de 22, e b será igual a 33.

Problemas similares acontecem quando uma variável é deletada (os espaços na EVM não são liberados).

Na realidade, qualquer atualização que mexa na ordem do armazenamento EVM pode danificar os dados.

Isso é conhecido como storage clash vulnerability.

3) Você não pode utilizar bibliotecas externas

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

Para resolver esse problema, o Openzeppelin reescreveu muitas das suas bibliotecas para serem seguras de atualizar, mas devem seguir algumas regras.

Então, é possível escrever contratos ERC20, ERC721, ERC1155 atualizáveis.

Para isso, você terá que instalar adicionalmente: @openzeppelin/contracts-upgradable

Os contratos são escritos aqui: GitHub - OpenZeppelin/openzeppelin-contracts-upgradeable: Upgradeable variant of OpenZeppelin Contracts, meant for use in upgradeable contracts.

Outras limitações estão descritas aqui:

https://docs.openzeppelin.com/upgrades-plugins/1.x/faq

4. Conclusão

Espero que você tenha gostado desse tutorial sobre contratos inteligentess atualizáveis.

Esse artigo foi traduzido por Lorenzo Battistela e pode ser encontrado originalmente aqui.

Top comments (0)