WEB3DEV

Cover image for Atualizando Contratos Inteligentes usando Plugin de Atualização do OpenZeppelin
Fatima Lima
Fatima Lima

Posted on

Atualizando Contratos Inteligentes usando Plugin de Atualização do OpenZeppelin

Image description

Uma pequena introdução

Quando me deparei com contratos atualizáveis, fiquei um pouco surpreendida. Atualização? Por que atualizar é um assunto já que contratos inteligentes são projetados para serem imutáveis por padrão? Uma vez que um contrato é criado na blockchain, não tem como mudá-lo. Você pode ter as mesmas perguntas/pensamentos que eu tinha ou até mais. Ao pesquisar como escrever um contrato atualizável, foi um desafio compreender e encontrar um guia bem explicativo, razão pela qual discutirei alguns fundamentos neste artigo ao mesmo tempo em que mostrarei a você como escrever um simples contrato inteligente atualizável usando o plugin do openzepplin.

O Porquê

Alguns cenários exigem a modificação dos contratos. Relacionando-o com a vida cotidiana regular, duas partes que assinaram um contrato podem decidir mudar os acordos, talvez tenham que remover alguns termos ou acrescentar mais alguns ou corrigir erros. Desde que ambas as partes concordem com isso, ele pode ser modificado. Em uma blockchain como a Ethereum, é possível que um bug tenha sido encontrado em um contrato inteligente que já tenha sido implantado na produção ou que mais funcionalidades sejam apenas necessárias. Pode ser qualquer coisa, realmente. Definitivamente, isso exige uma atualização.

OpenZepplin

Image description

OpenZeppelin

O OpenZeppelin é a empresa líder quando se trata de proteger produtos, automatizar e operar aplicações descentralizadas. Eles protegem as organizações líderes, realizando auditorias de segurança em seus sistemas e produtos. Eles têm uma biblioteca de contratos inteligentes modulares, reutilizáveis e seguros para a rede Ethereum, escritos em Solidity. Graças ao Plugin de Atualizações do OpenZeppelin, é bastante fácil modificar um contrato enquanto ainda preserva coisas importantes como endereço, estado e saldo.

O Como

Os contratos inteligentes podem ser atualizados usando um proxy. Basicamente, existem dois contratos:

  1. Contrato 1 (proxy/ponto de acesso): Este contrato é um proxy ou um wrapper com o qual será interagido diretamente. Também é responsável pelo envio de transações de e para o segundo contrato do qual eu falarei a seguir.
  2. Contrato 2 (contrato lógico): Este contrato possui a lógica.

Uma coisa a observar é que o proxy nunca muda, no entanto, você pode trocar o contrato lógico por outro contrato, o que significa que o ponto de acesso/proxy pode apontar para um contrato lógico diferente (em outras palavras, ele é atualizado). Isto é ilustrado abaixo

Image description

Fonte: https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#upgrading-via-the-proxy-pattern

Para saber mais sobre os conceitos de proxy, visite a página de documentos padrão proxy de atualização do openzepplin e a página proxy do openzepplin.

Padrões de “Atualizabilidade”

Temos vários padrões de “atualizabilidade”. Abaixo estão listados quatro padrões

  • UUPS proxy: EIP1822
  • Proxy transparente: EIP1967 (Vamos nos concentrar nele neste artigo)
  • Armazenamento Diamante: EIP2355
  • Armazenamento Eterno: ERC930

Proxy Transparente (EIP1967)

Os proxies transparentes incluem a atualização e a lógica administrativa no próprio proxy. Eu me referiria ao administrador como o proprietário do contrato que inicia a primeira atualização.

Usando o proxy transparente, qualquer conta que não seja o administrador que chama o proxy terá suas chamadas direcionadas para a implementação. Na mesma linha, se o admin chamar o proxy, ele poderá acessar as funções do admin, mas as chamadas do admin nunca serão direcionadas para a implementação.

Em resumo, é melhor que o administrador seja uma conta dedicada apenas para seu propósito, que é obviamente ser um administrador.

Etapas práticas

Pré-requisito: conhecimento de como criar um ambiente de desenvolvimento e de como redigir contratos inteligentes. Mais informações aqui

Vamos escrever um contrato atualizável! Seremos o plugin do openzeppelin de atualizações hardhat. Para instalar, basta executar

