Estoy buscando información sobre los patrones de diseño que las personas han implementado dentro de los contratos inteligentes de Ethereum para permitir la modificación posterior a la implementación de las estructuras de datos.
Por ejemplo, supongamos que tengo un contrato que contiene una estructura que define una dirección. ¿Debería darme cuenta de que quiero agregar una propiedad de dirección de correo electrónico a la Dirección? ¿Hay alguna consideración de patrón de diseño previa a la implementación que me permita hacer esto?
Un ejemplo un poco más complejo... Si tuviera dos propiedades de un contrato llamadas 'pregunta' y 'respuesta', y de repente quiero tener múltiples respuestas posibles... ¿cómo haría uno para hacer tal cambio?
Mi problema/preocupación es que si interactúa con dicho contrato a través de un navegador, por ejemplo, podría simplemente actualizar el contrato al que apunta el front-end (después de la actualización). PERO, ¿cómo resuelve el problema de mantener los datos del contrato a través de la actualización? despliegues..?
gracias
Esto tiene que ser planeado desde el principio. Deberá diseñar su contrato inteligente teniendo en cuenta los siguientes 5 puntos:
Actualización de contratos rotos
Será necesario cambiar el código si se descubren errores o si es necesario realizar mejoras. No es bueno descubrir un error, pero no tener forma de lidiar con él.
...
Sin embargo, hay dos enfoques básicos que se utilizan con mayor frecuencia. El más simple de los dos es tener un contrato de registro que contenga la dirección de la última versión del contrato. Un enfoque más sencillo para los usuarios de contratos es tener un contrato que reenvíe llamadas y datos a la última versión del contrato.
Ejemplo 1: usar un contrato de registro para almacenar la última versión de un contrato
En este ejemplo, las llamadas no se reenvían, por lo que los usuarios deben buscar la dirección actual cada vez antes de interactuar con ella.
contract SomeRegister {
address backendContract;
address[] previousBackends;
address owner;
function SomeRegister() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner)
_;
}
function changeBackend(address newBackend) public
onlyOwner()
returns (bool)
{
if(newBackend != backendContract) {
previousBackends.push(backendContract);
backendContract = newBackend;
return true;
}
return false;
}
}
Hay dos desventajas principales en este enfoque:
Los usuarios siempre deben buscar la dirección actual, y cualquier persona que no lo haga corre el riesgo de utilizar una versión anterior del contrato.
Deberá pensar detenidamente sobre cómo tratar los datos del contrato cuando reemplace el contrato.
El enfoque alternativo es tener un contrato de reenvío de llamadas y datos a la última versión del contrato:
Ejemplo 2: Usar DELEGATECALL para desviar datos y llamadas
contract Relay {
address public currentVersion;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function Relay(address initAddr) {
currentVersion = initAddr;
owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
}
function changeContract(address newVersion) public
onlyOwner()
{
currentVersion = newVersion;
}
function() {
require(currentVersion.delegatecall(msg.data));
}
}
Este enfoque evita los problemas anteriores pero tiene sus propios problemas. Debe tener mucho cuidado con la forma en que almacena los datos en este contrato. Si su nuevo contrato tiene un diseño de almacenamiento diferente al primero, sus datos pueden terminar corruptos. Además, esta versión simple del patrón no puede devolver valores de funciones, solo reenviarlos, lo que limita su aplicabilidad. ( Implementaciones más complejas intentan resolver esto con código ensamblador en línea y un registro de tamaños de retorno).
Independientemente de su enfoque, es importante tener alguna forma de actualizar sus contratos, o se volverán inutilizables cuando se descubran los errores inevitables en ellos.
Creé una historia en Medium para esto con el título: Consideración de diseño esencial para Ethereum dApps (1): Contratos inteligentes actualizables
También podríamos recurrir a una solución de tipo VARIANT (sí, tampoco me gusta).
Tener un mapeo (uint => StorageItem).
El uint es un ID de campo.
El contrato StorageItem tendría un valor de cadena y un tipo entero. El tipo le permitiría convertir de la cadena al tipo final necesario.
Una debilidad aquí es que, en algún momento, la solidez admitirá nuevos tipos que querremos aprovechar.
Supongo que podría mantener el almacenamiento en un contrato separado como se sugiere aquí, pero la estructura mantendría los datos en un objeto JSON (cadena). Luego puedes hacer las modificaciones que necesites (ampliar o cambiar el modelo).
por ejemplo:
{"address": "Some Street","phone": 123456}
se convertiría
{"address": "Some Street","phone": 123456, "email":"someone@email.com"}
Puede manejar los valores faltantes en el lado del cliente o en el contrato de llamadas.
Hay una serie de enfoques mediante los cuales puede actualizar un Contract1
a Contract2
, manteniendo su estado (datos y saldo) con la misma dirección que antes.
¿Como funciona esto? Una forma es usar un contrato de proxy con una fallback
función en la que cada llamada/trx de método se delegue al contrato de implementación (que contiene toda la lógica).
Una llamada de delegado es similar a una llamada regular, excepto que todo el código se ejecuta en el contexto de la persona que llama (proxy), no del destinatario (implementación). Debido a esto, una transferencia en el código del contrato de implementación transferirá el saldo del proxy, y cualquier lectura o escritura en el almacenamiento del contrato se leerá o escribirá desde el almacenamiento del proxy.
En este enfoque, los usuarios solo interactúan con el contrato de proxy y podemos cambiar el contrato de implementación manteniendo el mismo contrato de proxy.
La fallback
función se ejecutará en cualquier solicitud, redirigiendo la solicitud a la implementación y devolviendo el valor resultante (usando códigos de operación).
Esta fue una explicación básica que es suficiente para que podamos trabajar con contratos actualizables. En caso de que desee profundizar en el código de contrato de proxy y los diferentes patrones de proxy, consulte estas publicaciones.
¿Cómo puedo escribir contratos inteligentes actualizables?
OpenZeppelin proporciona increíbles herramientas CLI y bibliotecas JS que se encargan de todos los contratos complejos anteriores proxy
, vinculándolos al contrato de implementación (lógico) y administrando todos los contratos que implementa utilizando la CLI para la capacidad de actualización, lista para usar.
Lo único que debe hacer es escribir sus contratos y usar la CLI de OpenZeppelin o las bibliotecas para implementar los contratos.
NOTA: Hay algunas limitaciones que debe tener en cuenta, en términos de cómo debe escribir sus contratos y cómo debe actualizarlos. También hay una serie de soluciones alternativas a estas limitaciones en esta publicación .
áxico