Un Beacon Proxy es un patrón de actualización de smart contracts donde múltiples proxies utilizan el mismo contrato de implementación, y todos los proxies pueden ser actualizados en una sola transacción. Este artículo explica cómo funciona este patrón de proxy.
Requisitos previos
Vamos a asumir que ya sabes cómo funciona un minimal proxy y tal vez incluso UUPS o Transparent.
Motivación para los Beacon Proxies
Típicamente, un patrón de proxy utiliza un único contrato de implementación y un único contrato proxy. Sin embargo, es posible que múltiples proxies utilicen la misma implementación.

Para entender por qué querríamos esto, imaginemos un juego totalmente on-chain. Este juego quiere almacenar cada cuenta de usuario como un contrato separado, de modo que las cuentas puedan transferirse fácilmente a diferentes wallets y una wallet pueda poseer múltiples cuentas. Cada proxy almacena la información de la cuenta en sus respectivas variables de almacenamiento.
Hay un par de formas en las que podrías implementar esto:
- Usar el Minimal Proxy Standard (EIP1167) y desplegar cada cuenta como un clon.
- Usar los patrones de proxy UUPS o Transparent y desplegar un proxy para cada cuenta.
En la mayoría de los casos, cualquiera de las opciones funcionaría, pero ¿qué pasaría si quisieras añadir nueva funcionalidad a la cuenta?
En el caso del Minimal Proxy Standard, tendrías que volver a desplegar todo el sistema y migrar a todos socialmente porque los clones no son actualizables.
Los proxies tradicionales son actualizables, pero tendrías que actualizar cada proxy uno por uno. Esto sería costoso a medida que haya más cuentas.
Tanto los clones como los proxies son complicados de actualizar cuando hay una gran cantidad de ellos.
El patrón beacon está diseñado para resolver este problema: te permite desplegar un nuevo contrato de implementación y actualizar todos los proxies simultáneamente.
Esto significa que el patrón beacon te permitiría desplegar una nueva implementación de la cuenta y actualizar todos los proxies a la vez.
A un alto nivel, este estándar te permite crear una cantidad ilimitada de proxies por contrato de implementación, y aún así poder actualizarlos fácilmente.
Cómo funciona un beacon
Como sugiere el nombre, este estándar requiere un beacon, al cual OpenZeppelin se refiere como “UpgradeableBeacon” y lo implementa en UpgradeableBeacon.sol.
El beacon es un smart contract que proporciona la dirección de la implementación actual a los proxies a través de una función pública. El beacon es la fuente de la verdad para los proxies con respecto a la dirección de la implementación actual, razón por la cual se llama “beacon”.
Cuando un proxy recibe una transacción entrante, el proxy primero llama a la función view implementation() en el beacon para obtener la dirección de la implementación actual, y luego el proxy realiza un delegatecalls a esa dirección. Esto es lo que permite que el beacon funcione como la fuente de la verdad sobre dónde está la implementación.

Cualquier proxy adicional seguirá el mismo patrón: primero obtienen la dirección de la implementación desde el beacon usando implementation() y luego hacen delegatecall a esa dirección.
Nota: los proxies saben dónde llamar a implementation() porque almacenan la dirección del beacon en una variable inmutable. Explicaremos este mecanismo con más detalle más adelante.
Este patrón es altamente escalable porque cada proxy adicional simplemente lee la dirección de la implementación desde el beacon y luego usa delegatecall.

