यह लेख विस्तार से बताता है कि delegatecall कैसे काम करता है। Ethereum Virtual Machine (EVM) कॉन्ट्रैक्ट्स के बीच कॉल करने के लिए चार opcodes प्रदान करता है:
CALL (F1)CALLCODE (F2)STATICCALL (FA)- और
DELEGATECALL (F4)।
विशेष रूप से, CALLCODE opcode को Solidity v5 के बाद से हटा (deprecate) दिया गया है, और इसे DELEGATECALL द्वारा बदल दिया गया है। इन opcodes का Solidity में सीधा इम्प्लीमेंटेशन है और इन्हें address टाइप के वेरिएबल्स के मेथड्स के रूप में निष्पादित (execute) किया जा सकता है।
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()"));
}
}
address टाइप के प्रत्येक वेरिएबल, जैसे कि calledAddress वेरिएबल, में call नाम का एक मेथड होता है। यह मेथड ट्रांसैक्शन में निष्पादित होने वाले इनपुट डेटा को पैरामीटर के रूप में स्वीकार करता है, यानी ABI encoded calldata। ऊपर बताए गए मामले में, इनपुट डेटा को function selector 0xd09de08a के साथ, increment() फ़ंक्शन के सिग्नेचर के अनुरूप होना चाहिए। हम फ़ंक्शन परिभाषा से इस सिग्नेचर को जनरेट करने के लिए abi.encodeWithSignature मेथड का उपयोग करते हैं।
यदि आप Caller कॉन्ट्रैक्ट में callIncrement फ़ंक्शन को निष्पादित करते हैं, तो आप देखेंगे कि Called में स्टेट वेरिएबल number 1 से बढ़ (increment) जाएगा। call मेथड यह सत्यापित नहीं करता है कि डेस्टिनेशन एड्रेस वास्तव में किसी मौजूदा कॉन्ट्रैक्ट से मेल खाता है या नहीं, और न ही यह कि इसमें निर्दिष्ट (specified) फ़ंक्शन मौजूद है।
call ट्रांसैक्शन को नीचे दिए गए वीडियो में विज़ुअलाइज़ किया गया है:
Call एक ट्यूपल (tuple) रिटर्न करता है
call मेथड दो वैल्यूज़ के साथ एक ट्यूपल रिटर्न करता है। पहली वैल्यू एक बूलियन (boolean) है जो ट्रांसैक्शन की सफलता या विफलता को दर्शाती है। दूसरी वैल्यू, जो 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) को संभालना
आइए एक गैर-मौजूद फ़ंक्शन के लिए एक और कॉल शामिल करने के लिए उपरोक्त कॉन्ट्रैक्ट को संशोधित करें, जैसा कि नीचे दिखाया गया है।
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 रिटर्न करेगा। रिटर्न बूलियन को स्पष्ट रूप से (explicitly) संभाला गया है, और यदि success फाल्स है तो ट्रांसैक्शन रिवर्ट हो जाएगा।
हमें यह ट्रैक करने में सावधानी बरतनी चाहिए कि कॉल सफल हुआ या नहीं, और हम शीघ्र ही इस विषय पर फिर से विचार करेंगे।
EVM पर्दे के पीछे क्या करता है
increment फ़ंक्शन का उद्देश्य number नामक स्टेट वेरिएबल को बढ़ाना (increment) है। चूंकि EVM को स्टेट वेरिएबल्स का ज्ञान नहीं होता है बल्कि यह स्टोरेज स्लॉट्स (storage slots) पर काम करता है, इसलिए फ़ंक्शन वास्तव में स्टोरेज के पहले स्लॉट में वैल्यू को बढ़ाता है, जिसे slot 0 के रूप में जाना जाता है। यह ऑपरेशन Called कॉन्ट्रैक्ट के स्टोरेज के भीतर होता है।

