¿Qué patrones de diseño son apropiados para la modificación de la estructura de datos dentro de los contratos inteligentes de Ethereum?

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

Algunas de las respuestas en esta publicación pueden ayudarlo: ethereum.stackexchange.com/questions/2159/…

Respuestas (4)

Debes poner en consideración lo siguiente:

Esto tiene que ser planeado desde el principio. Deberá diseñar su contrato inteligente teniendo en cuenta los siguientes 5 puntos:

  1. Tienes que tener un buen ensayo de estrategias y tácticas . Porque el costo de actualizar su contrato inteligente realmente puede arruinar su vida.
  2. Mantenga sus contratos inteligentes modulares y reglas y lógica bastante separadas de la estructura de datos. Entonces, si necesita cambiar algo, cambiará solo el contrato relacionado y no necesitará cambiar muchos o todos los contratos.
  3. Debe estar preparado con una parada de emergencia o un disyuntor para poder detener todas las operaciones durante cualquier migración. Porque no desea estar en una situación en la que las personas aún puedan actualizar/insertar datos en la versión anterior del contrato inteligente mientras realiza la migración y posteriormente.
  4. Previamente , debe proporcionar la capacidad de leer todos los datos de su contrato inteligente . Por supuesto, puede hacer una lectura autorizada restringiendo la lectura de todos los datos al propietario o a cualquier otro usuario de confianza o incluso a otro contrato inteligente. Deberá leer la versión anterior de su contrato inteligente e insertar la nueva versión.
  5. Utilizará una de las siguientes estrategias para comunicarse con su contrato inteligente. Los copié de Smart Contact Best Practices :

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:

  1. 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.

  2. 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 Contract1a 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 fallbackfunción en la que cada llamada/trx de método se delegue al contrato de implementación (que contiene toda la lógica).ingrese la descripción de la imagen aquí

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.

ingrese la descripción de la imagen aquí

La fallbackfunció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 .