यह लेख विस्तार से बताता है कि delegatecall कैसे काम करता है। Ethereum Virtual Machine (EVM) कॉन्ट्रैक्ट्स के बीच कॉल करने के लिए चार opcodes प्रदान करता है:
CALL (F1)CALLCODE (F2)STATICCALL (FA)- और
DELEGATECALL (F4)।
विशेष रूप से, CALLCODE opcode को Solidity v5 के बाद से अप्रचलित (deprecated) कर दिया गया है, और इसे DELEGATECALL से बदल दिया गया है। इन opcodes का Solidity में सीधा इम्प्लीमेंटेशन है और इन्हें address टाइप के वेरिएबल्स के मेथड्स के रूप में एक्जीक्यूट किया जा सकता है।
delegatecall कैसे काम करता है, इसे बेहतर ढंग से समझने के लिए, आइए पहले CALL opcode की कार्यक्षमता (functionality) की समीक्षा करें।
CALL
call को प्रदर्शित करने के लिए निम्नलिखित कॉन्ट्रैक्ट पर विचार करें:
contract Called {
uint public number;
function increment() public {
number++;
}
}
किसी अन्य कॉन्ट्रैक्ट से increment() फंक्शन को एक्जीक्यूट करने का सबसे सीधा तरीका Called contract interface का उपयोग करना है। इस तरीके में, हम फंक्शन को एक साधारण स्टेटमेंट called.increment() के साथ एक्जीक्यूट कर सकते हैं जहाँ called, Called का एड्रेस है। लेकिन increment() को कॉल करना नीचे दिए गए कॉन्ट्रैक्ट में दिखाए गए अनुसार लो-लेवल call का उपयोग करके भी प्राप्त किया जा सकता है:
contract Caller {
address constant public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138; // Called's address
function callIncrement() public {
calledAddress.call(abi.encodeWithSignature("increment()"));
}
}
एड्रेस टाइप के प्रत्येक वेरिएबल में, जैसे कि calledAddress वेरिएबल, call नामक एक मेथड होता है। यह मेथड पैरामीटर के रूप में ट्रांजेक्शन में एक्जीक्यूट किए जाने वाले इनपुट डेटा की अपेक्षा करता है, यानी, ABI encoded calldata की। ऊपर बताए गए मामले में, इनपुट डेटा increment() फंक्शन के सिग्नेचर के अनुरूप होना चाहिए, जिसका function selector 0xd09de08a है। हम फंक्शन डेफिनेशन से इस सिग्नेचर को जनरेट करने के लिए abi.encodeWithSignature मेथड का उपयोग करते हैं।
यदि आप Caller कॉन्ट्रैक्ट में callIncrement फंक्शन को एक्जीक्यूट करते हैं, तो आप देखेंगे कि Called में स्टेट वेरिएबल number 1 से बढ़ जाएगा। call मेथड यह सत्यापित नहीं करता है कि डेस्टिनेशन एड्रेस वास्तव में किसी मौजूदा कॉन्ट्रैक्ट के अनुरूप है या नहीं, और न ही यह कि इसमें निर्दिष्ट फंक्शन मौजूद है।
call ट्रांजेक्शन को नीचे दिए गए वीडियो में विज़ुअलाइज़ किया गया है:
Call एक tuple रिटर्न करता है
call मेथड दो वैल्यूज के साथ एक tuple रिटर्न करता है। पहली वैल्यू एक boolean होती है जो ट्रांजेक्शन की सफलता (success) या विफलता (failure) को दर्शाती है। bytes टाइप की दूसरी वैल्यू, call द्वारा एक्जीक्यूट किए गए फंक्शन की रिटर्न वैल्यू (यदि कोई हो) को ABI-encoded रूप में होल्ड करती है।
call के रिटर्न को प्राप्त करने के लिए, हम callIncrement फंक्शन को इस प्रकार संशोधित कर सकते हैं:
function callIncrement() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("increment()")
);
}
call मेथड कभी भी revert नहीं होता है। यदि ट्रांजेक्शन successful नहीं होता है, तो success फॉल्स (false) होगा, और प्रोग्रामर को इसे तदनुसार हैंडल करने की आवश्यकता होती है।
Call की विफलताओं (Failures) को हैंडल करना
आइए नीचे दिखाए गए अनुसार एक गैर-मौजूद (non-existent) फंक्शन में एक और कॉल शामिल करने के लिए ऊपर दिए गए कॉन्ट्रैक्ट को संशोधित करें।
contract Caller {
address public constant calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function callIncrement() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("increment()")
);
if (!success) {
revert("Something went wrong");
}
}
// calls a non-existent function
function callWrong() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("thisFunctionDoesNotExist()")
);
if (!success) {
revert("Something went wrong");
}
}
}
मैंने जानबूझकर दो फंक्शन्स बनाए हैं: एक सही increment फंक्शन सिग्नेचर के साथ और दूसरा अमान्य (invalid) सिग्नेचर के साथ। पहला फंक्शन success के लिए true रिटर्न करेगा, जबकि दूसरा false रिटर्न करेगा। रिटर्न किए गए boolean को स्पष्ट रूप से हैंडल किया गया है, और यदि success फॉल्स (false) है तो ट्रांजेक्शन revert हो जाएगा।
हमें यह ट्रैक करने में सावधानी बरतनी चाहिए कि कॉल सफल हुआ या नहीं, और हम जल्द ही इस मुद्दे पर फिर से विचार करेंगे।
EVM बैकग्राउंड में क्या करता है
increment फंक्शन का उद्देश्य number नामक स्टेट वेरिएबल को बढ़ाना है। चूंकि EVM को स्टेट वेरिएबल्स का ज्ञान नहीं होता है बल्कि यह स्टोरेज स्लॉट्स (storage slots) पर काम करता है, इसलिए फंक्शन वास्तव में जो करता है वह स्टोरेज के पहले स्लॉट में वैल्यू को बढ़ाना है, जिसे slot 0 के रूप में जाना जाता है। यह ऑपरेशन Called कॉन्ट्रैक्ट के स्टोरेज के भीतर होता है।

