Si bien existen diferentes formas en las que podemos “
actualizar” la lógica de un
SmartContract como por ejemplo usando un modelo de “
módulos arbitrarios” aprovechándose de que estos contratos pueden ejecutar código de manera arbitraría. Pero el patrón más usado es el patrón “
Proxy”.
Figura 2: Idea general del patrón Proxy
Pero hoy nos vamos a centrar en los
Proxies, que son un patrón que nos permite actualizar completamente la lógica de nuestros contratos usando solamente dos contratos diferentes, cabe destacar que estos dos contratos son inmutables, pero combinándolos podremos actualizar su funcionamiento. La idea básica de su funcionamiento es la siguiente:
Figura 3: Despliegue de SmartContract Proxy
Tenemos dos contratos diferentes, el primero el
Proxy o
Storage que se encargará de guardar en la
Blockchain todos los datos del
SmartContract. Este contrato siempre será el mismo y nunca cambiará, esto es así dado que mover los datos de un contrato a otro es muy costoso. El segundo el contrato de la lógica, como su nombre nos indica este se encargará de manejar la lógica por la que se regirá el
SmartContract, este irá cambiando con el tiempo oséa que crearemos nuevos contratos y cambiaremos éste por los nuevos.
Figura 4: Actualizando contrato Proxy
Ahora, en cada interacción que el usuario haga la hará con el contrato
Storage y esté a su vez le pasará la llamada al contrato que guarda la lógica y éste modificará la memoria del contrato
Storage y no la suya. Ahora imaginemos que queremos actualizar el contrato que utiliza el usuario, pues simplemente subimos un nuevo contrato a la
Blockchain que tenga esta lógica nueva y cambiamos en el contrato storage el puntero que redirigía al contrato anterior por el nuevo.
Funcionamiento del Patrón Proxy
Acabamos de ver la idea general y básica sobre cómo funcionan los sistemas
proxies en los contratos basados en la
EVM, veamos ahora cómo convertir esta idea en algo real. En la
EVM (Ethereum Virtual Machine) existe una instrucción llamada
delegatecall que funciona de la siguiente manera.
“””
delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
“””
Cabe notar que en
Ethereum las instrucciones de código ensamblador se usan como si fueran funciones y no como “
add bx, 5; “ usado por la mayoría de lenguajes de ensamblador. Esta instrucción nos permite llamar a un contrato externo, igual que la instrucción “
call “. Bien pero, ¿qué diferencia hay entre estas instrucciones? Pues que
delegatecall no solamente llama a la función de un contrato externo sino que además pasa todo el contenido guardado en
msg.data (parámetros de llamada) y
msg.value (
ETH enviado).
Además teniendo la peculiaridad de que todas las modificaciones que haga el contrato al que se le delega la llamada no se reflejará en el propio contrato sino que lo hará en aquel que haya ejecutado
delegatecall. La instrucción
delegatecall recibe cinco parámetros:
- gas: El gas del que va a disponer el contrato al que se le delega la llamada. No siempre este es el máximo posible porque a veces antes de delegar la llamada primero evaluamos unas ciertas condiciones que gastan el gas inicial, por ello es importante calcularlo adecuadamente.
- _impl: El address del contrato hacia el cual delegamos la llamada. En el patrón proxy se le suele llamar implementación.
- prt: Los datos del msg.data que se pasan al nuevo contrato, puede bastar con pasar msg.data en vez de ptr, pero puede que primero necesitemos procesar los datos de entrada. El msg.data acabará convirtiendose en los parámetros que le lleguen al contrato final.
- calldatasize: El valor en bytes de la longitud de memoria que ocupa el msg.data.
- últimos 2 parámetros: Estos dos últimos parámetros no son importantes, solo sirven para que el valor devuelto por el contrato al que se delegue sea acotado entre el rango provisto en esos dos valores.
Cabe destacar que si usamos la función
addr.delegatecall(...) en
Solidity, ésta sólo nos devolverá un
booleano que nos dirá si la función ha fallado o no. Sin embargo si usamos la instrucción de ensamblador podremos acceder a los datos que devuelva el contrato al que se delegue usando un pequeño truco que veremos más adelante.
Implementación en Solidity
Toda esta teoría está muy bien pero veamos cómo podemos implementar todo esto en
Solidity. Para ello vamos ha hacer uso de él “
inline-assembly” que nos ofrece
Solidity para así poder escribir ensamblador dentro del propio código de
Solidity.
Figura 5: Función fallback en un Proxy
Antes de explicar que ocurre dentro de la función veamos el contexto de porque esa función. Si nos fijamos bien veremos que no estamos definiendo una función normal, sino una “
fallback” esta función tiene un rol muy importante dentro de los
SmartContracts basados en
EVM, ésta se ejecutará siempre que se llame a un contrato y la función a la que se está llamando no exista en el propio contrato.
Si queréis entender en profundidad cómo funciona este sistema de llamadas os recomiendo leer
este artículo sobre los selectores de las funciones en la EVM.
Explicacion del codigo:
Lo primero que hacemos es reservar en memoria un espacio en el que poder guardar los datos que se envían junto con la función como parámetros, etcétera. Y ahora os preguntaréis ¿Y cómo accedemos a ellos si no están declarados en la función? Pues en la
EVM todos los datos que se envían a una función están guardados dentro de una variable global llamada
calldata. Después copiamos al espacio reservado en memoria los datos guardados en
calldata. Esto es necesario ya que
calldata es un tipo de memoria que no se puede ni modificar ni pasar a otras funciones como parámetros.
Luego delegamos la llamada a un
address llamado
_impl y le pasamos el
calldata que escribimos en memoria,
delegatecall nos devolverá un valor
boolean que debemos guardar. Y también guardamos en memoria la longitud de la respuesta del
delegatecall. Una vez hecho eso, copiamos el espacio reservado en
0 los valores devueltos por
2 y
evaluamos si la llamada delegada fue exitosa o no. En caso de que no devolvemos el error que nos tiró el contrato de lógica, sino devolvemos los valores.
Problemas que pueden surgir
Una cosa muy importante a tener en cuenta es cómo se guarda el nuevo almacenamiento que creamos al añadir lógica a un contrato. Veamos primero cómo transforma
Solidity su código a instrucciones.
“””
contract storage{
uint256 age = 12;// transforms to sstore(0x00,0xC)
uint256 other = 1; // transforms to sstore (0x40, 0x1)
function modify() external{
age = 13; // transforms to sstore(0x00,0xD)
other = 2; // transforms to sstore(0x40, 0x2)
}
}
“””
A la hora de actualizar este contrato tenemos que tener mucho cuidado de no hacer override de estas variables o podríamos tener un problema bastante serio, miremos el siguiente ejemplo para ver a lo que me refiero.
“””
contract storageV2{
uint256 score = 100; // transforms to sstore(0x00,0x64)
uint256 age; // setted in V1 as 0x00 but not is 0x40
uint256 other; // we setted in V1 0x40 but now is in position 0x60
function modify() external{
age = 13; // now it stores age in other slot
other = 2; // now it stores other in a new slot
score = 75; // stores it in the age slot
}
}
“””
Este contrato que trata de actualizar la lógica del contrato anterior está mal, porque lleva a una colisión de almacenamiento. Esto es que cuando tratemos de leer lo que había en
age v1 estaremos leyendo lo que había en otro
v1 y si leemos
other obtendremos
0 dado que no hay datos guardados en el
slot nuevo. De manera visual la colisión sería la siguiente:
Figura 6: Storage Collision
Y los datos los podemos seguir leyendo porque aunque estén cambiados a fin de cuentas un
uint256 es
32bytes con lo que se lee de la misma manera, pero si estuviésemos leyendo un
mapping o un
string los datos que leeremos no tendrían siquiera sentido alguno. También existen otros tipos de problemas como el de la transparencia para el usuario, control de acceso y actualización. Estos se resuelven de manera un poco más compleja.
¿Descentralización y transparencia?
Es verdad que es muy útil el poder actualizar la lógica de tus contratos por si acaso algún día se encuentra una vulnerabilidad en tu código o simplemente necesitas extender su funcionalidad, pero esto rompe un poco con los principios de inmutabilidad y transparencia. ¿Pero es esto una carencia? ¿De verdad rompe con la descentralización? Os voy adelantando que no, existen mecanismos para mantener descentralizada la propiedad del contrato y sus actualizaciones, pero esto no lo veremos hoy.