Olá, amigo desenvolvedor!
Neste artigo, aprenderemos como criar, implementar e atualizar um Contrato Inteligente UUPS de forma resumida, usando o Hardhat, bibliotecas Open Zeppelin e a rede Rootstock. Mais precisamente, criaremos um alimentador de preços de stablecoin para qualquer DApp.
Mas antes, um pouco de teoria.
Padrões de Arquitetura
Existem muitos padrões de arquitetura para um contrato atualizável (por exemplo: Diamante (Diamond), Transparente, etc), mas, no momento, o UUPS (Universal Upgradeable Proxy Standard, ou Padrão Universal de Proxy Atualizável) é o padrão recomendado pela Open Zeppelin para criar novos contratos inteligentes atualizáveis. Claro, existem diferentes casos de uso que podem exigir outros padrões, mas o UUPS visa ser o padrão (daí o termo Universal em seu nome).
Padrão UUPS
No UUPS, dividimos o contrato em 2 contratos: Proxy + Implementação.
A principal diferença no padrão UUPS é que o estado do contrato é armazenado **no contrato Proxy
, então, a cada atualização, não precisamos pensar sobre uma migração de estado. O contrato de proxy irá delegar todas as chamadas para a implementação usando a função delegatecall
dentro da função fallback
,como você pode ver na imagem abaixo. Usar delegatecall
implica que o contrato de lógica usará o contexto do contrato Proxy
. Outra maneira de pensar sobre isso é que o contrato Proxy
importará o contrato de implementação (sua lógica) para usá-lo com seu próprio estado.
UUPS e delegatecall
Outra coisa importante é que a atualização é acionada através do Proxy
, mas na verdade é o contrato de implementação que vai atualizar para a nova versão, pois a implementação tem a lógica para encontrar o slot de memória onde seu endereço está armazenado dentro do Proxy
.
Bem, chega de teoria. Vamos começar a fazer um alimentador de preços de stablecoin usando o padrão UUPS.
Alimentador de Preços de Stablecoin UUPS
Primeiro, abra um terminal e execute este comando para criar a pasta do projeto e movê-la para lá:
mkdir my-uups-contract && cd my-uups-contract
Agora vamos instalar o hardhat juntamente com outras ferramentas necessárias:
npm install hardhat @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades @nomiclabs/hardhat-etherscan dotenv -D
Em seguida, execute este comando para inicializar o projeto hardhat (se você não souber o que escolher, apenas escolha javascript e pressione Enter para todas as opções)
npx hardhat
Você deve ter uma estrutura de pastas como esta:
Estrutura do projeto
Agora crie um arquivo .env
na pasta raiz do projeto com o seguinte conteúdo:
RSK_NODE=https://public-node.testnet.rsk.co
PRIVATE_KEY=<chave privada de um endereço que você possui>
PROXY_ADDRESS=<endereço proxy implementado>
Nota: Neste caso, usaremos uma conta da carteira Metamask para realizar as transações. Então, você precisará criar uma nova carteira, adicionar a rede de testes da Rootstock e obter a chave privada de um de seus endereços. Você vai precisar de tokens RBTC para as implementações e atualizações, então vá para uma torneira Rootstock para conseguir alguns. Tudo isso não deve levar mais de 5 minutos.
Aviso: Faremos isso na rede de testes, então está tudo bem. Mas sempre tenha cuidado com onde você coloca sua chave privada, pois ela corresponde a um endereço e o par de endereços/chave privada pode ser o mesmo em várias blockchains, o que significa que se alguém tiver sua chave privada, todos os seus tokens podem ser roubados.
Não se preocupe com a variável proxy
ainda. Vamos preenchê-la mais tarde.
Agora vamos configurar o arquivo hardhat.config.js
com as variáveis de ambiente e a rede Rootstock. Substitua o que está dentro do arquivo com este conteúdo:
require("@nomiclabs/hardhat-ethers")
require("@openzeppelin/hardhat-upgrades")
require("@nomiclabs/hardhat-etherscan")
require("dotenv").config()
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.17",
networks: {
rsk: {
url: process.env.RSK_NODE,
accounts: [process.env.PRIVATE_KEY],
}
}
}
Agora vá para a pasta de contratos, crie um arquivo chamado StablecoinPriceFeeder.sol
e adicione este conteúdo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract StablecoinPriceFeeder is Initializable, OwnableUpgradeable, UUPSUpgradeable {
// V1: função initialize com modificador initializer. Funciona como o construtor. Executa por padrão ao implantar.
// Versões posteriores: função initializeVxxx com modificador reinitializer(versionNumber). Precisará ser chamado manualmente.
function initialize() public initializer {
__Ownable_init(); // Apenas necessário na V1
__UUPSUpgradeable_init(); // Sempre necessário
}
// Usado ao atualizar para uma nova implementação, apenas o(s) proprietário(s) poderá(ão) atualizar
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
}
// Obter o preço da stablecoin. Esta lógica pode ser substituída se necessário (por exemplo: para usar "medianizers", que são componentes utilizados para determinar o preço ou valor de ativos dentro de um sistema, o que pode exigir uma lógica mais complexa)
// Adicione as palavras-chave "virtual override" para manter uma lógica atualizável enquanto mantém a mesma assinatura (nome da função + mesmos parâmetros)
function getPrice() public view virtual returns (uint256) {
// lógica para recuperar o preço
return 999999999999999999;
}
// Obter a versão atual da implementação (apenas 255 atualizações são possíveis). Não se esqueça do virtual + override
function version() public pure virtual returns(uint8) {
return 1;
}
}
contract StablecoinPriceFeederV2 is StablecoinPriceFeeder {
// A partir da V2, devido à herança, qualquer nova variável de estado será armazenada DEPOIS das variáveis declaradas na versão anterior.
uint256 public testVariableV2;
// Inicializador para implementação V2
// o modificador reinitializer(versionNumber) com o novo número de versão é necessário para cada nova versão.
function initializeV2(uint256 _testVariableV2) reinitializer(2) public onlyProxy onlyOwner {
__UUPSUpgradeable_init();
testVariableV2 = _testVariableV2;
}
// Ainda usa a lógica getPrice() da V1
// Atualiza a saída da versão
function version() public pure virtual override returns(uint8) {
return 2;
}
}
contract StablecoinPriceFeederV3 is StablecoinPriceFeederV2 {
// Lembre-se de que há um espaço de memória reservado para testVariable2 aqui.
// Inicializador para a V3
function initializeV3() reinitializer(3) public onlyProxy onlyOwner {
__UUPSUpgradeable_init();
}
// Sobrescrever a lógica de recuperação de preço
function getPrice() public view virtual override returns (uint256) {
// nova lógica
return 1;
}
// Atualiza a saída da versão
function version() public pure virtual override returns(uint8) {
return 3;
}
}
Para uma melhor compreensão de cada linha de código, leia os comentários escritos. Note que cada nova versão, na verdade, herda a anterior.
Resumindo, estamos importando os contratos Initializable
, OwnableUpgradeable
e UUPSUpgradeable
das bibliotecas do OpenZeppelin, e as funções necessárias para o padrão UUPS funcionar são as funções initialize()
(também com as funções __Ownable_init()
e __UUPSUpgradeable_init()
declaradas dentro dela) e _authorizeUpgrade
, sendo que esta última precisa ser declarada como uma substituição para autorizar a atualização para uma nova versão. Também declaramos os contratos V2 e V3 no mesmo arquivo.
Na função version()
, não se esqueça de atualizar o número para cada versão. Caso contrário, seu contrato atualizado emitirá um número de versão mais antigo (apesar de ter a funcionalidade da nova versão). Como todas as variáveis de estado que você definir serão armazenadas no Proxy
, você também pode definir uma variável de versão e atualizá-la em um a cada atualização dentro da função initialize
, que atua como um construtor (executa apenas uma vez). O número da versão do reinitializer
e o número de saída da função version()
devem ser os mesmos para consistência.
Agora vamos criar os scripts deploy.js
e upgradeV2.js
na pasta de scripts:
Script de implantação:
const { ethers, upgrades } = require("hardhat")
async function main () {
console.log("Implantando StablecoinPriceFeeder...")
const StablecoinPriceFeeder = await ethers.getContractFactory("StablecoinPriceFeeder")
const stablecoinPriceFeeder = await upgrades.deployProxy(StablecoinPriceFeeder, [], { initializer: "initialize" })
await stablecoinPriceFeeder.deployed()
console.log("StablecoinPriceFeeder (proxy) implantado em:", stablecoinPriceFeeder.address)
}
main()
Script de atualização:
const { ethers, upgrades } = require("hardhat")
const PROXY = process.env.PROXY_ADDRESS
if (!PROXY) {
throw new Error("Você deve especificar o endereço do proxy do pricefeeder no arquivo .env")
}
async function main () {
console.log("Atualizando StablecoinPriceFeeder para V2...")
const StablecoinPriceFeederV2 = await ethers.getContractFactory("StablecoinPriceFeederV2")
const stablecoinPriceFeederV2 = await upgrades.upgradeProxy(PROXY, StablecoinPriceFeederV2)
console.log("StablecoinPriceFeeder atualizado com sucesso", { version: await stablecoinPriceFeederV2.version() })
}
main()
Agora temos que compilar os contratos. Para fazer isso, execute:
npx hardhat clean && npx hardhat compile
Implantação
Para implantar o contrato, execute:
npx hardhat run scripts/deploy.js --network rsk >> deployResult.log
Isso fará com que seu endereço crie e envie duas transações para o nó Rootstock, uma para implantar o contrato de implementação e outra para implantar o contrato Proxy
.
Se tudo correu bem, você deve ver no arquivo deployResult.log
que o StablecoinPriceFeeder
foi implantado com sucesso e um endereço do proxy registrado que o confirma.
Nota: Se desejar verificar o contrato, você tem que carregar o código do contrato no Explorador da Rootstock. Procure por seu endereço do proxy e vá para a guia de código para completar as informações e fazer a verificação.
Atualização
Para atualizar para a V2, pegue o endereço do proxy que você encontrou no log de implantação, adicione-o ao seu arquivo .env
e execute o seguinte comando:
npx hardhat run /scripts/upgradeV2.js --network rsk >> upgradeV2ResultRSK.log
Novamente, você deve ver no arquivo upgradeV2ResultRSK.log
o resultado da atualização e o novo número de versão registrado. Isso significa que você substituiu o contrato de implementação e atualizou para a V2!
E é isso! Depois de definir e implementar sua lógica específica para recuperar um preço (por exemplo: chamando outros contratos/realizando alguns cálculos), você tem um alimentador de preços funcional rodando na Rootstock com o padrão UUPS, que é recomendado pela Open Zeppelin.
Dicas extras
No caso de você precisar escalar, você pode implantar vários proxies para uma determinada versão do contrato.
No Hardhat, os contratos lógicos e de proxy são implantados juntos para a V1 por padrão. Para implantar vários proxies usando a mesma lógica, execute novamente o script de implantação especificando a mesma versão do contrato.
Exemplo:
Para implantar múltiplos proxies da V2 usando o mesmo contrato lógico (V2), primeiro atualize de V1 para V2 usando o script de atualização, especificando a lógica do contrato V2. Em seguida, usando o script de implantação, especifique o contrato lógico da V2. Se você executá-lo 5 vezes, então 5 novos proxies serão implantados para a mesma lógica, além do primeiro implantado durante a atualização.
Em resumo: você tem 6 proxies usando a mesma lógica (que foi implantada com a atualização para a V2)
Para atualizar todos os proxies para uma nova versão, você deve usar o script de atualização, especificando um por um o endereço do proxy correspondente, já que todos os proxies são instâncias diferentes.
Artigo original publicado por Nicolas Vargas. Traduzido por Paulinho Giovannini.
Latest comments (0)