call मेथड का उपयोग करने की समीक्षा करने से हमें delegatecall का उपयोग करने के तरीके के बारे में एक विचार बनाने में मदद मिलेगी।
DELEGATECALL
एक कॉन्ट्रैक्ट जो किसी टार्गेट स्मार्ट कॉन्ट्रैक्ट को delegatecall करता है, वह टार्गेट कॉन्ट्रैक्ट के लॉजिक को अपने स्वयं के वातावरण (environment) के भीतर निष्पादित करता है।
एक मेंटल मॉडल (mental model) यह है कि यह टार्गेट स्मार्ट कॉन्ट्रैक्ट के कोड को कॉपी करता है और उस कोड को स्वयं चलाता है। टार्गेट किए गए स्मार्ट कॉन्ट्रैक्ट को आमतौर पर “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 कॉन्ट्रैक्ट ने अपने स्वयं के कॉन्टेक्स्ट (context) में निष्पादित करने के लिए Called का कोड उधार लिया हो।
नीचे दिया गया आरेख (diagram) आगे स्पष्ट करता है कि delegatecall कैसे Called के बजाय Caller के स्टोरेज को संशोधित करता है।

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

स्टोरेज स्लॉट कोलिजन (Storage slot collision)
delegatecall जारी करने वाले कॉन्ट्रैक्ट को यह अनुमान लगाने में बेहद सावधान रहना चाहिए कि उसके कौन से स्टोरेज स्लॉट संशोधित होंगे। पिछला उदाहरण पूरी तरह से काम कर गया क्योंकि Caller ने slot 0 में स्टेट वेरिएबल का उपयोग नहीं किया था। delegatecall का उपयोग करते समय एक सामान्य बग इस तथ्य को भूल जाना है। आइए इसका एक उदाहरण देखें।
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()")
);
}
}
ध्यान दें कि ऊपर दिए गए अपडेटेड कॉन्ट्रैक्ट में, स्लॉट 0 का कंटेंट Called कॉन्ट्रैक्ट का एड्रेस है, जबकि myNumber वेरिएबल अब स्लॉट 1 में स्टोर है।
यदि आप प्रदान किए गए कॉन्ट्रैक्ट्स को डिप्लॉय (deploy) करते हैं और callIncrement फ़ंक्शन को निष्पादित करते हैं, तो Caller स्टोरेज के slot 0 में वृद्धि (increment) होगी, लेकिन वहाँ calledAddress वेरिएबल है, myNumber वेरिएबल नहीं।
निम्नलिखित वीडियो इस बग को दर्शाता है:
आइए नीचे स्पष्ट करें कि क्या हुआ।

