¡Cómo escribir contratos inteligentes actualizables con solidez!

Mientras trabajamos en la plataforma de auditorías de seguridad de contratos inteligentes QuillAudits en QuillHash, dedicamos la mayor parte del tiempo a investigar sobre las mejores prácticas de seguridad en los contratos inteligentes. QuillAudits considera las siguientes facetas distintas y cruciales del código de contrato inteligente: si el código es seguro. Si el código corresponde a la documentación (incluido el documento técnico). Si el código cumple con las mejores prácticas en el uso eficiente del gas, la legibilidad del código, etc. Un enfoque para actualizar los contratos debe estar en la armadura para evitar daños causados ​​por errores de programación después de que el contrato se implementó.

El tema de los contratos actualizables no es muy nuevo en el mundo de ethereum. Existen algunos enfoques diferentes para actualizar los contratos inteligentes.

Algunos enfoques que consideramos en el desarrollo son: -

  1. Lógica y datos separados.
  2. Sistema de contratos inteligentes parcialmente actualizable.
  3. Lógica y datos separados en pares de valores clave.
  4. Almacenamiento eterno con contrato proxy

Con los primeros tres enfoques, el contrato puede actualizarse señalando a los usuarios que usen el nuevo contrato lógico (a través de un solucionador como ENS) y actualizando los permisos del contrato de datos para permitir que el nuevo contrato lógico pueda ejecutar los establecedores. no es necesario hacer esta redirección y es un enfoque muy flexible para actualizar contratos inteligentes. Descubrimos que el almacenamiento eterno con enfoque de contrato proxy es perfecto hasta ahora.

Los lectores son bienvenidos a comentar si conoce algún defecto en este enfoque. Será muy útil para la comunidad de desarrolladores.

Hay una buena razón a favor y en contra de poder actualizar los contratos inteligentes. La buena razón es que todos los hacks recientes se basaron en un error de programación y podrían solucionarse muy fácilmente si fuera posible actualizar esos contratos.

Sin embargo, la capacidad de actualizar los contratos inteligentes después de que se implementaron va en contra de la ética y la inmutabilidad de blockchain. Las personas deben confiar en que eres un buen chico. Una cosa que podría tener sentido sería tener actualizaciones multi-sig, donde el "OK" de varias personas es necesario antes de que se implemente un nuevo contrato y pueda acceder al almacenamiento. Creo que son los registros de almacenamiento los que deben ser inmutables en blockchain. Logic debe mejorarse con el tiempo como en todas las prácticas de ingeniería de software. Nadie puede garantizar el desarrollo de software libre de errores en la primera versión. Por lo tanto, los contratos inteligentes actualizables con algún mecanismo de gobierno de actualización pueden salvar muchos hacks.

En esta publicación, tocaré el mecanismo de actualización y en la publicación de seguimiento trataré de encontrar el mejor mecanismo de gobernanza de actualización de contratos.

¡Entonces comencemos con el enfoque de implementación!

  • Lo más importante a tener en cuenta al actualizar los contratos es cómo preservar el estado del contrato original en el contrato actualizado.
  • El estado del contrato puede separarse de la funcionalidad del contrato. Este enfoque permite que múltiples contratos compartan el mismo estado.
  • En este enfoque, un contrato proxy actuará como un contrato de almacenamiento inmutable y el contrato delegado contendrá la funcionalidad.
  • La estructura de almacenamiento de ambos contratos debe ser similar.
  • Para actualizar la lógica del contrato, debemos informar al contrato proxy la dirección del nuevo contrato de delegado.
  • Cuando se envía una transacción a un contrato proxy, no se conoce la función especificada en la transacción.
  • El contrato de proxy representará la transacción a lo que llamaremos un contrato "delegado" (que contiene la lógica de funcionalidad). Esto se hace utilizando el código EVM nativo, llamada delegada.
Con la llamada delegada, un contrato puede cargar dinámicamente el código de una dirección diferente en tiempo de ejecución. El almacenamiento, la dirección actual y el saldo aún se refieren al contrato de llamada, solo el código se toma de la dirección llamada.
Cuando un contrato proxy usa la funcionalidad de un contrato delegado, se realizarán modificaciones de estado en el contrato proxy. Esto significa que los dos contratos deben definir la misma memoria de almacenamiento. El orden en que se define el almacenamiento en la memoria debe coincidir entre los dos contratos.

