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
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:
- 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.
- 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
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
No seu arquivo hardhat.config
, você precisa carregá-lo em
// js
require('@openzeppelin/hardhat-upgrades');
// ts
import '@openzeppelin/hardhat-upgrades';
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",
};
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;
}
}
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");
});
});
Para testar, execute este comando
npx hardhat test
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;
});
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");
});
});
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
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);
}
}
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;
});
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");
});
});
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
Para fazer deploy na goerli, simplesmente substitua
npx hardhat run --network localhost
por
npx hardhat run --network goerli
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
Esse artigo foi escrito por Busayo Amowe e traduzido por Fátima Lima. O original pode ser lido aqui.
Latest comments (0)