Smart Contracts with Minimal Proxy (EIP-1167 Clone) Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Smart Contracts with Minimal Proxy (EIP-1167 Clone) Development
Medium
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1243
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    873
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1086
  • image_logo-advance_0.png
    B2B Advance company logo design
    563
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    830

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.