इसलिए, delegatecall का उपयोग करते समय सावधानी बरतनी चाहिए, क्योंकि यह अनजाने में हमारे कॉन्ट्रैक्ट को खराब (break) कर सकता है। उपरोक्त उदाहरण में, यह संभवतः प्रोग्रामर का इरादा नहीं है कि वह 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 के उचित कामकाज के लिए दोनों कॉन्ट्रैक्ट्स के स्टेट वेरिएबल्स को अलाइन (align) करना महत्वपूर्ण है।
इम्प्लीमेंटेशन को डेटा से अलग करना (Decouple implementation from data)
delegatecall का सबसे महत्वपूर्ण उपयोग उस कॉन्ट्रैक्ट को अलग (decouple) करना है जहां डेटा स्टोर किया जाता है, जैसे कि इस मामले में Caller, उस कॉन्ट्रैक्ट से जहां निष्पादन लॉजिक (execution logic) रहता है, जैसे कि Called। इसलिए, यदि कोई निष्पादन लॉजिक को बदलना चाहता है, तो वह बस स्टोरेज को छुए बिना Called को किसी अन्य कॉन्ट्रैक्ट से बदल सकता है और इम्प्लीमेंटेशन कॉन्ट्रैक्ट के रेफरेंस को अपडेट कर सकता है। Caller अब अपने पास मौजूद फ़ंक्शंस से विवश (constrained) नहीं है, यह अन्य कॉन्ट्रैक्ट्स से आवश्यक फ़ंक्शंस को delegatecall कर सकता है।
यदि निष्पादन लॉजिक को बदलने की आवश्यकता है, उदाहरण के लिए, myNumber की वैल्यू को जोड़ने के बजाय 1 यूनिट घटाना, तो आप एक नया इम्प्लीमेंटेशन कॉन्ट्रैक्ट बना सकते हैं, जैसा कि नीचे दिखाया गया है।
contract NewCalled {
uint public number;
function increment() public {
number = number - 1;
}
}
दुर्भाग्य से, कॉल किए जाने वाले फ़ंक्शन का नाम बदलना संभव नहीं है, क्योंकि ऐसा करने से उसका सिग्नेचर बदल जाएगा।
नया इम्प्लीमेंटेशन कॉन्ट्रैक्ट, NewCalled बनाने के बाद, कोई भी बस इस नए कॉन्ट्रैक्ट को डिप्लॉय कर सकता है और Caller में calledAddress स्टेट वेरिएबल को बदल सकता है। बेशक, Caller के पास उस एड्रेस को बदलने का एक तंत्र (mechanism) होना चाहिए जिसे वह delegateCall जारी कर रहा है, जिसे हमने कोड को संक्षिप्त रखने के लिए शामिल नहीं किया है।
हमने Caller कॉन्ट्रैक्ट द्वारा उपयोग किए जाने वाले बिज़नेस लॉजिक को सफलतापूर्वक संशोधित कर लिया है। डेटा को निष्पादन लॉजिक से अलग करने से हमें Solidity में अपग्रेडेबल (upgradable) स्मार्ट कॉन्ट्रैक्ट्स बनाने की अनुमति मिलती है।