call मेथड का उपयोग कैसे करना है, इसकी समीक्षा करने से हमें यह समझने में मदद मिलेगी कि delegatecall का उपयोग कैसे किया जाए।
DELEGATECALL
एक कॉन्ट्रैक्ट जो किसी टारगेट स्मार्ट कॉन्ट्रैक्ट को delegatecall करता है, वह टारगेट कॉन्ट्रैक्ट के लॉजिक को अपने स्वयं के एनवायरनमेंट के भीतर एक्जीक्यूट करता है।
इसे समझने का एक तरीका यह है कि यह टारगेट स्मार्ट कॉन्ट्रैक्ट के कोड को कॉपी करता है और उस कोड को स्वयं रन करता है। टारगेटेड स्मार्ट कॉन्ट्रैक्ट को आमतौर पर “implementation contract” कहा जाता है।
call की तरह ही, delegatecall में भी इनपुट डेटा होता है जिसे टारगेट कॉन्ट्रैक्ट द्वारा पैरामीटर के रूप में एक्जीक्यूट किया जाता है।
यहाँ ऊपर दिए गए एनिमेशन के अनुरूप Called कॉन्ट्रैक्ट का कोड दिया गया है, जो Caller के एनवायरनमेंट में रन होता है:
contract Called {
uint public number;
function increment() public {
number++;
}
}
और Caller का कोड:
contract Caller {
uint public number;
function callIncrement(address _calledAddress) public {
_calledAddress.delegatecall(
abi.encodeWithSignature("increment()")
);
}
}
यह delegatecall increment फंक्शन को एक्जीक्यूट करेगा; हालाँकि, एक्जीक्यूशन एक महत्वपूर्ण अंतर के साथ होगा। Caller कॉन्ट्रैक्ट का स्टोरेज मॉडिफाई होगा, Called का स्टोरेज नहीं। यह ऐसा है मानो Caller कॉन्ट्रैक्ट ने अपने स्वयं के कॉन्टेक्स्ट में एक्जीक्यूट करने के लिए Called का कोड उधार लिया हो।
नीचे दिया गया आरेख (diagram) आगे स्पष्ट करता है कि कैसे delegatecall Called के बजाय Caller के स्टोरेज को मॉडिफाई करता है।

नीचे दी गई छवि call और delegatecall का उपयोग करके increment फंक्शन को एक्जीक्यूट करने के बीच के अंतर को स्पष्ट करती है।

स्टोरेज स्लॉट कोलिजन (Storage slot collision)
delegatecall जारी करने वाले कॉन्ट्रैक्ट को यह अनुमान लगाने में अत्यधिक सावधान रहना चाहिए कि उसके कौन से स्टोरेज स्लॉट्स मॉडिफाई होंगे। पिछला उदाहरण पूरी तरह से काम कर गया क्योंकि Caller ने slot 0 में स्टेट वेरिएबल का उपयोग नहीं किया था। delegatecall का उपयोग करते समय एक आम बग (bug) इस तथ्य को भूल जाना है। आइए इसका एक उदाहरण देखें।
contract Called {
uint public number;
function increment() public {
number++;
}
}
contract Caller {
// there is a new storage variable here
address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
uint public myNumber;
function callIncrement() public {
calledAddress.delegatecall(
abi.encodeWithSignature("increment()")
);
}
}
ध्यान दें कि ऊपर अपडेट किए गए कॉन्ट्रैक्ट में, slot 0 का कंटेंट Called कॉन्ट्रैक्ट का एड्रेस है, जबकि myNumber वेरिएबल अब slot 1 में स्टोर है।
यदि आप दिए गए कॉन्ट्रैक्ट्स को डिप्लॉय करते हैं और callIncrement फंक्शन को एक्जीक्यूट करते हैं, तो Caller स्टोरेज के slot 0 में वृद्धि (increment) होगी, लेकिन वहाँ calledAddress वेरिएबल है, myNumber वेरिएबल नहीं।
निम्नलिखित वीडियो इस बग (bug) को स्पष्ट करता है:
आइए नीचे स्पष्ट करें कि क्या हुआ।

