Diamond Pattern (ERC-2535) एक proxy pattern है जहां proxy contract एक साथ कई implementation contracts का उपयोग करता है, जो Transparent Upgradeable Proxy और UUPS के विपरीत है, क्योंकि वे एक समय में केवल एक implementation contract पर निर्भर करते हैं। प्राप्त होने वाले calldata के function selector के आधार पर proxy contract यह तय करता है कि किस implementation contract को delegatecall करना है (सटीक तंत्र का वर्णन बाद में किया गया है):

कई implementation contracts होने का एक फायदा यह है कि proxy contract द्वारा उपयोग किए जा सकने वाले लॉजिक की मात्रा पर कोई व्यावहारिक ऊपरी सीमा (practical upper limit) नहीं होती है। याद रखें कि EVM smart contract के bytecode साइज़ को 24kb तक सीमित करता है। यदि डेवलपर को 48kb तक का bytecode डिप्लॉय करना है, तो एक व्यावहारिक समाधान fallback-extension pattern का उपयोग करना है। 48kb से अधिक के लिए, diamond pattern सबसे आम समाधान है।
Diamond pattern की शब्दावली में, “proxy contract” को diamond कहा जाता है और “implementation contracts” को “facets” कहा जाता है। एक ही चीज़ को संदर्भित करने वाले दो शब्दों के होने से भ्रम पैदा हुआ है, इसलिए हम इस बात को अभी स्पष्ट करना चाहते हैं:
- diamond = proxy contract
- facet = implementation contract
एक diamond (proxy) को एक या अधिक implementation contracts (facets) में बदलाव करके अपग्रेड किया जा सकता है। इसके विकल्प के तौर पर, facets (implementation contracts) को बदलने के तंत्र का समर्थन न करके diamond को non-upgradeable (immutable) भी बनाया जा सकता है।
Diamond Pattern को सीखना
एक ही समय में कई implementation contracts से निपटने की जटिलता के कारण diamond proxy को “expert design pattern” के रूप में जाना जाता है। वास्तव में, अपनी कथित जटिलता के कारण EVM डेवलपर्स के बीच diamond pattern थोड़ा विवादास्पद है (हम यहां इस बहस में नहीं पड़ते हैं या जटिलता पर कोई निर्णय नहीं देते हैं)।
इसका स्पेसिफिकेशन (specification) काफी छोटा है। इसके लिए चार public view functions की आवश्यकता होती है — लेकिन यदि diamond अपग्रेडेबल है, तो implementation contracts को बदलने के लिए एक पांचवें state-changing function की आवश्यकता होती है। Diamond pattern केवल एक सिंगल इवेंट को अनिवार्य करता है (भले ही कॉन्ट्रैक्ट अपग्रेडेबल हो या नहीं)। छोटे स्पेसिफिकेशन के बावजूद, उन चार (या पांच) functions को इम्प्लीमेंट (implement) करना अन्य proxy patterns की तुलना में काफी अधिक जटिल है।
हालांकि, सही पूर्व-आवश्यकताओं (prerequisites) के साथ (जो कि काफी महत्वपूर्ण हैं!), diamond pattern की संकल्पना को समझना विशेष रूप से कठिन नहीं है। हम यह मानकर चलते हैं कि पाठक हमारी Proxy Patterns Book के पहले तेरह अध्यायों में शामिल विषयों से परिचित हैं। यदि आपने उन अध्यायों को नहीं पढ़ा है या आप पहले से उन विषयों से परिचित नहीं हैं, तो diamond pattern सीखना थोड़ा संघर्षपूर्ण होगा, इसलिए सुनिश्चित करें कि पूर्व-आवश्यकताएं पूरी हों।
यहां कुछ मुद्दे दिए गए हैं जिनसे इस पैटर्न को निपटने की आवश्यकता होती है:
- जब diamond को कोई ट्रांजेक्शन प्राप्त होता है, तो उसे कैसे पता चलता है कि किस implementation contract को कॉल करना है?
- यदि कोई facet (implementation contract) अपग्रेड किया जाता है, तो proxy contract (diamond) को कैसे पता चलेगा कि नया facet (implementation contract) किस function का समर्थन करता है, और संभावित रूप से, किन functions का समर्थन अब नहीं किया जाता है?
- अपग्रेड लॉजिक कहां होना चाहिए — proxy bytecode में या facet में?
- चूंकि प्रत्येक implementation contract सीधे अन्य इम्प्लीमेंटेशन्स के बारे में नहीं जानता है, तो storage collisions से कैसे बचा जा सकता है?
- एक बाहरी एक्टर को कैसे पता चलेगा कि diamond proxy द्वारा किन functions का समर्थन किया जाता है? एक interface को इनहेरिट (inherit) करना पर्याप्त नहीं है क्योंकि अपग्रेड के दौरान functions बदल सकते हैं।
- क्या होगा यदि एक implementation contract के अंदर का function किसी अन्य implementation contract के function को कॉल करना चाहता है — यह ट्रांजेक्शन कैसे सुगम बनाया जाएगा?
इस लेख में, हम दिखाएंगे कि उपरोक्त सभी समस्याओं का समाधान कैसे किया जाए। ध्यान दें कि ERC-2535 के लिए यह आवश्यक नहीं है कि एक diamond proxy अपग्रेडेबल हो। Proxy में हार्डकोडेड (hardcoded) implementation contracts हो सकते हैं और फिर भी वह एक वैध diamond हो सकता है।
शुरुआत में चीजों को सरल रखने के लिए, हम immutable diamond को दिखाकर शुरू करते हैं।
Immutable diamond
एक immutable diamond, जिसे static diamond या single cut diamond भी कहा जाता है, कई implementation contracts वाला एक proxy contract होता है — और किसी भी implementation contract को अपग्रेड नहीं किया जा सकता है। (अपग्रेड कार्यक्षमता को हटाकर एक अपग्रेडेबल diamond का immutable बनना संभव है, लेकिन हम अपग्रेडेबल diamonds पर बाद में चर्चा करेंगे)।
Proxy contract के रूप में diamond pattern
यदि UUPS और Transparent Upgradeable Proxy द्वारा उपयोग किए जाने वाले OpenZeppelin Proxy से तुलना की जाए, तो diamond proxy के proxy हिस्से का कोड परिचित लगना चाहिए:
// Find facet for function that is called and execute the
// function if a facet is found and return any value.
fallback() external payable {
// get facet from function selector
address facet = facetAddress(msg.sig);
require(facet != address(0));
// The code below is the same as OpenZeppelin Proxy.sol
// Execute external function from facet using delegatecall and return any value.
assembly {
// copy function selector and any arguments
calldatacopy(0, 0, calldatasize())
// execute function call using the facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// get any return value
returndatacopy(0, 0, returndatasize())
// return any return value or error back to the caller
switch result
case 0 {revert(0, returndatasize())}
default {return (0, returndatasize())}
}
}
उपरोक्त कोड से एक पारंपरिक proxy में एकमात्र महत्वपूर्ण अंतर ये दो पंक्तियाँ हैं:
address facet = facetAddress(msg.sig);
require(facet != address(0));
उपरोक्त कोड msg.sig का उपयोग करके ट्रांजेक्शन के पहले चार बाइट्स (function selector) प्राप्त करता है, फिर उस facet का एड्रेस निर्धारित करने के लिए facetAddress() का उपयोग करता है जो function को इम्प्लीमेंट करता है (ध्यान दें कि facetAddress() proxy लॉजिक का हिस्सा हो सकता है या किसी अन्य facet के अंदर रह सकता है और delegatecalled किया जा सकता है — इस पर बाद में चर्चा की जाएगी)। इसके बाद proxy उसी calldata के साथ उस एड्रेस को delegatecalls करता है जो उसे प्राप्त हुआ था।

