WEB3DEV Español

Cover image for Estándares de diseño para contratos inteligentes - Mantenimiento
Delia Viloria T
Delia Viloria T

Posted on

Estándares de diseño para contratos inteligentes - Mantenimiento

Image description

Introducción

Esta es la cuarta sección de la serie de cinco partes sobre cómo resolver fallos de diseño recurrentes a través de patrones de diseño convencionales y reutilizables. Para ello, analizaremos en Mantenimiento un grupo de estándares que proporcionan mecanismos para contratos operativos activos. En contraste con las aplicaciones distribuidas comunes, que se pueden actualizar cuando se detectan errores, los contratos inteligentes son irreversibles e inmutables. Esto significa que no hay forma de actualizar un contrato inteligente a menos que se escriba una versión mejorada que luego se implemente como un nuevo contrato.

Segregación de datos

Problema

Los datos del contrato y su lógica generalmente se mantienen en el mismo contrato, lo que lleva a un acoplamiento estrechamente entrelazado.

Al actualizar un contrato inteligente, lo que realmente sucede es que una nueva versión del contrato se implementa en la red y coexiste con la versión antigua. Como el antiguo contrato no se actualiza a una nueva versión, el almacenamiento acumulado todavía reside en la dirección anterior. Esto generalmente incluye datos importantes, como información del usuario, saldos de cuentas o referencias a otros contratos, que aún son necesarios en la nueva versión del contrato.

Escribir en el almacenamiento es una de las operaciones más caras en Ethereum. Leer sucesivamente cada entrada de almacenamiento y almacenarla en la nueva dirección, siempre que se actualice un contrato, no sería razonable desde el punto de vista económico.

Además, habría incluso una posibilidad que la transacción que lleva a cabo la migración del almacenamiento se quedaría sin gas, en caso que haya demasiadas entradas para almacenar.

Por su otra parte, toda la migración del almacenamiento necesitaría ser planeado durante el tiempo de la creación y se necesitaría incluir más lógica adicional para realizar la migración.

Solución

El patrón de segregación de datos separa la lógica del contrato de sus datos subyacentes. La segregación promueve la separación de preocupaciones e imita un diseño en capas (es decir, la capa de lógica, la capa de datos).

Es favorable diseñar el contrato del almacenamiento de forma muy genérica para que, una vez que sea creado, pueda almacenar y acceder a diferentes tipos de datos con la ayuda de los métodos setter y getter.

Capa de Almacenamiento

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

contract DataStorage {
mapping(bytes32 => uint256) uintStorage;

function getUintValue(bytes32 key) public returns (uint256) {
return uintStorage[key];
}

function setUintValue(bytes32 key, uint256 value) public {
uintStorage[key] = value;
}
}
Enter fullscreen mode Exit fullscreen mode

Capa de la Lógica

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
import "./DataStorage.sol";

contract Logic {
DataStorage dataStorage;

constructor(address _address) {
dataStorage = DataStorage(_address);
}

function f() public {
bytes32 key = keccak256(abi.encode("ACCOUNT_BALANCES", msg.sender));
dataStorage.setUintValue(key, 100 ether);
dataStorage.getUintValue(key);
}
}
Enter fullscreen mode Exit fullscreen mode

Satélite

Problema

Los contratos son inmutables. Cambiar la funcionalidad de un contrato requiere que se despliegue un nuevo contrato.

Solución

El patrón satélite permite modificar y reemplazar la funcionalidad del contrato. Esto se logra a través de la creación de contratos satélites separados que encapsulan cierta funcionalidad del contrato. Las direcciones de estos contratos satélites están almacenadas en la base del contrato.

Este contrato luego puede llamar a los contratos satélites cuando necesita referenciar ciertas funcionalidades, usando los puntos de la dirección almacenada. Pero cuando esas funciones necesitan cambiar su lógica para hacer mejoras de seguridad o ajustes necesarios de negocios, etc, modificar la funcionalidad es tan simple como crear contratos satélites nuevos y cambiar las direcciones satelitales correspondientes.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
import "@openzeppelin/contracts/access/Ownable.sol";

contract Base is Ownable {
uint256 public variable;
address satelliteAddress;

function setVariable() public onlyOwner {
Satellite s = Satellite(satelliteAddress);
variable = s.calculateVariable();
}
function updateSatelliteAddress(address _address) public onlyOwner {
satelliteAddress = _address;
}
}