Inicialmente implementaremos estos contratos: -

  1. Contrato de almacenamiento clave (Contiene el estado compartido)
  2. Delegado contrato V1 y Delegado contrato V2
  3. Contrato de proxy (contiene la funcionalidad de llamada delegada)

Contrato de almacenamiento de claves: -

Contiene un almacenamiento común para todas las variables de estado de almacenamiento que se compartirán entre todas las versiones del contrato inteligente. También contiene funciones de obtención y establecimiento para actualizar y obtener el valor del estado del contrato delegado.

Cualquier contrato de delegado puede consumir el contrato de almacenamiento de claves una vez implementado el contrato de proxy. No podemos crear nuevos captadores y establecedores una vez que se haya implementado el almacenamiento de claves, por lo que debemos considerar esto al diseñar la versión inicial del contrato inteligente.

El mejor enfoque es hacer asignaciones para cada tipo de campo en el contrato de almacenamiento de claves. Donde la clave de la asignación será el nombre de la clave simplemente en bytes y el valor del tipo declarado en la asignación.

Por ejemplo: - mapeo (bytes32 => uint)

Ahora podemos usar esta asignación para establecer y obtener cualquier valor entero del contrato delegado llamando a la función getter y setter de almacenamiento de claves para el tipo uint. Por ejemplo: podemos establecer el suministro total con la clave "totalSupply" y con cualquier valor uint.

Pero espere que algo falte, ahora cualquiera puede llamar a nuestra función de obtención y configuración de contratos de almacenamiento de claves y actualizar el estado de almacenamiento que está utilizando nuestro contrato de delegado. Por lo tanto, para evitar este cambio de estado no autorizado, podemos usar la dirección del contrato proxy como clave de mapeo.

mapping (address => mapping (bytes32 => uint)) uintStorage
En nuestra función setter:
función setUintStorage (bytes32 keyField, uint value) public {
uintStorage [msg.sender] [keyField] = valor
}

Ahora, como estamos usando la dirección msg.sender en la función setter y solo este cambio de estado se reflejará en el estado del contrato proxy cuando usa la función getter para obtener el estado. De manera similar, podemos crear otras asignaciones de estado junto con las funciones getter y setter como se muestra en el código a continuación: -

Contrato delegado: -

El contrato de delegado contiene la funcionalidad real de dApp. También contiene una copia local del contrato de KeyStorage. En nuestro dApp, si incluimos cierta funcionalidad y luego encontramos un error en el contrato implementado, en ese caso podemos crear una nueva versión de delegado contrato.

En el siguiente código, se implementa la versión 1 del contrato de delegado ("DelegateV1.sol").

Después de implementar DelegateV1, notamos que cualquier usuario puede establecer el número de propietarios. Por lo tanto, ahora queremos actualizar el contrato inteligente para que solo el propietario del contrato pueda establecer el número de propietarios.

No podemos cambiar el código del contrato ya implementado en ethereum. Entonces, la solución obvia es crear un nuevo contrato y el nuevo contrato también contendrá una copia local del contrato de Key Value. Aquí estamos creando un contrato DelegateV2.sol con el modificador onlyOwner agregado.

Ahora hemos creado un nuevo contrato, pero el almacenamiento del contrato anterior no está disponible en la nueva versión del contrato, por lo que podemos incluir una referencia al contrato de almacenamiento de clave real en cada versión del contrato delegado. De esta manera, cada versión del contrato delegado comparte el mismo almacenamiento. Pero una cosa no es deseable aquí, tenemos que informar a cada usuario sobre la dirección de la versión actualizada del contrato para que puedan usar el contrato actualizado. Suena estúpido. Por lo tanto, no almacenaremos una copia real del contrato de almacenamiento clave en cada versión del contrato delegado. Para obtener un contrato de proxy de almacenamiento compartido que viene al rescate, pasemos al contrato de proxy.

Contrato de representación: -

Un contrato de proxy utiliza el código de operación delegatecall para reenviar llamadas de función a un contrato de destino que puede actualizarse. A medida que delegatecall retiene el estado de la llamada de función, la lógica del contrato de destino puede actualizarse y el estado permanecerá en el contrato de proxy para que la lógica del contrato de destino actualizada lo use. Al igual que con delegatecall, el msg.sender seguirá siendo el del llamante del contrato de representación.