EIP-2535 यह निर्दिष्ट नहीं करता है कि implementation contracts के एड्रेस पर function selectors को कैसे मैप किया जाए। Static diamond के लिए, एक उचित समाधान इस संबंध को हार्डकोड (hardcode) करना है। यही वह दृष्टिकोण होगा जिसे हम अगले भाग में अपनाएंगे।
अपग्रेडेबल diamond के लिए, सिलेक्टर्स को हार्डकोड करना स्पष्ट रूप से कोई विकल्प नहीं है, और हमें mappings पर निर्भर रहना होगा, जैसा कि हम संबंधित अनुभागों में देखेंगे।
Function Selector पर Conditional Branching
नीचे हम दो implementation contracts (facets) के साथ एक proxy contract दिखाते हैं:
- पहला implementation contract
Addएक सिंगल public functionadd()एक्सपोज़ करता है जो अपने आर्गुमेंट्स का योग (sum) लौटाता है। - दूसरा implementation contract
Multiplyदो public functionsmultiply()औरexponent()एक्सपोज़ करता है जो वही करते हैं जो उनके नाम से पता चलता है।
नीचे दिया गया कोड दो implementation contracts का उपयोग करके एक सिंगल proxy दिखाता है। Diamond में facetAddress() function msg.sig लेता है और उस facet का एड्रेस लौटाता है जो उस सिग्नेचर (signature) के साथ function को इम्प्लीमेंट करता है, यदि कोई हो। नीचे दिया गया कोड अभी तक diamond standard के अनुरूप नहीं है, लेकिन हम कोड को कंप्लायंट (compliant) बनाने के लिए विकसित करेंगे।
// first implementation contract
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
// second implementation contract
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 0x2f8cd8b1
function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x ** y;
}
}
// proxy contract
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
fallback() external payable {
address facet = facetAddress(msg.sig);
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()) }
}
}
}
EIP-2535 standard किसी ऐसे function को कॉल करने के लिए error message निर्दिष्ट नहीं करता है जो मौजूद नहीं है। हमारे मामले में, हम "Function does not exist" स्ट्रिंग के साथ revert करते हैं, लेकिन एक custom error अधिक gas efficient होगा।
सरलता के लिए, हमारा diamond constructor में दो facets को डिप्लॉय करता है, लेकिन व्यवहार में ऐसा नहीं किया जाता है। व्यवहार में, facets को अलग-अलग डिप्लॉय किया जाता है, और proxy को बाद में किसी तरीके से उनके बारे में “सूचित” किया जाता है, जैसे कि constructor arguments या एक अलग function के माध्यम से।
कुछ diamonds, facets को contracts के बजाय external functions के साथ libraries के रूप में इम्प्लीमेंट करते हैं, लेकिन EIP-2535 के लिए यह आवश्यक नहीं है कि facets, libraries या contracts ही हों।
सरलता के लिए, हम function selector को facet address से मैच करने के लिए else-if स्टेटमेंट्स की एक श्रृंखला का उपयोग करते हैं, लेकिन यदि बहुत सारे विकल्प हैं तो यह gas-efficient नहीं है। एक static diamond के लिए, सिलेक्टर्स को पहले से सॉर्ट (sort) करना और फिर binary search करना अधिक कुशल है — लेकिन हम इस पर बाद में चर्चा करेंगे।
Corner case — Ether transfers
जब ether ट्रांसफर किया जाता है, तो कोई calldata नहीं होगा। इस स्थिति में, msg.sig 0x00000000 लौटाएगा और यह किसी facet address पर मैप नहीं होगा। यह ठीक है यदि कॉन्ट्रैक्ट का इरादा ether प्राप्त करने का नहीं है। हालांकि, यदि कॉन्ट्रैक्ट से ether प्राप्त करने की उम्मीद है, या आने वाले ether पर प्रतिक्रिया देने की उम्मीद है, तो 0x00000000 को इच्छित लॉजिक वाले function के साथ, या कम से कम एक ऐसे function के साथ मैप किया जाना चाहिए जो revert न करे। ध्यान रखें कि एक हमलावर (attacker) कोई calldata न भेजकर या calldata के रूप में 0x00000000 भेजकर इस function को ट्रिगर कर सकता है, इसलिए लॉजिक को दोनों परिदृश्यों (scenarios) को सुचारू रूप से संभालने की आवश्यकता है।
हमारे कॉन्ट्रैक्ट को EIP-2535 कंप्लायंट बनाने के लिए, हमें चार अनिवार्य public view functions को इम्प्लीमेंट करना होगा, जिनमें से प्रत्येक पर नीचे चर्चा की गई है।
चार अनिवार्य Public View Functions
1/4 facetAddress()
ध्यान दें कि facetAddress() public है — EIP-2535 की आवश्यकता है कि एक Diamond proxy इस सिग्नेचर के साथ एक public function एक्सपोज़ करे:
function facetAddress(bytes4 selector) external view returns (address);
हम उस संबंध में पहले से ही कंप्लायंट हैं।
2/4 facetAddresses()
facetAddress(bytes4 selector) के अलावा, EIP-2535 facetAddresses() (बहुवचन) नामक एक function को अनिवार्य करता है जो diamond द्वारा उपयोग किए जाने वाले सभी facets को लौटाता है — दूसरे शब्दों में, एड्रेस की एक सूची (list of addresses)। सिग्नेचर इस प्रकार है:
function facetAddresses() public view returns (address[] memory addresses);
चूंकि हमारे diamond पर facets नहीं बदल सकते हैं, इसलिए हम बस facet addresses की सूची को हार्डकोड करते हैं:
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
function facetAddresses() public view returns (address[2] memory addresses) {
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
}
// --------
fallback() external payable {
address facet = facetAddress(msg.sig);
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()) }
}
}
}
facetAddresses() द्वारा लौटाई गई एड्रेस की सूची (facet addresses) पर कोई अनिवार्य क्रम (ordering) लागू नहीं है।
3/4 facetFunctionSelectors()
एक facet address को आर्गुमेंट के रूप में दिए जाने पर, facetFunctionSelectors() facet के सभी public functions के सिलेक्टर्स लौटाता है। इसका सिग्नेचर निम्नलिखित है:
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
हम इसे इस प्रकार इम्प्लीमेंट कर सकते हैं:
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
function facetAddresses() public view returns (address[] memory addresses) {
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
if (_facet == ADD_ADDR) {
bytes4[] memory facetFunctionSelectors_ = new bytes4[](1);
facetFunctionSelectors_[0] = 0x771602f7;
return facetFunctionSelectors_;
}
else if (_facet == MULTIPLY_ADDR) {
bytes4[] memory facetFunctionSelectors_ = new bytes4[](2);
facetFunctionSelectors_[0] = 0x165c4a16;
facetFunctionSelectors_[1] = 0x2f8cd8b1;
return facetFunctionSelectors_;
}
else {
bytes4[] memory facetFunctionSelectors_ = new bytes4[](0);
return facetFunctionSelectors_;
}
}
// --------
fallback() external payable {
address facet = facetAddress(msg.sig);
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()) }
}
}
}
4/4 facets()
अंत में, EIP-2535 एक function facets() को अनिवार्य करता है जो कोई आर्गुमेंट्स नहीं लेता है और structs की एक सूची लौटाता है जहां प्रत्येक struct में एक facet address और उस facet के function selectors की एक सूची होती है।
इसका सिग्नेचर निम्नलिखित है:
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() external view returns (Facet[] memory facets_);
facets() जो जानकारी लौटाता है, उसे निम्न प्रकार से प्राप्त किया जा सकता है:
- पहले सभी facet addresses प्राप्त करने के लिए
facetAddresses()को कॉल करके- प्रत्येक facet address पर लूप (loop) चलाकर, और उस एड्रेस को
Facetstruct केfacetAddressफील्ड में रखकर- और एड्रेस पर
facetFunctionSelectors()को कॉल करके और struct केfunctionSelectorsफील्ड में function selectors की सूची रखकर।
- और एड्रेस पर
- प्रत्येक facet address पर लूप (loop) चलाकर, और उस एड्रेस को
एक विकल्प यह है कि उत्तर को सीधे function में हार्डकोड किया जाए, जो कुछ मामलों में अधिक कुशल हो सकता है (अपग्रेडेबल diamonds के लिए, हार्डकोडिंग स्पष्ट रूप से काम नहीं करेगी)। हमारे diamond में, हम नीचे दिखाए गए अनुसार loop विधि का उपयोग करके facets() को इम्प्लीमेंट करेंगे। नया कोड देखने के लिए इस कोड ब्लॉक के नीचे स्क्रॉल करें:
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 0x2f8cd8b1
function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x ** y;
}
}
contract Diamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
function facetAddresses() public view returns (address[] memory) {
address[] memory addresses = new address[](2);
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
return addresses;
}
function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
if (_facet == ADD_ADDR) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = 0x771602f7;
return selectors;
}
else if (_facet == MULTIPLY_ADDR) {
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = 0x165c4a16;
selectors[1] = 0x2f8cd8b1;
return selectors;
}
// Return empty array for unknown facets
return new bytes4[](0);
}
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() public view returns (Facet[] memory) {
address[] memory fa = facetAddresses();
Facet[] memory _facets = new Facet[](2);
for (uint256 i = 0; i < fa.length; i++) {
_facets[i].facetAddress = fa[i];
_facets[i].functionSelectors = facetFunctionSelectors(fa[i]);
}
return _facets;
}
// --------
fallback() external payable {
address facet = facetAddress(msg.sig);
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()) }
}
}
}
इन चार public functions को इम्प्लीमेंट करने के बाद, अब हमने एक Diamond proxy के सभी अनिवार्य external functions को इम्प्लीमेंट कर लिया है।
IDiamondLoupe
सामूहिक रूप से, इन चार functions को IDiamondLoupe नामक एक interface में परिभाषित किया गया है। सभी diamonds को IDiamondLoupe इम्प्लीमेंट करना चाहिए। कोई भी यह याद रख सकता है कि “loupe” हीरे (diamonds) को देखने (“viewing”) के लिए एक छोटा आवर्धक लेंस (magnifying glass) होता है, और इनमें से प्रत्येक function एक view function है।
interface IDiamondLoupe {
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
/// @notice Gets all facet addresses and their four byte function selectors.
/// @return facets_ Facet
function facets() external view returns (Facet[] memory facets_);
/// @notice Gets all the function selectors supported by a specific facet.
/// @param _facet The facet address.
/// @return facetFunctionSelectors_
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
/// @notice Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses() external view returns (address[] memory facetAddresses_);
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}
हम अभी भी पूरी तरह से कंप्लायंट Diamond Proxy से एक कदम दूर हैं: diamond में किए गए बदलावों को लॉग (log) करना। यह वह विषय है जिस पर हम आगे चर्चा करेंगे।
DiamondCut — facets selectors को लॉग करना
हमारा चल रहा diamond का उदाहरण अपग्रेडेबल नहीं है। व्यवहार में Diamonds अपग्रेडेबल होते हैं और अपने facets और उनसे जुड़े function selectors को बदल सकते हैं।
किसी भी facet में हुए बदलाव को लॉग किया जाना चाहिए — यहां तक कि non-upgradeable (static) diamonds के लिए भी, बदलाव (facets और functions selectors का जुड़ना) डिप्लॉयमेंट (deployment) के समय होते हैं और इसलिए उन्हें लॉग करने की आवश्यकता होती है।
facet selectors को लॉग करने के पीछे का सिद्धांत यह है कि diamond द्वारा समर्थित function selectors को निर्धारित करने के दो तरीके होने चाहिए:
- ऊपर वर्णित public functions का उपयोग करके, या
- Logs को पार्स (Parse) करके।
यहां तक कि हमारे जैसे हार्डकोडेड facets को भी लॉग किया जाना चाहिए, इसलिए events को एमिट (emit) किया जाना चाहिए। हमारे मामले में, एमिशन (emission) डिप्लॉयमेंट के दौरान होना चाहिए। स्टैंडर्ड (standard) द्वारा ऐसे logs की आवश्यकता होती है।
जब हम कोई facet जोड़ते हैं (या कोई बदलाव करते हैं जैसे कि रिप्लेस करना या हटाना), तो उस क्रिया को diamond cut कहा जाता है।
Diamond cut का अर्थ facets को हटाना नहीं है, जैसा कि “cutting” से प्रतीत हो सकता है। यह किसी तरह से diamond को बदलने से मेल खाता है। facet में किसी भी बदलाव के लिए एक DiamondCut इवेंट को एमिट करने की आवश्यकता होती है जिसे आने वाले कोड ब्लॉक में परिभाषित किया गया है। (कोई भी “cut” के लिए इस नामकरण को यह ध्यान में रखकर याद रख सकता है कि जब एक वास्तविक हीरे के रत्न को “कट” किया जाता है तो उसे एक अतिरिक्त किनारा — या facet — मिलता है जहां कट हुआ था)।
DiamondCut इवेंट को IDiamond नामक एक नए interface में परिभाषित किया गया है। हर बार जब किसी facet से कोई function जोड़ा जाता है, रिप्लेस किया जाता है या हटाया जाता है, तो Events एमिट किए जाने चाहिए। DiamondCut इवेंट की परिभाषा नीचे दिखाई गई है:
interface IDiamond {
enum FacetCutAction {Add, Replace, Remove}
// Add=0, Replace=1, Remove=2
struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}
चूंकि FacetCut[] एक सूची (list) है, इसलिए एक साथ कई facets को बदला जा सकता है।
Transparent Upgradeable Proxies या UUPS Proxies जैसे अन्य proxy patterns के विपरीत, diamonds में facet address को बदलकर अपग्रेड करने का कोई तंत्र नहीं होता है। एक facet (implementation contract) को तब हटा दिया जाता है जब उससे जुड़े सभी function selectors हटा दिए जाते हैं। जब नए implementation address के साथ कोई function selector जोड़ा जाता है तो Facets अंतर्निहित रूप से (implicitly) जुड़ जाते हैं।
_init और _calldata पैरामीटर्स का उद्देश्य वही है जो OpenZeppelin Initializers का है — हम बाद में इस पर अधिक चर्चा करेंगे। यदि कोई इनीशियलाइजेशन डेटा (initialization data) आवश्यक नहीं है, तो _init को address(0) और _calldata को "" या empty bytes होना चाहिए।
आइए अपने diamond को constructor में इस इवेंट को एमिट करने के लिए अपडेट करें:
contract Add {
// selector: 0x771602f7
function add(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x + y;
}
}
contract Multiply {
// selector: 0x165c4a16
function multiply(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x * y;
}
// selector: 0x2f8cd8b1
function exponent(uint256 x, uint256 y) external pure returns (uint256 z) {
z = x ** y;
}
}
interface IDiamond {
enum FacetCutAction {Add, Replace, Remove}
// Add=0, Replace=1, Remove=2
struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}
contract Diamond is IDiamond {
address immutable ADD_ADDR;
address immutable MULTIPLY_ADDR;
constructor() {
ADD_ADDR = address(new Add());
MULTIPLY_ADDR = address(new Multiply());
// ┌────────────────────┐
// │ │
// │ THIS CODE IS NEW │
// │ │
// └────────────────────┘
// there are a total of 3 facets:
// [add, [add]]
// [multiply, [multiply, exponent]]
// [this, [facets, facetAddress, facetAddresses, facetFunctionSelectors]]
FacetCut[] memory _diamondCuts = new FacetCut[](3);
enum FacetCutAction {Add, Replace, Remove}
_diamondCuts[0].facetAddress = ADD_ADDR;
bytes4[] memory _addFacets = new bytes4[](1);
_addFacets[0] = 0x771602f7;
// add to _diamondCuts
_diamondCuts[0].action = Add;
_diamondCuts[0].functionSelectors = _addFacets;
_diamondCuts[1].facetAddress = MULTIPLY_ADDR;
bytes4[] memory _mulFacets = new bytes4[](2);
_mulFacets[0] = 0x165c4a16;
_mulFacets[1] = 0x2f8cd8b1;
// add to _diamondCuts
_diamondCuts[1].action = Add;
_diamondCuts[1].functionSelectors = _mulFacets;
// Note that the IDiamondLoupe interface functions are also logged.
_diamondCuts[2].facetAddress = address(this);
bytes4[] memory _loupeFacets = new bytes4[](4);
_loupeFacets[0] = this.facetAddress.selector;
_loupeFacets[1] = this.facetAddresses.selector;
_loupeFacets[2] = this.facets.selector;
_loupeFacets[3] = this.facetFunctionSelectors.selector;
// add to _diamondCuts
_diamondCuts[2].action = Add;
_diamondCuts[2].functionSelectors = _loupeFacets;
emit DiamondCut(_diamondCuts, address(0), "");
// --------
}
function facetAddress(bytes4 selector) public view returns (address) {
if (selector == 0x771602f7) return ADD_ADDR;
else if (selector == 0x165c4a16) return MULTIPLY_ADDR;
else if (selector == 0x2f8cd8b1) return MULTIPLY_ADDR;
else return address(0);
}
function facetAddresses() public view returns (address[] memory) {
address[] memory addresses = new address[](2);
addresses[0] = ADD_ADDR;
addresses[1] = MULTIPLY_ADDR;
return addresses;
}
function facetFunctionSelectors(address _facet) public view returns (bytes4[] memory) {
if (_facet == ADD_ADDR) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = 0x771602f7;
return selectors;
}
else if (_facet == MULTIPLY_ADDR) {
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = 0x165c4a16;
selectors[1] = 0x2f8cd8b1;
return selectors;
}
// Return empty array for unknown facets
return new bytes4[](0);
}
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() public view returns (Facet[] memory) {
address[] memory fa = facetAddresses();
Facet[] memory _facets = new Facet[](2);
for (uint256 i = 0; i < fa.length; i++) {
_facets[i].facetAddress = fa[i];
_facets[i].functionSelectors = facetFunctionSelectors(fa[i]);
}
return _facets;
}
fallback() external payable {
address facet = facetAddress(msg.sig);
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()) }
}
}
receive() external payable {}
}
अब हमारे पास पूरी तरह से कंप्लायंट EIP-2535 Diamond proxy है। सामान्य तौर पर, IDiamondLoupe functions को एक अलग facet में स्टोर किया जाता है, लेकिन सरलता के लिए हमने अभी चीजों को आसान रखने के लिए उन्हें diamond में ही रखा है।
इस बात की परवाह किए बिना कि IDiamondLoupe के functions कहां स्टोर किए गए हैं — चाहे वह diamond में ही हों या किसी अन्य facet में, मानक (standard) को उनके एड्रेस, हुई कार्रवाई (facet को जोड़ना), और function selectors को एमिट करने की आवश्यकता होती है।
IDiamondLoupe और DiamondCut के events के बीच डुप्लीकेशन (Duplication)
इस EIP के विवादास्पद पहलुओं में से एक यह तथ्य है कि function selectors को पिछले logs को पार्स करके और IDiamondLoupe में view functions को कॉल करके दोनों तरह से निर्धारित किया जा सकता है। यह एक ही काम को पूरा करने के लिए डुप्लीकेट लॉजिक बनाता है।
Public functions के माध्यम से समान डेटा को एक्सपोज़ करने के पीछे तर्क यह है कि यह ब्लॉक एक्सप्लोरर (block explorers) और अन्य बाहरी प्रणालियों के साथ एकीकरण (integration) को आसान बनाता है। इसके अतिरिक्त, अपग्रेड स्क्रिप्ट्स नए को रजिस्टर करने से पहले यह जांच सकती हैं कि क्या कोई function selector पहले से मौजूद है। DiamondCut events के पीछे का इरादा अपग्रेड के इतिहास को दिखाना है।
ERC-1967 में, एक ब्लॉक एक्सप्लोरर storage slot को क्वेरी (query) कर सकता और तुरंत पहचान सकता है कि लॉजिक कॉन्ट्रैक्ट कहां है — ब्लॉक एक्सप्लोरर को ERC-1967 द्वारा एमिट किए गए logs को पार्स करने की आवश्यकता नहीं होती है, जिनमें समान जानकारी होती है।
Diamond को अपग्रेडेबल बनाना, diamondCut Function को इम्प्लीमेंट करना
EIP-2535 standard नीचे दिखाए गए diamondCut() function को जोड़ने का सुझाव देता है जिसके द्वारा facets को जोड़ा, बदला या हटाया जा सकता है।
interface IDiamondCut is IDiamond {
/// @notice Add/replace/remove any number of functions and optionally execute
/// a function with delegatecall
/// @param _diamondCut Contains the facet addresses and function selectors
/// @param _init The address of the contract or facet to execute _calldata
/// @param _calldata A function call, including function selector and arguments
/// _calldata is executed with delegatecall on _init
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
}
मानक (standard) यह आवश्यक नहीं करता है कि अपग्रेड function को diamondCut() ही कहा जाए या उपरोक्त सिग्नेचर को इम्प्लीमेंट किया जाए। एक डेवलपर उदाहरण के लिए अपने स्वयं के function changeTheFacets() का उपयोग कर सकता है — लेकिन उस function को उसके द्वारा किए गए facet अपडेट के प्रकार के अनुसार DiamondCut इवेंट को एमिट करना चाहिए।
Facets और selectors के लिए Data structures
Function selectors और facets को स्टोर करने के लिए, हमें कम से कम function selectors से facet address तक एक मैपिंग (mapping) की आवश्यकता होती है:
mapping(bytes4 => address) facetToSelectors;
यह data structure हमें निम्न करने में सक्षम बनाता है:
- प्राप्त
msg.sigके आधार पर यह पता लगाना कि किस implementation contract को delegatecall करना है - यह निर्धारित करना कि क्या पहले से मौजूद function selector से टकराए (colliding) बिना एक नया function selector जोड़ा जा सकता है। हमें
facetToSelectors[selector] == address(0)की आवश्यकता होनी चाहिए - यह निर्धारित करना कि क्या किसी सिलेक्टर को रिप्लेस किया जा सकता है या हटाया जा सकता है। इसी जांच का उपयोग करके, हम यह निर्धारित कर सकते हैं कि क्या हम किसी गैर-मौजूद सिलेक्टर को रिप्लेस कर रहे हैं या हटा रहे हैं (यह ऑपरेशन revert होना चाहिए)।
IDiamondLoupe के लिए functions को इम्प्लीमेंट करना
संक्षेप में, यहाँ IDiamondLoupe में view functions दिए गए हैं जिनका हमें समर्थन करने की आवश्यकता है:
facetAddress(bytes4 selector)दिए गए function selector के आधार पर facet लौटानाfacetAddresses()सभी facet addresses लौटानाfacetFunctionSelectors(address facet)दिए गए एड्रेस के आधार पर सभी function selectors लौटानाfacets()सभी facet addresses और उनके function selectors लौटाना
यहाँ बताया गया है कि हम प्रत्येक को कैसे इम्प्लीमेंट करते हैं:
facetAddress(bytes4 selector)function केवलfacetToSelectorsमैपिंग के लिए एक view function हो सकता है — या हम मैपिंग को public बना सकते हैं।facetAddresses()के लिए सभी एड्रेस लौटाने के लिए, हमें यह करना होगा:- स्पष्ट रूप से (Explicitly) सभी एड्रेस को एक सूची में स्टोर करें
- स्पष्ट रूप से सभी function selectors को एक सूची में स्टोर करें, function selectors पर लूप चलाएं, और प्रत्येक पर
facetAddress(bytes4 selector)कॉल करें, फिर यूनिक एड्रेस (unique addresses) की एक सूची बनाएं।
facetFunctionSelectors(address facet)के माध्यम से एक facet के सभी function selectors लौटाने के लिए हमें यह करना होगा:- एक मैपिंग
mapping(address facet => bytes4[])बनाएं जो प्रत्येक मैपिंग से जुड़े function selectors की सूची को स्टोर करती है - सभी function selectors की एक array बनाए रखें, और उस सभी array पर लूप चलाएं और प्रत्येक पर
facetAddress(bytes4 selector)कॉल करें। एड्रेस को सूची में जोड़ें यदि लौटाया गयाfacetfacetFunctionSelectorsके आर्गुमेंट में दिया गयाfacetहै।
- एक मैपिंग
- चूंकि
facets()वही जानकारी लौटाता है जोfacetAddresses()पर लूप चलाने और प्रत्येक एड्रेस परfacetFunctionSelectors(address facet)को कॉल करने पर मिलती है, इसलिए हमfacets()को इम्प्लीमेंट करने की आगे की चर्चा को छोड़ देते हैं।
इसमें एक बुनियादी ट्रेडऑफ (tradeoff) है। यदि हम अधिक data structures का उपयोग करते हैं, तो view functions को कॉल करना सस्ता होगा क्योंकि उन्हें इन functions द्वारा लौटाए गए डेटा को “reconstruct” नहीं करना पड़ता है, लेकिन अपग्रेड के दौरान अधिक data structures को अपडेट करने की आवश्यकता होती है। इसलिए हमारे पास निम्न में से एक होना चाहिए:
- अपग्रेड सस्ता है, लेकिन view functions को ऑन-चेन (on-chain) कॉल करना अधिक महंगा है।
- View function को कॉल करना सस्ता है, लेकिन किसी facet को अपग्रेड करने के लिए अधिक बहीखाता (bookkeeping) की आवश्यकता होती है (गैस की लागत बढ़ जाती है)।
किसी भी IDiamondLoupe view functions को ऑन-चेन कॉल करना अत्यंत असामान्य है क्योंकि वे ऑफ-चेन (off-chain) उपयोग के लिए होते हैं। इसलिए, कम data structures का विकल्प चुनना बेहतर है।
Facets और selectors के लिए Storage variables — और storage collisions से बचना
जैसा कि देखा गया है, सिलेक्टर और एड्रेस की जानकारी स्टोर करने के लिए, हमें कम से कम एक मैपिंग की आवश्यकता है जैसे:
mapping(bytes4 => address) facetToAddress;
diamond में, लेकिन फिर पहले storage slot में असाइन की गई किसी भी facet के अंदर की कोई भी मैपिंग संभावित रूप से इस मैपिंग से टकराएगी (collide)।
Diamond proxy में Storage collisions अन्य proxy patterns की तुलना में अधिक जटिल होते हैं, क्योंकि collisions केवल एक ही implementation contract के अपग्रेड में नहीं हो सकते हैं, बल्कि facets के बीच भी हो सकते हैं।
Diamond pattern यह निर्दिष्ट नहीं करता है कि स्टोरेज को कैसे प्रबंधित (manage) किया जाना चाहिए। Collisions को संभालने का एक सीधा तरीका storage namespaces का उपयोग करना है। Namespaces के उपयोग का विस्तृत विवरण EIP-7201 storage namespaces में पाया जा सकता है।
एक समीक्षा के रूप में, storage namespaces में, कॉन्ट्रैक्ट के state variables को एक struct में समूहीकृत (grouped) किया जाता है, और इस struct का आधार (base) एक स्यूडोरैंडम स्लॉट (pseudorandom slot) में स्टोर किया जाता है, जो आमतौर पर एक स्ट्रिंग के हैश (hash) द्वारा निर्धारित होता है। परिणामस्वरूप, प्रत्येक कॉन्ट्रैक्ट का अपना बेस स्टोरेज स्लॉट (base storage slot) होता है, जिससे storage collisions की संभावना बहुत कम हो जाती है।
EIP-7201 को diamond pattern द्वारा प्रस्तावित एक पुराने समाधान से लिया गया था जिसे “diamond storage” कहा जाता था। EIP-2535 ने “App Storage” नामक एक अन्य पैटर्न का भी प्रस्ताव दिया। हालाँकि, EIP-2535 यह निर्देशित नहीं करता है कि स्टोरेज को कैसे प्रबंधित किया जाना चाहिए, इसलिए हम केवल पाठक को एक व्यावहारिक समाधान का संदर्भ देते हैं, जो कि EIP-7201 का उपयोग करना है। इच्छुक पाठक सीधे EIP से “diamond storage” और “app storage” पैटर्न के बारे में जान सकते हैं — ये दोनों EIP लेखक द्वारा अनुशंसित हैं।
Diamond को संचालित करने के लिए न्यूनतम स्टोरेज (minimum storage)
यदि हमने diamond को संचालित करने के लिए आवश्यक न्यूनतम स्टोरेज रखने का विकल्प चुना है, तो proxy के लिए namespace निम्नलिखित फ़ील्ड्स के साथ एक struct होना चाहिए:
- हमें सिलेक्टर्स की एक सूची चाहिए, यानी
bytes4[] selectors। हर बार जब हम एक सिलेक्टर जोड़ते हैं, तो हम यह सुनिश्चित करने के लिए इस सूची को स्कैन करते हैं कि हम पहले से मौजूद सिलेक्टर नहीं जोड़ रहे हैं। - हमें कम से कम सिलेक्टर्स से एड्रेस तक की एक मैपिंग की आवश्यकता है। हालाँकि सिलेक्टर्स को
selectorsमें उनके इंडेक्स (index) पर मैप करना भी मददगार होगा। इस तरह, जब हम एक सिलेक्टर को हटाते हैं, तो हम ऐरे (array) में उसका इंडेक्स जल्दी से देख सकते हैं। फिर, हम उस प्रविष्टि (entry) को अंतिम प्रविष्टि के साथ स्वैप (swap) करते हैं और सूची को पॉप (pop) करते हैं। इसलिएselector => addressस्टोर करने के बजाय हम एक struct स्टोर करते हैं जो ऐरे में एड्रेस और सिलेक्टर की स्थिति रखता है। इसलिए, हमारी मैपिंगselector => (address, index_in_selectors)रखती है।
नीचे दिया गया कोड उपरोक्त दो बिंदुओं को इम्प्लीमेंट करता है:
selectorsकेवल सिलेक्टर्स की सूची है- struct
FacetAddressAndSelectorPositionfacetAddress औरselectorsमें सिलेक्टर का इंडेक्स कहां है, यह स्टोर करता है
struct FacetAddressAndSelectorPosition {
address facetAddress;
uint16 selectorPosition; // index of the selector in `selectors`
}
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
}
EIP-7201 के समान पैटर्न का उपयोग करके struct को एक्सेस किया जा सकता है।
स्टोरेज प्रबंधित करने के लिए LibDiamond
चीजों को सरल रखने के लिए, उपरोक्त जानकारी रखने वाला struct और struct में स्टोरेज पॉइंटर सेट करने वाला function LibDiamond नामक एक अलग लाइब्रेरी में रखा जा सकता है। यह लाइब्रेरी एक function diamondStorage() प्रदान करती है जो struct के लिए एक पॉइंटर और facetAddress(bytes4 selector) लौटाता है। इस लाइब्रेरी में facetAddress को परिभाषित करना वैकल्पिक है और पूरी तरह से सुविधा के लिए है।
// ┌────────────────────┐
// │ │
// │ CODE FOR STORAGE │
// │ │
// └────────────────────┘
library LibDiamond {
// keccak256(abi.encode(uint256(keccak256("diamond.storage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 constant DIAMOND_STORAGE_POSITION = 0xd7ce2c87e6a71bef91a0dfa43113050aa4eae7c1a7c451ae61d9077904d7cd00;
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
}
function diamondStorage()
internal
pure
returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
// change the slot of the storage pointer
ds.slot := position
}
}
function facetAddress(bytes4 _functionSelector)
external
override
view
returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
}
}
अपग्रेडेबल diamonds के लिए, IDiamondLoupe functions को उनके स्वयं के facet में रखना पारंपरिक है, न कि स्वयं proxy में। इस कॉन्ट्रैक्ट का नाम क्या रखा जाए, इसकी कोई आवश्यकता नहीं है, लेकिन DiamondLoupeFacet उचित रूप से वर्णनात्मक (descriptive) है। नीचे हम LibDiamond लाइब्रेरी का उपयोग करके DiamondLoupeFacet दिखाते हैं जो IDiamondLoupe का हिस्सा है और बाहरी facetAddress function को इम्प्लीमेंट करता है।
import { LibDiamond } from "./libraries/LibDiamond.sol";
// ┌─────────────────────┐
// │ │
// │ DiamondLoupeFacet │
// │ │
// └─────────────────────┘
contract DiamondLoupeFacet is IDiamondLoupe {
/// @notice Gets the facet address that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector)
external
override
view
returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.facetAddressAndSelectorPosition[_functionSelector].facetAddress;
}
// other functions not shown
}
Diamond Standard के Reference Implementations
EIP-2535 के लेखक निक मडज (Nick Mudge), निम्नलिखित रेपो (repo) में तीन reference implementations (diamond-1, diamond-2, और diamond-3) मेंटेन करते हैं:
https://github.com/mudgen/diamond
ये इम्प्लीमेंटेशन्स उन ट्रेडऑफ़ (tradeoffs) के लिए ऑप्टिमाइज़ करते हैं जिन पर हमने पहले चर्चा की थी: यदि IDiamondLoupe में view functions को ऑन-चेन क्वेरी करना सस्ता है, तो उन्हें अपडेट करना महंगा होगा और इसके विपरीत।
Diamond-1 और diamond-2 facets और सिलेक्टर्स को ट्रैक करने के लिए यथासंभव कम स्टोरेज का उपयोग करते हैं, केवल function selectors की एक सूची और function selector से facet address तक एक मैपिंग का उपयोग करते हैं। नीचे हम diamond-1 के लिए स्टोरेज देखते हैं।
ध्यान दें कि reference implementation 8 function selectors को एक स्लॉट में पैक करने के लिए function selectors के ऐरे को uint256 => bytes32 सिलेक्टर स्लॉट से मैपिंग के रूप में इम्प्लीमेंट करता है। Mappings, arrays की तुलना में थोड़ी अधिक गैस कुशल (gas efficient) होती हैं क्योंकि वे लुकअप (lookup) करने से पहले अंतर्निहित रूप से (implicitly) ऐरे की लंबाई की जांच नहीं करती हैं। इस “array” की लंबाई अलग से selectorCount के रूप में स्टोर की जाती है।
struct DiamondStorage {
// function selector => facet address and selector position in selectors array
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
mapping(bytes4 => bool) supportedInterfaces;
// owner of the contract
address contractOwner;
}
दूसरी ओर Diamond-3 स्पष्ट रूप से facet addresses और facet address से इसके द्वारा स्टोर किए गए function selectors की सूची तक एक मैपिंग को स्टोर करता है:
struct FacetAddressAndPosition {
address facetAddress;
uint96 functionSelectorPosition; // position in facetFunctionSelectors.functionSelectors array
}
struct FacetFunctionSelectors {
bytes4[] functionSelectors;
uint256 facetAddressPosition; // position of facetAddress in facetAddresses array
}
struct DiamondStorage {
// maps function selector to the facet address and
// the position of the selector in the facetFunctionSelectors.selectors array
mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
// maps facet addresses to function selectors
mapping(address => FacetFunctionSelectors) facetFunctionSelectors;
// facet addresses
address[] facetAddresses;
// Used to query if a contract implements an interface.
// Used to implement ERC-165.
mapping(bytes4 => bool) supportedInterfaces;
// owner of the contract
address contractOwner;
}
Diamond-3 के लिए DiamondLoupe इम्प्लीमेंटेशन बहुत सरल है क्योंकि यह केवल उन स्टोरेज वेरिएबल्स के ऊपर एक पतला रैपर (thin wrapper) है:
contract DiamondLoupeFacet is IDiamondLoupe, IERC165 {
// Diamond Loupe Functions
////////////////////////////////////////////////////////////////////
/// These functions are expected to be called frequently by tools.
//
// struct Facet {
// address facetAddress;
// bytes4[] functionSelectors;
// }
/// @notice Gets all facets and their selectors.
/// @return facets_ Facet
function facets() external override view returns (Facet[] memory facets_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint256 numFacets = ds.facetAddresses.length;
facets_ = new Facet[](numFacets);
for (uint256 i; i < numFacets; i++) {
address facetAddress_ = ds.facetAddresses[i];
facets_[i].facetAddress = facetAddress_;
facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors;
}
}
/// @notice Gets all the function selectors provided by a facet.
/// @param _facet The facet address.
/// @return facetFunctionSelectors_
function facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory facetFunctionSelectors_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetFunctionSelectors_ = ds.facetFunctionSelectors[_facet].functionSelectors;
}
/// @notice Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses() external override view returns (address[] memory facetAddresses_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddresses_ = ds.facetAddresses;
}
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress;
}
// This implements ERC-165.
function supportsInterface(bytes4 _interfaceId) external override view returns (bool) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
return ds.supportedInterfaces[_interfaceId];
}
}
diamond-1 और diamond-2 के लिए view functions अधिक जटिल हैं क्योंकि उन्हें facet addresses को सूचीबद्ध (list out) करने के लिए सभी function selectors के माध्यम से लूप करना पड़ता है, फिर केवल यूनिक एड्रेस (unique addresses) लौटाने होते हैं।
Diamond को डिप्लॉय (Deploying) करना
Diamond को डिप्लॉय और अपग्रेड करने की प्रक्रिया अन्य अपग्रेडेबल proxy patterns के समान ही है:
डिप्लॉयमेंट के दौरान हम:
- Facets (इम्प्लीमेंटेशन्स) डिप्लॉय करते हैं
- Constructor के आर्गुमेंट्स के रूप में facets और selectors की सूची के साथ proxy डिप्लॉय करते हैं, और उसी ट्रांजेक्शन में इनिशियलाइज़ (initialize) करते हैं
और अपग्रेड के दौरान:
- नए facet(s) डिप्लॉय करते हैं
- हटाने वाले सिलेक्टर्स की सूची और जोड़ने वाले सिलेक्टर्स की सूची के साथ diamondCut() या उपयोगकर्ता-परिभाषित (user-defined) function को कॉल करते हैं। एक ही ट्रांजेक्शन में कई नए facets जोड़ना संभव है।
डिप्लॉयमेंट और अपग्रेड के दौरान storage variables को इनिशियलाइज़ करना
Proxy को डिप्लॉय करते समय, हम कुछ प्रारंभिक state variables सेट करना चाह सकते हैं जैसे कि हमारे पास एक constructor हो। इसी तरह, हम Transparent Upgradeable Proxy और UUPS के OpenZeppelin इम्प्लीमेंटेशन में upgradeToAndCall के समान, अपग्रेड के बाद कुछ storage variables को इनिशियलाइज़ करना चाह सकते हैं।
यह diamondCut में अंतिम दो आर्गुमेंट्स _init और _calldata का उपयोग है:
function diamondCut(Facets[] facets, address _init, bytes memory _calldata)
यदि _init ≠ address(0) है तो diamondCut को _calldata को आर्गुमेंट के रूप में उपयोग करके _init को delegatecall करना चाहिए। चूँकि diamondCut proxy (diamond) के संदर्भ (context) में चलता है, इसलिए delegatecalled कॉन्ट्रैक्ट (_init) proxy में storage variables को इनिशियलाइज़ कर सकता है।
इनिशियलाइज़ेशन लॉजिक एक बाहरी कॉन्ट्रैक्ट द्वारा चलाया जाता है। यदि हम एक ही ट्रांजेक्शन में कई storage variables सेट करना चाहते हैं, तो एक विशेष प्रयोजन (special purpose) वाले स्मार्ट कॉन्ट्रैक्ट का उपयोग करके इसे एकल ट्रांजेक्शन (single transaction) के रूप में करना आसान है।
एक उदाहरण कॉन्ट्रैक्ट नीचे दिखाया गया है:
import {LibDiamond} from "../libraries/LibDiamond.sol";
contract DiamondInit {
function init() external {
// read the ds struct from storage
// (remember, this executes in the context of the proxy)
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
// write to it
ds.owner = 0x456....;
ds.usdc = 0xa1...;
// ...
}
}
diamondCut पर कॉल DiamondInit के एड्रेस और init() की ABI एन्कोडिंग को पास करेगा।
इनिशियलाइज़ेशन तब भी हो सकता है जब कोई diamondCut ट्रांजेक्शन होता है — यह facets की प्रारंभिक डिप्लॉयमेंट तक सीमित नहीं है। किसी function selector को रिप्लेस करते या हटाते समय हम बाहरी कॉन्ट्रैक्ट को स्वचालित (atomically) रूप से delegatecall भी कर सकते हैं।
DiamondInit के साथ एक magic value की आवश्यकता पर विचार करें
EIP-2535 में इस सुरक्षा जांच (safety check) की आवश्यकता नहीं है कि DiamondInit कॉन्ट्रैक्ट वास्तव में एक कॉन्ट्रैक्ट है, लेकिन यह जाँचना निश्चित रूप से अधिक सुरक्षित है कि _init के एड्रेस में वास्तव में बाइटकोड (bytecode) है। अतिरिक्त सुरक्षा के लिए, ZkSync’s diamond implementation जांच करता है कि _init कॉन्ट्रैक्ट का delegatecall एक मैजिक वैल्यू (magic value) लौटाता है या नहीं।
Diamond standard के लिए Implementation विवरण
Diamond standard का उपयोग करने का सबसे सुरक्षित तरीका ऊपर लिंक किए गए reference contracts का उपयोग करना है, क्योंकि इनका ऑडिट किया जा चुका है। यदि आपका इरादा IDiamondLoupe functions को ऑन-चेन कॉल करने का नहीं है (जो आमतौर पर होता है), तो diamond-2 सबसे अधिक गैस कुशल होगा क्योंकि यह ERC-2535 के अनुपालन के लिए आवश्यक न्यूनतम संख्या में स्टोरेज वेरिएबल्स का उपयोग करता है।
Non-upgradeable diamonds — mapping के बजाय binary search का उपयोग करें
Non-upgradeable diamonds के लिए, हम function selectors के बीच के संबंध को हार्डकोड करने की सलाह देते हैं। Facet address खोजने के लिए लंबे if-else स्टेटमेंट का उपयोग करने के बजाय, binary search का उपयोग करें।
एक उदाहरण के रूप में, निम्नलिखित function पर विचार करें जो एक आर्गुमेंट के रूप में एक function selector लेता है और एक एड्रेस लौटाता है (यह कोड Pendle Finance से लिया गया है, जो एक Diamond Proxy का उपयोग करता है):

