Transparent Upgradeable Proxy एक डिज़ाइन पैटर्न है जो function selector क्लैश की संभावना को समाप्त करते हुए प्रॉक्सी को अपग्रेड करने की सुविधा देता है।
एक कार्यात्मक (functional) Ethereum प्रॉक्सी में कम से कम निम्नलिखित दो विशेषताएं होनी चाहिए:
- एक storage slot जिसमें implementation कॉन्ट्रैक्ट का एड्रेस स्टोर हो
- एडमिन के लिए implementation एड्रेस को बदलने का एक मैकेनिज्म (तंत्र)
ERC-1967 स्टैंडर्ड यह निर्धारित करता है कि implementation का एड्रेस रखने वाले storage slot को कहाँ रखा जाना चाहिए ताकि storage collision की संभावना कम से कम हो सके। हालाँकि, ERC-1967 स्टैंडर्ड यह निर्धारित नहीं करता है कि implementation का एड्रेस कैसे बदला जाए।
प्रॉक्सी के अंदर implementation को बदलने के लिए एक अतिरिक्त फ़ंक्शन (जैसे updateImplementation(address _newImplementation)) को रखने की समस्या यह है कि इस बात की गैर-नगण्य (non-negligible) संभावना होती है कि यह अपडेट फ़ंक्शन implementation के किसी फ़ंक्शन के साथ क्लैश कर जाए।
Function Selector क्लैशिंग
Implementation एड्रेस को अपडेट करने के लिए प्रॉक्सी के अंदर public फ़ंक्शन्स को डिक्लेयर करने से function selector क्लैश की संभावना उत्पन्न होती है।
यहाँ एक साधारण उदाहरण दिया गया है:
contract ProxyUnsafe {
function changeImplementation(
address newImplementation
) public {
// some code...
}
fallback(bytes calldata data) external payable (bytes memory) {
(bool ok, bytes memory data) = getImplementation().delegatecall(data);
require(ok, "delegatecall failed");
return data;
}
}
contract Implementation {
// an identical function is declared here -- they will clash
function changeImplementation(
address newImplementation
) public {
}
//...
}
याद रखें, fallback को हमेशा सबसे अंत में चेक किया जाता है। fallback को कॉल करने से पहले, प्रॉक्सी कॉन्ट्रैक्ट यह चेक करेगा कि क्या 4 बाइट का function selector changeImplementation (या प्रॉक्सी के किसी अन्य public फ़ंक्शन) से मेल खाता है।
इसलिए, यदि प्रॉक्सी में कोई public फ़ंक्शन डिक्लेयर किया गया है, तो दो प्रकार के function selector क्लैश हो सकते हैं:
- यदि implementation कॉन्ट्रैक्ट समान सिग्नेचर वाले फ़ंक्शन को लागू करता है, तो वह फ़ंक्शन अनकॉलेबल (uncallable) हो जाएगा क्योंकि समान सिग्नेचर वाले प्रॉक्सी के public फ़ंक्शन को कॉल किया जाएगा, न कि fallback को। और यदि fallback ट्रिगर नहीं होता है, तो implementation के लिए कोई delegatecall नहीं होगा।
- यदि implementation कॉन्ट्रैक्ट में एक ऐसा फ़ंक्शन है जिसका function selector प्रॉक्सी के public फ़ंक्शन के समान है, तो वह भी इसी कारण से अनकॉलेबल हो जाएगा। यह परिदृश्य एक function selector क्लैश है जो संयोग से (random chance) तब हो सकता है जब चार-बाइट्स आपस में मेल खाते हों। दो अलग-अलग फ़ंक्शन्स के समान selector होने की प्रायिकता (probability) 4.29 बिलियन में से 1 है; एक function selector 4 बाइट्स का होता है, इसलिए 4.29 बिलियन संभावनाएं हैं। यह एक छोटी प्रायिकता है, लेकिन नगण्य (negligible) नहीं है। उदाहरण के लिए,
clash550254402()का function selector वही है जोproxyAdmin()का है।
Transparent Upgradeable Proxy Pattern पूरी तरह से Function Selector क्लैशिंग को रोकता है
Transparent Upgradeable Proxy Pattern एक ऐसा डिज़ाइन पैटर्न है जो function selector क्लैशिंग की संभावना को पूरी तरह से समाप्त कर देता है।
विशेष रूप से, Transparent Upgradeable Proxy Pattern यह निर्धारित करता है कि प्रॉक्सी पर fallback के अलावा कोई public फ़ंक्शन नहीं होना चाहिए।
लेकिन केवल एक fallback फ़ंक्शन के साथ, हम प्रॉक्सी को अपग्रेड करने वाले फ़ंक्शन को कैसे कॉल करेंगे?
इसका उत्तर यह पता लगाना है कि क्या msg.sender एडमिन है।
contract Proxy is ERC1967 {
address immutable admin;
constructor(address admin_) {
admin = admin_
}
fallback() external payable {
if (msg.sender == admin) {
// upgrade logic
} else {
// delegatecall to implementation
}
}
}
इसका अर्थ यह है कि एडमिन सीधे प्रॉक्सी का उपयोग नहीं कर सकता क्योंकि उनके कॉल्स हमेशा delegatecall से दूर रूट (route) कर दिए जाते हैं। हालाँकि, एक अलग मैकेनिज्म का उपयोग करके जिस पर हम बाद में चर्चा करेंगे, एडमिन के लिए प्रॉक्सी को कॉल करना और प्रॉक्सी के लिए implementation को एक सामान्य ट्रांजेक्शन के रूप में delegatecall करना अभी भी संभव है।
एक immutable एडमिन को बदलना
ऊपर दिए गए कोड स्निपेट में, एडमिन immutable है। इसका मतलब है कि कॉन्ट्रैक्ट तकनीकी रूप से ERC-1967 का अनुपालन नहीं करता है जो कहता है कि एडमिन को storage slot 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 या bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) में रखा जाना चाहिए।
ERC-1967 के साथ कम्पैटिबल होने के लिए, Transparent Upgradeable Proxy उस storage slot में एडमिन का एड्रेस स्टोर करता है, लेकिन यह उस storage variable का उपयोग नहीं करता है।
उस storage slot में किसी एड्रेस की उपस्थिति ब्लॉक एक्सप्लोरर्स को यह संकेत देगी कि कॉन्ट्रैक्ट एक प्रॉक्सी है (जो ERC-1967 के उद्देश्यों में से एक है)। हालाँकि, जब भी प्रॉक्सी को कॉल किया जाता है तो हर बार स्टोरेज से पढ़ने में कॉल पर अतिरिक्त 2,100 गैस खर्च होती है। इसलिए, एक immutable variable का उपयोग करना अधिक वांछनीय (desirable) है।
एडमिन को “बदलना”
हालाँकि, एडमिन एड्रेस को अपडेट करने में सक्षम होना अभी भी वांछनीय है — लेकिन शुरुआत में यह असंभव लगता है क्योंकि प्रॉक्सी एक immutable variable का उपयोग करता है।
प्रॉक्सी कॉन्ट्रैक्ट एडमिन के बदलाव को सक्षम करने के लिए Transparent Upgradeable Proxy का दृष्टिकोण दोतरफा है। पहला, यह एक और कॉन्ट्रैक्ट जिसे ProxyAdmin के रूप में जाना जाता है, को प्रॉक्सी कॉन्ट्रैक्ट का एडमिन नियुक्त करता है।