Aunque el patrón de beacon proxy involucra más contratos, el proxy en sí es más simple que los proxies UUPS o Transparent Upgradeable Proxies.
Los beacon proxies siempre llaman a la misma dirección del beacon para obtener la dirección de la implementación actual, por lo que no necesitan preocuparse por detalles como quién es el administrador o cómo cambiar la dirección de la implementación.
Actualizando múltiples proxies simultáneamente
Dado que todos los proxies obtienen la dirección de la implementación del almacenamiento del beacon, cambiar la dirección en el slot de almacenamiento hace que todos los proxies hagan delegatecall a la nueva dirección, “redirigiéndolos” instantáneamente.
Para actualizar todos los proxies simultáneamente:
- Desplegar un nuevo contrato de implementación.
- Establecer la nueva dirección de implementación en el almacenamiento del beacon.
Establecer la nueva dirección de implementación se hace llamando a upgradeTo(address newImplementation) en el beacon y pasando la nueva dirección como argumento. upgradeTo() es una de las dos funciones públicas en UpgradeableBeacon.sol (el beacon). La otra función pública (view) es implementation() que mencionamos anteriormente.
Nota: upgradeTo() tiene un modificador onlyOwner que se establece en el constructor de UpgradeableBeacon.sol (el beacon).
/**
* @dev Upgrades the beacon to a new implementation.
*
* Emits an {Upgraded} event.
*
* Requirements:
*
* - msg.sender must be the owner of the contract.
* - `newImplementation` must be a contract.
*/
function upgradeTo(address newImplementation) public virtual onlyOwner {
_setImplementation(newImplementation);
}
upgradeTo() llama a una función interna _setImplementation(address newImplementation) (también en el beacon), que comprueba si la nueva dirección de implementación es un contrato y luego establece la variable de almacenamiento de la dirección, _implementation, en el beacon a la nueva dirección de implementación.
/**
* @dev Sets the implementation contract address for this beacon
*
* Requirements:
*
* - `newImplementation` must be a contract.
*/
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "UpgradeableBeacon: implementation is not a contract");
_implementation = newImplementation;
}
Ahora que la dirección de la implementación en el almacenamiento del beacon ha cambiado, todos los proxies leerán la nueva dirección en el beacon y dirigirán su delegatecall a la nueva implementación.
Esta forma de actualizar es simple porque solo estás “apuntando” el beacon, y a su vez los proxies, a una nueva implementación. Incluso podrías apuntar la implementación a una versión anterior si necesitaras revertir los cambios (ten en cuenta las colisiones de almacenamiento).

Recorrido por el código del contrato proxy
Para evitar confusiones, usamos la terminología “BeaconProxy” para referirnos al proxy del smart contract y “beacon proxy” para referirnos al patrón de diseño. Ahora discutiremos el contrato proxy que OpenZeppelin llama “BeaconProxy” y que implementa en BeaconProxy.sol.
El BeaconProxy de OpenZeppelin hereda de Proxy.sol y añade más funcionalidad:
- Almacena la dirección del contrato beacon en
_beacon - Se añade una función
_getBeacon()para devolver la variable_beacon - La función
_implementation()se sobrescribe para llamar a.implementation()en la dirección del_beacon - Se añade un constructor para establecer la variable
_beacony el parámetrodatainicializa el proxy
A continuación se muestra la implementación de OpenZeppelin de un BeaconProxy con los comentarios eliminados.
contract BeaconProxy is Proxy {
address private immutable _beacon;
constructor(address beacon, bytes memory data) payable {
ERC1967Utils.upgradeBeaconToAndCall(beacon, data);
_beacon = beacon;
}
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}
function _getBeacon() internal view virtual returns (address) {
return _beacon;
}
}
La función _implementation() se sobrescribe porque Proxy.sol llama a esa función para recuperar la dirección de la implementación antes del delegatecall.
El constructor del BeaconProxy tiene dos propósitos:
- establecer la dirección del
_beacon - inicializar el proxy con
data
Este parámetro opcional data se usa en un delegatecall a la implementación, permitiendo la inicialización del almacenamiento del proxy. En nuestro ejemplo del juego, esto podría significar inicializar la cuenta (proxy) con las estadísticas iniciales del jugador. Esencialmente, el argumento data sirve como el constructor de Solidity para el proxy: los datos se usan en un delegatecall a la implementación para que la lógica de implementación pueda configurar las variables de almacenamiento del proxy.
function upgradeBeaconToAndCall(address newBeacon, bytes memory data) internal {
_setBeacon(newBeacon);
emit IERC1967.BeaconUpgraded(newBeacon);
if (data.length > 0) {
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
} else {
_checkNonPayable();
}
}
ERC1967 & BeaconProxy.sol
Para que los exploradores de bloques sepan que el BeaconProxy es un proxy, debe adherirse a la especificación ERC-1967. Dado que es específicamente un beacon proxy, necesita almacenar la dirección del Beacon en el slot de almacenamiento: 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50, calculado a partir de bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1).
De manera similar al Transparent Upgradeable Proxy, esta dirección de almacenamiento en realidad no es utilizada por el BeaconProxy. Es simplemente una señal para los exploradores de bloques de que el contrato es un Beacon Proxy. La dirección de implementación real se almacena en una variable inmutable para propósitos de optimización de gas; la dirección del beacon nunca cambia.
EIP2930
Siempre usa transacciones con access list con este patrón, ya que puedes ahorrar gas al realizar una llamada entre contratos y acceder al almacenamiento de otro contrato. Específicamente, el proxy está llamando al beacon y obteniendo la dirección de la implementación desde el almacenamiento. Los benchmarks de access list para un Beacon Proxy se pueden ver aquí.
Desplegando múltiples BeaconProxies
Desplegar varios BeaconProxies manualmente sería una molestia. Ahí es donde entra el contrato factory. El factory despliega nuevos proxies y establece la dirección del beacon en su constructor.
OpenZeppelin no requiere ni proporciona un contrato factory estándar en su patrón beacon. Sin embargo, en la práctica, un contrato factory ayuda a desplegar nuevos proxies.
A continuación se proporciona un ejemplo de un factory. El factory almacena la dirección del beacon e incluye una función para crear nuevos proxies que usan ese beacon. La función createBeaconProxy() toma la data como entrada para pasarla al constructor del BeaconProxy. Después de desplegar el proxy, devuelve la dirección del proxy.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
/// @dev THIS CONTRACT IS FOR TEACHING PURPOSES ONLY
contract ExampleFactory {
address public immutable beacon;
constructor(address Beacon) {
beacon = Beacon;
}
function createBeaconProxy(bytes memory data) external returns (address) {
BeaconProxy proxy = new BeaconProxy(beacon, data);
return address(proxy);
}
}
Ahora que entendemos cómo desplegar proxies con un contrato factory, veamos cómo encaja en la estructura general.