ऊपर दी गई छवि में, बाईं ओर का कॉन्ट्रैक्ट डेटा और लॉजिक दोनों को संभालता है। दाईं ओर, शीर्ष कॉन्ट्रैक्ट में डेटा होता है, लेकिन डेटा को अपडेट करने का तंत्र लॉजिक कॉन्ट्रैक्ट में रखा जाता है। डेटा को अपडेट करने के लिए, लॉजिक कॉन्ट्रैक्ट को एक delegatecall किया जाता है।
delegatecall के रिटर्न्स को संभालना
call की तरह ही, delegatecall भी दो वैल्यूज़ वाला एक ट्यूपल रिटर्न करता है: निष्पादन की सफलता का संकेत देने वाला एक बूलियन और 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 कॉन्ट्रैक्ट में डिस्काउंट प्राइस की गणना करने का लॉजिक होता है। हम delegatecall के माध्यम से calculateDiscountPrice फ़ंक्शन को निष्पादित करके इस लॉजिक का उपयोग करते हैं। यह फ़ंक्शन एक वैल्यू रिटर्न करता है, जिसे हमें abi.decode का उपयोग करके डिकोड करना होगा। इस रिटर्न वैल्यू के आधार पर कोई भी निर्णय लेने से पहले, यह जांचना महत्वपूर्ण है कि क्या फ़ंक्शन सफलतापूर्वक निष्पादित किया गया था, अन्यथा हम एक ऐसे रिटर्न को पार्स (parse) करने का प्रयास कर सकते हैं जो वहां मौजूद नहीं है या अंत में एक रिवर्ट रीज़न स्ट्रिंग (revert reason string) को पार्स कर सकते हैं।
जब call और delegatecall false रिटर्न करते हैं
समझने के लिए एक महत्वपूर्ण बिंदु यह है कि सफलता (success) का मान true या false कब होगा। अनिवार्य रूप से, यह इस बात पर निर्भर करता है कि निष्पादित किया जा रहा फ़ंक्शन रिवर्ट होगा या नहीं। तीन तरीके हैं जिनसे निष्पादन रिवर्ट हो सकता है:
- यदि इसका सामना किसी REVERT opcode से होता है,
- यदि इसका गैस खत्म हो जाता है (runs out of gas),
- यदि यह कुछ निषिद्ध (prohibited) करने का प्रयास करता है, जैसे शून्य (zero) से विभाजित करना।
यदि delegatecall (या call) के माध्यम से निष्पादित किए जा रहे फ़ंक्शन का सामना इनमें से किसी भी स्थिति से होता है, तो यह रिवर्ट हो जाएगा, और delegatecall की रिटर्न वैल्यू फाल्स (false) होगी।
एक प्रश्न जो अक्सर डेवलपर्स को भ्रमित करता है वह यह है कि एक गैर-मौजूद कॉन्ट्रैक्ट के लिए delegatecall रिवर्ट क्यों नहीं होता है और फिर भी रिपोर्ट करता है कि निष्पादन सफल रहा। हमने जो कहा उसके आधार पर, एक खाली एड्रेस (empty address) रिवर्ट होने की तीन शर्तों में से किसी एक को भी कभी पूरा नहीं करेगा, इसलिए यह कभी रिवर्ट नहीं होगा।
स्टोरेज वेरिएबल की समस्याओं (gotchas) का एक और उदाहरण
स्टोरेज लेआउट से संबंधित बग का एक और उदाहरण देने के लिए आइए उपरोक्त कोड में थोड़ा संशोधन करें।
Caller कॉन्ट्रैक्ट अभी भी delegatecall के माध्यम से एक इम्प्लीमेंटेशन कॉन्ट्रैक्ट को इनवोक (invoke) करता है, लेकिन अब Called कॉन्ट्रैक्ट एक स्टेट वेरिएबल से वैल्यू पढ़ता है। यह एक मामूली बदलाव लग सकता है, लेकिन यह वास्तव में आपदा (disaster) का कारण बनता है। क्या आप पता लगा सकते हैं कि क्यों?
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 पर कब्जा कर लेते हैं।
आपको 200% की छूट प्राप्त होगी, जो काफी अधिक है (और फ़ंक्शन को रिवर्ट करने का कारण बनेगी, क्योंकि नया गणना किया गया मूल्य नकारात्मक (negative) हो जाएगा, जिसकी अनुमति एक uint टाइप के वेरिएबल के लिए नहीं है)।
delegatecall में Immutable और Constant वेरिएबल्स: एक बग स्टोरी
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? आइए इसके बारे में तर्क (reason) करें।
getValueDelegateफ़ंक्शन,getValueफ़ंक्शन को निष्पादित करता है, जो संभवतः slot 0 में स्टेट वेरिएबल के अनुरूप वैल्यू रिटर्न करता है।- चूंकि यह delegatecall है, इसलिए हमें कॉलिंग कॉन्ट्रैक्ट में स्लॉट की जांच करनी चाहिए, न कि कॉल किए गए कॉन्ट्रैक्ट की।
Callerमें वेरिएबलaकी वैल्यू 3 है, इसलिए जवाब 3 होना चाहिए। बिलकुल सही (Nailed it)।
आश्चर्यजनक रूप से, सही उत्तर 2 है। क्यों?!
Immutable या constant स्टेट वेरिएबल्स वास्तविक (true) स्टेट वेरिएबल्स नहीं होते हैं: वे किसी स्लॉट पर कब्जा नहीं करते हैं। जब हम immutable वेरिएबल्स घोषित करते हैं, तो उनका मान (value) कॉन्ट्रैक्ट बाइटकोड में हार्डकोड (hardcode) किया जाता है जो delegatecall के दौरान निष्पादित होता है। इस प्रकार, getValue फ़ंक्शन हार्डकोडेड मान 2 रिटर्न करता है।
msg.sender, msg.value और address(this)
यदि हम Called कॉन्ट्रैक्ट के भीतर msg.sender, msg.value और address(this) का उपयोग करते हैं, तो ये सभी मान Caller कॉन्ट्रैक्ट के msg.sender, msg.value और address(this) मानों के अनुरूप होंगे। आइए याद करें कि delegatecall कैसे संचालित होता है: सब कुछ कॉलर कॉन्ट्रैक्ट (caller contract) के कॉन्टेक्स्ट के भीतर होता है। इम्प्लीमेंटेशन कॉन्ट्रैक्ट केवल निष्पादित किए जाने वाले बाइटकोड को प्रदान करता है, इससे अधिक कुछ नहीं।

