- Published on
Ethereum Proxy Patterns: ERC-1167, ERC-1967, and Diamond Standard
- Authors
- Name
- Frank
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
- Unlimited Size: Bypass 24KB contract size limit
- Modular Upgrades: Update specific functionality without affecting others
- Granular Control: Add, replace, or remove individual functions
- Gas Efficiency: Only deploy changed facets during upgrades
- 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
- Storage Layout: Carefully plan storage to avoid collisions
- Initialization: Use initializer patterns instead of constructors
- Access Control: Implement robust admin controls
- 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.