इसलिए, delegatecall का उपयोग करते समय सावधानी बरतनी चाहिए, क्योंकि यह अनजाने में हमारे कॉन्ट्रैक्ट को ब्रेक कर सकता है। ऊपर दिए गए उदाहरण में, संभवतः प्रोग्रामर का इरादा callIncrement फंक्शन के माध्यम से calledAddress वेरिएबल को बदलने का नहीं था।
आइए स्टेट वेरिएबल myNumber को slot 0 में ले जाकर Caller में एक छोटा सा बदलाव करें।
contract Caller {
uint public myNumber;
address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function callIncrement() public {
called.delegatecall(
abi.encodeWithSignature("increment()")
);
}
}
अब, callIncrement फंक्शन को एक्जीक्यूट करते समय, myNumber वेरिएबल में वृद्धि होगी, क्योंकि increment फंक्शन का यही उद्देश्य है। मैंने जानबूझकर Caller में वेरिएबल का नाम Called से अलग चुना है ताकि यह प्रदर्शित किया जा सके कि वेरिएबल का नाम मायने नहीं रखता; मूल बात यह है कि यह किस स्लॉट में है। delegatecall के उचित कामकाज के लिए दोनों कॉन्ट्रैक्ट्स के स्टेट वेरिएबल्स को अलाइन करना (aligning) महत्वपूर्ण है।
डेटा से इम्प्लीमेंटेशन को अलग करना (Decouple implementation from data)
delegatecall का सबसे महत्वपूर्ण उपयोग उस कॉन्ट्रैक्ट को अलग (decouple) करना है जहां डेटा स्टोर किया जाता है (जैसे इस मामले में Caller), उस कॉन्ट्रैक्ट से जहां एक्जीक्यूशन लॉजिक मौजूद होता है (जैसे Called)। इसलिए, यदि कोई एक्जीक्यूशन लॉजिक को बदलना चाहता है, तो वह स्टोरेज को छुए बिना केवल Called को किसी अन्य कॉन्ट्रैक्ट से बदल सकता है और इम्प्लीमेंटेशन कॉन्ट्रैक्ट के रेफरेंस को अपडेट कर सकता है। Caller अब अपने पास मौजूद फंक्शन्स तक सीमित नहीं है, यह अन्य कॉन्ट्रैक्ट्स से आवश्यक फंक्शन्स को delegatecall कर सकता है।
यदि एक्जीक्यूशन लॉजिक को बदलने की आवश्यकता है, उदाहरण के लिए, myNumber की वैल्यू में जोड़ने के बजाय 1 यूनिट घटाना, तो आप नीचे दिखाए अनुसार एक नया इम्प्लीमेंटेशन कॉन्ट्रैक्ट बना सकते हैं।
contract NewCalled {
uint public number;
function increment() public {
number = number - 1;
}
}
दुर्भाग्य से, उस फंक्शन का नाम बदलना संभव नहीं है जिसे कॉल किया जाएगा, क्योंकि ऐसा करने से उसका सिग्नेचर बदल जाएगा।
नया इम्प्लीमेंटेशन कॉन्ट्रैक्ट NewCalled बनाने के बाद, कोई भी बस इस नए कॉन्ट्रैक्ट को डिप्लॉय कर सकता है और Caller में calledAddress स्टेट वेरिएबल को बदल सकता है। बेशक, Caller के पास उस एड्रेस को बदलने का एक मैकेनिज्म होना चाहिए जिसे वह delegateCall जारी कर रहा है, जिसे हमने कोड को संक्षिप्त रखने के लिए शामिल नहीं किया है।
हमने Caller कॉन्ट्रैक्ट द्वारा उपयोग किए जाने वाले बिज़नेस लॉजिक को सफलतापूर्वक मॉडिफाई कर लिया है। एक्जीक्यूशन लॉजिक से डेटा को अलग करने से हम Solidity में अपग्रेडेबल (upgradable) स्मार्ट कॉन्ट्रैक्ट्स बना सकते हैं।

