Los contratos proxy permiten a los contratos inteligentes conservar su estado mientras posibilitan la actualización de su lógica.
Por defecto, los contratos inteligentes no se pueden actualizar porque el bytecode desplegado no se puede modificar.
El único mecanismo en la EVM para cambiar el bytecode es desplegar un nuevo contrato. Sin embargo, el almacenamiento en este nuevo contrato no “sabría nada” sobre el contrato anterior. Esto significa que los valores anteriores guardados en el almacenamiento no estarían disponibles para el nuevo contrato.
La solución que introducen los proxies es mantener el almacenamiento en un contrato, y obtener la lógica de negocio y la funcionalidad (proporcionada por el bytecode) de otro contrato. Si se necesita una nueva funcionalidad, se despliega un nuevo “contrato lógico”, pero el contrato de almacenamiento sigue siendo el mismo.
Cuando se realiza una llamada al contrato de almacenamiento, este simplemente ejecuta un delegatecalls hacia la función en el contrato lógico para realizar la actualización del estado.
Para entender los proxies, comprender delegatecall es crucial, así que por favor lee primero el artículo enlazado.
A lo que aquí llamamos “contrato de almacenamiento” es al contrato proxy.
En este artículo, enseñaremos:
- Cómo funcionan los proxies y cómo crearlos
- Cómo actualizar un contrato inteligente proxy
Aviso de exención de responsabilidad: La implementación del proxy demostrada aquí es solo con fines de aprendizaje y no debe usarse en producción. Para proxies a nivel de producción, por favor consulta los capítulos posteriores de nuestro libro sobre Proxy Patterns. Pero deberías leer este capítulo primero para tener una mejor base antes de leer los capítulos posteriores.
¿Qué es un contrato proxy?
Un contrato proxy es un contrato inteligente que almacena variables de estado mientras delega toda su lógica a uno o varios contratos de implementación. Es decir, un contrato proxy simplemente conserva las variables de almacenamiento, mientras que la lógica de un contrato separado actualiza dichas variables.
Puedes pensar en un contrato proxy como algo similar a las aplicaciones y los datos en tu teléfono móvil. El teléfono retiene tus datos personales (contactos, fotos e historial de navegación) al igual que el proxy retiene su estado. El contrato de implementación es como el sistema operativo (OS) del teléfono y las aplicaciones, responsable de su funcionalidad y comportamiento. Cuando el OS o las aplicaciones se actualizan, las características del teléfono mejoran, pero tus datos permanecen intactos.
Esta analogía ilustra cómo un contrato proxy conserva su estado mientras delega la funcionalidad a un contrato de implementación.

Un contrato proxy y su contrato lógico se configuran de la siguiente manera:
- Despliegas un contrato proxy
- Luego despliegas el contrato de implementación
- Almacenas la dirección del contrato de implementación en el almacenamiento del proxy
- Ahora, el proxy reenvía todas las llamadas a la dirección de implementación a través de
DELEGATECALL
¿Cómo se reenvían las llamadas?
Dado que el proxy no tiene lógica propia, cualquier llamada realizada al contrato proxy será capturada por la función fallback. La función fallback maneja los casos donde una llamada a una función no coincide con ninguna función definida en un contrato.
Luego, el proxy realizará un delegatecall a la implementación usando el mismo calldata que recibió el proxy, tal como se ilustra en el siguiente diagrama:

El contrato proxy siempre realiza delegatecalls a la implementación usando el mismo calldata que recibió.
Aquí tienes una animación de nuestro artículo sobre delegatecall a modo de repaso:
¿Por qué usar contratos proxy?
Los contratos proxy tienen dos casos de uso notables:
1. Capacidad de actualización
La capacidad de actualizar un contrato es el caso de uso más común de los contratos proxy. Un patrón proxy te permite crear contratos que pueden ser actualizados (incorporar nueva lógica o características) sin interrumpir el estado o la dirección del contrato existente.
Por otro lado, un contrato no actualizable requiere que convenzas a todos los usuarios, proveedores de wallets y exchanges de migrar a una nueva dirección de contrato inteligente cada vez que solucionas un error o agregas nuevas características.
2. Ahorro en el costo de gas de despliegue
Los proxies pueden ahorrar gas si se necesitan desplegar múltiples copias de un contrato, ya que todos los proxies pueden usar el mismo contrato lógico mientras mantienen su propio estado separado. En lugar de desplegar toda la lógica del contrato para cada copia, despliegas un único contrato de implementación, y todos los proxies usan delegatecall para interactuar con él. Dado que la lógica es muy simple, el bytecode desplegado es mucho más pequeño y, por lo tanto, más barato de desplegar. Este patrón se llama el Minimal Proxy Pattern (más sobre esto más adelante en el artículo).
Cómo desplegar un contrato inteligente actualizable (no para producción)
Comencemos creando un contrato proxy simple que realice un delegatecalls a una única dirección de implementación programada estáticamente. Esto nos ayudará a comprender el patrón básico antes de hacerlo actualizable.
1. Despliega tu contrato de implementación
Consideremos el contrato a continuación como nuestro contrato de implementación. Toma dos números como argumentos y emite su suma como un evento.
contract Implementation {
event Result(uint256 newValue);
function addNumbers(uint256 number1, uint256 number2) public returns (uint256 result ) {
result = number1 + number2;
emit Result(result);
}
}
Desplegamos el contrato de implementación usando Remix como se muestra a continuación. Después del despliegue, copiamos la dirección:

2. Despliega un contrato proxy y configura la dirección de implementación
Ahora podemos desplegar el contrato Proxy con la dirección del contrato Implementation almacenada en el contrato Proxy.
// Replace this with the implementation address
// you get when you deployed the implementation
// contract
address immutable implementation = 0x44fE319Fc0C2d3e09F9a86AF94eEabB6173A1d58;
A continuación mostramos un ejemplo de un contrato proxy:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
contract Proxy {
// Change the implementation address to the one you get after deploying the
// implementation contract
address immutable implementation = 0x44fE319Fc0C2d3e09F9a86AF94eEabB6173A1d58;
fallback(bytes calldata data) external returns (bytes memory) {
(bool success, bytes memory result) = implementation.delegatecall(data);
require(success, "Delegatecall failed");
return result;
}
}
Despliega el contrato proxy en Remix siguiendo la ilustración del siguiente diagrama.

Probando/Interactuando con tu contrato
Cuando el despliegue del contrato esté completo, el siguiente paso sería interactuar con el contrato proxy. Exploraremos dos enfoques para interactuar con tu contrato.
1. Interactuando con el contrato proxy usando la interfaz Low Level Interaction en Remix
La primera forma de interactuar con el contrato proxy en Remix es usando la interfaz Low Level Interaction. Esto implica construir un calldata para nuestra función objetivo y pasarlo a la caja de entrada de calldata.
Así que, para activar la función addNumbers para sumar dos números 5 y 4, construiremos el calldata realizando la codificación ABI de la función y sus parámetros como se muestra a continuación:
function seeEncoding() external pure returns (bytes memory) {
return abi.encodeWithSignature("addNumbers(uint256,uint256)", 5,4);
}
El resultado será el código a continuación:
0xef9fc50b00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000004
Ahora que hemos construido el calldata, usémoslo para llamar a la función en el contrato proxy.
Pega el calldata en la caja de entrada. Deberíamos esperar que el resultado sea 9, ya que 5+4 es 9, como se muestra en la captura de pantalla a continuación:

Aunque el contrato Proxy no tiene lógica para emitir eventos, todavía vemos un evento emitido cuando enviamos el calldata al proxy. Eso se debe a que la lógica de emisión de eventos fue ejecutada a través de delegatecall por el proxy.
Claramente este proceso parece un poco complicado para probar tus contratos, especialmente si tenemos que codificar manualmente tu propio calldata. Una alternativa mucho más simple es interactuar con el contrato Proxy usando el ABI del contrato Implementation.
2. Interactuando con el contrato Proxy usando el ABI del contrato Implementation en Remix
Sigue estos pasos después del despliegue para configurar el contrato proxy de modo que puedas interactuar con él a través del ABI del contrato de implementación:
- En el menú desplegable
CONTRACT, selecciona el contratoImplementation.