contract Satellite {
function calculateVariable() public pure returns (uint256){
// calcula la variable
return 2 * 3;
}
}
Enter fullscreen mode Exit fullscreen mode

Registro del Contrato

Problema

Los participantes del contrato deben ser referidos a la última versión del contrato.

Solución

El patrón de registro es una forma de acercarse al proceso de actualizar un contrato. El patrón mantiene un registro de las diferentes versiones (direcciones) de un contrato y puntos a petición de la última. Antes de interactuar con un contrato, un usuario siempre tendría que consultar el registro para la última dirección del contrato. También es importante determinar cómo manejar los datos del contrato existente, cuando una versión antigua del contrato es reemplazada.

Una solución alternativa que apunta a la dirección del último contrato sería usar el Servicio de Nombre de Ethereum (Ethereum Name Service, ENS).

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "./ImplementationContract.sol";

contract Factory is Ownable {
address implementation;
address[] previousImpl;

event UpdateImplementation(address indexed oldImpl, address indexed newImpl);

function getLatestVersion() external view returns(address) {
return implementation;
}

function createNewVersion(
/** parámetros */
) external onlyOwner {
/** Validez de los parámetros previos al control */

bytes memory bytecode = type(ImplementationContract).creationCode;
/** Parámetros del constructor concatenados con creationCode con 32-byte */
bytecode = abi.encodePacked(
bytecode,
abi.encode(/** parámetros del constructor */)
);

bytes32 salt = keccak256(
abi.encodePacked(/** materiales azarosos, es decir,[block.timestamp, _msgSender(), etc] */)
);
address newImplAddress;

assembly {
newImplAddress := create2(0, add(bytecode, 32), mload(bytecode), salt)
}

/** actualiza la variable del estado */
previousImpl.push(implementation);
implementation = newImplAddress;

ImplementationContract(implementation).initialize(
/** parámetros de inicialización */
);

/** Hacer lógica adicional */

emit UpdateImplementation(previousImpl[previousImpl.length - 1], implementation);
}
}
Enter fullscreen mode Exit fullscreen mode

Relay del Contrato

Problema

Participantes del contrato tienen que ser referidos a la última versión del contrato;

Solución

Un relay es otra forma de manejar el proceso de actualización de un contrato. El patrón relay provee un método para actualizar un contrato (Implementación del Contrato) a una nueva versión mientras mantiene la dirección del contrato antiguo (la dirección Proxy). Esto se logra usando una forma del proxy del contrato que reenvía las llamadas y datos a la última versión del contrato.

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

abstract contract Proxy {
/**
* @dev Dice la dirección de la implementación donde cada llamada será delegada.
* @retorna la dirección de la implementación para la cual será delegada
*/
function implementation() public view virtual returns (address);

function _fallback() internal {
address _impl = implementation();
require(_impl != address(0), "Implementation is invalid");

assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)

switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}

/**
* @dev Recibe la función, permitiendo realizar un delegatecall a dicha implementación.
* Esta función retornará cualquier implementación que la llamada retorna
*/
receive() external payable {
_fallback();
}

/**
* @dev Recibe la función permitiendo realizar un delegatecall a dicha implementación.
* Esta función retornará cualquier implementación que la llamada retorna

*/
fallback() external payable {
_fallback();
}
}
Enter fullscreen mode Exit fullscreen mode

Otra desventaja a este acercamiento es que el diseño del almacenamiento de los datos, necesitan ser consistentes en nuevas versiones del contrato sino, puede que los datos se corrompan, lo cual quiere decir que la secuencia del almacenamiento no debe cambiar, sólo las adiciones son permitidas.

Conclusión

He descrito el grupo de patrón de Mantenimiento en detalle y he proveído códigos con ejemplos para ilustrarlo mejor. Te recomiendo que uses, al menos, uno de estos patrones en tu próximo proyecto de Solidity para probar tu entendimiento sobre este tópico. En el siguiente post, hablaremos y estaremos al siguiente y último patrón grupal: Seguridad.

Sígueme en LinkedIn para Seguir Conectados

https://www.linkedin.com/in/ninh-kim-927571149/

Este artículo fue escrito por BrianKim y traducido por Delia Viloria T. Su original se puede leer 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_eshttps://twitter.com/web3dev_es en Twitter.

Discussion (0)