UUPS पैटर्न एक प्रॉक्सी पैटर्न है जहां अपग्रेड फंक्शन इंप्लीमेंटेशन कॉन्ट्रैक्ट (implementation contract) में मौजूद होता है, लेकिन प्रॉक्सी से एक delegatecall के माध्यम से प्रॉक्सी कॉन्ट्रैक्ट में स्टोर किए गए इंप्लीमेंटेशन एड्रेस को बदल देता है। इसका हाई-लेवल मैकेनिज्म नीचे दिए गए एनिमेशन में दिखाया गया है:

ट्रांसपेरेंट अपग्रेडेबल प्रॉक्सी (Transparent Upgradeable Proxy) के समान, UUPS पैटर्न प्रॉक्सी में पब्लिक फंक्शन्स को पूरी तरह से हटाकर फंक्शन सिलेक्टर क्लैश (function selector clashes) की समस्या को हल करता है।
यह आर्टिकल मानकर चलता है कि पाठक ने Transparent Upgradeable Proxy पैटर्न पर हमारा आर्टिकल पढ़ लिया है।
ERC-1967 प्रॉक्सी स्टोरेज स्लॉट्स स्टैंडर्ड
जैसा कि ट्रांसपेरेंट अपग्रेडेबल प्रॉक्सी पर हमारे आर्टिकल में बताया गया है, एक फंक्शनल Ethereum प्रॉक्सी के लिए कम से कम निम्नलिखित दो विशेषताओं की आवश्यकता होती है:
- एक स्टोरेज स्लॉट जो इंप्लीमेंटेशन कॉन्ट्रैक्ट का एड्रेस होल्ड करता है।
- एडमिन के लिए इंप्लीमेंटेशन एड्रेस बदलने का एक मैकेनिज्म।
ERC-1967 standard यह तय करता है कि इंप्लीमेंटेशन का एड्रेस रखने वाले स्टोरेज स्लॉट को कहां रखा जाना चाहिए, लेकिन यह तय नहीं करता कि इंप्लीमेंटेशन का एड्रेस कैसे बदला जाए, यानी, यह अपग्रेड मैकेनिज्म का चुनाव डेवलपर पर छोड़ देता है।
UUPS एक प्रॉक्सी पैटर्न है जिसमें इंप्लीमेंटेशन कॉन्ट्रैक्ट का एड्रेस बदलने का मैकेनिज्म प्रॉक्सी कॉन्ट्रैक्ट के बजाय स्वयं इंप्लीमेंटेशन कॉन्ट्रैक्ट में ही मौजूद होता है।
इस अंतर को निम्नलिखित सरल कोड में दर्शाया गया है:

एक अपग्रेड के दौरान, _upgradeLogic() फंक्शन को UUPSProxy में delegatecall किया जाता है। ट्रांसपेरेंट अपग्रेडेबल प्रॉक्सी के विपरीत, इसमें AdminProxy की कोई आवश्यकता नहीं है — यदि चाहें तो एक सामान्य EOA एडमिन हो सकता है।
ट्रांसपेरेंट अपग्रेडेबल प्रॉक्सी एडमिन के एड्रेस को स्थिर (constant) रखने के लिए AdminProxy का उपयोग करता था। चूंकि एक ट्रांसपेरेंट अपग्रेडेबल प्रॉक्सी को हर ट्रांजैक्शन पर एडमिन के साथ msg.sender की तुलना करनी होती है, इसलिए msg.sender की तुलना एक इम्यूटेबल (immutable) वेरिएबल से करना उचित होता है। हालांकि, एक UUPS प्रॉक्सी को केवल यह जांचने की आवश्यकता होती है कि क्या msg.sender एडमिन है, यदि वे स्पष्ट रूप से प्रॉक्सी पर _upgradeLogic() को कॉल कर रहे हैं (जो इंप्लीमेंटेशन में _upgradeLogic() को delegatecall करता है)।
इस पैटर्न के फायदों में से एक यह है कि इंप्लीमेंटेशन लॉजिक को स्वयं अपग्रेड किया जा सकता है, यानी, अपग्रेडेबिलिटी मैकेनिज्म को एक इंप्लीमेंटेशन से दूसरे इंप्लीमेंटेशन में संशोधित किया जा सकता है। उदाहरण के लिए, एक साधारण अपग्रेड लॉजिक से वोटिंग या टाइमलॉक मैकेनिज्म वाले अधिक जटिल लॉजिक में बदलना संभव हो जाता है।
इस स्टैंडर्ड का एक महत्वपूर्ण ट्रेडऑफ (tradeoff) यह है कि यदि किसी ऐसे नए इंप्लीमेंटेशन कॉन्ट्रैक्ट में अपग्रेड किया जाता है जिसमें वैध अपग्रेड मैकेनिज्म की कमी है, तो अपग्रेड चेन समाप्त हो जाती है, क्योंकि अगले इंप्लीमेंटेशन पर जाना संभव नहीं होता। दूसरे शब्दों में, चूंकि अपग्रेड मैकेनिज्म स्वयं अपग्रेडेबल हो सकता है, इसलिए अपग्रेड मैकेनिज्म के टूटने का जोखिम होता है।
इस ट्रेडऑफ से निपटने के लिए, कुछ प्रस्ताव रखे गए हैं जो यह जांचते हैं कि क्या नए इंप्लीमेंटेशन कॉन्ट्रैक्ट में वैध अपग्रेड मैकेनिज्म है या नहीं, इससे पहले कि उस पर माइग्रेट किया जाए। UUPS ऐसा ही एक प्रस्ताव है।
इस आर्टिकल में, हम बताएंगे कि सामान्य रूप से UUPS कैसे काम करता है, OpenZeppelin इंप्लीमेंटेशन की विस्तार से जांच करेंगे, और कुछ कमजोरियों (vulnerabilities) पर चर्चा करेंगे जिन्हें इस पैटर्न का उपयोग करते समय ध्यान में रखा जाना चाहिए।
UUPS बनाम Transparent Proxy
OpenZeppelin वर्तमान में Transparent और UUPS प्रॉक्सी स्टैंडर्ड दोनों के लिए इंप्लीमेंटेशन प्रदान करता है, लेकिन बाद वाले (UUPS) का उपयोग करने की सलाह देता है। इसका कारण यह है कि अपग्रेड मैकेनिज्म को संशोधित करने के लचीलेपन (flexibility) के अलावा, UUPS इंप्लीमेंटेशन हल्का होता है और इसलिए डिप्लॉयमेंट और उपयोग के दौरान कम गैस (gas) की खपत करता है।
ऐसा इसलिए है क्योंकि किसी एडमिनिस्ट्रेशन कॉन्ट्रैक्ट को डिप्लॉय करने या यह जांचने की कोई आवश्यकता नहीं है कि क्या ट्रांजैक्शन कॉन्ट्रैक्ट ओनर द्वारा किया गया है, जैसा कि Transparent Proxies में आवश्यक होता है। हालांकि, इस पैटर्न में प्रत्येक नए इंप्लीमेंटेशन कॉन्ट्रैक्ट के लिए एक अपग्रेड फंक्शन की आवश्यकता होती है, जो नए इंप्लीमेंटेशन कॉन्ट्रैक्ट की डिप्लॉयमेंट कॉस्ट (deployment costs) को थोड़ा बढ़ा देता है।
यदि UUPS का उपयोग करते समय इंप्लीमेंटेशन कॉन्ट्रैक्ट्स 24kb की साइज लिमिट तक पहुंच रहे हैं, तो Transparent Upgradeable Pattern अधिक उपयुक्त हो सकता है क्योंकि इसमें अपग्रेड लॉजिक को शामिल करने की आवश्यकता नहीं होती है।
UUPS कैसे काम करता है
UUPS को शुरुआत में ERC-1822 में परिभाषित किया गया था।
जैसा कि हमने पिछले सेक्शन में देखा, प्रॉक्सी कॉन्ट्रैक्ट को एक ऐसे नए इंप्लीमेंटेशन कॉन्ट्रैक्ट को स्वीकार करने से रोकना आवश्यक है जो UUPS स्टैंडर्ड को लागू नहीं करता है। दूसरे शब्दों में, किसी भी नॉन-UUPS-कंप्लायंट (non-UUPS-compliant) इंप्लीमेंटेशन कॉन्ट्रैक्ट पर माइग्रेट करने का प्रयास रिवर्ट (revert) होना चाहिए।
proxiableUUID() फंक्शन
इसीलिए स्टैंडर्ड यह अनिवार्य करता है कि प्रत्येक इंप्लीमेंटेशन कॉन्ट्रैक्ट में proxiableUUID() सिग्नेचर वाला एक फंक्शन शामिल हो। इस फंक्शन का उद्देश्य कम्पैटिबिलिटी चेक (compatibility check) के रूप में काम करना है ताकि यह सुनिश्चित किया जा सके कि नया इंप्लीमेंटेशन कॉन्ट्रैक्ट यूनिवर्सल अपग्रेडेबल प्रॉक्सी स्टैंडर्ड (Universal Upgradeable Proxy Standard) का पालन करता है।
फंक्शन को वह स्टोरेज स्लॉट वापस (return) करना चाहिए जहां इंप्लीमेंटेशन एड्रेस स्टोर किया गया है। हालांकि रिटर्न वैल्यू मनमानी (arbitrary) है और स्टैंडर्ड के समर्थक फंक्शन को “Hey, I’m UUPS compliant” जैसी स्ट्रिंग रिटर्न करने के लिए परिभाषित कर सकते थे, लेकिन स्टोरेज स्लॉट रिटर्न करना अधिक गैस-कुशल (gas efficient) है।
विचार यह है कि वास्तव में उस पर माइग्रेट करने से पहले नए इंप्लीमेंटेशन में proxiableUUID() फंक्शन को कॉल (invoke) किया जाए। यदि नए इंप्लीमेंटेशन कॉन्ट्रैक्ट ने proxiableUUID() को सही ढंग से लागू किया है, तो इसे UUPS-कंप्लायंट माना जाता है, और माइग्रेशन जारी रहना चाहिए। अन्यथा, ट्रांजैक्शन को रोल बैक (rolled back) किया जाना चाहिए।
सफल माइग्रेशन और विफल माइग्रेशन प्रयास दोनों की प्रक्रिया को नीचे दिए गए चित्र में दर्शाया गया है।