- Copia la dirección del contrato
Proxyy pégala en la caja de entradaAt Address.

- Haz clic en el botón
At Addresspara interactuar con el contrato, como se muestra en el diagrama a continuación.

Ahora, deberías poder interactuar con la función addNumbers como se muestra en el diagrama a continuación:

Nota: aunque Remix etiquetó el contrato con el que estamos interactuando como Implementation, en realidad es el contrato Proxy usando el ABI del contrato Implementation.
Como podemos ver, cuando el usuario interactúa con el contrato Proxy e intenta llamar a la función addNumbers, se activa la función fallback porque la función addNumbers no existe en el Proxy. Una vez activada, la función fallback reenvía la ejecución al contrato Implementation usando delegatecall, donde la función está definida.
La grabación de pantalla a continuación resume los pasos anteriores:
Hasta ahora, hemos visto un contrato proxy básico que delega llamadas a una dirección de implementación preestablecida. Sin embargo, este enfoque no es actualizable, ya que la dirección de implementación está fija en el bytecode del contrato debido a la palabra clave immutable.
address immutable implementation = 0x44fE319Fc0C2d3e09F9a86AF94eEabB6173A1d58;
Actualizar la implementación del proxy
Para hacer que el contrato proxy sea actualizable, necesitamos almacenar la dirección de implementación de una manera que pueda ser actualizada después del despliegue. Podemos hacer esto agregando una función setImplementation al contrato proxy:
function setImplementation(address _implementation) public onlyOwner {
implementation = _implementation;
}
Ahora, en lugar de preestablecer la dirección de implementación, la almacenamos en una variable de estado implementation. De esa forma, podemos actualizarla cuando sea necesario.
contract Proxy {
// Store the implementation contract address
address implementation;
function setImplementation(address _implementation) public {
implementation = _implementation;
}
fallback(bytes calldata data) external returns (bytes memory) {
(bool success, bytes memory result) = implementation.delegatecall(data);
require(success, "Delegatecall failed");
return result;
}
}
Por motivos de seguridad, necesitamos asegurarnos de que solo un administrador pueda actualizar la dirección de implementación. Logramos esto introduciendo una variable de estado admin:
contract Proxy {
address public implementation;
address public admin;
...
Y restringiendo el acceso a la función setImplementation con un modificador:
modifier onlyOwner() {
require(msg.sender == admin, "Not the contract owner");
_;
}
Problema de colisión de almacenamiento
Ahora nuestro contrato proxy incluye las variables de almacenamiento implementation, admin y number (hemos introducido el number para ilustrar el problema de colisión de almacenamiento) y el diseño de almacenamiento ahora se ve así:
contract Proxy {
address public implementation;
address public admin;
uint256 public number;
...
Y en nuestro contrato de implementación, tenemos la variable de estado number:
contract Implementation {
uint256 public number;
...
Esto resultará en una colisión de almacenamiento. Cuando intentemos actualizar la variable de estado number en el contrato proxy, ¡estaremos sobrescribiendo el estado de la dirección implementation, lo cual no es lo que queremos!
En Solidity, las variables de almacenamiento se asignan a storage slots fijos según su orden de declaración en el contrato. Esto significa que si el diseño de las variables de almacenamiento en el proxy y en la implementación no coincide, ocurrirán conflictos.
En nuestro caso:
- El contrato proxy almacena la dirección
implementationen el slot 0,adminen el slot 1 ynumberen el slot 2. - En el contrato de implementación, la variable
numberse almacena en el slot 0.
Esto causa un conflicto porque cuando el contrato de implementación intenta actualizar su variable number, termina modificando en su lugar la dirección implementation en el contrato proxy, la cual está almacenada en el mismo slot (slot 0).

Esto conduce a un comportamiento inesperado: deseas actualizar el number en el proxy, pero en realidad estás sobrescribiendo la dirección implementation en el almacenamiento.
Nuestros artículos Storage Slots in Solidity: Storage Allocation and Low-level assembly storage operations y Storage Slot III (Complex Types) explican en detalle cómo funcionan los storage slots en Solidity con diagramas y animaciones útiles.
Usemos el siguiente contrato como ejemplo:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
contract Proxy {
address public implementation;
address public admin;
uint256 public number;
constructor() {
admin = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == admin, "Not the contract owner");
_;
}
function setImplementation(address _implementation) public onlyOwner {
implementation = _implementation;
}
fallback(bytes calldata data) external returns (bytes memory) {
require(implementation != address(0), "Implementation not set");
(bool success, bytes memory result) = implementation.delegatecall(data);
require(success, "Delegatecall failed");
return result;
}
}
contract Implementation {
uint256 public number;
function increment() public {
number++;
}
}
Notarás que el incremento no funcionará correctamente porque está actualizando la dirección del contrato de implementación en lugar de number en el almacenamiento:

¿Cómo resolvemos este problema de colisión de almacenamiento en los proxies?
Una forma es elegir un slot aleatorio de manera que una colisión sea extremadamente improbable. Más detalles sobre cómo elegimos dicho slot se encuentran en ERC-1967.
Así que, siguiendo la convención de ERC-1967, usaremos el slot a continuación para almacenar la dirección implementation:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
El slot se deriva pseudoaleatoriamente de:
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
Y usaremos el slot a continuación para almacenar la dirección admin:
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
Derivado de:
bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
Para leer o escribir en estos storage slots específicos, necesitarás usar sload y sstore respectivamente mediante inline assembly.
El código a continuación es una versión revisada de nuestro contrato proxy inicial, utilizando ahora los storage slots definidos por el estándar ERC-1967 para asegurar que no haya colisiones de almacenamiento entre los contratos proxy y de implementación.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
contract Proxy {
/**
* @dev Storage slot for the implementation address.
* This is derived from `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)`.
*/
bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
/**
* @dev Storage slot for the admin address.
* This is derived from `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)`.
*/
bytes32 private constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016eaf15eb9e8e9f03347e2db6a3ec1e1cb0;
// Initialize proxy with the owner
constructor() {
address admin = msg.sender;
assembly {
// Store admin in the ERC-1967 admin slot
sstore(_ADMIN_SLOT, admin)
}
}
modifier onlyOwner() {
address admin;
assembly {
// Load admin from the ERC-1967 admin slot
admin := sload(_ADMIN_SLOT)
}
require(msg.sender == admin, "Not the contract owner");
_;
}
function setImplementation(address _implementation) public onlyOwner {
assembly {
// Store implementation in the ERC-1967 implementation slot
sstore(_IMPLEMENTATION_SLOT, _implementation)
}
}
fallback(bytes calldata data) external payable returns (bytes memory) {
address implementation;
assembly {
// Load implementation from the ERC-1967 implementation slot
implementation := sload(_IMPLEMENTATION_SLOT)
}
require(implementation != address(0), "Implementation not set");
(bool success, bytes memory result) = implementation.delegatecall(data);
require(success, "Delegatecall failed");
return result;
}
}
contract Implementation {
uint256 public number;
function increment() public {
number++;
}
}
Ahora, si desplegamos este contrato y lo ejecutamos, obtendremos el resultado deseado como se muestra a continuación:

Discutimos EIP-1967 en detalle en nuestro artículo Storage Slots for Proxies, y es el siguiente capítulo en este libro.
Conclusión
En este artículo, hemos explorado el concepto de los contratos proxy, su importancia y cómo permiten la capacidad de actualización y reducen los costos de despliegue de contratos inteligentes en Solidity.
Aquí tienes algunos puntos clave de este artículo:
- El código de operación
DELEGATECALLhace posible la actualización con proxies. - Los contratos proxy (tanto actualizables como no actualizables) contienen el almacenamiento, incluyendo la dirección de implementación. Los contratos de implementación contienen la lógica.
- Un Proxy actualizable contiene el almacenamiento, la dirección de implementación y proporciona una función setter para actualizar la dirección de implementación. La implementación contiene únicamente la lógica.
Recomendación de lectura adicional: