
ERC-1967: Proxy Storage Slots
Tener la capacidad de actualizar smart contracts es una necesidad común en el desarrollo. Sin embargo, el código de los contratos desplegados es inmutable por diseño. Para evitar esta limitación y permitir mejoras o correcciones de bugs sin perder el estado del contrato, se utilizan proxies.
En resumen un contrato proxy actúa como un intermediario. Los usuarios interactúan con el proxy, que a su vez delega la ejecución de la lógica
a otro contrato (el contrato de implementación) utilizando la operación delegatecall
.
La clave de delegatecall
es que el código de la implementación se ejecuta en el contexto del proxy,
lo que significa que el estado (variables de almacenamiento) utilizado es el del proxy, no el de la implementación.
Este patrón nos permite actualizar la lógica cambiando la dirección de la implementación al que apunta el proxy.
Sin embargo, surge una pregunta: ¿dónde almacena el proxy la dirección de la implementación evitando que haya colisiones con
las variables definidas por la implementación? Aquí es donde ERC-1967 entra en juego.
Colisiones de almacenamiento
El estado de un smart contract se almacena en “slots” de 256 bits. Las variables de estado se asignan a estos slots de forma secuencial, siguiendo las reglas definidas por el compilador de Solidity.
Cuando un proxy utiliza delegatecall
para ejecutar código de una implementación, accede a los slots de almacenamiento del proxy.
Si el proxy almacena su información (la dirección de la implementación o la dirección del administrador) en los mismos slots que el contrato
de implementación utiliza para sus propias variables de estado, habría una colisión.
Una operación de escritura en el contrato de implementación podría sobrescribir accidentalmente los datos del proxy, o viceversa, llevando a resultados inesperados, pérdida de estado, o incluso impidiendo futuras actualizaciones.
Ejemplo:
Si el proxy almacena la address implementationContract
en el slot 0 y la implementación define un uint256 value
que también se asigna al slot 0, cualquier operación que escriba en value
en la implementación sobrescribiría implementationContract
en el proxy.
Solución: ERC-1967
El ERC-1967 (“proxy Storage Slots”) propone definir ubicaciones de almacenamiento (slots) específicas donde los contratos proxy deben almacenar sus datos de configuración.
Los slots definidos por ERC-1967 son:
- Slot Logic address:
- Propósito: Almacena la address del contrato de implementación actual
- Dirección del slot:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
- Derivado de:
keccak256("eip1967.proxy.implementation") - 1
- Slot de Admin address:
- Propósito: Almacenar la address que tiene permiso para gestionar el proxy.
- Dirección del slot:
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
- Derivado de:
keccak256("eip1967.proxy.admin") - 1
- Slot de Beacon address:
- Propósito: Almacenar la address de un contrato Beacon. En el patrón Beacon, múltiples proxies apuntan a un único Beacon, y el Beacon a su vez almacena la dirección de implementación. Esto permite actualizar muchos proxies simultáneamente actualizando solo el Beacon.
- Dirección del slot:
0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
- Derivado de:
keccak256("eip1967.proxy.beacon") - 1
Se realiza el -1
al realizar los keccak256
para que el slot resultante no sea la salida directa de ningúna cadena conocida,
evitando posibles colisiones en el almacenamiento y añadiendo una capa extra de seguridad.
¿Por qué estos slots específicos?
La elección de derivar las direcciones de los slots de hashes de strings significativos (“eip1967.proxy.implementation”, etc.) asegura que las ubicaciones resultantes sean direcciones de slot muy grandes y pseudo-aleatorias. Volviendo el riesgo de una colisión accidental muy pequeño.
Accediendo a los Slots
Para leer o escribir en estos slots de almacenamiento específicos, los contratos proxy deben utilizar código ensamblador (assembly) en línea, ya que el acceso directo a un slot por su dirección hexadecimal no es una operación nativa en Solidity a nivel de lenguaje de alto nivel.
Aquí tienes un ejemplo de cómo un contrato podría leer la dirección de implementación:
contract proxyWithERC1967 {
// Constante para la dirección del slot de implementación según ERC-1967
bytes32 private constant _IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
// Función para obtener la dirección de implementación
function getImplementationAddress() public view returns (address impl) {
assembly {
impl := sload(_IMPLEMENTATION_SLOT)
}
}
// Función para actualizar la dirección de implementación
function setImplementationAddress(address newImplementation) public {
assembly {
sstore(_IMPLEMENTATION_SLOT, newImplementation)
}
}
...
}
Este código ilustra lo fundamental. Sin embargo, la implementación completa de un proxy actualizable implica más complejidad.