स्टोरेज स्लॉट
मूल प्रस्ताव सुझाव देता है कि स्टोरेज स्लॉट एड्रेस को keccak256("PROXIABLE") फॉर्मूले द्वारा परिभाषित किया जाए, हालांकि, चूंकि OpenZeppelin इंप्लीमेंटेशन ERC-1967 स्टैंडर्ड का उपयोग करता है, इसलिए उनके इंप्लीमेंटेशन में स्लॉट एड्रेस को keccak256("eip1967.proxy.implementation") - 1 द्वारा परिभाषित किया गया है। हम इसे जल्द ही कोड में देखेंगे।
एक नए इंप्लीमेंटेशन में माइग्रेशन प्रक्रिया दिखाने वाला एनिमेशन नीचे देखा जा सकता है:
OpenZeppelin UUPS Upgradeable का वॉकथ्रू
OpenZeppelin लाइब्रेरी में UUPS स्टैंडर्ड को लागू करने वाले कॉन्ट्रैक्ट का नाम UUPSUpgradeable.sol है। इस कॉन्ट्रैक्ट को इंप्लीमेंटेशन कॉन्ट्रैक्ट्स द्वारा इनहेरिट (inherit) किया जाना चाहिए, न कि प्रॉक्सी कॉन्ट्रैक्ट द्वारा। प्रॉक्सी आमतौर पर ERC1967Proxy से इनहेरिट करता है, जो ERC-1967 स्टैंडर्ड का पालन करने वाला एक मिनिमल प्रॉक्सी कॉन्ट्रैक्ट है।
UUPSUpgradeable.sol के दो उद्देश्य हैं:
- यह
proxiableUUID()फंक्शन प्रदान करता है, जिसे UUPS-कंप्लायंट होने के लिए हर इंप्लीमेंटेशन में शामिल होना चाहिए, - यह
updateToAndCall()फंक्शन भी प्रदान करता है, जिसका उपयोग नए इंप्लीमेंटेशन में माइग्रेट करने के लिए किया जाता है। जैसा कि हमने देखा है, इस उद्देश्य वाला एक फंक्शन प्रत्येक इंप्लीमेंटेशन कॉन्ट्रैक्ट में मौजूद होना चाहिए।
proxiableUUID() फंक्शन
proxiableUUID() फंक्शन, जिसे माइग्रेशन से पहले नए इंप्लीमेंटेशन कॉन्ट्रैक्ट्स में कॉल किया जाना चाहिए, को निम्नानुसार परिभाषित किया गया है, और यह ERC-1967 स्टैंडर्ड का स्टोरेज स्लॉट रिटर्न करता है।
function proxiableUUID() external view virtual notDelegated returns (bytes32) {
return ERC1967Utils.IMPLEMENTATION_SLOT; // conformal to the ERC-1967 standard
}
upgradeToAndCall फंक्शन
अगले इंप्लीमेंटेशन में अपग्रेड करने के लिए जिम्मेदार फंक्शन का कोई भी नाम हो सकता है। चूंकि यह इंप्लीमेंटेशन कॉन्ट्रैक्ट के भीतर परिभाषित होता है, इसलिए फंक्शन सिग्नेचर कॉलिजन (function signature collisions) का कोई जोखिम नहीं होता है, जैसा कि Transparent Proxy में भी होता है।
UUPSUpgradeable.sol में इस फंक्शन का नाम upgradeToAndCall है और इसकी परिभाषा नीचे दी गई है:
function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy {
// checks whether the upgrade can proceed
_authorizeUpgrade(newImplementation);
// upgrade to the new implementation
_upgradeToAndCallUUPS(newImplementation, data);
}
function _authorizeUpgrade(address newImplementation) internal virtual;
function _upgradeToAndCallUUPS(address newImplementation, bytes memory data) private {
// checks whether the new implementation implements ERC-1822
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) {
revert UUPSUnsupportedProxiableUUID(slot);
}
ERC1967Utils.upgradeToAndCall(newImplementation, data);
} catch {
// The implementation is not UUPS
revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation);
}
}
चूंकि यह एक पब्लिक फंक्शन है जिसे केवल सक्रिय प्रॉक्सी के माध्यम से ही कॉल किया जाना चाहिए, इसलिए यह सुनिश्चित करने के लिए इसमें onlyProxy मॉडिफायर (modifier) होता है।
_authorizeUpgrade इंटरनल फंक्शन
कोड में _authorizeUpgrade फंक्शन को लागू करना डेवलपर की जिम्मेदारी है। यह फंक्शन निर्धारित करता है कि अपग्रेड कौन कर सकता है। एक सीधा-सादा इंप्लीमेंटेशन अपग्रेड करने के लिए केवल ओनरशिप (ownership) की जांच कर सकता है, जैसा कि नीचे दिखाया गया है:
function _authorizeUpgrade(address newImplementation)
internal onlyOwner override {}
जैसा कि पहले बताया गया है, प्रत्येक नए इंप्लीमेंटेशन का अपना _authorizeUpgrade फंक्शन यूनिक लॉजिक (unique logic) के साथ हो सकता है। उदाहरण के लिए, यदि ओनर किसी नए इंप्लीमेंटेशन में मल्टी-सिग्नेचर स्कीम (multi-signature scheme) में स्विच करना चाहता है, तो आवश्यक कोड को इस फंक्शन में शामिल किया जा सकता है।
चूंकि UUPSUpgradeable एक एब्सट्रैक्ट कॉन्ट्रैक्ट (abstract contract) है, इसलिए कोड तब तक कंपाइल नहीं होगा जब तक कि आप स्पष्ट रूप से _authorizeUpgrade को लागू नहीं करते।
Remix का उपयोग करके UUPS के बारे में सीखना
इस सेक्शन में, हम UUPS के मूल काम-काज का स्पष्ट विज़ुअलाइज़ेशन (visualization) प्राप्त करने के लिए Remix का उपयोग करेंगे। हालांकि Remix प्रॉक्सी का उपयोग करके एडवांस्ड डिप्लॉयमेंट फीचर्स प्रदान करता है, लेकिन अंडरलाइंग (underlying) प्रक्रियाओं को पारदर्शी रखने के लिए हम यहां उन फीचर्स का उपयोग नहीं करेंगे।
साथ ही, कोड को अधिक संक्षिप्त (concise) बनाने के लिए, हम इनिशियलाइज़ेशन (initialization) फंक्शन्स और मॉडिफायर्स को छोड़ देंगे। यह हमारे कोड को असुरक्षित बना देगा, लेकिन लक्ष्य इस मुख्य कांसेप्ट को समझने पर ध्यान केंद्रित करना है कि UUPS कैसे काम करता है।
प्रॉक्सी कॉन्ट्रैक्ट
हमारा प्रॉक्सी कॉन्ट्रैक्ट ERC1967Proxy.sol लाइब्रेरी का उपयोग करेगा, जो ERC-1967 स्टैंडर्ड के अनुरूप एक मिनिमल प्रॉक्सी स्कीम को लागू करता है। प्रारंभिक इंप्लीमेंटेशन कॉन्ट्रैक्ट का एड्रेस कंस्ट्रक्टर (constructor) में पास किया जाता है। हालांकि, प्रॉक्सी में स्वयं नए इंप्लीमेंटेशन में अपडेट करने के लिए किसी मैकेनिज्म का अभाव होता है; इस मैकेनिज्म को इंप्लीमेंटेशन कॉन्ट्रैक्ट के भीतर ही लागू किया जाना चाहिए।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract UUPSProxy is ERC1967Proxy {
constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) payable {}
}
कॉपी और पेस्ट करने के लिए कोड यहाँ दिया गया है:
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract UUPSProxy is ERC1967Proxy {
constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data)
payable {}
}
इंप्लीमेंटेशन कॉन्ट्रैक्ट
इंप्लीमेंटेशन कॉन्ट्रैक्ट्स को UUPSUpgradeable कॉन्ट्रैक्ट से इनहेरिट करना चाहिए, जो UUPS पैटर्न का पालन करता है और इसमें अगले इंप्लीमेंटेशन में जाने का एक मैकेनिज्म शामिल होता है। _authorizeUpgrade फंक्शन को ओवरराइड (override) करना आवश्यक है, क्योंकि ऑथराइज़ेशन मैकेनिज्म पूर्व-परिभाषित (predefined) नहीं है और इसे लागू किया जाना चाहिए।
नीचे दिए गए कोड में, हम इसे अत्यधिक असुरक्षित तरीके से लागू करते हैं, क्योंकि कोई भी व्यक्ति अपग्रेड करने के लिए ऑथराइज़्ड (authorized) है।

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract UUPSProxy is ERC1967Proxy {
constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) payable {}
}
// UUPS Implementation
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract ImplementationOne is UUPSUpgradeable {
function myNumber() public pure returns (uint256) {
return 1; // A function to test the implementation
}
// In practice, this function should include an onlyOwner modifier
// or some other form of ownership protection mechanism
function _authorizeUpgrade(address _newImplementation) internal override {}
}
कॉन्ट्रैक्ट के लिए एक ओनर (owner) को परिभाषित करने के लिए, एक इनिशियलाइज़ेशन फंक्शन बनाया जाना चाहिए क्योंकि इंप्लीमेंटेशन कॉन्ट्रैक्ट्स को कंस्ट्रक्टर्स का उपयोग नहीं करना चाहिए। आप इस विषय के बारे में Initializable.sol पर हमारे आर्टिकल में अधिक जान सकते हैं। फिर से, कोड यहाँ दिया गया है:
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract ImplementationOne is UUPSUpgradeable {
function myNumber() public pure returns (uint256) {
return 1; // A function to test the implementation
}
// In practice, this function should include an onlyOwner modifier
// or some other form of ownership protection mechanism
function _authorizeUpgrade(address _newImplementation) internal override {}
}
Remix में उपरोक्त कॉन्ट्रैक्ट्स को टेस्ट करने के लिए, आपको निम्नलिखित स्टेप्स का पालन करना होगा:
ImplementationOneनामक इंप्लीमेंटेशन कॉन्ट्रैक्ट को डिप्लॉय करें।MyProxyनामक प्रॉक्सी कॉन्ट्रैक्ट को डिप्लॉय करें। कंस्ट्रक्टर के लिए दो मापदंडों (parameters) की आवश्यकता होती है: इंप्लीमेंटेशन कॉन्ट्रैक्ट का एड्रेस (ImplementationOneका एड्रेस) और इंप्लीमेंटेशन कॉन्ट्रैक्ट को इनिशियलाइज़ करने के लिएbytesप्रकार का एक आर्गुमेंट। यह आर्गुमेंट0xहो सकता है, क्योंकि इसका उपयोग नहीं किया जाएगा।- कॉन्ट्रैक्ट को टेस्ट करने के लिए, इंप्लीमेंटेशन कॉन्ट्रैक्ट ABI का उपयोग करके एक प्रॉक्सी कॉन्ट्रैक्ट इंस्टेंस (instance) खोलें। ऐसा करने के लिए, डिप्लॉयमेंट टैब में, इंप्लीमेंटेशन कॉन्ट्रैक्ट,
ImplementationOneका चयन करें, और नीचे दिए गए चित्र में दिखाए अनुसार At Address फ़ील्ड में प्रॉक्सी कॉन्ट्रैक्ट एड्रेस दर्ज करें।

