Smart Contract Development with Minimal Proxy (EIP-1167 Clone)
Deploying a single staking contract costs 0.05 ETH at 30 gwei gas price. If you need to create 1000 instances of that contract for 1000 users, that's 50 ETH for deployment. EIP-1167 Minimal Proxy cuts this to 0.003 ETH per instance: 3 ETH total instead of 50.
The pattern is used by: Uniswap V2 (pair created as a clone), Gnosis Safe (each wallet is a clone), most modern NFT factories.
What is Minimal Proxy
EIP-1167 defines a standard 45-byte bytecode that is a proxy to an implementation. The entire bytecode is just DELEGATECALL to the implementation address. No logic, no storage — just delegation.
Proxy bytecode in hex:
3d602d80600a3d3981f3363d3d373d3d3d363d73<implementation_address>5af43d82803e903d91602b57fd5bf3
Where <implementation_address> is the 20-byte implementation address, hardcoded in bytecode. That's why the contract is so cheap: just 45 bytes of bytecode.
DELEGATECALL means the implementation code executes in the proxy's context: msg.sender and msg.value are preserved, address(this) is the proxy address, storage is written to the proxy. The implementation has no storage, only code.
OpenZeppelin Clones
OpenZeppelin provides a Clones library for working with EIP-1167:
import "@openzeppelin/contracts/proxy/Clones.sol";
contract StakingFactory {
address public immutable implementation;
event StakingDeployed(address indexed clone, address indexed owner);
constructor(address _implementation) {
implementation = _implementation;
}
function deployStaking(
address rewardToken,
uint256 rewardRate,
address owner
) external returns (address clone) {
// Deterministic deployment via CREATE2
bytes32 salt = keccak256(abi.encodePacked(owner, rewardToken, block.timestamp));
clone = Clones.cloneDeterministic(implementation, salt);
// Initialization (initialize function instead of constructor)
IStaking(clone).initialize(rewardToken, rewardRate, owner);
emit StakingDeployed(clone, owner);
}
// Predict address before deployment
function predictAddress(
address owner,
address rewardToken,
uint256 timestamp
) external view returns (address) {
bytes32 salt = keccak256(abi.encodePacked(owner, rewardToken, timestamp));
return Clones.predictDeterministicAddress(implementation, salt);
}
}
Difference Between clone and cloneDeterministic
Clones.clone() uses the CREATE opcode — address depends on the factory's nonce. Clones.cloneDeterministic() uses CREATE2 — address is determined by salt and factory address. CREATE2 is preferable: you can calculate the address off-chain before deployment, important for UI and pre-approval (approve before contract deployment).
Critical Point: initializer Instead of Constructor
The implementation's constructor runs once at implementation deployment, not at clone deployment. Clones get clean storage. So the implementation uses an initializer pattern:
contract StakingImplementation {
address public rewardToken;
uint256 public rewardRate;
address public owner;
bool private _initialized;
// Protection from re-initialization
modifier initializer() {
require(!_initialized, "Already initialized");
_initialized = true;
_;
}
function initialize(
address _rewardToken,
uint256 _rewardRate,
address _owner
) external initializer {
rewardToken = _rewardToken;
rewardRate = _rewardRate;
owner = _owner;
}
// Business logic...
}
OpenZeppelin provides an Initializable base contract with more robust protection. Use it, don't write your own:
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract StakingImplementation is Initializable {
function initialize(...) external initializer {
// ...
}
}
Vulnerability: Uninitialized Implementation
The implementation is also a contract with a public initialize() function. If you don't call it on the implementation itself — anyone can call initialize() on the implementation with arbitrary parameters and become the owner. This doesn't break clones (they have their own storage), but is a problem if the implementation has any functionality or stores state.
Solution: call _disableInitializers() in the implementation's constructor:
constructor() {
_disableInitializers(); // OpenZeppelin Initializable
}
This deactivates initialize() on the implementation contract itself, keeping it functional only via DELEGATECALL from clones.
Comparison with Other Proxy Patterns
| Pattern | Gas on deploy | Upgradeability | Complexity | Use case |
|---|---|---|---|---|
| EIP-1167 Clone | ~40k gas | No | Low | Many instances of same logic |
| Transparent Proxy | ~400k gas | Yes | Medium | Single upgradeable contract |
| UUPS | ~300k gas | Yes | Medium | Upgradeable, logic in implementation |
| Beacon Proxy | ~200k gas | Yes (all at once) | High | Many upgradeable instances |
| Diamond (EIP-2535) | ~500k gas | Yes (per facets) | High | Complex modular logic |
Clones aren't upgradeable by definition: bytecode is hardcoded in the proxy. If you need upgradeability for many instances — use Beacon Proxy: all clones point to a beacon contract that stores the implementation address. Change the address in the beacon — all instances update.
Common Mistakes
Storage collision when changing implementation. If you deploy a new implementation with different storage layout — old clones read storage by old offsets, but the new implementation interprets them differently. Without upgradeability this isn't a problem: the implementation is fixed. But if you decide to add upgradeability via Beacon post-factum — you need a storage gap.
Don't pass ETH in initialize(). If the implementation's constructor accepted ETH — initializer should too. But in most cases initialization doesn't require ETH — don't complicate it.
Using immutable variables in the implementation. Immutable is stored in the implementation's bytecode, not in storage. During DELEGATECALL the clone reads storage from itself but code from the implementation. Immutable works correctly: the clone reads the value from the implementation's bytecode. This is normal behavior, but you need to understand it.
Factory with Minimal Proxy development: 2-3 working days. Cost calculated individually.







