WEB3DEV Español

Cover image for Cómo crear un Contrato Inteligente actualizable usando UUPS en Rootstock
Hector
Hector

Posted on • Updated on

Cómo crear un Contrato Inteligente actualizable usando UUPS en Rootstock

¡Hola, compañeros desarrolladores!

En este artículo, aprenderemos a cómo crear, desplegar y actualizar un Contrato Inteligente de UUPS en pocas palabras, usando HardHat, bibliotecas Open Zeppelin y la red Rootstock. Más preciso, crearemos un stablecoin pricefeeder para cualquier DApp.

Pero primero, un poco de teoría.

Patrones de Arquitectura

Hay muchos patrones de arquitectura para un contrato actualizable (por ejemplo: Diamond, Transparent, etc) pero ahora mismo, UUPS (o Universal Upgradeable Proxy Standard, Estándar Proxy Universal Actualizable) es el patrón recomendado de Open Zeppelin para crear nuevos y actualizables contratos inteligentes. Por supuesto, hay casos de uso diferentes los cuales pueden que requieran otros patrones, pero UUPS apunta a ser el estándar (y por eso, el keyword Universal está en su nombre).

Patrón UUPS

En UUPS, dividimos el contratos en 2 contratos: Proxy+Implementación.

La diferencia clave en el patrón UUPS es que el estado del contrato es almacenado en el Contrato Proxy, así que con cada actualización, no tenemos que pensar acerca del estado de la migración. El contrato proxy delegará todas las llamadas a la implementación usando la función delegatecall dentro de la función fallback, como puedes ver en la imagen de abajo. Usando delegatecall implica que la lógica del contrató usará el contexto del Proxy del contrato. Otra forma de pensar sobre esto es que el Proxy del contrato importará la implementación del contrato (la lógica) para usarla con su propio estado.

Image description

Otra cosa importante es que la actualización es desencadenada a través del Proxy pero actualmente, la Implementación del contrato es el que se actualizará a la nueva versión, ya que la implementación tiene la lógica para encontrar el slot de la memoria donde su dirección está almacenado dentro del Proxy.

Bueno, suficiente teoría. Empecemos a hacer un alimentador de precios stablecoin usando el patrón UUPS.

Alimentador de precios de Stablecoin UUPS

Primero, abre un terminal y ejecuta este comando para crear el archivo del proyecto y moverlo ahí:

mkdir my-uups-contract && cd my-uups-contract
Enter fullscreen mode Exit fullscreen mode

Ahora, vamos a instalar hardhat junto a otras herramientas requeridas:

npm install hardhat @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades @nomiclabs/hardhat-etherscan dotenv -d
Enter fullscreen mode Exit fullscreen mode

Luego ejecuta este comando para inicializar el proyecto hardhat (si no sabes qué elegir, elige javascript y presiona enter a todas las opciones)

npx hardhat
Enter fullscreen mode Exit fullscreen mode

Deberías tener una estructura con la carpeta así:

Image description

Ahora crea un archivo .env en la raíz de la carpeta del proyecto con el siguiente contenido:

RSK_NODE=https://public-node.testnet.rsk.co
PRIVATE_KEY=<private key of an address that you own>
PROXY_ADDRESS=<deployed proxy address>
Enter fullscreen mode Exit fullscreen mode

Nota: en este caso, usaremos una cuenta de cartera de Metamask para realizar las transacciones. Así que, necesitarás crear una nueva cartera, añadir la red de pruebas de Rootstock y obtener la clave privada de una de tus direcciones. Vas a necesitar RBTC para los despliegues y actualizaciones así que ve a un Faucet Rootstock para obtener algunos. Todo esto no debería tomarte más de 5 minutos.

Alerta: esto es una red de pruebas, así que está bien pero siempre ten cuidado sobre dónde pones tu clave privada, ya que corresponde a una dirección y el par de la dirección/clave privada puede ser la misma entre múltiples blockchains, lo que quiere decir que si alguien tiene tu clave privada, todos tus tokens pueden ser robados.

No te preocupes aún sobre la variable proxy. Vamos a cumplirlo luego.

