Difficulty: Intermediate
The Factory pattern is one of the most widely used design patterns in smart contract development. It allows a single contract (the factory) to deploy multiple instances of another contract on-chain. This is especially useful when your application needs to create many similar contracts dynamically, such as deploying a new token for each user, creating individual escrow contracts, or spinning up isolated liquidity pools.
At its core, a factory contract contains a function that uses the `new` keyword in Solidity to deploy a child contract. The factory typically stores the addresses of all deployed children in an array or mapping, enabling easy tracking and enumeration. This centralized deployment model also simplifies frontend integration since users only need to interact with one known factory address rather than discovering child contracts independently.
Beyond basic deployment, advanced factory patterns leverage the `CREATE2` opcode to enable deterministic address generation. With `CREATE2`, you can predict the address of a contract before it is deployed by combining the deployer address, a salt, and the contract bytecode. This is invaluable for counterfactual deployments, where you want to reference a contract address before it exists on-chain, a technique used extensively in account abstraction and state channel designs.
The Minimal Proxy pattern (EIP-1167) takes the factory concept further by deploying extremely lightweight clone contracts that delegate all calls to a single implementation contract. Instead of deploying the full bytecode each time, clones are only 45 bytes and use `delegatecall` under the hood. This can reduce deployment gas costs by 10x or more compared to deploying full contract copies, making it ideal for applications that need to create thousands of similar instances.
When designing a factory, consider whether child contracts need initialization parameters, whether you want deterministic addresses, and how much gas efficiency matters. Libraries like OpenZeppelin's `Clones` library provide battle-tested implementations of the minimal proxy pattern that you can integrate directly into your factory contracts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Wallet {
address public owner;
string public name;
constructor(address _owner, string memory _name) {
owner = _owner;
name = _name;
}
receive() external payable {}
function withdraw(uint256 amount) external {
require(msg.sender == owner, "Not owner");
payable(owner).transfer(amount);
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract WalletFactory {
Wallet[] public wallets;
mapping(address => Wallet[]) public ownerWallets;
event WalletCreated(address indexed owner, address walletAddress, string name);
function createWallet(string memory _name) external returns (address) {
Wallet wallet = new Wallet(msg.sender, _name);
wallets.push(wallet);
ownerWallets[msg.sender].push(wallet);
emit WalletCreated(msg.sender, address(wallet), _name);
return address(wallet);
}
function getWalletCount() external view returns (uint256) {
return wallets.length;
}
function getWalletsByOwner(address _owner) external view returns (Wallet[] memory) {
return ownerWallets[_owner];
}
}
The WalletFactory deploys new Wallet contracts using the `new` keyword. Each wallet is tracked in both a global array and an owner-specific mapping for easy lookups.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DeterministicFactory {
event Deployed(address addr, bytes32 salt);
// Predict the address before deployment
function getAddress(bytes32 salt, bytes memory bytecode)
public
view
returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(bytecode)
)
);
return address(uint160(uint256(hash)));
}
// Deploy using CREATE2
function deploy(bytes32 salt, bytes memory bytecode)
external
payable
returns (address addr)
{
assembly {
addr := create2(
callvalue(),
add(bytecode, 0x20),
mload(bytecode),
salt
)
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}
emit Deployed(addr, salt);
}
}
CREATE2 allows computing the deployed address in advance using the factory address, a salt value, and the bytecode hash. The same salt + bytecode always produces the same address.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/proxy/Clones.sol";
contract VaultImplementation {
bool private initialized;
address public owner;
uint256 public vaultId;
function initialize(address _owner, uint256 _vaultId) external {
require(!initialized, "Already initialized");
initialized = true;
owner = _owner;
vaultId = _vaultId;
}
function deposit() external payable {}
function withdraw() external {
require(msg.sender == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}
}
contract VaultFactory {
using Clones for address;
address public immutable implementation;
uint256 public vaultCount;
event VaultCreated(address indexed owner, address vault, uint256 vaultId);
constructor(address _implementation) {
implementation = _implementation;
}
function createVault() external returns (address) {
uint256 id = vaultCount++;
address clone = implementation.clone();
VaultImplementation(clone).initialize(msg.sender, id);
emit VaultCreated(msg.sender, clone, id);
return clone;
}
function createVaultDeterministic(bytes32 salt) external returns (address) {
uint256 id = vaultCount++;
address clone = implementation.cloneDeterministic(salt);
VaultImplementation(clone).initialize(msg.sender, id);
emit VaultCreated(msg.sender, clone, id);
return clone;
}
function predictAddress(bytes32 salt) external view returns (address) {
return implementation.predictDeterministicAddress(salt);
}
}
Minimal proxies deploy a tiny 45-byte contract that forwards all calls via delegatecall to a shared implementation. Note the use of initialize() instead of a constructor, since clones cannot use constructors.
Factory, Clone, Create2, Minimal Proxy