Los inicializadores son la forma en que los contratos actualizables logran el comportamiento de un constructor.
Al desplegar contratos, es común llamar a un constructor para inicializar las variables de almacenamiento. Por ejemplo, un constructor podría establecer el nombre de un token o el suministro máximo. En un contrato actualizable, esta información necesita almacenarse en las variables de almacenamiento del proxy, no en las variables de almacenamiento de la implementación. Añadir un constructor en el proxy como el siguiente:
constructor(
string memory name_,
string memory symbol_,
uint256 maxSupply_
) {
name = name_;
symbol = symbol_;
maxSupply = maxSupply_;
}
no es una buena solución porque es propenso a errores alinear las ubicaciones de las variables de almacenamiento entre el proxy y la implementación. Crear un constructor en la implementación no funcionará porque establecerá las variables de almacenamiento en la implementación.
Cómo funcionan los inicializadores
La solución a todos los problemas anteriores es crear una función initializer() en la implementación que establezca las variables de almacenamiento de la misma manera que lo haría un constructor y hacer que el proxy haga un delegatecall a la función initialize() de la implementación. Colocar el initializer() en la implementación garantiza que la alineación de las variables de almacenamiento sea automáticamente correcta. Para imitar a un constructor, es crucial que el proxy solo pueda hacer delegatecall a esta función una vez.
El propósito del contrato Initializable.sol de OpenZeppelin es proporcionar una implementación robusta de este patrón de inicialización. Actualmente, Initializable.sol se utiliza en los contratos actualizables de OpenZeppelin, como ERC20Upgradeable.sol.
El propósito de este artículo es proporcionar una explicación detallada de cómo funciona Initializable.sol. Pero antes de eso, mostraremos cómo se podría implementar este patrón de forma ingenua y por qué la implementación ingenua solo funciona en los escenarios más simples.
A continuación se muestra una ilustración de alto nivel del proceso del inicializador en la siguiente animación:
Una implementación ingenua
Puede ser tentador escribir un contrato como el siguiente, donde se diseña un modificador para restringir la ejecución de una función a una sola vez y nunca más.
contract NaiveInitialization {
// initialized indicates if the contract has been initialized
bool initialized = false;
// restricts the function to be executed only once
modifier initializer() {
require(initialized == false, "Already initialized");
initialized = true;
_;
}
// can be executed only once
function initialize() public initializer {
// Initialize necessary storage variables
}
}
El código anterior funciona para este contrato en específico, asegurando que la función initialize() solo se pueda ejecutar una vez. Sin embargo, el mismo patrón falla cuando se utiliza con herencia.
Demostración de implementación fallida de Initialize
El problema con el patrón anterior es que no tiene soporte para cuando los contratos utilizan herencia y los contratos padre también tienen que ser inicializados. Examinemos un ejemplo de este problema en el siguiente código.
contract Initializable {
// initialized indicates if the contract has been initialized
bool initialized = false;
// restricts the function to be executed only once
modifier initializer() {
require(initialized == false, "Already initialized");
initialized = true;
_;
}
}
contract ParentNaive is Initializable {
// initialize the Parent contract
function initializeParent() internal initializer {
// Initialize some state variables
}
}
contract ChildNaive is ParentNaive {
// initialize the Child contract
function initializeChild() public initializer {
super.initializeParent();
// Initialize other state variables
}
}
El orden de ejecución esperado del contrato anterior es el siguiente:
- Se llama a la función
initializeChild(). Con el modificadorinitializer, esta actualiza la variableinitializedatrue. Esta variable es utilizada por todos los contratos en la cadena de herencia. - A continuación, se llama a la función
initializeParent()dentro deinitializeChild().initializeParent()también tiene el modificadorinitializer, por lo que requiere que la variableinitializedseafalse. - Pero la variable
initializedya se ha establecido entruecuando se ejecutóinitializeChild, por lo que la transacción se revertirá cuando se llame ainitializeParent().
El contrato Initializable.sol de OpenZeppelin aborda este problema permitiendo la inicialización de todos los contratos dentro de una cadena de herencia, al mismo tiempo que evita que los inicializadores sean llamados después de la transacción de inicialización.
Entendiendo Initializable.sol
El núcleo de Initializable.sol consta de tres modificadores: initializer, reinitializer y onlyInitializing, y dos variables de estado: _initializing y _initialized.
Cada modificador se utiliza solo en un escenario específico y tiene un propósito distinto. Una visión general de su uso es la siguiente:
- El modificador
initializerdebe utilizarse durante el despliegue inicial del contrato actualizable y exclusivamente en el contrato más derivado. - El modificador
reinitializerdebe utilizarse para inicializar nuevas versiones del contrato de implementación, de nuevo solo dentro de los contratos más derivados. - El modificador
onlyInitializingse utiliza con los inicializadores padre para que se ejecuten durante la inicialización y evita que esos inicializadores sean llamados en una transacción posterior. Esto resuelve el problema mencionado en la sección anterior, donde los inicializadores padre no podían ejecutarse debido a que el inicializador más derivado los deshabilitaba. Con este esquema, es posible inicializar todos los contratos padre, así como el contrato más derivado.
A continuación se muestra un diagrama visual que ilustra estos escenarios. Se proporcionará una explicación más detallada sobre el uso de estos modificadores en las siguientes secciones.

