Universal Upgrade Proxy & ProxyFactory — A modern walkthrough

cryptogenics
10 min readMay 1, 2021

There are many patterns of proxies detailed out at The State of Smart Contract Upgrades and Proxy Patterns to name a few. One of the most common used patterns today is TransparentUpgradeableProxy, which along with ProxyAdmin is the recommended OpenZeppelin way. There is another proxy pattern called Universal Upgradeable Proxy Standard (UUPS) as specified in EIP 1822. This is implemented as UUPSUpgradeable in v0.4.1-rc.0 of openzeppelin/contracts-upgradeable and was released late April 2021.

Proxy Patterns, Upgradeability and Governance go hand-in-hand. While there are many patterns available and implemented, the important thing is that since OpenZeppelin stopped supporting their ProxyFactory many developers still use that code (2–3 years old), and this most often it leads them to copy and put those old contracts in their codebase. This is a potential security issue apart from a hygiene issue as well.

I had to write a few upgradeable contracts, and decided (a) to use the UUPS pattern, because of obvious reasons of gas savings, and (b) that it’s high time to take a rewrite of `ProxyFactory` in a more modern scenario. This makes it more aligned with the OpenZeppelin libraries of today, and hence maintainable, and also security patched (maybe a little future).

So in this post, I will give a detail example of upgrading via all the possible methods today, and compare each of them and finally implement. The source code is available on my github repo. The code has not been reviwed (leave aside audited) for any security issue, so it’s just a guideline and not a production ready implementation.

You’ll need v4.1.0-rc.0 for all openzeppelin libs

  "@openzeppelin/contracts": "^4.1.0-rc.0",
"@openzeppelin/contracts-upgradeable": "^4.1.0-rc.0",

We start with a famous Box contract, which in its vanilla form is non upgradeable. (Hint: Because of the constructor)

contract Box {
uint256 internal length;
uint256 internal width;
uint256 internal height;

constructor (uint256 l, uint256 w, uint256 h) {
length = l;
width = w;
height = h;
}

function volume() public returns (uint256) {
return length * width * height;
}

}

In order to make it UUPS Upgradeable, we simply (a) Inherit from UUPSUpgradeable contract (b) replace the constructor with an initialize and, (c) Implement the _authorizeUpgrade function which is required by UUPSUpgradeable.

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract BoxV1 is UUPSUpgradeable {

address internal owner;
uint256 internal length;
uint256 internal width;
uint256 internal height;

function initialize(uint256 l, uint256 w, uint256 h) public initializer {
owner = msg.sender;
length = l;
width = w;
height = h;
}

function volume() public returns (uint256) {
return length * width * height;
}

function _authorizeUpgrade(address newImplementation) internal override virtual {
require(msg.sender == owner, "Unauthorized Upgrade");
}

}

Now that we have made this upgradeable, we also need to deploy a Proxy for this. It’s the proxy that “users” will interact with. There are many options depending on the use cases. Its best to do a quick sum up of them

  1. upgrades-plugin — This is the simplest so far. It is a convenient High Level JavaScript wrapper that uses ProxyAdmin and TransparentUpgradeableProxy to securely deploy and upgrade Proxy.
  2. Low Level Proxy Implementations. There are four broad kind of Proxies (a) ERC1967Proxy, (b)TransparentUpgradeableProxy, (c) BeaconProxy and (d) Minimal Proxy (also called Clones).

We’ll look at the upgrades-plugin first

1. Using Upgrades-Plugin

Using this is really simple. All you need to do is to add the requirement of `@openzeppelin/truffle-upgrades` which can be done so that your package.json looks like below

"dependencies": {
"@openzeppelin/contracts": "^4.1.0-rc.0",
"@openzeppelin/contracts-upgradeable": "^4.1.0-rc.0",
"@openzeppelin/truffle-upgrades": "^1.6.0",
"@truffle/compile-solidity": "^5.2.6",
},

Then you can simply use the `deployProxy` function to deploy a proxy to BoxV1

const { deployProxy, upgradeProxy } = require('@openzeppelin/truffle-upgrades');
const BoxV1 = artifacts.require('./BoxV1.sol');
const assert = require('assert');
const boxv1 = await deployProxy(BoxV1, [1, 2, 3], { kind: 'uups' });
const vol = await boxv1.volume.call();
assert.strictEqual(vol1.toNumber(), 6);

Now let’s write another version of the Box contract called BoxV2 that multiplies the volume by 2.

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract BoxV2 is UUPSUpgradeable {

address internal owner;
uint256 internal length;
uint256 internal width;
uint256 internal height;

function initialize(uint256 l, uint256 w, uint256 h) public initializer {
owner = msg.sender;
length = l;
width = w;
height = h;
}

function volume() public returns (uint256) {
return 2 * length * width * height;
}

function _authorizeUpgrade(address newImplementation) internal override virtual {
require(msg.sender == owner, "Unauthorized Upgrade");
}

}

