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

  1. Proxy Contract: This is the contract that users interact with. It holds the state and delegates calls to the implementation.

  2. 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.

  3. 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 function incrementValue.

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:

  1. Created an implementation contract with upgradeable functionality.

  2. Deployed the contract to the network.

  3. Added a new version of the contract with additional features.

  4. Upgraded the contract to the new implementation.

  5. 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