Creating A Proxy Factory Using Clones
Preface
Proxies are an advanced topic in solidity, and those reading this should already be familiar with the concept of a proxy contract, clones, storage slots, and the delegatecall opcode. If you are not familiar with these concepts, I would recommend reading the following articles first:
Motivation
Creating a proxy factory can allow you to cheaply clone a logic contract
cheaply by creating a minimal proxy that sits behind it.
The proxy contract delegates all functionality to the logic contract,
but holds executes it within the context of its own storage.
Many ecosystems have already created proxy factories, including OpenZeppelin, Aragon, and Gnosis Safe. Uniswap V3 is also using a proxy factory to create minimal proxies for each pool between two tokens.
There are different types of proxy factories, but in this article we will be exploring how to create a proxy factory that uses "clones" where we can cheaply deploy a minimal proxy that delegates all functionality to a logic contract.
First, let's take a dive into the typical proxy factory architecture.
Graph Reference a)
In the above graph, we can see that the proxy factory is the master contract that creates minimal proxies. The minimal proxies are created by deploying a minimal proxy contract that delegates all functionality to the logic contract.
The minimal proxy contract delegatecall
s the logic contract, and
executes the logic contract within the context of its own storage.
This pattern can be used to make infinite combinations of proxies that all delegate to the same logic contract or delegate calls to different logic contracts. It is a very powerful pattern that can be used to create a wide variety of contracts.
When to use a proxy factory
- When you need many replicas of the same contract, but each with their respective storage slots
- When you want to create a deployment system that tracks new contracts
When not to use a proxy factory
- When you realistically don't need to create clones of this contract
- When you don't need to track the contracts that are created
Code example
Let's start by taking a look at some sample code for a proxy factory that deploys ERC721 NFT's.
First thing's first, let's import the ClonesUpgradeable
library from OpenZeppelin.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import '@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol';
Next, let's look into a minimal Master contract that will be used to create clones.
contract ThingFactory {
address public implementationContract;
event CloneCreated(address indexed proxyAddress,address indexed logicContract);
constructor (address _implementationContract) {
implementationContract = _implementationContract;
}
function createThing(bytes memory data) public returns (address){
address proxy = ClonesUpgradeable.clone(implementationContract);
MinimalProxy(proxy).initialize(data);
emit CloneCreated(proxy,implementationContract);
return proxy;
}
}
In this example, we store the address of the logic contract in the implementationContract
variable.
We also emit an event that logs the address of the proxy contract and the logic contract.
The createThing
function is the function that creates the clone.
It uses the ClonesUpgradeable
library to create a clone of the logic contract.
It then calls the initialize
function on the clone, which is a function that is defined in the logic contract.
The initialize
function is a function that is used to initialize the storage slots of the clone.
Let's take a look at the logic contract now.
interface MinimalProxy {
function initialize(bytes memory data) external;
}
contract Thing {
/**
* @dev Logic contract that proxies point to
*/
function initialize(bytes memory data) external {
// do stuff
}
}
Now, in my personal use case, I needed for the contract factory to be able to deploy proxies to different implementations. I also needed to be able to track the contracts that were created, so I created a mapping that stored the address of the proxy contract and the address of the logic contract.
contract ThingFactory {
event CloneCreated(address indexed proxyAddress,address indexed logicContract);
function createThing(address impl,bytes memory data) public returns (address){
address proxy = ClonesUpgradeable.clone(impl);
MinimalProxy(proxy).initialize(data);
proxyToLogic[proxy] = impl;
emit CloneCreated(proxy,impl);
return proxy;
}
}
The initialize function.
The initialize function is a common pattern that is used to initialize the storage slots of a contract. It is a function that is called when the contract is first deployed, and it is used to set the initial values of the storage slots.
In our "thing" contracts, we wrap the initialize function in an interface so that we can call it from the master contract. We also encode the data that we want to pass into the initializer function, which allows us to basically pass in any arguments into our "Thing" contracts when we create them.
An example on how to do that below, would be the following.
Here, we'll look at deploying two proxies. One pointing to a modified Thing1, and one pointing to a Thing2. In reality, you would probably want to deploy a bunch of proxies pointing to the same contract, but this is just an example.
Here are the modified contracts.
We first add these imports
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol';
import 'erc721a-upgradeable/contracts/ERC721AUpgradeable.sol';
contract Thing1 is Initializable, OwnableUpgradeable,ERC721AUpgradeable {
/**
* @dev Logic contract that proxies point to
*/
function initialize(bytes memory data) external initializerERC721A initializer {
(address _owner,string memory _name,string memory _symbol) = abi.decode(data,(address,string,string));
__ERC721A_init(_name, _symbol);
__Ownable_init();
transferOwnership(_owner);
}
function mint(uint256 quantity) external payable {
// `_mint`'s second argument now takes in a `quantity`, not a `tokenId`.
_mint(msg.sender, quantity);
}
function adminMint(uint256 quantity) external payable onlyOwner {
_mint(msg.sender, quantity);
}
function transferOwnership(address newOwner) public override onlyOwner {
super.transferOwnership(newOwner);
}
}
contract Thing2 is Initializable, OwnableUpgradeable,ERC721AUpgradeable {
/**
* @dev Logic contract that proxies point to
*/
uint price;
function initialize(bytes memory data) external initializerERC721A initializer {
(address _owner,string memory _name,string memory _symbol,uint _price) = abi.decode(data,(address,string,string,uint));
price = _price;
__ERC721A_init(_name, _symbol);
__Ownable_init();
transferOwnership(_owner);
}
function mint(uint256 quantity) external payable {
if(msg.value < price * quantity) revert("Too Lazy To Make A Custom Error");
// `_mint`'s second argument now takes in a `quantity`, not a `tokenId`.
_mint(msg.sender, quantity);
}
function adminMint(uint256 quantity) external payable onlyOwner {
_mint(msg.sender, quantity);
}
function transferOwnership(address newOwner) public override onlyOwner {
super.transferOwnership(newOwner);
}
}
For deployments, we'll be using hardhat to deploy the contracts.
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
import { ethers as _ethers } from "ethers";
import * as fs from 'fs';
import { MinimalProxy__factory } from "../typechain-types";
describe("Lock", function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployOneYearLockFixture() {
}
describe("Deployment", function () {
it("Should set the right unlockTime", async function () {
const[owner] = await ethers.getSigners();
const LogicContract = await ethers.getContractFactory("Thing1");
const logicContract = await LogicContract.deploy();
await logicContract.deployed();
const ProxyFactory = await ethers.getContractFactory("ThingFactory");
const proxyFactory = await ProxyFactory.deploy();
await proxyFactory.deployed();
console.log("ProxyFactory deployed to:", proxyFactory.address);
//Try To Init
//abi.encode
const abiCoder = new _ethers.utils.AbiCoder();
const encoded = abiCoder.encode(["address",'string','string'], [owner.address,'Mock Name','Mock Symbol']);
const tx = await proxyFactory.createThing(encoded,logicContract.address);
//Find all CloneCreated events
const filter = proxyFactory.filters.CloneCreated(null, null);
const events = await proxyFactory.queryFilter(filter);
fs.writeFileSync('./scripts/deployed.json', JSON.stringify(events,null,4));
const proxyAddress = events[0].args[0];
const minimalProxy = await ethers.getContractAt("Thing1", proxyAddress);
const _owner = await minimalProxy.owner();
expect(_owner).to.equal(owner.address);
//attach the miniaml proxy to the _logicContract
//mint 10 from the the _logicContract
await minimalProxy.mint( 10);
//log the balance of the minimal proxy
const minimalBalance = await minimalProxy.balanceOf(owner.address);
console.log("Minimal Proxy Balance: ", minimalBalance.toString());
expect(minimalBalance).to.equal(10);
//Thing 2
const LogicContract2 = await ethers.getContractFactory("Thing2");
const logicContract2 = await LogicContract2.deploy();
await logicContract2.deployed();
const encoded2 = abiCoder.encode(["address",'string','string','uint'], [owner.address,'Mock Name2','Mock Symbol2',_ethers.utils.parseEther('.1')]);
const tx2 = await proxyFactory.createThing(encoded2,logicContract2.address);
//Find all CloneCreated events
const filter2 = proxyFactory.filters.CloneCreated(null, null);
const events2 = await proxyFactory.queryFilter(filter2);
//Find the last contract deployed
const proxyAddress2 = events2[events2.length-1].args[0];
const minimalProxy2 = await ethers.getContractAt("Thing2", proxyAddress2);
const _owner2 = await minimalProxy2.owner();
expect(_owner2).to.equal(owner.address);
const price = await minimalProxy2.price();
expect(price).to.equal(_ethers.utils.parseEther('.1'));
});
it("Should set the right owner", async function () {
});
});
});
Conclusion
And with that, we have a working factory contract that deploys minimal proxies. This is a very powerful tool that can be used to deploy many different types of contracts. I hope you enjoyed this tutorial and learned something new. If you have any questions, feel free to DM me on twitter @0xSimon. Thanks for reading!