स्मार्ट कॉन्ट्रैक्ट का एड्रेस कभी नहीं बदलेगा, इसलिए यह Transparent Upgradeable Proxy द्वारा एक immutable variable में एडमिन का एड्रेस स्टोर करने के अनुकूल है।
दूसरा, ProxyAdmin का owner ही “असली” एडमिन होता है। ProxyAdmin बस owner से प्रॉक्सी तक कॉल्स को रूट करता है। “असली” एडमिन ProxyAdmin को कॉल करता है और ProxyAdmin Transparent Proxy को कॉल करता है। ProxyAdmin के owner को बदलकर हम यह बदल सकते हैं कि Transparent Proxy को अपग्रेड करने की क्षमता किसके पास है।
AdminProxy
नीचे OpenZeppelin AdminProxy का कोड दिया गया है (कमेंट्स हटा दिए गए हैं)। ध्यान दें कि इसमें केवल एक ही फ़ंक्शन upgradeAndCall() है जो प्रॉक्सी पर केवल upgradeToAndCall() को कॉल कर सकता है।
pragma solidity ^0.8.20;
import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol";
import {Ownable} from "../../access/Ownable.sol";
contract ProxyAdmin is Ownable {
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
constructor(address initialOwner) Ownable(initialOwner) {}
function upgradeAndCall(
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
यह एक आम गलतफहमी है कि Transparent Proxy का एडमिन कॉन्ट्रैक्ट का उपयोग करने में सक्षम नहीं है क्योंकि उनके कॉल्स अपग्रेड के लिए फ़ॉरवर्ड कर दिए जाते हैं। हालाँकि, AdminProxy का owner बिना किसी समस्या के प्रॉक्सी का उपयोग कर सकता है, जैसा कि नीचे दिया गया चित्र प्रदर्शित करता है।
वास्तव में, हम बाद में देखेंगे कि ProxyAdmin के लिए प्रॉक्सी पर एक मनमाना (arbitrary) कॉल करने का एक मैकेनिज्म मौजूद है, जैसा कि फ़ंक्शन का नाम upgradeToAndCall() दर्शाता है।

प्रॉक्सी को नॉन-अपग्रेडेबल बनाना
यदि owner को एड्रेस शून्य (address zero) में बदल दिया जाता है, या किसी ऐसे अन्य स्मार्ट कॉन्ट्रैक्ट में बदल दिया जाता है जो upgradeAndCall() फ़ंक्शन का सही ढंग से उपयोग करने (या owner को बदलने) में सक्षम नहीं है, तो Transparent Upgradeable Proxy अब अपग्रेडेबल नहीं रहेगा। ऐसा हो सकता है, उदाहरण के लिए, यदि AdminProxy का owner किसी अन्य AdminProxy कॉन्ट्रैक्ट को सेट कर दिया जाए।
Implementation डिटेल्स
OpenZeppelin Transparent Upgradeable Proxy तीन कॉन्ट्रैक्ट्स के साथ स्टैंडर्ड को लागू करता है:
- Proxy.sol
- ERC1967Proxy.sol (Proxy.sol को इनहेरिट करता है)
- TransparentUpgradeableProxy.sol (ERC1967Proxy.sol को इनहेरिट करता है)
Parentmost कॉन्ट्रैक्ट: Proxy.sol
बेस कॉन्ट्रैक्ट Proxy.sol है। एक implementation एड्रेस दिए जाने पर, यह implementation को एक delegatecall भेजता है। _implementation() फ़ंक्शन को Proxy में इम्प्लीमेंट नहीं किया गया है — इसे ओवरराइड (override) किया जाता है और इसके चाइल्ड ERC1967Proxy द्वारा लागू किया जाता है जो इसे संबंधित storage slot रिटर्न करने के लिए प्रेरित करेगा।
abstract contract Proxy {
function _delegate(address implementation) internal virtual {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
function _implementation() internal view virtual returns (address);
function _fallback() internal virtual {
_delegate(_implementation());
}
fallback() external payable virtual {
_fallback();
}
}
Proxy.sol का चाइल्ड: ERC1967Proxy.sol
ERC1967Proxy.sol Proxy.sol से इनहेरिट करता है। यह इंटरनल _implementation() फ़ंक्शन को जोड़ता (और ओवरराइड करता) है जो ERC-1967 द्वारा निर्दिष्ट स्लॉट पर स्टोर किए गए implementation एड्रेस को रिटर्न करता है। इस कॉन्ट्रैक्ट का constructor ERC-1967 द्वारा निर्दिष्ट storage slot में implementation को स्टोर करता है। हालाँकि, Transparent Upgradeable Proxy इस फ़ंक्शन का उपयोग नहीं करेगा — इसके बजाय यह अपने स्वयं के immutable variable का उपयोग करेगा।
pragma solidity ^0.8.20;
import {Proxy} from "../Proxy.sol";
import {ERC1967Utils} from "./ERC1967Utils.sol";
contract ERC1967Proxy is Proxy {
constructor(address implementation, bytes memory _data) payable {
ERC1967Utils.upgradeToAndCall(implementation, _data);
}
// reads from bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
function _implementation() internal view virtual override returns (address) {
return ERC1967Utils.getImplementation();
}
}
ERC1967Proxy.sol का चाइल्ड: TransparentUpgradeableProxy.sol
अंत में, TransparentUpgradeableProxy.sol ERC1967Proxy.sol से इनहेरिट करता है। इस कॉन्ट्रैक्ट के constructor में, ProxyAdmin डिप्लॉय किया जाता है और immutable एडमिन (कॉन्ट्रैक्ट में पहला variable) constructor में ProxyAdmin का एड्रेस सेट किया जाता है।
contract TransparentUpgradeableProxy is ERC1967Proxy {
address private immutable _admin;
error ProxyDeniedAdminAccess();
constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
_admin = address(new ProxyAdmin(initialOwner));
// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(_proxyAdmin());
}
function _proxyAdmin() internal view virtual returns (address) {
return _admin;
}
function _fallback() internal virtual override {
if (msg.sender == _proxyAdmin()) {
if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
revert ProxyDeniedAdminAccess();
} else {
_dispatchUpgradeToAndCall();
}
} else {
super._fallback();
}
}
function _dispatchUpgradeToAndCall() private {
(address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
}
आइए उस मामले पर विचार करें जहां msg.sender _proxyAdmin है। उस स्थिति में, कॉल को _dispatchUpgradeToAndCall() पर रूट किया जाता है, लेकिन _fallback() पहले यह चेक करता है कि प्रदान किया गया function selector upgradeToAndCall का function selector है। यहाँ “selector” कोई “वास्तविक” selector नहीं है, क्योंकि Transparent Upgradeable Proxy में public फ़ंक्शन्स नहीं होते हैं। हालाँकि, ProxyAdmin को एक Solidity interface call (high level call) करने की अनुमति देने के लिए, इसे ProxyAdmin से upgradeToAndCall() के लिए ABI-encoded calldata को स्वीकार करने की आवश्यकता होती है।
याद करें, ProxyAdmin प्रॉक्सी में upgradeToAndCall के लिए एक इंटरफ़ेस कॉल कर रहा है, भले ही प्रॉक्सी में fallback के अलावा कोई public फ़ंक्शन न हो (ProxyAdmin का कोड आगे दिखाया गया है):

नीचे एक वीडियो है जो तीनों कोड ब्लॉक्स को अगल-बगल दिखाता है और यह बताता है कि इनहेरिटेंस चेन में मौजूद विभिन्न कॉन्ट्रैक्ट्स (Proxy, ERC1967Proxy, और TransparentUpgradeableProxy) एक-दूसरे के साथ कैसे इंटरैक्ट करते हैं:
केवल upgradeTo() के बजाय upgradeToAndCall() क्यों?
Implementation कॉन्ट्रैक्ट को अपग्रेड करते समय, इसे इस तरह से कॉल करना संभव है जैसे कि ProxyAdmin ही msg.sender हो और ट्रांजेक्शन को implementation पर इस तरह से delegatecall किया जाए जैसे कि यह एक सामान्य प्रॉक्सी इंटरैक्शन हो। बेशक, यह fallback के अंदर नहीं होता है क्योंकि ProxyAdmin से आने वाले कॉल्स को अपग्रेड लॉजिक पर रूट किया जाता है।
नीचे दिया गया कोड ERC1967Utils.sol से है जिसके साथ TransparentUpgradeableProxy implementation स्लॉट को अपडेट करने में सक्षम होने के लिए रचना (compose) करता है। लाइब्रेरी implementation एड्रेस रखने वाले storage slot को अपडेट करने के लिए एक इंटरनल हेल्पर फ़ंक्शन प्रदान करती है।
/**
* @dev Performs implementation upgrade with additional setup call if data is nonempty.
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-Upgraded} event.
*/
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
_setImplementation(newImplementation);
emit IERC1967.Upgraded(newImplementation);
if (data.length > 0) {
Address.functionDelegateCall(newImplementation, data);
} else {
_checkNonPayable();
}
}
यह implementation कॉन्ट्रैक्ट को केवल तभी delegatecall करेगा जब data.length > 0 हो।
upgradeToAndCall() भी अपग्रेड के समान ट्रांजेक्शन में प्रॉक्सी से implementation तक एक delegatecall करता है। यह बिल्कुल वैसा ही है जैसे कि ProxyAdmin ने data में निर्दिष्ट किसी भी calldata का उपयोग करके प्रॉक्सी को कॉल किया हो, और फिर प्रॉक्सी ने implementation को एक delegatecall किया हो।
इस तरह, ProxyAdmin प्रॉक्सी पर मनमाने (arbitrary) कॉल्स कर सकता है।
ध्यान दें कि upgradeToAndCall के लिए यह आवश्यक नहीं है कि अपग्रेड किया गया कॉन्ट्रैक्ट एक अलग implementation हो — समान implementation पर “अपग्रेड” करना संभव है।
इसका अर्थ यह है कि ProxyAdmin कॉन्ट्रैक्ट प्रॉक्सी के माध्यम से implementation कॉन्ट्रैक्ट में मनमाने delegatecalls कर सकता है — लेकिन Transparent Proxy के दृष्टिकोण से msg.sender ProxyAdmin होता है।
यह कोई “समस्या” नहीं है कि ProxyAdmin कॉन्ट्रैक्ट का उपयोग कर सकता है — ProxyAdmin में implementation को पूरी तरह से बदलने की क्षमता है — ProxyAdmin के owner के पास पहले से ही प्रॉक्सी पर एडमिन कंट्रोल होता है।
अपग्रेड करने पर ProxyAdmin के लिए एकमात्र प्रतिबंध यह है कि वे इसे किसी खाली कॉन्ट्रैक्ट (बिना बाइटकोड वाले एड्रेस) में अपग्रेड नहीं कर सकते हैं। _setImplementation फ़ंक्शन यह चेक करता है कि नए implementation की code length शून्य से अधिक है या नहीं
/**
* @dev Stores a new address in the ERC-1967 implementation slot.
*/
function _setImplementation(address newImplementation) private {
if (newImplementation.code.length == 0) {
revert ERC1967InvalidImplementation(newImplementation);
}
StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
}
Transparent Upgradeable Proxy का सारांश
- Transparent Upgradeable Proxy प्रॉक्सी और implementation के बीच function selector क्लैशिंग को रोकने के लिए एक डिज़ाइन पैटर्न है।
- fallback फ़ंक्शन Transparent Upgradeable Proxy पर एकमात्र public फ़ंक्शन है।
- अपग्रेड कार्यक्षमता (functionality) केवल एडमिन द्वारा fallback फ़ंक्शन के माध्यम से इनवोक (invoke) की जा सकती है। गैर-एडमिन (non-admin) एड्रेस से आने वाले सभी कॉल्स प्रॉक्सी के लिए delegatecalls में बदल जाते हैं।
- Transparent Upgradeable Proxy गैस बचाने के लिए एडमिन को स्टोर करने हेतु एक immutable variable का उपयोग करता है। ERC-1967 के अनुरूप होने के लिए, यह ERC-1967 द्वारा निर्दिष्ट
adminस्लॉट में एडमिन का एड्रेस स्टोर करता है, भले ही यह उस स्लॉट से कभी पढ़ता (read) नहीं है। - चूंकि एडमिन को बदला नहीं जा सकता है, इसलिए एडमिन को
AdminProxyनामक एक स्मार्ट कॉन्ट्रैक्ट के रूप में सेट किया जाता है।AdminProxyकेवल एक फ़ंक्शनupgradeAndCall()को एक्सपोज़ करता है जिसे केवलAdminProxyके owner द्वारा ही कॉल किया जा सकता है।AdminProxyके owner को बदला जा सकता है। ऐसा बदलाव यह परिवर्तित कर देता है कि Transparent Upgradeable Proxy में implementation स्लॉट को कौन अपडेट कर सकता है।
हम इस लेख की समीक्षा करने और उपयोगी सुझाव देने के लिए OpenZeppelin के @ernestognw को धन्यवाद देना चाहते हैं।
मूल रूप से 4 जून को प्रकाशित