In order to upgrade the deployed boxv1 to the new contract, we simply need to call the upgradeProxy call

const BoxV2 = artifacts.require('./BoxV2.sol');
await upgradeProxy(boxv1.address, BoxV2);
const vol2 = await boxv1.volume.call();
assert.strictEqual(vol2.toNumber(), 12);

See? It’s really easy. In this example, we did all this in same lines-of-code file, namely the test/BoxV1Upgrade.js file, so it’s easy. In a more real world, if you use this method, you will need to store the addresses of deployed Proxies externally. Typically people store them in a JSON file.

const fs = require('fs');
let addresses = [];
boxes = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
for (let _b of boxes) {
let _boxv1 = await deployProxy(BoxV1, _b, { kind: 'uups' });
addresses.push(_boxv1.address);
}
let data = JSON.stringify(addresses);
fs.writeFileSync('addresses_deployed.json', data);

And then when you have the upgraded contract ready and audited, you can simply load the deployed addresses from this JSON file and upgrade them.

let rawData = fs.readFileSync('addresses_deployed.json');
let _addresses = JSON.parse(rawData);
for (let _addr of _addresses) {
await upgradeProxy(_addr, BoxV2);
}

Few things are important to note:

  1. The deployed addresses are stored by default “off-chain”. This has its downside, so if you loose this JSON file of addresses, the proxy addresses are lost. In order to mitigate this, one could create another Contract that will serve as registry contract. The deployment code could call the registry contract and update it with the addresses of the proxies that are deployed.
  2. If you’re building a system where your users deploy Contracts (ex: a Fund Management Protocol, where users can launch a Fund), there has to be a registry contract that serves as storage. JSON storage is simply not an option there.
  3. The UUPS Proxy can be upgraded by the owner of the contract and not by a dedicated ProxyAdmin. This is a key difference because earlier the upgrades were centralised.

2. Low level implementations

Another way to think about registry contracts is in terms of a contract factory. Contract Factory is a contract that deploys or upgrades another contract. So, we will need to do two things

  1. Move the `deployProxy` code somehow from JavaScript to Solidity
  2. Create a BoxFactory contract that will use the above port of deployProxy to deploy contracts.

In good ol’ days, OpenZeppelin supported their SDK which had a ProxyFactory contract. Since OpenZeppelin has stopped supporting this SDK (more details here), and the code is still there, people are using that code in their repos and creating factories. There are total of 949 usages of ProxyFactory.sol across Github while writing of this article. This includes dHedge’s DHedgeFactory is a ProxyFactory, endaoment’s FundFactory is a ProxyFactory, Consensys uses ProxyFactory for many of its contracts, and its dependent Contracts like tcr as well. It’s a very apt way to let users deploy contract and keep the details on chain.

2 a. ProxyFactory using Mininal Proxies (Clones)

So let’s look at how we can implement ProxyFactory in our Box example, and use it to create a BoxFactory that will deploy the Box contract. We will create a simple ProxyFactory (inspired from endaoment) that will use minimal clones to deploy proxies to contracts. The key difference is that while endaoment’s ProxyFactory uses the raw assembly, (it was written 9 months back), now there’s a library called ClonesUpgradeable which abstracts out this functionality, and we can use that to have a better ProxyFactory

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

import "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol";


contract ProxyFactory {

event ProxyCreated(address proxy);


function deployMinimal(address _logic, bytes memory _data)
public
returns (address)
{
address proxy = ClonesUpgradeable.clone(_logic);
emit ProxyCreated(address(proxy));
if(_data.length > 0)
{
(bool success,) = proxy.call(_data);
require(success);
}
return proxy;
}

}

Now we can create a BoxFactory that uses this deployMinimal to create proxies

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

import "@openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol";
import "./ProxyFactory.sol";

contract BoxFactory is ProxyFactory {

event BoxCreated(address proxy);
address[] public proxies;

address public _logic;

constructor (address logic) {
_logic = logic;
}
function payload(uint256 l, uint256 w, uint256 h) public pure returns (bytes memory) {
return abi.encodeWithSignature(
"initialize(uint256,uint256,uint256)", l, w, h
);
}
function createByDeployingMinimal(uint256 l, uint256 w, uint256 h) public returns (address){
address proxy = deployMinimal(_logic, payload(l, w, h));

emit BoxCreated(proxy);

proxies.push(proxy);

return proxy;
}


}

We can test this out by writing following in our test cases.

box = await BoxV1.new();
factory = await BoxFactory.new(box.address);
let tx = await factory.createByDeployingMinimal(
1,
2,
3
);
let proxyAddr = tx.logs[0].args['proxy'];
let _box = await BoxV1.at(proxyAddr);
let vol = await _box.volume.call();

assert.strictEqual(vol.toNumber(), 6);

The only issue with Minimal Proxies is that they are non-upgradeable.

2 b. ProxyFactory using TransparentUpgradeableProxy

Now let’s try to add a `TransparentUpgradeableProxy` to this. Most of the implementations of Github are a few years old. And their code generally doesn’t support having a Proxy with a constructor. That’s because in good ol’ days of Openzeppelin SDK, the Proxies were initialized separately. But these days the proxies have constructors. Hence, we need to edit some of the code and make it work for our scenario. We will need to add the following three functions to the existing ProxyFactory.sol contract.

function deployTransparent(address _logic, address _admin, bytes memory _data) public returns (address) {
bytes memory creationByteCode = getCreationBytecode(_logic, _admin, _data);
TransparentUpgradeableProxy proxy = _deployTransparentProxy(creationByteCode);
return address (proxy);
}

function getCreationBytecode(address _logic, address _admin, bytes memory _data)
public
pure
returns (bytes memory)
{
bytes memory bytecode = type(TransparentUpgradeableProxy).creationCode;
return abi.encodePacked(bytecode, abi.encode(_logic, _admin, _data));
}

function _deployTransparentProxy(bytes memory _creationByteCode) internal returns (TransparentUpgradeableProxy){
address payable addr;

assembly {
addr := create(0, add(_creationByteCode, 0x20), mload(_creationByteCode))
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}

return TransparentUpgradeableProxy(addr);
}

Once we do that, we can update the BoxFactory to use this function and also write a test case.

//contract BoxFactoryfunction createByTransparentProxy(uint256 l, uint256 w, uint256 h) public returns (address) {
address proxy = deployTransparent(_logic,
msg.sender,
payload(l, w, h));
emit BoxCreated(proxy);
proxies.push(proxy);
return proxy;
}

The updates test case goes like below

let tx = await factory.createByTransparentProxy(
1,
2,
3
);

let proxyAddr = tx.logs[0].args['proxy'];
let _box = await BoxV1.at(proxyAddr);
let vol = await _box.volume.call({from: accounts[1]});

assert.strictEqual(vol.toNumber(), 6);

Once this is done, and we trust in OpenZeppelin’s implementations so our contracts should be upgradeable. Lets try that out below

let tx = await factory.createByTransparentProxy(
1,
2,
3
);
let proxyAddr = tx.logs[0].args['proxy'];
let _box = await BoxV1.at(proxyAddr);
let boxV2 = await BoxV2.new();

await _box.upgradeTo(boxV2.address);

let vol = await _box.volume.call({from: accounts[1]});
assert.strictEqual(vol.toNumber(), 12);

That’s it. We are successfully able to upgrade our implementation contract.

Something’s not right. Based on what we did, does it mean that if the Factory deploys 100 Box contracts, then each of those contract needs to be updates individually? Yes that’s true unfortunately.

2c. ProxyFactory using BeaconProxy

This is where BeaconProxy Pattern comes to rescue. In order to add Beacon capability to our ProxyFactory, we will need to add the following.

import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";


contract ProxyFactory {
event BeaconCreated(address beacon);
UpgradeableBeacon private _beacon;
bool private _isBeaconSet;
function codeForBeaconProxy(address beacon, bytes memory data) public pure returns (bytes memory) {
bytes memory bytecode = type(BeaconProxy).creationCode;
return abi.encodePacked(bytecode, abi.encode(beacon, data));
}
function _deployBeaconProxy(bytes memory _creationByteCode) internal returns (BeaconProxy) {
address payable addr;
assembly {
addr := create(0, add(_creationByteCode, 0x20), mload(_creationByteCode))
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}
return BeaconProxy(addr);
}
function _deployBeacon(address _logic) internal {
if (!_isBeaconSet) {
_beacon = new UpgradeableBeacon(_logic);
_isBeaconSet = true;
emit BeaconCreated(address (_beacon));
}
}

function getBeacon() public returns (address) {
return address(_beacon);
}

function deployBeaconProxy(address _logic, bytes memory payload) public returns (address) {
if (!_isBeaconSet) {
_deployBeacon(_logic);
}
bytes memory code = codeForBeaconProxy(address (_beacon), payload);
BeaconProxy proxy = _deployBeaconProxy(code);
_beaconProxies[address (proxy)] = true;
return address (proxy);
}

function upgradeBeacon(address _logic) public {
_beacon.upgradeTo(_logic);
}
}

Once this is done, we can simply add another function in our BoxFactory which will call the deployBeaconProxy function.

