WEB3DEV Español

Cover image for Hacer que tu Contrato Inteligente de Solidity sea Actualizable
Hector
Hector

Posted on

Hacer que tu Contrato Inteligente de Solidity sea Actualizable

Y el prompt ChatGPT usado

En el mundo blockchain, los contratos inteligentes son los anclas para cualquier aplicación descentralizada. Como cualquier software, los contratos inteligentes también necesitan actualizaciones y arreglar bugs regularmente. Pero, dada la naturaleza inmutable de la blockchain, lograr esto no es tan directo. Esta guía te llevará a través de cómo hacer que tu contrato inteligente sea actualizable, asegurando un ciclo de desarrollo más fluido y seguro para el futuro.

Este artículo está dentro de una serie de artículos donde intentamos desarrollar un “cripto banco”. Puedes revisar el primer artículo que ofrece esta idea aquí. En esta parte, primero mostraremos el contrato inteligente originalmente usado. Luego, mostraremos cómo hicimos que fuese actualizable usando un prompt específico, dado por ChatGPT. Finalmente, mostraremos un ejemplo de añadir funcionalidad al contrato inteligente.

El Contrato Inteligente Original

Nuestro punto de partida es un contrato inteligente que opera como un sistema de cheques, donde los usuarios pueden depositar y retirar fondos y crear “cheques” para que otros lo reclamen. Aquí está el contrato original, escrito en Solidity.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract CheckClaim {
using ECDSA for bytes32;

mapping(address => uint256) public balances;
mapping(address => mapping(uint256 => bool)) public usedNonces;

function deposit() public payable {
balances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}

function depositCheck(address recipient, bytes memory signature, uint256 amount, uint256 nonce) public {
bytes32 message = prefixed(keccak256(abi.encodePacked(recipient, nonce, amount)));
address signer = recoverSigner(message, signature);

// Establece el destinatario a la dirección del remitente si se establece en la dirección(0)
// ¡Esto permite que cualquiera pueda reclamar el cheque!
if (recipient == address(0)) {
recipient = msg.sender;
}

require(!usedNonces[signer][nonce], "The check has already been used");
require(signer != address(0), "Invalid signature");
require(balances[signer] >= amount, "Insufficient balance");

balances[signer] -= amount;
balances[recipient] += amount;
usedNonces[signer][nonce] = true;
}

function claimCheckAndWithdraw(address recipient, bytes memory signature, uint256 amount, uint256 nonce) public {
// Primero, reclama el cheque usando la función depositCheck
depositCheck(recipient, signature, amount, nonce);

// Luego, retira la cantidad reclamada al balance actual del usuario, usando la función existente para retirar
withdraw(amount);
}

function prefixed(bytes32 hash) internal pure returns (bytes32) {
return hash.toEthSignedMessageHash();
}

function recoverSigner(bytes32 message, bytes memory signature) internal pure returns (address) {
return message.recover(signature);
}
}
Enter fullscreen mode Exit fullscreen mode

En detalle, las funciones claves del contrato son:

  • deposit(): esta función permite al usuario depositar ethers en el contrato. Los ethers son añadidos al balance del usuario, en el contrato.
  • withdraw(uint256 amount): esta función permite al usuario retirar ethers desde su balance, en el contrato.
  • depositCheck(address recipient, bytes memory signature, uint256 amount, uint256 nonce): esta función permite al usuario depositar un cheque que puede ser reclamado por un destinatario.
  • claimCheckAndWithdraw(address recipient, bytes memory signature, uint256 amount, uint256 nonce): esta función es una combinación de depositCheck y withdraw. Permite al receptor reclamar un cheque y retirar la cantidad del cheque, desde su balance en el contrato.

El contrato también incluye dos funciones de ayuda: recoverSigner(bytes32 message, bytes memory signature) y prefixed(bytes32 hash). Estas funciones son usadas para verificar la firma del cheque, en la función depositCheck

Hacer que el Contrato sea Actualizable