Y esos son todos los contratos necesarios para diseñar un patrón beacon:
- Implementación
- Beacon
- Factory (opcional)
- Proxy(s)
Despliegue
Bien, pero ¿cómo desplegamos todo este sistema? No es tan aterrador como puede parecer.
OpenZeppelin ofrece un plugin de Upgrades tanto para Hardhat como para Foundry. Es tan simple como instalar la biblioteca y simplemente llamar a deployBeacon() con los parámetros para el contrato beacon. A partir de ahí, los BeaconProxies se pueden desplegar llamando a deployBeaconProxy(). La actualización es similar: se llama a la función upgradeBeacon() con los parámetros para la nueva implementación.
El sistema también se puede desplegar manualmente:
- Desplegar el contrato de implementación.
- Desplegar el contrato beacon y en el constructor, introducir la dirección de implementación y la dirección de quien está autorizado a actualizar la dirección de implementación.
- Desplegar el contrato factory.
- Usar el factory para desplegar tantos proxies como sean necesarios.

Un ejemplo del mundo real
¿Cuándo se usaría un beacon proxy en la vida real? Creé un beacon proxy para Kwenta que está activo en Optimism con más de 20M en TVL.
El beacon proxy fue para los paquetes de vesting de Kwenta. Un “paquete de vesting” es un smart contract que libera tokens lentamente (KWENTA) a intereses especiales y contribuidores principales del protocolo. A cada persona se le asigna un paquete de vesting que varía en cantidad de tokens y duración (generalmente 1-4 años). Para aprender más sobre el vesting en crypto, mira aquí.
¿Por qué un beacon proxy específicamente?
-
Tenía que ser fácilmente actualizable. Los paquetes de vesting debían ser actualizables porque llaman a funciones en el sistema de staking de Kwenta, que también es actualizable. Si el sistema de staking se actualiza en el futuro, es posible que la funcionalidad de los paquetes de vesting ya no funcione. Hacer que los paquetes de vesting sean actualizables permite que estén preparados para el futuro.
-
Todos los paquetes tenían la misma lógica de vesting (
vest(),stake(), etc.) pero diferentes parámetros inicializados (cantidades de tokens, duraciones de vesting). Parte de esto requería hacer que los paquetes de vesting fueran contratos independientes o “aislados” porquea. Desarrollo más sencillo: tener un contrato inicializable por persona era mucho más simple que tener un gran contrato con mapeos complejos para llevar un registro del paquete de vesting diferente de cada uno. Además, los KWENTA para cada paquete se ponían en staking automáticamente al momento de la creación del paquete, lo que significaba que cada persona estaba acumulando recompensas. Si los paquetes de todos estuvieran juntos en 1 contrato, las recompensas se mezclarían y sería un desastre.
b. La propiedad de los paquetes de vesting se podría transferir fácilmente a otras direcciones o a un multi-sig.
c. Hacer vesting significaba llamar a
unstake()en el contrato de staking de Kwenta. El contrato de staking tiene un tiempo de espera de 2 semanas paraunstake(). Así que, si los paquetes de todos estuvieran en un solo contrato, y una persona hiciera su vesting (y a su vez su unstake), entonces nadie más podría hacer su vesting por al menos 2 semanas. Aislar los paquetes en contratos separados evita este bug. -
Los paquetes de vesting debían soportar a más de 10 personas. Esto significaba más de 10 proxies.
Un beacon proxy fue capaz de hacer todo eso sin sacrificar nada.
Los clones podrían desplegar fácilmente más de 10 contratos inicializables pero no son actualizables.
Transparent y UUPS son actualizables pero requerirían que cada paquete de vesting se actualizara uno por uno, lo que habría tomado mucho tiempo y costado más gas.
Y se consideró un diamond proxy pero era demasiado complejo para esta estructura.
FactoryBeacon de Kwenta
Como optimización, FactoryBeacon combina los contratos UpgradeableBeacon.sol y Factory. Esta combinación simplifica la configuración y reduce la superficie de ataque.