कोड function selector पर एक binary search करता है और उस implementation contract (facet) का एड्रेस लौटाता है जो function रखता है। बस इतना ही।
उपरोक्त एड्रेस constructor में सेट किए गए immutable एड्रेस हैं, जो आवश्यकतानुसार DiamondCut इवेंट को भी एमिट करते हैं:

जब तक वे EIP-7201 का उपयोग न करें, तब तक storage variables में लिखने वाली libraries का उपयोग न करें
कोई भी facet जो non-namespaced स्टोरेज में लिखता या पढ़ता है, उसमें storage collision होने की संभावना होती है। हम सभी स्टोरेज को प्रबंधित करने के लिए EIP-7201 का उपयोग करने की सलाह देते हैं।
किसी अन्य facet में functions को कॉल करना
जब किसी facet (implementation contract) में कोई function चलता है, तो यह diamond (proxy contract) के संदर्भ में ऐसा करता है। इसलिए, किसी अन्य facet में public function को कॉल करने के लिए, हम proxy address को कॉल कर सकते हैं।
हमारे शुरुआती उदाहरण पर विचार करें जहां हमारे पास add() function के साथ एक facet Add और एक अलग facet Multiply था। मान लीजिए कि हम Multiply facet से add() को कॉल करना चाहते हैं। नीचे हम दिखाते हैं कि इसे कैसे पूरा किया जाए; Multiply facet diamond proxy के एड्रेस को निर्दिष्ट करके add() function को कॉल करता है:
interface IAdd {
function add(uint256 x, uint256 y) external view returns (uint256);
}
contract Multiply {
function callAdd(uint256 x, uint256 y) external {
uint256 sum = IAdd(address(this)).add(x, y);
// rest of the code
}
}
यह पर्दे के पीछे (under the hood) दो कॉल्स करेगा:
- पहले
callAddस्वयं को कॉल करता है (proxy के संदर्भ में) - Proxy function selector को Add facet से मैच करता है
- Proxy Add facet को delegatecalls करता है
यह कुछ डेवलपर्स के लिए आश्चर्य की बात हो सकती है कि एक कॉन्ट्रैक्ट इस तरह से खुद को कॉल कर सकता है, लेकिन वास्तव में EVM द्वारा इसकी अनुमति है।
आप इसे प्रदर्शित करने के लिए निम्नलिखित कॉन्ट्रैक्ट का परीक्षण कर सकते हैं:
contract SelfCall {
uint256 public x = 0;
function setToOne() external {
x = 1;
}
function selfCall() external {
SelfCall(address(this)).setToOne();
// alternatively,
// address(this).call(abi.encodeWithSignature("setToOne()"));
}
}
हालांकि, self-call करना थोड़ा फिजूल (wasteful) है। गैस बचाने के लिए, एक facet दूसरे facet को “सीधे (directly)” delegatecall कर सकता है। पर्दे के पीछे, कॉल करने वाले facet का लॉजिक proxy में चलता है, और लॉजिक दूसरे facet को delegatecall कर रहा है, ताकि वह facet proxy में स्टोरेज वेरिएबल्स को “देख (see)” सके। इसे पूरा करने वाला कोड उतना साफ़ नहीं होगा क्योंकि delegatecalls केवल एक लो-लेवल कॉल (low-level call) हो सकता है। यहाँ EIP-2535 से लिया गया एक उदाहरण है:
// get the mapping from selector => facet address
DiamondStorage storage ds = diamondStorage(); // EIP-7201
// compute the selector
bytes4 functionSelector = bytes4(keccak256("functionToCall(uint256)"));
// get the facet address
address facet = ds.selectorToFacet[functionSelector];
// delegatecall
bytes memory myFunctionCall = abi.encodeWithSelector(functionSelector, 4);
(bool success, bytes memory result) = address(facet).delegatecall(myFunctionCall);
हालाँकि, ऊपर दिया गया कोड सुंदर (elegant) नहीं है और एक साधारण कॉल करने के लिए अतिरिक्त तीन से चार लाइनों की आवश्यकता होती है।
एक तीसरा समाधान एक Solidity लाइब्रेरी(ies) बनाना है जिसमें केवल internal functions हों, फिर उन internal functions को किसी भी facet के भीतर इम्पोर्ट (import) करें जिन्हें उनकी आवश्यकता है। इससे facets के बीच डुप्लीकेट बाइटकोड हो सकता है, लेकिन यदि facets 24kb की सीमा से अधिक नहीं हैं या डिप्लॉयमेंट लागत में अत्यधिक वृद्धि नहीं करते हैं, तो यह कोई बड़ी समस्या नहीं होनी चाहिए।
उदाहरण Diamond (Example Diamond)
हम एक diamond बनाएंगे जो एक काउंटर कॉन्ट्रैक्ट (counter contract) के रूप में कार्य करता है। एक facet काउंटर के मान (value) के view function को रखेगा और दूसरा facet काउंटर को बढ़ाने (increment) के लॉजिक को रखेगा।
Diamond init लॉजिक को स्पष्ट करने के लिए, हम अपने काउंटर को 0 के बजाय 8 से शुरू करेंगे।
आरंभ करने के लिए, diamond-2 reference hardhat repository को फोर्क (fork) करें। हम किसी भी ऑडिटेड Foundry इम्प्लीमेंटेशन के बारे में अवगत नहीं हैं।
IncrementLibrary
Namespace स्टोरेज को पढ़ने के लिए कोड को एक लाइब्रेरी में रखना मददगार होता है जिसे facets इम्पोर्ट कर सकते हैं। काउंटर को अपडेट करने के लिए लॉजिक को लाइब्रेरी के अंदर रखना एक डिज़ाइन विकल्प है। इंक्रीमेंट (increment) लॉजिक को सीधे लाइब्रेरी में रखने से हर उस अन्य facet के लिए बाइटकोड का आकार बढ़ जाएगा जो उस लाइब्रेरी में functions को कॉल करता है। हालाँकि, यह इंक्रीमेंट facet के लिए लॉजिक को भी सरल बनाता है, क्योंकि इंक्रीमेंट facet को इस बारे में कुछ भी जानने की आवश्यकता नहीं है कि namespaced स्टोरेज कैसे संरचित (structured) है।
ध्यान दें कि इस लाइब्रेरी में सभी functions internal होने चाहिए, क्योंकि Solidity कंपाइलर यह अपेक्षा करता है कि external functions वाली libraries को अलग से डिप्लॉय किया जाए।
pragma solidity ^0.8.0;
library LibInc {
// keccak256(abi.encode(uint256(keccak256("RareSkills.Facet.Increment")) - 1)) ^ bytes32(uint256(0xff))
bytes32 constant STORAGE_LOCATION = 0xfa04c3581a2244f8cd60ed05a316a89d13b0e00f0bfbe2b8a2155985a9d65e00;
struct IncrementStorage {
uint256 x;
}
function incStorage()
internal
pure
returns (IncrementStorage storage iStor) {
bytes32 location = STORAGE_LOCATION;
assembly {
iStor.slot := location
}
}
function x()
internal
view
returns (uint256 x) {
x = incStorage().x;
}
function increment() internal {
incStorage().x++;
}
}
contracts/libraries/LibInc.sol फ़ाइल जोड़ें
DiamondInit के साथ storage variable को इनिशियलाइज़ करना
इनिशियलाइज़ेशन एक अलग कॉन्ट्रैक्ट में होता है जिसे diamondCut() कॉन्ट्रैक्ट delegatecalls करता है। Reference implementation में, यह contracts/upgradeInitializers/DiamondInit.sol में है।
हम संक्षेप में (brevity) पूरा कॉन्ट्रैक्ट नहीं दिखाते हैं। DiamondInit.sol में निम्न कोड जोड़ें:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// ADD THIS LINE
import {LibInc} from "../libraries/LibInc.sol";
//...
contract DiamondInit {
// You can add arguments to this function in order to pass in
// data to set your own state variables
function init() external {
// ...
// ADD THESE LINES
LibInc.IncrementStorage storage _is = LibInc.incStorage();
_is.x = 8; // initialize x to 8
}
}
Facet जो केवल x को देखता (views) है
contracts/facets/ में एक नई फ़ाइल LibIncFacet.sol बनाएं:
pragma solidity ^0.8.0;
import { LibInc } from "../libraries/LibInc.sol";
contract IncViewFacet {
function x() external view returns (uint256 x) {
x = LibInc.x();
}
}
Facet जो केवल x को बढ़ाता (increments) है
contracts/facets में IncrementFacet.sol बनाएं:
import { LibInc } from "../libraries/LibInc.sol";
contract IncrementFacet {
function increment() external {
LibInc.increment();
}
}
निम्नलिखित test जोड़ें
test/diamondTest.js में निम्नलिखित test जोड़ें
it.only('test increment', async () => {
const incViewInterface = await ethers.getContractAt('IncViewFacet', diamondAddress)
const initialX = await incViewInterface.x()
console.log(initialX.toString())
assert.equal(initialX, 8)
const incrementInterface = await ethers.getContractAt('IncrementFacet', diamondAddress)
await incrementInterface.increment();
const afterX = await incViewInterface.x()
assert.equal(afterX, 9)
});
नए facets को डिप्लॉय करने के लिए scripts/deploy.js को अपडेट करें
scripts/deploy.js खोलें और facets के कॉन्ट्रैक्ट नाम जोड़ें। Hardhat फ़ाइल पथ (file paths) निर्दिष्ट किए बिना कॉन्ट्रैक्ट्स को खोजने के लिए पर्याप्त स्मार्ट है। बदलाव नीचे लाल बॉक्स में दिखाया गया है:

test को अपडेट करें
नए facets को डिप्लॉय करने के लिए diamondTest.js फ़ाइल में before हुक (hook) को निम्नानुसार अपडेट करें।
एक त्वरित जानकारी (Quick aside): Solidity कंपाइलर स्वचालित रूप से public/external functions के function selectors को आउटपुट करने में सक्षम है। उदाहरण के लिए, मान लीजिए कि हमारे पास C.sol में निम्नलिखित कॉन्ट्रैक्ट स्टोर है:
contract C {
function foo() public {}
function bar() external {}
}
यदि हम कॉन्ट्रैक्ट पर solc --hashes C.sol चलाते हैं, तो हमें निम्नलिखित आउटपुट मिलेगा:
======= C.sol:C =======
Function signatures:
febb0f7e: bar()
c2985578: foo()
इस रिपॉजिटरी में दी गई स्क्रिप्ट्स इस तकनीक का उपयोग करके हमारे लिए function selectors निकालती हैं, जो हमें स्पष्ट रूप से (explicitly) function selectors को स्वयं निर्दिष्ट करने की परेशानी से बचाती है।
फिर से, Hardhat फ़ाइल पथों को निर्दिष्ट किए बिना कॉन्ट्रैक्ट्स को खोजने में सक्षम है:

इसके साथ test चलाएँ
npx hardhat test
ध्यान दें कि अन्य tests विफल (fail) हो जाएंगे क्योंकि वे नए सिलेक्टर्स की अपेक्षा नहीं कर रहे हैं। हम अपने test पर .only मॉडिफायर (modifier) का उपयोग करके इसे अनदेखा करते हैं। .only मॉडिफायर अन्य unit tests को चलने से रोकता है और केवल .only के साथ संशोधित (modified) unit test चलाता है।
यदि आपको समस्याओं का सामना करना पड़ता है, तो सुनिश्चित करें कि आप Node वर्जन 20 का उपयोग कर रहे हैं।
सारांश (Summary)
- Diamond pattern एक proxy है जिसमें कई implementation contracts होते हैं।
- Diamond को आने वाले calldata के function selector के आधार पर पता होता है कि किस facet को delegatecall करना है।
- यदि diamond अपग्रेडेबल है, तो सिलेक्टर से implementation address तक की मैपिंग को diamondCut के माध्यम से बदला जा सकता है। इस मैपिंग को बदलने का लॉजिक स्टैंडर्ड द्वारा निर्देशित नहीं है, यह proxy बाइटकोड का हिस्सा या किसी facet का हिस्सा हो सकता है।
- Diamond में सभी facets और सिलेक्टर्स एमिट किए गए events और IDiamondLoupe में public functions के माध्यम से निर्धारित किए जाने योग्य होने चाहिए।
- अधिक data structures जोड़कर
IDiamondLoupeमें functions को देखने के लिए गैस की लागत (Gas costs) को कम किया जा सकता है। हालाँकि, इससे अपग्रेड की लागत बढ़ जाती है। लगभग सभी मामलों में, हमें कम data structures का विकल्प चुनना चाहिए क्योंकिIDiamondLoupefunctions ऑफचेन (offchain) उपयोगकर्ताओं के लिए होते हैं।
हम इस लेख के पुराने संस्करण (earlier version) पर टिप्पणियां प्रदान करने के लिए Nick Mudge को धन्यवाद देना चाहते हैं।