Transient storage: Las nuevas variables fantasmas


La última actualización de Solidity (0.8.28) trae una mejora notable con el soporte completo para las variables de estado de tipo transient storage para uint y bool. Esta funcionalidad fue parcialmente introducida en la versión anterior, pero ahora está completamente operativa, lo que ofrece nuevas posibilidades para nuestros smart contracts.

¿Qué es el transient storage?

El transient storage es una nueva ubicación de datos en la Ethereum Virtual Machine (EVM), introducida por el EIP-1153.

Es similar al almacenamiento permanente, pero con una gran diferencia: los datos en el transient storage solo persisten durante la transacción actual. Es decir al finalizar la transacción, los datos son restablecidos a su valor predeterminado.

A diferencia del almacenamiento permanente, que es costoso en términos de gas y se almacena indefinidamente en la blockchain, el transient storage es mucho más barato y está diseñado para usos temporales, como por ejemplo, implementaciones para evitar los reentrancy attacks.

¿Cuándo utilizar transient storage?

El caso más fácil de identificar es para evitar los reentrancy attacks donde el transient storage permite establecer una flag para bloquear durante la ejecución de una función crítica y luego restablecerla al final, garantizando que no se pueda llamar la función varias veces en la misma transacción, todo esto sin incurrir en costos elevados de almacenamiento permanente.

Veamos un ejemplo:

pragma solidity ^0.8.28;

contract Generosity {
 mapping(address => bool) sentGifts;
 bool transient locked;

 modifier nonReentrant {
   require(!locked, "Reentrancy attempt");
   locked = true;
   _;
   // Unlocks the guard, making the pattern composable.
   // After the function exits, it can be called again,
   // even in the same transaction.
   locked = false;
 }

 function claimGift() nonReentrant public {
   require(address(this).balance >= 1 ether);
   require(!sentGifts[msg.sender]);
   (bool success, ) = msg.sender.call{value: 1 ether}("");
   require(success);

   // In a reentrant function, 
   //doing this last would open up the vulnerability
   sentGifts[msg.sender] = true;
 }
}

En este contrato, utilizamos una variable transient locked para implementar un patrón nonReentrant, evitando así el reentrancy attack. Este enfoque reduce significativamente el costo de gas comparado con el uso de almacenamiento permanente para la variable de bloqueo.

Limitaciones y Consideraciones

Aunque el transient storage tiene beneficios, también presenta ciertas limitaciones y posibles trampas.

1. Visibilidad limitada y tipos soportados

Por el momento, el transient storage solo soporta uint y boolean. Tampoco es posible declarar variables locales o parámetros como transient.

Además, las variables transient no pueden ser inicializadas en su declaración, ya que su valor se reinicia al finalizar la transacción. Se inicializan automáticamente a su valor por defecto según su tipo (por ejemplo, 0 para uint o false para bool).

2. Composabilidad

Una de las advertencias más importantes es que el transient storage puede romper la composabilidad de los contratos inteligentes. Imaginemos un caso donde se almacena un multiplicador en transient storage y se realizan varias llamadas para multiplicar diferentes valores:

contract MulService {
    uint transient multiplier;
    
    function setMultiplier(uint mul) external {
        multiplier = mul;
    }

    function multiply(uint value) external view returns (uint) {
        return value * multiplier;
    }
}

y seguimos la siguiente secuencia de llamadas desde fuera de la DLT:

setMultiplier(42);
multiply(1);
multiply(2);

Si estas funciones se llaman en transacciones separadas, el valor de multiplier se restablecería a su valor por defecto (0) al final de la transacción, lo que llevaría a resultados inesperados. Esto rompe la capacidad de componer funciones en diferentes contratos y transacciones, un principio fundamental en el diseño de contratos inteligentes.

3. Reentrancia

El uso de transient storage para prevenir ataques de reentrancia es seguro, pero solo si se restablecen los valores correctamente. Si se omite esta etapa, el contrato quedaría bloqueado para su uso en transacciones más complejas que involucren múltiples llamadas. Es recomendable siempre limpiar el transient storage al finalizar una llamada.

Conclusiones

La versión 0.8.28 de Solidity marca un avance importante con la introducción del transient storage. Esta funcionalidad ofrece beneficios claros, como una reducción en los costos de gas para ciertos patrones de diseño, pero también requiere una atención especial a las limitaciones que impone, como la composabilidad y la necesidad de limpiar correctamente los datos al finalizar una transacción.

Es fundamental que nos familiaricemos con estos conceptos y comprendamos cuándo y cómo usar el transient storage para evitar errores difíciles de detectar.