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

कई इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स होने का एक फायदा यह है कि प्रॉक्सी कॉन्ट्रैक्ट द्वारा उपयोग किए जा सकने वाले लॉजिक की मात्रा पर कोई व्यावहारिक ऊपरी सीमा (practical upper limit) नहीं है। याद रखें कि EVM स्मार्ट कॉन्ट्रैक्ट के बाइटकोड (bytecode) साइज़ को 24kb तक सीमित करता है। यदि डेवलपर को 48kb तक का बाइटकोड डिप्लॉय करना है, तो एक व्यवहार्य समाधान fallback-extension pattern का उपयोग करना है। 48kb से अधिक के लिए, डायमंड पैटर्न सबसे आम समाधान है।
डायमंड पैटर्न की शब्दावली में, “प्रॉक्सी कॉन्ट्रैक्ट” को डायमंड और “इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स” को “फैसेट्स (facets)” कहा जाता है। एक ही चीज़ को संदर्भित करने वाले दो शब्दों से भ्रम पैदा होता है, इसलिए हम इस बिंदु को स्पष्ट करना चाहते हैं:
- diamond = प्रॉक्सी कॉन्ट्रैक्ट
- facet = इम्प्लीमेंटेशन कॉन्ट्रैक्ट
एक डायमंड (प्रॉक्सी) को एक या अधिक इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स (फैसेट्स) को बदलकर अपग्रेड किया जा सकता है। वैकल्पिक रूप से, फैसेट्स (इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स) को बदलने के तंत्र का समर्थन न करके एक डायमंड को नॉन-अपग्रेडेबल (इम्यूटेबल) भी बनाया जा सकता है।
डायमंड पैटर्न सीखना
एक साथ कई इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स को संभालने की उभरती जटिलता के कारण डायमंड प्रॉक्सी को “विशेषज्ञ डिज़ाइन पैटर्न (expert design pattern)” के रूप में जाना जाता है। वास्तव में, अपनी कथित जटिलता के कारण EVM डेवलपर्स के बीच डायमंड पैटर्न कुछ हद तक विवादास्पद है (हम यहां इस बहस में नहीं पड़ते या जटिलता पर कोई निर्णय नहीं देते हैं)।
विनिर्देश (specification) अपने आप में काफी छोटा है। इसमें चार public view फ़ंक्शंस की आवश्यकता होती है — लेकिन यदि डायमंड अपग्रेडेबल है, तो इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स को बदलने के लिए एक पांचवें स्टेट-चेंजिंग फ़ंक्शन की आवश्यकता होती है। डायमंड पैटर्न केवल एक सिंगल इवेंट को अनिवार्य करता है (भले ही कॉन्ट्रैक्ट अपग्रेडेबल हो या नहीं)। छोटे विनिर्देश के बावजूद, उन चार (या पांच) फ़ंक्शंस को लागू (implement) करना अन्य प्रॉक्सी पैटर्न की तुलना में काफी अधिक जटिल है।
हालाँकि, सही पूर्वापेक्षाओं (prerequisites) (जो कि काफी अधिक हैं!) के साथ, डायमंड पैटर्न को समझना विशेष रूप से कठिन नहीं है। हम मान कर चलते हैं कि पाठक हमारी Proxy Patterns Book के पहले तेरह अध्यायों में शामिल विषयों से परिचित है। यदि आपने उन अध्यायों को नहीं पढ़ा है या उन विषयों से पहले से परिचित नहीं हैं, तो डायमंड पैटर्न सीखना एक संघर्ष होगा, इसलिए सुनिश्चित करें कि पूर्वापेक्षाएँ पूरी हों।
यहाँ कुछ समस्याएँ दी गई हैं जिनसे पैटर्न को निपटने की आवश्यकता है:
- जब डायमंड को कोई ट्रांज़ैक्शन प्राप्त होता है, तो उसे कैसे पता चलता है कि किस इम्प्लीमेंटेशन कॉन्ट्रैक्ट को कॉल करना है?
- यदि किसी फैसेट (इम्प्लीमेंटेशन कॉन्ट्रैक्ट) को अपग्रेड किया जाता है, तो प्रॉक्सी कॉन्ट्रैक्ट (डायमंड) को कैसे पता चलता है कि नया फैसेट (इम्प्लीमेंटेशन कॉन्ट्रैक्ट) कौन सा फ़ंक्शन सपोर्ट करता है, और संभावित रूप से, कौन से फ़ंक्शंस अब सपोर्ट नहीं किए जाते हैं?
- अपग्रेड लॉजिक कहाँ होना चाहिए — प्रॉक्सी बाइटकोड में या फैसेट में?
- चूंकि प्रत्येक इम्प्लीमेंटेशन कॉन्ट्रैक्ट सीधे अन्य इम्प्लीमेंटेशन के बारे में नहीं जानता है, तो स्टोरेज कॉलिज़न (storage collisions) से कैसे बचा जा सकता है?
- किसी बाहरी एक्टर (external actor) को कैसे पता चल सकता है कि डायमंड प्रॉक्सी द्वारा किन फ़ंक्शंस को सपोर्ट किया जाता है? एक इंटरफ़ेस को इनहेरिट (Inheriting) करना पर्याप्त नहीं है क्योंकि अपग्रेड के दौरान फ़ंक्शंस बदल सकते हैं।
- क्या होगा यदि एक इम्प्लीमेंटेशन कॉन्ट्रैक्ट के अंदर का फ़ंक्शन दूसरे इम्प्लीमेंटेशन कॉन्ट्रैक्ट के फ़ंक्शन को कॉल करना चाहता है — इस ट्रांज़ैक्शन को कैसे सुगम बनाया जाएगा?
इस लेख में, हम दिखाएंगे कि उपरोक्त सभी समस्याओं का समाधान कैसे किया जाए। ध्यान दें कि ERC-2535 को अनिवार्य रूप से डायमंड प्रॉक्सी के अपग्रेडेबल होने की आवश्यकता नहीं है। प्रॉक्सी में हार्डकोडेड इम्प्लीमेंटेशन कॉन्ट्रैक्ट हो सकते हैं और फिर भी वह एक मान्य डायमंड हो सकता है।
चीजों को शुरुआत में सरल रखने के लिए, हम इम्यूटेबल (immutable) डायमंड को दिखाकर शुरुआत करते हैं।
इम्यूटेबल डायमंड (Immutable diamond)
एक इम्यूटेबल डायमंड, जिसे स्टैटिक डायमंड (static diamond) या सिंगल कट डायमंड (single cut diamond) भी कहा जाता है, कई इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स वाला एक प्रॉक्सी कॉन्ट्रैक्ट है — और किसी भी इम्प्लीमेंटेशन कॉन्ट्रैक्ट को अपग्रेड नहीं किया जा सकता है। (अपग्रेड कार्यक्षमता को हटाकर अपग्रेडेबल डायमंड का इम्यूटेबल बनना संभव है, लेकिन हम अपग्रेडेबल डायमंड्स पर बाद में चर्चा करेंगे)।
प्रॉक्सी कॉन्ट्रैक्ट के रूप में डायमंड पैटर्न
यदि UUPS और Transparent Upgradeable Proxy द्वारा उपयोग किए जाने वाले OpenZeppelin 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())}
}
}
उपरोक्त कोड से लेकर पारंपरिक प्रॉक्सी तक एकमात्र महत्वपूर्ण अंतर ये दो पंक्तियाँ हैं:
address facet = facetAddress(msg.sig);
require(facet != address(0));
उपरोक्त कोड msg.sig का उपयोग करके ट्रांज़ैक्शन के पहले चार बाइट्स (function selector) प्राप्त करता है, फिर उस फैसेट का एड्रेस निर्धारित करने के लिए facetAddress() का उपयोग करता है जो फ़ंक्शन को इम्प्लीमेंट करता है (ध्यान दें कि facetAddress() प्रॉक्सी लॉजिक का हिस्सा हो सकता है या किसी अन्य फैसेट के अंदर हो सकता है और उसे delegatecall किया जा सकता है — इस पर बाद में चर्चा की जाएगी)। प्रॉक्सी फिर उसी calldata के साथ उस एड्रेस को delegatecall करता है जो उसे प्राप्त हुआ था।

