ERC-7201 (पूर्व में EIP-7201) storage variables को एक सामान्य आइडेंटिफायर, जिसे namespace कहा जाता है, द्वारा एक साथ ग्रुप करने और NatSpec एनोटेशन के माध्यम से variables के समूह को डॉक्यूमेंट करने का एक स्टैंडर्ड है। इस स्टैंडर्ड का उद्देश्य अपग्रेड के दौरान storage variables के प्रबंधन को सरल बनाना है।
Namespaces
प्रोग्रामिंग भाषाओं में नेमिंग कॉन्फ्लिक्ट्स (naming conflicts) को रोकने के लिए संबंधित आइडेंटिफायर्स जैसे कि variables, functions, classes, या modules को व्यवस्थित और ग्रुप करने के तरीके के रूप में Namespaces एक सामान्य दृष्टिकोण है। Solidity में मूल रूप से namespaces का कॉन्सेप्ट नहीं है, लेकिन हम इसे सिमुलेट (simulate) कर सकते हैं। हमारे मामले में, हम contract state variables को एक namespace में एक साथ ग्रुप करना चाहते हैं।
Solidity में namespaces का उपयोग करने का विचार सबसे पहले ERC-7201 द्वारा प्रस्तावित नहीं किया गया था; इसका उपयोग diamond proxy pattern (ERC-2535) द्वारा भी किया जाता है। अपग्रेडेबल (upgradeable) smart contracts में namespaces के उपयोग के महत्व को समझने के लिए, किसी को उस समस्या को समझना होगा जिसे ERC-7201 हल करना चाहता है।
Inheritance के साथ एक समस्या
प्रदर्शन के उद्देश्यों के लिए, आइए एक अपग्रेडेबल (upgradable) contract की जांच करें जिसमें एक proxy contract और एक implementation contract शामिल है जिसे एक parent और एक child contract के बीच inheritance का उपयोग करके बनाया गया है। Implementation की ओर, हमारे पास एक parent contract और एक child contract है, जिनमें से प्रत्येक के प्रारंभिक स्लॉट (initial slot) में एक state variable है। इन implementation contracts के storage structure को proxy contract में रेप्लिकेट किया जाएगा, जो एक transparent proxy हो सकता है। सरलता के लिए, मान लेते हैं कि प्रत्येक variable ठीक एक स्लॉट घेरता है, जिसका अर्थ है कि हम केवल uint256 या bytes32 जैसे variables का उपयोग कर रहे हैं।

समस्या तब उत्पन्न होती है जब अपग्रेड के दौरान implementation contracts में state variables का लेआउट बदल दिया जाता है। एक ऐसे परिदृश्य पर विचार करें जहां parent contract में एक नया state variable जोड़ने की आवश्यकता होती है। परिणामस्वरूप, storage structure को इस प्रकार संशोधित किया जाएगा:

यह परिदृश्य एक चुनौती प्रस्तुत करता है: जहां variableB पहले मौजूद था, अब variableC रखा जाएगा। अपग्रेड ने storage layout को बाधित कर दिया, जिसके परिणामस्वरूप नया variableC पुराने variableB के मान (value) को पढ़ने लगा, जो कि एक slot collision है।
Gap दृष्टिकोण
OpenZeppelin ने संस्करण 4 (version 4) तक अपने upgradeable contracts में प्रत्येक contract के अंत में एक “gap” डालकर इस समस्या का समाधान किया। नीचे, हम ERC20Upgradeable.sol v4.9 contract का कोड देख सकते हैं।
![uint256[45] private __gap variable के लिए कोड स्निपेट](https://static.wixstatic.com/media/706568_b9dbd4392cf641d296c29878da33df6d~mv2.png/v1/fill/w_740,h_197,al_c,q_85,usm_0.66_1.00_0.01,enc_auto/706568_b9dbd4392cf641d296c29878da33df6d~mv2.png)
__gap variable के आकार की गणना इस प्रकार की जाती है कि contract हमेशा 50 उपलब्ध storage slots का उपयोग करे, इसलिए ऊपर दिए गए चित्र में दिखाए गए contract में 5 state variables हैं। आइए इस कॉन्सेप्ट को अपने उदाहरण में शामिल करें।
यदि 5 state variables वाले parent contract में gap के रूप में 45 खाली स्लॉट (empty slots) वाला एक array शामिल है, तो implementation (और proxy) contract का storage structure नीचे दी गई छवि जैसा दिखेगा।

अब, अपग्रेड के मामले में parent contract द्वारा उपयोग के लिए 45 खाली स्लॉट उपलब्ध हैं। मान लीजिए कि parent contract में एक नया state variable, variableN जोड़ने की आवश्यकता है; उस परिदृश्य में, हम बस उस variable को gap से पहले सम्मिलित करते हैं और gap के आकार को एक से कम कर देते हैं, जैसा कि नीचे दिया गया एनीमेशन स्पष्ट करता है:


Gap मौजूदा कार्यक्षमता (existing functionality) को बाधित किए बिना contracts में नए variables डालने की सुविधा प्रदान करता है, भविष्य के अतिरिक्त परिवर्धन (additions) के लिए एक प्लेसहोल्डर (placeholder) के रूप में कार्य करता है और storage collisions से बचाता है। इस दृष्टिकोण का उपयोग करते समय, सभी implementation contracts में एक gap शामिल करने की सलाह दी जाती है।
हालांकि यह दृष्टिकोण parent contract में variables सम्मिलित करने की समस्या को कम करता है, लेकिन यह implementation contracts में लेआउट को बदलने से संबंधित सभी समस्याओं को पूरी तरह से हल नहीं करता है। उदाहरण के लिए, यदि हम वर्तमान parent contract के ऊपर एक नया parent contract बनाते हैं, तो नीचे की हर चीज नए parent में storage variables की संख्या के हिसाब से नीचे शिफ्ट हो जाएगी, इसलिए केवल gap पर निर्भर रहना प्रभावी नहीं होगा।

इसलिए, slot collisions उत्पन्न किए बिना implementation contracts के लेआउट को एडजस्ट करने का एक तरीका खोजना आवश्यक है।
सर्वोत्तम समाधान (optimal solution) में inheritance श्रृंखला में प्रत्येक implementation contract को उसका अपना समर्पित storage location निर्दिष्ट करना शामिल होगा।
दुर्भाग्य से, Solidity में वर्तमान में ऐसा करने के लिए एक नेटिव मैकेनिज्म (contract में variables के लिए एक namespace) का अभाव है। इसलिए, इस प्रकृति के निर्माण (constructions) को Solidity और YUL की सीमाओं के भीतर लागू किया जाना चाहिए। यह structs का उपयोग करके प्राप्त किया जा सकता है। आइए समीक्षा करें कि Solidity में storage layout कैसे काम करता है और namespace-आधारित root layout कैसे स्थापित करें।
एक namespace-आधारित root layout
Solidity द्वारा जनरेट किए गए एक contract के storage layout को संक्षेप में इस प्रकार समझा जा सकता है, जहां L storage में स्थान (location) को दर्शाता है, n एक प्राकृत संख्या (natural number) है, और H(k) एक विशिष्ट प्रकार की कुंजी k पर लागू एक फलन (function) है, जो उदाहरण के लिए, एक mapping key या एक array का इंडेक्स (index) हो सकता है।
उपरोक्त सूत्र (formula) इंगित करता है कि state variables पाए जा सकते हैं:
- रूट (root) में, जो डिफ़ॉल्ट रूप से slot 0 है,
- व्याकरण (grammar) का कोई भी तत्व (element) प्लस एक प्राकृत संख्या।
- एक कुंजी (key) से निश्चित रूप से (deterministically) गणना किए गए एक निश्चित मान (value) के keccak के भीतर और जहां state variable रूट से स्थित है।
हमें यह महसूस करने की आवश्यकता है कि storage layout में सभी स्थान रूट (root) पर निर्भर करते हैं। Solidity किसी भी contract के लिए रूट को शून्य (zero) मान निर्दिष्ट करता है।
यदि हम किसी contract के variables को स्टोर करने के लिए अपना खुद का स्थान बनाना चाहते हैं, तो हमें उस contract के लिए अद्वितीय (unique) किसी लेबल के आधार पर रूट को “बदलने” की आवश्यकता है। यही वह सटीक लेबल है जिसे हम contract के namespace के रूप में परिभाषित करते हैं।
Smart contracts में namespaces की अवधारणा का उद्देश्य यह सुनिश्चित करना है कि namespace का उपयोग करने वाले किसी contract के storage layout का रूट अब स्लॉट शून्य (slot zero) में स्थित नहीं है, बल्कि चुने गए namespace द्वारा निर्धारित एक विशिष्ट स्लॉट में है।

इसे केवल Solidity के साथ प्राप्त करना संभव नहीं है क्योंकि कंपाइलर (compiler) हमेशा storage layout के लिए रूट के रूप में स्लॉट शून्य का उपयोग करता है, लेकिन हम structs और assembly का उपयोग करके एक तरीका खोज सकते हैं, जैसा कि हम शीघ्र ही देखेंगे।
उससे पहले, हम एक स्ट्रिंग (string) से नए रूट के मान (value) की गणना करने के लिए ERC-7201 द्वारा प्रस्तावित सूत्र (formula) की जांच करेंगे जो एक namespace के रूप में कार्य करता है।
Namespace-आधारित storage roots की गणना के लिए एक प्रस्तावित सूत्र
यदि हम किसी namespaced contract के रूट storage slot को “बदलने” जा रहे हैं, तो हमें इस नए रूट की गणना करने के लिए एक सूत्र (formula) परिभाषित करने की आवश्यकता है। इस ERC में प्रस्तावित सूत्र इस प्रकार है:
keccak256(keccak256(namespace) - 1) & ~0xff
सूत्र (formula) के पीछे का तर्क (rationale) इस प्रकार है:
keccak256namespace जनरेट करने के बाद 1 से घटाने (decrementing) से यह सुनिश्चित होता है कि hash preimage अज्ञात रहे।- दूसरी बार
keccak256hash लेने से Solidity द्वारा उत्पन्न स्लॉट्स के साथ संभावित टकरावों (conflicts) को रोकने में मदद मिलती है, क्योंकि storage में dynamic size variables का स्थानkeccak256hash द्वारा निर्धारित किया जाता है। AND NOT 0xffऑपरेशन करने से स्थान (location) का सबसे दाहिना बाइट (rightmost byte)00में बदल जाता है। यह भविष्य के अपग्रेड के लिए तैयार करता है जब Ethereum अपने storage data structure को Verkle Trees में स्विच करेगा और 256 आसन्न (adjacent) स्लॉट्स को एक साथ वार्म (warmed) किया जा सकेगा।
ऊपर प्रस्तावित सूत्र का उपयोग नए रूट की एक महत्वपूर्ण विशेषता (crucial property) की गारंटी देने के लिए किया जाता है: कि यह किसी मूल व्याकरण तत्व (original grammar element) के साथ नहीं टकराता है — यानी storage locations का वह संभावित स्थान जिसे Solidity कंपाइलर डिफ़ॉल्ट रूप से किसी variable को असाइन कर सकता है।
यदि आप प्रयास करना चाहते हैं, तो एक Solidity contract जो दिए गए namespace से root location value की गणना करता है, इस प्रकार है:
pragma solidity ^0.8.20;
contract Erc7201 {
function getStorageAddress(
string calldata namespace
) public pure returns (bytes32) {
return
keccak256(
abi.encode(uint256(keccak256(abi.encodePacked(namespace))) - 1)
) & ~bytes32(uint256(0xff));
}
}
यदि हम openzeppelin.storage.ERC20 प्लग इन करते हैं तो हमें निम्नलिखित hash प्राप्त होता है।
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20")) - 1)) ^ bytes32(uint256(0xff))
bytes32 private constant ERC20StorageLocation = 0x52C63247Ef47d19d5ce046630c49f7C67dcaEcfb71ba98eedaab2ebca6e0;
वास्तव में, इसी तरह OpenZeppelin ERC20UpgradeableContract v5 के लिए storage root सेट करता है जैसा कि हम आगामी अनुभाग (section) में देखेंगे।
Variables के रूप में Struct fields
पिछले अनुभाग (section) में, हमने देखा कि किसी contract के namespace के आधार पर उसके रूट की गणना कैसे की जाती है। अब हमें उस नए रूट से शुरू करके storage variables को एक साथ ग्रुप करने में सक्षम होना चाहिए। हम state variables डिक्लेयर नहीं कर सकते क्योंकि ऐसा करने से, Solidity स्लॉट 0 (slot 0) से variables का आवंटन (allocating) शुरू कर देगा, जिससे हम बचना चाहते हैं।
Variables को एक साथ ग्रुप करने के लिए, हम एक struct का उपयोग करते हैं। एक struct के भीतर, fields सामान्य storage slot ऑर्डरिंग का पालन करते हैं। निम्नलिखित contract पर विचार करें:
contract StructStorage {
// **ERC-7201 uses a struct to group variables together, but the struct is never
// actually declared, nor any other state variable.**
struct MyStruct {
uint256 fieldA;
uint256 fieldB;
mapping(address => uint256) fieldC;
}
// Contract functions...
}
काल्पनिक रूप से, यदि हम इस struct को पहले storage variable के रूप में डिक्लेयर करते हैं (जो ERC-7201 नहीं करता है), fieldA स्लॉट 0 में होगा, fieldB स्लॉट 1 में होगा, fieldC mapping का बेस (base) storage 2 में होगा, इत्यादि। Storage में वह स्थान खोजने का एक सूत्र (formula) जहां एक struct-प्रकार के variable का field लिखा जा सकता है, इस प्रकार है, जहां struct base वह स्लॉट है जहां struct storage slots पर कब्जा करना शुरू करता है।
ध्यान दें कि यह storage layout के लिए पहले वाले सूत्र (formula) के समान ही है; हमने बस रूट को struct के बेस (base) से बदल दिया है, यानी, struct अपने fields के माध्यम से storage layout को बनाए रखता है। इसका मतलब है कि हम नए रूट के रूप में struct base का उपयोग कर सकते हैं।
ऊपर दिए गए उदाहरण में, struct base स्लॉट शून्य है, लेकिन हम struct का बेस बनने के लिए कोई अन्य स्लॉट चुन सकते हैं। यह YUL का उपयोग करके किया जा सकता है, जैसा कि नीचे दिए गए उदाहरण में दिखाया गया है।
contract StructOnStorage {
// NO STATE VARIABLES
struct MyStruct{
uint256 fieldA;
mapping(uint => uint) fieldB;
}
function setMyStruct() public {
MyStruct storage myStruct; // Grab a struct
assembly {
myStruct.slot := 0x02 // Change its base slot
}
myStruct.fieldA = 100; // FieldA will be in the first slot from the base at 0x02, which is 0x02 itself
myStruct.fieldB[10] = 101; // The storage address of this mapping item will be calculated below
}
function getMyStruct() public view returns (uint256 fieldA, uint256 fieldBSingleValue) {
// keccak256(abi.encode(key, struct base + location inside the struct)
// The mapping is located in the second slot inside the struct, so struct base + 1
bytes32 locationSingleValue = keccak256(abi.encode(0x0a, 0x02 + 1));
assembly {
fieldA := sload(0x02) // Read storage at 0x02
fieldBSingleValue := sload(locationSingleValue)
}
}
}
जब हम myStruct.slot := 0x02 स्टेटमेंट का उपयोग करते हैं, तो हम स्पष्ट रूप से struct के बेस को बदल देते हैं और एक storage layout की नकल (mimic) कर सकते हैं जहां रूट अब स्लॉट शून्य (slot zero) में नहीं है। Struct के अंदर हमें उन सभी variables को रखना चाहिए जो struct fields के रूप में state variables होंगे। Struct base इसके fields के लिए नए रूट के रूप में कार्य करता है, ठीक वही जो हम प्राप्त करना चाहते थे।
इस पद्धति का एक नुकसान यह है कि हमें हर बार इसके fields को सेव करते या पढ़ते समय struct के बेस को स्पष्ट रूप से इंगित करने की आवश्यकता होती है।
चूंकि हमें हमेशा struct के बेस का संदर्भ (refer) देने की आवश्यकता होती है, इसलिए ऐसा करने के लिए एक उपयोगिता फ़ंक्शन (utility function) बनाने की सिफारिश की जाती है। OpenZeppelin के upgradeable contracts में, struct के बेस के लिए एक पॉइंटर (pointer) बनाने के लिए डिज़ाइन किया गया एक private function होता है। उदाहरण के लिए, ERC20Upgradeable.sol में**:**

नीचे, हम देखते हैं कि कैसे सभी “होने वाले (would be)” state variables को एक struct के fields के रूप में डिक्लेयर किया जाना चाहिए।
abstract contract ERC20Upgradeable is Initializable, ContextUpgradeable, IERC20, IERC20Metadata, IERC20Errors {
/// @custom:storage-location crc7201:openzeppelin.storage.ERC20
struct ERC20Storage {
mapping(address account => uint256) _balances;
mapping(address account => mapping(address spender => uint256)) _allowances;
uint256 _totalSupply;
string _name;
string _symbol;
}
आइए एक उदाहरण देखें कि हम एक struct field को प्राप्त (retrieve) करने के लिए utility function का उपयोग कैसे कर सकते हैं, जैसे ERC20Upgradeable.sol contract का टोकन नाम (token name)।
/**
* @dev Returns the name of the token.
*/
function name() public view virtual returns (string memory) {
ERC20Storage storage $ = _getERC20Storage();
return $._name;
}
जैसा कि ऊपर देखा जा सकता है, जब हम storage variables को प्राप्त (retrieve) करना चाहते हैं, तो हम बस _getERC20StorageLocation() को कॉल करते हैं जो namespace storage root को bytes32 के रूप में लौटाता है।
यही बात तब लागू होती है जब हम किसी field को अपडेट करना चाहते हैं। $ पॉइंटर (pointer) struct के बेस पर है, इसलिए हम fields को पढ़ने/अपडेट करने के लिए $.[field] सिंटैक्स (syntax) का उपयोग कर सकते हैं। नीचे दी गई छवि में, हम ERC20Upgradeable.sol contract से _update फ़ंक्शन कोड का एक स्निपेट (snippet) देखते हैं, और एक ट्रांसफर (transfer) के दौरान बैलेंस को अपडेट करने के लिए इसका उपयोग कैसे किया जाता है।

Namespace-आधारित root layout को कैसे लागू करें का सारांश
इस पैटर्न को लागू करने के लिए, बस इन चरणों का पालन करें:
- State variables का उपयोग न करें।
- होने वाले (Would be) state variables को एक struct में fields के रूप में परिभाषित किया जाना चाहिए।
- Contract के लिए एक अद्वितीय (unique) namespace चुनें।
- Namespace से इस contract के नए रूट की गणना करने के लिए एक फ़ंक्शन का उपयोग करें। ERC-7201 उपयोग किए जाने वाले एक फ़ंक्शन का प्रस्ताव करता है।
- Struct base का संदर्भ (reference) वापस करने के लिए एक utility function बनाएं। यह स्पष्ट रूप से इंगित करने के लिए assembly का उपयोग करें कि वह स्लॉट जहां struct का बेस स्थित है, वह स्लॉट है जिसकी गणना पिछले बिंदु में परिभाषित फ़ंक्शन द्वारा की गई है।
- हर बार जब आप किसी struct field को पढ़ते या अपडेट करते हैं, तो struct के बेस को इंगित करने के लिए utility function का उपयोग करें।
अगले अनुभाग (section) में, हम देखेंगे कि किसी contract के भीतर namespaces के उपयोग को कैसे डॉक्यूमेंट किया जाए।
Custom storage location के लिए NatSpec
Ethereum Natural Language Specification Format (NatSpec) उन टिप्पणियों (comments) के लिए एक विधि है जो contracts के भीतर डॉक्यूमेंटेशन के रूप में कार्य करती हैं। यहाँ एक फ़ंक्शन को डॉक्यूमेंट करने वाले NatSpec टिप्पणी (comment) का एक उदाहरण दिया गया है:
/**
* @dev Returns the name of the token.
*/
ERC-7201 के लक्ष्यों में से एक NatSpec में namespaces के उपयोग को डॉक्यूमेंट करने के लिए एक विधि प्रस्तावित करना है:
@custom:storage-location <FORMULA_ID>:<NAMESPACE_ID>
FormulaID namespace से storage root की गणना के लिए उपयोग किए गए सूत्र (formula) को दर्शाता है, जबकि namespaceId विचाराधीन (under consideration) विशिष्ट namespace को संदर्भित करता है। जिसे एनोटेट (annotate) किया गया है वह struct है, इसलिए एनोटेशन (annotation) ठीक इसके ऊपर आना चाहिए।
इस ERC में प्रस्तावित सूत्र को erc7201 के रूप में लेबल किया गया है, इसलिए एक NatSpec जो इस सूत्र का उपयोग करता है वह इस रूप का होना चाहिए:
@custom:storage-location erc7201:<NAMESPACE_ID>
एक उदाहरण के रूप में, ERC20Upgradeable contract में, चुना गया namespace openzeppelin.storage.ERC20 है, इसलिए एनोटेशन (annotation) निम्नानुसार होना चाहिए
/// @custom:storage-location erc7201:openzeppelin.storage.ERC20
struct ERC20Storage {
...
}
आभार और लेखकत्व (Acknowledgments and Authorship)
यह लेख RareSkills के सहयोग से João Paulo Morais द्वारा लिखा गया था।
हम इस लेख के पहले ड्राफ्ट (draft) पर उपयोगी टिप्पणियों के लिए OpenZeppelin के Hadrien Croubois (@Amxx) को धन्यवाद देना चाहते हैं।
मूल रूप से 13 जून को प्रकाशित