Initializable.sol implementa el patrón ERC-7201, donde las variables de estado se declaran dentro de un struct. Si no estás al tanto de este patrón, simplemente considera _initializing y _initialized como variables de estado.
struct InitializableStorage {
/**
* @dev Indicates that the contract has been initialized.
*/
uint64 _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool _initializing;
}
La variable _initializing es un booleano que indica si el contrato está en proceso de inicialización, mientras que la variable _initialized almacena la versión actual del contrato. Comienza en el valor 0 y será 1 después de la primera inicialización. Podría ser mayor si los desarrolladores decidieran desplegar una nueva implementación y desearan “reinicializar” las variables de almacenamiento a nuevos valores.
El siguiente video ilustra cómo estas partes funcionan juntas:
El modificador Initializer
El modificador initializer es el siguiente. Algunas partes del código se explicarán con más detalle más adelante.

El código anterior no es del todo simple debido a la necesidad de abordar problemas de compatibilidad hacia atrás con versiones previas. Sin embargo, la idea principal tiene dos propósitos:
- Establecer la variable
_initializeda1para evitar que la función se ejecute nuevamente (cuadro verde). - Permitir temporalmente que los inicializadores padre, modificados con
onlyInitializing, se ejecuten mientras_initializingsea verdadero. Como se puede observar en el código anterior,_initializinges falso cuando el contrato no ha sido inicializado, verdadero mientras la transacción de inicialización está en ejecución, y falso cuando la transacción de inicialización termina.
Debido a que initializer requiere que _initializing sea falso, no puede utilizarse en contratos padre dentro de la cadena de herencia, ya que _initializing es verdadero mientras estos se están ejecutando. En su lugar, las funciones de inicialización de los contratos padre deben utilizar un modificador diferente, específicamente onlyInitializing, el cual permite que la función se ejecute solo cuando _initializing es verdadero.
El modificador onlyInitializing
El modificador onlyInitializing está diseñado para su uso en contratos padre, ya que solo se ejecuta mientras _initializing es verdadero, como se muestra a continuación.
modifier onlyInitializing() {
_checkInitializing();
_;
}
function _checkInitializing() internal view virtual {
if (!_isInitializing()) {
revert NotInitializing(); // reverts if _initializing is false
}
}
A continuación se muestra una representación visual de este flujo.

En resumen, los inicializadores padre están protegidos por el modificador onlyInitializing, el cual evita que sean llamados a menos que el initializer del contrato más derivado se esté ejecutando en ese momento.
El modificador reinitializer
El modificador reinitializer cumple un rol similar a initializer, pero debe utilizarse para inicializar nuevas versiones del contrato de implementación si una nueva versión necesita actualizar las variables de almacenamiento durante la inicialización.
Este modificador tiene un argumento uint64 que indica una versión del contrato, la cual debe ser mayor que la versión actual. Si se desea prevenir futuras reinicializaciones, la versión puede establecerse en o type(uint64).max. Hacer que la variable sea uint64 permite que la variable booleana _initializing sea empaquetada en el mismo slot, y versiones dejan mucho espacio para futuras actualizaciones. Aquí está el código del modificador reinitializer.
modifier reinitializer(uint64 version) {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
if ($._initializing || $._initialized >= version) {
revert InvalidInitialization();
}
$._initialized = version;
$._initializing = true;
_;
$._initializing = false;
emit Initialized(version);
}
Ilustremos su uso con un ejemplo. Supongamos que en la primera actualización de un contrato ERC20Upgradeable, queremos cambiar el nombre y el símbolo del token. Esta función debe escribirse de la siguiente manera, donde el valor 2 indica que esta es la segunda versión del contrato:
function initialize() reinitializer(2) public {
__ERC20_init("MyToken2", "MTK2");
}
Para resumir cómo proceder con las actualizaciones:
- No se puede utilizar
initializeren una nueva versión. - Se debe utilizar
reinitializersi se desea cambiar alguna variable de estado en una función de inicialización. - Alternativamente, podría no tenerse ningún inicializador si no es necesario realizar cambios de estado durante la actualización.
Vulnerabilidad de contratos no inicializados
En el modificador initializer, hay una línea de código que tal vez llamó tu atención, pero no la mencioné en mi explicación inicial. Es la siguiente:
bool construction = initialized == 1 && address(this).code.length == 0;
La expresión address(this).code.length == 0 es verdadera solo durante el despliegue del contrato. Por lo tanto, la variable construction solo puede ser verdadera si el modificador initializer se está utilizando en un constructor.
De hecho, el modificador initializer puede utilizarse en el constructor del contrato de implementación para “inicializarlo”. Esto podría parecer contraintuitivo, ya que el almacenamiento del contrato de implementación no debería importar. Sin embargo, como veremos, esta inicialización sirve como medida de seguridad.
La vulnerabilidad UUPS que involucra contratos no inicializados.
El punto crucial es que la función de inicialización de un contrato es pública y puede ser llamada ya sea a través del proxy o directamente desde una EOA o por otro contrato, como se ilustra en la figura a continuación. En los contratos Ownable actualizables, el propietario se establece típicamente en la función de inicialización.
- Si se llama a
initializevía delegatecall desde el proxy, el propietario se almacena en el almacenamiento del proxy. - Si se llama a
initializedirectamente en la implementación, el propietario se almacena en el almacenamiento de la implementación.

