>/D_
Published on

Ethereum Proxy Patterns: ERC-1167, ERC-1967, and Diamond Standard

Authors
  • avatar
    Name
    Frank
    Twitter

Ethereum Proxy Patterns: ERC-1167, ERC-1967, and Diamond Standard

Smart contract immutability is both a feature and a limitation. While it provides security and trust, it also makes bug fixes and feature additions challenging. Proxy patterns have emerged as elegant solutions to this problem, enabling upgradeable and modular smart contracts while maintaining the benefits of blockchain's immutability. This guide explores three major proxy standards that have revolutionized smart contract architecture.

Understanding Proxy Fundamentals

At its core, a proxy pattern uses delegatecall to separate contract storage from contract logic. When a proxy contract receives a function call, it forwards that call to an implementation contract using delegatecall, which executes the implementation's code in the context of the proxy's storage.

Key Concepts

  • Proxy Contract: Holds the state and forwards calls
  • Implementation Contract: Contains the business logic
  • Delegatecall: EVM operation that preserves the calling contract's context
  • State Preservation: Proxy maintains state while logic can be updated

This separation enables powerful patterns for creating efficient, upgradeable, and modular smart contracts.

ERC-1167: Minimal Proxy Standard

ERC-1167 addresses the high cost of deploying multiple instances of similar contracts by introducing "clone contracts" or "minimal proxies."

The Problem

Deploying multiple instances of the same contract (like ERC-20 tokens or multi-sig wallets) can be extremely expensive. Each deployment requires storing the entire contract bytecode on-chain, multiplying costs with each instance.

The Solution

ERC-1167 creates lightweight proxy contracts that delegate all calls to a single master contract, dramatically reducing deployment costs.

Key Features

  • Minimal Bytecode: Clone contracts contain only ~45 bytes of code
  • Gas Efficiency: Reduces deployment costs by up to 99%
  • Shared Logic: Multiple clones share a single implementation
  • Separate State: Each clone maintains its own storage

Implementation Example

// Master contract
contract TokenMaster {
    string public name;
    string public symbol;
    mapping(address => uint256) public balances;

    function initialize(string memory _name, string memory _symbol) external {
        require(bytes(name).length == 0, "Already initialized");
        name = _name;
        symbol = _symbol;
    }

    function mint(address to, uint256 amount) external {
        balances[to] += amount;
    }
}

// Factory for creating clones
contract TokenFactory {
    address public immutable tokenMaster;

    constructor() {
        tokenMaster = address(new TokenMaster());
    }

    function createToken(
        string memory name,
        string memory symbol
    ) external returns (address clone) {
        clone = Clones.clone(tokenMaster);
        TokenMaster(clone).initialize(name, symbol);
    }
}

Use Cases

  • Token Factories: Creating multiple ERC-20 tokens efficiently
  • Multi-sig Wallets: Deploying wallet instances for different teams
  • Voting Contracts: Creating separate voting instances for different proposals
  • Game Items: Generating multiple instances of similar game contracts

Benefits and Limitations

Benefits:

  • Dramatically reduced gas costs
  • Simplified deployment process
  • Proven security model

Limitations:

  • No upgradeability (unless combined with other patterns)
  • All clones must use identical logic
  • Master contract becomes a critical dependency

ERC-1967: Upgradeable Proxy Standard

ERC-1967 standardizes storage slots for upgradeable contracts, preventing storage collisions and ensuring consistent upgrade mechanisms.

The Challenge

Upgradeable contracts need to store proxy-related data (like implementation addresses) without interfering with the implementation contract's storage layout. Random storage slots could collide with application data.

The Solution

ERC-1967 defines specific storage slots for proxy data using deterministic, collision-resistant slot calculations.

Standard Storage Slots

// Implementation slot: keccak256("eip1967.proxy.implementation") - 1
bytes32 internal constant IMPLEMENTATION_SLOT =
    0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

// Admin slot: keccak256("eip1967.proxy.admin") - 1
bytes32 internal constant ADMIN_SLOT =
    0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

// Beacon slot: keccak256("eip1967.proxy.beacon") - 1
bytes32 internal constant BEACON_SLOT =
    0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

Proxy Implementation

contract UpgradeableProxy {
    constructor(address implementation, address admin, bytes memory data) {
        _setImplementation(implementation);
        _setAdmin(admin);

        if (data.length > 0) {
            (bool success,) = implementation.delegatecall(data);
            require(success, "Initialization failed");
        }
    }

    function _implementation() internal view returns (address impl) {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            impl := sload(slot)
        }
    }

    function _admin() internal view returns (address admin) {
        bytes32 slot = ADMIN_SLOT;
        assembly {
            admin := sload(slot)
        }
    }

    fallback() external payable {
        _delegate(_implementation());
    }

    function _delegate(address implementation) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())

            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Upgrade Process

contract ProxyAdmin {
    function upgrade(address proxy, address newImplementation) external {
        require(msg.sender == UpgradeableProxy(proxy)._admin(), "Not admin");
        UpgradeableProxy(proxy).upgradeTo(newImplementation);
    }
}

Security Considerations

  • Admin Control: Only designated admin can perform upgrades
  • Transparent Proxy: Admin calls don't reach implementation
  • Storage Isolation: Proxy storage doesn't interfere with implementation
  • Initialization: Careful handling of initialization to prevent conflicts

EIP-2535: Diamond Multi-Facet Proxy Standard

The Diamond Standard represents the most sophisticated proxy pattern, enabling modular, upgradeable contracts that can exceed Ethereum's contract size limits.

The Vision

