¡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 libro blanco). 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 la implementación del contrato.

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 desarrollo son: –

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

Los lectores son bienvenidos a comentar si conocen 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 ataques recientes se basaron en errores 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. La gente debe 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.La lógica 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 gobernanza de actualización pueden ahorrar muchos hacks.

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

¡¡Entonces comencemos con el enfoque de implementación !!

Con la llamada de delegado, un contrato 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 se toma el código de la dirección llamada.

Cuando un contrato de proxy utiliza la funcionalidad de un contrato de delegado, se producirán modificaciones de estado en el contrato de proxy. Esto significa que los dos contratos deben definir la misma memoria de almacenamiento. El orden en el que se define el almacenamiento en la memoria debe coincidir entre los dos contratos.

Implementaremos estos contratos inicialmente: –

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 de contrato inteligente. También contiene funciones getter y setter para actualizar y obtener el valor del estado del contrato delegado.

El contrato de almacenamiento de claves puede ser consumido por cualquier contrato delegado a través del contrato de proxy una vez implementado. No podemos crear nuevos captadores y definidores una vez 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 mapeos para cada tipo de campo en el contrato de almacenamiento de claves, donde la clave del mapeo será el nombre de la clave simplemente en bytes y el valor será del tipo declarado en el mapeo.

Por ejemplo: – mapeo (bytes32 = & gt; uint)

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

Pero espere, algo falta, ahora cualquiera puede llamar a nuestra función getter y setter de contrato de almacenamiento de claves y actualizar el estado del almacenamiento que está siendo utilizado por nuestro contrato delegado. Entonces, para evitar este cambio de estado no autorizado, podemos usar la dirección del proxy contrato como la clave del mapeo.

mapeo (dirección = & gt; mapeo (bytes32 = & gt; uint)) uintStorage

En nuestra función de establecedor:

function setUintStorage (bytes32 keyField, uint value) public {

uintStorage [msg.sender] [keyField] = valor

}

Ahora que estamos usando msg.sender address en la función setter y solo este cambio de estado se reflejará en el estado del contrato proxy cuando use 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 siguiente código: –

Contrato de delegado: –

El contrato delegado contiene la funcionalidad actual de dApp, también contiene una copia local del contrato de KeyStorage, en nuestra dApp, si incluimos una determinada funcionalidad y luego encontramos un error en el contrato desplegado, en ese caso podemos crear una nueva versión del contrato delegado.

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

Después de implementar DelegateV1, notamos que el número de propietarios puede ser establecido por cualquier usuario. Así que 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. La solución tan obvia es crear un nuevo contrato y el nuevo contrato también contendrá una copia local del contrato de valor clave. 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 tanto, podemos incluir una referencia al contrato de almacenamiento de claves 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í, necesitamos 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 de claves. en cada versión del contrato de 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 se puede actualizar. Como delegatecall conserva el estado de la llamada a la función, la lógica del contrato de destino se puede actualizar y el estado permanecerá en el contrato de proxy para que lo utilice la lógica del contrato de destino actualizado. Al igual que con delegatecall, msg.sender seguirá siendo el del llamador del contrato de proxy.

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

Desglosemos simplemente lo que se está haciendo en el código ensamblador: –

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

En la función anterior, la llamada de delegado está llamando al código en la dirección “_impl” con la entrada “agregar (datos, 0x20)” y con el tamaño de la memoria de entrada “mload (datos)”, la llamada de delegado devolverá 0 en caso de error y 1 en caso de éxito y el resultado de la función de reserva es lo que devolverá la función de contrato llamada.

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

Aquí es importante el orden de extensión del contrato de estado de almacenamiento antes del contrato de propiedad. Este contrato de estado de almacenamiento será extendido por nuestros contratos de delegado y toda la lógica de funciones ejecutadas en el contrato de delegado será desde el contexto del contrato de proxy. La estructura del contrato de apoderado y el contrato de 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 ser compartido 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 copia local del contrato keyStorage para obtener el getter, la lógica de las funciones del setter y tener una estructura de almacenamiento similar como el contrato de proxy, pero los cambios de almacenamiento reales se realizan solo desde el contexto del contrato de proxy.

Implementarlo y probarlo juntos: –

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

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

Tenga en cuenta que estamos llamando a getNumberOfOwners () cada vez desde la misma dirección del contrato de 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.

Terminemos el artículo con algunos diagramas: –


Puedes ver el código completo aquí:

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

Gracias por leer. Esperamos que esta guía le haya resultado útil y le ayude a redactar contratos inteligentes actualizables con solidez y También consulte nuestras publicaciones de blog anteriores .

En QuillHash , entendemos la Ethereum Blockchain y contar con un buen equipo de desarrolladores que puedan desarrollar aplicaciones de cadena de bloques 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