Ser el propietario del contrato de implementación no debería importar, ya que ejecutar funciones directamente en el contrato de implementación modifica su propio almacenamiento, el cual no es el almacenamiento “real”. Por esta razón, muchos equipos no consideraron ejecutar la función de inicialización directamente sobre el contrato de implementación.
El problema crítico es que cualquier función definida como onlyOwner puede entonces ser ejecutada por este “otro” propietario en el contrato de implementación.
Y fue precisamente este escenario el que expuso una vulnerabilidad en los contratos UUPS de OpenZeppelin desde la versión v4.1.0 hasta la v4.3.1. La función responsable de migrar de un contrato de implementación al siguiente también ejecutaba un delegatecall a esa nueva dirección.

Esta función estaba protegida por un modificador onlyOwner y su intención era ser ejecutada únicamente por el “propietario legítimo”. Sin embargo, también podía ser ejecutada por el propietario del contrato de implementación.
Modificar el almacenamiento de la implementación no era el problema. Sin embargo, el propietario ahora podía hacer un delegatecall hacia un contrato que contuviera un opcode selfdestruct. Esta acción borraría el código del contrato de implementación, impidiendo que el proxy migrara hacia una nueva implementación. Esencialmente, esta vulnerabilidad podría potencialmente bloquear millones de dólares en activos dentro del proxy de forma indefinida.
Cualquier proxy que utilizara estas versiones de la biblioteca de UUPS, cuyos contratos de implementación no hubieran sido “inicializados”, estaban en riesgo. Cualquiera podía ejecutar la función de inicialización, convertirse en el propietario y ejecutar un delegatecall a un contrato con un opcode selfdestruct.
Mitigación original ante atacantes que tomaban la propiedad de un contrato de implementación no inicializado
Para mitigar este problema, la primera recomendación del equipo de ingeniería de OpenZeppelin fue “inicializar” siempre los contratos de implementación utilizando un constructor junto con el modificador initializer, de la siguiente manera.
constructor() initializer {}
Esta era solo una medida de seguridad para evitar que un atacante inicializara variables de almacenamiento en la implementación para convertirse en el propietario. Este modificador estaba destinado a colocarse en los constructores de todos los contratos de implementación en una cadena de herencia.
Para los contratos más derivados, la variable initialSetup siempre será verdadera. Sin embargo, en los contratos padre de los contratos de implementación, durante el despliegue, initialized será 1 y address(this).code.length == 0. Es para este escenario que existe la variable construction—para permitir la inicialización de los contratos padre del contrato de implementación.
En otras palabras, la línea en cuestión está diseñada para tener en cuenta casos como el del esquema de contrato a continuación, donde los contratos padre también necesitan ser inicializados. Se debe utilizar el modificador initializer; el modificador onlyInitializing no está pensado para inicializar constructores.
contract ImplementationParent is Initializable {
// here initialSetup will be false
// but construction will be true
constructor() initializer {}
}
contract ImplementationChild is Initializable, ImplementationParent {
// here initialSetup will be true
constructor() ImplementationParent() initializer {}
}
Una vez que el contrato de implementación está “inicializado”, ya no es posible que nadie ejecute la función de inicialización y se convierta en el propietario del contrato.
Esta ya no es la mitigación recomendada, la solución recomendada para evitar que un atacante se convierta en el propietario de un contrato de implementación se muestra en la siguiente sección, pero el modificador initializer conserva la variable construction por razones de compatibilidad hacia atrás. Es posible que sea eliminada en una versión futura de este contrato.
La función _disableInitializers()
El enfoque más reciente y recomendado propuesto por OpenZeppelin para “inicializar” los contratos de implementación es usar la función __disableInitializers().
El código es el siguiente:
function _disableInitializers() internal virtual {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
if ($._initializing) {
revert InvalidInitialization();
}
if ($._initialized != type(uint64).max) {
$._initialized = type(uint64).max; // set initialized to its max value, preventing reinitializations
emit Initialized(type(uint64).max);
}
}
Por lo tanto, actualmente, la forma recomendada para prevenir vulnerabilidades causadas por contratos “no inicializados” es incluir el siguiente constructor en todos los contratos de implementación:
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
Los contratos de implementación en sí mismos nunca son actualizados, solo el proxy. Así que establecer la versión en type(uint64).max (_initialized en la implementación) asegura que el contrato de implementación nunca será inicializado. Bloquear los inicializadores en la implementación no impide que el proxy haga delegatecall a ellos, porque el almacenamiento _initialized que prevendría el delegatecall se encuentra en el proxy.
Las funciones _init y _init_unchained
La función que inicializa los contratos actualizables puede tener cualquier nombre, pero los contratos de OpenZeppelin siguen un estándar. De hecho, todos tienen dos funciones de inicialización, que utilizan dos nombres: <Contract Name>_init y <Contract Name>_init_unchained.
La función <Contract Name>_init_unchained contiene todo el código que debe ejecutarse cuando el contrato de implementación se inicializa. Por ejemplo, en el caso de un token ERC20, esta función establece el nombre y el símbolo del token.
function __ERC20_init_unchained(string memory name_, string memory symbol_)
internal
onlyInitializing
{
ERC20Storage storage $ = _getERC20Storage();
$._name = name_;
$._symbol = symbol_;
}
La función <Contract Name>_init ejecuta <Contract Name>_init_unchained así como <Parent Contract>_init_unchained para todos sus contratos padre que deban ser inicializados.
Consideremos el caso del contrato GovernorUpgradeable.sol v5, el cual necesita inicializarse a sí mismo y a uno de sus contratos padre, el contrato EIP712Upgradeable.