EIP-2535 यह निर्दिष्ट नहीं करता है कि इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स के एड्रेस में function selectors को कैसे मैप किया जाए। एक स्टैटिक डायमंड के लिए, एक उचित समाधान इस संबंध को हार्डकोड करना है। अगले भाग में हम यही दृष्टिकोण अपनाएंगे।
अपग्रेडेबल डायमंड के लिए, सेलेक्टर्स को हार्डकोड करना स्पष्ट रूप से कोई विकल्प नहीं है, और हमें मैपिंग्स (mappings) पर निर्भर रहना होगा, जैसा कि हम संबंधित अनुभागों में देखेंगे।
Function Selector पर कंडीशनल ब्रांचिंग
नीचे हम दो इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स (फैसेट्स) के साथ एक प्रॉक्सी कॉन्ट्रैक्ट दिखा रहे हैं:
- पहला इम्प्लीमेंटेशन कॉन्ट्रैक्ट
Addएक सिंगल public फ़ंक्शनadd()को एक्सपोज़ करता है जो अपने तर्कों (arguments) का योग (sum) लौटाता है। - दूसरा इम्प्लीमेंटेशन कॉन्ट्रैक्ट
Multiplyदो public फ़ंक्शंसmultiply()औरexponent()को एक्सपोज़ करता है जो वह करते हैं जो उनके नाम सुझाते हैं।
नीचे दिया गया कोड दो इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स का उपयोग करके एक सिंगल प्रॉक्सी दिखाता है। Diamond में facetAddress() फ़ंक्शन msg.sig लेता है और उस फैसेट का एड्रेस लौटाता है जो उस सिग्नेचर के साथ फ़ंक्शन को इम्प्लीमेंट करता है, यदि कोई हो। नीचे दिया गया कोड अभी तक डायमंड मानक के अनुरूप नहीं है, लेकिन हम कोड को कंप्लायंट (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 मानक ऐसे फ़ंक्शन को कॉल करने के लिए त्रुटि संदेश (error message) निर्दिष्ट नहीं करता है जो मौजूद नहीं है। हमारे मामले में, हम "Function does not exist" स्ट्रिंग के साथ revert करते हैं, लेकिन एक कस्टम त्रुटि (custom error) अधिक गैस कुशल (gas efficient) होगी।
सरलता के लिए, हमारा डायमंड constructor में दो फैसेट्स को डिप्लॉय करता है, लेकिन व्यवहार में ऐसा नहीं किया जाता है। व्यवहार में, फैसेट्स अलग से डिप्लॉय किए जाते हैं, और प्रॉक्सी को बाद में किसी तरीके से “सूचित” किया जाता है, जैसे कि कंस्ट्रक्टर तर्कों (constructor arguments) के माध्यम से या एक अलग फ़ंक्शन के माध्यम से।
कुछ डायमंड्स कॉन्ट्रैक्ट्स के बजाय बाहरी फ़ंक्शंस (external functions) वाली लाइब्रेरीज़ के रूप में फैसेट्स को इम्प्लीमेंट करते हैं, लेकिन EIP-2535 को यह आवश्यक नहीं है कि फैसेट्स लाइब्रेरीज़ हों या कॉन्ट्रैक्ट्स।
सरलता के लिए, हम फ़ंक्शन सेलेक्टर को फैसेट एड्रेस से मैच करने के लिए else-if स्टेटमेंट्स की एक श्रृंखला का उपयोग करते हैं, लेकिन यदि बहुत सारे विकल्प हैं तो यह गैस-कुशल (gas-efficient) नहीं है। एक स्टैटिक डायमंड के लिए, सेलेक्टर्स को पहले से सॉर्ट (sort) करना और फिर बाइनरी सर्च (binary search) करना अधिक कुशल है — लेकिन हम इस पर बाद में चर्चा करेंगे।
कॉर्नर केस (Corner case) — ईथर ट्रांसफर
जब ईथर ट्रांसफर किया जाता है, तो कोई calldata नहीं होगा। इस स्थिति में, msg.sig, 0x00000000 लौटाएगा और यह फैसेट एड्रेस पर मैप नहीं होगा। यह ठीक है यदि कॉन्ट्रैक्ट का इरादा ईथर प्राप्त करने का नहीं है। हालाँकि, यदि कॉन्ट्रैक्ट को ईथर प्राप्त करने की उम्मीद है, या आने वाले ईथर पर प्रतिक्रिया करने की उम्मीद है, तो 0x00000000 को इच्छित लॉजिक वाले फ़ंक्शन पर, या कम से कम एक ऐसे फ़ंक्शन पर मैप किया जाना चाहिए जो revert न करे। ध्यान रखें कि एक हमलावर (attacker) बिना किसी calldata के या calldata के रूप में 0x00000000 भेजकर इस फ़ंक्शन को ट्रिगर कर सकता है, इसलिए लॉजिक को दोनों परिदृश्यों (scenarios) को सुचारू रूप से संभालने की आवश्यकता है।
हमारे कॉन्ट्रैक्ट को EIP-2535 कंप्लायंट बनाने के लिए, हमें चार अनिवार्य public view फ़ंक्शंस को लागू करना होगा, जिनमें से प्रत्येक पर नीचे चर्चा की गई है।
चार अनिवार्य Public View फ़ंक्शंस
1/4 facetAddress()
ध्यान दें कि facetAddress() public है — EIP-2535 के लिए आवश्यक है कि एक डायमंड प्रॉक्सी इस सिग्नेचर के साथ एक public फ़ंक्शन को एक्सपोज़ करे:
function facetAddress(bytes4 selector) external view returns (address);
हम पहले से ही इस संबंध में कंप्लायंट हैं।
2/4 facetAddresses()
facetAddress(bytes4 selector) के अलावा, EIP-2535 facetAddresses() (बहुवचन) नामक एक फ़ंक्शन को अनिवार्य करता है जो डायमंड द्वारा उपयोग किए जाने वाले सभी फैसेट्स को लौटाता है — दूसरे शब्दों में, एड्रेस की एक सूची। सिग्नेचर इस प्रकार है:
function facetAddresses() public view returns (address[] memory 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() द्वारा लौटाए गए एड्रेस (फैसेट एड्रेस) की सूची पर कोई अनिवार्य क्रम (enforced ordering) नहीं है।
3/4 facetFunctionSelectors()
एक फैसेट एड्रेस को एक तर्क (argument) के रूप में देखते हुए, facetFunctionSelectors() सभी फैसेट के public फ़ंक्शंस के सेलेक्टर्स लौटाता है। इसका सिग्नेचर निम्नलिखित है:
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 एक फ़ंक्शन facets() अनिवार्य करता है जो कोई तर्क नहीं लेता है और structs की एक सूची लौटाता है जहां प्रत्येक struct में एक फैसेट एड्रेस और उस फैसेट के फ़ंक्शन सेलेक्टर्स की एक सूची होती है।
इसका सिग्नेचर निम्नलिखित है:
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
function facets() external view returns (Facet[] memory facets_);
facets() द्वारा लौटाई जाने वाली जानकारी निम्न द्वारा प्राप्त की जा सकती है:
- पहले सभी फैसेट एड्रेस प्राप्त करने के लिए
facetAddresses()को कॉल करके- प्रत्येक फैसेट एड्रेस के माध्यम से लूप (looping) करके, और उस एड्रेस को
Facetstruct केfacetAddressफ़ील्ड में डालकर- और एड्रेस पर
facetFunctionSelectors()को कॉल करके और फ़ंक्शन सेलेक्टर्स की सूची को struct केfunctionSelectorsफ़ील्ड में डालकर।
- और एड्रेस पर
- प्रत्येक फैसेट एड्रेस के माध्यम से लूप (looping) करके, और उस एड्रेस को
एक विकल्प सीधे फ़ंक्शन में उत्तर को हार्डकोड करना है, जो कुछ मामलों में अधिक कुशल हो सकता है (अपग्रेडेबल डायमंड्स के लिए, हार्डकोडिंग स्पष्ट रूप से काम नहीं करेगी)। हमारे डायमंड में, हम नीचे दिखाए गए अनुसार लूप विधि (loop method) का उपयोग करके 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 फ़ंक्शंस को लागू करने के बाद, अब हमने डायमंड प्रॉक्सी के सभी अनिवार्य बाहरी फ़ंक्शंस (external functions) लागू कर दिए हैं।
IDiamondLoupe
सामूहिक रूप से, इन चार फ़ंक्शंस को IDiamondLoupe नामक एक इंटरफ़ेस में परिभाषित किया गया है। सभी डायमंड्स को IDiamondLoupe लागू करना चाहिए। आप यह याद रख सकते हैं कि “loupe” डायमंड्स को देखने (“viewing”) के लिए एक छोटा आवर्धक लेंस (magnifying glass) है, और इनमें से प्रत्येक फ़ंक्शन view फ़ंक्शन है।
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_);
}
हम अभी भी एक पूरी तरह से कंप्लायंट डायमंड प्रॉक्सी से एक कदम दूर हैं: डायमंड में किए गए परिवर्तनों को लॉग करना। यह वह विषय है जिस पर हम आगे चर्चा करेंगे।
DiamondCut — फैसेट सेलेक्टर्स को लॉग करना
हमारा रनिंग डायमंड उदाहरण अपग्रेडेबल नहीं है। व्यवहार में डायमंड्स अपग्रेडेबल होते हैं और अपने फैसेट्स और उनके साथ जुड़े फ़ंक्शन सेलेक्टर्स को बदल सकते हैं।
किसी फैसेट में कोई भी परिवर्तन लॉग किया जाना चाहिए — यहां तक कि नॉन-अपग्रेडेबल (स्टैटिक) डायमंड्स के लिए भी, डिप्लॉयमेंट के समय परिवर्तन (फैसेट्स और फ़ंक्शंस सेलेक्टर्स का जोड़) होते हैं और इसलिए उन्हें लॉग करने की आवश्यकता होती है।
फैसेट सेलेक्टर्स को लॉग करने के पीछे का सिद्धांत यह है कि डायमंड द्वारा सपोर्टेड फ़ंक्शन सेलेक्टर्स को निर्धारित करने के दो तरीके होने चाहिए:
- ऊपर वर्णित public फ़ंक्शंस का उपयोग करना, या
- लॉग्स (logs) को पार्स करना।
हमारे जैसे हार्डकोड किए गए फैसेट्स को भी लॉग किया जाना चाहिए, इसलिए इवेंट्स (events) को एमिट (emit) किया जाना चाहिए। हमारे मामले में, यह एमिशन (emission) डिप्लॉयमेंट के दौरान होना चाहिए। मानक द्वारा ऐसे लॉग्स आवश्यक हैं।
जब हम कोई फैसेट जोड़ते हैं (या कोई भी परिवर्तन करते हैं जैसे कि बदलना या हटाना), तो उस क्रिया (action) को डायमंड कट (diamond cut) कहा जाता है।
डायमंड कट का अर्थ फैसेट्स को हटाना नहीं है, जैसा कि “कटिंग” का अर्थ हो सकता है। यह किसी तरह से डायमंड को बदलने से मेल खाता है। फैसेट में किसी भी बदलाव के लिए एक DiamondCut इवेंट को एमिट करने की आवश्यकता होती है जिसे आगामी कोड ब्लॉक में परिभाषित किया गया है। (कोई भी “कट” के लिए इस शब्दावली को यह याद रखते हुए समझ सकता है कि जब एक वास्तविक हीरे (gem) को “काटा” जाता है तो उसे एक अतिरिक्त पहलू — या फैसेट — मिलता है जहाँ कट लगा था)।
DiamondCut इवेंट को IDiamond नामक एक नए इंटरफ़ेस में परिभाषित किया गया है। हर बार जब किसी फैसेट में कोई फ़ंक्शन जोड़ा जाता है, बदला जाता है या हटाया जाता है तो इवेंट्स को एमिट किया जाना चाहिए। 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[] एक सूची है, इसलिए एक साथ कई फैसेट्स को बदला जा सकता है।
अन्य प्रॉक्सी पैटर्न जैसे Transparent Upgradeable Proxies या UUPS Proxies के विपरीत, डायमंड्स में फैसेट एड्रेस बदलकर अपग्रेड करने का तंत्र नहीं होता है। एक फैसेट (इम्प्लीमेंटेशन कॉन्ट्रैक्ट) तब हटा दिया जाता है जब उससे जुड़े सभी फ़ंक्शन सेलेक्टर्स हटा दिए जाते हैं। जब एक नए इम्प्लीमेंटेशन एड्रेस के साथ कोई फ़ंक्शन सेलेक्टर जोड़ा जाता है तो फैसेट्स परोक्ष रूप से (implicitly) जुड़ जाते हैं।
_init और _calldata पैरामीटर्स वही उद्देश्य पूरा करते हैं जो OpenZeppelin Initializers — हम इस पर बाद में अधिक चर्चा करेंगे। यदि किसी इनिशियलाइज़ेशन डेटा की आवश्यकता नहीं है, तो _init, address(0) होना चाहिए और _calldata, "" या empty bytes होना चाहिए।
आइए कंस्ट्रक्टर में इस इवेंट को एमिट करने के लिए अपने डायमंड को अपडेट करें:
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 डायमंड प्रॉक्सी है। आम तौर पर, IDiamondLoupe फ़ंक्शंस को एक अलग फैसेट में संग्रहीत किया जाता है, लेकिन सरलता के लिए हमने चीजों को अभी सरल रखने के लिए उन्हें डायमंड में रखा है।
चाहे IDiamondLoupe के फ़ंक्शंस कहीं भी संग्रहीत हों — डायमंड में या किसी अन्य फैसेट में, मानक के अनुसार उनके एड्रेस, जो कार्रवाई हुई (फैसेट जोड़ना), और फ़ंक्शन सेलेक्टर्स को एमिट करना आवश्यक है।
IDiamondLoupe और DiamondCut के इवेंट्स के बीच दोहराव (Duplication)
इस EIP के विवादास्पद पहलुओं में से एक यह तथ्य है कि फ़ंक्शन सेलेक्टर्स को पिछले लॉग्स को पार्स करके और IDiamondLoupe में view फ़ंक्शंस को कॉल करके दोनों तरह से निर्धारित किया जा सकता है। यह एक ही काम को पूरा करने के लिए डुप्लिकेट लॉजिक बनाता है।
public फ़ंक्शंस के माध्यम से समान डेटा को एक्सपोज़ करने के पीछे का तर्क यह है कि यह ब्लॉक एक्सप्लोरर (block explorers) और अन्य बाहरी प्रणालियों (external systems) के साथ एकीकरण (integration) को आसान बनाता है। इसके अतिरिक्त, अपग्रेड स्क्रिप्ट्स नए फ़ंक्शन सेलेक्टर को रजिस्टर करने से पहले एटॉमिक रूप से (atomically) जांच सकती हैं कि फ़ंक्शन सेलेक्टर पहले से मौजूद है या नहीं। DiamondCut इवेंट्स के पीछे का उद्देश्य अपग्रेड का इतिहास दिखाना है।
ERC-1967 में, एक ब्लॉक एक्सप्लोरर स्टोरेज स्लॉट को क्वेरी कर सकता है और तुरंत पहचान सकता है कि लॉजिक कॉन्ट्रैक्ट कहाँ है — ब्लॉक एक्सप्लोरर को ERC-1967 द्वारा एमिट किए गए लॉग को पार्स करने की आवश्यकता नहीं है, जिसमें समान जानकारी होती है।
डायमंड को अपग्रेडेबल बनाना, diamondCut फ़ंक्शन को लागू करना
EIP-2535 मानक एक फ़ंक्शन diamondCut() जोड़ने का सुझाव देता है जिसे नीचे दिखाया गया है जिसके द्वारा फैसेट्स को जोड़ा, बदला या हटाया जा सकता है।
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;
}
मानक के लिए यह आवश्यक नहीं है कि अपग्रेड फ़ंक्शन को diamondCut() कहा जाए या उपरोक्त सिग्नेचर को लागू किया जाए। एक डेवलपर उदाहरण के लिए अपने स्वयं के फ़ंक्शन changeTheFacets() का उपयोग कर सकता है — लेकिन उस फ़ंक्शन को फैसेट अपडेट के प्रकार के अनुसार DiamondCut इवेंट को एमिट करना चाहिए जो उसने किया है।
फैसेट्स और सेलेक्टर्स के लिए डेटा स्ट्रक्चर्स
फ़ंक्शन सेलेक्टर्स और फैसेट्स को संग्रहीत करने के लिए, कम से कम हम फ़ंक्शन सेलेक्टर्स से फैसेट एड्रेस तक एक मैपिंग (mapping) चाहते हैं:
mapping(bytes4 => address) facetToSelectors;
यह डेटा स्ट्रक्चर हमें निम्न में सक्षम बनाता है:
- प्राप्त
msg.sigके आधार पर यह पता लगाना कि किस इम्प्लीमेंटेशन कॉन्ट्रैक्ट को delegatecall करना है - यह निर्धारित करना कि क्या पहले से मौजूद फ़ंक्शन सेलेक्टर से टकराए (colliding) बिना कोई नया फ़ंक्शन सेलेक्टर जोड़ा जा सकता है। हमें
facetToSelectors[selector] == address(0)की आवश्यकता (require) होनी चाहिए - यह निर्धारित करना कि क्या किसी सेलेक्टर को बदला या हटाया जा सकता है। उसी चेक का उपयोग करके, हम यह निर्धारित कर सकते हैं कि क्या हम किसी गैर-मौजूद सेलेक्टर को बदल रहे हैं या हटा रहे हैं (इस ऑपरेशन को revert होना चाहिए)।
IDiamondLoupe के लिए फ़ंक्शंस लागू करना
संक्षेप में, यहाँ IDiamondLoupe में दिए गए view फ़ंक्शंस हैं जिन्हें हमें सपोर्ट करने की आवश्यकता है:
facetAddress(bytes4 selector)एक फ़ंक्शन सेलेक्टर दिए जाने पर, फैसेट लौटाएंfacetAddresses()सभी फैसेट एड्रेस लौटाएंfacetFunctionSelectors(address facet)एक एड्रेस दिए जाने पर, सभी फ़ंक्शन सेलेक्टर्स लौटाएंfacets()सभी फैसेट एड्रेस और उनके फ़ंक्शन सेलेक्टर्स लौटाएं
यहाँ बताया गया है कि हम प्रत्येक को कैसे लागू करते हैं:
- फ़ंक्शन
facetAddress(bytes4 selector)केवलfacetToSelectorsमैपिंग के लिए एक view फ़ंक्शन हो सकता है — या हम मैपिंग को public बना सकते हैं। facetAddresses()के लिए सभी एड्रेस वापस करने के लिए, हमें या तो:- स्पष्ट रूप से एक सूची में सभी एड्रेस संग्रहीत करने होंगे
- स्पष्ट रूप से एक सूची में सभी फ़ंक्शन सेलेक्टर्स संग्रहीत करने होंगे, फ़ंक्शन सेलेक्टर्स के माध्यम से लूप करना होगा, और प्रत्येक पर
facetAddress(bytes4 selector)को कॉल करना होगा, फिर अद्वितीय (unique) एड्रेस की एक सूची बनानी होगी।
facetFunctionSelectors(address facet)के माध्यम से किसी फैसेट के सभी फ़ंक्शन सेलेक्टर्स वापस करने के लिए हमें या तो:- एक मैपिंग
mapping(address facet => bytes4[])बनानी होगी जो प्रत्येक मैपिंग से जुड़े फ़ंक्शन सेलेक्टर्स की सूची संग्रहीत करती है - सभी फ़ंक्शन सेलेक्टर्स का एक एरे (array) बनाए रखना होगा, और उस सभी एरे के माध्यम से लूप करना होगा और प्रत्येक पर
facetAddress(bytes4 selector)को कॉल करना होगा। यदि लौटाया गयाfacet,facetFunctionSelectorsके तर्क में दिया गयाfacetहै, तो एड्रेस को सूची में जोड़ें।
- एक मैपिंग
- चूंकि
facets()वही जानकारी देता है जोfacetAddresses()के माध्यम से लूप करने और प्रत्येक एड्रेस परfacetFunctionSelectors(address facet)कॉल करने से मिलती है, इसलिए हमfacets()को लागू करने की आगे की चर्चा छोड़ देते हैं।
एक बुनियादी ट्रेडऑफ़ (tradeoff) है। यदि हम अधिक डेटा स्ट्रक्चर्स का उपयोग करते हैं, तो view फ़ंक्शंस को कॉल करना सस्ता होगा क्योंकि उन्हें इन फ़ंक्शंस द्वारा लौटाए जाने वाले डेटा को “पुनर्निर्मित (reconstruct)” नहीं करना पड़ता है, लेकिन अपग्रेड के दौरान अधिक डेटा स्ट्रक्चर्स को अपडेट करने की आवश्यकता होती है। इसलिए हमें या तो यह चुनना होगा:
- अपग्रेड सस्ता है, लेकिन ऑन-चेन (on-chain) view फ़ंक्शंस को कॉल करना अधिक महंगा है।
- view फ़ंक्शन को कॉल करना सस्ता है, लेकिन एक फैसेट को अपग्रेड करने के लिए अधिक बहीखाता (bookkeeping) (गैस लागत में वृद्धि) की आवश्यकता होती है।
ऑन-चेन किसी भी IDiamondLoupe view फ़ंक्शन को कॉल करना बहुत ही असामान्य है क्योंकि वे ऑफ-चेन (off-chain) उपभोग के लिए अभिप्रेत हैं। इसलिए, कम डेटा स्ट्रक्चर्स का विकल्प चुनना बेहतर है।
फैसेट्स और सेलेक्टर्स के लिए स्टोरेज वेरिएबल्स — और स्टोरेज कॉलिज़न से बचना
जैसा कि देखा गया है, सेलेक्टर और एड्रेस की जानकारी संग्रहीत करने के लिए, हमें कम से कम एक मैपिंग की आवश्यकता है जैसे:
mapping(bytes4 => address) facetToAddress;
डायमंड में, लेकिन फिर पहले स्टोरेज स्लॉट को असाइन किए गए फैसेट के भीतर कोई भी मैपिंग संभावित रूप से इस मैपिंग से टकरा जाएगी (collide)।
डायमंड प्रॉक्सी में स्टोरेज कॉलिज़न (storage collisions) अन्य प्रॉक्सी पैटर्न की तुलना में अधिक जटिल हैं, क्योंकि कॉलिज़न न केवल एक ही इम्प्लीमेंटेशन कॉन्ट्रैक्ट के अपग्रेड में हो सकते हैं, बल्कि फैसेट्स के बीच भी हो सकते हैं।
डायमंड पैटर्न यह निर्दिष्ट नहीं करता है कि स्टोरेज को कैसे प्रबंधित किया जाना चाहिए। कॉलिज़न को संभालने का एक सीधा तरीका स्टोरेज नेमस्पेस (storage namespaces) का उपयोग करना है। नेमस्पेस के उपयोग का विस्तृत विवरण EIP-7201 storage namespaces में पाया जा सकता है।
समीक्षा के रूप में, स्टोरेज नेमस्पेस में, किसी कॉन्ट्रैक्ट के स्टेट वेरिएबल्स (state variables) को एक struct में समूहीकृत किया जाता है, और इस struct का आधार (base) एक स्यूडोरैंडम स्लॉट (pseudorandom slot) में संग्रहीत किया जाता है, जो आमतौर पर एक स्ट्रिंग के हैश (hash) द्वारा निर्धारित होता है। परिणामस्वरूप, प्रत्येक कॉन्ट्रैक्ट का अपना स्वयं का बेस स्टोरेज स्लॉट होता है, जिससे स्टोरेज कॉलिज़न की संभावना बहुत कम हो जाती है।
EIP-7201 को डायमंड पैटर्न द्वारा प्रस्तावित पहले के समाधान से लिया गया था जिसे “डायमंड स्टोरेज (diamond storage)” कहा जाता था। EIP-2535 ने एक और पैटर्न भी प्रस्तावित किया जिसे “ऐप स्टोरेज (App Storage)” कहा जाता है। हालाँकि, EIP-2535 यह निर्देशित नहीं करता है कि स्टोरेज को कैसे प्रबंधित किया जाना चाहिए, इसलिए हम केवल पाठक को एक व्यवहार्य समाधान का संदर्भ देते हैं, जो कि EIP-7201 का उपयोग करना है। इच्छुक पाठक सीधे EIP से “डायमंड स्टोरेज” और “ऐप स्टोरेज” पैटर्न के बारे में जान सकते हैं — ये दोनों EIP लेखक द्वारा अनुशंसित हैं।
डायमंड को संचालित करने के लिए न्यूनतम स्टोरेज
यदि हमने डायमंड को संचालित करने के लिए आवश्यक न्यूनतम स्टोरेज रखने का विकल्प चुना है, तो प्रॉक्सी के लिए नेमस्पेस निम्नलिखित फ़ील्ड्स वाला एक struct होना चाहिए:
- हमें सेलेक्टर्स की एक सूची की आवश्यकता है, यानी
bytes4[] selectors। हर बार जब हम कोई सेलेक्टर जोड़ते हैं, तो हम यह सुनिश्चित करने के लिए इस सूची को स्कैन करते हैं कि हम पहले से मौजूद सेलेक्टर तो नहीं जोड़ रहे हैं। - हमें कम से कम सेलेक्टर्स से एड्रेस तक की एक मैपिंग की आवश्यकता है। हालाँकि, सेलेक्टर्स को
selectorsमें उनके इंडेक्स (index) पर मैप करना भी मददगार होगा। इस तरह, जब हम कोई सेलेक्टर हटाते हैं, तो हम एरे में उसके इंडेक्स को जल्दी से देख सकते हैं। फिर, हम उस प्रविष्टि (entry) को अंतिम प्रविष्टि के साथ स्वैप करते हैं और सूची को पॉप (pop) करते हैं। इसलिएselector => addressको संग्रहीत करने के बजाय हम एक struct संग्रहीत करते हैं जो एड्रेस और एरे में सेलेक्टर की स्थिति (position) रखता है। इसलिए, हमारी मैपिंगselector => (address, index_in_selectors)रखती है।
नीचे दिया गया कोड उपरोक्त दो बिंदुओं को लागू करता है:
selectorsकेवल सेलेक्टर्स की सूची है- struct
FacetAddressAndSelectorPosition, facetAddress को औरselectorsमें सेलेक्टर का इंडेक्स कहाँ है, इसे संग्रहीत करता है
struct FacetAddressAndSelectorPosition {
address facetAddress;
uint16 selectorPosition; // index of the selector in `selectors`
}
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition;
bytes4[] selectors;
}
struct को EIP-7201 के समान पैटर्न का उपयोग करके एक्सेस किया जा सकता है।
स्टोरेज को प्रबंधित करने के लिए LibDiamond
चीजों को सरल रखने के लिए, उपरोक्त जानकारी रखने वाला struct और struct में स्टोरेज पॉइंटर सेट करने वाला फ़ंक्शन LibDiamond नामक एक अलग लाइब्रेरी में रखा जा सकता है। लाइब्रेरी एक फ़ंक्शन 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;
}
}
अपग्रेडेबल डायमंड्स के लिए, यह पारंपरिक है कि IDiamondLoupe फ़ंक्शंस को उनके अपने फैसेट में रखा जाए, न कि प्रॉक्सी में। इस कॉन्ट्रैक्ट का नाम क्या रखा जाए इसके लिए कोई आवश्यकता नहीं है, लेकिन DiamondLoupeFacet यथोचित रूप से विवरणात्मक (descriptive) है। नीचे हम DiamondLoupeFacet दिखा रहे हैं जो बाहरी facetAddress फ़ंक्शन को लागू करने के लिए LibDiamond लाइब्रेरी का उपयोग कर रहा है जो IDiamondLoupe का हिस्सा है।
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
}
डायमंड स्टैंडर्ड के रेफरेंस इम्प्लीमेंटेशन
EIP-2535 के लेखक निक मडज (Nick Mudge), निम्नलिखित रेपो में तीन रेफरेंस इम्प्लीमेंटेशन (diamond-1, diamond-2, और diamond-3) बनाए रखते हैं:
https://github.com/mudgen/diamond
ये कार्यान्वयन उन ट्रेडऑफ़ (tradeoffs) के लिए ऑप्टिमाइज़ करते हैं जिन पर हमने पहले चर्चा की थी: यदि IDiamondLoupe में view फ़ंक्शंस को ऑन-चेन क्वेरी करना सस्ता है, तो उन्हें अपडेट करना महंगा होगा और इसके विपरीत।
Diamond-1 और diamond-2 फैसेट्स और सेलेक्टर्स को ट्रैक करने के लिए यथासंभव कम स्टोरेज का उपयोग करते हैं, केवल फ़ंक्शन सेलेक्टर्स की सूची और फ़ंक्शन सेलेक्टर से फैसेट एड्रेस तक एक मैपिंग का उपयोग करते हैं। नीचे हम diamond-1 के लिए स्टोरेज देखते हैं।
ध्यान दें कि रेफरेंस इम्प्लीमेंटेशन 8 फ़ंक्शन सेलेक्टर्स को एक स्लॉट में पैक करने के लिए फ़ंक्शन सेलेक्टर्स के एरे को uint256 => bytes32 सेलेक्टर स्लॉट्स की मैपिंग के रूप में लागू करता है। मैपिंग्स एरे की तुलना में गैस के मामले में थोड़ी अधिक कुशल होती हैं क्योंकि वे लुकअप (lookup) करने से पहले एरे की लंबाई (length) की परोक्ष रूप से जांच नहीं करती हैं। इस “एरे” की लंबाई अलग से 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 स्पष्ट रूप से फैसेट एड्रेस और फैसेट एड्रेस से फ़ंक्शन सेलेक्टर्स की सूची तक एक मैपिंग संग्रहीत करता है जो इसमें होती है:
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 फ़ंक्शंस अधिक जटिल हैं क्योंकि उन्हें फैसेट एड्रेस को सूचीबद्ध करने के लिए सभी फ़ंक्शन सेलेक्टर्स के माध्यम से लूप करना पड़ता है, फिर केवल अद्वितीय (unique) एड्रेस वापस करने होते हैं।
डायमंड डिप्लॉय करना
डायमंड को डिप्लॉय करने और अपग्रेड करने की प्रक्रिया अन्य अपग्रेडेबल प्रॉक्सी पैटर्न के समान है:
डिप्लॉयमेंट के दौरान हम:
- फैसेट्स (इम्प्लीमेंटेशन्स) डिप्लॉय करते हैं
- कंस्ट्रक्टर के तर्कों (arguments) के रूप में फैसेट्स और सेलेक्टर्स की सूची के साथ प्रॉक्सी को डिप्लॉय करते हैं, और उसी ट्रांज़ैक्शन में इनिशियलाइज़ (initialize) करते हैं
और अपग्रेड के दौरान:
- नया फैसेट (या फैसेट्स) डिप्लॉय करते हैं
- हटाने के लिए सेलेक्टर्स की सूची और जोड़ने के लिए सेलेक्टर्स की सूची के साथ diamondCut() या यूज़र-डिफ़ाइंड फ़ंक्शन को कॉल करते हैं। एक ही ट्रांज़ैक्शन में कई नए फैसेट्स जोड़ना संभव है।
डिप्लॉयमेंट और अपग्रेड के दौरान स्टोरेज वेरिएबल्स को इनिशियलाइज़ करना
प्रॉक्सी डिप्लॉय करते समय, हम कुछ शुरुआती स्टेट वेरिएबल्स सेट करना चाह सकते हैं जैसे कि हमारे पास एक कंस्ट्रक्टर हो। इसी तरह, हम Transparent Upgradeable Proxy और UUPS के OpenZeppelin कार्यान्वयन में upgradeToAndCall के समान, अपग्रेड के बाद कुछ स्टोरेज वेरिएबल्स को इनिशियलाइज़ करना चाह सकते हैं।
diamondCut में अंतिम दो तर्कों _init और _calldata का उपयोग इसी के लिए है:
function diamondCut(Facets[] facets, address _init, bytes memory _calldata)
यदि _init ≠ address(0) है तो diamondCut को तर्क के रूप में _calldata का उपयोग करके _init को delegatecall करना होगा। चूँकि diamondCut प्रॉक्सी (डायमंड) के संदर्भ (context) में चलता है, delegatecalled कॉन्ट्रैक्ट (_init) प्रॉक्सी में स्टोरेज वेरिएबल्स को इनिशियलाइज़ कर सकता है।
इनिशियलाइज़ेशन लॉजिक एक बाहरी कॉन्ट्रैक्ट द्वारा चलाया जाता है। यदि हम एक ही ट्रांज़ैक्शन में कई स्टोरेज वेरिएबल्स सेट करना चाहते हैं, तो एक विशेष उद्देश्य वाले स्मार्ट कॉन्ट्रैक्ट का उपयोग करके इसे एक ही ट्रांज़ैक्शन के रूप में करना आसान है।
एक उदाहरण कॉन्ट्रैक्ट नीचे दिखाया गया है:
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 ट्रांज़ैक्शन होता है — यह फैसेट्स के प्रारंभिक डिप्लॉयमेंट तक सीमित नहीं है। किसी फ़ंक्शन सेलेक्टर को बदलते या हटाते समय हम बाहरी कॉन्ट्रैक्ट को एटॉमिक रूप से (atomically) delegatecall भी कर सकते हैं।
DiamondInit के साथ एक मैजिक वैल्यू (magic value) की आवश्यकता पर विचार करें
EIP-2535 को सुरक्षा जांच की आवश्यकता नहीं है कि DiamondInit कॉन्ट्रैक्ट वास्तव में एक कॉन्ट्रैक्ट है, लेकिन यह जांचना निश्चित रूप से अधिक सुरक्षित है कि _init के एड्रेस में वास्तव में बाइटकोड है। अतिरिक्त सुरक्षा के लिए, ZkSync’s diamond implementation जांचता है कि _init कॉन्ट्रैक्ट को delegatecall एक मैजिक वैल्यू लौटाता है।
डायमंड स्टैंडर्ड के लिए इम्प्लीमेंटेशन विवरण
डायमंड स्टैंडर्ड का उपयोग करने का सबसे सुरक्षित तरीका ऊपर लिंक किए गए रेफरेंस कॉन्ट्रैक्ट्स का उपयोग करना है, क्योंकि इनका ऑडिट किया जा चुका है। यदि आप IDiamondLoupe फ़ंक्शंस को ऑन-चेन कॉल करने का इरादा नहीं रखते हैं (जो आमतौर पर होता है), तो diamond-2 सबसे अधिक गैस कुशल होगा क्योंकि यह ERC-2535 के अनुरूप होने के लिए आवश्यक स्टोरेज वेरिएबल्स की सबसे कम संख्या का उपयोग करता है।
नॉन-अपग्रेडेबल डायमंड्स — मैपिंग के बजाय बाइनरी सर्च का उपयोग करें
नॉन-अपग्रेडेबल डायमंड्स के लिए, हम फ़ंक्शन सेलेक्टर्स के बीच संबंध को हार्डकोड करने की सलाह देते हैं। फैसेट एड्रेस खोजने के लिए लंबे if-else स्टेटमेंट का उपयोग करने के बजाय, बाइनरी सर्च का उपयोग करें।
उदाहरण के रूप में, निम्नलिखित फ़ंक्शन पर विचार करें जो तर्क (argument) के रूप में एक फ़ंक्शन सेलेक्टर लेता है और एक एड्रेस लौटाता है (कोड Pendle Finance से लिया गया है, जो एक डायमंड प्रॉक्सी का उपयोग करता है):