आइए इस अवधारणा (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 ईथर के मान को दर्शाता है, जिसे मूल ट्रांसैक्शन में भेजा गया था।address(this)Caller कॉन्ट्रैक्ट का एड्रेस है, जैसा कि चित्र के बाईं ओर देखा जा सकता है, न कि Called कॉन्ट्रैक्ट का एड्रेस।

Remix में, हम msg.sender (0), msg.value (1) और address(this) (2) के लिए लॉग मान प्रदर्शित करते हैं।
delegatecall में msg.data और इनपुट डेटा
msg.data प्रॉपर्टी निष्पादित किए जा रहे कॉन्टेक्स्ट का calldata रिटर्न करती है। जब EOA द्वारा किसी ट्रांसैक्शन के माध्यम से सीधे निष्पादित किए जा रहे फ़ंक्शन में msg.data को कॉल किया जाता है, तो msg.data ट्रांसैक्शन के इनपुट डेटा का प्रतिनिधित्व करता है।
जब हम कोई call या delegatecall निष्पादित करते हैं, तो हम तर्क (argument) के रूप में उस इनपुट डेटा को निर्दिष्ट करते हैं जो इम्प्लीमेंटेशन कॉन्ट्रैक्ट में निष्पादित किया जाएगा। इसलिए मूल calldata, delegatecall द्वारा बनाए गए सब-कॉन्टेक्स्ट (sub-context) के भीतर के 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 फ़ंक्शन को delegatecall करता है। इसे पूरा करने के लिए, रनटाइम को पास किए गए calldata में returnMsgData का सिग्नेचर होना चाहिए। परिणामस्वरूप, returnMsgData के अंदर msg.data का मान इसका अपना सिग्नेचर है, जो 0x0b1c837f द्वारा दिया गया है।
नीचे दी गई छवि में, हम देख सकते हैं कि returnMsgData का रिटर्न इसका अपना सिग्नेचर (ABI-encoded) है।

डिकोड किया गया आउटपुट returnMsgData फ़ंक्शन का सिग्नेचर है, जो bytes के रूप में ABI-encoded है।
काउंटरएग्ज़ाम्पल (counterexample) के रूप में Codesize
हमने उल्लेख किया था कि हम delegatecall की कल्पना इस विचार के साथ कर सकते हैं कि हम इम्प्लीमेंटेशन कॉन्ट्रैक्ट से बाइटकोड उधार ले रहे हैं और इसे कॉलिंग कॉन्ट्रैक्ट में निष्पादित कर रहे हैं। इसका एक अपवाद है, CODESIZE opcode।
मान लीजिए कि किसी स्मार्ट कॉन्ट्रैक्ट के बाइटकोड में CODESIZE है, CODESIZE उस कॉन्ट्रैक्ट का आकार (size) रिटर्न करता है। Codesize एक delegatecall के दौरान कॉलर के कोड का आकार रिटर्न नहीं करता है — यह उस कोड का आकार रिटर्न करता है जिसे delegatecalled किया गया था।
इस प्रॉपर्टी को प्रदर्शित करने के लिए, हमने नीचे कोड प्रदान किया है। Solidity में, CODESIZE को असेंबली (assembly) में codesize() फ़ंक्शन के माध्यम से निष्पादित किया जा सकता है। हमारे पास दो इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स हैं, CalledA और CalledB, जो केवल एक स्थानीय (local) वेरिएबल द्वारा भिन्न होते हैं (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 कॉन्ट्रैक्ट का आकार रिटर्न करता, तो ContractA और ContractB को delegate call करके getSizes() से रिटर्न किए गए मान समान होते। यानी, वे Caller का आकार होते, जो 1103 है। हालाँकि, जैसा कि हम नीचे दिए गए चित्र में देख सकते हैं, मान अलग-अलग हैं, जो स्पष्ट रूप से दर्शाता है कि ये CalledA और CalledB के आकार हैं।

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

याद रखें, delegatecall केवल delegatecall किए गए कॉन्ट्रैक्ट का बाइटकोड उधार लेता है। इसकी कल्पना करने का एक तरीका यह है कि बाइटकोड को अस्थायी रूप से कॉलिंग कॉन्ट्रैक्ट में “अवशोषित” (absorbed) कर लिया जाता है। जब हम इसे इस तरह देखते हैं, तो हम पाते हैं कि msg.sender हमेशा मूल msg.sender होता है क्योंकि सब कुछ Caller के अंदर हो रहा है। नीचे दिया गया एनीमेशन देखें:
नीचे हम एक delegatecall को delegatecall करने की अवधारणा का परीक्षण करने के लिए कुछ स्रोत कोड (source code) प्रदान करते हैं:
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 समान है
एक delegatecall से Call
आइए कहानी में एक नया मोड़ (plot twist) लाएँ और CalledFirst कॉन्ट्रैक्ट को संशोधित करें। अब, CalledFirst, CalledLast को delegatecall का उपयोग करके नहीं, बल्कि call का उपयोग करके इनवोक (invoke) करेगा।

दूसरे शब्दों में, 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 को call करता है और CalledFirst CalledLast को delegatecalls करता है, और प्रत्येक कॉन्ट्रैक्ट msg.sender को लॉग करता है, तो प्रत्येक कॉन्ट्रैक्ट किस मैसेज सेंडर को लॉग करेगा?
लो-लेवल delegatecall
इस खंड में, हम YUL में delegatecall का उपयोग इसके कार्यक्षमता का गहरे स्तर पर पता लगाने के लिए करेंगे। YUL में फ़ंक्शंस काफी हद तक opcode सिंटैक्स के समान होते हैं, जिससे पहले DELEGATECALL opcode की परिभाषा की जांच करना फायदेमंद होता है।
DELEGATECALL स्टैक (stack) से क्रम में 6 आर्ग्यूमेंट्स लेता है: gas, address, argsOffset, argsSize, retOffset और retSize, और स्टैक में एक वैल्यू रिटर्न करता है जो दर्शाता है कि ऑपरेशन सफलतापूर्वक किया गया था (1) या नहीं (0)।
प्रत्येक आर्ग्यूमेंट का स्पष्टीकरण इस प्रकार है (evm.codes से लिया गया):
- gas: निष्पादित करने के लिए सब कॉन्टेक्स्ट (sub context) को भेजी जाने वाली गैस की मात्रा। जो गैस सब कॉन्टेक्स्ट द्वारा उपयोग नहीं की जाती है, वह इसे वापस कर दी जाती है।
- address: वह अकाउंट जिसका कोड निष्पादित करना है।
- argsOffset: बाइट्स में मेमोरी में बाइट ऑफ़सेट, सब कॉन्टेक्स्ट का calldata।
- argsSize: कॉपी किए जाने वाले बाइट का आकार (calldata का आकार)।
- retOffset: बाइट्स में मेमोरी में बाइट ऑफ़सेट, जहां सब कॉन्टेक्स्ट के रिटर्न डेटा को स्टोर करना है।
- retSize: कॉपी किए जाने वाले बाइट का आकार (रिटर्न डेटा का आकार)।
delegatecall का उपयोग करके किसी कॉन्ट्रैक्ट में ईथर भेजने की अनुमति नहीं है (कल्पना करें कि यदि ऐसा होता तो कितने संभावित कारनामे (exploits) हो सकते थे!)। दूसरी ओर, CALL opcode ईथर ट्रांसफर की अनुमति देता है और इसमें यह इंगित करने के लिए एक अतिरिक्त पैरामीटर शामिल होता है कि कितना ईथर भेजा जाना चाहिए।
YUL में, delegatecall फ़ंक्शन DELEGATECALL opcode को प्रतिबिंबित (mirror) करता है और इसमें ऊपर बताए गए समान 6 आर्ग्यूमेंट्स शामिल हैं। इसका सिंटैक्स है:
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 मेथड का उपयोग करता हूँ, abi.encodeWithSignature मेथड का उपयोग करके गणना किए गए sayOne फ़ंक्शन के सिग्नेचर को एक पैरामीटर के रूप में पास करता हूँ।
यदि हम पहले से रिटर्न का आकार नहीं जानते हैं, तो चिंता न करें, हम इसे संभालने के लिए बाद में returndatacopy फ़ंक्शन का उपयोग कर सकते हैं। एक अन्य लेख में, जब हम delegatecall का उपयोग करके अपग्रेडेबल (upgradable) कॉन्ट्रैक्ट्स लिखने में गहराई से उतरेंगे, तो हम इन सभी विवरणों को कवर करेंगे।
EIP 150 और गैस फॉरवर्डिंग (gas forwarded)
फॉरवर्ड की गई गैस के संबंध में एक मुद्दे पर एक नोट: हम delegatecall के पहले पैरामीटर के रूप में gas() फ़ंक्शन का उपयोग करते हैं, जो उपलब्ध गैस रिटर्न करता है। यह इस बात का संकेत होना चाहिए कि हम सभी उपलब्ध गैस को फॉरवर्ड करने का इरादा रखते हैं। हालाँकि, Tangerine Whistle fork के बाद से, delegatecall (और अन्य opcodes) के माध्यम से फॉरवर्ड करने के लिए कुल संभावित गैस की 63/64 की सीमा (cap) लगा दी गई है। दूसरे शब्दों में, यद्यपि gas() फ़ंक्शन सभी उपलब्ध गैस रिटर्न करता है, इसका केवल 63/64 हिस्सा नए सबकॉन्टेक्स्ट को फॉरवर्ड किया जाता है, जबकि 1/64 हिस्सा बरकरार (retained) रखा जाता है।
निष्कर्ष (Conclusion)
इस लेख को समाप्त करने के लिए, आइए हम जो कुछ भी सीखे हैं उसका सारांश दें। Delegatecall कॉलिंग कॉन्ट्रैक्ट के कॉन्टेक्स्ट के भीतर अन्य कॉन्ट्रैक्ट्स में परिभाषित फ़ंक्शंस के निष्पादन की अनुमति देता है। कॉल किया गया कॉन्ट्रैक्ट, जिसे इम्प्लीमेंटेशन कॉन्ट्रैक्ट के रूप में भी जाना जाता है, केवल अपना बाइटकोड प्रदान करता है, और इसके भीतर कुछ भी बदला या इसके स्टोरेज से प्राप्त नहीं किया जाता है।
Delegatecall का उपयोग उस कॉन्ट्रैक्ट को अलग करने के लिए किया जाता है जहां डेटा स्टोर किया जाता है, उस कॉन्ट्रैक्ट से जहां बिज़नेस लॉजिक या फ़ंक्शन इम्प्लीमेंटेशन स्थित होता है। यह Solidity में कॉन्ट्रैक्ट अपग्रेडेबिलिटी (upgradability) के सबसे अधिक उपयोग किए जाने वाले पैटर्न की नींव बनाता है। हालाँकि, जैसा कि हमने देखा है, delegatecall का उपयोग बहुत सावधानी के साथ किया जाना चाहिए, क्योंकि स्टेट वेरिएबल्स में अनपेक्षित बदलाव हो सकते हैं, जिससे संभावित रूप से कॉलिंग कॉन्ट्रैक्ट अनुपयोगी हो सकता है।
RareSkills के साथ और जानें
जो लोग Solidity में नए हैं, उनके लिए हमारा मुफ्त Solidity course देखें। इंटरमीडिएट Solidity डेवलपर्स कृपया हमारा Solidity Bootcamp देखें।
लेखकत्व (Authorship)
यह लेख RareSkills के सहयोग से João Paulo Morais द्वारा लिखा गया था।
मूल रूप से 3 मई को प्रकाशित