UUPS Proxy Example
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:
mkdir UUPSExample
cd UUPSExample
npm init -y
npm install --save-dev hardhat
npm install @openzeppelin/contracts
Step 2: Create the Implementation Contract
npx hardhat
Create a file named MyContract.sol
in the contracts
folder:
// 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 {}
}
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
:
async function main() {
const MyContract = await ethers.getContractFactory("MyContract");
const myContract = await MyContract.deploy();
await myContract.deployed();
console.log("MyContract deployed to:", myContract.address);
// Initialize the contract
const tx = await myContract.initialize(42);
await tx.wait();
console.log("Contract initialized with value:", 42);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Step 6: Deploy the Contract
Run the deployment script:
npx hardhat run scripts/deploy.js --network <your_network>
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
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./MyContract.sol";
contract MyContractV2 is MyContract {
function incrementValue() public {
value += 1;
}
}
Explanation:
This version extends
MyContract
and adds a new functionincrementValue
.
Step 8: Deploy the New Implementation
In the scripts
folder, create a new script named upgrade.js
:
async function main() {
const myContractAddress = "YOUR_CONTRACT_ADDRESS"; // replace with your deployed contract address
const MyContractV2 = await ethers.getContractFactory("MyContractV2");
const myContractV2 = await MyContractV2.deploy();
await myContractV2.deployed();
console.log("MyContractV2 deployed to:", myContractV2.address);
const myContract = await ethers.getContractAt("MyContract", myContractAddress);
const tx = await myContract.upgradeTo(myContractV2.address);
await tx.wait();
console.log("Upgraded to MyContractV2");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Step 9: Upgrade Your Contract
Run the upgrade script:
npx hardhat run scripts/upgrade.js --network <your_network>
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
:
async function main() {
const myContractAddress = "YOUR_CONTRACT_ADDRESS"; // replace with your deployed contract address
const myContract = await ethers.getContractAt("MyContract", myContractAddress);
// Call incrementValue
const tx = await myContract.incrementValue();
await tx.wait();
const newValue = await myContract.value();
console.log("New value after increment:", newValue.toString());
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Run the interaction script:
npx hardhat run scripts/interact.js --network sepolia
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.
Last updated