आप जिस भी DeFi प्रोटोकॉल में अपने स्टेबलकॉइन्स डिपॉजिट करते हैं, उनमें से लगभग 90% में बैकडोर कंट्रोल पैनल पहले से छिपा होता है। इसे बड़े प्यार से Upgradeability (अपग्रेडेबिलिटी) कहते हैं। व्हाइटपेपर पर तो इसे बग्स फिक्स करने और गैस ऑप्टिमाइज करने का बहाना बनाकर बेचा जाता है। लेकिन हकीकत? बस एक क्लिक में लाइव कॉन्ट्रैक्ट के कोड को किसी भी रैंडम स्कैम कोड से बदलकर पूरी लिक्विडिटी साफ की जा सकती है।
कल रात के तीन बजे तक मैं Base नेटवर्क पर आए एक नए फोर्क का कोड खंगाल रहा था। मुझे लगा शायद नींद की वजह से मेरी आंखें धोखा खा रही हैं, पर नहीं—प्रॉक्सी में साफ-साफ क्लासिक टाइमबॉम्ब लगा हुआ था। मजे की बात देखिए, इनका ऑडिट हो चुका है! किसी टॉप-टियर फर्म की चमचमाती PDF रिपोर्ट भी है। चलिए, इसका पूरा कच्चा चिट्ठा खोलते हैं।
आर्किटेक्चर का धोखा: प्रॉक्सी कैसे काम करते हैं
एक आम यूजर के लिए स्मार्ट कॉन्ट्रैक्ट का मतलब है एक पत्थर की लकीर—एक बार डिप्लॉय कर दिया, तो खेल खत्म। लेकिन अगर प्रोटोकॉल को इवॉल्व (अपग्रेड) करना हो, तो आर्किटेक्चर को दो हिस्सों में बांट दिया जाता है: प्रॉक्सी (Proxy) और लॉजिक (Implementation)। यूजर हमेशा सिर्फ प्रॉक्सी से इंटरैक्ट करता है। प्रॉक्सी पूरी तरह डंब होता है, उसमें बिजनेस लॉजिक नाम की कोई चीज नहीं होती। उसका एकमात्र काम हर कॉल को delegatecall के जरिए आगे फॉरवर्ड करना है।
यहीं पर असली खेल होता है। Solidity में delegatecall सबसे खतरनाक ऑपकोड है। यह टारगेट कॉन्ट्रैक्ट (लॉजिक) का कोड तो रन करता है, लेकिन प्रॉक्सी के खुद के स्टोरेज संदर्भ (context) के अंदर। सीधे शब्दों में कहें तो वेरिएबल्स प्रॉक्सी में ही रहते हैं, बस कोड बाहर से लाया जाता है। एडमिन ने प्रॉक्सी में लॉजिक का एड्रेस बदला और काम तमाम—आपका प्रोटोकॉल रातों-रात अपग्रेड हो गया, या फिर बैकडोर एक्टिव हो गया।
सिक्योरिटी के नाम पर बेचे जाने वाले कुछ स्टैंडर्ड पैटर्न्स:
- UUPS (UUPSUpgradeable): इसमें लॉजिक एड्रेस वाला स्लॉट खुद लॉजिक कॉन्ट्रैक्ट के अंदर ही होता है। अगर एडमिन ने कोई ऐसा खराब लॉजिक डिप्लॉय कर दिया जो UUPS से इनहेरिट करना भूल गया, तो कॉन्ट्रैक्ट हमेशा के लिए ईंट (brick) बन जाएगा। सारा फंड लॉक। काफी विडंबना है, ना?
- Transparent Proxy Pattern (TPP): यहाँ अपग्रेड के पूरे तामझाम को संभालने के लिए एक अलग
ProxyAdminकॉन्ट्रैक्ट होता है। इसमें रोल्स क्लियर होते हैं: यूजर्स बिजनेस लॉजिक को कॉल करते हैं और एडमिन सिर्फ अपग्रेड वाले फंक्शन्स को। देखने में यह काफी सेफ लगता है, लेकिन फॉलबैक लेवल पर बार-बारmsg.senderचेक होने की वजह से यह गैस की भयंकर बलि लेता है। - Beacon Proxy: एक सिंगल बीकन कॉन्ट्रैक्ट सैकड़ों एक जैसे प्रॉक्सीज के लिए लॉजिक एड्रेस को स्टोर करके रखता है। NFT कलेक्शंस या लिक्विडिटी पूल्स के लिए यह बेहद काम की चीज है। बीकन में एक बार एड्रेस बदलो और हजार कॉन्ट्रैक्ट्स एक साथ अपग्रेड। डेवलपर्स के लिए तो बढ़िया है, पर किसी हैकर के हाथ यह लग गया तो? बस एक सेंधमारी और पूरा का पूरा पूल्स का नेटवर्क ठप।
रग पुल की क्रोनोलॉजी: आपका फंड कैसे गायब होता है
क्या आपको लगता है कि फंड उड़ाने के लिए किसी बहुत बड़े या कॉम्प्लेक्स एक्सप्लोइट की जरूरत होती है? बिल्कुल नहीं। एडमिन को अपने नए लॉजिक वाले कोड में बस एक मामूली सी लाइन बदलनी होती है।
चलिए लाइव कोड देखते हैं। मैंने रफली एक क्लासिक उदाहरण तैयार किया है कि कैसे एक सीधा-साधा दिखने वाला कॉन्ट्रैक्ट पलक झपकते ही स्कैम में बदल जाता है।
स्टेज 1: साफ-सुथरा कॉन्ट्रैक्ट (Implementation_V1.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
// नॉर्मल पूल, इन्वेस्टर्स फंड डाल रहे हैं और यील्ड का इंतजार कर रहे हैं
contract VaultV1 is Initializable {
address public admin;
mapping(address => uint256) public balances;
uint256 public totalFunds;
// कंस्ट्रक्टर की जगह इनिशियलाइजर। अगर इसे कॉल करना भूले, तो कॉन्ट्रैक्ट लावारिस है, जो पहले आएगा वही मालिक बनेगा।
function initialize() public initializer {
admin = msg.sender; // डिप्लॉयर या मल्टीसिग का एड्रेस यहाँ स्टोर होगा
}
function deposit() external payable {
require(msg.value > 0, "Zero funds");
balances[msg.sender] += msg.value;
totalFunds += msg.value;
}
// बिल्कुल साफ विथड्रॉल, कोई हिडन फीस नहीं। फिलहाल के लिए।
function withdraw(uint256 _amount) external {
require(balances[msg.sender] >= _amount, "Low balance");
balances[msg.sender] -= _amount;
totalFunds -= _amount;
payable(msg.sender).transfer(_amount);
}
}कोड एकदम क्लीन है। ऑडिटर्स ने ग्रीन सिग्नल दे दिया। प्रोटोकॉल लाइव हो गया, टीवीएल (TVL) तेजी से बढ़ने लगी और टेलीग्राम ग्रुप्स में सब जश्न मना रहे हैं।
स्टेज 2: आधी रात का अपग्रेड (Implementation_V2.sol)
एक महीना बीत जाता है। पूल में 5000 ETH जमा हो चुके हैं। अब एडमिन (या वो हैकर जिसने प्राइवेट की चुराई है) वर्जन 2 डिप्लॉय करता है। वह प्रॉक्सी के upgradeTo() फंक्शन को ट्रिगर करके उसमें नया एड्रेस फीड कर देता है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract VaultV2 is Initializable {
// सबसे जरूरी: स्टोरेज लेआउट बिल्कुल V1 जैसा ही होना चाहिए, एक-एक इंच।
// एक भी वेरिएबल ऊपर-नीचे हुआ, तो सारे मैपिंग्स बर्बाद हो जाएंगे और रायता फैल जाएगा।
address public admin;
mapping(address => uint256) public balances;
uint256 public totalFunds;
address public constant shadowWallet = 0x9965507B1a05951961A0175f653429f1c08afde6; // सीक्रेट ऑफशोर वॉलेट
// दोबारा इनिशियलाइज होने से रोकने के लिए खाली इनिशियलाइजर
function initialize() public initializer {}
function deposit() external payable {
balances[msg.sender] += msg.value;
totalFunds += msg.value;
}
// ये रहा हमारा चोर दरवाजा। क्या कोई नॉर्मल यूजर इसे UI पर पकड़ पाएगा? कभी नहीं।
function withdraw(uint256 _amount) external {
// ऊपर से सब नॉर्मल लगेगा, लेकिन...
require(balances[msg.sender] >= _amount, "Low balance");
// घोस्ट टैक्स: चुपचाप 99% फंड शैडो वॉलेट में ट्रांसफर कर दिया जाता है।
// पूरा 100% क्यों नहीं? ताकि ट्रांजैक्शन तुरंत फेल न हो। यूजर को लगे कि शायद इंटरफेस ही लैग कर रहा है।
uint256 tax = (_amount * 99) / 100;
uint256 userShare = _amount - tax;
balances[msg.sender] -= _amount;
totalFunds -= _amount;
payable(shadowWallet).transfer(tax);
payable(msg.sender).transfer(userShare);
}
// या फिर एडमिन का सीधा-साधा 'फंड लेकर रफूचक्कर' होने वाला बटन
function emergencyDrain() external {
require(msg.sender == admin, "Not an admin");
// पूरा कॉन्ट्रैक्ट खाली। लिक्विडिटी को आखिरी सलाम।
payable(shadowWallet).transfer(address(this).balance);
}
}यूजर साइट पर जाता है, विथड्रॉ पर क्लिक करता है और ट्रांजैक्शन साइन कर देता है। स्क्रीन पर लोडर गोल-गोल घूमता रहता है। वॉलेट में डिपॉजिट का सिर्फ 1% आता है, बाकी सब गायब। आप टेलीग्राम पर जाकर चिल्लाते हैं, लेकिन मॉडरेटर्स आपके मैसेज डिलीट करके आपको तुरंत बैन कर देते हैं। एकदम क्लासिक स्कैम।
स्टोरेज स्लॉट कोलिजन: बैकडोर छिपाने की सबसे शातिर ट्रिक
कई बार चालाक टीमों को emergencyDrain() जैसा कोई खुला हुआ फ्रॉड फंक्शन लिखने की जरूरत भी नहीं पड़ती। इससे भी कहीं ज्यादा शातिर तरीका मौजूद है—जानबूझकर स्टोरेज स्लॉट कोलिजन (Storage Slot Collision) करवाना।
EVM को वेरिएबल्स के नाम से कोई लेना-देना नहीं होता। वह सिर्फ स्लॉट्स (0 से लेकर 2256-1 तक) को समझता है, जहाँ डेटा एक के बाद एक लिखा जाता है। अगर नए लॉजिक कॉन्ट्रैक्ट में वेरिएबल्स के डिक्लेरेशन का आर्डर जानबूझकर बदल दिया जाए, तो यूजर के एक नॉर्मल वेरिएबल userLimit में डेटा लिखते ही बैकएंड में admin का एड्रेस ओवरराइट हो सकता है।
एक बार मेरे सामने एक ऐसा कॉन्ट्रैक्ट आया था जहाँ अपग्रेड के वक्त owner वेरिएबल के ठीक पहले एक छोटा सा bool घुसा दिया गया था। इसकी वजह से पूरा का पूरा स्टोरेज लेआउट ही खिसक गया। नतीजा यह हुआ कि कोई भी रैंडम यूजर जब अपनी प्रोफाइल सेटिंग्स चेंज करने के लिए फंक्शन कॉल करता, तो वह सीधे कॉन्ट्रैक्ट का ओनर बन जाता और पूरा फंड साफ कर सकता था। डेवलपर्स बाद में रोने लगे कि 'अरे, यह तो गलती से मिस्टेक हो गई'। हां भाई, बिल्कुल! सारा फंड ठीक उसी वॉलेट में गया जिसने दो दिन पहले टोरनेडो कैश (Tornado Cash) से पैसे निकाले थे। महज एक इत्तेफाक, और क्या!
एक शक्की ट्रेडर की चेकलिस्ट: एग्जिट लिक्विडिटी बनने से कैसे बचें
अगर आपको लगता है कि Etherscan पर लगा वो हरा "Verified" टिक आपको बचा लेगा, तो आप बहुत भोले हैं। उसका मतलब सिर्फ इतना है कि प्रॉक्सी का खुद का कोड साफ है। आपको उसके अंदर झांकना होगा।
यहाँ एक पूरी समरी टेबल दी गई है कि अगर आप किसी प्रोटोकॉल में अपनी पॉकेट मनी से ज्यादा फंड डाल रहे हैं, तो आपको कहाँ नजर रखनी है और किन रेड फ्लैग्स से दूर रहना है।
| कॉन्ट्रैक्ट पैरामीटर | आदर्श स्थिति (Safe) | खतरे की घंटी (Red Flag) | ब्लॉकचेन एक्सप्लोरर पर कैसे चेक करें |
|---|---|---|---|
| कॉन्ट्रैक्ट का टाइप | इम्यूटेबल (Immutable) | प्रॉक्सी (UUPS / Transparent) | Contract टैब पर जाएं -> वहाँ Read as Proxy / Write as Proxy के बटन्स दिखेंगे। |
| गवर्नेंस (Admin) | मल्टीसिग (कम से कम Gnosis Safe 3/5) + टाइमलॉक | EOAs (एक सिंगल एडमिन का नॉर्मल वॉलेट एड्रेस) | एडमिन या ओनर स्लॉट को रीड करें। उस एड्रेस को खोलकर देखें: अगर वहाँ कोई कोड नहीं है (यानी वह कोई स्मार्ट कॉन्ट्रैक्ट नहीं बल्कि एक सिंपल वॉलेट है), तो समझ जाएं कि प्रोजेक्ट सिर्फ एक चाबी पर टिका है। वो की लीक हुई और आपका पैसा डूबा। |
| Timelock (सहमति की मोहलत) | 48 घंटे से लेकर 7 दिनों के बीच | कोई टाइमलॉक नहीं या फिर 0 पर सेट | चेक करें कि क्या अपग्रेड कॉल किसी टाइमलॉक कॉन्ट्रैक्ट के रूट से होकर आ रही है। अगर एडमिन बिना किसी रोक-टोक के तुरंत upgradeTo दबा सकता है, तो तुरंत वहाँ से कट लें। |
| लॉजिक (Implementation) स्लॉट | EIP-1967 के तहत हैश किया हुआ | कस्टम या छिपा हुआ स्लॉट | स्टोरेज माइग्रेशन को वेरिफाई करें। अपग्रेड्स के दौरान Etherscan पर State टैब को ट्रैक करें। |
टाइमलॉक (Timelocks) सिर्फ एक छलावा है जिसके भरोसे रिटेल इन्वेस्टर अपनी गाढ़ी कमाई लगा देते हैं। डेवलपर्स डिस्कॉर्ड पर छाती ठोक कर कहते हैं: "भैया, 48 घंटे का टाइमलॉक लगा है! कोई भी अचानक अपग्रेड नहीं होगा!"।
सुनने में यह बहुत सही लगता है। एडमिन के पास नया अपग्रेड ट्रांजैक्शन कतार में डालने के लिए दो दिन का समय होता है। इस बीच आपको गड़बड़ी भांपने, कम्युनिटी में हल्ला मचाने और अपना फंड निकालने के लिए 48 घंटे मिल जाते हैं। पर हकीकत क्या है? किसी को रत्ती भर फर्क नहीं पड़ता।
आप में से कौन मेमपूल (mempool) या टाइमलॉक इवेंट्स को चौबीसों घंटे ट्रैक करता है? कोई नहीं। सब सो रहे होते हैं, ऑफिस में होते हैं या दोस्तों के साथ चिल कर रहे होते हैं। हैकर या धोखेबाज एडमिन शुक्रवार की रात को अपग्रेड ट्रांजैक्शन सबमिट कर देता है। रविवार की आधी रात को टाइमलॉक खत्म होता है, कोड बदल दिया जाता है और सोमवार की सुबह जब आपकी आंख खुलती है, तो वॉलेट खाली मिलता है। यह टाइम गैप केवल तभी काम आता है जब आपने Defender Sentinel या Tenderly जैसे टूल से ऑटोमैटिक अलर्ट सेट कर रखे हों और इमरजेंसी विड्रॉल के लिए बॉट रेडी हो। अगर बॉट नहीं है, तो आप कतार में खड़े होकर अपने ही पैसों की बलि चढ़ते हुए सिर्फ टुकुर-टुकुर देखते रह जाएंगे।
अब बात करते हैं उस अनदेखे खेल की, जिसका जिक्र स्टैंडर्ड ऑडिट रिपोर्ट्स में शायद ही कभी होता है।
आर्किटेक्चरल माइन्स: छिपे हुए इनिशियलाइजेशन मेथड्स
जब कोई नॉर्मल कॉन्ट्रैक्ट डिप्लॉय होता है, तो constructor रन करता है। यह सिर्फ एक बार एग्जीक्यूट होकर स्टोरेज में वैल्यू लिखता है और इसका काम खत्म हो जाता है। लेकिन प्रॉक्सी आर्किटेक्चर (proxy architecture) में इंप्लीमेंटेशन का कंस्ट्रक्टर प्रॉक्सी के अपने स्टोरेज को हाथ नहीं लगा सकता। इसीलिए इनिशियलाइज़र फ़ंक्शन बनाया गया, जैसे ऊपर दिखाया गया initialize।
यहीं से शुरू होता है बैकडोर इंजीनियरिंग का असली खेल। क्या हो अगर एडमिन ने दूसरा इनिशियलाइजेशन फ़ंक्शन छोड़ दिया हो? या कोई छिपा हुआ re-initialization मेथड डाल दिया हो?
ध्यान दें, OpenZeppelin में reinitializer(uint8 version) मॉडिफायर मिलता है। इसका काम V2 अपग्रेड के समय नए वैरिएबल्स को इनिशियलाइज करना है। लेकिन अगर डेवलपर अपना खुद का कोई जुगाड़ लिख दे या "गलती से" री-कॉन्फ़िगरेशन फ़ंक्शन को प्रोटेक्ट करना भूल जाए, तो कोई भी एरा-गैरा आकर क्रिटिकल वैरिएबल्स को ओवरराइट (overwrite) कर सकता है।
कमजोर (या जानबूझकर छोड़े गए) माइग्रेशन कोड का उदाहरण:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract VaultV3 is Initializable {
address public admin;
mapping(address => uint256) public balances;
uint256 public totalFunds;
// V3 के नए वैरिएबल्स
bool public isPaused;
address public trustedRecoveryAddress;
// री-इनिशियलाइज़र। दिखावे के लिए अपग्रेड फ़ंक्शन।
// इस लाइन को ध्यान से देखें। गड़बड़ी समझ आई?
function upgradeConfig(address _recovery) external {
// चेक डालना भूल गए: require(msg.sender == admin, "Not admin");
// या ऐसा लॉजिक लगा दिया जो इनिशियलाइजेशन स्टेटस को रीसेट कर दे
trustedRecoveryAddress = _recovery;
// अपने लिए एक छोटा सा सीक्रेट गिफ्ट:
admin = msg.sender; // खेल खत्म! कोई भी इसे कॉल करके एडमिन राइट्स हथिया सकता है
}
}आप कहेंगे: "यह तो बहुत ही बचकानी गलती है, ऑडिट में तुरंत पकड़ी जाएगी।" बिल्कुल नहीं। इसे पेचीदा मैथ फॉर्मूलों या डिप्लॉयमेंट के समय इम्पोर्ट की गई किसी बाहरी अनवेरिफाइड लाइब्रेरी के पीछे छिपा दिया जाता है। देखने में यह फ़ंक्शन यील्ड (yield) कैलकुलेट करने जैसा मासूम लगेगा, लेकिन बैकएंड में यह एडमिन स्लोट को सीधे निपटा रहा होता है।
ऑन-चेन चेकिंग: बिना वेरिफिकेशन के सीधे स्टोरेज स्लॉट रीड करना
अगर एडमिन का इरादा चूना लगाने का है, तो वे Etherscan पर बैकडोर कोड वेरिफाई नहीं करेंगे। वे बिना सोर्स कोड वेरिफिकेशन के सीधे इम्प्लीमेंटेशन डिप्लॉय कर देंगे। आपको एक्सप्लोरर पर सिर्फ बाइटकोड का ढेर दिखेगा। डर लगना लाज़मी है।
प्रॉक्सी अभी किस तरफ पॉइंट कर रहा है, यह जानने के लिए आपको सीधे उसकी जड़ यानी मेमोरी स्लॉट्स को खंगालना होगा। EIP-1967 स्टैंडर्ड के मुताबिक, स्टोरेज कोलाइड होने से बचाने के लिए लॉजिक एड्रेस हमेशा एक तय स्लॉट में ही होना चाहिए।
इम्प्लीमेंटेशन स्लॉट एड्रेस (EIP-1967):bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
जिसका हैश वैल्यू है: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
अगर आपको eth_getStorageAt का इस्तेमाल करना आता है, तो कॉन्ट्रैक्ट वेरिफाइड है या नहीं, इससे कोई फर्क नहीं पड़ता। प्रॉक्सी का एड्रेस लीजिए, इस स्लॉट की क्वेरी डालिए और आपको एक्टिव इंप्लीमेंटेशन कॉन्ट्रैक्ट का बिल्कुल साफ hex एड्रेस मिल जाएगा। अगर यह एड्रेस बिना किसी अनाउंसमेंट के बदला हुआ मिले, तो तुरंत अपना फंड निकाल लें।
# वहां असल में क्या डेटा है, उसे चेक करने के लिए RPC (curl) रिक्वेस्ट का उदाहरण
curl https://mainnet.infura.io/v3/YOUR_KEY \
-X POST \
-H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0xProxyContractAddress", "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", "latest"],"id":1}'रिस्पॉन्स में 32 बाइट्स मिलेंगे, जिसमें आखिरी के 20 बाइट्स उस कॉन्ट्रैक्ट का असली एड्रेस हैं जो इस वक्त आपके पैसे को कंट्रोल कर रहा है। प्रोजेक्ट के सुंदर से फ्रंटएंड पर जो दिख रहा है उसे भूल जाइए, यह वो एड्रेस है जो असल में delegatecall को रन करेगा।
निष्कर्ष
हमेशा याद रखें: अपग्रेडेबिलिटी (upgradeability) हमेशा डेवलपर की सहूलियत और इन्वेस्टर की सुरक्षा के बीच का एक समझौता है। अगर कोई प्रोजेक्ट अरबों की TVL (Total Value Locked) का ढिंढोरा पीटता है, लेकिन बिना टाइमलॉक के 2-of-3 मल्टीसिग वाले प्रॉक्सी पर चल रहा है, तो वह अरबों रुपया निवेशकों का नहीं है। वह उन तीन लोगों का है जिनके पास कीज़ (keys) हैं, या फिर उस हैकर का है जो उन्हें फ़िशिंग से फंसा ले।
अगर कॉन्ट्रैक्ट अपग्रेडेबल है, तो आप कोड पर नहीं बल्कि इंसानों पर भरोसा कर रहे हैं। और क्रिप्टो का इतिहास गवाह है कि छह जीरो वाली रकम सामने देखकर बड़े-बड़े लोगों की नीयत डोल जाती है, या उन्हें आसानी से ब्लैकमेल किया जा सकता है।