Let us dive into the UUPS (Universal Upgradeable Proxy Standard) proxy pattern with a practical example. This will allow us to create upgradeable smart contracts while minimizing gas costs and maintaining a clear separation between your contract logic and storage.
What is UUPS Proxy?
UUPS is an upgradeable proxy pattern that leverages a single implementation contract and a proxy to delegate calls to the implementation. The proxy holds the state, while the implementation contract contains the logic. UUPS allows the implementation to be upgraded through a function call on the proxy itself, reducing gas costs compared to other patterns like Transparent Proxies.
Key Components
Proxy Contract: This is the contract that users interact with. It holds the state and delegates calls to the implementation.
Implementation Contract: This contains the actual logic of the contract. It can be upgraded by deploying a new version and pointing the proxy to the new implementation.
Admin Functionality: The proxy needs to have a way to authorize upgrades.
UUPS Proxy Implementation
Step 1: Set Up Your Project
First, make sure you have a working environment with Node.js, Hardhat, and OpenZeppelin:
Create a file named MyContract.sol in the contracts folder:
Explanation:
The contract uses UUPSUpgradeable for the upgrade mechanism.
The initialize function sets the initial state.
The _authorizeUpgrade function restricts who can upgrade the contract to the owner.
Step 4: Create the Proxy Contract
With UUPS, the proxy is automatically handled by OpenZeppelin, so you don't need to create a separate proxy contract manually. Instead, you will deploy the implementation contract and interact with it via the proxy mechanism.
Step 5: Write the Deployment Script
In the scripts folder, create a file named deploy.js:
Step 6: Deploy the Contract
Run the deployment script:
Step 7: Upgrade the Contract
Let’s say you want to add new functionality in the future. Create a new version of your contract. Create MyContractV2.sol:
Explanation:
This version extends MyContract and adds a new function incrementValue.
Step 8: Deploy the New Implementation
In the scripts folder, create a new script named upgrade.js:
Step 9: Upgrade Your Contract
Run the upgrade script:
Step 10: Interact with the Upgraded Contract
You can now call the new incrementValue function on the upgraded contract. Create a script named interact.js:
Run the interaction script:
You’ve successfully implemented a UUPS proxy pattern for upgradeable contracts! Here’s a recap of what we did:
Created an implementation contract with upgradeable functionality.
Deployed the contract to the network.
Added a new version of the contract with additional features.
Upgraded the contract to the new implementation.
Interacted with the upgraded contract to demonstrate its new capabilities.
This UUPS pattern provides a flexible way to maintain and upgrade your smart contracts while optimizing for gas costs, making it an ideal choice for modern Ethereum development.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is UUPSUpgradeable, Ownable {
uint256 public value;
function initialize(uint256 initialValue) public initializer {
value = initialValue;
}
function setValue(uint256 newValue) public onlyOwner {
value = newValue;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
npx hardhat run scripts/deploy.js --network <your_network>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./MyContract.sol";
contract MyContractV2 is MyContract {
function incrementValue() public {
value += 1;
}
}