ऊपर की छवि में, बाईं ओर का कॉन्ट्रैक्ट डेटा और लॉजिक दोनों को हैंडल करता है। दाईं ओर, ऊपर वाला कॉन्ट्रैक्ट डेटा होल्ड करता है, लेकिन डेटा को अपडेट करने का मैकेनिज्म लॉजिक कॉन्ट्रैक्ट में रखा गया है। डेटा को अपडेट करने के लिए, लॉजिक कॉन्ट्रैक्ट को एक delegatecall किया जाता है।
delegatecall रिटर्न्स को हैंडल करना
call की तरह, delegatecall भी दो वैल्यूज वाला एक tuple रिटर्न करता है: एक्जीक्यूशन की सफलता को दर्शाने वाला एक boolean और delegatecall के माध्यम से एक्जीक्यूट किए गए फंक्शन का रिटर्न (bytes में)। इस रिटर्न को कैसे हैंडल करें, यह देखने के लिए आइए एक नया उदाहरण लिखते हैं।
contract Called {
function calculateDiscountPrice(
uint256 amount,
uint256 discountRate
) public pure returns (uint) {
return amount - (amount * _discountRate)/100;
}
}
contract Caller {
uint public price = 200;
uint public discountRate = 10;
address public called;
function setCalled(address _called) public {
called = _called;
}
function setDiscountPrice() public {
(bool success, bytes memory data) = called.delegatecall(
abi.encodeWithSignature(
"calculateDiscountPrice(uint256,uint256)",
price,
discountRate)
);
if (success) {
uint newPrice = abi.decode(data, (uint256));
price = newPrice;
}
}
}
Called कॉन्ट्रैक्ट में डिस्काउंट मूल्य (discount price) की गणना करने का लॉजिक है। हम delegatecall के माध्यम से calculateDiscountPrice फंक्शन को एक्जीक्यूट करके इस लॉजिक का उपयोग करते हैं। यह फंक्शन एक वैल्यू रिटर्न करता है, जिसे हमें abi.decode का उपयोग करके डिकोड करना होगा। इस रिटर्न वैल्यू के आधार पर कोई भी निर्णय लेने से पहले, यह जांचना महत्वपूर्ण है कि फंक्शन सफलतापूर्वक एक्जीक्यूट हुआ था या नहीं, अन्यथा हम ऐसे रिटर्न को पार्स (parse) करने का प्रयास कर सकते हैं जो वहां है ही नहीं, या किसी रिवर्ट रीज़न (revert reason) स्ट्रिंग को पार्स कर सकते हैं।
जब call और delegatecall false रिटर्न करते हैं
समझने के लिए एक महत्वपूर्ण बिंदु यह है कि सफलता (success) वैल्यू कब true या false होगी। मूल रूप से, यह इस बात पर निर्भर करता है कि एक्जीक्यूट किया जा रहा फंक्शन revert होगा या नहीं। एक एक्जीक्यूशन तीन तरीकों से revert हो सकता है:
- यदि इसका सामना REVERT opcode से होता है,
- यदि गैस (gas) खत्म हो जाती है,
- यदि यह किसी प्रतिबंधित चीज का प्रयास करता है, जैसे शून्य (zero) से विभाजित (divide) करना।
यदि delegatecall (या call) के माध्यम से एक्जीक्यूट किए जा रहे फंक्शन का सामना इनमें से किसी भी स्थिति से होता है, तो यह revert हो जाएगा, और delegatecall की रिटर्न वैल्यू false होगी।
एक प्रश्न जो अक्सर डेवलपर्स को भ्रमित करता है, वह यह है कि किसी गैर-मौजूद कॉन्ट्रैक्ट के लिए delegatecall revert क्यों नहीं होता है और फिर भी रिपोर्ट करता है कि एक्जीक्यूशन सफल रहा। हमने जो कहा है उसके आधार पर, एक खाली (empty) एड्रेस कभी भी revert होने की तीन शर्तों में से किसी एक को पूरा नहीं करेगा, इसलिए यह कभी भी revert नहीं होगा।
स्टोरेज वेरिएबल संबंधी समस्याओं (gotchas) का एक और उदाहरण
स्टोरेज लेआउट से संबंधित बग्स (bugs) का एक और उदाहरण देने के लिए आइए ऊपर दिए गए कोड में थोड़ा संशोधन करें।
Caller कॉन्ट्रैक्ट अभी भी delegatecall के माध्यम से एक इम्प्लीमेंटेशन कॉन्ट्रैक्ट को इन्वोक करता है, लेकिन अब Called कॉन्ट्रैक्ट एक स्टेट वेरिएबल से एक वैल्यू को रीड करता है। यह एक मामूली बदलाव लग सकता है, लेकिन वास्तव में यह एक बड़ी समस्या पैदा कर सकता है। क्या आप पता लगा सकते हैं क्यों?
contract Called {
uint public discountRate = 20;
function calculateDiscountPrice(uint256 amount) public pure returns (uint) {
return amount - (amount * discountRate)/100;
}
}
contract Caller {
uint public price = 200;
address public called;
function setCalled(address _called) public {
called = _called;
}
function setDiscount() public {
(bool success, bytes memory data) =called.delegatecall(
abi.encodeWithSignature(
"calculateDiscountPrice(uint256)",
price
)
);
if (success) {
uint newPrice = abi.decode(data, (uint256));
price = newPrice;
}
}
}
समस्या इसलिए उत्पन्न होती है क्योंकि calculateDiscountPrice एक स्टेट वेरिएबल को रीड कर रहा है, विशेष रूप से वह जो slot 0 में है। याद रखें कि delegatecall में, फंक्शन्स कॉलिंग कॉन्ट्रैक्ट के स्टोरेज में एक्जीक्यूट होते हैं। दूसरे शब्दों में, आप सोच सकते हैं कि आप नए price की गणना करने के लिए Called कॉन्ट्रैक्ट से discountRate वेरिएबल का उपयोग कर रहे हैं, लेकिन वास्तव में आप Caller कॉन्ट्रैक्ट के price वेरिएबल का उपयोग कर रहे हैं! स्टोरेज वेरिएबल्स Called.discountRate और Called.price slot 0 पर कब्ज़ा करते हैं (occupy करते हैं)।
आपको 200% का भारी-भरकम डिस्काउंट मिलेगा (और इससे फंक्शन revert हो जाएगा, क्योंकि नया कैलकुलेट किया गया मूल्य नेगेटिव हो जाएगा, जिसकी अनुमति uint टाइप के वेरिएबल के लिए नहीं है)।
delegatecall में Immutable और Constant वेरिएबल्स: एक बग की कहानी (A Bug Story)
delegatecall के साथ एक और पेचीदा समस्या तब उत्पन्न होती है जब immutable या constant वेरिएबल्स शामिल होते हैं। आइए एक ऐसे उदाहरण की जांच करें जिसे कई अनुभवी Solidity प्रोग्रामर गलत समझते हैं:
contract Caller {
uint256 private immutable a = 3;
function getValueDelegate(address called) public pure returns (uint256) {
(bool success, bytes memory data) = called.delegatecall(
abi.encodeWithSignature("getValue()"));
return abi.decode(data, (uint256)); // is this 3 or 2?
}
}
contract Called {
uint256 private immutable a = 2;
function getValue() public pure returns (uint256) {
return a;
}
}
सवाल यह है: getValueDelegate को एक्जीक्यूट करते समय, रिटर्न 2 होगा या 3? आइए इसका तर्क समझते हैं।
getValueDelegateफंक्शनgetValueफंक्शन को एक्जीक्यूट करता है, जो कथित तौर पर slot 0 में स्टेट वेरिएबल के अनुरूप वैल्यू रिटर्न करता है।- चूंकि यह delegatecall है, इसलिए हमें कॉलिंग कॉन्ट्रैक्ट के स्लॉट की जांच करनी चाहिए, न कि कॉल्ड कॉन्ट्रैक्ट की।
Callerमें वेरिएबलaकी वैल्यू 3 है, इसलिए उत्तर 3 होना चाहिए। बिल्कुल सही (Nailed it)।
आश्चर्यजनक रूप से, सही उत्तर 2 है। क्यों?!
Immutable या constant स्टेट वेरिएबल्स सच्चे स्टेट वेरिएबल्स नहीं होते हैं: वे किसी स्लॉट में जगह नहीं घेरते हैं। जब हम immutable वेरिएबल्स डिक्लेयर करते हैं, तो उनकी वैल्यू कॉन्ट्रैक्ट बाइटकोड (bytecode) में हार्डकोड (hardcoded) हो जाती है जिसे delegatecall के दौरान एक्जीक्यूट किया जाता है। इस प्रकार, getValue फंक्शन हार्डकोडेड वैल्यू 2 रिटर्न करता है।
msg.sender, msg.value and address(this)
यदि हम Called कॉन्ट्रैक्ट के भीतर msg.sender, msg.value और address(this) का उपयोग करते हैं, तो ये सभी वैल्यूज Caller कॉन्ट्रैक्ट की msg.sender, msg.value और address(this) वैल्यूज के अनुरूप होंगी। आइए याद करें कि delegatecall कैसे काम करता है: सब कुछ कॉलर (caller) कॉन्ट्रैक्ट के कॉन्टेक्स्ट के भीतर होता है। इम्प्लीमेंटेशन कॉन्ट्रैक्ट केवल एक्जीक्यूट किए जाने वाले बाइटकोड को प्रदान करता है, इससे ज्यादा कुछ नहीं।