Para hacer que este contrato sea actualizable, usaremos el patrón Proxy junto a las bibliotecas de OpenZeppelin TransparentUpgradeableProxy, Initializable y TimelockController.

Aquí está el contrato modificado en tres partes. Primero, el contrato actualizado:

// The updated original contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract CheckClaimV1 is Initializable {
using ECDSA for bytes32;

mapping(address => uint256) public balances;
mapping(address => mapping(uint256 => bool)) public usedNonces;

function initialize() public initializer {
}

// Otras funciones del contrato original deberían ir aquí
// depositar, retirar, ...
}
Enter fullscreen mode Exit fullscreen mode

El Proxy:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract YourProxy is TransparentUpgradeableProxy {
constructor(address _logic, address _admin, bytes memory _data)
TransparentUpgradeableProxy(_logic, _admin, _data) {
}
}
Enter fullscreen mode Exit fullscreen mode

El TimeLockController:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/governance/TimelockController.sol";

contract YourTimelockController is TimelockController {
constructor(uint256 minDelay, address[] memory proposers, address[] memory executors)
TimelockController(minDelay, proposers, executors) {
renounceRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
}
Enter fullscreen mode Exit fullscreen mode

Esto fue creado luego de darle el siguiente prompt a ChatGPT:

Diseña un contrato inteligente actualizable en Solidity, que sea compatible con cualquier blockchain basada en la EVM (como Ethereum, Polygon, etc.) utilizando el Patrón proxy. Usa las bibliotecas de OpenZeppelin TransparentUpgradeableProxy, Initializable y TimelockController

El contrato actualizado debe contener:

  • Una demora mínima de 72 horas desde el momento que una actualización comienza hasta el momento que toma efecto, aplicado por TimelockController.
  • Un evento que sea ejecutado al comienzo del proceso de la actualización, conteniendo la dirección de la nueva implementación del contrato para la verificación del usuario. Esta demora se hace para permitir a los usuarios a que validen los cambios antes que sean implementados.
  • Una estructura que permita añadir y modificar funciones fácilmente, en versiones futuras mientras preserva la estructura de las funciones existentes. También debería permitir añadir nuevos estados de variables sin que necesite la migración de datos.
  • Una nomenclatura para la implementación del contrato, lo cual debería ser sufijo con V1. Este contrato no debe ser consciente del proxy.

Requisitos:

El código debería compilar sin placeholders, excepto por las funciones del contrato original, que están sin modificar.

  • Mantén la solución tan simple como sea posible
  • Cada contrato debería ser escrito en su propio bloque de código. Esperamos tres códigos de bloque, uno por cada contrato.
  • Provee un script de desarrollo, compatible con Hardhat. Tras el despliegue, el rol de administrador del TimeLockController, debería ser renunciado.
  • Ambos, el proxy y el TimeLockController de los contratos heredados, deberían mantenerse básicos (sólo el constructor), ya que todas las funciones necesarias (upgradeTo, schedule, execute) ya están incluídas en el contrato base. La mayoría del trabajo será hecho en el script de despliegue y el script de actualización.
  • El último prototipo del constructor TimeLockController, tiene un cuarto argumento adicional TimelockController(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin);. El último argumento es el administrador. Para simplificar, coloca el administrador como el desplegador (msg.sender). También renuncia al rol de administrador, llamando renounceRole(DEFAULT_ADMIN_ROLE, msg.sender); dentro del constructor.

Aquí está el contrato inteligente:
(código original del contrato inteligente)

Aquí está un diagrama que explica las interacciones entre las diferentes partes:

Image description

Desplegando el contrato en Remix

Puedes desplegar el contrato usando la línea de comando o un IDE online como Remix. Si estás usando la línea de comando, puedes tener despliegue del script auto generado por ChatGPT. En el prompt descrito en la sección anterior, le pedimos a ChatGPT para que lo generara en HardHat. Siéntete libre de modificar las líneas correspondientes del prompt para pedir por otros tipos de despliegues de script. También, revisa la sección de Anexos para los resultados obtenidos.

Es importante revisar el orden en el que desplegamos el contrato. Necesitamos desplegar ambos: la implementación del contrato y el TimelockController antes de desplegar el contrato Proxy. Esto es necesario, ya que los dos argumentos del contrato proxy son direcciones de despliegue.

Aquí está el diagrama que representa el orden:

Image description

Finalmente, uno debería asegurarse que la función renounceRole() del TimeLockController. Esto es importante para evitar que cualquier individuo tenga control total sobre el contrato y hacer que sea descentralizado. En nuestro caso, ChatGPT ha puesto el renounceRole() en el constructor en sí mismo, así que no hay necesidad de llamarlo en el momento del despliegue.

Nota: para Remix, cuando desplegamos el proxy del campo _DATA del constructor, debería colocarse a “0x” para una cadena vacía de bytes.

Actualizando el Contrato Inteligente

Actualizar un contrato envuelve crear una nueva versión que introduce variables y funciones adicionales, sin eliminar las existentes. Esto asegura la continuidad operacional y previene interrupciones. Permite mejoras sin comprometer la compatibilidad o la necesidad de migrar datos.

Una vez que el contrato nuevo haya sido escrito, primero, necesitarás proponerlo usando la función schedule(), después que la demora configurada (configurado en la fase de despliegue) haya expirado, puedes invocar la función execute() para desplegar el nuevo contrato. Ambos schedule() y execute() son partes del TimeLockContract.

Aquí también puedes pedirle a ChatGPT para que genere los scripts necesarios para ejecutar y actualizar (ve la sección de Extras para que veas el script que obtuve). Personalmente, no fui tan lejos con lo que respecta a actualizar el contrato actual, ya que no tenía cosas nuevas que añadir. Actualizaré este artículo en caso que suceda.

Conclusión

¡Y ahí lo tienes! Hacer que tus contratos inteligentes de Solidity sean actualizables, es un paso invaluable para asegurar que tus proyectos puedan soportar el paso del tiempo. Bien sea que eres un desarrollador experimentado de blockchain o una persona nueva en el espacio, espero que esta guía te sea útil en el esfuerzo como futuro desarrollador. ¡Feliz codeo!

Anexo

El script de despliegue que usé:



const hre = require("hardhat");

async function main() {
const [deployer, admin] = await hre.ethers.getSigners();
console.log("Deployed by:", deployer.address, "admin", admin.address);

const minDelay = 72 * 60 * 60; // 72 hours in seconds
const proposers = [admin.address];
const executors = [admin.address];

const CheckClaimV1 = await ethers.getContractFactory("CheckClaimV1");
const checkClaimV1 = await CheckClaimV1.deploy();
await checkClaimV1.deployed();
console.log("CheckClaimV1 deployed at:", checkClaimV1.address);

const CheckClaimTimelock = await ethers.getContractFactory("CheckClaimTimelock");
const timelock = await CheckClaimTimelock.deploy(minDelay, proposers, executors, admin.address);
await timelock.deployed();
console.log("CheckClaimTimelock deployed at:", timelock.address);

const CheckClaimProxy = await ethers.getContractFactory("CheckClaimProxy");
const proxy = await CheckClaimProxy.deploy(checkClaimV1.address, timelock.address, '0x');
await proxy.deployed();
console.log("CheckClaimProxy deployed at:", proxy.address);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
}); 

>Este artículo es una traducción de TechExplorer, hecha por Héctor Botero. Puedes encontrar el artículo original [aquí](https://medium.com/coinmonks/making-your-solidity-smart-contract-upgradeable-58c30311c700).
>Sería genial escucharte en nuestro [Discord](https://discord.gg/Qf7utym), puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas. 
>Si prefieres puedes escribirnos a [@web3dev_es](https://twitter.com/web3dev_es) en Twitter.
Enter fullscreen mode Exit fullscreen mode

Discussion (0)