El Transparent Upgradeable Proxy es un patrón de diseño para actualizar un proxy eliminando la posibilidad de una colisión de selectores de función (function selector clash).
Un proxy funcional de Ethereum necesita al menos las siguientes dos características:
- Un espacio de almacenamiento (storage slot) que contenga la dirección del contrato de implementación
- Un mecanismo para que un administrador cambie la dirección de la implementación
El estándar ERC-1967 dicta dónde debe mantenerse el espacio de almacenamiento que contiene la dirección de la implementación para minimizar la posibilidad de una colisión de almacenamiento (storage collision). Sin embargo, el estándar ERC-1967 no dicta cómo cambiar la dirección de la implementación.
El problema de colocar una función adicional dentro del proxy para cambiar la implementación (como updateImplementation(address _newImplementation)) es que la función de actualización tiene una probabilidad no insignificante de colisionar con una función en la implementación.
Colisión de Selectores de Función
Declarar funciones públicas dentro del proxy para actualizar la dirección de la implementación introduce la posibilidad de una colisión del function selector.
Aquí hay un ejemplo trivial:
contract ProxyUnsafe {
function changeImplementation(
address newImplementation
) public {
// some code...
}
fallback(bytes calldata data) external payable (bytes memory) {
(bool ok, bytes memory data) = getImplementation().delegatecall(data);
require(ok, "delegatecall failed");
return data;
}
}
contract Implementation {
// an identical function is declared here -- they will clash
function changeImplementation(
address newImplementation
) public {
}
//...
}
Recuerda, el fallback siempre se comprueba al final. Antes de que se llame al fallback, el contrato Proxy comprobará si el selector de función de 4 bytes coincide con changeImplementation (o con cualquier otra función pública en el proxy).
Por lo tanto, si se declara una función pública en el proxy, hay dos tipos de colisiones de selectores de función que pueden ocurrir:
- Si el contrato de implementación implementa una función con la misma firma (signature), esa función no se podrá llamar porque se llamará a la función pública del proxy con la misma firma, no al fallback. Y si el fallback no se activa, no habrá un delegatecall a la implementación.
- Si el contrato de implementación tiene una función con el mismo selector de función que la función pública en el proxy, también será imposible de llamar por la misma razón. Este escenario es una colisión de selectores de función que puede ocurrir por casualidad cuando los cuatro bytes coinciden. La probabilidad de que dos funciones diferentes tengan el mismo selector es de 1 en 4.290 millones; un selector de función consta de 4 bytes, por lo que hay 4.290 millones de posibilidades. Es una probabilidad pequeña, pero no insignificante. Por ejemplo,
clash550254402()tiene el mismo selector de función queproxyAdmin().
El Patrón Transparent Upgradeable Proxy Previene por Completo la Colisión de Selectores de Función
El patrón Transparent Upgradeable Proxy es un patrón de diseño para eliminar por completo la posibilidad de colisión de selectores de función.
Específicamente, el patrón Transparent Upgradeable Proxy dicta que no debe haber funciones públicas en el proxy a excepción del fallback.
Pero con solo una función fallback, ¿cómo llamamos a la función para actualizar el proxy?
La respuesta es detectar si msg.sender es el administrador.
contract Proxy is ERC1967 {
address immutable admin;
constructor(address admin_) {
admin = admin_
}
fallback() external payable {
if (msg.sender == admin) {
// upgrade logic
} else {
// delegatecall to implementation
}
}
}
La implicación de esto es que el administrador no puede usar el proxy directamente, ya que sus llamadas siempre se desvían del delegatecall. Sin embargo, utilizando un mecanismo diferente que discutiremos más adelante, sigue siendo posible que el administrador llame al proxy y que el proxy haga un delegatecall a la implementación como una transacción normal.
Cambiando un administrador inmutable
En el fragmento de código anterior, el administrador es inmutable. Esto significa que técnicamente el contrato no cumple con ERC-1967, el cual establece que el administrador debe mantenerse en el espacio de almacenamiento 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 o bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1).
Para ser compatible con ERC-1967, el Transparent Upgradeable Proxy almacena la dirección del administrador en ese espacio de almacenamiento, pero no utiliza esa variable de almacenamiento.
La presencia de una dirección en ese espacio de almacenamiento señalará a los exploradores de bloques que el contrato es un proxy (una de las intenciones de ERC-1967). Sin embargo, leer del almacenamiento cada vez que se hace una llamada al proxy añade otros 2.100 de gas a la llamada. Por lo tanto, es deseable utilizar una variable inmutable.
“Cambiando” el administrador
Sin embargo, sigue siendo deseable poder actualizar la dirección del administrador — pero inicialmente esto parece imposible porque el Proxy utiliza una variable inmutable.
El enfoque del Transparent Upgradeable Proxy para permitir la alteración del administrador del contrato proxy es doble. Primero, designa a otro contrato, conocido como ProxyAdmin, para ser el administrador del contrato proxy.