अब आप प्रॉक्सी के माध्यम से इंप्लीमेंटेशन कॉन्ट्रैक्ट में myNumber() फंक्शन को एक्सीक्यूट (execute) कर सकेंगे।
अगले इंप्लीमेंटेशन पर जाना
अगले इंप्लीमेंटेशन पर जाने के लिए, किसी को सबसे पहले एक नया कॉन्ट्रैक्ट बनाना होगा जो UUPS पैटर्न का पालन करता हो, नीचे दिए गए उदाहरण के समान।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract UUPSProxy is ERC1967Proxy {
constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) payable {}
}
// UUPS Implementation
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract ImplementationOne is UUPSUpgradeable {
function myNumber() public pure returns (uint256) {
return 1; // A function to test the implementation
}
// In practice, this function should include an onlyOwner modifier
// or some other form of ownership protection mechanism
function _authorizeUpgrade(address _newImplementation) internal override {}
}
// NEW UUPS Implementation
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract ImplementationTwo is UUPSUpgradeable {
function myNumber() public pure returns (uint256) {
return 2; // A function to test the implementation
}
// In practice, this function should include an onlyOwner modifier
// or some other form of ownership protection mechanism
function _authorizeUpgrade(address _newImplementation) internal override {}
}
// ---- updated implementation ----
contract ImplementationTwo is UUPSUpgradeable {
function myNumber() public pure returns (uint256) {
return 2; // A function to test the implementation
}
// In practice, this function should include an onlyOwner modifier
// or some other form of ownership protection mechanism
function _authorizeUpgrade(address _newImplementation) internal override {}
}
कॉन्ट्रैक्ट बनाने के बाद, अगले स्टेप्स इस प्रकार हैं:
ImplementationTwoको डिप्लॉय करें।- प्रॉक्सी के माध्यम से पिछले इंप्लीमेंटेशन कॉन्ट्रैक्ट पर
upgradeToAndCallफंक्शन को कॉल करें, जिसमें पहले पैरामीटर के रूप मेंImplementationTwoका एड्रेस पास किया गया हो (दूसरा पैरामीटर0xहो सकता है)।proxiableUUID()फंक्शन पैरेंट कॉन्ट्रैक्ट (parent contract) में परिभाषित किया गया है, और माइग्रेशन से पहले इसकी सही रिटर्न वैल्यू को वैरीफाई (verify) किया जाएगा।
एक ऐसे इंप्लीमेंटेशन पर माइग्रेट करने का प्रयास जो UUPS-कंप्लायंट नहीं है, विफल हो जाएगा क्योंकि proxiableUUID() फंक्शन के माध्यम से मौजूद सुरक्षा जांच तंत्र (security check mechanism) इसे रोक देगा।
UUPS में कमजोरियां (Vulnerabilities)
1. अनइनिशियलाइज़्ड (uninitialized) कॉन्ट्रैक्ट्स से जुड़ी कमजोरी
इंप्लीमेंटेशन कॉन्ट्रैक्ट्स को इनिशियलाइज़ करना आम बात है। उदाहरण के लिए, एक ERC20 अपग्रेडेबल कॉन्ट्रैक्ट के मामले में, डिप्लॉयमेंट के दौरान टोकन का नाम और सिंबल सेट करना आम है। यह प्रक्रिया आमतौर पर कंस्ट्रक्टर्स के साथ की जाती है। हालांकि, इंप्लीमेंटेशन कॉन्ट्रैक्ट में कंस्ट्रक्टर्स का उपयोग करना सहायक नहीं है, क्योंकि यह इंप्लीमेंटेशन की स्टोरेज को बदल देगा, जबकि ‘असली’ (true) स्टोरेज प्रॉक्सी कॉन्ट्रैक्ट में होती है।
इंप्लीमेंटेशन कॉन्ट्रैक्ट्स को इनिशियलाइज़ करने के लिए, हमें रेगुलर फंक्शन्स पर निर्भर रहना चाहिए जो केवल एक बार एक्सीक्यूट होने के लिए कॉन्फ़िगर किए गए हैं। यह OpenZeppelin द्वारा Initializable.sol लाइब्रेरी में प्रदान किए गए मॉडिफायर्स का उपयोग करके किया जा सकता है।
इनिशियलाइज़ेशन फंक्शन का एक उदाहरण नीचे दिया गया कोड है।
function initialize(address initialOwner) initializer public {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
इंप्लीमेंटेशन का ओनरशिप लेना
एक प्रमुख कमजोरी इस तथ्य में निहित है कि यह एक पब्लिक फंक्शन है। इसे प्रॉक्सी के माध्यम से कॉल किया जाना चाहिए, लेकिन इसे सीधे इंप्लीमेंटेशन कॉन्ट्रैक्ट पर भी कॉल किया जा सकता है। चूंकि यह कॉन्ट्रैक्ट का ओनर सेट करता है, जो कोई भी इस फंक्शन को सीधे इंप्लीमेंटेशन कॉन्ट्रैक्ट पर पहले कॉल करता है, वह उस कॉन्ट्रैक्ट का “ओनर” (owner) बन जाएगा।
स्पष्ट करने के लिए, इस बिंदु पर, कॉन्ट्रैक्ट के दो ओनर होंगे:
- प्रॉक्सी के माध्यम से सेट किया गया ओनर।
- इंप्लीमेंटेशन कॉन्ट्रैक्ट को सीधे कॉल करके सेट किया गया “ओनर”।

onlyOwner के रूप में चिह्नित किसी भी फंक्शन को इन दोनों ओनर्स में से किसी के भी द्वारा कॉल करने की अनुमति होगी। यह अनपेक्षित व्यवहार (unintended behavior) कॉन्ट्रैक्ट के लिए जोखिम पैदा कर सकता है, जैसा कि हम जल्द ही देखेंगे।
समाधान
इस कमजोरी का समाधान हमेशा इंप्लीमेंटेशन कॉन्ट्रैक्ट के भीतर सीधे वांछित स्टेट वेरिएबल्स (state variables) सेट करके इंप्लीमेंटेशन कॉन्ट्रैक्ट को “इनिशियलाइज़” करना है। उदाहरण के लिए, आपको इंप्लीमेंटेशन कॉन्ट्रैक्ट का ओनर सेट करना चाहिए या किसी को भी इंप्लीमेंटेशन कॉन्ट्रैक्ट पर सीधे इनिशियलाइज़ेशन फंक्शन को कॉल करने से रोकना चाहिए।
OpenZeppelin _disableInitializers() फंक्शन प्रदान करता है जिसे इसे प्राप्त करने के लिए कंस्ट्रक्टर में एक्सीक्यूट किया जाना चाहिए, जैसा कि निम्नलिखित कोड में दिखाया गया है:
constructor() {
_disableInitializers();
}
2. delegatecall के माध्यम से कमजोरी
इंप्लीमेंटेशन कॉन्ट्रैक्ट में, आपको आर्बिट्रेरी (arbitrary) कॉन्ट्रैक्ट्स पर delegatecall का उपयोग करने से बचना चाहिए। सबसे बड़ा जोखिम अनजाने में selfdestruct ऑपकोड (opcode) पर delegatecall करने में होता था। Cancun fork के बाद से, selfdestruct अब कॉन्ट्रैक्ट कोड को डिलीट नहीं करता है। हालांकि, इंप्लीमेंटेशन कॉन्ट्रैक्ट्स में delegatecall का उपयोग करने से बचने की सिफारिश अभी भी बनी हुई है, संभवतः उन चेन्स (chains) में उपयोग के कारण जहां selfdestruct अभी भी सक्रिय है।
Contracts v4.1.0 से v4.3.1 तक OpenZeppelin के UUPS इंप्लीमेंटेशन में एक गंभीर कमजोरी (severe vulnerability) ऊपर बताई गई दो कमजोरियों के कॉम्बिनेशन के कारण हुई थी:
अगले इंप्लीमेंटेशन में जाने वाले कोड में, इंप्लीमेंटेशन एड्रेस को बदलने के अलावा, नए कॉन्ट्रैक्ट को इनिशियलाइज़ करने के लिए एक delegatecall भी शामिल था। यह फंक्शन, upgradeToAndCall, केवल ओनर द्वारा ही एक्सीक्यूट किया जा सकता था और इसका उद्देश्य विशेष रूप से प्रॉक्सी के माध्यम से कॉल किया जाना था। हालांकि, जैसा कि पहले उल्लेख किया गया है, यदि कॉन्ट्रैक्ट ठीक से “इनिशियलाइज़” नहीं किया गया था, तो कोई भी ओनर की भूमिका ग्रहण कर सकता था और upgradeToAndCall फंक्शन का उपयोग करके किसी ऐसे कॉन्ट्रैक्ट पर delegatecall कर सकता था जिसमें selfdestruct ऑपकोड शामिल हो।
उल्लिखित कमजोरियों के कारण, OpenZeppelin इंप्लीमेंटेशन कॉन्ट्रैक्ट्स के भीतर delegatecall का उपयोग करना असुरक्षित मानता है।
UUPS का उपयोग करने के लिए एक चेकलिस्ट (Checklist)
यहां कुछ दिशा-निर्देश (guidelines) दिए गए हैं जिनका OpenZeppelin लाइब्रेरी में UUPS स्टैंडर्ड का उपयोग करते समय पालन किया जाना चाहिए:
- यदि आप
upgradeToAndCallको ओवरराइड करते हैं तो अत्यंत सावधान रहें ताकि अपग्रेड कार्यक्षमता (upgrade functionality) बाधित न हो। - सुनिश्चित करें कि
_authorizeUpgradeमें एकonlyOwnerमॉडिफायर या कोई अन्य मैकेनिज्म शामिल है जो केवल ऑथराइज़्ड एकाउंट्स तक ही एक्सेस को सीमित करता है। - अपग्रेड में, इंप्लीमेंटेशन कॉन्ट्रैक्ट के नए वर्ज़न में ऑथराइज़ेशन स्कीमा (authorization schema) को बदलते समय सावधान रहें। उदाहरण के लिए, किसी ऐसे ऑथराइज़ेशन प्रकार में स्विच करना जहां एडमिन ने पहले अपने विशेषाधिकार (privileges) त्याग दिए हों और यह किसी के ध्यान में न आया हो।
- इनिशियलाइज़ेशन को रोकने के लिए इंप्लीमेंटेशन कॉन्ट्रैक्ट के कंस्ट्रक्टर में
_disableInitializers()फंक्शन का उपयोग करें। - किसी भी
delegatecallयाselfdestructका उपयोग न किया गया हो।
हम इस काम के शुरुआती ड्राफ्ट की समीक्षा करने के लिए OpenZeppelin के ernestognw.eth को धन्यवाद देना चाहते हैं।
मूल रूप से 26 अगस्त, 2024 को प्रकाशित