आइए इस कांसेप्ट (concept) को एक उदाहरण में लागू करें। निम्नलिखित कोड पर विचार करें:
contract Called {
function getInfo() public payable returns (address, uint, address) {
return (msg.sender, msg.value, address(this));
}
}
contract Caller {
function getDelegatedInfo(
address _called
) public payable returns (address, uint, address) {
(bool success, bytes memory data) = _called.delegatecall(
abi.encodeWithSignature("getInfo()")
);
return abi.decode(data, (address, uint, address));
}
}
Called कॉन्ट्रैक्ट में, मैं msg.sender, msg.value और address(this) का उपयोग कर रहा हूँ, और इन वैल्यूज को getInfo फंक्शन में रिटर्न कर रहा हूँ। नीचे दिए गए चित्र में, Remix का उपयोग करते हुए getDelegateInfo का एक्जीक्यूशन दर्शाया गया है, जो रिटर्न की गई वैल्यूज प्रदर्शित करता है।
msg.senderउस अकाउंट के अनुरूप है जिसने ट्रांजेक्शन को एक्जीक्यूट किया, विशेष रूप से पहला Remix डिफ़ॉल्ट अकाउंट, जो0x5B38Da6a701c568545dCfcB03FcB875f56beddC4है।msg.value1 ether की वैल्यू को दर्शाता है, जिसे मूल ट्रांजेक्शन में भेजा गया था।address(this)Caller कॉन्ट्रैक्ट का एड्रेस है, जैसा कि चित्र के बाईं ओर देखा जा सकता है, न कि Called कॉन्ट्रैक्ट का एड्रेस।

Remix में, हम msg.sender (0), msg.value (1) और address(this) (2) के लिए लॉग वैल्यूज प्रदर्शित करते हैं।
delegatecall में msg.data और इनपुट डेटा
msg.data प्रॉपर्टी एक्जीक्यूट किए जा रहे कॉन्टेक्स्ट का calldata रिटर्न करती है। जब किसी EOA (Externally Owned Account) द्वारा ट्रांजेक्शन के माध्यम से सीधे एक्जीक्यूट किए जा रहे फंक्शन में msg.data को कॉल किया जाता है, तो msg.data ट्रांजेक्शन के इनपुट डेटा को दर्शाता है।
जब हम कॉल या delegatecall एक्जीक्यूट करते हैं, तो हम उस इनपुट डेटा को आर्ग्यूमेंट (argument) के रूप में निर्दिष्ट करते हैं जो इम्प्लीमेंटेशन कॉन्ट्रैक्ट में एक्जीक्यूट होगा। इसलिए मूल (original) calldata delegatecall द्वारा बनाए गए सब-कॉन्टेक्स्ट के भीतर के calldata से भिन्न होता है, और परिणामस्वरूप msg.data भी भिन्न होगा।

इसे प्रदर्शित करने के लिए नीचे दिए गए कोड का उपयोग किया जाएगा।
contract Called {
function returnMsgData() public pure returns (bytes memory) {
return msg.data;
}
}
contract Caller {
function delegateMsgData(
address _called
) public returns (bytes memory data) {
(, data) = _called.delegatecall(
abi.encodeWithSignature("returnMsgData()"));
}
}
मूल ट्रांजेक्शन delegateMsgData फंक्शन को एक्जीक्यूट करता है, जिसके लिए address टाइप के एक पैरामीटर की आवश्यकता होती है। परिणामस्वरूप, इनपुट डेटा में एड्रेस के साथ-साथ फंक्शन सिग्नेचर शामिल होगा, जो ABI-encoded होगा।
delegateMsgData फंक्शन, बदले में, returnMsgData फंक्शन को delegatecalls करता है। इसे पूरा करने के लिए, रनटाइम को पास किए गए calldata में returnMsgData का सिग्नेचर होना चाहिए। परिणामस्वरूप, returnMsgData के अंदर msg.data की वैल्यू इसका अपना सिग्नेचर है, जो 0x0b1c837f द्वारा दिया गया है।
नीचे दी गई छवि में, हम देख सकते हैं कि returnMsgData का रिटर्न इसका अपना सिग्नेचर है, जो ABI-encoded है।

