La magia negra de los contratos metamórficos de Ethereum.

Si conoces algo acerca de los smart contracts de Ethereum, sabrás que son inmutables: una vez que se ha codificado el código de bytes, se mantiene a menos que el contrato llame a la función selfdestruct.

Si esto te parece bastante obvio, prepárate… porque el juego esta a punto de cambiar. El hardfork Constantinopla incluye el famoso EIP-1014, que introduce un nuevo código de operación llamado create2, que sirve para permitir interactuar con direcciones inexistentes en la cadena pero en las que se puede confiar. Se aplicaría en canales de estado que permitan interacciones contrafácticas en contratos.

Además, dicho código de operación permite un nuevo tipo de magia negra en la que, en condiciones adecuadas, se puede destruir un contrato y luego volver a implementarlo en la misma dirección con un nuevo código de bytes.

El curioso caso de los contratos CREATE2

La dirección de un contrato que se ha desplegado con CREATE2 depende de la dirección de la persona que lo llama, un parámetro de la sal suministrada, y el código de inicialización del contrato que se creará. Si se modifica alguno de estos argumentos, también se modificará la dirección del contrato. Esto parecería sugerir que, dado que no puede cambiar el código de inicialización del contrato, no puede cambiar el código de bytes final.

Incluso si eso fuera así, todavía existe el peligro de que un contrato sea destruido y recreado, lo que borrará su almacenamiento. Sin embargo, hay algo más que considerar: el código de inicialización del contrato puede no ser determinista, lo que significa que factores adicionales pueden hacer que el código de bytes resultante cambie. Cómo ejemplo de un contrato no determinista, el código de inicialización podría llamar a algún contrato externo con almacenamiento mutable y utilizar los datos devueltos para construir el nuevo código de bytes final.

Al utilizar un código de inicialización no determinista, los contratos pueden mutar repentinamente e intercambiar códigos de bytes arbitrarios. Si la capacidad de actualizarse ya parece errónea, esta característica puede convertirse en un monstruo, un contrato metamórfico.

Defensa contra las artes oscuras

Si la idea de que un contrato pueda sufrir una metamorfosis repentina e inesperadamente tampoco te gusta demasiado, te explico a continuación algunas cosas que puedes hacer para evitarlo:

  • Asegúrate de que selfdestruct no es accesible por ningún opcode, ya sea o a través de delegatecall o callcode. Si el contrato no puede editar selfdestruct, no se puede volver a implementar.
  • Asegúrate de que el contrato se implementó desde una fuente que no permite re-despliegues ( por ejemplo, no usar CREATE2 o almacenar cada implementación y evitar implementaciones duplicadas). También debes asegurarte de que el implementador no sea un contrato metamórfico en sí mismo.
  • Asegúrate de que el contrato con el que estas interactuando no haya cambiado a través del opcode EXTCODEHASH, o similares al inicio, antes de continuar con el resto de la transacción.

Para la mayoría de las aplicaciones honestas de CREATE2 (como canales estatales o creación de instancias contrafácticas), esto no debería ser un gran problema. En general, deberemos tener mucho cuidado al interactuar con cualquier contrato que pueda acceder a la función selfdestruct, es decir, editarla o cambiarla de forma peligrosa. Pero si buscas un contrato actualizable y liviano, con suerte uno con controles y gobernanza adecuados, !no busques mas!

La receta para el desastre

Hay más de una forma de hacerlo, pero una relativamente sencilla para crear contratos metamórficos es la siguiente:

  • Primero implementaremos un contrato de implementación, uno que no dependa de un constructor (pero que pueda tener una función regular como initialize que realice el mismo rol) y que también tenga la capacidad de llamar a selfdestruct.
  • Luego, guardaremos una referencia a la dirección del contrato de implementación en un lugar de almacenamiento fijo y conocido. El contrato de fábrica que inicializará la llamada a CREATE2 es la elección natural.
  • Ahora, utilizando CREATE2, implementamos nuestro contrato metamórfico con código de inicialización fijo y no determinista que recuperará la dirección de implementación de la función de fábrica, clonará el código de bytes en tiempo de ejecución y lo utilizará para implementar el código de bytes en tiempo de ejecución del contrato metamórfico. (También puedes utilizar un contrato transitorio intermedio con código de inicialización fijo que luego implementará el contrato metamórfico a través de CREATE, e inmediatamente hacer una llamada a selfdestruct).
  • Cuando llegue el momento de cambiar el contrato metamórfico, simplemente destruiremos el contrato existente llamando a selfdestruct, implementaremos el contrato, y esta vez clonaremos la nueva implementación. Dado que el código de inicialización es el mismo, !la dirección del contrato metamórfico también será la misma!

Tanto éste método, así como un método para prevenir la implementación de contratos metamórficos sin dejar de tener acceso a CREATE2, puedes encontrarlo en este repositorio, y existe también un desglose detallado de cómo construir un código de inicialización que se puede utilizar para hacer la referencia y clonar un contrato. Puedes encontrarlo aquí. !Mucho cuidado con éstos contratos y bienvenido a las artes oscuras!

El hermanastro feo de los proxies transparentes

Por último, una comparación rápida con el método más común para crear contratos con la capacidad de actualizarse, son los proxies transparentes:

  • Las actualizaciones con proxies transparentes persistirán en el almacenamiento después de las actualizaciones, mientras que los contratos metamórficos borrarán el estado por completo (incluido el saldo de la cuenta; ten cuidado de no reenviar la dotación de selfdestruct a la misma dirección). Esto hace que los poxies transparentes sean la elección natural para los contratos ERC20 o ERC721 actualizables, mientras que los contratos metamórficos son posiblemente más adecuados para los contratos de identidad ERC725 u otros contratos auto-soberanos.
  • La sobrecarga de llamar a un contrato metamórfico se reducirá en comparación con llamar a un proxy transparente, ya que el proxy primero debe verificar a la identidad de quien lo llama para asegurarse de que no sea el administrador de actualización y luego hacer delegatecall a otro contrato lógico.
  • El proceso de actualización no será tan sencillo para los contratos metamórficos, ya que las operaciones de autodestrucción se registran en el sub-estado de la transacción y se realizan al final de una transacción. Esto significa que una actualización requerirá de dos transacciones, por lo que el código de contrato estará vacío (y susceptible de un uso intermedio) entre ambas transacciones. Por otro lado, un proxy transparente que llama a selfdestruct será eliminado, pero en un contrato metamórfico aún se podría recuperar.
  • Ninguno de los métodos permite utilizar el constructor durante la inicialización del contrato. En su lugar, se debe proporcionar una función de inicialización que será llamada inmediatamente después de configurar el nuevo contrato con la implementación clonada. La excepción a esta regla es si se usa un contrato transitorio intermedio, implementado vía CREATE2, para implementar el contrato metamórfico vía CREATE. En este caso, aún se pueden usar constructores al implementar el contrato metamórfico.

Gracias a 0age y Jason Carver por descubrir éste fenómeno, y a Martin Holst Swende por el mecanismo de clonación que utilizan los contratos metamórficos, así como a todo el equipo de Zeppelin que ha contribuido a la exploración y descubrimiento sobre este tema.