npm install --save-dev @openzeppelin/hardhat-upgrades @nomiclabs/hardhat-ethers ethers

Enter fullscreen mode Exit fullscreen mode

No seu arquivo hardhat.config, você precisa carregá-lo em

// js
require('@openzeppelin/hardhat-upgrades');
// ts
import '@openzeppelin/hardhat-upgrades';

Enter fullscreen mode Exit fullscreen mode

Eu usarei js para esse artigo

Seu arquivo hardhat.config.js deve ficar semelhante a isso

require("@nomiclabs/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-etherscan");
//Usando o alchemy, como pretendo utilizar a testnet goerli, é necessária uma apikey
//A mnemônica é a mnemônica de sua conta
//se você pretende verificar seus contratos, precisa abrir uma conta no etherscan e copiar a apikey
//todas as chaves importantes não devem ser expostas. Elas podem ser mantidas em um arquivo secret.json e adicionadas ao gitignore
const { alchemyApiKey, mnemonic } = require("./secrets.json");
module.exports = {
 networks: {
   goerli: {
     url: `https://eth-goerli.alchemyapi.io/v2/${alchemyApiKey}`,
     accounts: { mnemonic: mnemonic },
   },
 },
 etherscan: {
   apiKey: "SUA_CHAVE_DE_API",
 },
 solidity: "0.8.4",
};
Enter fullscreen mode Exit fullscreen mode

Contrato 1 (contracts/Atm.sol) (contrato proxy)

Em sua pasta de contratos, crie um novo arquivo .sol. Neste artigo, eu simularei um atm/banco. Então, crie o Atm.sol. O código deve ser semelhante a isso

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Atm {
   // Declare as variáveis de estado do contrato
   uint256 bankBalances;
   // Permitir que o proprietário deposite dinheiro na conta
   function deposit(uint256 amount) public {
       bankBalances += amount;
   }
   function getBalance() public view returns (uint256) {
       return bankBalances;
   }
}
Enter fullscreen mode Exit fullscreen mode

Testar o Contrato

Teste seu contrato em test/Atm-test.js como ilustrado abaixo

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Atm", function () {
 before(async function () {
   this.Atm = await ethers.getContractFactory("Atm");
 });
 beforeEach(async function () {
   this.atm = await this.Atm.deploy();
   await this.atm.deployed();
   it("", async function () {});
   await this.atm.deposit(1000);
   expect((await this.atm.getBalance()).toString()).to.equal("1000");
 });
});
Enter fullscreen mode Exit fullscreen mode

Para testar, execute este comando

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

Fazer a implantação do Contrato

!Importante: A fim de poder atualizar o contrato Atm, precisamos primeiro implantá-lo como um contrato atualizável. É diferente do procedimento de implantação a que estamos acostumados. Estamos inicializando para que o saldo inicial seja 0. O script usa o método deployProxy que é do plugin.

Criar um script deploy-atm.js