डिकोडेड आउटपुट returnMsgData फंक्शन का सिग्नेचर है, जो bytes के रूप में ABI-encoded है।
एक प्रति-उदाहरण (counterexample) के रूप में Codesize
हमने उल्लेख किया कि हम delegatecall की कल्पना इस विचार के साथ कर सकते हैं कि हम इम्प्लीमेंटेशन कॉन्ट्रैक्ट से बाइटकोड उधार ले रहे हैं और इसे कॉलिंग कॉन्ट्रैक्ट में एक्जीक्यूट कर रहे हैं। इसका एक अपवाद (exception) है, CODESIZE opcode।
मान लीजिए कि एक स्मार्ट कॉन्ट्रैक्ट के बाइटकोड में CODESIZE है, CODESIZE उस (that) कॉन्ट्रैक्ट का आकार (size) रिटर्न करता है। Codesize delegatecall के दौरान कॉलर के कोड का आकार रिटर्न नहीं करता है — यह उस कोड का आकार रिटर्न करता है जिसे delegatecalled किया गया था।
इस गुण (property) को प्रदर्शित करने के लिए, हमने नीचे कोड प्रदान किया है। Solidity में, CODESIZE को असेंबली में codesize() फंक्शन के माध्यम से एक्जीक्यूट किया जा सकता है। हमारे पास दो इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स हैं, CalledA और CalledB, जो केवल एक लोकल वेरिएबल द्वारा भिन्न होते हैं (ContractB में unused — वह वेरिएबल ContractA में अनुपस्थित है), जिसका उद्देश्य यह सुनिश्चित करना है कि कॉन्ट्रैक्ट्स का आकार अलग-अलग हो। इन कॉन्ट्रैक्ट्स को Caller कॉन्ट्रैक्ट के getSizes फंक्शन द्वारा delegatecall के माध्यम से कॉल किया जाता है।
// codesize 1103
contract Caller {
function getSizes(
address _calledA,
address _calledB
) public returns (uint sizeA, uint sizeB) {
(, bytes memory dataA) = _calledA.delegatecall(
abi.encodeWithSignature("getCodeSize()")
);
(, bytes memory dataB) = _calledB.delegatecall(
abi.encodeWithSignature("getCodeSize()")
);
sizeA = abi.decode(dataA, (uint256));
sizeB = abi.decode(dataB, (uint256));
}
}
// codesize 174
contract CalledA {
function getCodeSize() public pure returns (uint size) {
assembly {
size := codesize()
}
}
}
// codesize 180
contract CalledB {
function getCodeSize() public pure returns (uint size) {
uint unused = 100;
assembly {
size := codesize()
}
}
}
// You can use this contract to check the size of contracts
contract MeasureContractSize {
function measureConctract(address c) external view returns (uint256 size){
size = c.code.length;
}
}
यदि codesize फंक्शन Caller कॉन्ट्रैक्ट का आकार रिटर्न करता, तो delegate calling के माध्यम से ContractA और ContractB से getSizes() द्वारा रिटर्न की गई वैल्यूज समान होतीं। यानी, वे Caller का आकार होतीं, जो 1103 है। हालाँकि, जैसा कि हम नीचे दिए गए चित्र में देख सकते हैं, वैल्यूज भिन्न हैं, जो स्पष्ट रूप से इंगित करती हैं कि ये CalledA और CalledB के आकार हैं।

एक delegatecall को Delegatecall करना
कोई आश्चर्य कर सकता है: क्या होता है यदि एक कॉन्ट्रैक्ट दूसरे कॉन्ट्रैक्ट को delegatecall जारी करता है जो किसी तीसरे कॉन्ट्रैक्ट को delegatecall जारी करता है? ऐसे मामले में, कॉन्टेक्स्ट (context) मध्यवर्ती (intermediate) कॉन्ट्रैक्ट के बजाय उस कॉन्ट्रैक्ट का ही बना रहेगा जिसने पहला delegatecall इनिशिएट किया था।
यह इस प्रकार काम करता है:
Callerकॉन्ट्रैक्टCalledFirstकॉन्ट्रैक्ट मेंlogSender()फंक्शन को delegatecalls करता है।- इस फंक्शन का उद्देश्य एक इवेंट एमिट (emit) करना है जो
msg.senderको लॉग करता है। - इसके अतिरिक्त,
CalledFirstकॉन्ट्रैक्ट, इस लॉग को बनाने के अलावा,CalledLastकॉन्ट्रैक्ट को भी delegatecalls करता है। CalledLastकॉन्ट्रैक्ट भी एक इवेंट एमिट करता है, जोmsg.senderको भी लॉग करता है।
इस फ्लो को दर्शाने वाला एक आरेख नीचे प्रस्तुत किया गया है।

याद रखें, delegatecall केवल delegatecalled कॉन्ट्रैक्ट के बाइटकोड को उधार लेता है। इसकी कल्पना करने का एक तरीका यह है कि बाइटकोड अस्थायी रूप से कॉलिंग कॉन्ट्रैक्ट में “समाहित (absorbed)” हो जाता है। जब हम इसे इस तरह से देखते हैं, तो हम देखते हैं कि msg.sender हमेशा मूल (original) msg.sender होता है क्योंकि सब कुछ Caller के अंदर हो रहा है। नीचे एनिमेशन देखें:
नीचे हम delegatecall से delegatecall के कांसेप्ट का परीक्षण (test) करने के लिए कुछ सोर्स कोड प्रदान करते हैं:
contract Caller {
address calledFirst = 0xF27374C91BF602603AC5C9DaCC19BE431E3501cb;
function delegateCallToFirst() public {
calledFirst.delegatecall(
abi.encodeWithSignature("logSender()")
);
}
}
contract CalledFirst {
event SenderAtCalledFirst(address sender);
address constant calledLast = 0x1d142a62E2e98474093545D4A3A0f7DB9503B8BD;
function logSender() public {
emit SenderAtCalledFirst(msg.sender);
calledLast.delegatecall(
abi.encodeWithSignature("logSender()")
);
}
}
contract CalledLast {
event SenderAtCalledLast(address sender);
function logSender() public {
emit SenderAtCalledLast(msg.sender);
}
}
हम यह सोचने के लिए प्रेरित हो सकते हैं कि CalledLast में msg.sender CalledFirst का एड्रेस होगा, क्योंकि इसी ने CalledLast को कॉल किया था, लेकिन यह हमारे उस मॉडल का सम्मान नहीं करेगा कि delegatecall के माध्यम से कॉल किए गए कॉन्ट्रैक्ट का बाइटकोड केवल उधार लिया गया है, और यह कि कॉन्टेक्स्ट हमेशा उस कॉन्ट्रैक्ट का होता है जिसने delegatecall को एक्जीक्यूट किया था।
अंतिम परिणाम यह है कि दोनों msg.sender वैल्यूज उस अकाउंट के अनुरूप हैं जिसने Caller.delegateCallToFirst() के साथ ट्रांजेक्शन इनिशिएट किया था। यह नीचे दिए गए चित्र में देखा जा सकता है, जहाँ हम इस प्रक्रिया को Remix में एक्जीक्यूट करते हैं और लॉग कैप्चर करते हैं।

