El EIP 1967 es un estándar sobre dónde almacenar la información que los contratos proxy necesitan para ejecutarse. Tanto el UUPS (Universal Upgradeable Proxy Standard) como el Transparent Upgradeable Proxy Pattern lo utilizan.
Recuerda: El EIP 1967 solo establece dónde van ciertas variables de almacenamiento (storage variables) y qué registros (logs) se emiten cuando cambian, nada más. No establece cómo se actualizan esas variables o quién puede administrarlas. No define ninguna función pública a implementar. La especificación para actualizar estas variables se proporciona en el Transparent Upgradeable Proxy Pattern o en la especificación del UUPS.
Hay dos variables críticas que un proxy necesita para operar: la implementation address y el admin. La implementation address es hacia dónde el proxy está delegando las llamadas. Durante una actualización, la implementation address cambia al contrato actualizado; solo se aceptarán las llamadas del admin para realizar cambios.
Requisitos previos
Este artículo asume que el lector tiene una idea básica de cómo funcionan los proxies y delegatecall, qué son los storage slots, qué son los function selectors y qué es la colisión de selectores de función (function selector clashing) en el contexto de un proxy.
La forma incorrecta de diseñar los slots del proxy
El siguiente es un mal diseño de proxy:

En primer lugar, hay una probabilidad no despreciable de que el function selector para changeAdmin() choque con una función en la implementación. La especificación del EIP 1967 no dice cómo manejar esto: la forma correcta de prevenir este problema se maneja en la especificación del Transparent Upgradeable Proxy o del UUPS. El EIP 1967 no tiene nada que ver con la colisión de function selectors.
El problema que resuelve el ERC 1967 es que las variables implementation y admin muy probablemente chocarán con una variable de almacenamiento definida en el contrato de implementación. Específicamente, utilizan los storage slots 0 y 1, que los contratos de implementación probablemente utilizarán.
Previniendo colisiones
Debido a que las direcciones de admin e implementation pueden cambiar, estas deben estar en variables de almacenamiento; no pueden ser inmutables. Pero deben estar en storage slots que no colisionen con las variables de almacenamiento en el contrato de implementación.
Aquí está la idea clave: el espacio de posibles storage slots es extremadamente grande: 2**256 - 1.
Si eligiéramos un storage slot al azar, es esencialmente imposible que el contrato de implementación elija el mismo slot. Las posibilidades de que un contrato de implementación elija el mismo slot son casi las mismas que las de una colisión de función hash, por lo que el riesgo es esencialmente inexistente.
Los storage slots para implementation y address
La implementation address se almacena en el slot
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
La dirección del admin se almacena en el slot
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
Estos slots se derivaron de forma pseudoaleatoria a partir de
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) y
bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) respectivamente.
En formato decimal, el storage slot para la implementation y el admin son
24440054405305269366569402256811496959409073762505157381672968839269610695612
y
81955473079516046949633743016697847541294818689821282749996681496272635257091
respectivamente.
Ningún contrato podría tener tantas variables, por lo que la colisión de storage variables es insignificante. Los mapeos (mappings) dinámicos y los arrays toman el hash del número de slot y el valor de la clave, y por lo tanto utilizan un storage slot pseudoaleatorio. Nuevamente, la colisión por números pseudoaleatorios es insignificante.
Derivación de los storage slots
Si tomamos el hash keccak256 de un string, el resultado es esencialmente un número pseudoaleatorio. Al restar 1 del resultado, producimos un número aleatorio que no tiene una preimagen de hash conocida, por lo que no hay forma de que un contrato pueda introducir algo en keccak256 para derivar un storage slot que choque con ellos.
Suposiciones sobre el uso de los storage slots
Por supuesto, los desarrolladores que escriben contratos de implementación podrían escribir deliberadamente en esos storage slots con el siguiente código
assembly {
// implementation slot
sstore(
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, 0x00
)
}
¡Esto rompería el proxy apuntándolo a la zero address! La suposición es que los desarrolladores no harán esto.
El EIP 1967 facilita que Etherscan determine si está viendo un contrato proxy
Aquí hay un ejemplo del contrato proxy de Compound Finance.

Al verificar si un contrato tiene un valor distinto de cero en los slots mencionados anteriormente, los exploradores de bloques pueden saber si un contrato es un proxy o no. Un par de observaciones sobre la captura de pantalla anterior:
- En el círculo morado, vemos que Etherscan ha identificado que el contrato sigue el patrón EIP-1967.
- En el círculo naranja, vemos una nota sobre dónde está el contrato de implementación y dónde estaba el anterior. El explorador de bloques simplemente mira el slot de implementation actual y también recuerda los valores pasados para este.
- En el círculo rojo, vemos que tenemos las opciones de escribir y leer desde el proxy o desde la implementación. Generalmente, queremos leer o escribir en el proxy, ya que este mantiene el estado del contrato.
¿Qué es un beacon slot?
Si lees el EIP 1967 original, verás una referencia a un beacon slot. Los beacons se utilizan muy raramente en la práctica, por lo que aplazamos su discusión para el final del artículo.
Los beacons son un tema para otro artículo, pero esencialmente, son un mecanismo para actualizar múltiples proxies al mismo tiempo. Por ejemplo, podemos apuntar varios proxies al mismo contrato de implementación. Dado que el almacenamiento se mantiene en cada proxy de forma individual, los proxies no interferirán entre sí.
El contrato beacon es muy simple: solo devuelve la dirección del contrato de implementación:
interface IBeacon {
function implementation() external view returns (address);
}
Cada uno de los proxies le pregunta al beacon cuál es la dirección actual del contrato de implementación antes de hacer un delegatecall. Al cambiar el valor de retorno de la función implementation() en el beacon, todos los proxies pueden actualizarse a la vez.
El storage slot del beacon es 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
y se deriva de bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1).
El valor address(0) se puede almacenar aquí para los proxies que no utilizan beacons (o el slot se puede dejar vacío).
Implementación de OpenZeppelin y Solady
Los contratos Transparent Upgradeable Proxy de OpenZeppelin y UUPS utilizan el ERC 1967 para definir dónde almacenar las variables discutidas en este artículo.
La librería eficiente en gas Solady también proporciona una implementación de proxy UUPS que utiliza el ERC 1967.
Conclusión
El ERC 1967 es un estándar sobre dónde colocar las storage variables para el contrato de implementación, el admin y el beacon. Permite a los exploradores de bloques identificar fácilmente si un contrato es un proxy y elimina la posibilidad de colisiones de almacenamiento entre el proxy y la implementación.
Aprende más con RareSkills
Este artículo es parte de nuestro Solidity Bootcamp avanzado. Por favor, consulta el programa para obtener más información.
Publicado originalmente el 20 de diciembre de 2023