const { ethers, upgrades } = require("hardhat");
async function main() {
 const Atm = await ethers.getContractFactory("Atm");
 console.log("Implantando Atm...");
 const atm = await upgrades.deployProxy(Atm, [0], {
   initializer: "deposit",
 });
 console.log(atm.address, " atm(proxy) address");
}
main().catch((error) => {
 console.error(error);
 process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Você pode decidir testar isto também. Se você deseja testar, seu arquivo de teste deve ser semelhante a este

Testar o Script

const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");
const { Contract, BigNumber } = "ethers";
describe("Atm (proxy)", function () {
 let box = Contract;
 beforeEach(async function () {
   const Atm = await ethers.getContractFactory("Atm");
   //inicialize com 0
   atm = await upgrades.deployProxy(Atm, [0], { initializer: "deposit" });
 });
 it("should return available balance", async function () {
   expect((await atm.getBalance()).toString()).to.equal("0");
   await atm.deposit(1000);
   expect((await atm.getBalance()).toString()).to.equal("1000");
 });
});
Enter fullscreen mode Exit fullscreen mode

Antes de confirmar os testes,

Vamos primeiro implantar na rede local, usando o comando run e implantar o contrato Atm para a rede de desenvolvimento (dev).

$ npx hardhat run --network localhost scripts/deploy-atm.js
Implantando Atm...
0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9  atm(proxy) address
Enter fullscreen mode Exit fullscreen mode

Neste ponto, fizemos o deploy com sucesso e temos nosso endereço proxy e o admin.

Contrato 2

Queremos acrescentar um novo recurso ao nosso contrato, um recurso simples que é incluir uma função add que acrescenta 500 ao nosso saldo.

Criar contracts/AtmV2.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Atm.sol";
contract AtmV2 is Atm{
   // acrescenta 500 ao saldo
   function add() public {
       deposit(getBalance()+500);
   }
}
Enter fullscreen mode Exit fullscreen mode

Testar o Contrato

Verifique como testamos o Contrato 1 e basicamente siga a mesma lógica.

Atualizar o Contrato

Agora é a hora de usar nosso endereço de proxy/ponto de acesso. Nós usaremos os métodos upgradeProxy e 'getAdmin' do plugin. Lembre-se de nosso endereço de proxy do nosso console de implantação acima, pois precisaremos dele aqui.

Criar scripts/upgrade-atmV2.js. Seu script deve ser semelhante a isso

const { ethers, upgrades } = require("hardhat");
const proxyAddress = "Seu_ENDEREÇO_PROXY_DE_IMPLANTAÇÂO";
async function main() {
 console.log(proxyAddress, " original Atm(proxy) address");
 const AtmV2 = await ethers.getContractFactory("AtmV2");
 console.log("atualizar para AtmV2...");
 const atmV2 = await upgrades.upgradeProxy(proxyAddress, AtmV2);
 console.log(atmV2.address, " Endereço AtmV2 (deve ser o mesmo)");
 console.log(
   await upgrades.erc1967.getAdminAddress(atmV2.address),
   "Proxy Admin"
 );
console.log('Atm atualizado');
}
main().catch((error) => {
 console.error(error);
 process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Testar o script

Criar um scripts/AtmProxyV2-test.js. Deve ficar semelhante a isso

const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");
const { Contract, BigNumber } = "ethers";
describe("Atm (proxy) V2", function () {
 let atm = Contract;
 let atmV2 = Contract;
 beforeEach(async function () {
   const Atm = await ethers.getContractFactory("Atm");
   const AtmV2 = await ethers.getContractFactory("AtmV2");
   //inicialize com 0
   atm = await upgrades.deployProxy(Atm, [0], { initializer: "deposit" });
   atmV2 = await upgrades.upgradeProxy(atm.address, AtmV2);
 });
 it("should get balance and addition correctly", async function () {
   expect((await atmV2.getBalance()).toString()).to.equal("0");
   await atmV2.add();
   //resultado = 0 + 500 = 500
   expect((await atmV2.getBalance()).toString()).to.equal("500");
   //o saldo agora é 500, então acrescente 100;
   await atmV2.deposit(100);
   //resultado = 500 + 100 = 600
   expect((await atmV2.getBalance()).toString()).to.equal("600");
 });
});
Enter fullscreen mode Exit fullscreen mode

Após confirmação dos testes,

Vamos implantar nosso novo contrato com o recurso adicional usando o comando run e implantar o contrato AtmV2 para a rede dev.

npx hardhat run --network localhost scripts/upgrade-atmV2.js
Compilation finished successfully
upgrade to AtmV2...
0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9  AtmV2 address(should be the same)
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512  Proxy Admin
Enter fullscreen mode Exit fullscreen mode

Para fazer deploy na goerli, simplesmente substitua

npx hardhat run --network localhost
Enter fullscreen mode Exit fullscreen mode

por

npx hardhat run --network goerli
Enter fullscreen mode Exit fullscreen mode

Aí está, confira seus endereços em Goerli Explorer e verifique-o.

Para uma visão de todos os contratos, você pode conferir meus contratos em

Resumo

Embora seja uma abordagem rápida para usar o plugin do openzeppelin e ele varie entre times, a melhor maneira de entender e fazer atualizações é copiar os arquivos transparency proxy sol e arquivos sol relacionados do openzeppelin para seu projeto. Isto o protege de ataques de fluxo ascendente (upstream attacks).

Chegamos ao final deste artigo. Espero que você tenha aprendido uma ou duas coisas. Eu também gostaria de receber feedbacks! Por gentileza, deixe um comentário. Feliz construção!

Referências:

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

https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy

https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916

Esse artigo foi escrito por Busayo Amowe e traduzido por Fátima Lima. O original pode ser lido aqui.

Top comments (0)