Ahora, vamos a configurar el archivo hardhat.config.js con nuevas variables env y la red de Rootstock. Reemplaza lo que está dentro del archivo con este contenido:

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],
}
}
}
Enter fullscreen mode Exit fullscreen mode

Ahora ve al archivo de contratos, crea un archivo llamado StablecoinPriceFeeder.sol y añade este contenido:

// 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: inicializa la función del modificador de inicialización. Se comporta como el constructor. Se ejecuta por defecto cuando despliegas.
// Versiones posteriores: inicializa la función Vxxx con el modificador de reinicializar (versionNumber). Necesitará ser llamado manualmente
function initialize() public initializer {
__Ownable_init(); // Sólo requerido en la V1
__UUPSUpgradeable_init(); // Siempre requerido
}

// Usado cuando se actualiza a una nueva implementación, sólo el dueño(s) será capaz de actualizar
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
}

// Obtén el precio del stablecoin. Esta lógica puede ser sobreescrita si es necesario (por ejemplo: para usar un medianizer, el cual requiere una lógica más compleja)
// Añade los keywords "virtual override" para mantener una lógica actualizable mientras se mantiene la misma firma (la función nombre + mismo sigma)
function getPrice() public view virtual returns (uint256) {
// lógica para retirar el precio
return 999999999999999999;
}

// Obtén la versión de la implementación actual (sólo 255 actualizaciones son posibles). No te olvides de sobreescribir + virtual
function version() public pure virtual returns(uint8) {
return 1;
}
}

contract StablecoinPriceFeederV2 is StablecoinPriceFeeder {
// Comenzando desde la V2, por la inherencia, cualquier variable de estado nueva será almacenada LUEGO de las variables declaradas en la versión previa.
uint256 public testVariableV2;

// Inicializador para la implementación V2
// Modificador del Reinicializador (versionNumber)para la nueva función del número, es requerida para cada nueva versión.
function initializeV2(uint256 _testVariableV2) reinitializer(2) public onlyProxy onlyOwner {
__UUPSUpgradeable_init();

testVariableV2 = _testVariableV2;
}

// Aún utiliza la lógica getPrice() logic de la V1

// Actualiza la versión del output
function version() public pure virtual override returns(uint8) {
return 2;
}
}

contract StablecoinPriceFeederV3 is StablecoinPriceFeederV2 {
// Recuerda que hay un espacio de memoria reservado para testVariable2 aquí.

// Inicializador para la V3
function initializeV3() reinitializer(3) public onlyProxy onlyOwner {
__UUPSUpgradeable_init();
}

// Sobreescribe la lógica del precio de retirada
function getPrice() public view virtual override returns (uint256) {
// nueva lógica
return 1;
}

// Actualiza la versión del output
function version() public pure virtual override returns(uint8) {
return 3;
}
}
Enter fullscreen mode Exit fullscreen mode

Para obtener un mejor entendimiento de cada línea del código, lee los comentarios escritos. Observa que cada nueva versión es heredada de la anterior.

Pero el sumario es que vamos a importar los contratos Initializable, OwnableUpgradeable, UUPSUpgradeable desde las bibliotecas de OpenZeppelin y las funciones requeridas para que los patrones UUPS trabajen son de la función initialize() (también con __Ownable_init() y las funciones __UUPSUpgradeable_init(), declaradas dentro de estas) y la función _authorizeUpgrade la cual necesita ser decladara como sobrescrito para autorizar la actualización a una nueva versión. También declararemos los contratos V2 y V3 en el mismo archivo.

En la función version(), no te olvides de actualizar los números para cada versión. Si no, tus contratos actualizados tendrán números de versiones anteriores (a pesar de tener la funcionalidad de las nuevas versiones). Ya que todos los estados de las variables que definas serán almacenados en el Proxy, también puedes configurar una versión variable y actualizarla por una de cada actualización dentro de la función de inicializar, el cual actúa como constructor (sólo se ejecuta una vez). El número de la versión de reinicializar y la función version() del número de salida, deberían ser los mismos para la consistencia.

Ahora, crearemos el deploy.js y actualizaremos los scripts upgradeV2.js en el archivo de scripts:

Desplegar el script:

const { ethers, upgrades } = require("hardhat")

async function main () {
console.log("Deploying StablecoinPriceFeeder...")

const StablecoinPriceFeeder = await ethers.getContractFactory("StablecoinPriceFeeder")
const stablecoinPriceFeeder = await upgrades.deployProxy(StablecoinPriceFeeder, [], { initializer: "initialize" })
await stablecoinPriceFeeder.deployed()

console.log("StablecoinPriceFeeder (proxy) deployed to:", stablecoinPriceFeeder.address)
}

main()
Enter fullscreen mode Exit fullscreen mode

Actualizar el script:

const { ethers, upgrades } = require("hardhat")

const PROXY = process.env.PROXY_ADDRESS

if (!PROXY) {
throw new Error("You must specify the pricefeeder proxy address in .env file")
}

async function main () {
console.log("Upgrading StablecoinPriceFeeder to V2...")

const StablecoinPriceFeederV2 = await ethers.getContractFactory("StablecoinPriceFeederV2")
const stablecoinPriceFeederV2 = await upgrades.upgradeProxy(PROXY, StablecoinPriceFeederV2)

console.log("StablecoinPriceFeeder upgraded successfully", { version: await stablecoinPriceFeederV2.version() })
}

main()
Enter fullscreen mode Exit fullscreen mode

Ahora tenemos que compilar los contratos. Para hacer esto ejecuta:

npx hardhat clean && npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

Desplegar

Para desplegar el contrato ejecuta:

npx hardhat run scripts/deploy.js --network rsk >> deployResult.log
Enter fullscreen mode Exit fullscreen mode

Esto hará que tu dirección cree y envíe dos transacciones al nodo Rootstock, uno para desplegar el contrato de Implementación y el otro para desplegar el contrato Proxy.

Si todo salió bien, deberías ver el archivo deployResult.log que el StablecoinPriceFeeder desplegó exitosamente y una dirección Proxy registrada que lo confirme.

Nota: en caso que quieras verificar el contrato, tienes que actualizar el código del contrato al Explorador Rootstock. Busca para tu dirección proxy y ve al la pestaña del código para completar la información y hacer la verificación.

Actualizar

Para actualizar a la V2, toma la dirección del proxy que encontraste en el registro del despliegue, añade tu archivo .env y ejecuta el siguiente comando:

npx hardhat run /scripts/upgradeV2.js --network rsk >> upgradeV2ResultRSK.log
Enter fullscreen mode Exit fullscreen mode

De nuevo, deberías poder ver en el archivo upgradeV2ResultRSK.logel resultado de la actualización y la nueva versión del número registrado. ¡Esto quiere decir que has reemplazado el contrato de la implementación y actualizado a la V2!

¡Y eso es todo! Luego que definas e implementes tu lógica específica para retirar un precio (por ejemplo: llamar otros contratos/realizar alguna matemática), tiene un alimentador de precios funcional, ejecutado en Rootstock con algunos patrones UUPS, lo cual es lo recomendado por Open Zeppelin.

Tips Adicionales

En caso que necesites escalar, puedes desplegar múltiples proxys para una versión de contrato determinada.

Por defecto, en Hardhat ambas, la lógica y los contratos proxy son desplegados juntos para la V1. Para desplegar múltiples proxys usando la misma lógica, vuelve a ejecutar el script desplegado, especificando la misma versión del contrato.

Ejemplo:

Para desplegar múltiples proxys V2 usando la misma lógica del contrato (V2), primero actualiza de V1 a V2 usando el script para actualizar, especificando la lógica del contrato V2. Luego, usando el script desplegado, especifica la lógica del contrato V2. Si la ejecutas 5 veces, entonces 5 proxys nuevas serán desplegadas para la misma lógica, aparte de la primera que fue desplegada durante la actualización.

En resumen: tienes 6 proxys usando la misma lógica (la cual fue desplegada con la actualización a V2).

Para actualizar todos los proxys a una nueva versión, debes usar el script de actualizar, especificando una por una las direcciones de proxy correspondientes, ya que todos son instancias diferentes.

Este artículo es una traducción de Nicolas Vargas, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)