En general, es suficiente ejecutar la función _init de un contrato, la cual también inicializa a los contratos padre. Se debe tener cuidado de no inicializar el mismo contrato dos veces, lo que puede ocurrir en una cadena de herencia donde dos contratos comparten el mismo padre.
Esa es la razón por la que existen dos funciones, _init y _init_unchained. Si uno necesita inicializar un contrato sin inicializar a sus padres, se debe utilizar la función _init_unchained.
Inicializando un contrato ERC20 Upgradeable
Un ejemplo de cómo inicializar contratos actualizables puede verse en la imagen a continuación, que muestra un contrato de token ERC20 actualizable generado por el OpenZeppelin Wizard.

Nota que llama a la función __<Contract Name>_init en sus padres. Independientemente del esquema utilizado para inicializar los contratos, es esencial asegurar que todos los contratos en la cadena de herencia estén debidamente inicializados y que ningún contrato se inicialice dos veces o que los inicializadores sean idempotentes.
Advertencias y recomendaciones
Antes de concluir este artículo, deben darse algunas recomendaciones para el uso correcto del contrato Initializable.sol.
- OpenZeppelin tiene otro contrato llamado Initializable.sol en su biblioteca openzeppelin-upgrades. Este contrato existe por razones de compatibilidad hacia atrás y no debería utilizarse en nuevos proyectos. La forma recomendada de importarlo es a través de
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";. - Dado que la función de inicialización es una función normal, existe el riesgo de sufrir un frontrun por otra transacción. Si esto sucede, el contrato proxy debe ser redesplegado. Para prevenir esto, el constructor del contrato ERC1967Proxy realiza una llamada a la implementación al momento del despliegue. La llamada de inicialización debe hacerse en este momento, codificada en la variable
_data. - Como se mencionó en la sección anterior, cuando el contrato es parte de una cadena de herencia, debe tenerse cuidado de forma manual para no invocar el inicializador de un padre dos veces. El esquema no identifica este tipo de problemas potenciales, por lo que la verificación debe realizarse de manera manual. Una forma de resolver esto es asegurar que todas las funciones de inicialización sean idempotentes, lo que significa que tendrán el mismo efecto sin importar cuántas veces se ejecuten.
Publicado originalmente el 8 de julio