CalledFirst और CalledLast में msg.sender समान है
भ्रम का एक स्रोत यह है कि कुछ लोग इस ऑपरेशन का वर्णन इस प्रकार कर सकते हैं कि “Caller CalledFirst को delegatecalls करता है और CalledFirst CalledLast को delegatecalls करता है।” लेकिन ऐसा लगता है कि CalledFirst delegatecall कर रहा है — ऐसा नहीं है। CalledFirst बाइटकोड Called को प्रदान कर रहा है — और वह बाइटकोड CalledLast को एक delegatecall कर रहा है — Called से।
एक delegatecall से Call
आइए एक प्लॉट ट्विस्ट पेश करें और CalledFirst कॉन्ट्रैक्ट को मॉडिफाई करें। अब, CalledFirst call का उपयोग करके CalledLast को इन्वोक (invoke) करेगा, delegatecall का नहीं।

दूसरे शब्दों में, CalledFirst कॉन्ट्रैक्ट को निम्नलिखित कोड में अपडेट करने की आवश्यकता है:
contract CalledFirst {
event SenderAtCalledFirst(address sender);
address constant calledLast = ...;
function logSender() public {
emit SenderAtCalledFirst(msg.sender);
calledLast.call(
abi.encodeWithSignature("logSender()")
); // this is new
}
}
प्रश्न उठता है: SenderAtCalledLast इवेंट में लॉग किया गया msg.sender क्या होगा? निम्नलिखित एनिमेशन स्पष्ट करता है कि क्या होता है:

जब Caller delegatecall के माध्यम से CalledFirst में किसी फंक्शन को कॉल करता है, तो वह फंक्शन Caller के कॉन्टेक्स्ट के भीतर एक्जीक्यूट होता है। याद रखें, CalledFirst केवल Caller द्वारा एक्जीक्यूट किए जाने के लिए अपने बाइटकोड को “उधार (lends)” देता है। इस बिंदु पर, यह ऐसा है मानो हमने Caller कॉन्ट्रैक्ट में msg.sender को एक्जीक्यूट किया हो, जिसका अर्थ है कि msg.sender वह एड्रेस है जिसने ट्रांजेक्शन इनिशिएट किया था।

अब, CalledFirst CalledLast को कॉल करता है, लेकिन CalledFirst का उपयोग Caller के कॉन्टेक्स्ट में किया जा रहा है, इसलिए यह ऐसा है मानो Caller ने CalledLast को कॉल किया हो। इस मामले में, CalledLast में msg.sender Caller का एड्रेस होगा।
नीचे दिए गए चित्र में, हम Remix में लॉग्स को देखते हैं। ध्यान दें कि इस बार msg.sender की वैल्यूज अलग-अलग हैं।