Traditional contracts hit Ethereum's 24KB bytecode limit as they grow. The Diamond Standard solves this by creating a modular architecture where functionality is split across multiple "facets."

Core Concepts

  • Diamond: The main proxy contract that routes calls
  • Facets: Individual contracts containing specific functionality
  • Function Selectors: Map function calls to appropriate facets
  • Diamond Cut: The upgrade mechanism for adding, replacing, or removing functions

Architecture Overview

contract Diamond {
    struct FacetAddressAndPosition {
        address facetAddress;
        uint96 functionSelectorPosition;
    }

    struct FacetFunctionSelectors {
        bytes4[] functionSelectors;
        uint256 facetAddressPosition;
    }

    struct DiamondStorage {
        mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
        mapping(address => FacetFunctionSelectors) facetFunctionSelectors;
        address[] facetAddresses;
    }

    fallback() external payable {
        DiamondStorage storage ds = diamondStorage();
        address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
        require(facet != address(0), "Function does not exist");

        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())

            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Facet Example

contract ERC20Facet {
    function transfer(address to, uint256 amount) external returns (bool) {
        // Implementation using diamond storage
        return _transfer(msg.sender, to, amount);
    }

    function balanceOf(address account) external view returns (uint256) {
        return _balanceOf(account);
    }
}

contract GovernanceFacet {
    function propose(bytes memory proposal) external returns (uint256) {
        // Governance logic
    }

    function vote(uint256 proposalId, bool support) external {
        // Voting logic
    }
}

Diamond Cut Mechanism

enum FacetCutAction {
    Add,
    Replace,
    Remove
}

struct FacetCut {
    address facetAddress;
    FacetCutAction action;
    bytes4[] functionSelectors;
}

function diamondCut(
    FacetCut[] memory cuts,
    address init,
    bytes memory initData
) external {
    // Perform the diamond cut
    for (uint256 i = 0; i < cuts.length; i++) {
        FacetCut memory cut = cuts[i];

        if (cut.action == FacetCutAction.Add) {
            addFunctions(cut.facetAddress, cut.functionSelectors);
        } else if (cut.action == FacetCutAction.Replace) {
            replaceFunctions(cut.facetAddress, cut.functionSelectors);
        } else if (cut.action == FacetCutAction.Remove) {
            removeFunctions(cut.facetAddress, cut.functionSelectors);
        }
    }
}

Benefits of Diamond Standard

  1. Unlimited Size: Bypass 24KB contract size limit
  2. Modular Upgrades: Update specific functionality without affecting others
  3. Granular Control: Add, replace, or remove individual functions
  4. Gas Efficiency: Only deploy changed facets during upgrades
  5. Organizational Clarity: Separate concerns across different facets

Real-World Example

// A DeFi protocol using Diamond Standard
contract DeFiDiamond {
    // Facet for lending functionality
    // Facet for borrowing functionality
    // Facet for governance
    // Facet for rewards
    // Facet for emergency functions
}

// Each facet can be developed, tested, and upgraded independently
contract LendingFacet {
    function deposit(address asset, uint256 amount) external { /* ... */ }
    function withdraw(address asset, uint256 amount) external { /* ... */ }
}

contract BorrowingFacet {
    function borrow(address asset, uint256 amount) external { /* ... */ }
    function repay(address asset, uint256 amount) external { /* ... */ }
}

Choosing the Right Proxy Pattern

Use ERC-1167 When:

  • Deploying multiple similar contracts
  • Cost optimization is the primary concern
  • No upgradeability needed
  • Simple, well-tested logic

Use ERC-1967 When:

  • Need upgradeable contracts
  • Moderate complexity applications
  • Standard upgrade patterns sufficient
  • Security and simplicity are priorities

Use Diamond Standard When:

  • Building complex, large-scale applications
  • Need modular upgrade capabilities
  • Approaching contract size limits
  • Building long-term, evolving protocols

Security Best Practices

General Proxy Security

  1. Storage Layout: Carefully plan storage to avoid collisions
  2. Initialization: Use initializer patterns instead of constructors
  3. Access Control: Implement robust admin controls
  4. Upgrade Governance: Consider multi-sig or DAO governance for upgrades

Testing Strategies

contract ProxyTest {
    function testUpgrade() public {
        // Deploy proxy with V1 implementation
        address proxy = deployProxy(implementationV1);

        // Test V1 functionality
        assertEq(IContract(proxy).version(), 1);

        // Upgrade to V2
        upgradeProxy(proxy, implementationV2);

        // Test V2 functionality
        assertEq(IContract(proxy).version(), 2);

        // Verify state preservation
        assertEq(IContract(proxy).previousData(), expectedValue);
    }
}

Future of Proxy Patterns

The evolution of proxy patterns reflects the maturation of the Ethereum ecosystem:

  • Gas Optimization: Continued focus on reducing deployment and upgrade costs
  • Security Improvements: Better patterns for preventing storage collisions
  • Developer Experience: Tools and frameworks making proxies easier to use
  • Standardization: More standardized patterns for common use cases

Conclusion

Proxy patterns have become essential tools in the Ethereum developer's toolkit, enabling sophisticated applications that can evolve over time. Each pattern serves specific needs:

  • ERC-1167 excels at cost-efficient contract factories
  • ERC-1967 provides secure, standardized upgradeability
  • Diamond Standard enables complex, modular applications

Understanding these patterns and their trade-offs enables developers to build more flexible, maintainable, and cost-effective smart contracts. As the ecosystem continues to evolve, these proxy patterns will remain fundamental to building production-ready decentralized applications.

My shorthand notes were the source material for this article produced by generative AI.