La dirección de un contrato inteligente nunca cambiará, por lo que esto es compatible con el hecho de que el Transparent Upgradeable Proxy almacene la dirección del administrador en una variable inmutable.
En segundo lugar, el owner del ProxyAdmin es el “verdadero” administrador. El ProxyAdmin simplemente enruta las llamadas desde el owner al Proxy. El “verdadero” administrador llama al ProxyAdmin y el ProxyAdmin llama al Transparent Proxy. Al cambiar el owner del ProxyAdmin podemos cambiar quién tiene la capacidad de actualizar el Transparent Proxy.
AdminProxy
A continuación se muestra el código del AdminProxy de OpenZeppelin (con los comentarios eliminados). Observa que solo hay una única función upgradeAndCall() que únicamente puede llamar a upgradeToAndCall() en el Proxy.
pragma solidity ^0.8.20;
import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol";
import {Ownable} from "../../access/Ownable.sol";
contract ProxyAdmin is Ownable {
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
constructor(address initialOwner) Ownable(initialOwner) {}
function upgradeAndCall(
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
Existe un malentendido común de que el administrador del Transparent Proxy no puede usar el contrato porque sus llamadas se reenvían a la actualización. Sin embargo, el owner del AdminProxy puede usar el Proxy sin ningún problema, como demuestra el diagrama a continuación.
De hecho, veremos más adelante que existe un mecanismo para que el ProxyAdmin realice una llamada arbitraria al proxy, como implica el nombre de la función upgradeToAndCall().

Haciendo que el proxy no sea actualizable
Si el owner se cambia a la dirección cero, o a otro contrato inteligente que no sea capaz de usar correctamente la función upgradeAndCall() (o cambiar el owner), entonces el Transparent Upgradeable Proxy ya no será actualizable. Esto puede suceder, por ejemplo, si el propietario del AdminProxy se configura para ser un contrato AdminProxy diferente.
Detalles de Implementación
El Transparent Upgradeable Proxy de OpenZeppelin implementa el estándar con tres contratos:
- Proxy.sol
- ERC1967Proxy.sol (hereda de Proxy.sol)
- TransparentUpgradeableProxy.sol (hereda de ERC1967Proxy.sol)
Contrato principal (Parentmost): Proxy.sol
El contrato base es Proxy.sol. Dada una dirección de implementación, envía un delegatecall a la implementación. La función _implementation() no está implementada en Proxy — es anulada (overridden) e implementada por su hijo ERC1967Proxy, el cual hará que devuelva el espacio de almacenamiento correspondiente.
abstract contract Proxy {
function _delegate(address implementation) internal virtual {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
function _implementation() internal view virtual returns (address);
function _fallback() internal virtual {
_delegate(_implementation());
}
fallback() external payable virtual {
_fallback();
}
}
Hijo de Proxy.sol: ERC1967Proxy.sol
ERC1967Proxy.sol hereda de Proxy.sol. Esto añade (y anula) la función interna _implementation() que devuelve la dirección de la implementación almacenada en el espacio especificado por ERC-1967. El constructor de este contrato almacena la implementación en el espacio de almacenamiento especificado por ERC-1967. Sin embargo, el Transparent Upgradeable Proxy no hará uso de esta función — en su lugar, utilizará su propia variable inmutable.
pragma solidity ^0.8.20;
import {Proxy} from "../Proxy.sol";
import {ERC1967Utils} from "./ERC1967Utils.sol";
contract ERC1967Proxy is Proxy {
constructor(address implementation, bytes memory _data) payable {
ERC1967Utils.upgradeToAndCall(implementation, _data);
}
// reads from bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
function _implementation() internal view virtual override returns (address) {
return ERC1967Utils.getImplementation();
}
}
Hijo de ERC1967Proxy.sol: TransparentUpgradeableProxy.sol
Finalmente, TransparentUpgradeableProxy.sol hereda de ERC1967Proxy.sol. En el constructor de este contrato, se despliega el ProxyAdmin y el administrador inmutable (primera variable en el contrato) se establece como la dirección del ProxyAdmin en el constructor.
contract TransparentUpgradeableProxy is ERC1967Proxy {
address private immutable _admin;
error ProxyDeniedAdminAccess();
constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
_admin = address(new ProxyAdmin(initialOwner));
// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(_proxyAdmin());
}
function _proxyAdmin() internal view virtual returns (address) {
return _admin;
}
function _fallback() internal virtual override {
if (msg.sender == _proxyAdmin()) {
if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
revert ProxyDeniedAdminAccess();
} else {
_dispatchUpgradeToAndCall();
}
} else {
super._fallback();
}
}
function _dispatchUpgradeToAndCall() private {
(address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
}
Consideremos el caso en el que msg.sender es el _proxyAdmin. En ese caso, la llamada se enruta a _dispatchUpgradeToAndCall(), pero _fallback() comprueba primero que el selector de función proporcionado es el selector de función para upgradeToAndCall. El “selector” aquí no es un selector “real”, ya que el Transparent Upgradeable Proxy no tiene funciones públicas. Sin embargo, para permitir que el ProxyAdmin haga una llamada de interfaz en Solidity (llamada de alto nivel), necesita aceptar el calldata codificado en ABI para upgradeToAndCall() desde el ProxyAdmin.
Recuerda, el ProxyAdmin está haciendo una llamada de interfaz a upgradeToAndCall en el Proxy a pesar de que el proxy no tiene funciones públicas aparte del fallback (el código de ProxyAdmin se muestra a continuación):

A continuación hay un video que muestra los tres bloques de código lado a lado y cómo los diferentes contratos en la cadena de herencia (Proxy, ERC1967Proxy y TransparentUpgradeableProxy) interactúan entre sí:
¿Por qué upgradeToAndCall() en lugar de solo upgradeTo()?
Al actualizar el contrato de implementación, es posible hacerle una llamada como si el ProxyAdmin fuera el msg.sender y hacer que la transacción haga un delegatecall a la implementación como si fuera una interacción de proxy normal. Por supuesto, esto no ocurre dentro del fallback porque las llamadas desde ProxyAdmin se enrutan a la lógica de actualización.
El código a continuación es de ERC1967Utils.sol, con el cual el TransparentUpgradeableProxy se compone para permitir la actualización del espacio de implementación. La biblioteca proporciona una función auxiliar interna para actualizar el espacio de almacenamiento que contiene la dirección de la implementación.
/**
* @dev Performs implementation upgrade with additional setup call if data is nonempty.
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-Upgraded} event.
*/
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
_setImplementation(newImplementation);
emit IERC1967.Upgraded(newImplementation);
if (data.length > 0) {
Address.functionDelegateCall(newImplementation, data);
} else {
_checkNonPayable();
}
}
Solo hará un delegatecall al contrato de implementación si data.length > 0.
upgradeToAndCall() también realiza un delegatecall desde el Proxy a la implementación en la misma transacción que la actualización. Esto es lo mismo que si ProxyAdmin llamara al proxy usando cualquier calldata que se especifique en data, y luego el proxy realizara un delegatecall a la implementación.
De esta manera, el ProxyAdmin puede realizar llamadas arbitrarias al Proxy.
Ten en cuenta que upgradeToAndCall no requiere que el contrato actualizado sea una implementación diferente — es posible “actualizar” a la misma implementación.
La implicación de esto es que el contrato ProxyAdmin puede realizar llamadas de delegatecall arbitrarias al contrato de implementación a través del Proxy — pero el msg.sender desde la perspectiva del Transparent Proxy es el ProxyAdmin.
Que el ProxyAdmin pueda usar el contrato no es un “problema” — el ProxyAdmin tiene la capacidad de cambiar completamente la implementación — el owner del ProxyAdmin ya tiene control de administrador sobre el Proxy.
La única restricción que tiene el ProxyAdmin al actualizar es que no puede actualizar a un contrato vacío (una dirección sin bytecode). La función _setImplementation comprueba si la longitud del código de la nueva implementación es mayor que cero
/**
* @dev Stores a new address in the ERC-1967 implementation slot.
*/
function _setImplementation(address newImplementation) private {
if (newImplementation.code.length == 0) {
revert ERC1967InvalidImplementation(newImplementation);
}
StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
}
Resumen del Transparent Upgradeable Proxy
- El Transparent Upgradeable Proxy es un patrón de diseño para evitar la colisión de selectores de función entre el proxy y la implementación.
- La función fallback es la única función pública en el Transparent Upgradeable Proxy.
- La funcionalidad de actualización solo puede ser invocada por el administrador a través de la función fallback. Todas las llamadas de direcciones que no son del administrador se convierten en delegatecall al proxy.
- El Transparent Upgradeable Proxy utiliza una variable inmutable para almacenar el administrador y ahorrar gas. Para cumplir con ERC-1967, almacena la dirección del administrador en el espacio (slot)
adminespecificado por ERC-1967, aunque nunca lee de ese espacio. - Debido a que el administrador no puede ser cambiado, el administrador se configura como un contrato inteligente llamado
AdminProxy. ElAdminProxyexpone una única funciónupgradeAndCall()que solo puede ser llamada por el owner delAdminProxy. El owner delAdminProxypuede ser cambiado. Dicho cambio altera quién puede actualizar el espacio de implementación en el Transparent Upgradeable Proxy.
Nos gustaría agradecer a @ernestognw de OpenZeppelin por revisar este artículo y aportar sugerencias útiles.
Publicado originalmente el 4 de junio