कोड फ़ंक्शन सेलेक्टर पर बाइनरी सर्च करता है और इम्प्लीमेंटेशन कॉन्ट्रैक्ट (फैसेट) का एड्रेस लौटाता है जो फ़ंक्शन को धारण करता है। बस इतना ही।
उपरोक्त एड्रेस इम्यूटेबल एड्रेस हैं जो कंस्ट्रक्टर में सेट किए गए हैं, जो आवश्यकतानुसार DiamondCut इवेंट भी एमिट करता है:

ऐसी लाइब्रेरीज़ का उपयोग न करें जो स्टोरेज वेरिएबल्स में लिखती हैं जब तक कि वे EIP-7201 का उपयोग न करें
कोई भी फैसेट जो गैर-नेमस्पेस (non-namespaced) स्टोरेज में लिखता या पढ़ता है, उसमें स्टोरेज कॉलिज़न (storage collision) होने की संभावना होती है। हम सभी स्टोरेज को प्रबंधित करने के लिए EIP-7201 का उपयोग करने की सलाह देते हैं।
दूसरे फैसेट में फ़ंक्शंस को कॉल करना
जब किसी फैसेट (इम्प्लीमेंटेशन कॉन्ट्रैक्ट) में कोई फ़ंक्शन चलता है, तो वह डायमंड (प्रॉक्सी कॉन्ट्रैक्ट) के संदर्भ में ऐसा करता है। इसलिए, किसी अन्य फैसेट में public फ़ंक्शन को कॉल करने के लिए, हम प्रॉक्सी एड्रेस को कॉल कर सकते हैं।
हमारे प्रारंभिक उदाहरण पर विचार करें जहां हमारे पास फ़ंक्शन add() के साथ एक फैसेट Add था और एक अलग फैसेट Multiply था। मान लीजिए कि हम Multiply फैसेट से add() को कॉल करना चाहते हैं। नीचे हम दिखाते हैं कि इसे कैसे पूरा किया जाए; Multiply फैसेट डायमंड प्रॉक्सी के एड्रेस को निर्दिष्ट करके add() फ़ंक्शन को कॉल करता है:
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स्वयं को कॉल करता है (प्रॉक्सी के संदर्भ में) - प्रॉक्सी फ़ंक्शन सेलेक्टर को Add फैसेट से मैच करता है
- प्रॉक्सी Add फैसेट को 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) करना थोड़ा व्यर्थ है। गैस बचाने के लिए, एक फैसेट दूसरे फैसेट को “सीधे” delegatecall कर सकता है। परदे के पीछे, कॉलिंग फैसेट का लॉजिक प्रॉक्सी में चलता है, और लॉजिक दूसरे फैसेट को delegatecall कर रहा है, जिससे वह फैसेट प्रॉक्सी में स्टोरेज वेरिएबल्स को “देख” सकता है। इसे पूरा करने के लिए कोड उतना साफ़ नहीं होगा क्योंकि 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) नहीं है और एक साधारण कॉल करने के लिए अतिरिक्त तीन से चार पंक्तियों की आवश्यकता होती है।
तीसरा समाधान एक ऐसी सॉलिडिटी लाइब्रेरी (या लाइब्रेरीज़) बनाना है जिसमें केवल internal फ़ंक्शंस हों, फिर उन internal फ़ंक्शंस को किसी भी फैसेट के भीतर इम्पोर्ट करना जिसे उनकी आवश्यकता हो। इससे फैसेट्स के बीच डुप्लिकेट बाइटकोड हो सकता है, लेकिन यदि फैसेट्स 24kb की सीमा से अधिक नहीं हैं या डिप्लॉयमेंट लागत में अत्यधिक वृद्धि नहीं करते हैं, तो यह कोई बड़ी समस्या नहीं होनी चाहिए।
उदाहरण डायमंड
हम एक डायमंड बनाएंगे जो काउंटर कॉन्ट्रैक्ट के रूप में कार्य करता है। एक फैसेट काउंटर के मान (value) के view फ़ंक्शन को धारण करेगा और दूसरा फैसेट काउंटर को बढ़ाने (increment) के लॉजिक को धारण करेगा।
डायमंड init लॉजिक को स्पष्ट करने के लिए, हम अपना काउंटर 0 के बजाय 8 से शुरू करेंगे।
आरंभ करने के लिए, diamond-2 reference hardhat repository को फोर्क (fork) करें। हमें किसी भी ऑडिट किए गए Foundry कार्यान्वयन के बारे में जानकारी नहीं है।
IncrementLibrary
नेमस्पेस स्टोरेज को पढ़ने के लिए कोड को एक लाइब्रेरी में रखना मददगार होता है जिसे फैसेट्स इम्पोर्ट कर सकें। काउंटर को अपडेट करने के लॉजिक को लाइब्रेरी के अंदर रखना एक डिज़ाइन विकल्प है। इंक्रीमेंट लॉजिक को सीधे लाइब्रेरी में रखने से हर दूसरे फैसेट के लिए बाइटकोड साइज़ बढ़ जाएगा जो उस लाइब्रेरी के फ़ंक्शंस को कॉल करता है। हालाँकि, यह इंक्रीमेंट फैसेट के लॉजिक को भी सरल बनाता है, क्योंकि इंक्रीमेंट फैसेट को इस बारे में कुछ भी जानने की आवश्यकता नहीं है कि नेमस्पेस्ड स्टोरेज कैसे संरचित (structured) है।
ध्यान दें कि इस लाइब्रेरी में सभी फ़ंक्शंस internal होने चाहिए, क्योंकि सॉलिडिटी कंपाइलर को उम्मीद है कि external फ़ंक्शंस वाली लाइब्रेरीज़ अलग से डिप्लॉय की जाएंगी।
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 के साथ स्टोरेज वेरिएबल को इनिशियलाइज़ करना
इनिशियलाइज़ेशन एक अलग कॉन्ट्रैक्ट में होता है जिसे diamondCut() कॉन्ट्रैक्ट delegatecall करता है। रेफरेंस कार्यान्वयन में, यह contracts/upgradeInitializers/DiamondInit.sol में है।
हम संक्षिप्तता के लिए पूरा कॉन्ट्रैक्ट नहीं दिखाते हैं। 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
}
}
फैसेट जो केवल x को देखता है (views x)
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();
}
}
फैसेट जो केवल x को बढ़ाता है (increments x)
contracts/facets में IncrementFacet.sol बनाएँ:
import { LibInc } from "../libraries/LibInc.sol";
contract IncrementFacet {
function increment() external {
LibInc.increment();
}
}
निम्नलिखित टेस्ट जोड़ें
test/diamondTest.js में निम्नलिखित टेस्ट जोड़ें
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)
});
नए फैसेट्स डिप्लॉय करने के लिए scripts/deploy.js को अपडेट करें
scripts/deploy.js खोलें और फैसेट्स के कॉन्ट्रैक्ट नाम जोड़ें। हार्डहैट (Hardhat) फ़ाइल पथ (file paths) निर्दिष्ट किए बिना कॉन्ट्रैक्ट्स खोजने के लिए काफी स्मार्ट है। परिवर्तन नीचे लाल बॉक्स में दिखाया गया है:

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

