Actualizar un smart contract es un proceso de múltiples pasos y propenso a errores, por lo que para minimizar las posibilidades de error humano, es deseable utilizar una herramienta que automatice el procedimiento lo más posible.
Por lo tanto, el OpenZeppelin Upgrade Plugin optimiza el despliegue, la actualización y la gestión de smart contracts construidos con Foundry o Hardhat.
En este artículo, aprenderemos cómo utilizar el Upgrade Plugin con Foundry para gestionar las actualizaciones de contratos, tanto localmente como en la Sepolia Testnet. También cubriremos cómo estos plugins protegen contra los problemas comunes de actualización.
Requisitos previos
Para aprovechar al máximo esta guía, el lector debe estar familiarizado con:
- La operación delegatecall en Solidity.
- Estándares como ERC1967 y el rol de los initializers en la configuración del estado en contratos actualizables.
- Fallos comunes en actualizaciones, incluyendo colisiones de function selector y storage slot.
- Conocimiento de patrones de proxy comunes como el Transparent Upgradeable Proxy, UUPS Proxy, o Beacon Proxy.
- El diseño de almacenamiento Namespace, o EIP-7201.
Upgrades plugin para Foundry
El plugin de OpenZeppelin para Foundry es una utilidad que puede ser importada en scripts de Solidity o pruebas unitarias de Foundry, y se puede importar de la siguiente manera:
import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol";
Esta biblioteca expone funciones para desplegar proxies, contratos de implementación y otros contratos relacionados. En la siguiente sección ofrecemos una visión general de alto nivel sobre sus capacidades, y más adelante mostramos cómo escribir pruebas unitarias para actualizaciones, y cómo crear un script que asista en el despliegue y actualización de smart contracts usando este plugin.
Características de los Upgrade Plugins
- Dada una referencia a la implementación anterior del smart contract, el plugin compara la implementación previa con la nueva para verificar problemas potenciales como colisiones de storage slot y otros problemas que discutiremos más adelante.
- Los plugins soportan el despliegue y la actualización de los patrones UUPS, Transparent y Beacon Proxy. No soporta el patrón Diamond Proxy.
- Cuando un contrato actualizable se despliega por primera vez usando este plugin, se crean automáticamente hasta tres componentes (dependiendo de si el patrón de actualización es UUPS, Transparent o Beacon Proxy):
- El contrato de implementación: Contiene la lógica real del contrato.
- Proxy: Si se despliega un nuevo proxy, el plugin maneja su creación y lo vincula al contrato de implementación especificado. Sin embargo, si ya existe un proxy, el plugin facilita el proceso de actualización vinculando el proxy existente a una nueva implementación.
- ProxyAdmin: Este componente administrativo gestiona quién puede actualizar el proxy exclusivamente para transparent proxies (solo los Transparent Upgradeable Proxies usan un Proxy Admin).
- Beacon Proxies: El patrón Beacon Proxy no asigna direcciones de administrador individuales a los proxies. En su lugar, un único beacon tiene un propietario que actualiza la implementación para todos los proxies vinculados. El plugin automatiza la configuración del beacon y los proxies, y actualiza la implementación del beacon.
- Los plugins están diseñados para funcionar tanto con Hardhat como con Foundry. Mientras que el entorno de Hardhat mantiene un registro detallado de los despliegues, Foundry se centra en el uso de reference contracts para garantizar la seguridad de las actualizaciones.
Definiendo Reference Contracts
A diferencia del plugin OpenZeppelin Upgrade para Hardhat, que rastrea automáticamente los contratos de implementación y sus versiones a través de JSON, el plugin de Foundry requiere que los desarrolladores definan explícitamente los Reference Contracts.
Considera el NatSpec en el siguiente contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @custom:oz-upgrades-from MyContractV1
contract MyContractV2 {
...
}
o
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @custom:oz-upgrades-from contracts/MyContract.sol:MyContractV1
contract MyContractV2 {
...
}
El natspec hace referencia al reference contract, y es lo que el plugin de Foundry utiliza para obtener una referencia a dicho reference contract.
El reference contract es el contrato que se va a actualizar y sirve como línea base para asegurar que la nueva implementación sea compatible con el estado y diseño original.
En general, puedes pensar en el “reference contract” como la “implementación previa”.
La función validateUpgrade del plugin verifica que un nuevo contrato de implementación sea compatible con un reference contract. Requiere que se establezca la opción referenceContract, o que la anotación @custom:oz-upgrades-from <reference> esté presente en el nuevo contrato.
Los detalles sobre cómo se utiliza la función validateUpgrade de la herramienta Upgrades se discutirán más adelante en esta guía.
Probando actualizaciones de Smart Contracts localmente en Foundry
Ahora mostraremos los pasos para desplegar y actualizar un Transparent Upgradeable Proxy localmente. Más adelante mostraremos cómo hacer esto en una testnet. Aquí están los pasos que tomaremos a alto nivel:
- Desplegar el proxy y la implementación
ContractA:- Desplegar
ContractAe inicializarlo. - Este paso configura automáticamente
ContractA, unTransparentUpgradeableProxyy un contratoProxy Admin. ContractAactúa como el reference contract para este proceso de actualización.
- Desplegar
- Actualizar a
ContractB:- La herramienta utiliza el método
upgradeAndCallen el proxy para actualizar el contrato aContractB.
- La herramienta utiliza el método
Paso 1: Configurando el entorno
Comienza creando un nuevo directorio de proyecto e inicializando Foundry. Abre tu terminal y ejecuta los siguientes comandos:
mkdir rareskills-foundry-upgrades && cd rareskills-foundry-upgrades
forge init
A continuación, necesitamos preparar los archivos esenciales para el proyecto. Estos incluyen los archivos del smart contract, un archivo de prueba y un archivo para mapeos de dependencias.
Ejecuta el siguiente comando para crear estos archivos en el directorio de tu proyecto:
touch src/ContractA.sol && touch src/ContractB.sol && touch test/Upgrades.t.sol && touch remappings.txt
Paso 2: Configurar el Proyecto
Una vez que el proyecto está inicializado, la siguiente tarea es configurar las bibliotecas de OpenZeppelin necesarias para manejar contratos actualizables.
Actualiza el archivo remappings de la siguiente manera:
forge remappings > remappings.txt
Ejecuta los siguientes comandos en la terminal para instalar las bibliotecas requeridas.
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit
forge install OpenZeppelin/openzeppelin-foundry-upgrades --no-commit
La flag no-commit evita la molestia de hacer commit del estado actual del repositorio antes de usar los comandos anteriores.
A continuación, necesitamos asegurarnos de que el proyecto sepa dónde encontrar los archivos de OpenZeppelin. Esto se hace configurando el archivo remappings.txt, que creamos previamente.
Abre remappings.txt e inserta las siguientes líneas:
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
Si los remappings de OpenZeppelin ya están en el archivo remapping.txt, reemplázalos con los anteriores.
A continuación, elimina la prueba test/Counter.t.sol y src/Counter.sol que se crean automáticamente:
rm test/Counter.t.sol
rm src/Counter.sol
Finalmente, abre foundry.toml y añade la siguiente configuración:
[profile.default]
src = "src"
out = "out"
libs = ["node_modules", "lib"]
build_info = true
extra_output = ["storageLayout"]
ffi = true
ast = true
Paso 3: Crear los Contratos Actualizables
Ahora crearemos dos smart contracts, ContractA y ContractB, para demostrar el proceso de actualización.
Comienza con ContractA.sol. Este contrato contiene una única variable pública, value, y un método, initialize, que reemplaza al constructor.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract ContractA is Initializable{
uint256 public value;
function initialize(uint256 _setValue) public initializer {
value = _setValue;
}
}
A continuación, crearemos ContractB.sol para mostrar la ruta de actualización desde ContractA.
ContractB amplía la funcionalidad de ContractA añadiendo un método para incrementar value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
/// @custom:oz-upgrades-from ContractA
contract ContractB is Initializable {
uint256 public value;
function initialize(uint256 _setValue) public initializer {
value = _setValue;
}
function increaseValue() public {
value += 10;
}
}
Al usar la anotación @custom:oz-upgrades-from ContractA, especificamos que ContractB es la versión actualizada de ContractA.
Esta anotación no requiere que ContractA y ContractB estén en el mismo directorio. Identifica a ContractA por su nombre, siempre y cuando esté definido de forma única dentro del proyecto; de lo contrario, se requiere un fully qualified name.
Sin esta anotación, el plugin no continuará y mostrará un error como el siguiente:
`The contract ${sourceContract.fullyQualifiedName} does not specify what contract it upgrades from. Add the \`@custom:oz-upgrades-from <REFERENCE_CONTRACT>\` annotation to the contract, or include the reference contract name when running the validate command or function.`
Aunque estamos preparando tanto ContractA como ContractB para este ejemplo, esto no significa que necesitemos “anticipar” futuras actualizaciones de ContractA. Simplemente estamos creando ContractB por adelantado para propósitos de conveniencia de este ejemplo.
Paso 4: Probando la Funcionalidad Actualizable
Este paso implica compilar los contratos, desplegarlos como un Transparent Proxy Pattern, realizar una actualización y verificar si el estado del contrato se actualiza como se espera.
Preparando el Entorno de Pruebas
Primero, navega a la carpeta test e inserta el siguiente código en el archivo Upgrades.t.sol.
Esta configuración prueba la capacidad de actualización de ContractA a ContractB.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "openzeppelin-foundry-upgrades/Upgrades.sol";
import "../src/ContractA.sol";
import "../src/ContractB.sol";
contract UpgradesTest is Test {
// future code will go here
}
La prueba inicial implica dos acciones principales:
- Desplegar
ContractAcon un valor inicial usando un transparent proxy. - Actualizar a
ContractB. - Por último, invocar
increaseValuepara modificar el estado.
Aquí está el código para la prueba. Por favor, lee los comentarios en el código a continuación para entender el flujo de trabajo:
function testTransparent() public {
// Deploy a transparent proxy with ContractA as the implementation and initialize it with 10
address proxy = Upgrades.deployTransparentProxy(
"ContractA.sol",
msg.sender,
abi.encodeCall(ContractA.initialize, (10))
);
// Get the instance of the contract
ContractA instance = ContractA(proxy);
// Get the implementation address of the proxy
address implAddrV1 = Upgrades.getImplementationAddress(proxy);
// Get the admin address of the proxy
address adminAddr = Upgrades.getAdminAddress(proxy);
// Ensure the admin address is valid
assertFalse(adminAddr == address(0));
// Log the initial value
console.log("----------------------------------");
console.log("Value before upgrade --> ", instance.value());
console.log("----------------------------------");
// Verify initial value is as expected
assertEq(instance.value(), 10);
// Upgrade the proxy to ContractB
Upgrades.upgradeProxy(proxy, "ContractB.sol", "", msg.sender);
// Get the new implementation address after upgrade
address implAddrV2 = Upgrades.getImplementationAddress(proxy);
// Verify admin address remains unchanged
assertEq(Upgrades.getAdminAddress(proxy), adminAddr);
// Verify implementation address has changed
assertFalse(implAddrV1 == implAddrV2);
// Invoke the increaseValue function separately
ContractB(address(instance)).increaseValue();
// Log and verify the updated value
console.log("----------------------------------");
console.log("Value after upgrade --> ", instance.value());
console.log("----------------------------------");
assertEq(instance.value(), 20);
}
A modo de resumen, la herramienta está haciendo lo siguiente:
- Desplegar
ContractAa través de un transparent proxy usandoUpgrades.deployTransparentProxy("ContractA.sol", msg.sender, abi.encodeCall(ContractA.initialize, (10)));einitializeContractAcon un valor específico. - Actualizar el proxy para usar
ContractBusandoUpgrades.upgradeProxy(proxy, "ContractB.sol", "", msg.sender); - Verificar el valor actualizado y la consistencia de la dirección de administración después de la actualización.
Ejecutando la Prueba
Para ejecutar las pruebas, introduce el siguiente comando en tu terminal:
forge clean && forge test -vvv --ffi
Verás una salida similar a la siguiente:
pari@MacBook-Air rareskills-foundry-upgrades % forge clean && forge test --mt testTransparent -vvv
[⠢] Compiling...
[⠊] Compiling 54 files with 0.8.24
[⠆] Solc 0.8.24 finished in 4.94s
Compiler run successful!
Ran 1 test for test/Upgrades.t.sol:UpgradesTest
[PASS] testTransparent() (gas: 1355057)
Logs:
----------------------------------
Value before upgrade --> 10
----------------------------------
----------------------------------
Value after upgrade --> 20
----------------------------------
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.43s (1.43s CPU time)
Ran 1 test suite in 5.47s (1.43s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Es posible que necesites ejecutar forge cache clean y forge clean antes de ejecutar la prueba por segunda vez.
Un ejemplo con un Beacon Proxy
Ahora demostramos cómo usar este plugin con el patrón Beacon Proxy. Usaremos los mismos contratos de implementación ContractA y ContractB del ejemplo anterior.
Resumen de la Prueba
- Desplegar
ContractAcomo la implementación inicial para un Beacon. - Crear dos beacon proxies, cada uno inicializado con diferentes valores. Recuerda, en el patrón beacon proxy, múltiples proxies apuntan a una única implementación, pero tienen su propio estado separado.
- Validar la nueva implementación (
ContractB) frente a la implementación original (ContractA) usando la funciónvalidateUpgrade. - Actualizar la implementación del beacon a
ContractB, actualizando ambos proxies simultáneamente. - Probar las nuevas funcionalidades en ambos proxies para asegurar que la actualización aplica los cambios como se espera.
Añade esta función al archivo Upgrades.t.sol para llevar a cabo la prueba. Los comentarios explican el flujo de trabajo:
import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
import {Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
function testBeacon() public {
// Deploy a beacon with ContractA as the initial implementation
address beacon = Upgrades.deployBeacon("ContractA.sol", msg.sender);
// Get the initial implementation address of the beacon
address implAddrV1 = IBeacon(beacon).implementation();
// Deploy the first beacon proxy and initialize it
address proxy1 = Upgrades.deployBeaconProxy(beacon, abi.encodeCall(ContractA.initialize, 15));
ContractA instance1 = ContractA(proxy1);
// Deploy the second beacon proxy and initialize it
address proxy2 = Upgrades.deployBeaconProxy(beacon, abi.encodeCall(ContractA.initialize, 20));
ContractA instance2 = ContractA(proxy2);
// Check if both proxies point to the same beacon
assertEq(Upgrades.getBeaconAddress(proxy1), beacon);
assertEq(Upgrades.getBeaconAddress(proxy2), beacon);
console.log("----------------------------------");
console.log("Value before upgrade in Proxy 1 --> ", instance1.value());
console.log("Value before upgrade in Proxy 2 --> ", instance2.value());
console.log("----------------------------------");
// Validate the new implementation before upgrading
Options memory opts;
opts.referenceContract = "ContractA.sol";
Upgrades.validateUpgrade("ContractB.sol", opts);
// Upgrade the beacon to use ContractB
Upgrades.upgradeBeacon(beacon, "ContractB.sol", msg.sender);
// Get the new implementation address of the beacon after upgrade
address implAddrV2 = IBeacon(beacon).implementation();
// Activate the increaseValue function in both proxies
ContractB(address(instance1)).increaseValue();
ContractB(address(instance2)).increaseValue();
console.log("----------------------------------");
console.log("Value after upgrade in Proxy 1 --> ", instance1.value());
console.log("Value after upgrade in Proxy 2 --> ", instance2.value());
console.log("----------------------------------");
// Check if the values have been correctly increased
assertEq(instance1.value(), 25);
assertEq(instance2.value(), 30);
// Check if the implementation address has changed
assertFalse(implAddrV1 == implAddrV2);
}
Funcionalidad soportada del plugin Upgrades
El plugin soporta más funcionalidad de la que usamos en los dos ejemplos anteriores. A continuación ofrecemos una visión general de las otras funciones en Upgrades:
Funciones de Despliegue Inicial:
Estas funciones se utilizan principalmente para el despliegue de las versiones iniciales de los contratos y para configurar sus estructuras de proxy:
deployUUPSProxy(*string contractName, bytes initializerData, struct Options opts*): Despliega un proxy UUPS utilizando el contrato dado como implementación.deployTransparentProxy(*string contractName, address initialOwner, bytes initializerData, struct Options opts*): Despliega un transparent proxy utilizando el contrato dado como implementación.deployBeaconProxy(*address beacon, bytes data, struct Options opts*): Despliega un beacon proxy utilizando el beacon dado y los datos de llamada.deployBeacon(*string contractName, address initialOwner, struct Options opts*): Despliega un beacon actualizable utilizando el contrato dado como implementación.
Funciones de Validación e Implementación:
Estas funciones se utilizan para garantizar la compatibilidad y seguridad de las implementaciones durante las actualizaciones:
validateImplementation(*string contractName, struct Options opts*): Valida un contrato de implementación, pero no lo despliega.deployImplementation(*string contractName, struct Options opts*): Valida y despliega un contrato de implementación, y devuelve su dirección.validateUpgrade(*string contractName, struct Options opts*): Valida un nuevo contrato de implementación en comparación con un reference contract, pero no lo despliega.prepareUpgrade(*string contractName, struct Options opts*): Valida un nuevo contrato de implementación en comparación con un reference contract, despliega el nuevo contrato de implementación y devuelve su dirección.
Funciones de Actualización:
Estas funciones se utilizan para gestionar e implementar actualizaciones en contratos ya desplegados:
upgradeProxy(*address proxy, string contractName, bytes data, struct Options opts*): Actualiza un proxy a un nuevo contrato de implementación. Esta función llama avalidateUpgrade()bajo el capó, por lo que fallará si la validación no tiene éxito. Si el usuario desea omitir algunas de las validaciones, esto se puede configurar en el argumento opts.upgradeBeacon(*address beacon, string contractName, struct Options opts*): Actualiza un beacon a un nuevo contrato de implementación.
Otras Funciones:
getAdminAddress(*address proxy*): Obtiene la dirección de administración de un transparent proxy desde su storage slot de administración ERC1967.
Desplegando y Verificando en la Sepolia Testnet
Esta sección te guía a través del despliegue de ContractA, su actualización a ContractB utilizando un Transparent Proxy y la verificación de los contratos en el Sepolia Explorer.
Paso 1: Añadir Variables de Entorno
Primero, configura los ajustes necesarios para el despliegue en la testnet:
- Crea un archivo
.envpara almacenar datos sensibles. - Incluye el archivo
.enven tu.gitignorepara evitar la exposición de claves privadas u otra información sensible en plataformas de control de versiones como Github. - Completa el archivo
.envcon tus datos específicos:
SEPOLIA_RPC_URL=your-sepolia-endpoint
PRIVATE_KEY=your-private-key
ETHERSCAN_API_KEY=your-etherscan-api
SENDER=your-EOA-address
Detalles de la configuración:
SEPOLIA_RPC_URL: La URL de RPC para conectarse a la red Sepolia.PRIVATE_KEY: Tu clave privada, utilizada para firmar transacciones. Asegúrate de que esta wallet tenga Ether para pagar el coste de gas del despliegue.ETHERSCAN_API_KEY: Tu clave API de Etherscan, necesaria para la verificación del contrato.SENDER: La dirección de Ethereum desde la cual se iniciarán las transacciones de despliegue.
Paso 2: Configurar el archivo foundry.toml
Actualiza el archivo foundry.toml para incluir la configuración para la Sepolia Testnet y la verificación en Etherscan:
[etherscan]
sepolia = { key = "${ETHERSCAN_API_KEY}" }
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
Esta configuración logra dos tareas:
- Configuración de Etherscan: Vincula la API Key de Etherscan con la red Sepolia para la verificación del contrato posterior al despliegue.
- Endpoints RPC: Especifica la URL de RPC para la red Sepolia.
Paso 3: Scripting de Despliegue y Actualización
Navega a la carpeta script y abre el archivo Upgrades.s.sol para insertar el código necesario para las tareas de despliegue y actualización.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Script} from "forge-std/Script.sol";
import {ContractA} from '../src/ContractA.sol';
import {ContractB} from '../src/ContractB.sol';
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
contract UpgradesScript is Script {
function setUp() public {}
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// Deploy `ContractA` as a transparent proxy using the Upgrades Plugin
address transparentProxy = Upgrades.deployTransparentProxy(
"ContractA.sol",
msg.sender,
abi.encodeCall(ContractA.initialize, 10)
);
}
}
Bajo el capó, esto desplegará el Proxy, el ProxyAdmin y ContractA por nosotros.
La función deployTransparentProxy toma 3 parámetros:
contractName(string): Nombre del contrato para la implementación, como “MyContract.sol”, “MyContract.sol:MyContract”, o una ruta relativa de artefacto.initialOwner(address): Dirección establecida como propietaria del contrato ProxyAdmin, que se despliega automáticamente con el proxy.initializerData(bytes): Call data codificada para la función del initializer que se ejecutará durante la creación del proxy; dejar vacío si no se necesita inicialización.
La función devuelve la dirección del proxy desplegado.
Ejecutando el Script
Utiliza el siguiente comando para desplegar y verificar la actualización en la red Sepolia.
forge clean && forge script script/UpgradesScript.s.sol --rpc-url sepolia --private-key $PRIVATE_KEY --broadcast --verify --sender $SENDER
Este comando limpia builds anteriores, ejecuta el script y verifica los contratos en el Sepolia Explorer.
Tras la ejecución, deberías esperar una salida que indique el despliegue y la verificación exitosa de los contratos:
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.00356888630073128 ETH (862220 gas * avg 4.139182924 gwei)
##
Start verification for (3) contracts
Start verifying contract `0x427186c574B5fA11cB9d796871861EF87c74Ad37` deployed on sepolia
Submitting verification for [src/ContractA.sol:ContractA] 0x427186c574B5fA11cB9d796871861EF87c74Ad37.
Submitted contract for verification:
Response: `OK`
GUID: `hf2dplvhjmjpj3nixun3kupamtsgmbacngfeygpm9p34mbzb3g`
URL: https://sepolia.etherscan.io/address/0x427186c574b5fa11cb9d796871861ef87c74ad37
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
Start verifying contract `0xbA58580452Bc758C9a044584F6CEa468e5569a13` deployed on sepolia
Submitting verification for [lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy] 0xbA58580452Bc758C9a044584F6CEa468e5569a13.
Submitted contract for verification:
Response: `OK`
GUID: `biaqcdgrhwjfu8d7b3n9jg6btmsjpktgd61rdx42n7ptttuvre`
URL: https://sepolia.etherscan.io/address/0xba58580452bc758c9a044584f6cea468e5569a13
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
Start verifying contract `0x8bB6A51C24ad9b6bA276c2bf0380e5E8Ce31E866` deployed on sepolia
Submitting verification for [lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol:ProxyAdmin] 0x8bB6A51C24ad9b6bA276c2bf0380e5E8Ce31E866.
Submitted contract for verification:
Response: `OK`
GUID: `rfnvjnxa8j2rqxxgwtnx9if17rdlyky2nk9eixrmzjbp5pn4gy`
URL: https://sepolia.etherscan.io/address/0x8bb6a51c24ad9b6ba276c2bf0380e5e8ce31e866
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `NOTOK`
Details: `Already Verified`
Contract source code already verified
All (3) contracts were verified!
Transactions saved to: /Users/nest/rareskills/rareskills-foundry-upgrades/broadcast/UpgradesScript.s.sol/11155111/run-latest.json
Sensitive values saved to: /Users/nest/rareskills/rareskills-foundry-upgrades/cache/UpgradesScript.s.sol/11155111/run-latest.json
El comando deployTransparentProxy ejecutado durante este script desplegó ContractA y un Transparent Upgradeable Proxy junto con un Proxy Admin Contract.
Estas transacciones son visibles en el Sepolia Explorer:
- Despliegue de ContractA: https://sepolia.etherscan.io/tx/0x9dba6d8293629cb9557500d8645659de3127e75abcfa705b06e6cf379092a10e
- Transparent upgradeable Proxy: https://sepolia.etherscan.io/tx/0x9dba6d8293629cb9557500d8645659de3127e75abcfa705b06e6cf379092a10e
- Proxy Admin Contract: https://sepolia.etherscan.io/address/0xba58580452bc758c9a044584f6cea468e5569a13#code#F6#L1
Actualizando el contrato
Antes de actualizar a ContractB, validaremos la nueva implementación frente al reference contract, ContractA, utilizando la función validateUpgrade del plugin.
Una vez confirmada la validación, procederemos con la actualización utilizando la función upgradeProxy. Por favor, lee los comentarios en el código a continuación:
import {Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// Specifying the address of the existing transparent proxy
address transparentProxy = 'your-transparent-proxy-address';
// Setting options for validating the upgrade
Options memory opts;
opts.referenceContract = "ContractA.sol";
// Validating the compatibility of the upgrade
Upgrades.validateUpgrade("ContractB.sol", opts);
// Upgrading to ContractB and attempting to increase the value
Upgrades.upgradeProxy(transparentProxy, "ContractB.sol", abi.encodeCall(ContractB.increaseValue, ()));
}
Si la nueva implementación del contrato no es compatible con el reference contract, lanzará el siguiente error:
revert: Upgrade safety validation failed:
De nuevo. Utiliza el mismo script para desplegar y actualizar ContractB y verifícalo en el Explorer.