contract BoxFactory {function createByBeaconProxy(uint256 l, uint256 w, uint256 h) public returns (address) {
address proxy = deployBeaconProxy(_logic, payload(l, w, h));
emit BoxCreated(proxy);
proxies.push(proxy);
return proxy;
}
}

And finally, we are good to test it like

boxes_dimensions = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
let boxes = [];
for (let dims of boxes_dimensions) {
let tx = await factory.createByBeaconProxy(dims[0], dims[1], dims[2]);
let proxy;
if (tx.logs.length === 1){
proxy = tx.logs[0].args['proxy'];
}
else {
proxy = tx.logs[1].args['proxy'];
}
boxes.push(proxy);
}

let beaconAddr = await factory.getBeacon.call();

let boxV2 = await BoxV2.new();
await factory.upgradeBeacon(boxV2.address);

let volumes_expected = [12, 240, 1008];
let volumes = []
for (let box of boxes) {
let vol = await (await BoxV2.at(box)).volume.call();
volumes.push(vol.toNumber());
}

assert.deepEqual(volumes, volumes_expected);

Thus we see that in just one upgrade, we were able to update all the proxies.

So finally we are able to create a ProxyFactory contract that does all the job it’s supposed to do, while using the modern libraries.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";


contract ProxyFactory {

event ProxyCreated(address proxy);
event BeaconCreated(address beacon);

mapping(address => bool) private _minimalProxies;
mapping(address => bool) private _transparenrProxies;
mapping(address => bool) private _beaconProxies;

UpgradeableBeacon private _beacon;
bool private _isBeaconSet;

function deployMinimalOld(address _logic, bytes memory _data) public returns (address proxy) {
// Adapted from https://github.com/optionality/clone-factory/blob/32782f82dfc5a00d103a7e61a17a5dedbd1e8e9d/contracts/CloneFactory.sol
bytes20 targetBytes = bytes20(_logic);
assembly {
let clone := mload(0x40)
mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(clone, 0x14), targetBytes)
mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
proxy := create(0, clone, 0x37)
}

emit ProxyCreated(address(proxy));

if(_data.length > 0) {
(bool success,) = proxy.call(_data);
require(success);
}
}

function deployMinimal(address _logic, bytes memory _data) public returns (address) {
address proxy = ClonesUpgradeable.clone(_logic);
emit ProxyCreated(address(proxy));
if(_data.length > 0) {
(bool success,) = proxy.call(_data);
require(success);
}
return proxy;
}

function deployTransparent(address _logic, address _admin, bytes memory _data) public returns (address) {
bytes memory creationByteCode = codeForTransparentProxy(_logic, _admin, _data);
TransparentUpgradeableProxy proxy = _deployTransparentProxy(creationByteCode);
return address (proxy);
}

function codeForTransparentProxy(address _logic, address _admin, bytes memory _data)
public
pure
returns (bytes memory)
{
bytes memory bytecode = type(TransparentUpgradeableProxy).creationCode;
return abi.encodePacked(bytecode, abi.encode(_logic, _admin, _data));
}

function _deployTransparentProxy(bytes memory _creationByteCode) internal returns (TransparentUpgradeableProxy){
address payable addr;

assembly {
addr := create(0, add(_creationByteCode, 0x20), mload(_creationByteCode))
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}

return TransparentUpgradeableProxy(addr);
}

// https://docs.openzeppelin.com/contracts/4.x/api/proxy#BeaconProxy-constructor-address-bytes-
function codeForBeaconProxy(address beacon, bytes memory data) public pure returns (bytes memory) {
bytes memory bytecode = type(BeaconProxy).creationCode;
return abi.encodePacked(bytecode, abi.encode(beacon, data));
}

function _deployBeaconProxy(bytes memory _creationByteCode) internal returns (BeaconProxy) {
address payable addr;
assembly {
addr := create(0, add(_creationByteCode, 0x20), mload(_creationByteCode))
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}
return BeaconProxy(addr);
}

function _deployBeacon(address _logic) internal {
if (!_isBeaconSet) {
_beacon = new UpgradeableBeacon(_logic);
_isBeaconSet = true;
emit BeaconCreated(address (_beacon));
}
}

function getBeacon() public returns (address) {
return address(_beacon);
}

function deployBeaconProxy(address _logic, bytes memory payload) public returns (address) {
if (!_isBeaconSet) {
_deployBeacon(_logic);
}
bytes memory code = codeForBeaconProxy(address (_beacon), payload);
BeaconProxy proxy = _deployBeaconProxy(code);
_beaconProxies[address (proxy)] = true;
return address (proxy);
}

function upgradeBeacon(address _logic) public {
_beacon.upgradeTo(_logic);
}


}

There are security and governance implications, that is for another post.

--

--