Factory/Clone

Published by Mario Oettler on

Another pattern that helps us to maintain a simple code is the factory pattern. In this pattern, a factory contract creates new contracts. Let’s take a closer look at the example of a token trading smart contract.

Let us assume we have three tokens A, B, and C, and we want to offer a trading smart contract for all pairs A-B, A-C, and B-C.

We could design one single smart contract that covers all trading pairs. But this contract would have a lot of overhead, making sure that each transaction is assigned to the right trading pair. Instead, it is possible and clearer to create a smart contract for each pair. This means that there is one smart contract for A-B, another smart contract for A-C and a third smart contract for B-C.

To ensure that all trading contracts offer the same functionality, it is recommended to deploy them based on a base contract through one single smart contract. The base contract serves as a template for the resulting trading contract.

There are two ways to achieve this with Solidity – normal factory and clone factory:

Normal Factory

The normal factory is very simple. You create a new contract (here, we call it productContract) from the factory contract by calling:

ProductContract productAddress = new ProductContract();

Then, you can save the address in an array or mapping.

productContractAddresses.push(productAddress);
pragma solidity 0.8.20;


contract FactoryContract{
    
    ProductContract[] public ProductContractAddresses;
    
    function createProduct() public{
        ProductContract productAddress = new ProductContract();
        ProductContractAddresses.push(productAddress);   
    }
}


contract ProductContract{
    
    address public creator;
    
    constructor(){
        creator = msg.sender;
    }
    
    function helloWorld() public pure returns (string memory _greeting){
        return "Hello World";
    }
}

The downside of this method are the relatively high gas costs. Each time you create a product contract, you have to pay for the whole deployment.

Clone Factory or Minimal Proxy

A solution to the high gas costs mentioned in the normal factory approach is to use a minimal proxy. Instead of deploying a new contract with all its logic, every time we need a new instance, we deploy a tiny (minimal) contract that simply forwards our function call with the delegateCall statement to a contract at a fixed address that contains all logic.

We call the contract that does all the logic logic contract. The contract that is there for forwarding the call is called proxy.

With the delegate call statement, the called target contract (logic contract) is executed in the context of the calling (proxy) contract. All changes that are made during a delegate call are stored in the caller’s realm.

This helps us to deploy the logic contract only once and delegate call it through our proxy. There are different implementations possible. EIP-1167 [LINK: https://eips.ethereum.org/EIPS/eip-1167] provides a gas saving way to create a proxy.

Basic Concept

The proxy contract has four tasks:

  1. Taking the arguments from the call
  2. Forward the arguments via delegatecall to the logic contract
  3. Receive the return values from the logic contracts
  4. Return the values from 3. if successful

Our proxy contract only needs one function that forwards all calls. The arguments contain the function name and parameters of the target function in the logic contract. All variables and balances are stored under the address of the proxy contract.

In order to set up the minimal proxy, we need a logic contract, a proxy factory, and the proxy contract. The address of the logic contract is used as the target address in the proxy contracts.

Coding a minimal proxy

Now, we want to code our own minimal proxy factory and test it. The code is similar to EIP-1167.

For starters, we show the principle of the proxy with a pseudo solidity contract. The code of our clone factory would look like that:

contract Proxy{
    
    address logicContractAddress;
    
    constructor(address _logicContractAddress){
        logicContractAddress = _logicContractAddress;
    }
    
    function forward() external returns (bytes memory){
        (bool success, bytes memory data) = logicContractAddress.delegatecall(msg.data);
        require(success);
        return data;
    }
}

There, you can see in the first line in the function that it uses the msg.data to call a function in the logic contract.

If the call was successful, it returns the return data.

But instead of using the Solidity code above, the EIP suggests using EVM code which is even shorter. Since the EVM doesn’t understand Solidity but only machine readable code, we need to provide it in byte code. The byte code looks like that:

3d602d80600a3d3981f363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3

The byte code consists of three parts. The yellow part is the delegate call, and the red part is the address to where the delegate call goes to.

You can learn more here: https://www.youtube.com/watch?v=9xqoK2nKkM4

The blue part is the code for returning the yellow and red part. It is the code that is executed during the creation of the runtime code (yellow and red).

Notice that this byte code doesn’t call the constructor from the Solidity example. Instead, the address is added to the byte code directly.

A breakdown of the single EVM instructions can be found here: https://blog.openzeppelin.com/deep-dive-into-the-minimal-proxy-contract/

In the example above, the red part is only a placeholder for the address of our logic contract. In order to add the real address, we need some assembly in our Solidity code like below:

pragma solidity 0.8.20;


contract cloneFactory{
    
    
    function createClone(address logicContractAddress) internal returns(address result){
        
        bytes20 addressBytes = bytes20(logicContractAddress);
        assembly{
            
            let clone:= mload(0x40) // Jump to the end of the currently allocated memory- 0x40 is the free memory pointer. It allows us to add own code
            
            /*
                0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000
                       
            */
            mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) // store 32 bytes (0x3d602...) to memory starting at the position clone

            /*
                0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe
                |        20 bytes                       |    20 bytes address                   |
            */
            mstore(add(clone, 0x14), addressBytes) // add the address at the location clone + 20 bytes. 0x14 is hexadecimal and is 20 in decimal
            
            /*
                0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000
                 |        20 bytes                       |    20 bytes address                   |  15 bytes                     |
            */
            mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) // add the rest of the code at position 40 bytes (0x28 = 40)
           
            /* 
                create a new contract
                send 0 Ether
                the code starts at the position clone
                the code is 55 bytes long (0x37 = 55)
            */
           result := create(0, clone, 0x37)
        }
    }
}

In order to work, we need a small logic contract. We simply take the following contract.

pragma solidity 0.8.20;


contract LogicContract{
    
    function addSomething(uint256 _a, uint256 _b) public view returns(uint256 result){
        return _a + _b;
    }
}

Now, we want to test it in the Remix IDE.

  1. Deploy the logic contract and copy the address and its ABI
  2. Deploy the cloneFactory contract
  3. Execute the function createClone and pass the address of the logic contract
  4. Copy the returned address from the console
  1. Open a new file called logicABI.abi
  2. Paste the ABI from step 1 into it
  3. Go to the tab deploy & run transactions
  4. Copy the clone proxy address from step 4 into the field “At Address

Click the button “At Address”

Then, you can interact with the logic contract via your proxy.

This is only a very simple example that leaves space for improvement. Existing libraries help you to deploy a minimal factory quickly and safely.

Categories:

if()