Esto es posible porque el factory no necesita ser un contrato independiente: son solo unas pocas líneas de código que despliegan un nuevo BeaconProxy y establecen la dirección de su beacon y los datos de inicialización.
Aquí hay un ejemplo de un contrato factory y beacon combinados. Al heredar de UpgradeableBeacon, el contrato conserva la misma funcionalidad que un beacon regular, mientras que la función createBeaconProxy() añade la funcionalidad de factory. Además, ya no hay necesidad de almacenar la dirección del beacon, ya que ahora se puede usar address(this).

Sin embargo, la “estructura beacon” general sigue siendo la misma.
Cada persona llama a su BeaconProxy que tiene todo el almacenamiento para su paquete de vesting específico (cantidades de vesting, duración).
Luego, el BeaconProxy obtiene la dirección de implementación del FactoryBeacon, que sigue teniendo las mismas funciones que un beacon regular.
Después de obtener la dirección de implementación del FactoryBeacon, el BeaconProxy entonces hace delegatecall al VestingBaseV2 que es simplemente la implementación.
Ten en cuenta que el único que puede llamar al FactoryBeacon es el adminDAO (un multisig de administradores). Un administrador es el único que puede crear un nuevo paquete de vesting (BeaconProxy) y actualizar los proxies a una nueva implementación.
Conclusión
El patrón de beacon proxy permite la creación de múltiples proxies para una sola implementación, con la capacidad de actualizarlos todos a la vez. El factory despliega nuevos proxies, que utilizan delegatecall a una dirección obtenida del beacon. El beacon sirve como la fuente de verdad para la implementación.
Cabe señalar que el patrón beacon proxy incurre en costos de gas más altos durante la configuración en comparación con otros patrones como UUPS o Transparent porque deben desplegarse tanto un factory como un beacon además del proxy. Adicionalmente, cada llamada a un proxy incurre en un costo adicional para llamar al beacon. Este costo extra de gas es la principal desventaja. No es necesariamente una desventaja si necesitas múltiples proxies, ya que es aquí cuando el patrón beacon proxy resulta más beneficioso. El mayor costo de gas es la razón por la que no verás típicamente un patrón beacon proxy utilizado con un solo proxy.
Si bien los beacons permiten actualizar múltiples proxies simultáneamente, la configuración es más compleja y costosa. Requiere más gas e implica configurar contratos adicionales, lo que lo hace más caro en términos de desarrollo y auditoría. Por lo tanto, el patrón beacon proxy es ventajoso solo si necesitas una gran cantidad de proxies.
Autoría
Este artículo fue escrito por Andrew Chiaramonte (LinkedIn, Twitter).
Publicado originalmente el 20 de julio de 2024