In the previous article in this series, we saw how you could create your multi-sig wallet contract using Solidity. In this article, we will continue the journey and create a contract factory for that multi-sig contract we developed earlier.
This will allow anyone to deploy their multi-sig contract through your contract (and the app we will make later in the series) and enable cheap deployments. This contract factory will clone the already-deployed multi-sig contract and configure it according to the user deploying it. We will also have a built-in registry to track who deployed the contract and the list of contracts deployed by a specific address.
If you prefer video tutorials, I have a video on this same topic on my YouTube channel. Make sure you go watch the video if you prefer videos over articles!
Changes to the multi-sig contract
To make our multi-sig contract proxy-friendly and deployable by our contract factory, we need to make a minor change to our multi-sig contract. Instead of using a constructor, we need to use a regular function, which we will name initialize
. This is because when a contract is cloned, the constructor is not called, so there's no way for us to set the owners just after the contract is deployed. Luckily, OpenZeppelin has a Initializable
contract that helps us make our custom function act like a constructor, i.e., make it so that it can only be run once, just like a constructor.
So, when the contract is deployed, the initialize
function is called to do the job constructor was supposed to do and after that, initialize
function cannot be used on the same contract again!
Add the following import to your Multisig.sol
contract:
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
Now, you need to inherit this contract:
contract Multisig is Initializable {
...
}
Now, let's edit the constructor to be a function named initialize
:
function initialize(address[] memory owners, uint256 requiredSignatures) public initializer {
require(owners.length > 0, "At least one owner required");
require(requiredSignatures > 0 && requiredSignatures <= owners.length, "Invalid number of required signatures");
_owners = owners;
_requiredSignatures = requiredSignatures;
}
In the above code, we are also using the initializer
modifier that comes with Initializable
contract that makes this function to be able to run only once.
Creating the factory contract
Now that we made our multi-sig contract factory deployment friendly, we can proceed to write our factory contract. Create a new file under contracts
folder named MultisigFactory.sol
.
Importing OpenZeppelin libraries
We need to use some OpenZeppelin libraries here as well:
import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
Clones
allow us to clone any contract just by providing the contract address. If you look at the source code, it's all assembly. Under the hood, it's creating a contract that points to an existing implementation.EnumerableSet
will allow us to have anAddressSet
and have easy functions to deal with addresses. We will use this to save all the contracts deployed by a particular wallet (basically acting like a registry).
States and events
Our contract factory will have the following states and events:
using EnumerableSet for EnumerableSet.AddressSet;
address public owner;
address public implementation;
mapping(address => EnumerableSet.AddressSet) private deployments;
event ImplementationUpdated(address _caller, address _implementation);
event ContractDeployed(address _deployer, address _deployedContract, address _implementation);
owner
is the owner of the contract factory that is allowed to update the implementation address.implementation
is the contract that will be cloned when someone calls thedeployContract()
function, which we will take a look at later.deployments
keep track of the multi-sig contracts deployed by wallets. You can thinkAddressSet
to be an array of addresses but with easy functions available.ImplementationUpdated
event is fired when the contract owner updates the implementation, changing the contract deployed moving forward.ContractDeployed
event is fired whenever someone deploys a contract. It will be super useful when we move to the app part to detect when the contract is successfully deployed.
Writing the constructor
Since a factory is not deploying this contract, and we are deploying it the normal way, we can use the constructor here:
constructor(address _implementation) {
owner = msg.sender;
implementation = _implementation;
}
In the above code, we set the contract owner and the implementation.
Writing the setImplementation()
function
This function can only be called by the contract owner and will allow the owner to update the implementation that will be cloned moving forward:
function setImplementation(address _implementation) public {
require(msg.sender == owner, "Not owner!");
implementation = _implementation;
emit ImplementationUpdated(msg.sender, _implementation);
}
In the above code, we check if the function caller is the owner; if not, we revert. We set the implementation and emit the ImplementationUpdated
event.
Writing the deployContract()
function
Following is the code for the deployContract()
function:
function deployContract(bytes memory _data) public {
address deployedContract = Clones.clone(implementation);
(bool success, ) = deployedContract.call(_data);
require(success, "Failed to initialize contract!");
bool added = deployments[msg.sender].add(deployedContract);
require(added, "Failed to add to registry!");
emit ContractDeployed(msg.sender, deployedContract, implementation);
}
The deployContract
function is used to create a new Multisig contract instance. It takes a parameter of type bytes, the initialization data for the Multisig contract. The function uses the Clones.clone()
function to create a new instance of the Multisig contract using the implementation address stored in the state variable. The initialization data is then passed to the new contract instance using the call()
function. If the contract is successfully initialized, the address of the new contract is added to a registry maintained by the contract.
Writing the getDeployed()
and countDeployed()
functions
function getDeployed(address _deployer) public view returns(address[] memory) {
return deployments[_deployer].values();
}
function countDeployed(address _deployer) public view returns(uint256) {
return deployments[_deployer].length();
}
The above two functions simply access the registry and return relevant data by using functions from AddressSet
.
Complete contract
If you missed something, the following is the complete code for the MultisigFactory.sol
contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
contract MultisigFactory {
using EnumerableSet for EnumerableSet.AddressSet;
address public owner;
address public implementation;
mapping(address => EnumerableSet.AddressSet) private deployments;
event ImplementationUpdated(address _caller, address _implementation);
event ContractDeployed(address _deployer, address _deployedContract, address _implementation);
constructor(address _implementation) {
owner = msg.sender;
implementation = _implementation;
}
function setImplementation(address _implementation) public {
require(msg.sender == owner, "Not owner!");
implementation = _implementation;
emit ImplementationUpdated(msg.sender, _implementation);
}
function deployContract(bytes memory _data) public {
address deployedContract = Clones.clone(implementation);
(bool success, ) = deployedContract.call(_data);
require(success, "Failed to initialize contract!");
bool added = deployments[msg.sender].add(deployedContract);
require(added, "Failed to add to registry!");
emit ContractDeployed(msg.sender, deployedContract, implementation);
}
function getDeployed(address _deployer) public view returns(address[] memory) {
return deployments[_deployer].values();
}
function countDeployed(address _deployer) public view returns(uint256) {
return deployments[_deployer].length();
}
}
Writing script to encode data for initialize()
function
In the factory contract, we have accepted bytes
as a parameter in the deployContract()
function. Although, we need to encode the call to the initialize()
function to pass the bytes to the function. For testing, I've created a script to encode this data. However, our app will do this automatically, and the user won't need to be in the hassle the encode the data.
Create a new file under scripts
folder named generateEncodedData.js
and have the following contents:
const hre = require("hardhat");
async function main() {
const Lock = await hre.ethers.getContractFactory("Multisig");
const encodedData = await Lock.interface.encodeFunctionData("initialize", [
["0x53b8E7c9D1e0E9BdF5e2c3197b070542611995e7"],
1,
]);
console.log(`Encoded data - ${encodedData}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Feel free to replace the addresses with the owner addresses you desire.
The script uses the Hardhat framework to retrieve the contract factory for the Multisig contract using the getContractFactory()
method. It then calls the encodeFunctionData()
method on the contract interface to encode the arguments for the initialize function.
The encodeFunctionData()
method takes two parameters. The first parameter is a string representing the name of the function to encode, which is "initialize" in this case. The second parameter is an array of values representing the function's arguments.
In this script, the arguments for the initialize function are an array containing a single address and an integer value of 1. The script encodes these arguments using the contract interface and outputs the resulting encoded data using the console.log()
method.
The script output is a string representing the encoded data for the initialize function that can be used to initialize a new instance of the Multisig contract with the specified arguments.
Now you can play around, deploy the contract, pass the encoded data to the deployContract
function, and you will find the initialize
function already called for you and everything set up.
If you want to see the complete testing for this contract, watch my YouTube video on the same topic where we deploy the factory contract and deploy new multi-sig contracts through it, along with encoding data for
initialize
function.
Conclusion
In this article, you learnt about writing a contract factory for the multi-sig contract we developed in the previous article. Feel free to refer to the previous article in this series.
If you have any suggestions about content, feel free to leave them in the comments section, I would love to address them!