राम-राम भाई! अगर तुम इस पोस्ट को पढ़ रहे हो, तो या तो तुम्हें स्मार्ट कॉन्ट्रैक्ट सिक्योरिटी में भयंकर इंटरेस्ट है, या फिर तुम अभी किसी नए DeFi प्रोटोकॉल का आर्किटेक्चर डिज़ाइन कर रहे हो और तुम्हारी यह सोचकर थोड़ी फटी पड़ी है कि कहीं कल को किसी छोटी सी गड़बड़ की वजह से पूरा का पूरा फंड स्वाहा न हो जाए।
देखो भाई, मैं इस फील्ड में काफी टाइम से हूं। हैकाथॉन में पिज्जा और एनर्जी ड्रिंक के दम पर जुगाड़ू एक्सप्लॉइट्स बनाने से लेकर एक क्रिप्टो एक्सचेंज के CTO की कुर्सी तक का सफर तय किया है मैंने। और सच बताऊं? जितने भी बड़े हैक्स मैंने देखे हैं, जिनकी जांच की है (और कुछ को तो ऑडिट के टाइम पर ही होने से रोका है), उनमें से ज्यादातर इसलिए नहीं हुए कि क्रिप्टोग्राफी कमजोर थी या Solidity का कंपाइलर पागल हो गया था। वो इसलिए हुए क्योंकि लोगों को बुनियादी समझ ही नहीं थी कि अलग-अलग ERC स्टैंडर्ड्स जब आपस में इंटरैक्ट करते हैं, तो उनका बिहेवियर कैसा होता है।
हमें लगता है कि स्टैंडर्ड्स का मतलब है पक्की सुरक्षा और नियम। लेकिन असली खेल (या कहें असली गड़बड़) इम्प्लीमेंटेशन की बारीकियों और छुपे हुए साइड-इफेक्ट्स में छिपा होता है। चलो सीधे मुद्दे पर आते हैं और देखते हैं कि आर्किटेक्ट्स अक्सर कहां मात खा जाते हैं, और सिस्टम को ऐसे कैसे डिज़ाइन करें कि रात को चैन की नींद सो सको।
1. ERC-20 का छुपा हुआ खतरा: एक खतरनाक क्लासिक गेम
कहने को तो लगता है कि ERC-20 को सबने रट रखा है। भला इसमें क्या ही गड़बड़ होगी? भाई, सब कुछ गड़बड़ हो सकता है अगर तुम बिना पूरी जांच-पड़ताल किए किसी दूसरे के टोकन को अपने प्रोटोकॉल में सीधे इंटीग्रेट कर लेते हो।
रिटर्न वैल्यू न मिलने का लोचा (The No-Return Dilemma)
नियम (स्पेसिफिकेशन) के हिसाब से transfer और transferFrom को एक bool वैल्यू रिटर्न करनी चाहिए। लेकिन हकीकत में, कई पुराने और बड़े टोकन्स (जैसे कुछ पुराने कॉन्ट्रैक्ट्स पर USDT और BNB) ऐसा नहीं करते। ट्रांजैक्शन सक्सेसफुल होने पर भी वे कुछ रिटर्न नहीं करते, बिल्कुल सन्नाटा।
अब अगर तुम्हारा कॉन्ट्रैक्ट स्टैंडर्ड इंटरफेस के भरोसे bool की उम्मीद कर रहा है, कुछ इस तरह:
// अगर अनजान टोकन्स के साथ काम कर रहे हो, तो ऐसा करने की भूल कभी मत करना!
IERC20(token).transferFrom(msg.sender, address(this), amount);तो जब भी USDT के साथ इंटरैक्शन होगा, ट्रांजैक्शन सीधे फेल (revert) हो जाएगा। ऐसा इसलिए क्योंकि EVM स्टैक पर रिटर्न वैल्यू ढूंढेगा और वहां कुछ मिलेगा ही नहीं। या इससे भी बुरा तब होगा जब तुम रिजल्ट चेक ही नहीं करते (यानी require(token.transfer(...)) के बजाय सिर्फ token.transfer(...) लिख देते हो)। ऐसे में कुछ टोकन्स गड़बड़ होने पर रीवर्ट करने के बजाय चुपचाप false रिटर्न कर देते हैं, और तुम्हारा कॉन्ट्रैक्ट सोचेगा सब चंगा सी और आगे बढ़ जाएगा। नतीजा? यूजर ने बिना एक भी धेला दिए हवा में अपना बैलेंस बढ़ा लिया।
समाधान: सीधा transfer और transferFrom कॉल करना हमेशा के लिए भूल जाओ। OpenZeppelin की SafeERC20 लाइब्रेरी और उसके मेथड्स safeTransfer / safeTransferFrom का ही इस्तेमाल करो। ये बैकएंड पर लो-लेवल रिटर्न को खुद चेक कर लेती है और टेढ़े-मेढ़े कॉन्ट्रैक्ट्स को भी सही से संभाल लेती है।
Weird ERC-20 Tokens: जब स्टैंडर्ड रायता फैला दे
यहाँ उन टोकन्स की एक छोटी सी चीट-शीट दी गई है जो किताबों में लिखी बातों से बिल्कुल अलग बर्हेव करते हैं। एक आर्किटेक्ट होने के नाते तुम्हें लिक्विडिटी पूल बनाते समय इन बातों का ध्यान रखना ही पड़ेगा।
| टोकन का प्रकार (Weird ERC-20) | इसमें क्या सीन है? | आर्किटेक्चर के लिए क्यों खतरनाक है? |
|---|---|---|
| Deflationary / Fee-on-Transfer (जैसे STA, PAXG) | ये टोकन ट्रांसफर के दौरान ही सीधे टैक्स या फीस काट लेते हैं। | तुम्हें लगा कि कॉन्ट्रैक्ट को 100 टोकन मिले, पर असल में बैलेंस में सिर्फ 99 ही आए। इससे तुम्हारा इंटरनल लिक्विडिटी का हिसाब-किताब बिगड़ जाएगा और फंड कम पड़ जाएगा। |
| Upgradable Proxies (जैसे USDC, USDT) | इसके एडमिन्स जब चाहें टोकन का पूरा लॉजिक बदल सकते हैं। | ब्लैकलिस्ट का खतरा। अगर उन्होंने तुम्हारे कॉन्ट्रैक्ट का एड्रेस ब्लॉक कर दिया, तो तुम्हारी सारी लिक्विडिटी अंदर ही जाम हो जाएगी, हमेशा के लिए टाटा-बायबाय। |
| Rebasing Tokens (जैसे AMPL) | वॉलेट्स में इनका बैलेंस अपने आप बदलता रहता है (कीमत स्टेबल रखने के लिए सप्लाई कम-ज्यादा होती है)। | बिना किसी ट्रांसफर फंक्शन को कॉल किए ही तुम्हारे कॉन्ट्रैक्ट का बैलेंस अपने आप घट या बढ़ सकता है। |
2. ERC-721 और ERC-1155: onERC721Received और Reentrancy का जाल
ओहो, यह तो मेरा सबसे पसंदीदा टॉपिक है। न जाने कितने NFT मार्केटप्लेस और लैंडिंग प्रोटोकॉल्स इस 'सेफ ट्रांसफर' के चक्कर में पूरी तरह लुट चुके हैं!
जब तुम ERC-721 या ERC-1155 में safeTransferFrom कॉल करते हो, तो टोकन कॉन्ट्रैक्ट चेक करता है कि रिसीवर कोई स्मार्ट कॉन्ट्रैक्ट है या नहीं। अगर है, तो वो रिसीवर कॉन्ट्रैक्ट के अंदर एक हुक (hook) कॉल करता है—onERC721Received या onERC1155Received।
ऐसा क्यों? ताकि तसल्ली हो सके कि सामने वाला कॉन्ट्रैक्ट NFT संभाल सकता है और वो वहां जाकर फंसी नहीं रहेगी।
लेकिन असली झोल कहाँ है? यह हुक तुम्हारी ट्रांजैक्शन के ठीक बीच में एक अनजान, बाहरी कोड को कंट्रोल सौंप देता है, और वह भी तब जब तुमने अभी तक अपने कॉन्ट्रैक्ट का इंटरनल स्टेट अपडेट ही नहीं किया है!
खतरे वाले मिंटिंग / मार्केटप्लेस का कोड
जरा इस कोड के टुकड़े को देखो। मैंने इसे जानबूझकर ऐसा लिखा है ताकि तुम्हें एक क्लासिक आर्किटेक्चरल गड़बड़ दिखाई दे—यानी Checks-Effects-Interactions पैटर्न की धज्जियां उड़ना।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract VulnerableNFCLending {
// कोलैटरल की जानकारी: यूजर => टोकन ID => क्या कोलैटरल एक्टिव है
mapping(address => mapping(uint256 => bool)) public hasCollateral;
IERC721 public nftToken;
constructor(address _nft) {
nftToken = IERC721(_nft);
}
// यूजर NFT गिरवी रखकर लोन लेना चाहता है
function depositCollateral(uint256 tokenId) external {
// 1. Interactions: NFT को कॉन्ट्रैक्ट पर ट्रांसफर करना
// क्या safeTransferFrom रिसीवर कॉन्ट्रैक्ट पर onERC721Received हुक ट्रिगर करेगा?
// अरे रुको, यहाँ रिसीवर तो हम खुद हैं। लेकिन अगर कॉन्ट्रैक्ट safeMint कॉल करे तो...
// चलो सीन को थोड़ा बदलते हैं जब हम NFT वापस कर रहे हों या कोई हैकर बीच में कंट्रोल हथिया ले।
// नया परिदृश्य: कॉन्ट्रैक्ट NFT वापस दे रहा है (जैसे कोलैटरल विथड्रॉ करते समय)
// या फिर यह एक मिंटिंग कॉन्ट्रैक्ट है जो पहले ट्रांसफर करता है और स्टेट बाद में अपडेट करता है।
}
}चलो, इसे समझने के लिए मैं तुम्हें एक सीधा-साधा कमजोर मिंटिंग कॉन्ट्रैक्ट दिखाता हूँ, वहां पूरा सीन एकदम क्लियर हो जाएगा। मान लो हमारा एक कॉन्ट्रैक्ट है जो हर बंदे को सिर्फ 1 NFT मुफ्त में मिंट करने की इजाजत देता है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract VulnerableMint is ERC721 {
mapping(address => bool) public hasMinted;
uint256 public currentTokenId;
constructor() ERC721("DangerNFT", "DNFT") {}
function freeMint() external {
// Checks
require(!hasMinted[msg.sender], "Brother, you already have one!");
// Interactions (_safeMint के अंदर एक बाहरी कॉन्ट्रैक्ट को कॉल करने का कोड छुपा है!)
_safeMint(msg.sender, currentTokenId);
currentTokenId++;
// Effects (स्टेट अपडेट करने में बहुत ज्यादा देर कर दी भाई)
hasMinted[msg.sender] = true;
}
}और अब देखो अटैकर का कॉन्ट्रैक्ट। वो इस हुक को बीच में ही पकड़ लेता है, देखता है कि मेंन कॉन्ट्रैक्ट पर hasMinted अभी भी false ही है, और बस फिर क्या, वो बार-बार freeMint को तब तक कॉल करता रहता है जब तक कि पूरी लिमिट खत्म न हो जाए या उसका गैस न उड़ जाए।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IVulnerableMint {
function freeMint() external;
}
contract Attacker {
IVulnerableMint public target;
uint256 count;
constructor(address _target) {
target = IVulnerableMint(_target);
}
function attack() external {
target.freeMint();
}
// वही मनहूस हुक जिसे ERC-721 स्टैंडर्ड कॉल करता है
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external returns (bytes4) {
if (count < 5) {
count++;
// Reentrancy हमला! शिकार का इंटरनल स्टेट अभी तक अपडेट नहीं हुआ है
target.freeMint();
}
return this.onERC721Received.selector;
}
}भाईसाहब, मैंने ऑडिट के दौरान कितनी ही बार यह सेम चीज देखी है। डेवलपर्स सोचते हैं: "अरे, सिर्फ NFT ट्रांसफर ही तो है, कोई नेटिव ETH थोड़े ही भेज रहे हैं, क्या ही गलत हो जाएगा।" और यहीं पर सारा खेल खत्म, तुम पूरी तरह कंगाल हो जाते हो।
आर्किटेक्ट का थम्ब रूल: सबसे पहले स्टेट अपडेट करो (hasMinted[msg.sender] = true;), और उसके बाद ही मिंटिंग या ट्रांसफर का कोई भी मेथड कॉल करो। और हाँ, कान खोलकर सुन लो, NFT ट्रांसफर के साथ डील करने वाले फंक्शन्स पर OpenZeppelin का nonReentrant मॉडिफायर लगाना कभी मत भूलना।
चलो आगे बढ़ते हैं। हुक्स (hooks) के थ्रू होने वाली Reentrancy को तो हमने अच्छे से डिकोड कर लिया है, अब एक नए और ज्यादा शातिर (sophisticated) अटैक वेक्टर को खंगालते हैं। इस प्रॉब्लम पर आर्किटेक्चर डिज़ाइन के टाइम पर बहुत कम लोग ध्यान देते हैं—जब तक कि सीधे तौर पर लग न जाएं।
3. ERC-2612 (Permit): फैंटम अप्रूवल्स (Phantom Approvals) और फ्रंट-रनिंग अटैक्स
ERC-2612 स्टैंडर्ड ने permit फंक्शन लाकर Web3 की दुनिया में एक बहुत बड़ी राहत दी थी। इसके जरिए यूजर्स टोकन स्पेंड करने की परमिशन (allowance) देने के लिए ऑफलाइन ही एक मैसेज साइन (EIP-712) कर सकते हैं, और गैस फीस का लोड किसी रिलेयर या खुद प्रोटोकॉल पर डाल सकते हैं। इससे UX का लेवल एकदम कतई जहर हो गया: जहां पहले दो अलग-अलग ट्रांजैक्शंस करने पड़ते थे (approve + transferFrom), वहां अब यूजर का काम सिंगल ट्रांजैक्शन में हो जाता है।
लेकिन स्मार्ट कॉन्ट्रैक्ट आर्किटेक्ट्स अक्सर यह भूल जाते हैं कि यह सिग्नेचर बैकएंड में असल में काम कैसे करता है, और यहीं पर वे लॉजिक में ब्लंडर कर बैठते हैं।
सिग्नेचर फ्रंट-रनिंग (Signature Front-running)
मान लो permit का यूज करने वाला एक क्लासिक डिपॉजिट फंक्शन कुछ ऐसा दिखता है:
function depositWithPermit(
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
// सबसे पहले, यूजर के दिए गए सिग्नेचर का यूज करके permit को एक्जीक्यूट करते हैं
IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s);
// इसके बाद, टोकन्स को पुल करते हैं
IERC20(token).transferFrom(msg.sender, address(this), amount);
// इंटरनल पॉइंट्स या पूल शेयर्स मिंट करते हैं
_mintShares(msg.sender, amount);
}अब इसमें लूपहोल कहां है? कोई भी MEV बॉट जो पब्लिक मेमपूल (mempool) को स्कैन कर रहा है, उसे यह पेंडिंग ट्रांजैक्शन साफ दिख जाएगा। वो बॉट आपके ट्रांजैक्शन से वैलिड सिग्नेचर (v, r, s) और बाकी पैरामीटर्स को आसानी से निकाल सकता है, सीधे टोकन कॉन्ट्रैक्ट पर token.permit(...) कॉल करने के लिए अपना खुद का एक ट्रांजैक्शन तैयार कर सकता है, और गैस प्राइस (Gas Price) को यूजर से ज्यादा रखकर उसे फ्रंट-रन (front-run) कर सकता है।
उस बॉट का ट्रांजैक्शन पहले प्रोसेस हो जाएगा। सिग्नेचर सक्सेसफुली यूज हो जाएगा और अलाउंस सेट हो जाएगा। इसके तुरंत बाद, उस ईमानदार यूजर का ट्रांजैक्शन चेन पर हिट करेगा। लेकिन क्योंकि सिग्नेचर पहले ही इस्तेमाल हो चुका है, इसलिए टोकन कॉन्ट्रैक्ट में यूजर का नॉन्स (nonce) बढ़ चुका होगा! ऐसे में depositWithPermit के अंदर वाली permit कॉल तुरंत revert हो जाएगी क्योंकि सिग्नेचर अब इनवैलिड हो चुका है।
नतीजा क्या होगा? यूजर का ट्रांजैक्शन फेल हो जाएगा, उसकी गैस फीस फालतू में स्वाहा हो जाएगी, उसका UX खराब होगा, और अगर यह किसी लीवरेज्ड लॉन्ग पोजीशन को लिक्विडेशन से बचाने के लिए एक क्रिटिकल मार्जिन टॉप-अप था, तो इस डिले की वजह से उसकी पोजीशन का सीधे रीप (liquidate) हो जाएगा।
अपने आर्किटेक्चर को कैसे सेक्योर करें?
इसके लिए permit कॉल को एक try/catch ब्लॉक में रैप (wrap) कर दो। अगर सिग्नेचर को किसी फ्रंट-रनर ने पहले ही नेटवर्क पर ब्रॉडकास्ट करके यूज कर लिया है, तो टोकन कॉन्ट्रैक्ट में ऑलरेडी रिक्वायर्ड allowance सेट हो चुका है। आपके कॉन्ट्रैक्ट को बस उस डुप्लिकेट सिग्नेचर वाले एरर को इग्नोर करना है और सीधे आगे बढ़कर transferFrom को एक्जीक्यूट कर देना है।
try IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s) {}
catch {
// अगर ये रिवर्ट हुआ है—तो चांस है कि सिग्नेक्शन ऑलरेडी फ्रंट-रन हो चुका है।
// चेक करो कि क्या करेंट अलाउंस ऑपरेशन पूरा करने के लिए काफी है या नहीं।
require(IERC20(token).allowance(msg.sender, address(this)) >= amount, "Permit failed and allowance insufficient");
}"फैंटम परमिट" (Phantom Permit) की दिक्कत
अब बात करते हैं क्रिप्टो डेवलपर्स के उस असली इनसाइडर दर्द की, जिसके बारे में कहीं ज्यादा लिखा नहीं मिलता। क्या होगा अगर आपका DeFi प्रोटोकॉल आर्बिट्रेरी (arbitrary) टोकन्स को सपोर्ट करता है, और कोई यूजर किसी ऐसे टोकन के साथ depositWithPermit कॉल करने की कोशिश करता है जो असल में ERC-2612 को सपोर्ट ही नहीं करता?
आप सोच रहे होंगे: "सिंपल है, ट्रांजैक्शन फेल हो जाएगा क्योंकि वहां परमिट फंक्शन है ही नहीं।" पर भाई, हर बार ऐसा नहीं होता!
अगर उस टोकन कॉन्ट्रैक्ट में कोई जेनेरिक fallback() या receive() फंक्शन लगा है जो किसी अननोन फंक्शन सिलेक्टर के आने पर ट्रांजैक्शन को रिवर्ट नहीं करता (जैसा कि कुछ प्रॉक्सी आर्किटेक्चर या पुराने WETH जैसे लेगेसी टोकन्स में देखा जाता है), तो वह permit कॉल सक्सेसफुल दिखेगी (यानी success = true रिटर्न करेगी), जबकि हकीकत में कोई अलाउंस अप्रूव हुआ ही नहीं होगा।
इसके बाद आपका कॉन्ट्रैक्ट आगे बढ़कर transferFrom चलाने की कोशिश करेगा, जो कि फेल हो जाएगा बशर्ते वहां कोई पुराना अलाउंस न बचा हो। लेकिन अगर यह कमजोरी किसी ऐसे लॉजिक के साथ टकरा जाए जहां अलाउंस को किसी दूसरे फ्लो से वैलिडेट किया जा रहा हो, तो प्रोटोकॉल के बुरे वाले लग सकते हैं (reked)। हमेशा यह वेरिफाई करो कि टारगेट टोकन ERC-165 के जरिए IERC20Permit को सच में सपोर्ट करता है या नहीं, या फिर सीधे टोकन की व्हाइटलिस्ट (whitelist) मेंटेन करो।
4. ERC-3156 (Flash Loans): एक ही ट्रांजैक्शन के अंदर बैलेंस मैनिपुलेशन का खतरा
फ्लैश लोन (Flash loans) एक बेहद पावरफुल टूल हैं, लेकिन ये उन टाइमिंग अजम्पशन्स की धज्जियां उड़ा देते हैं जिन्हें सोचकर ट्रेडिशनल सॉफ्टवेयर आर्किटेक्ट्स कोडिंग करते हैं। एक सिंगल एटॉमिक ट्रांजैक्शन (atomic transaction) के अंदर, कोई भी अटैकर लाखों डॉलर उधार ले सकता है, स्टेट्स को मैनिपुलेट कर सकता है और पूरी रकम वापस भी कर सकता है।
यहां सबसे खतरनाक आर्किटेक्चरल गलती यह होती है कि डेवलपर्स पूल शेयर की प्राइसिंग या एसेट की वैल्यूएशन निकालने के लिए balanceOf(address(this)) फंक्शन पर भरोसा कर लेते हैं।
// आर्किटेक्चर में एक भयंकर ब्लंडर
function getSharePrice() public view returns (uint256) {
// शेयर की कीमत सीधे कॉन्ट्रैक्ट के करेंट टोकन बैलेंस पर डिपेंड करती है
return token.balanceOf(address(this)) / totalShares;
}अगर आपका कॉन्ट्रैक्ट उन्हीं सेम टोकन्स का फ्लैश लोन लेना अलाउ करता है, तो जैसे ही बॉरोअर फंड्स को पुल करेगा, कॉन्ट्रैक्ट का बैलेंस गिरकर लगभग जीरो हो जाएगा। अगर ठीक उसी मोमेंट पर (यानी onFlashLoan के कॉलबैक के अंदर) आपका प्रोटोकॉल दूसरे ऑपरेशन्स को ट्रिगर करना अलाउ कर देता है—जैसे कि लिक्विडेशन या रिवॉर्ड डिस्ट्रीब्यूशन—तो कैलकुलेट की गई शेयर प्राइस पूरी तरह से डिस्टॉर्ट (distort) हो जाएगी।
अटैकर फ्लैश लोन लेता है -> पूल का बैलेंस एकदम से खाली होता है -> शेयर प्राइस पाताल में गिर जाती है -> अटैकर अपने किसी दूसरे वॉलेट का यूज करके कौड़ियों के दाम में शेयर्स समेट लेता है -> फ्लैश लोन रीपे कर दिया जाता है -> पूल का बैलेंस वापस नॉर्मल हो जाता है -> अटैकर उन शेयर्स को नॉर्मल रेट पर डंप कर देता है। और इसी के साथ, आपका पूरा पूल साफ।
गोल्डन रूल: क्रिटिकल इकोनॉमिक या मैथमैटिकल कैलकुलेशंस के लिए कभी भी balanceOf(address(this)) पर आंख बंद करके भरोसा मत करो, अगर उस बैलेंस को सिस्टम की लॉजिकल स्टेट बदले बिना टेम्परेरी तौर पर बदला जा सकता है। इसकी जगह हमेशा इंटरनल अकाउंटिंग (internal accounting) का यूज करो, जैसे कि uint256 internalReserve स्टेट वेरिएबल, जिसे सिर्फ पूरी तरह कंट्रोल्ड डिपॉजिट्स और विड्रॉल्स के टाइम पर ही अपडेट किया जाए।
चलो, अब उन चीज़ों पर आते हैं जो एक्सचेंज इंफ्रास्ट्रक्चर की सिक्योरिटी संभालने वाले बंदे के तौर पर सच में मेरे होश उड़ा देती हैं। बात करते हैं उन नए और एकदम ताज़ा स्टैंडर्ड्स की, जहाँ अभी सैकड़ों डेवलपर्स के पैर पड़ने बाकी हैं और गलतियों के गड्ढे अभी पूरी तरह भरे नहीं हैं।
5. ERC-4337 (Account Abstraction): बैच ट्रांजैक्शन्स और Paymaster के लेवल पर छिपे खेल
अकाउंट एब्स्ट्रैक्शन (Account Abstraction) एकदम कड़क चीज़ है, इसमें कोई शक नहीं है। हम EOAs (नॉर्मल वॉलेट्स) को छोड़कर सीधे स्मार्ट कॉन्ट्रैक्ट्स को यूजर वॉलेट की तरह यूज़ कर रहे हैं। सीड फ़्रेज़ (seed phrases) का कोई झंझट नहीं, सोशल रिकवरी मिल जाती है और बंदा Paymasters के ज़रिए स्टेबलकॉइन्स में भी गैस फीस भर सकता है।
लेकिन एक प्रोटोकॉल आर्किटेक्ट के नज़रिए से, जो ERC-4337 के साथ इंटीग्रेशन कर रहा है, यहाँ अजीबोगरीब अटैक वेक्टर्स (attack vectors) का एक पूरा कुआँ खुल जाता है।
validateUserOp की सिग्नेचर वाली कमज़ोरी
ERC-4337 में वॉलेट की कस्टम वैलिडेशन का सबसे मेन पॉइंट validateUserOp मेथड है। इसका काम ट्रांजैक्शन सिग्नेचर को चेक करना और एक खास स्टेटस कोड रिटर्न करना है।
// स्मार्ट वॉलेट में वैलिडेशन लॉजिक का एक सिंपल एक्ज़ाम्पल
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData) {
// क्रिटिकल मिस्टेक: क्या हम किसी भी (ANY) एड्रेस से आने वाली कॉल पर भरोसा कर रहे हैं?
// नहीं, बुंडलर (Bundler) इसे EntryPoint के ज़रिए कॉल करता है। पर अगर हम ये चेक भूल गए तो...
require(msg.sender == entryPoint, "Only EntryPoint can trigger validation");
// खुद का सिग्नेचर वैलिडेशन
if (_verifySignature(userOp, userOpHash)) {
// वैलिडेशन सक्सेसफुल होने पर 0 रिटर्न करें
return 0;
}
// गड़बड़ होने पर SIG_VALIDATION_FAILED (आमतौर पर 1) रिटर्न करें
return 1;
}रुको, खेल समझ आया? ERC-4337 के स्पेसिफिकेशन के मुताबिक, अगर सिग्नेचर वैलिडेशन फेल होता है, तो फंक्शन को revert नहीं मारना चाहिए। उसे एक खास पैक्ड वैल्यू (एरर कॉन्स्टैंट) रिटर्न करनी होगी ताकि EntryPoint (जो पूरे कॉन्ट्रैक्ट्स का कैप्टन है) समझ जाए कि ट्रांजैक्शन गड़बड़ है। इससे वो वॉलेट से गैस फीस नहीं काटेगा और बुंडलर के लेवल पर ही ऑपरेशन को सीधे ड्रॉप कर देगा।
अगर तुम एक आर्किटेक्ट होकर पुरानी आदत के चक्कर में वहाँ require(isValid, "Invalid signature"); ठोक दोगे, तो तगड़ा चूना लगेगा। जब ट्रांजैक्शन्स को एक साथ बल्क (batch) में भेजा जाएगा, तो सिर्फ एक ट्रांजैक्शन के हार्ड revert होने की वजह से बुंडलर का पूरा का पूरा बैच ही ठप हो जाएगा। नतीजा ये होगा कि बुंडलर्स तुम्हारे वॉलेट या Paymaster को सीधे ब्लैकलिस्ट (ban) कर देंगे, और तुम्हारे यूजर्स कोई ट्रांजैक्शन ही नहीं कर पाएंगे। वैलिडेशन लॉजिक एकदम एटॉमिक होना चाहिए और सॉलिडिटी के पुराने घिसे-पिटे पैटर्न की जगह सीधे ERC-4337 के रिटर्न-वैल्यू मैथ को फॉलो करना चाहिए।
Paymaster पर अटैक (Gas Drain)
अगर तुम्हारा DeFi प्रोटोकॉल खुद एक Paymaster की तरह काम कर रहा है (जैसे तुम अपने यूजर्स के लिए गैस फीस स्पॉन्सर कर रहे हो ताकि वो बिना किसी फीस के ट्रेड कर सकें), तो तुम्हें वैलिडेशन स्टेज को बाहरी दुनिया से पूरी तरह अलग-थलग (isolate) रखना होगा।
Paymaster कॉन्ट्रैक्ट में एक मेथड होता है validatePaymasterUserOp। इस मेथड के अंदर ऐसी डायनेमिक स्टेट (dynamic state) का इस्तेमाल करना **सख्त मना** है, जो बुंडलर के सिमुलेशन टाइम और ब्लॉक में ट्रांजैक्शन शामिल होने के टाइम के बीच बदल सके। एक्ज़ाम्पल के लिए, तुम पेमास्टर वैलिडेशन के अंदर सीधे प्राइस ओरैकल्स (जैसे Chainlink) को कॉल नहीं कर सकते ताकि यह कैलकुलेट किया जा सके कि यूजर से गैस के बदले कितने टोकन काटने हैं।
क्यों? कोई भी अटैकर एक ट्रांजैक्शन भेज सकता है; सिमुलेशन के दौरान ओरैकल एक सही प्राइस दिखाएगा (वैलिडेशन पास हो जाएगा), लेकिन ठीक ब्लॉक में शामिल होने से पहले हैकर फ्लैश लोन (flash loan) या किसी क्विक ट्रेड के ज़रिए ओरैकल की प्राइस को मैनिपुलेट कर देगा। ऑन-चेन वैलिडेशन फेल होने लगेगा, लेकिन बुंडलर पहले ही ट्रांजैक्शन को प्रोसेस करने में अपनी गैस फूंक चुका होगा। गैस के पैसे तुम्हारे पेमास्टर से कट जाएंगे और यूजर की जेब से चवन्नी भी नहीं जाएगी। कुछ ही घंटों में तुम्हारा पूरा गैस बैलेंस साफ हो जाएगा।
6. ERC-4626 (Tokenized Vaults): पहले डिपॉजिट पर फ्रंट-रनिंग (Inflation Attack)
ERC-4626 टोकनाइज्ड वॉल्ट्स (स्टेकिंग, यील्ड पूल्स, लैंडिंग) के लिए एकदम तगड़ा स्टैंडर्ड है। इसने deposit, mint, withdraw और redeem जैसे मेथड्स को स्टैंडर्डाइज कर दिया है। यह मास्टरस्ट्रोक है क्योंकि अब Yearn जैसा कोई भी यील्ड एग्रीगेटर किसी भी नए पूल को सिर्फ 5 मिनट में सीधे इंटीग्रेट कर सकता है।
लेकिन इस स्टैंडर्ड के मैथ के डिज़ाइन में ही एक टाइम बम छिपा है, जिसे **Inflation Attack** (एसेट इन्फ्लेशन अटैक) कहते हैं। यह पूल्स पर ठीक उस वक्त वार करता है जब वे नेटवर्क पर एकदम नए-नए डिप्लॉय होते हैं और उनका बैलेंस बिल्कुल जीरो होता है।
अटैक की क्रोनोलॉजी
यूजर जब कोई एसेट (assets) डिपॉजिट करता है, तो उसे मिलने वाले शेयर्स (shares) की कैलकुलेशन का फार्मूला आमतौर पर ऐसा होता है:
$$\text{shares} = \frac{\text{assets} \times \text{totalShares}}{\text{totalAssets}}$$
अगर पूल एकदम खाली है (totalShares == 0), तो डिफ़ॉल्ट रूप से shares == assets होता है। यानी 1-to-1 का सीधा रेशियो।
अब हैकर का असली हाथ की सफाई देखो:
- एक सीधा-साधा यूजर एकदम नए और खाली ERC-4626 पूल में 1000 USDC का
depositट्रांजैक्शन भेजता है। - हैकर इसे मेमपूल (mempool) में ताड़ लेता है और ज्यादा गैस फीस देकर उसे फ्रंट-रन (front-run) कर देता है। वो पूल में सिर्फ 1 wei USDC डिपॉजिट करता है। पूल उसे ठीक 1 wei शेयर्स मिंट करके दे देता है। अब सीन ये है:
totalShares = 1,totalAssets = 1। - इसके बाद, उसी सेम एटॉमिक ट्रांजैक्शन में, हैकर एक बहुत बड़ी रकम—मान लो 10,000 USDC—सीधे वॉल्ट कॉन्ट्रैक्ट के एड्रेस पर डायरेक्ट ट्रांसफर (नॉर्मल ERC20
transferके ज़रिए, न किdepositफंक्शन से) कर देता है। - अब पूल के मैथ का क्या कबाड़ा हुआ?
totalSharesअभी भी 1 है, लेकिनtotalAssetsअब 10,001 USDC हो चुका है (डायरेक्ट ट्रांसफर की वजह से कॉन्ट्रैक्ट का बैलेंस तो बढ़ गया, लेकिन नए शेयर्स मिंट नहीं हुए)। एक सिंगल शेयर की कीमत सीधे आसमान छूने लगती है। आखिर में, उस सीधे-साधे यूजर का 1000 USDC वाला ट्रांजैक्शन एग्जीक्यूट होता है। कॉन्ट्रैक्ट ऊपर वाले फार्मूले से शेयर्स कैलकुलेट करता है:
$$\text{shares} = \frac{1000 \times 1}{10\,001} = 0$$
सॉलिडिटी में इंटीजर डिवीज़न के राउंडिंग डाउन (नीचे की तरफ राउंड करने) के नेचर की वजह से यूजर को मिलते हैं पूरे 0 शेयर्स! लेकिन उसके 1000 USDC मज़े से पूल के बैलेंस में चले जाते हैं।
- हैकर चुपके से अपने इकलौते शेयर (1 wei shares) को
withdrawकरता है और पूल का सब कुछ उड़ा ले जाता है: अपने 10,000 USDC, अपना 1 wei और उस बेचारे यूजर के चोरी किए हुए 1000 USDC। गेम ओवर।
आर्किटेक्चरल सोल्यूशन: इससे बचने के दो रास्ते हैं। पहला—पूल बनाते टाइम ही ज़ीरो एड्रेस (zero address) पर ज़बरदस्ती "डेड शेयर्स" (dead shares) मिंट कर दो (शुरुआती 1000 wei शेयर्स को वहाँ लॉक कर दो, जैसा Uniswap V2 में किया गया है)। दूसरा—ओपनज़ेपलिन (OpenZeppelin) की लेटेस्ट लाइब्रेरीज़ का इस्तेमाल करो, जहाँ वर्चुअल ऑफसेट्स (virtual assets और virtual shares) के ज़रिए इन-बिल्ट प्रोटेक्शन मिलती है। यह मैनिपुलेशन के दौरान फ्रैक्शन के डिनोमिनेटर (हर) को कभी भी जीरो या वन नहीं होने देता।
देखो भाई, अब थोड़ा और ऊपर के लेवल पर बात करते हैं। हमने स्पेसिफिक टोकन्स, NFT, परमिट्स और वॉल्ट्स की बात कर ली। लेकिन इन सबको एक सॉलिड आर्किटेक्चर में कैसे फिट करें और इंटीग्रेशन के वक्त अपना दिमाग फटने से कैसे बचाएं?
जब तुम कोई बड़ा सिस्टम डिजाइन करते हो, जैसे कि यील्ड एग्रीगेटर या क्रॉस-चेन ब्रिज, तो तुम्हें इन सारे स्टैंडर्ड्स के साथ एक साथ काम करना पड़ता है। और यहीं पर वल्नेरेबिलिटीज़ का 'सिनर्जिस्टिक इफ़ेक्ट' (synergistic effect) आता है—जहाँ दो फीचर्स अलग-अलग तो एकदम सेफ होते हैं, लेकिन साथ मिलकर एक खतरनाक बग पैदा कर देते हैं।
7. सिस्टम डिजाइन में आर्किटेक्चरल रिस्क मैट्रिक्स
तुम्हारी क्लैरिटी के लिए, मैंने ये टेबल ड्राफ्ट की है। इसे अपने अगले आर्किटेक्चर रिव्यू के लिए एक चेकलिस्ट की तरह इस्तेमाल करो। इसे Notion में सेव कर लो या प्रिंट निकाल लो।
| ERC स्टैंडर्ड | मेन हिडन खतरा (Hidden Threat) | लॉजिक में इम्पैक्ट | डिजाइन लेवल पर फिक्स? |
|---|---|---|---|
| ERC-20 | रिटर्न वैल्यू न मिलना / अनकन्वेंशनल ट्रांसफर | ट्रांजैक्शन का गिरना (revert) या एरर को साइलेंटली इग्नोर कर देना | सिर्फ SafeERC20 (OpenZeppelin) का ही इस्तेमाल करो। |
| ERC-20 (Weird) | Fee-on-Transfer / बैलेंस में बदलाव (Rebase) | सिस्टम की इंटरनल अकाउंटिंग और कॉन्ट्रैक्ट के रियल बैलेंस में मिसमैच | amount पर भरोसा करने के बजाय balanceAfter - balanceBefore का डिफरेंस कैलकुलेट करो। |
| ERC-721 / 1155 | onERC...Received हुक्स के जरिए कंट्रोल हाइजैक करना | इंटरनल स्टेट अपडेट होने से पहले री-एंट्रेंसी (Reentrancy) | Checks-Effects-Interactions पैटर्न और nonReentrant मॉडिफायर को सख्ती से फॉलो करो। |
| ERC-2612 | मेमपूल (mempool) में सिग्नेचर फ्रंट-रनिंग | लेजिटिमेट यूजर के लिए सर्विस डिनायल (DoS) | permit कॉल्स को try/catch ब्लॉक में लपेटो। |
| ERC-3156 | पूरे पूल को खाली करना (Flash Loan) | balanceOf पर डिपेंडेंट स्पॉट प्राइसेस में हेरफेर | डायरेक्ट बैलेंस के बजाय इंटरनल एक्युमलेटेड वेरिएबल्स (internal reserves) का यूज करो। |
| ERC-4337 | बैच वैलिडेशन के दौरान हार्ड revert | बंडलर (bundlers) में कॉन्ट्रैक्ट या वॉलेट का बैन होना | require से ट्रांजैक्शन फेल करने के बजाय 'मैजिक' एरर कॉन्स्टेंट्स रिटर्न करो। |
| ERC-4626 | इन्फ्लेशन अटैक (पहले डिपॉजिट पर अटैक) | शेयर्स का जीरो पर राउंड-ऑफ होना, पहले डिपॉजिटर का फंड चोरी होना | इनिशियलाइजेशन पर address(0) पर "डेड शेयर्स" मिंट करो या वर्चुअल ऑफसेट्स यूज करो। |
8. कुछ थॉट्स और सुरक्षित आर्किटेक्चर के सुनहरे नियम
पता है, CTO की कुर्सी पर तीन साल बिताने के बाद मैंने एक चीज सीखी है—सबसे सुरक्षित कोड वही है जो लिखा ही नहीं गया। तुम्हारा आर्किटेक्चर जितना कॉम्प्लेक्स होगा, उसमें उतनी ही ज्यादा हिडन लिंक्स होंगी, और उतनी ही ज्यादा संभावना है कि कोई हैकाथॉन वाला जीनियस ऐसी लूपहोल ढूंढ ले जिसके बारे में तुमने सुबह की कॉफी पीते हुए सोचा भी नहीं होगा।
अगर मुझे तुम्हें बस तीन ऐसे नियम देने हों जो तुम्हारे प्रोजेक्ट को Rekt News की सुर्खियों से बचा सकें, तो वो ये होंगे:
- बाहरी कॉन्ट्रैक्ट्स पर कभी भरोसा मत करो। भले ही वो दुनिया का सबसे पॉपुलर टोकन क्यों न हो। कल उसके एडमिन्स प्रॉक्सी अपडेट कर देंगे, ब्लैकलिस्ट्स डाल देंगे और तुम्हारा सिस्टम ठप हो जाएगा। कोड ऐसे लिखो जैसे कि बाहरी टोकन नेटवर्क का सबसे शातिर और अनप्रेडिक्टेबल प्लेयर है।
- पहले स्टेट बदलो, फिर ट्रांसफर करो। मैं ये बार-बार बोलकर नहीं थकूँगा। ये स्मार्ट कॉन्ट्रैक्ट्स के किसी भी बेसिक कोर्स में सिखाया जाता है, लेकिन लोग अपनी मैपिंग में फिगर्स अपडेट करने से पहले टोकन्स भेजने की जिद करते हैं। पहले अपने कॉन्ट्रैक्ट के अंदर राइट्स या बैलेंस सेटल करो, उसे ब्लॉकचेन पर कमिट करो, और सबसे आखिरी स्टेप में
transfer,safeMintयाcallका इस्तेमाल करो। - गणित को एक्सटर्नल बैलेंस से आइसोलेट रखो। EVM में तुम्हारे कॉन्ट्रैक्ट का बैलेंस पब्लिक है और उसे मैनिपुलेट करना हलुआ है। कोई भी फ्लैश लोन से तुम्हें मिलियन डॉलर्स भेज सकता है या
selfdestructसे जबरदस्ती तुम्हारे एड्रेस पर ईथर डंप कर सकता है। अगर तुम्हारा रिवॉर्ड कैलकुलेशन या शेयर प्राइसिंग सीधे कॉन्ट्रैक्ट बैलेंस पर डिपेंडेंट है, तो तुम हार चुके हो। तुम्हारा इंटरनल अकाउंटिंग सिस्टम एक पायलट के कॉकपिट की तरह आइसोलेटेड होना चाहिए।
शायद, अब तक हमने उन सभी स्टैंडर्ड्स के पेन-पॉइंट्स कवर कर लिए हैं जो एक तगड़ी टीम की मेहनत को मिट्टी में मिला सकते हैं, अगर आर्किटेक्ट ने सही समय पर सही जगह 'जुगाड़' (सही मायने में) नहीं लगाया है।