Una llamada de delegado puede cargar código dinámicamente desde una dirección diferente en tiempo de ejecución. El almacenamiento, la dirección actual y el saldo aún se refieren al contrato de llamada, solo el código se toma de la dirección llamada.
Por lo tanto, solo tenemos que pasar la dirección de la nueva versión del contrato al proxy mediante la función upgradeTo.

El código del contrato proxy es bastante complicado en la función alternativa ya que aquí se usa el código de ensamblaje de llamada de delegado de bajo nivel.

Vamos a descomponerlo simplemente lo que se está haciendo en el código de ensamblaje: -

delegatecall (gas, _impl, add (datos, 0x20), mload (datos), 0, 0);

En la función anterior, la llamada delegada está llamando al código en la dirección "_impl" con la entrada "add (data, 0x20)" y con el tamaño de la memoria de entrada "mload (data)", la llamada delegada devolverá 0 en caso de error y 1 en caso de éxito y resultado de la función alternativa es lo que devolverá la función de contrato llamada.

En el contrato proxy, estamos extendiendo el contrato StorageState que contendrá una variable global para almacenar la dirección del contrato keyStorage.

El orden de extender el contrato de estado de almacenamiento antes del contrato de propiedad es importante aquí. Este contrato de estado de almacenamiento será extendido por nuestros contratos de delegado y todas las funciones lógicas ejecutadas en el contrato de delegado serán del contexto del contrato de proxy. El orden de la estructura de almacenamiento de Proxy El contrato y el contrato del delegado deben ser iguales.

Ahora el usuario siempre interactuará con dapp a través de la misma dirección del contrato de proxy y el estado del contrato de almacenamiento de claves parece compartirse entre todas las versiones del contrato, pero en realidad solo el contrato de proxy contiene la referencia al contrato de almacenamiento de claves real. Los contratos delegados contienen una copia local del contrato de almacenamiento de claves para obtener el getter, la lógica de las funciones del setter y tener una estructura de almacenamiento similar, como un contrato de proxy, pero los cambios de almacenamiento reales se realizan solo desde el contexto del contrato de proxy.

Implementándolo y probándolo juntos: -

Aquí la salida de los casos de prueba será: 10 10 y 20

Estamos llamando a getNumberOfOwners () tres veces en caso de prueba. Primero para obtener el cambio de estado por contrato de DelegateV1. Segunda vez para que DelegateV1 modifique el estado del contrato de DelegateV2 y logramos completamente retener el estado modificado por DelegateV1 y la tercera vez para obtener la modificación de estado realizada por contrato DelegateV2.

Tenga en cuenta aquí que estamos llamando a getNumberOfOwners () cada vez desde la misma dirección del contrato proxy, por lo que logramos actualizar con éxito la funcionalidad de nuestro contrato sin perder el estado anterior.

Si llamamos a setNumberOfOwners () desde cualquier otra dirección, excepto la cuenta [2], que es la dirección del propietario del contrato, arrojará un error de reversión.

Vamos a terminar el artículo con algunos diagramas: -

llame a la actualización a DelegateV2

Puedes ver el código completo aquí: -

https://github.com/Quillhash/upradeableToken.git

Gracias por leer. Espero que esta guía le haya sido útil y lo ayude a escribir contratos inteligentes actualizables con solidez y también consulte nuestras publicaciones de blog anteriores.
En QuillHash, entendemos Ethereum Blockchain y tenemos un buen equipo de desarrolladores que pueden desarrollar aplicaciones de blockchain como Smart Contracts, dApps, DeFi, DEX en Ethereum Blockchain.
Para estar al día con nuestro trabajo, únase a nuestra comunidad: -
Telegram | Twitter | Facebook | LinkedIn

Referencias

https://blog.colony.io/writing-upgradeable-contracts-in-solidity-6743f0eecc88
https://medium.com/level-k/flexible-upgradability-for-smart-contracts-9778d80d1638
https://medium.com/cardstack/upgradable-contracts-in-solidity-d5af87f0f913
https://blog.zeppelinos.org/smart-contract-upgradeability-using-eternal-storage/
https://medium.com/rocket-pool/upgradable-solidity-contract-design-54789205276d