Diving into Smart Contract’s Minimal Proxy “EIP-1167”

0
57

Overview

Proxy contracts can be tricky to grasp at the beginning, in this post, we are going to analyze the minimal proxy standard or “EIP-1167” and create an example with code.

Before starting, it is very important to not get confused between upgradable proxies and minimal proxies, they are completely different. For this post, we are solely going to review the minimal proxy and in a later post we will review the upgradable proxy.

The minimal proxy standard was officially published back in 2018, you can find the post here. The main idea of this standard is to deploy copies of a base contract as cheap as possible. Let me expand a little further:

The best example is a multi-sig wallet. Let’s say that you created a very simple multi-sig wallet that can receive funds, send funds and set n amount of owners. Of course, in order to send funds you need to reach a certain quorum given n ≥ m. Once you have your smart contract ready, there are 2 main approaches that come to mind to get it ready for production. The first one is deploying the contract and make all the users interact directly with it, meaning all the funds will be stored inside of this contract, in order to have a tracking as to who owns what, you would create a mapping(address => uint) public balances + some modifiers. The problem with this approach is that you concentrate everything in one place and most importantly, you open the possibilities of additional attack vectors. In other words, you are making the contract more complex. We don’t want to do that, we want to have security as our main priority. The second approach that comes to mind is to make the user deploy the contract, in order to do this, you would compile the contract, put the bytecode in the frontend, and make the user deploy the contract. The problem with this approach is that it is very inefficient and gas costly. Imagine if the contract gets too large and complex, the deployment costs are going to be very high, plus we are bombarding the chain with huge amounts of storage. The solution for this problem is to implement the minimal proxy standard.

What the minimal proxy does, is create a cheap clone (we call it cheap because it is very cheap to deploy) that has the exact same logic as the implementation contract but with its own storage state. The way that this happens is through the low-level delegate-call.

Image from Open Zeppelin

In order to implement this standard we need:

  1. Implementation contract: This is sometimes called base contract, core contract, main contract etc.. The important thing is that the implementation contract is where all the logic will be.
  2. Proxy Factory or Clone Factory: As the name suggests, the Clone Factory contract will be our factory. This means that a user will call a function of the factory and the factory will clone an exact copy of the Implementation contract but with its own storage. This means that every clone will have the same logic, but with independent storage state.
  3. Proxy: As previously mentioned, the proxy contract is a clone of the Implementation contract but with its unique storage.

Now that we have a general understanding, let’s create an example to solidify our knowledge. We are going to do this example in Remix to make it simpler.

The contracts are going to be extremely simple, the purpose here is to understand the standard.

Ok… In order to this we need the following contracts:

  1. Implementation: This is where our logic is going to be, we will call it Implementation.sol.
  2. CloneFactory: This is going to be our factory, we will have a clone() function that users will trigger and as a result, the factory will output the address of the proxy. The name for the factory will be CloneFactory.sol.
  3. Proxy: There is nothing to do with the proxy, the proxy will be the output of the clone() function from CloneFactory.sol. There can be as many different proxies, that is the whole purpose, to make a lot of clones of the Implementation.sol

This is how it would look:

One very important aspect to keep in mind, is that the clones do not know about the constructor, so instead of using a constructor to assign important variables, we use an initialize() function “replacing the constructor”. We just need to make sure that the initialize() function can be called just once, so people cannot tamper with the contract, similar to how the constructor works. In order to do this, we normally use openZeppelin’s Initializable, you can find it here. For this example, we are not going to use any third-party contracts, just to make it more clear.

Let’s start with the Implementation.sol. The only thing the contract will do is have a uint public variable with a setter function and a modifier, restricting the access to change the variable only for the owner.

Implementation.sol

Let’s brake it down:

uint public x → The unsigned integer that we are going to change in the setter function (it defaults to 0).

bool public isBase → This boolean will ensure that the implementation contract can never be initialized. If we see in the constructor, we set: isBase to true and the first require statement of the initialize() function is require(isBase ==false). This guarantee us that the Implementation contract is used only for the logic and no-one can tamper with. Remember that the proxy or clone contract doesn’t know about the constructor, so isBase will be set to its default value that is false.

address public owner → The owner of the contract (externally owned account). The owner is default to address(0). In Solidity, if you leave an address type without assignment, the default value is address(0).

modifier onlyOwner() → Hopefully you don’t need explanation for this, but basically this is saying only the owner can call this function.

initialize(address _owner) → The initialize function needs to be called immediately once the proxy clone is created. This is like our constructor, meaning that if someone calls this function before, it will have control over the contract. As you can see, it has one argument (address _owner). This argument will be provided in the CloneFactory (you will see). There are 2 important considerations with this:

  1. You need to make sure the initialize function gets called ONLY ONCE. The way we did it, is by checking that the owner is the address(0). Once the owner gets assigned and you try to call the function again, the transaction will revert. I strongly recommend to follow this architecture + OpenZppelin’s initializer() modifier inside of the Initializable contract. This ensures that the function can only be called once.
  2. Make the implementation contract unusable: By assigning isBase=true in the constructor and requiring isBase== false in the initialize() function, we ensure that no one person can tamper with the contract. This contract’s only purpose is to serve as a logic contract, if someone tries to call the initialize function of the base contract, it will immediately revert.

Once we have the Implementation.sol ready to go, let’s create CloneFactory.sol. To do this, we are going to use the clone() function from OpenZeppelin’s Clone library, you can find it here.

CloneFactory.sol

Let’s brake it down:

interface Implementation → initialize() is the only function we will need from Implementation.sol. We will call it immediately once the clone contract is created.

address public implementation → The address of Implementation.sol.

mapping(address => address[]) public allClones → This is just a mapping to keep track of all the deployed clones, the first address is the msg.sender or the owner of the clone.

clone(address _implementation) → This function is from Open Zeppelin. On a high level, we provide the implementation address (Implementation.sol) and it returns an instance of that address, in other words, it returns an exact clone of Implementation.sol.

_clone() → This is the function that users will call. Once someone calls this function, the first thing that is going to happen is create a new clone and save it under address identicalChild . This address will hold the same logic as Implementation.sol, but with its own storage state. As we can see in the third line of _clone() we are calling the initialize function:

This is crucially important, that is why we are calling it immediately. This will make the caller of the _clone() function the owner of the clone contract. Once it is done, there is no way back.

You can clone as many contracts as you want, each keeping its own storage. Whoever calls the _clone() function will be the only account that has access to change “x”.

Conclusion

The Minimal Proxy standard is very efficient when you need to create many copies of one contract, each copy with its own storage state. The most common use cases are: Multi-Sig-Wallet, escrow accounts, some type of liquidity pools etc..

As a reminder, whenever you are implementing this standard, always be extremely careful of the following things:

  1. The initialize() function from the implementation contract can only be called once.
  2. The initialize() function from the implementation contract needs to be triggered as soon as possible.
  3. Make the implementation contract unusable.
Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also, Read


Diving into Smart Contract’s Minimal Proxy “EIP-1167” was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.