La transacción de actualización se puede ver aquí.
Cómo ayuda el OpenZeppelin Foundry Plugin contra problemas de actualización
Esta sección enumera los posibles problemas de seguridad que son específicos de las actualizaciones de proxy y cómo la herramienta protege contra ellos.
1. Variables Inmutables
En Solidity, la palabra clave immutable permite establecer variables de forma permanente durante la creación del contrato integrando sus valores directamente en el bytecode. Esto obliga a los futuros despliegues a usar exactamente los mismos argumentos para el constructor, de modo que los futuros despliegues tengan el mismo bytecode. Como esto es difícil de rastrear, el plugin desaconseja el uso de variables inmutables.
Para permitir variables inmutables en contratos actualizables, los desarrolladores pueden omitir esta verificación de seguridad usando la anotación @custom:oz-upgrades-unsafe-allow, como se muestra a continuación.
contract ImmutableVar {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
uint256 public immutable a;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(uint256 _a) {
a = _a;
}
}
Esto permite el uso de variables inmutables, pero exige una gestión cuidadosa para mantener la consistencia del despliegue.
2. Storage Layout
Mantener un Storage Layout consistente es esencial al actualizar smart contracts. Cualquier cambio en el orden o tipo de las variables de estado puede provocar corrupción de datos.
Esto significa que si tienes un contrato inicial que se ve así:
contract MyContract {
uint256 private x;
string private y;
}
Entonces no puedes cambiar el tipo de una variable:
contract MyContract {
string private x; // uint256 became a string
string private y;
}
O cambiar el orden en el que se declaran:
contract MyContract {
string private y; // x and y switched places
uint256 private x;
}
Si necesitas introducir una nueva variable, asegúrate de hacerlo siempre al final:
contract MyContract {
uint256 private x;
string private y;
bytes private z;
}
Usando nuestros ejemplos de ContractA y ContractB del principio de este artículo, supongamos que hubiéramos insertado incorrectamente una variable de storage en ContractB de la siguiente manera:

Obtendríamos el siguiente error del plugin de OpenZeppelin (estamos usando la misma prueba que en el primer ejemplo con el Transparent Upgradeable Proxy):

3. Validando que __gap se utiliza correctamente
Como se discutió en nuestro artículo sobre Storage Namespaces, usar la variable __gap es una estrategia para evitar que los contratos padre desplacen las variables de storage de los contratos hijo cuando se inserta una nueva variable de storage. Para usar la variable __gap correctamente, el tamaño del gap debe reducirse cuando se inserta una nueva variable. En el siguiente ejemplo, el desarrollador insertó una nueva variable de storage y olvidó reducir el tamaño del __gap:

y la herramienta detecta esto:

La herramienta de OpenZeppelin no obliga el uso de __gap. En su lugar, si una variable gap está presente, la herramienta verifica que la actualización respete cómo se pretende usar la variable __gap, es decir, manteniendo la alineación de las variables de storage.
Una alternativa más robusta a la estrategia de __gap es el uso de Named Storage Layouts, como se discutió en nuestro tutorial sobre Storage Namespaces, y proporcionamos un ejemplo de ello en la siguiente sección.
4. Validando que ERC-7201 se sigue correctamente
Usando nuestro ejemplo actual de ContractA y ContractB del principio, modifiquemos el contrato para usar ERC-7201.
Ten en cuenta los siguientes cambios:
- En lugar de ser una variable pública,
valuees ahora una función pública. - El storage subyacente para
valuese ha movido dentro del structMyStorage. - Las funciones setter ahora deben interactuar con el struct
MyStorage(nota que el struct se usa para agrupar las variables, nunca se inicializa realmente).
Aquí está ContractA ajustado para seguir ERC-7201:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract ContractA is Initializable {
/// @custom:storage-location erc7201:ContractA.storage.MyStorage
struct MyStorage {
uint256 value;
}
// keccak256(abi.encode(uint256(keccak256("ContractA.storage.MyStorage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant MyStorageLocation = 0xd255ccbed1486709ef10c220c9b584c9ad5cacd00961bdfc2156c2c7f2e4fc00;
function _getMyStorage() private pure returns (MyStorage storage $) {
assembly {
$.slot := MyStorageLocation
}
}
function value() public view returns (uint256) {
MyStorage storage $ = _getMyStorage();
return $.value;
}
function initialize(uint256 _setValue) public initializer {
MyStorage storage $ = _getMyStorage();
$.value = _setValue;
}
}
y ContractB:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
/// @custom:oz-upgrades-from ContractA
contract ContractB is Initializable {
/// @custom:storage-location erc7201:ContractA.storage.MyStorage
struct MyStorage {
uint256 value;
}
// keccak256(abi.encode(uint256(keccak256("ContractA.storage.MyStorage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant MyStorageLocation = 0xd255ccbed1486709ef10c220c9b584c9ad5cacd00961bdfc2156c2c7f2e4fc00;
function _getMyStorage() private pure returns (MyStorage storage $) {
assembly {
$.slot := MyStorageLocation
}
}
function value() public view returns (uint256) {
MyStorage storage $ = _getMyStorage();
return $.value;
}
function initialize(uint256 _setValue) public initializer {
MyStorage storage $ = _getMyStorage();
$.value = _setValue;
}
function increaseValue() public {
MyStorage storage $ = _getMyStorage();
$.value += 10;
}
}
Digamos que estropeamos el struct MyStorage en ContractB insertando una variable en el struct en algún lugar que no sea el final:
/// @custom:storage-location erc7201:ContractA.storage.MyStorage
struct MyStorage {
uint256 badInsert; // this should not be here, it should be at the end
uint256 value;
}
Con ese cambio, la herramienta presentará el siguiente error:

Otro problema que la herramienta evitará es renombrar el slot del namespace. Supongamos que cambiamos la anotación encima del struct de la siguiente manera:
/// @custom:storage-location erc7201:ContractA.storage.MyStorage
/// @custom:storage-location erc7201:ContractA.storage.MyStorage2
Ahora obtenemos el siguiente error:

Se supone que los Namespaces no deben eliminarse entre actualizaciones.
El fallo al llamar a los initializers padres no puede detectarse automáticamente
En el siguiente ejemplo, tanto ContractA como ContractB heredan de Base. Sin embargo, ninguno de ellos llama al initializer del padre, lo cual es un bug. Como entender si una función sirve como initializer requiere una interpretación semántica, la herramienta no puede detectar este problema automáticamente. El desarrollador o auditor debe verificar manualmente que todos los initializers se llaman correctamente (generalmente este tipo de problema puede detectarse fácilmente con pruebas unitarias si las variables se inicializan a un valor distinto de cero).
El siguiente código no provocará ningún problema con la herramienta, aunque no se llame a la función __Base_init.

Conclusión
El artículo detalló cómo usar el plugin de actualizaciones de OpenZeppelin en Foundry, cómo se puede usar el plugin para pruebas unitarias y scripts de Foundry, y cómo simplifica el proceso de múltiples pasos del despliegue y las actualizaciones. Mostramos cómo configurar el entorno para el despliegue y dimos algunos ejemplos de cómo la herramienta puede detectar automáticamente varios errores.
Autoría y Agradecimientos
Este artículo fue escrito por Pari Tomar en colaboración con RareSkills.
Nos gustaría agradecer a Eric Lau de OpenZeppelin por sus útiles comentarios sobre un borrador de este artículo.
Publicado originalmente el 28 de agosto de 2024