CalledLast में msg.sender, Caller का एड्रेस है
अभ्यास (Exercise): यदि Caller CalledFirst को calls करता है और CalledFirst CalledLast को delegatecalls करता है, और प्रत्येक कॉन्ट्रैक्ट msg.sender को लॉग करता है, तो प्रत्येक कॉन्ट्रैक्ट कौन सा message sender लॉग करेगा?
लो-लेवल delegatecall
इस खंड में, हम इसकी कार्यक्षमता (functionality) को गहरे स्तर पर एक्सप्लोर करने के लिए YUL में delegatecall का उपयोग करेंगे। YUL में फंक्शन्स काफी हद तक opcode सिंटैक्स के समान होते हैं, जिससे पहले DELEGATECALL opcode की परिभाषा की जांच करना फायदेमंद हो जाता है।
DELEGATECALL स्टैक (stack) से क्रम में 6 आर्ग्यूमेंट्स लेता है: gas, address, argsOffset, argsSize, retOffset और retSize, और स्टैक में एक वैल्यू रिटर्न करता है जो दर्शाता है कि ऑपरेशन सफलतापूर्वक (1) पूरा किया गया था या नहीं (0)।
प्रत्येक आर्ग्यूमेंट का स्पष्टीकरण इस प्रकार है (evm.codes से लिया गया है):
- gas: एक्जीक्यूट करने के लिए सब-कॉन्टेक्स्ट को भेजी जाने वाली गैस (gas) की मात्रा। जो गैस सब-कॉन्टेक्स्ट द्वारा उपयोग नहीं की जाती है, वह इसमें वापस कर दी जाती है।
- address: वह अकाउंट जिसका कोड एक्जीक्यूट करना है।
- argsOffset: मेमोरी में बाइट्स (bytes) में बाइट ऑफसेट (byte offset), जो सब-कॉन्टेक्स्ट का calldata है।
- argsSize: कॉपी किए जाने वाले बाइट का आकार (calldata का आकार)।
- retOffset: मेमोरी में बाइट्स में बाइट ऑफसेट, जहाँ सब-कॉन्टेक्स्ट का रिटर्न डेटा स्टोर करना है।
- retSize: कॉपी किए जाने वाले बाइट का आकार (रिटर्न डेटा का आकार)।
delegatecall का उपयोग करके किसी कॉन्ट्रैक्ट में ईथर (ether) भेजना अनुमत (allowed) नहीं है (कल्पना करें कि यदि ऐसा होता तो संभावित कारनामे (exploits) क्या हो सकते थे!)। दूसरी ओर, CALL opcode ईथर ट्रांसफर की अनुमति देता है और इसमें यह इंगित करने के लिए एक अतिरिक्त पैरामीटर शामिल होता है कि कितना ईथर भेजा जाना चाहिए।
YUL में, delegatecall फंक्शन DELEGATECALL opcode को दर्शाता है और इसमें ऊपर बताए गए समान 6 आर्ग्यूमेंट्स शामिल हैं। इसका सिंटैक्स (syntax) है:
delegatecall(g, a, in, insize, out, outsize).
नीचे, हम दो फंक्शन्स के साथ एक कॉन्ट्रैक्ट प्रस्तुत करते हैं जो समान क्रिया (action) करते हैं, एक delegatecall एक्जीक्यूट करते हुए। एक शुद्ध Solidity में लिखा गया है, और दूसरे में YUL शामिल है।
contract DelegateYUL {
function delegateInSolidity(
address _address
) public returns (bytes memory data) {
(, data) = _address.delegatecall(
abi.encodeWithSignature("sayOne()")
);
}
function delegateInYUL(
address _address
) public returns (uint data) {
assembly {
mstore(0x00, 0x34ee2172) // Load the calldata I intend to send into memory at 0x00. The first slot will become 0x0000000000000000000000000000000000000000000000000000000034ee2172
let result := delegatecall(gas(), _address, 0x1c, 4, 0, 0x20) // The third parameter indicates the starting position in memory where the calldata is located, the fourth parameter specifies its size in bytes, and the fifth parameter specifies where the returned calldata, if any, should be stored in memory
data := mload(0) // Read delegatecall return from memory
}
}
}
contract Called {
function sayOne() public pure returns (uint) {
return 1;
}
}
delegateInSolidity फंक्शन में, मैं Solidity में delegatecall मेथड का उपयोग करता हूँ, sayOne फंक्शन के सिग्नेचर को पैरामीटर के रूप में पास करता हूँ, जिसकी गणना abi.encodeWithSignature मेथड का उपयोग करके की गई है।
यदि हमें पहले से रिटर्न का आकार नहीं पता है, तो चिंता न करें, हम इसे हैंडल करने के लिए बाद में returndatacopy फंक्शन का उपयोग कर सकते हैं। एक अन्य लेख में, जब हम delegatecall का उपयोग करके अपग्रेडेबल (upgradable) कॉन्ट्रैक्ट्स लिखने में गहराई से उतरेंगे, तो हम इन सभी विवरणों को कवर करेंगे।
EIP 150 और फॉरवर्ड की गई गैस (gas forwarded)
फॉरवर्ड की गई गैस (forwarded gas) के संबंध में एक मुद्दे पर ध्यान दें: हम delegatecall के पहले पैरामीटर के रूप में gas() फंक्शन का उपयोग करते हैं, जो उपलब्ध गैस रिटर्न करता है। यह इंगित करना चाहिए कि हम सभी उपलब्ध गैस को फॉरवर्ड करने का इरादा रखते हैं। हालाँकि, Tangerine Whistle fork के बाद से, delegatecall (और अन्य opcodes) के माध्यम से फॉरवर्डिंग के लिए कुल संभावित गैस का 63/64 का कैप (cap) लगा दिया गया है। दूसरे शब्दों में, हालाँकि gas() फंक्शन सभी उपलब्ध गैस रिटर्न करता है, लेकिन इसका केवल 63/64 हिस्सा ही नए सब-कॉन्टेक्स्ट में फॉरवर्ड किया जाता है, जबकि 1/64 हिस्सा रख (retained) लिया जाता है।
निष्कर्ष (Conclusion)
इस लेख को समाप्त करने के लिए, आइए हमने जो सीखा है उसका सारांश दें। Delegatecall कॉलिंग कॉन्ट्रैक्ट के कॉन्टेक्स्ट के भीतर अन्य कॉन्ट्रैक्ट्स में परिभाषित (defined) फंक्शन्स को एक्जीक्यूट करने की अनुमति देता है। कॉल्ड कॉन्ट्रैक्ट, जिसे इम्प्लीमेंटेशन कॉन्ट्रैक्ट के रूप में भी जाना जाता है, केवल अपना बाइटकोड प्रदान करता है, और इसके भीतर कुछ भी बदला नहीं जाता है या इसके स्टोरेज से प्राप्त (fetched) नहीं किया जाता है।
Delegatecall का उपयोग उस कॉन्ट्रैक्ट को अलग करने के लिए किया जाता है जहां डेटा स्टोर किया जाता है उस कॉन्ट्रैक्ट से जहां बिज़नेस लॉजिक या फंक्शन इम्प्लीमेंटेशन स्थित है। यह Solidity में कॉन्ट्रैक्ट अपग्रेडेबिलिटी (upgradability) के सबसे अधिक उपयोग किए जाने वाले पैटर्न की नींव बनाता है। हालाँकि, जैसा कि हमने देखा है, delegatecall का उपयोग बहुत सावधानी के साथ किया जाना चाहिए, क्योंकि अनजाने में स्टेट वेरिएबल्स में बदलाव हो सकते हैं, जो संभावित रूप से कॉलिंग कॉन्ट्रैक्ट को अनुपयोगी (unusable) बना सकते हैं।
RareSkills के साथ और जानें
Solidity में नए लोगों के लिए, हमारा निःशुल्क Solidity course देखें। इंटरमीडिएट Solidity डेवलपर्स कृपया हमारा Solidity Bootcamp देखें।
लेखक (Authorship)
यह लेख João Paulo Morais द्वारा RareSkills के सहयोग से लिखा गया था।
मूल रूप से 3 मई को प्रकाशित (Originally Published May 3)