इसके साथ टेस्ट चलाएँ:
npx hardhat test
ध्यान दें कि अन्य टेस्ट विफल हो जाएंगे क्योंकि वे नए सेलेक्टर्स की अपेक्षा नहीं कर रहे हैं। हम अपने टेस्ट पर .only मॉडिफ़ायर (modifier) का उपयोग करके इसे अनदेखा करते हैं। .only मॉडिफ़ायर अन्य यूनिट टेस्ट्स को चलने से रोकता है और केवल .only के साथ संशोधित यूनिट टेस्ट चलाता है।
यदि आप समस्याओं का सामना करते हैं, तो सुनिश्चित करें कि आप नोड (Node) संस्करण 20 का उपयोग कर रहे हैं।
सारांश
- डायमंड पैटर्न एक प्रॉक्सी है जिसमें कई इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स होते हैं।
- डायमंड जानता है कि आने वाले calldata के फ़ंक्शन सेलेक्टर के आधार पर किस फैसेट को delegatecall करना है।
- यदि डायमंड अपग्रेडेबल है, तो सेलेक्टर से इम्प्लीमेंटेशन एड्रेस तक की मैपिंग को diamondCut के माध्यम से बदला जा सकता है। इस मैपिंग को बदलने का लॉजिक मानक द्वारा निर्देशित नहीं है, यह प्रॉक्सी बाइटकोड का हिस्सा या किसी फैसेट का हिस्सा हो सकता है।
- डायमंड में सभी फैसेट्स और सेलेक्टर्स एमिट किए गए इवेंट्स और IDiamondLoupe में public फ़ंक्शंस के माध्यम से निर्धारित किए जा सकने चाहिए (determinable)।
- अधिक डेटा स्ट्रक्चर्स जोड़कर
IDiamondLoupeमें फ़ंक्शंस को देखने (viewing) की गैस लागत को कम किया जा सकता है। हालाँकि, इससे अपग्रेड की लागत बढ़ जाती है। लगभग सभी मामलों में, हमें कम डेटा स्ट्रक्चर्स का विकल्प चुनना चाहिए क्योंकिIDiamondLoupeफ़ंक्शंस ऑफ-चेन (offchain) यूज़र्स के लिए अभिप्रेत हैं।
हम इस लेख के पूर्व संस्करण पर टिप्पणियाँ प्रदान करने के लिए Nick Mudge को धन्यवाद देना चाहेंगे।