March 4, 2023

Using UUPS proxy in foundry

UUPS proxy is said to be a novel way or doing EVM proxies. It is more reliable and have a nice feature of being able to become immutable when the time comes.

The problem is though, that it comes usually bundled with hardhat-upgrades module by OpenZeppelin which hides the process. In this article I'm going to show what is the minimal amount of steps needed to deploy UUPS proxy from foundry.

First thing to do is to add a module (if you haven't already) for openzeppelin contracts.

$ forge i https://github.com/OpenZeppelin/openzeppelin-contracts.git

I will be working with my real contract, which overall looks like:

contract ENGCReferalProgram is Initializable,
                               UUPSUpgradeable,
                               OwnableUpgradeable {
    // ...
    function _authorizeUpgrade(
        address newImplementation
    )internal virtual override onlyOwner {}
}

And I'm going to write a test, which deploys this proxy in its setUp method. It is actually pretty easy.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";

import {ENGCReferalProgram} from "../src/ReferalProgram.sol";
import {IENGC} from "../src/IENGC.sol";

import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";


contract ReferalProgramTest is Test {
    ENGCReferalProgram public rp;

    function setUp() public {
        ENGCReferalProgram impl = new ENGCReferalProgram();
        ERC1967Proxy proxy = new ERC1967Proxy(address(impl), "");
        rp = ENGCReferalProgram(address(proxy));
        rp.initialize();
        rp.setNftContractAddress(IENGC(address(this)));
    }

    function testChangeImpl() public {
        addNodesToRoot();
        ENGCReferalProgram impl2 = new ENGCReferalProgram();
        rp.upgradeTo(address(impl2));
        assertEq(rp.getTokenLevel(0xffffff), 3);
    }

    function testFailChangeImpl() public {
        ENGCReferalProgram impl2 = new ENGCReferalProgram();
        vm.prank(0x094BAfcb40a0226d210E51E9ee4dA0FbFe99D326);
        rp.upgradeTo(address(impl2));
    }

If you want to get more into details on how to pass parameters to initializers etc. you can find more detail here.