यह लेख स्मार्ट कॉन्ट्रैक्ट सुरक्षा पर एक मिनी कोर्स के रूप में कार्य करता है और उन समस्याओं और कमजोरियों (vulnerabilities) की एक विस्तृत सूची प्रदान करता है जो बार-बार Solidity स्मार्ट कॉन्ट्रैक्ट्स में देखने को मिलती हैं।
Solidity में एक सुरक्षा समस्या का मुख्य कारण स्मार्ट कॉन्ट्रैक्ट्स का उस तरह से काम न करना है जैसा उन्हें करने का इरादा था। इसे चार व्यापक श्रेणियों में बांटा जा सकता है:
- फंड्स की चोरी होना
- फंड्स का कॉन्ट्रैक्ट के अंदर लॉक या फ्रीज हो जाना
- लोगों को उम्मीद से कम रिवॉर्ड मिलना (रिवॉर्ड में देरी या कमी होना)
- लोगों को उम्मीद से अधिक रिवॉर्ड मिलना (जिससे इन्फ्लेशन और अवमूल्यन होता है)
गलत हो सकने वाली हर चीज़ की एक व्यापक सूची बनाना संभव नहीं है। हालाँकि, जिस तरह पारंपरिक सॉफ्टवेयर इंजीनियरिंग में SQL injection, buffer overruns, और cross site scripting जैसी कमजोरियों के सामान्य विषय होते हैं, वैसे ही स्मार्ट कॉन्ट्रैक्ट्स में भी बार-बार होने वाले एंटी-पैटर्न्स (anti-patterns) होते हैं जिन्हें डॉक्यूमेंट किया जा सकता है।
स्मार्ट कॉन्ट्रैक्ट हैक्स और कमजोरियां (vulnerabilities)
इस गाइड को एक संदर्भ (reference) के रूप में अधिक सोचें। हर कॉन्सेप्ट पर विस्तार से चर्चा करना संभव नहीं है, अन्यथा यह एक किताब बन जाएगी (चेतावनी: यह लेख 10 हज़ार से अधिक शब्दों का है, इसलिए बेझिझक इसे बुकमार्क करें और टुकड़ों में पढ़ें)। हालाँकि, यह इस बात की सूची के रूप में कार्य करता है कि आपको किन चीज़ों पर ध्यान देना चाहिए और क्या अध्ययन करना चाहिए। यदि कोई विषय अपरिचित लगता है, तो यह एक संकेत होना चाहिए कि उस विशिष्ट श्रेणी की कमजोरी को पहचानने का अभ्यास करने में समय लगाना उचित है।
पूर्व आवश्यकताएं (Prerequisites)
यह लेख मानकर चलता है कि आपको Solidity में बुनियादी दक्षता हासिल है। यदि आप Solidity में नए हैं, तो कृपया हमारा मुफ्त Solidity ट्यूटोरियल देखें।
रीएंट्रेंसी (Reentrancy)
हमने स्मार्ट कॉन्ट्रैक्ट रीएंट्रेंसी पर विस्तार से लिखा है, इसलिए हम इसे यहाँ नहीं दोहराएंगे। लेकिन यहाँ एक त्वरित सारांश दिया गया है:
जब भी कोई स्मार्ट कॉन्ट्रैक्ट किसी अन्य स्मार्ट कॉन्ट्रैक्ट के फंक्शन को कॉल करता है, उसे Ether भेजता है, या उसे कोई टोकन ट्रांसफर करता है, तो रीएंट्रेंसी की संभावना होती है।
- जब Ether ट्रांसफर किया जाता है, तो प्राप्तकर्ता कॉन्ट्रैक्ट का fallback या receive फंक्शन कॉल किया जाता है। यह कंट्रोल रिसीवर को सौंप देता है।
- कुछ टोकन प्रोटोकॉल प्राप्तकर्ता स्मार्ट कॉन्ट्रैक्ट को सचेत करते हैं कि उन्हें टोकन प्राप्त हुआ है, जिसके लिए एक पूर्वनिर्धारित (predetermined) फंक्शन को कॉल किया जाता है। यह कंट्रोल फ्लो उस फंक्शन को सौंप देता है।
- जब कोई अटैकिंग कॉन्ट्रैक्ट कंट्रोल प्राप्त करता है, तो उसे उसी फंक्शन को कॉल करने की आवश्यकता नहीं होती जिसने कंट्रोल सौंपा था। वह पीड़ित स्मार्ट कॉन्ट्रैक्ट के एक अलग फंक्शन को कॉल कर सकता है (cross-function reentrancy) या किसी अलग कॉन्ट्रैक्ट को भी कॉल कर सकता है (cross-contract reentrancy)।
- रीड-ओनली रीएंट्रेंसी (Read-only reentrancy) तब होती है जब एक view फंक्शन को तब एक्सेस किया जाता है जब कॉन्ट्रैक्ट एक इंटरमीडिएट स्टेट में होता है।
रीएंट्रेंसी संभवतः सबसे प्रसिद्ध स्मार्ट कॉन्ट्रैक्ट वल्नरेबिलिटी होने के बावजूद, वास्तविक दुनिया में होने वाले हैक्स में इसका केवल एक छोटा प्रतिशत ही हिस्सा है। सुरक्षा शोधकर्ता Pascal Caversaccio (pcaveraccio) रीएंट्रेंसी अटैक्स की एक अद्यतन (up-to-date) github सूची बनाए रखते हैं। अप्रैल 2023 तक, उस रिपॉजिटरी में 46 रीएंट्रेंसी हमलों का दस्तावेजीकरण किया गया है।
एक्सेस कंट्रोल (Access Control)
यह एक साधारण गलती लगती है, लेकिन जो संवेदनशील फंक्शन (जैसे ईथर निकालना या ओनरशिप बदलना) कॉल कर सकते हैं, उन पर प्रतिबंध लगाना भूल जाना आश्चर्यजनक रूप से अक्सर होता है।
भले ही एक मॉडिफायर मौजूद हो, ऐसे मामले भी सामने आए हैं जहाँ मॉडिफायर को सही तरीके से लागू नहीं किया गया था, जैसे कि नीचे दिए गए उदाहरण में जहाँ require स्टेटमेंट गायब है।
// DO NOT USE!
modifier onlyMinter {
minters[msg.sender] == true
_;
}
ऊपर दिया गया कोड इस ऑडिट का एक वास्तविक उदाहरण है: https://code4rena.com/reports/2023-01-rabbithole/#h-01-bad-implementation-in-minter-access-control-for-rabbitholereceipt-and-rabbitholetickets-contracts
यहाँ एक और तरीका है जिससे एक्सेस कंट्रोल गलत हो सकता है:
function claimAirdrop(bytes32 calldata proof[]) {
bool verified = MerkleProof.verifyCalldata(proof, merkleRoot, keccak256(abi.encode(msg.sender)));
require(verified, "not verified");
require(!alreadyClaimed[msg.sender], "already claimed");
_transfer(msg.sender, AIRDROP_AMOUNT);
}
इस मामले में, “alreadyClaimed” को कभी भी true पर सेट नहीं किया जाता है, इसलिए दावा करने वाला व्यक्ति इस फंक्शन को कई बार कॉल कर सकता है।
असल जिंदगी का उदाहरण: Trader bot को एक्सप्लॉइट किया गया
अपरर्याप्त एक्सेस कंट्रोल का एक हालिया उदाहरण ट्रेडिंग बॉट द्वारा फ्लैशलोन प्राप्त करने के लिए एक असुरक्षित फंक्शन था (जिसका नाम 0xbad था, क्योंकि एड्रेस उसी अनुक्रम से शुरू होता था)। इसने लाभ के रूप में एक मिलियन डॉलर से अधिक की कमाई की, जब तक कि एक दिन एक हमलावर ने यह नहीं देखा कि कोई भी एड्रेस फ्लैशलोन रिसीव फंक्शन को कॉल कर सकता है, न कि केवल फ्लैशलोन प्रोवाइडर।
जैसा कि अक्सर ट्रेडिंग बॉट्स के साथ होता है, ट्रेड्स को निष्पादित (execute) करने के लिए स्मार्ट कॉन्ट्रैक्ट कोड वेरीफाइड नहीं था, लेकिन हमलावर ने फिर भी कमजोरी का पता लगा लिया। अधिक जानकारी rekt news कवरेज में है।
अनुचित इनपुट वैलिडेशन (Improper Input Validation)
यदि एक्सेस कंट्रोल इस बारे में है कि कोई फंक्शन कौन कॉल करता है, तो इनपुट वैलिडेशन इस बारे में है कि वे कॉन्ट्रैक्ट को किस डेटा के साथ कॉल करते हैं।
यह आमतौर पर उचित require स्टेटमेंट्स को लागू करना भूल जाने के कारण होता है। यहाँ एक बुनियादी उदाहरण दिया गया है:
contract UnsafeBank {
mapping(address => uint256) public balances;
// allow depositing on other's behalf
function deposit(address for) public payable {
balances += msg.value;
}
function withdraw(address from, uint256 amount) public {
require(balances[from] <= amount, "insufficient balance");
balances[from] -= amount;
msg.sender.call{value: amount}("");
}
}
ऊपर दिया गया कॉन्ट्रैक्ट यह जाँचता है कि आप अपने खाते में मौजूद राशि से अधिक नहीं निकाल रहे हैं, लेकिन यह आपको किसी भी यादृच्छिक (arbitrary) खाते से पैसे निकालने से नहीं रोकता है।
असल जिंदगी का उदाहरण: Sushiswap
Sushiswap ने एक एक्सटर्नल फंक्शन के मापदंडों (parameters) में से एक को सही से सैनिटाइज (sanitize) न किए जाने के कारण इसी प्रकार के हैक का अनुभव किया।

https://twitter.com/peckshield/status/1644907207530774530
अनुचित एक्सेस कंट्रोल और अनुचित इनपुट वैलिडेशन के बीच क्या अंतर है?
अनुचित एक्सेस कंट्रोल का मतलब है कि msg.sender पर पर्याप्त प्रतिबंध नहीं हैं। अनुचित इनपुट वैलिडेशन का मतलब है कि फंक्शन के तर्कों (arguments) को पर्याप्त रूप से सैनिटाइज नहीं किया गया है। इस एंटी-पैटर्न का एक विपरीत पहलू भी है: फंक्शन कॉल पर बहुत अधिक प्रतिबंध लगाना।
अत्यधिक फंक्शन प्रतिबंध (Excessive function restriction)
अत्यधिक वैलिडेशन का शायद यह अर्थ हो कि फंड चोरी नहीं होंगे, लेकिन इसका मतलब यह हो सकता है कि फंड कॉन्ट्रैक्ट में लॉक हो जाएं। बहुत अधिक सुरक्षा उपाय होना भी अच्छी बात नहीं है।
असल जिंदगी का उदाहरण: Akutars NFT
सबसे चर्चित घटनाओं में से एक Akutars NFT थी, जिसके स्मार्ट कॉन्ट्रैक्ट के अंदर 34 मिलियन डॉलर मूल्य का Eth फंस गया और जिसे निकाला नहीं जा सका।
कॉन्ट्रैक्ट में एक अच्छी मंशा वाला मैकेनिज्म था जो कॉन्ट्रैक्ट के ओनर को तब तक पैसे निकालने से रोकता था जब तक कि डच नीलामी मूल्य से अधिक भुगतान करने वालों को सभी रिफंड न दे दिए जाएं। लेकिन नीचे दिए गए ट्विटर थ्रेड में बताए गए एक बग के कारण, ओनर फंड निकालने में असमर्थ था।

https://twitter.com/0xInuarashi/status/1517674505975394304
सही संतुलन बनाना
Sushiswap ने अविश्वसनीय उपयोगकर्ताओं को बहुत अधिक शक्ति दे दी, और Akutars NFT ने एडमिन को बहुत कम शक्ति दी। स्मार्ट कॉन्ट्रैक्ट्स को डिज़ाइन करते समय, उपयोगकर्ताओं के प्रत्येक वर्ग को कितनी स्वतंत्रता मिलनी चाहिए, इसके बारे में एक व्यक्तिपरक (subjective) निर्णय लिया जाना चाहिए, और इस निर्णय को स्वचालित परीक्षण (automated testing) और टूलींग पर नहीं छोड़ा जा सकता है। डिसेंट्रलाइजेशन, सुरक्षा और UX के बीच महत्वपूर्ण ट्रेडऑफ़ होते हैं जिन पर विचार किया जाना चाहिए।
स्मार्ट कॉन्ट्रैक्ट प्रोग्रामर के लिए, यह स्पष्ट रूप से लिखना कि उपयोगकर्ताओं को कुछ फंक्शन्स के साथ क्या करने में सक्षम होना चाहिए और क्या नहीं, विकास प्रक्रिया का एक महत्वपूर्ण हिस्सा है।
हम बाद में अत्यधिक शक्तिशाली एडमिन्स के विषय पर फिर से विचार करेंगे।
सुरक्षा अक्सर इस बात पर निर्भर करती है कि कॉन्ट्रैक्ट से पैसा कैसे बाहर निकलता है
जैसा कि परिचय में बताया गया है, स्मार्ट कॉन्ट्रैक्ट्स मुख्य रूप से चार तरीकों से हैक किए जाते हैं:
- पैसे की चोरी
- पैसा फ्रीज हो जाना
- अपर्याप्त रिवॉर्ड
- अत्यधिक रिवॉर्ड
यहाँ “पैसे” का अर्थ है किसी भी मूल्यवान चीज़ से, जैसे टोकन, न कि केवल क्रिप्टोकरेंसी। कोडिंग या स्मार्ट कॉन्ट्रैक्ट को ऑडिट करते समय, डेवलपर को कॉन्ट्रैक्ट के अंदर और बाहर मूल्य (value) के प्रवाह के इच्छित तरीकों के प्रति सचेत रहना चाहिए। ऊपर सूचीबद्ध मुद्दे वे प्राथमिक तरीके हैं जिनसे स्मार्ट कॉन्ट्रैक्ट हैक किए जाते हैं, लेकिन कई अन्य मूल कारण भी हैं जो प्रमुख समस्याओं का रूप ले सकते हैं, जिन्हें नीचे प्रलेखित (documented) किया गया है।
डबल वोटिंग या msg.sender स्पूफिंग
वोट को तौलने के लिए वेनिला (vanilla) ERC20 टोकन या NFTs को टिकट के रूप में उपयोग करना असुरक्षित है क्योंकि हमलावर एक एड्रेस से वोट कर सकते हैं, फिर उन टोकन को किसी अन्य एड्रेस पर ट्रांसफर कर सकते हैं, और उस एड्रेस से फिर से वोट कर सकते हैं।
यहाँ एक न्यूनतम उदाहरण दिया गया है:
// A malicious voter can simply transfer their tokens to
// another address and vote again.
contract UnsafeBallot {
uint256 public proposal1VoteCount;
uint256 public proposal2VoteCount;
IERC20 immutable private governanceToken;
constructor(IERC20 _governanceToken) {
governanceToken = _governanceToken;
}
function voteFor1() external notAlreadyVoted {
proposal1VoteCount += governanceToken.balanceOf(msg.sender);
}
function voteFor2() external notAlreadyVoted {
proposal2VoteCount += governanceToken.balanceOf(msg.sender);
}
// prevent the same address from voting twice,
// however the attacker can simply
// transfer to a new address
modifier notAlreadyVoted {
require(!alreadyVoted[msg.sender], "already voted");
_;
alreadyVoted[msg.sender] = true;
}
}
इस हमले को रोकने के लिए, ERC20 Snapshot या ERC20 Votes का उपयोग किया जाना चाहिए। अतीत के किसी एक समय का स्नैपशॉट लेने से, अवैध मतदान शक्ति प्राप्त करने के लिए वर्तमान टोकन बैलेंस में हेरफेर नहीं किया जा सकता है।
फ्लैशलोन गवर्नेंस अटैक (Flashloan Governance Attacks)
हालाँकि, स्नैपशॉट या वोट क्षमता वाले ERC20 टोकन का उपयोग करना समस्या को पूरी तरह से हल नहीं करता है, यदि कोई अपने बैलेंस को अस्थायी रूप से बढ़ाने के लिए फ्लैशलोन ले सकता है, और फिर उसी ट्रांजैक्शन में अपने बैलेंस का स्नैपशॉट ले सकता है। यदि उस स्नैपशॉट का उपयोग मतदान के लिए किया जाता है, तो उनके पास उपयोग करने के लिए अनुचित रूप से बड़ी मात्रा में वोट होंगे।
एक फ्लैशलोन एक एड्रेस को बड़ी मात्रा में ईथर या टोकन उधार देता है, लेकिन यदि पैसा उसी ट्रांजैक्शन में वापस नहीं किया जाता है तो यह रिवर्ट (revert) हो जाता है।
contract SimpleFlashloan {
function borrowERC20Tokens() public {
uint256 before = token.balanceOf(address(this));
// send tokens to the borrower
token.transfer(msg.sender, amount);
// hand control back to the borrower to
// let them do something
IBorrower(msg.sender).onFlashLoan();
// require that the tokens got returned
require(token.balanceOf(address(this) >= before);
}
}
एक हमलावर अपने पक्ष में प्रस्तावों को स्विंग करने और/या कुछ दुर्भावनापूर्ण करने के लिए अचानक बहुत सारे वोट प्राप्त करने के लिए फ्लैशलोन का उपयोग कर सकता है।
फ्लैशलोन प्राइस अटैक (Flashloan Price Attacks)
यह यकीनन DeFi पर सबसे आम (या कम से कम सबसे हाई-प्रोफाइल) हमला है, जिसके कारण करोड़ों डॉलर का नुकसान हुआ है। यहाँ हाई-प्रोफाइल हमलों की एक सूची दी गई है।
ब्लॉकचेन पर किसी संपत्ति की कीमत की गणना अक्सर संपत्तियों के बीच वर्तमान विनिमय दर (exchange rate) के रूप में की जाती है। उदाहरण के लिए, यदि एक कॉन्ट्रैक्ट वर्तमान में 100 k9coin के लिए 1 USDC का व्यापार कर रहा है, तो आप कह सकते हैं कि k9coin की कीमत 0.01 USDC है। हालाँकि, कीमतें आमतौर पर खरीदने और बेचने के दबाव के जवाब में चलती हैं, और फ्लैश लोन बड़े पैमाने पर खरीद और बिक्री का दबाव बना सकते हैं।
किसी संपत्ति की कीमत के बारे में दूसरे स्मार्ट कॉन्ट्रैक्ट से पूछताछ करते समय, डेवलपर को बहुत सावधान रहने की आवश्यकता है क्योंकि वे यह मान रहे हैं कि वे जिस स्मार्ट कॉन्ट्रैक्ट को कॉल कर रहे हैं वह फ्लैश लोन के हेरफेर से प्रतिरक्षित (immune) है।
कॉन्ट्रैक्ट चेक को बायपास करना
आप किसी एड्रेस के बाइटकोड साइज़ (bytecode size) को देखकर “चेक” कर सकते हैं कि क्या वह स्मार्ट कॉन्ट्रैक्ट है। एक्सटर्नली ओन्ड अकाउंट्स (EOA - सामान्य वॉलेट्स) में कोई बाइटकोड नहीं होता है। ऐसा करने के कुछ तरीके यहां दिए गए हैं:
import "@openzeppelin/contracts/utils/Address.sol"
contract CheckIfContract {
using Address for address;
function addressIsContractV1(address _a) {
return _a.code.length != 0;
}
function addressIsContractV2(address _a) {
// use the openzeppelin libraryreturn _a.isContract();
}
}
हालाँकि, इसकी कुछ सीमाएँ हैं:
- यदि कोई कॉन्ट्रैक्ट कंस्ट्रक्टर से बाहरी कॉल (external call) करता है, तो उसका स्पष्ट बाइटकोड आकार शून्य होगा क्योंकि स्मार्ट कॉन्ट्रैक्ट डिप्लॉयमेंट कोड ने अभी तक रनटाइम कोड वापस नहीं किया है।
- वह स्थान (space) अभी खाली हो सकता है, लेकिन एक हमलावर जान सकता है कि वे भविष्य में create2 का उपयोग करके वहां एक स्मार्ट कॉन्ट्रैक्ट डिप्लॉय कर सकते हैं।
सामान्य तौर पर यह जाँचना कि कोई एड्रेस एक कॉन्ट्रैक्ट है या नहीं, आमतौर पर (लेकिन हमेशा नहीं) एक एंटीपैटर्न होता है। मल्टीसिग्नेचर (Multisignature) वॉलेट्स स्वयं स्मार्ट कॉन्ट्रैक्ट होते हैं, और ऐसा कुछ भी करना जो मल्टीसिग्नेचर वॉलेट्स को तोड़ सकता है, कंपोजेबिलिटी (composability) को तोड़ देता है।
इसका अपवाद यह जांचना है कि ट्रांसफर हुक (transfer hook) कॉल करने से पहले लक्ष्य (target) एक स्मार्ट कॉन्ट्रैक्ट है या नहीं। इसके बारे में बाद में और जानकारी दी जाएगी।
tx.origin
tx.origin का उपयोग करने का शायद ही कोई अच्छा कारण हो। यदि प्रेषक (sender) की पहचान करने के लिए tx.origin का उपयोग किया जाता है, तो मैन-इन-द-मिडिल (man-in-the-middle) अटैक संभव है। यदि उपयोगकर्ता को दुर्भावनापूर्ण स्मार्ट कॉन्ट्रैक्ट कॉल करने के लिए धोखा दिया जाता है, तो स्मार्ट कॉन्ट्रैक्ट tx.origin के पास मौजूद सभी अधिकारों का उपयोग करके तबाही मचा सकता है।
निम्नलिखित अभ्यास पर विचार करें, और कोड के ऊपर दी गई टिप्पणियों को पढ़ें।
contract Phish {
function phishingFunction() public {
// this fails, because this contract does not have approval/allowance
token.transferFrom(msg.sender, address(this), token.balanceOf(msg.sender));
// this also fails, because this creates approval for the contract,
// not the wallet calling this phishing function
token.approve(address(this), type(uint256).max);
}
}
इसका मतलब यह नहीं है कि आप मनमाने (arbitrary) स्मार्ट कॉन्ट्रैक्ट्स को कॉल करने में सुरक्षित हैं। लेकिन अधिकांश प्रोटोकॉल में सुरक्षा की एक परत होती है जिसे यदि प्रमाणीकरण (authentication) के लिए tx.origin का उपयोग किया जाता है, तो बायपास कर दिया जाएगा।
कभी-कभी, आप ऐसा कोड देख सकते हैं:
require(msg.sender == tx.origin, "no contracts");
जब कोई स्मार्ट कॉन्ट्रैक्ट किसी अन्य स्मार्ट कॉन्ट्रैक्ट को कॉल करता है, तो msg.sender स्मार्ट कॉन्ट्रैक्ट होगा और tx.origin उपयोगकर्ता का वॉलेट होगा, इस प्रकार यह एक विश्वसनीय संकेत देता है कि आने वाली कॉल एक स्मार्ट कॉन्ट्रैक्ट से है। यह तब भी सच है जब कॉल कंस्ट्रक्टर से होती है।
ज्यादातर मामलों में, यह डिज़ाइन पैटर्न एक अच्छा विचार नहीं है। मल्टीसिग्नेचर वॉलेट और EIP 4337 के वॉलेट्स उस फंक्शन के साथ इंटरेक्ट नहीं कर पाएंगे जिसमें यह कोड है। यह पैटर्न आमतौर पर NFT मिंट्स में देखा जा सकता है, जहाँ यह उम्मीद करना उचित है कि अधिकांश उपयोगकर्ता पारंपरिक वॉलेट का उपयोग कर रहे हैं। लेकिन जैसे-जैसे अकाउंट एब्स्ट्रेक्शन (account abstraction) अधिक लोकप्रिय हो रहा है, यह पैटर्न मदद करने से ज्यादा बाधा उत्पन्न करेगा।
गैस ग्रीफिंग (Gas Griefing) या डिनायल ऑफ सर्विस (Denial of Service)
ग्रीफिंग अटैक का मतलब है कि हैकर अन्य लोगों को “नुकसान या दुख (grief) पहुंचाने” की कोशिश कर रहा है, भले ही ऐसा करने से उन्हें कोई आर्थिक लाभ न हो।
एक स्मार्ट कॉन्ट्रैक्ट एक अनंत लूप (infinite loop) में जाकर उसे फॉरवर्ड की गई सभी गैस का दुर्भावनापूर्ण रूप से उपयोग कर सकता है। निम्नलिखित उदाहरण पर विचार करें:
contract Mal {
fallback() external payable {
// infinite loop uses up all the gas
while (true) {
}
}
}
यदि कोई अन्य कॉन्ट्रैक्ट एड्रेस की सूची में ईथर वितरित करता है, जैसे कि:
contract Distribute {
funtion distribute(uint256 total) public nonReentrant {
for (uint i; i < addresses.length; ) {
(bool ok, ) addresses.call{value: total / addresses.length}("");
// ignore ok, if it reverts we move on
// traditional gas saving trick for for loops
unchecked {
++i;
}
}
}
}
तब फंक्शन रिवर्ट (revert) हो जाएगा जब वह Mal को ईथर भेजेगा। ऊपर दिए गए कोड में कॉल उपलब्ध गैस का 63/64 फॉरवर्ड करता है (इस नियम के बारे में अधिक जानने के लिए हमारे लेख EIP 150 को पढ़ें), इसलिए केवल 1/64 गैस शेष रहने पर ऑपरेशन को पूरा करने के लिए संभवतः पर्याप्त गैस नहीं होगी।
एक स्मार्ट कॉन्ट्रैक्ट एक बहुत बड़ा मेमोरी एरे (memory array) वापस कर सकता है जो बहुत सारी गैस की खपत करता है। निम्नलिखित उदाहरण पर विचार करें:
function largeReturn() public {
// result might be extremely long!
(book ok, bytes memory result) =
otherContract.call(abi.encodeWithSignature("foo()"));
require(ok, "call failed");
}
मेमोरी एरे 724 बाइट्स के बाद द्विघात (quadratic) मात्रा में गैस का उपयोग करते हैं, इसलिए सावधानीपूर्वक चुना गया रिटर्न डेटा साइज कॉलर को परेशान (grief) कर सकता है।
भले ही वेरिएबल result का उपयोग न किया गया हो, फिर भी इसे मेमोरी में कॉपी किया जाता है। यदि आप रिटर्न साइज़ को एक निश्चित मात्रा तक सीमित करना चाहते हैं, तो आप असेंबली (assembly) का उपयोग कर सकते हैं:
function largeReturn() public {
assembly {
let ok := call(gas(), destinationAddress, value, dataOffset, dataSize, 0x00, 0x00);
// nothing is copied to memory until you
// use returndatacopy()
}
}
ऐसे एरे को डिलीट करना जिनमें अन्य लोग डेटा जोड़ सकते हैं, यह भी एक डिनायल ऑफ सर्विस वेक्टर है
हालाँकि स्टोरेज को मिटाना एक गैस-कुशल (gas-efficient) ऑपरेशन है, फिर भी इसकी एक शुद्ध लागत (net cost) होती है। यदि कोई एरे बहुत लंबा हो जाता है, तो उसे डिलीट करना असंभव हो जाता है। यहाँ एक न्यूनतम उदाहरण दिया गया है:
contract VulnerableArray {
address[] public stuff;
function addSomething(address something) public {
stuff.push(something);
}
// if stuff is too long, this will become undeletable due to
// the gas cost
function deleteEverything() public onlyOwner {
delete stuff;
}
}
ERC777, ERC721, और ERC1155 भी ग्रीफिंग वेक्टर हो सकते हैं
यदि कोई स्मार्ट कॉन्ट्रैक्ट ऐसे टोकन ट्रांसफर करता है जिनमें ट्रांसफर हुक हैं, तो एक हमलावर एक ऐसा कॉन्ट्रैक्ट सेट कर सकता है जो टोकन को स्वीकार नहीं करता है (या तो इसमें onReceive फंक्शन नहीं है या यह फंक्शन को रिवर्ट करने के लिए प्रोग्राम करता है)। यह टोकन को गैर-हस्तांतरणीय (untransferable) बना देगा और पूरे ट्रांजैक्शन को रिवर्ट कर देगा।
safeTransfer या transfer का उपयोग करने से पहले, इस संभावना पर विचार करें कि रिसीवर ट्रांजैक्शन को रिवर्ट करने के लिए मजबूर कर सकता है।
contract Mal is IERC721Receiver, IERC1155Receiver, IERC777Receiver {
// this will intercept any transfer hook
fallback() external payable {
// infinite loop uses up all the gas
while (true) {
}
}
// we could also selectively deny transactions
function onERC721Received(address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
if (wakeUpChooseViolence()) {
revert();
}
else {
return IERC721Receiver.onERC721Received.selector;
}
}
}
असुरक्षित रैंडमनेस (Insecure Randomness)
ब्लॉकचेन पर एक ही ट्रांजैक्शन के साथ सुरक्षित रूप से रैंडमनेस उत्पन्न करना वर्तमान में संभव नहीं है। ब्लॉकचेन को पूरी तरह से नियतात्मक (deterministic) होना चाहिए, अन्यथा डिस्ट्रिब्यूटेड नोड्स स्टेट के बारे में आम सहमति (consensus) तक पहुंचने में सक्षम नहीं होंगे। क्योंकि वे पूरी तरह से नियतात्मक हैं, किसी भी “रैंडम” संख्या की भविष्यवाणी की जा सकती है। निम्नलिखित पासा फेंकने वाले फंक्शन का शोषण किया जा सकता है:
contract UnsafeDice {
function randomness() internal returns (uint256) {
return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1);
}
// our dice can land on one of {0,1,2,3,4,5}
function rollDice() public payable {
require(msg.value == 1 ether);
if (randomness() % 6) == 5) {
msg.sender.call{value: 2 ether}("");
}
}
}
contract ExploitDice {
function randomness() internal returns (uint256) {
return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1);
}
function betSafely(IUnsafeDice game) public payable {
if (randomness % 6) == 5)) {
game.betSafely{value: 1 ether}()
}
// else don't do anything
}
}
इससे कोई फर्क नहीं पड़ता कि आप रैंडमनेस कैसे उत्पन्न करते हैं क्योंकि एक हमलावर इसे बिल्कुल दोहरा (replicate) सकता है। “एन्ट्रॉपी” (entropy) के अधिक स्रोत जैसे msg.sender, टाइमस्टैम्प आदि को शामिल करने से कोई प्रभाव नहीं पड़ेगा क्योंकि स्मार्ट कॉन्ट्रैक्ट भी इसे माप सकता है।
Chainlink रैंडमनेस ओरेकल (Oracle) का गलत इस्तेमाल
Chainlink सुरक्षित रैंडम नंबर प्राप्त करने का एक लोकप्रिय समाधान है। यह इसे दो चरणों में करता है। सबसे पहले, स्मार्ट कॉन्ट्रैक्ट ओरेकल को रैंडमनेस का अनुरोध भेजता है, फिर कुछ ब्लॉक्स के बाद, ओरेकल एक रैंडम नंबर के साथ प्रतिक्रिया देता है।
चूंकि एक हमलावर भविष्य की भविष्यवाणी नहीं कर सकता है, इसलिए वे रैंडम नंबर की भविष्यवाणी नहीं कर सकते हैं।
जब तक कि स्मार्ट कॉन्ट्रैक्ट ओरेकल का गलत इस्तेमाल न करे।
- रैंडमनेस का अनुरोध करने वाले स्मार्ट कॉन्ट्रैक्ट को तब तक कुछ नहीं करना चाहिए जब तक कि रैंडम नंबर वापस न आ जाए। अन्यथा, एक हमलावर ओरेकल द्वारा रैंडमनेस लौटाए जाने के लिए मेमपूल (mempool) की निगरानी कर सकता है और ओरेकल को फ्रंटरन (frontrun) कर सकता है, यह जानते हुए कि रैंडम नंबर क्या होगा।
- रैंडमनेस ओरेकल्स स्वयं आपके एप्लिकेशन में हेरफेर करने का प्रयास कर सकते हैं। वे अन्य नोड्स से सर्वसम्मति (consensus) के बिना रैंडम संख्या नहीं चुन सकते हैं, लेकिन यदि आपका एप्लिकेशन एक ही समय में कई अनुरोध करता है तो वे रैंडम संख्याओं को रोक सकते हैं और फिर से ऑर्डर (re-order) कर सकते हैं।
- Ethereum या अधिकांश अन्य EVM चेन्स पर अंतिमता (Finality) तुरंत नहीं होती है। सिर्फ इसलिए कि कोई ब्लॉक सबसे हाल का है, इसका मतलब यह नहीं है कि यह जरूरी रूप से वैसा ही रहेगा। इसे “चेन री-ऑर्ग” (chain re-org) कहा जाता है। वास्तव में, चेन अंतिम ब्लॉक से अधिक बदल सकती है। इसे “री-ऑर्ग डेप्थ” (re-org depth) कहा जाता है। Etherscan विभिन्न चेन्स के लिए री-ऑर्ग रिपोर्ट करता है, उदाहरण के लिए Ethereum reorgs और Polygon reorgs। बहुभुज (Polygon) पर री-ऑर्ग 30 या अधिक ब्लॉक जितने गहरे हो सकते हैं, इसलिए कम ब्लॉक्स की प्रतीक्षा करने से एप्लिकेशन असुरक्षित हो सकता है (यह तब बदल सकता है जब zk-evm बहुभुज पर मानक सर्वसम्मति बन जाए, क्योंकि फाइनलिटी एथेरियम से मेल खाएगी लेकिन यह भविष्य की भविष्यवाणी है, वर्तमान के बारे में कोई तथ्य नहीं)।
- यहाँ अन्य चेनलिंक रैंडमनेस सुरक्षा विचार दिए गए हैं।
प्राइस ओरेकल से पुराना (stale) डेटा प्राप्त करना
एक निश्चित समय सीमा के भीतर अपने प्राइस ओरेकल्स को अद्यतित रखने के लिए Chainlink के लिए कोई SLA (service level agreement) नहीं है। जब चेन में भारी भीड़ होती है (जैसे जब Yuga Labs Otherside मिंट ने Ethereum को इतना प्रभावित किया कि कोई ट्रांजैक्शन पास नहीं हो रहा था), तो प्राइस अपडेट्स में देरी हो सकती है।
एक स्मार्ट कॉन्ट्रैक्ट जो प्राइस ओरेकल का उपयोग करता है, उसे स्पष्ट रूप से जांचना चाहिए कि डेटा पुराना (stale) नहीं है, यानी हाल ही में कुछ थ्रेशोल्ड के भीतर अपडेट किया गया है। अन्यथा, यह कीमतों के संबंध में एक विश्वसनीय निर्णय नहीं ले सकता है।
इसमें एक और जटिलता है कि यदि कीमत किसी डेविएशन थ्रेशोल्ड (deviation threshold) से अधिक नहीं बदलती है, तो ओरेकल गैस बचाने के लिए कीमत को अपडेट नहीं कर सकता है, इसलिए यह इस बात को प्रभावित कर सकता है कि किस समय सीमा को “बासी” (stale) माना जाता है।
यह समझना महत्वपूर्ण है कि स्मार्ट कॉन्ट्रैक्ट किस ओरेकल के SLA पर निर्भर करता है।
केवल एक ओरेकल पर निर्भर रहना
कोई ओरेकल कितना भी सुरक्षित क्यों न लगे, भविष्य में कोई हमला खोजा जा सकता है। इससे बचाव का एकमात्र तरीका कई स्वतंत्र ओरेकल्स का उपयोग करना है।
सामान्य तौर पर ओरेकल्स का सही उपयोग करना मुश्किल है
ब्लॉकचेन काफी सुरक्षित हो सकता है, लेकिन डेटा को पहली बार चेन पर डालने के लिए किसी प्रकार के ऑफ-चेन ऑपरेशन की आवश्यकता होती है जो ब्लॉकचेन द्वारा प्रदान की जाने वाली सभी सुरक्षा गारंटियों को छोड़ देता है। भले ही ओरेकल्स ईमानदार रहें, उनके डेटा के स्रोत में हेरफेर किया जा सकता है। उदाहरण के लिए, एक ओरेकल एक केंद्रीकृत एक्सचेंज से मज़बूती से कीमतों की रिपोर्ट कर सकता है, लेकिन बड़े खरीद और बिक्री आदेशों (buy and sell orders) के साथ उनमें हेरफेर किया जा सकता है। इसी तरह, सेंसर डेटा या किसी वेब2 एपीआई पर निर्भर ओरेकल्स पारंपरिक हैकिंग वैक्टर के अधीन हैं।
एक अच्छा स्मार्ट कॉन्ट्रैक्ट आर्किटेक्चर जहाँ तक संभव हो ओरेकल्स के उपयोग से पूरी तरह बचता है।
मिक्स्ड अकाउंटिंग (Mixed accounting)
निम्नलिखित कॉन्ट्रैक्ट पर विचार करें:
contract MixedAccounting {
uint256 myBalance;
function deposit() public payable {
myBalance = myBalance + msg.value;
}
function myBalanceIntrospect() public view returns (uint256) {
return address(this).balance;
}
function myBalanceVariable() public view returns (uint256) {
return myBalance;
}
function notAlwaysTrue() public view returns (bool) {
return myBalanceIntrospect() == myBalanceVariable();
}
}
ऊपर दिए गए कॉन्ट्रैक्ट में receive या fallback फंक्शन नहीं है, इसलिए सीधे इसे ईथर ट्रांसफर करने से रिवर्ट हो जाएगा। हालाँकि, कोई कॉन्ट्रैक्ट selfdestruct के साथ बलपूर्वक इसे ईथर भेज सकता है। उस मामले में, myBalanceIntrospect() का मान myBalanceVariable() से अधिक होगा। ईथर अकाउंटिंग का कोई भी तरीका ठीक है, लेकिन यदि आप दोनों का उपयोग करते हैं, तो कॉन्ट्रैक्ट में असंगत (inconsistent) व्यवहार हो सकता है।
यही बात ERC20 टोकन के लिए भी लागू होती है।
contract MixedAccountingERC20 {
IERC20 token;
uint256 myTokenBalance;
function deposit(uint256 amount) public {
token.transferFrom(msg.sender, address(this), amount);
myTokenBalance = myTokenBalance + amount;
}
function myBalanceIntrospect() public view returns (uint256) {
return token.balanceOf(address(this));
}
function myBalanceVariable() public view returns (uint256) {
return myTokenBalance;
}
function notAlwaysTrue() public view returns (bool) {
return myBalanceIntrospect() == myBalanceVariable();
}
}
फिर से हम यह नहीं मान सकते कि myBalanceIntrospect() और myBalanceVariable() हमेशा समान मान लौटाएंगे। सीधे MixedAccountingERC20 को ERC20 टोकन ट्रांसफर करना संभव है, जो जमा (deposit) फंक्शन को बायपास कर देता है और myTokenBalance वेरिएबल को अपडेट नहीं करता है।
इंट्रोस्पेक्शन (introspection) के साथ बैलेंस चेक करते समय, सख्त समानता जांच (strict equality checks) से बचा जाना चाहिए क्योंकि कोई बाहरी व्यक्ति अपनी इच्छानुसार बैलेंस को बदल सकता है।
क्रिप्टोग्राफिक प्रूफ़्स को पासवर्ड की तरह मानना
यह Solidity की कोई खामी नहीं है, बल्कि यह डेवलपर्स के बीच एक आम गलतफहमी है कि एड्रेस को विशेष विशेषाधिकार (privileges) देने के लिए क्रिप्टोग्राफी का उपयोग कैसे किया जाए। निम्नलिखित कोड असुरक्षित है:
contract InsecureMerkleRoot {
bytes32 merkleRoot;
function airdrop(bytes[] calldata proof, bytes32 leaf) external {
require(MerkleProof.verifyCalldata(proof, merkleRoot, leaf), "not verified");
require(!alreadyClaimed[leaf], "already claimed airdrop");
alreadyClaimed[leaf] = true;
mint(msg.sender, AIRDROP_AMOUNT);
}
}
यह कोड तीन कारणों से असुरक्षित है:
- कोई भी व्यक्ति जो उन एड्रेस को जानता है जिन्हें एयरड्रॉप के लिए चुना गया है, वह मर्कल ट्री (merkle tree) को फिर से बना सकता है और एक मान्य प्रमाण (valid proof) बना सकता है।
- leaf हैश्ड (hashed) नहीं है। एक हमलावर एक leaf सबमिट कर सकता है जो मर्कल रूट (merkle root) के बराबर है और require स्टेटमेंट को बायपास कर सकता है।
- भले ही उपरोक्त दो मुद्दों को ठीक कर दिया जाए, एक बार जब कोई वैध प्रमाण सबमिट करता है, तो उन्हें फ्रंटरन (frontrun) किया जा सकता है।
क्रिप्टोग्राफिक प्रूफ़्स (मर्कल ट्री, सिग्नेचर, आदि) को msg.sender से जोड़ा जाना चाहिए, जिसमें कोई हमलावर निजी कुंजी (private key) प्राप्त किए बिना हेरफेर नहीं कर सकता है।
Solidity अंतिम uint साइज़ में अपकास्ट (upcast) नहीं करता है
function limitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) {
product = a * b;
}
हालाँकि product एक uint256 वेरिएबल है, लेकिन गुणन (multiplication) का परिणाम 255 से बड़ा नहीं हो सकता है अन्यथा कोड रिवर्ट हो जाएगा।
प्रत्येक वेरिएबल को व्यक्तिगत रूप से अपकास्ट करके इस समस्या को कम किया जा सकता है।
function unlimitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) {
product = uint256(a) * uint256(b);
}
ऐसी स्थिति तब उत्पन्न हो सकती है जब किसी स्ट्रक्ट (struct) में पैक किए गए इंटिजर्स को गुणा किया जाता है। किसी स्ट्रक्ट में पैक किए गए छोटे मानों को गुणा करते समय आपको इसका ध्यान रखना चाहिए:
struct Packed {
uint8 time;
uint16 rewardRate
}
//...
Packed p;
p.time * p.rewardRate; // this might revert!
Solidity छुपके से कुछ लिटरल्स (literals) को uint8 बना देता है
निम्नलिखित कोड रिवर्ट हो जाएगा क्योंकि टर्नरी ऑपरेटर (ternary operator) यहाँ uint8 रिटर्न करता है।
function result(bool inp) external pure returns (uint256) {
return uint256(255) + (inp ? 1 : 0);
}
इसे रिवर्ट न करने के लिए, निम्नलिखित कार्य करें:
function result(bool inp) external pure returns (uint256) {
return uint256(255) + (inp ? uint256(1) : uint256(0));
}
आप इस ट्वीट थ्रेड में इस घटना के बारे में अधिक जान सकते हैं।
ओवरफ्लो होने पर Solidity का डाउनकास्टिंग रिवर्ट (revert) नहीं होता है
Solidity यह नहीं जाँचता है कि क्या किसी इंटिजर को छोटे इंटिजर में कास्ट (cast) करना सुरक्षित है। जब तक कि कोई बिज़नेस लॉजिक यह सुनिश्चित न करे कि डाउनकास्टिंग सुरक्षित है, तब तक SafeCast जैसी लाइब्रेरी का उपयोग किया जाना चाहिए।
function test(int256 value) public pure returns (int8) {
return int8(value + 1); // overflows and does not revert
}
स्टोरेज पॉइंटर्स पर लिखने से नया डेटा सेव नहीं होता है
यह कोड ऐसा लगता है कि यह myArray[1] के डेटा को myArray[0] में कॉपी करता है, लेकिन यह ऐसा नहीं करता है। यदि आप फंक्शन में अंतिम लाइन को कमेंट करते हैं, तो कंपाइलर कहेगा कि फंक्शन को view फंक्शन में बदला जाना चाहिए। foo पर लिखने से अंतर्निहित (underlying) स्टोरेज में कुछ नहीं लिखा जाता।
contract DoesNotWrite {
struct Foo {
uint256 bar;
}
Foo[] public myArray;
function moveToSlot0() external {
Foo storage foo = myArray[0];
foo = myArray[1]; // myArray[0] is unchanged
// we do this to make the function a state
// changing operation
// and silence the compiler warning
myArray[1] = Foo({bar: 100});
}
}
इसलिए स्टोरेज पॉइंटर्स पर न लिखें।
डायनेमिक डेटाटाइप्स वाले स्ट्रक्ट्स (structs) को डिलीट करने से डायनेमिक डेटा डिलीट नहीं होता है
यदि कोई मैपिंग (या डायनेमिक एरे) किसी स्ट्रक्ट के अंदर है, और स्ट्रक्ट को डिलीट कर दिया जाता है, तो मैपिंग या एरे डिलीट नहीं होगा।
एरे को डिलीट करने के अपवाद के साथ, delete कीवर्ड केवल एक स्टोरेज स्लॉट को डिलीट कर सकता है। यदि स्टोरेज स्लॉट में अन्य स्टोरेज स्लॉट के संदर्भ (references) हैं, तो उन्हें डिलीट नहीं किया जाएगा।
contract NestedDelete {
mapping(uint256 => Foo) buzz;
struct Foo {
mapping(uint256 => uint256) bar;
}
Foo foo;
function addToFoo(uint256 i) external {
buzz[i].bar[5] = 6;
}
function getFromFoo(uint256 i) external view returns (uint256) {
return buzz[i].bar[5];
}
function deleteFoo(uint256 i) external {
// internal map still holds the data in the
// mapping and array
delete buzz[i];
}
}
अब निम्नलिखित ट्रांजैक्शन अनुक्रम (sequence) करते हैं:
addToFoo(1)getFromFoo(1)6 रिटर्न करता हैdeleteFoo(1)getFromFoo(1)अभी भी 6 रिटर्न करता है!
याद रखें, Solidity में मैप्स कभी भी “खाली” (empty) नहीं होते हैं। इसलिए यदि कोई किसी ऐसे आइटम को एक्सेस करता है जिसे हटा दिया गया है, तो ट्रांजैक्शन रिवर्ट नहीं होगा बल्कि इसके बजाय उस डेटाटाइप के लिए शून्य मान (zero value) लौटाएगा।
ERC20 टोकन की समस्याएं
यदि आप केवल विश्वसनीय (trusted) ERC20 टोकन से निपटते हैं, तो इनमें से अधिकांश समस्याएं लागू नहीं होती हैं। हालाँकि, किसी मनमाने या आंशिक रूप से अविश्वसनीय ERC20 टोकन के साथ इंटरेक्ट करते समय, यहाँ कुछ बातों का ध्यान रखना चाहिए।
ERC20: ट्रांसफर पर शुल्क (Fee on transfer)
अविश्वसनीय टोकन से निपटते समय, आपको यह नहीं मानना चाहिए कि आपका बैलेंस आवश्यक रूप से उसी राशि (amount) से बढ़ जाएगा। यह संभव है कि कोई ERC20 टोकन अपने ट्रांसफर फंक्शन को इस प्रकार लागू करे:
contract ERC20 {
// internally called by transfer() and transferFrom()
// balance and approval checks happen in the caller
function _transfer(address from, address to, uint256 amount) internal returns (bool) {
fee = amount * 100 / 99;
balanceOf[from] -= to;
balanceOf[to] += (amount - fee);
balanceOf[TREASURY] += fee;
emit Transfer(msg.sender, to, (amount - fee));
return true;
}
}
यह टोकन हर ट्रांजैक्शन पर 1% टैक्स लगाता है। इसलिए यदि कोई स्मार्ट कॉन्ट्रैक्ट टोकन के साथ इस प्रकार इंटरैक्ट करता है, तो हमें या तो अप्रत्याशित रिवर्ट मिलेंगे या पैसे चोरी हो जाएंगे।
contract Stake {
mapping(address => uint256) public balancesInContract;
function stake(uint256 amount) public {
token.transferFrom(msg.sender, address(this), amount);
balancesInContract[msg.sender] += amount; // THIS IS WRONG!
}
function unstake() public {
uint256 toSend = balancesInContract[msg.sender];
delete balancesInContract[msg.sender];
// this could revert because toSend is 1% greater than
// the amount in the contract. Otherwise, 1% will be "stolen"
// from other depositors.
token.transfer(msg.sender, toSend);
}
}
ERC20: रिबेसिंग (rebasing) टोकन
रिबेसिंग टोकन को Olympus DAO के sOhm टोकन और Ampleforth के AMPL टोकन द्वारा लोकप्रिय बनाया गया था। Coingecko रिबेसिंग ERC20 टोकनों की एक सूची बनाए रखता है।
जब कोई टोकन रिबेस होता है, तो कुल सप्लाई (total supply) बदल जाती है और रिबेस की दिशा के आधार पर सभी का बैलेंस बढ़ या घट जाता है।
रिबेसिंग टोकन के साथ काम करते समय निम्नलिखित कोड के टूटने की संभावना है:
contract WillBreak {
mapping(address => uint256) public balanceHeld;
IERC20 private rebasingToken
function deposit(uint256 amount) external {
balanceHeld[msg.sender] = amount;
rebasingToken.transferFrom(msg.sender, address(this), amount);
}
function withdraw() external {
amount = balanceHeld[msg.sender];
delete balanceHeld[msg.sender];
// ERROR, amount might exceed the amount
// actually held by the contract
rebasingToken.transfer(msg.sender, amount);
}
}
कई कॉन्ट्रैक्ट्स का समाधान केवल रिबेसिंग टोकन को अस्वीकार करना है। हालाँकि, कोई अकाउंट का बैलेंस सेंडर को ट्रांसफर करने से पहले balanceOf(address(this)) की जांच करने के लिए ऊपर दिए गए कोड को संशोधित कर सकता है। तब यह तब भी काम करेगा जब बैलेंस बदल जाता है।
ERC20: ERC20 के वेश में ERC777
ERC20, यदि मानक (standard) के अनुसार लागू किया गया है, तो ERC20 टोकन में ट्रांसफर हुक नहीं होते हैं, और इस प्रकार transfer और transferFrom में रीएंट्रेंसी समस्या नहीं होती है।
ट्रांसफर हुक वाले टोकन के सार्थक लाभ हैं, यही वजह है कि सभी NFT मानक उन्हें लागू करते हैं, और क्यों ERC777 को अंतिम रूप दिया गया था। हालाँकि, इससे इतना भ्रम पैदा हुआ कि OpenZeppelin ने ERC777 लाइब्रेरी को हटा दिया (deprecated)।
यदि आप चाहते हैं कि आपका प्रोटोकॉल ऐसे टोकन के साथ संगत (compatible) हो जो ERC20 टोकन की तरह व्यवहार करते हैं लेकिन उनमें ट्रांसफर हुक होते हैं, तो यह केवल transfer और transferFrom कार्यों को इस तरह मानने की बात है जैसे कि वे रिसीवर को एक फंक्शन कॉल जारी करेंगे।
यह ERC777 री-एंट्रेंसी Uniswap के साथ हुई थी (यदि आप उत्सुक हैं तो OpenZeppelin ने इस एक्सप्लॉइट को यहाँ डॉक्यूमेंट किया है)।
ERC20: सभी ERC20 टोकन true रिटर्न नहीं करते हैं
ERC20 विनिर्देश (specification) निर्देशित करता है कि ट्रांसफर सफल होने पर एक ERC20 टोकन को true रिटर्न करना चाहिए। क्योंकि अधिकांश ERC20 कार्यान्वयन विफल नहीं हो सकते जब तक कि अनुमति (allowance) अपर्याप्त न हो या ट्रांसफर की गई राशि बहुत अधिक न हो, इसलिए अधिकांश डेव्स को ERC20 टोकन के रिटर्न मान (return value) को अनदेखा करने और यह मानने की आदत हो गई है कि विफल ट्रांसफर रिवर्ट हो जाएगा।
सच कहूं तो, अगर आप केवल उस विश्वसनीय ERC20 टोकन के साथ काम कर रहे हैं जिसका व्यवहार आप जानते हैं, तो यह महत्वपूर्ण नहीं है। लेकिन जब मनमाने ERC20 टोकन से निपटते हैं, तो व्यवहार में इस भिन्नता को ध्यान में रखा जाना चाहिए।
कई कॉन्ट्रैक्ट्स में एक अंतर्निहित अपेक्षा होती है कि विफल ट्रांसफर को हमेशा रिवर्ट होना चाहिए, false नहीं रिटर्न करना चाहिए क्योंकि अधिकांश ERC20 टोकन में false रिटर्न करने का कोई मैकेनिज्म नहीं होता है, इसलिए इससे बहुत भ्रम पैदा हुआ है।
इस मामले को और जटिल बनाते हुए कुछ ERC20 टोकन true रिटर्न करने के प्रोटोकॉल का पालन नहीं करते हैं, विशेष रूप से Tether। कुछ टोकन ट्रांसफर में विफलता पर रिवर्ट हो जाते हैं, जिससे कॉलर को रिवर्ट बबल अप (bubble up) हो जाएगा। इस प्रकार, कुछ लाइब्रेरीज़ रिवर्ट को रोकने और इसके बजाय बूलियन (boolean) रिटर्न करने के लिए ERC20 टोकन ट्रांसफर कॉल्स को रैप (wrap) करती हैं। यहाँ कुछ कार्यान्वयन दिए गए हैं:
OpenZeppelin SafeTransfer
Solady SafeTransfer (गैस के मामले में काफी अधिक कुशल)
ERC20: एड्रेस पॉइज़निंग (Address Poisoning)
यह कोई स्मार्ट कॉन्ट्रैक्ट वल्नरेबिलिटी नहीं है, लेकिन हम पूर्णता के लिए इसका यहां उल्लेख कर रहे हैं।
शून्य (zero) ERC20 टोकन ट्रांसफर करने की विनिर्देश द्वारा अनुमति है। यह फ्रंटएंड (frontend) एप्लिकेशन्स के लिए भ्रम पैदा कर सकता है, और संभावित रूप से उपयोगकर्ताओं को इस बारे में धोखा दे सकता है कि उन्होंने हाल ही में किसे टोकन भेजे थे। Metamask ने इस थ्रेड में इसके बारे में अधिक बताया है।
ERC20: पूरी तरह से रग्ड (Just flat out rugged)
(वेब3 की भाषा में “rugged” (रग्ड) का मतलब है “आपके पैरों के नीचे से गलीचा खींच लेना (धोखा देना)।”)
किसी को भी ERC20 टोकन में ऐसा फंक्शन जोड़ने से रोकने वाला कोई नहीं है जो उन्हें अपनी इच्छानुसार टोकन बनाने (create), ट्रांसफर करने और बर्न (burn) करने की सुविधा देता हो — या selfdestruct या अपग्रेड करने की सुविधा देता हो। इसलिए मौलिक रूप से, इसकी एक सीमा है कि कोई ERC20 टोकन कितना “अविश्वसनीय” (untrusted) हो सकता है।
लेंडिंग प्रोटोकॉल में लॉजिक बग्स
लेंडिंग और बॉरोइंग पर आधारित DeFi प्रोटोकॉल कैसे टूट सकते हैं, इस पर विचार करते समय, यह सोचना मददगार होता है कि सॉफ्टवेयर स्तर पर बग कैसे फैलते हैं और व्यावसायिक तर्क (business logic) स्तर को प्रभावित करते हैं। बॉन्ड कॉन्ट्रैक्ट बनाने और बंद करने के कई चरण होते हैं। विचार करने के लिए यहाँ कुछ अटैक वेक्टर्स (attack vectors) दिए गए हैं।
ऐसे तरीके जिनसे उधारदाताओं (lenders) को नुकसान होता है:
- ऐसे बग जो देय मूलधन (principal) को बिना कोई भुगतान किए कम करने (शायद शून्य तक) में सक्षम बनाते हैं।
- जब ऋण (loan) वापस नहीं किया जाता है या संपार्श्विक (collateral) थ्रेशोल्ड से नीचे चला जाता है, तो खरीदार का संपार्श्विक लिक्विडेट (liquidate) नहीं किया जा सकता है।
- यदि प्रोटोकॉल में ऋण स्वामित्व (debt ownership) को स्थानांतरित करने का कोई मैकेनिज्म है, तो यह उधारदाताओं से बॉन्ड चुराने का एक वेक्टर हो सकता है।
- ऋण मूलधन या भुगतान की देय तिथि (due date) को अनुचित रूप से बाद की तारीख में ले जाया जाता है।
ऐसे तरीके जिनसे उधारकर्ताओं (borrowers) को नुकसान होता है:
- एक बग जहाँ मूलधन चुकाने से मूलधन कम नहीं होता है।
- कोई बग या ग्रीफिंग अटैक उपयोगकर्ता को भुगतान करने से रोकता है।
- मूलधन या ब्याज दर (interest rate) अवैध रूप से बढ़ा दी जाती है।
- ओरेकल मैनिपुलेशन के कारण संपार्श्विक का अवमूल्यन (devaluation) होता है।
- ऋण मूलधन या भुगतान की देय तिथि को अनुचित रूप से पहले की तारीख में ले जाया जाता है।
यदि प्रोटोकॉल से संपार्श्विक (collateral) निकाल लिया जाता है, तो ऋणदाता और उधारकर्ता दोनों को नुकसान होता है, क्योंकि उधारकर्ता को ऋण चुकाने का कोई प्रोत्साहन नहीं होता है, और उधारकर्ता मूलधन खो देता है।
जैसा कि ऊपर देखा जा सकता है, एक DeFi प्रोटोकॉल के “हैक” होने के कई स्तर होते हैं, जो प्रोटोकॉल से भारी मात्रा में पैसे निकाले जाने (इस प्रकार की घटनाएँ जो आमतौर पर समाचारों में आती हैं) से कहीं अधिक हैं।
स्टेकिंग प्रोटोकॉल में कमजोरियां
समाचारों में आने वाले हैक्स स्टेकिंग प्रोटोकॉल के लाखों डॉलर के हैक होने के होते हैं, लेकिन जांच करने के लिए केवल यही एकमात्र समस्या नहीं है।
- क्या रिवॉर्ड्स के भुगतान में देरी हो सकती है, या इसका दावा बहुत जल्दी किया जा सकता है?
- क्या रिवॉर्ड्स को अनुचित रूप से कम या बढ़ाया जा सकता है? सबसे खराब स्थिति में, क्या उपयोगकर्ता को कोई भी रिवॉर्ड प्राप्त करने से रोका जा सकता है?
- क्या लोग उस मूलधन या रिवॉर्ड का दावा कर सकते हैं जो उनका नहीं है, सबसे खराब स्थिति में प्रोटोकॉल को खाली कर सकते हैं?
- क्या जमा की गई संपत्ति प्रोटोकॉल में फंस सकती है (आंशिक या पूरी तरह से) या निकासी (withdrawal) में अनुचित रूप से देरी हो सकती है?
- इसके विपरीत, यदि स्टेकिंग के लिए समय की प्रतिबद्धता (time commitment) की आवश्यकता होती है, तो क्या उपयोगकर्ता प्रतिबद्धता समय से पहले विथड्रॉ (withdraw) कर सकते हैं?
- यदि भुगतान किसी भिन्न संपत्ति या मुद्रा में है, तो क्या प्रश्नगत स्मार्ट कॉन्ट्रैक्ट के दायरे में इसके मूल्य में हेरफेर किया जा सकता है? यह तब प्रासंगिक होता है जब प्रोटोकॉल लिक्विडिटी प्रदाताओं या स्टेकर्स को पुरस्कृत करने के लिए अपने स्वयं के टोकन बनाता (mint) है।
- यदि मूलधन स्टेकिंग को खोने का एक अपेक्षित और प्रकट (disclosed) जोखिम तत्व है, तो क्या उस जोखिम में अनुचित रूप से हेरफेर किया जा सकता है?
- क्या प्रोटोकॉल के प्रमुख मापदंडों में एडमिन, केंद्रीकरण (centralization), या गवर्नेंस का जोखिम है?
खोजने के लिए प्रमुख क्षेत्र कोड के वे क्षेत्र हैं जो कोड के “money exit” (पैसा बाहर निकलने) वाले हिस्सों को छूते हैं।
खोजने के लिए एक “money entrance” (पैसा अंदर आने) की भेद्यता (vulnerability) भी है:
- क्या जिन उपयोगकर्ताओं को प्रोटोकॉल में स्टेकिंग एसेट्स में भाग लेने का अधिकार है, उन्हें अनुचित रूप से ऐसा करने से रोका जा सकता है?
उपयोगकर्ताओं को मिलने वाले रिवॉर्ड्स में एक निहित जोखिम-प्रतिफल प्रोफ़ाइल (risk-reward profile) और धन के प्रत्याशित समय-मूल्य (time-value of money) का पहलू होता है। यह स्पष्ट होना मददगार है कि वे धारणाएँ क्या हैं और प्रोटोकॉल को उम्मीदों से भटकने का कारण कैसे बनाया जा सकता है।
अनचेक्ड रिटर्न वैल्यूज़ (Unchecked return values)
बाहरी (external) स्मार्ट कॉन्ट्रैक्ट को कॉल करने के दो तरीके हैं: 1) इंटरफ़ेस परिभाषा (interface definition) के साथ फंक्शन को कॉल करना; 2) .call विधि का उपयोग करना। यह नीचे दर्शाया गया है:
contract A {
uint256 public x;
function setx(uint256 _x) external {
require(_x > 10, "x must be bigger than 10");
x = _x;
}
}
interface IA {
function setx(uint256 _x) external;
}
contract B {
function setXV1(IA a, uint256 _x) external {
a.setx(_x);
}
function setXV2(address a, uint256 _x) external {
(bool success, ) =
a.call(abi.encodeWithSignature("setx(uint256)", _x));
// success is not checked!
}
}
कॉन्ट्रैक्ट B में, setXV2 चुपचाप विफल हो सकता है यदि _x 10 से कम है। जब .call विधि के माध्यम से किसी फंक्शन को कॉल किया जाता है, तो कैली (callee) रिवर्ट हो सकता है, लेकिन पैरेंट (parent) रिवर्ट नहीं होगा। success के मान की जांच की जानी चाहिए और कोड के व्यवहार को तदनुसार ब्रांच किया जाना चाहिए।
लूप में msg.value
लूप के अंदर msg.value का उपयोग करना खतरनाक है क्योंकि इससे प्रेषक को msg.value का “पुन: उपयोग” (re-use) करने की अनुमति मिल सकती है।
यह payable मल्टीकॉल्स (multicalls) के साथ दिखाई दे सकता है। मल्टीकॉल्स उपयोगकर्ता को बार-बार 21,000 गैस ट्रांजैक्शन शुल्क के भुगतान से बचने के लिए ट्रांजैक्शन्स की एक सूची सबमिट करने में सक्षम बनाते हैं। हालाँकि, निष्पादित (execute) किए जाने वाले कार्यों के माध्यम से लूपिंग करते समय msg.value का “पुन: उपयोग” हो जाता है, जिससे उपयोगकर्ता को संभावित रूप से दोहरा खर्च (double spend) करने की क्षमता मिल जाती है।
यह Opyn Hack का मूल कारण था।
प्राइवेट वेरिएबल्स (Private Variables)
प्राइवेट वेरिएबल्स (Private variables) ब्लॉकचेन पर अभी भी दिखाई देते हैं, इसलिए संवेदनशील जानकारी को वहाँ कभी संग्रहीत (stored) नहीं किया जाना चाहिए। यदि वे एक्सेस करने योग्य नहीं होते, तो वैलिडेटर्स उन ट्रांजैक्शन्स को कैसे प्रोसेस कर पाते जो उनके मानों (values) पर निर्भर करते हैं? प्राइवेट वेरिएबल्स को बाहर के किसी Solidity कॉन्ट्रैक्ट से नहीं पढ़ा जा सकता है, लेकिन उन्हें एक Ethereum क्लाइंट का उपयोग करके ऑफ-चेन पढ़ा जा सकता है।
वेरिएबल को पढ़ने के लिए, आपको उसका स्टोरेज स्लॉट जानना होगा। निम्नलिखित उदाहरण में, myPrivateVar का स्टोरेज स्लॉट 0 है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PrivateVarExample {
uint256 private myPrivateVar;
constructor(uint256 _initialValue) {
myPrivateVar = _initialValue;
}
}
डिप्लॉय किए गए स्मार्ट कॉन्ट्रैक्ट के प्राइवेट वेरिएबल को पढ़ने के लिए javascript कोड यहां दिया गया है:
const Web3 = require("web3");
const PRIVATE_VAR_EXAMPLE_ADDRESS = "0x123..."; // Replace with your contract address
async function readPrivateVar() {
const web3 = new Web3("http://localhost:8545"); // Replace with your provider's URL
// Read storage slot 0 (where 'myPrivateVar' is stored)
const storageSlot = 0;
const privateVarValue = await web3.eth.getStorageAt(
PRIVATE_VAR_EXAMPLE_ADDRESS,
storageSlot
);
console.log("Value of private variable 'myPrivateVar':",
web3.utils.hexToNumberString(privateVarValue));
}
readPrivateVar();
Try Catch को सही तरीके से लागू करना मुश्किल है
जबकि एक low-level call केवल सफलता के आधार पर true या false लौटाएगा, एक try catch पैनिक (panic) और रिवर्ट (revert) के बीच अंतर करता है, और यदि रिटर्न डेटा को ठीक से पार्स नहीं किया जा सकता है तो यह चुपचाप विफल हो सकता है। इससे बचना और बस लो-लेवल कॉल्स का उपयोग करना सबसे अच्छा है।
असुरक्षित डेलीगेट कॉल (Insecure Delegate Call)
Delegatecall का उपयोग कभी भी अविश्वसनीय कॉन्ट्रैक्ट्स के साथ नहीं किया जाना चाहिए क्योंकि यह delegatecallee को सारा नियंत्रण सौंप देता है। इस उदाहरण में, अविश्वसनीय कॉन्ट्रैक्ट कॉन्ट्रैक्ट में मौजूद सभी ईथर चुरा लेता है।
contract UntrustedDelegateCall {
constructor() payable {
require(msg.value == 1 ether);
}
function doDelegateCall(address _delegate, bytes calldata data) public {
(bool ok, ) = _delegate.delegatecall(data);
require(ok, "delegatecall failed");
}
}
contract StealEther {
function steal() public {
// you could also selfdestruct here
// if you really wanted to be mean
(bool ok,) = tx.origin.call{value: address(this).balance}("");
require(ok);
}
function attack(address victim) public {
UntrustedDelegateCall(victim).doDelegateCall(
address(this),
abi.encodeWithSignature("steal()"));
}
}
प्रॉक्सी से संबंधित अपग्रेड बग्स
हम इस विषय के साथ एक ही सेक्शन में न्याय नहीं कर सकते। अधिकांश अपग्रेड बग्स को आमतौर पर OpenZeppelin के hardhat प्लगइन का उपयोग करके और यह किन समस्याओं से बचाता है, इसके बारे में पढ़कर टाला जा सकता है। (https://docs.openzeppelin.com/upgrades-plugins/1.x/)।
त्वरित सारांश के रूप में, स्मार्ट कॉन्ट्रैक्ट अपग्रेड से संबंधित समस्याएं यहां दी गई हैं:
- इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स (implementation contracts) के अंदर
selfdestructऔरdelegatecallका उपयोग नहीं किया जाना चाहिए। - यह ध्यान रखा जाना चाहिए कि अपग्रेड के दौरान स्टोरेज वेरिएबल कभी भी एक-दूसरे को ओवरराइट न करें।
- इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स में बाहरी (external) लाइब्रेरीज़ को कॉल करने से बचना चाहिए क्योंकि यह अनुमान लगाना संभव नहीं है कि वे स्टोरेज एक्सेस को कैसे प्रभावित करेंगे।
- डिप्लॉयर (deployer) को कभी भी इनिशियलाइज़ेशन फंक्शन को कॉल करना नहीं भूलना चाहिए।
- बेस कॉन्ट्रैक्ट (base contract) में नए वेरिएबल्स जोड़े जाने पर स्टोरेज टकराव (storage collision) को रोकने के लिए बेस कॉन्ट्रैक्ट्स में गैप (gap) वेरिएबल शामिल न करना (यह hardhat प्लगइन द्वारा स्वचालित रूप से संभाला जाता है)।
- इम्यूटेबल (immutable) वेरिएबल्स के मान (values) अपग्रेड्स के बीच संरक्षित (preserved) नहीं रहते हैं।
- कंस्ट्रक्टर (constructor) में कुछ भी करने को अत्यधिक हतोत्साहित (discouraged) किया जाता है क्योंकि भविष्य के अपग्रेड्स को संगतता (compatibility) बनाए रखने के लिए समान कंस्ट्रक्टर लॉजिक को पूरा करना होगा।
अत्यधिक शक्तिशाली एडमिन्स (Overpowered Admins)
सिर्फ इसलिए कि किसी कॉन्ट्रैक्ट का ओनर (owner) या एडमिन (admin) है, इसका मतलब यह नहीं है कि उनकी शक्ति असीमित होनी चाहिए। NFT पर विचार करें। यह उचित है कि केवल ओनर ही NFT बिक्री से होने वाली कमाई को निकाल सके, लेकिन कॉन्ट्रैक्ट को रोकने (transfers ब्लॉक करने) में सक्षम होने से तबाही मच सकती है यदि ओनर की प्राइवेट कीज़ से समझौता (compromised) किया जाता है। आम तौर पर, अनावश्यक जोखिम को कम करने के लिए एडमिनिस्ट्रेटर विशेषाधिकार (priviledges) यथासंभव न्यूनतम होने चाहिए।
कॉन्ट्रैक्ट ओनरशिप की बात करें तो…
Ownable के बजाय Ownable2Step का उपयोग करें
यह तकनीकी रूप से कोई भेद्यता (vulnerability) नहीं है, लेकिन OpenZeppelin ownable कॉन्ट्रैक्ट ओनरशिप के नुकसान का कारण बन सकता है यदि ओनरशिप किसी गैर-मौजूद एड्रेस पर ट्रांसफर की जाती है। Ownable2step के लिए रिसीवर को ओनरशिप की पुष्टि करने की आवश्यकता होती है। यह गलती से किसी गलत टाइप किए गए एड्रेस पर ओनरशिप भेजने से बचाता है।
राउंडिंग एरर्स (Rounding Errors)
Solidity में फ्लोट्स (floats) नहीं होते हैं, इसलिए राउंडिंग एरर्स (Rounding errors) अपरिहार्य (inevitable) हैं। डिज़ाइनर को इस बात के प्रति सचेत रहना चाहिए कि क्या सही काम ऊपर की ओर राउंड करना है या नीचे की ओर राउंड करना है, और किसके पक्ष में राउंडिंग होनी चाहिए।
विभाजन (Division) हमेशा अंत में किया जाना चाहिए। निम्नलिखित कोड स्टेबलकॉइन्स (stablecoins) के बीच गलत तरीके से परिवर्तित होता है जिनमें दशमलव (decimals) की संख्या अलग-अलग होती है। निम्नलिखित विनिमय तंत्र (exchange mechanism) एक उपयोगकर्ता को dai (जिसमें 18 दशमलव हैं) के लिए विनिमय करते समय थोड़ी मात्रा में USDC (जिसमें 6 दशमलव हैं) मुफ्त में लेने की अनुमति देता है। वेरिएबल daiToTake शून्य पर राउंड डाउन हो जाएगा, जिससे उपयोगकर्ता से गैर-शून्य usdcAmount के बदले कुछ भी नहीं लिया जाएगा।
contract Exchange {
uint256 private constant CONVERSION = 1e12;
function swapDAIForUSDC(uint256 usdcAmount) external pure returns (uint256 a) {
uint256 daiToTake = usdcAmount / CONVERSION;
conductSwap(daiToTake, usdcAmount);
}
}
फ्रंटरनिंग (Frontrunning)
Ethereum (और इसी तरह की चेन्स) के संदर्भ में फ्रंटरनिंग (Frontrunning) का अर्थ है एक पेंडिंग ट्रांजैक्शन का निरीक्षण करना और उच्च गैस मूल्य का भुगतान करके उससे पहले एक और ट्रांजैक्शन निष्पादित करना। यानी, हमलावर ट्रांजैक्शन के “सामने दौड़ (run in front)” चुका है। यदि ट्रांजैक्शन एक लाभदायक व्यापार (profitable trade) है, तो उच्च गैस मूल्य का भुगतान करने के अलावा ट्रांजैक्शन की पूरी तरह से प्रतिलिपि (copy) बनाना समझ में आता है। इस घटना को कभी-कभी MEV के रूप में जाना जाता है, जिसका अर्थ है माइनर एक्सट्रैक्टेबल वैल्यू (miner extractable value), लेकिन कभी-कभी अन्य संदर्भों में मैक्सिमल एक्सट्रैक्टेबल वैल्यू (maximal extractable value) होता है। ब्लॉक उत्पादकों (Block producers) के पास ट्रांजैक्शन्स को फिर से ऑर्डर करने और अपना स्वयं का सम्मिलित करने की असीमित शक्ति होती है, और ऐतिहासिक रूप से, Ethereum के प्रूफ़ ऑफ स्टेक पर जाने से पहले ब्लॉक उत्पादक माइनर्स थे, इसलिए यह नाम पड़ा।
फ्रंटरनिंग: असुरक्षित विथड्रॉल (Unprotected withdraw)
स्मार्ट कॉन्ट्रैक्ट से ईथर निकालना एक “लाभदायक व्यापार” माना जा सकता है। आप एक शून्य-लागत (शून्य गैस को छोड़कर) ट्रांजैक्शन करते हैं और शुरुआत की तुलना में अधिक क्रिप्टोकरेंसी के साथ समाप्त होते हैं।
contract UnprotectedWithdraw {
constructor() payable {
require(msg.value == 1 ether, "must create with 1 eth");
}
function unsafeWithdraw() external {
(bool ok, ) = msg.sender.call{value: address(this).value}("");
require(ok, "transfer failed").
}
}
यदि आप इस कॉन्ट्रैक्ट को डिप्लॉय करते हैं और विथड्रॉ करने का प्रयास करते हैं, तो एक फ्रंटरनर बॉट मेमपूल (mempool) में आपके “unsafeWithdraw” की कॉल को नोटिस करेगा और ईथर पहले प्राप्त करने के लिए इसकी प्रतिलिपि बनाएगा।
फ्रंटरनिंग: ERC4626 इन्फ्लेशन अटैक, फ्रंटरनिंग और राउंडिंग एरर का एक संयोजन
हमने अपने ERC4626 ट्यूटोरियल में ERC-4626 इन्फ्लेशन अटैक के बारे में गहराई से लिखा है। लेकिन इसका सार यह है कि एक ERC4626 कॉन्ट्रैक्ट किसी ट्रेडर द्वारा योगदान किए गए “एसेट्स” (assets) के प्रतिशत के आधार पर “शेयर” (share) टोकन वितरित करता है। मोटे तौर पर, यह इस प्रकार काम करता है:
function getShares(...) external {
// code
shares_received = assets_contributed / total_assets;
// more code
}
बेशक, कोई भी एसेट्स का योगदान नहीं करेगा और बदले में कोई शेयर प्राप्त नहीं करेगा, लेकिन वे भविष्यवाणी नहीं कर सकते कि ऐसा होगा यदि कोई शेयरों को प्राप्त करने के लिए व्यापार (trade) को फ्रंटरन करता है।
उदाहरण के लिए, वे 200 एसेट्स का योगदान करते हैं जब पूल में 20 होते हैं, तो उन्हें 100 शेयर मिलने की उम्मीद होती है। लेकिन अगर कोई 200 एसेट्स जमा करने के लिए ट्रांजैक्शन को फ्रंटरन करता है, तो फॉर्मूला 200 / 220 होगा, जो शून्य पर राउंड डाउन हो जाता है, जिससे पीड़ित को एसेट्स का नुकसान होता है और बदले में शून्य शेयर मिलते हैं।
फ्रंटरनिंग: ERC20 अप्रूवल (approval)
इसे संक्षेप में वर्णित करने के बजाय एक वास्तविक उदाहरण के साथ चित्रित करना सबसे अच्छा है:
- मान लीजिए Alice, Eve को 100 टोकन के लिए अप्रूवल (स्वीकृति) देती है। (Eve हमेशा दुष्ट व्यक्ति है, Bob नहीं, इसलिए हम परंपरा को बनाए रखेंगे)।
- Alice अपना मन बदल लेती है और Eve के अप्रूवल को 50 में बदलने के लिए एक ट्रांजैक्शन भेजती है।
- इससे पहले कि अप्रूवल को 50 में बदलने का ट्रांजैक्शन ब्लॉक में शामिल किया जाए, यह मेमपूल (mempool) में बैठता है जहाँ Eve इसे देख सकती है।
- Eve 50 के अप्रूवल को फ्रंटरन करने के लिए अपने 100 टोकन का दावा करने के लिए एक ट्रांजैक्शन भेजती है।
- 50 का अप्रूवल पास हो जाता है।
- Eve 50 टोकन एकत्र कर लेती है।
अब Eve के पास 100 या 50 के बजाय 150 टोकन हैं। अविश्वसनीय अप्रूवल से निपटते समय, इसका समाधान अप्रूवल को बढ़ाने या घटाने से पहले शून्य पर सेट करना है।
फ्रंटरनिंग: सैंडविच अटैक (Sandwich attacks)
किसी संपत्ति की कीमत खरीदने और बेचने के दबाव के जवाब में चलती है। यदि कोई बड़ा ऑर्डर मेमपूल में बैठा है, तो ट्रेडर्स के पास ऑर्डर की प्रतिलिपि बनाने लेकिन उच्च गैस मूल्य के साथ प्रोत्साहन (incentive) होता है। इस तरह, वे एसेट को खरीदते हैं, बड़े ऑर्डर को कीमत बढ़ाने देते हैं, फिर वे तुरंत बेच देते हैं। सेल ऑर्डर को कभी-कभी “बैक रनिंग” (backrunning) कहा जाता है। सेल ऑर्डर कम गैस मूल्य के साथ सेल ऑर्डर देकर किया जा सकता है ताकि अनुक्रम (sequence) इस तरह दिखे:
- फ्रंटरन बाय (buy)
- बड़ा बाय (buy)
- सेल (sell)
इस हमले के खिलाफ प्राथमिक रक्षा एक “स्लिपेज” (slippage) पैरामीटर प्रदान करना है। यदि “फ्रंटरन बाय” स्वयं कीमत को एक निश्चित सीमा से ऊपर धकेलता है, तो “बड़ा बाय” ऑर्डर रिवर्ट हो जाएगा जिससे फ्रंटरनर व्यापार पर विफल हो जाएगा। स्लिपेज से संबंधित अतिरिक्त बग्स और कमजोरियों (vulnerabilities) के लिए इस संसाधन को देखें।
इसे सैंडविच कहा जाता है, क्योंकि बड़ा बाय फ्रंटरन बाय और बैकरन सेल के बीच सैंडविच होता है। यह हमला बड़े सेल (sell) ऑर्डर के साथ भी काम करता है, बस विपरीत दिशा में।
फ्रंटरनिंग के बारे में अधिक जानें
फ्रंटरनिंग एक बहुत बड़ा विषय है। Flashbots ने इस विषय पर बड़े पैमाने पर शोध किया है और इसके नकारात्मक बाह्यताओं (externalities) को कम करने में मदद के लिए कई टूल और शोध लेख प्रकाशित किए हैं। क्या उचित ब्लॉकचेन आर्किटेक्चर के साथ फ्रंटरनिंग को “डिज़ाइन से दूर” (designed away) किया जा सकता है, यह बहस का विषय है जिसे निर्णायक रूप से तय नहीं किया गया है। निम्नलिखित दो लेख इस विषय पर स्थायी क्लासिक्स हैं:
Ethereum is a dark forest
Escaping the dark forest
सिग्नेचर से संबंधित (Signature Related)
स्मार्ट कॉन्ट्रैक्ट्स के संदर्भ में डिजिटल सिग्नेचर्स के दो उपयोग हैं:
- एड्रेसेस (addresses) को वास्तविक ट्रांजैक्शन किए बिना ब्लॉकचेन पर कुछ ट्रांजैक्शन अधिकृत (authorize) करने में सक्षम बनाना।
- एक स्मार्ट कॉन्ट्रैक्ट को साबित करना कि सेंडर (sender) के पास एक पूर्वनिर्धारित (predefined) एड्रेस के अनुसार कुछ करने का अधिकार है।
यहाँ उपयोगकर्ता को NFT मिंट करने का विशेषाधिकार देने के लिए सुरक्षित रूप से डिजिटल सिग्नेचर्स का उपयोग करने का एक उदाहरण दिया गया है:
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract NFT is ERC721("name", "symbol") {
function mint(bytes calldata signature) external {
address recovered = keccak256(abi.encode(msg.sender)).toEthSignedMessageHash().recover(signature);
require(recovered == authorizer, "signature does not match");
}
}
एक क्लासिक उदाहरण ERC20 में अप्रूव (Approve) कार्यक्षमता है। किसी एड्रेस को हमारे खाते से निश्चित मात्रा में टोकन निकालने की मंजूरी देने के लिए, हमें एक वास्तविक Ethereum ट्रांजैक्शन करना होगा, जिसमें गैस खर्च होती है।
प्राप्तकर्ता (recipient) को ऑफ-चेन डिजिटल सिग्नेचर पास करना कभी-कभी अधिक कुशल होता है, फिर प्राप्तकर्ता यह साबित करने के लिए स्मार्ट कॉन्ट्रैक्ट को सिग्नेचर प्रदान करता है कि वे ट्रांजैक्शन करने के लिए अधिकृत थे।
ERC20Permit डिजिटल सिग्नेचर के साथ अप्रूवल सक्षम करता है। फंक्शन का वर्णन इस प्रकार किया गया है:
function permit(address owner,
address spender,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public
वास्तविक अप्रूवल ट्रांजैक्शन भेजने के बजाय, ओनर (owner) स्पेंडर (spender) के लिए अप्रूवल को “हस्ताक्षरित” (sign) कर सकता है (एक समय सीमा (deadline) के साथ)। इसके बाद अनुमोदित स्पेंडर (approved spender) प्रदान किए गए मापदंडों के साथ permit फंक्शन को कॉल कर सकता है।
सिग्नेचर की संरचना (Anatomy of a signature)
आप v, r, और s वेरिएबल्स को अक्सर देखेंगे। इन्हें solidity में क्रमशः uint8, bytes32, और bytes32 डेटाटाइप्स के साथ दर्शाया जाता है। कभी-कभी, सिग्नेचर्स को 65 बाइट एरे के रूप में दर्शाया जाता है जो कि abi.encodePacked(r, s, v) के रूप में एक साथ जुड़े हुए ये सभी मान हैं;
सिग्नेचर के अन्य दो आवश्यक घटक (components) मैसेज हैश (32 बाइट्स) और साइनिंग एड्रेस हैं। अनुक्रम (sequence) इस तरह दिखता है:
- एक पब्लिक एड्रेस (
ethAddress) जनरेट करने के लिए प्राइवेट की (privKey) का उपयोग किया जाता है। - एक स्मार्ट कॉन्ट्रैक्ट
ethAddressको पहले से स्टोर करता है। - एक ऑफचेन (offchain) उपयोगकर्ता एक मैसेज को हैश करता है और हैश पर हस्ताक्षर करता है। यह जोड़ी
msgHashऔरsignature (r, s, v)उत्पन्न करता है। - स्मार्ट कॉन्ट्रैक्ट एक संदेश प्राप्त करता है,
msgHashका उत्पादन करने के लिए इसे हैश करता है, फिर यह देखने के लिए कि कौन सा एड्रेस बाहर आता है, इसे(r, s, v)के साथ जोड़ता है। - यदि एड्रेस
ethAddressसे मेल खाता है, तो सिग्नेचर वैध (valid) है (कुछ मान्यताओं के तहत जिन्हें हम जल्द ही देखेंगे!)
स्मार्ट कॉन्ट्रैक्ट्स जिसे हमने संयोजन (combination) कहा था उसे करने और एड्रेस वापस पाने के लिए चरण 4 में प्रीकंपाइल्ड कॉन्ट्रैक्ट ecrecover का उपयोग करते हैं।
इस प्रक्रिया में कई कदम हैं जहां चीजें गलत हो सकती हैं।
सिग्नेचर्स: ecrecover address(0) रिटर्न करता है और एड्रेस अमान्य होने पर रिवर्ट नहीं करता है
इससे वल्नरेबिलिटी हो सकती है यदि किसी अनइनिशियलाइज्ड (uninitialized) वेरिएबल की तुलना ecrecover के आउटपुट से की जाती है।
यह कोड असुरक्षित है:
contract InsecureContract {
address signer;
// defaults to address(0)
// who lets us give the beneficiary the airdrop without them
// spending gas
function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
// ecrecover returns address(0) if the signature is invalid
require(signer == ecrecover(keccak256(abi.encode(who, amount)), v, r, s), "invalid signature");
mint(msg.sender, AIRDROP_AMOUNT);
}
}
सिग्नेचर रीप्ले (Signature replay)
सिग्नेचर रीप्ले तब होता है जब कोई कॉन्ट्रैक्ट ट्रैक नहीं करता है कि किसी सिग्नेचर का पहले उपयोग किया गया है या नहीं। निम्नलिखित कोड में, हम पिछली समस्या को ठीक करते हैं, लेकिन यह अभी भी सुरक्षित नहीं है।
contract InsecureContract {
address signer;
function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s);
require(recovered != address(0), "invalid signature");
require(recovered == signer, "recovered signature not equal signer");
mint(msg.sender, amount);
}
}
लोग जितनी बार चाहें एयरड्रॉप का दावा कर सकते हैं!
हम निम्नलिखित पंक्तियाँ जोड़ सकते हैं:
bytes memory signature = abi.encodePacked(v, r, s);
require(!used[signature], "signature already used");
// mapping(bytes => bool);
used[signature] = true;
अफ़सोस, कोड अभी भी सुरक्षित नहीं है!
सिग्नेचर मैलाबिलिटी (Signature malleability)
एक वैध (valid) सिग्नेचर को देखते हुए, एक हमलावर एक अलग सिग्नेचर प्राप्त करने के लिए कुछ त्वरित अंकगणित (arithmetic) कर सकता है। फिर हमलावर इस संशोधित (modified) सिग्नेचर को “रीप्ले” (replay) कर सकता है। लेकिन पहले, आइए कुछ कोड प्रदान करें जो प्रदर्शित करता है कि हम एक वैध सिग्नेचर के साथ शुरू कर सकते हैं, इसे संशोधित कर सकते हैं, और दिखा सकते हैं कि नया सिग्नेचर अभी भी पास हो जाता है।
contract Malleable {
// v = 28
// r = 0xf8479d94c011613baeffe9239e4ff65e2adbac744c34217ca7d51378e72c5204
// s = 0x57af17590a914b759c45aaeabaf513d5ef72d7da1bdd19d9f2e1bc371ece5b86
// m = 0x0000000000000000000000000000000000000000000000000000000000000003
function foo(bytes calldata msg, uint8 v, bytes32 r, bytes32 s) public pure returns (address, address){
bytes32 h = keccak256(msg);
address a = ecrecover(h, v, r, s);
// The following is math magic to invert the
// signature and create a valid one
// flip s
bytes32 s2 = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint256(s));
// invert v
uint8 v2;
require(v == 27 || v == 28, "invalid v");
v2 = v == 27 ? 28 : 27;
address b = ecrecover(h, v2, r, s2);
assert(a == b);
// different signatures, same address!;
return (a, b);
}
}
इस प्रकार, हमारा रनिंग उदाहरण अभी भी असुरक्षित है। एक बार जब कोई वैध सिग्नेचर प्रस्तुत करता है, तो इसकी मिरर इमेज सिग्नेचर (mirror image signature) तैयार की जा सकती है और उपयोग किए गए सिग्नेचर चेक को बायपास किया जा सकता है।
contract InsecureContract {
address signer;
function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s);
require(recovered != address(0), "invalid signature");
require(recovered == signer, "recovered signature not equal signer");
bytes memory signature = abi.encodePacked(v, r, s);
require(!used[signature], "signature already used"); // this can be bypassed
used[signature] = true;
mint(msg.sender, amount);
}
}
सुरक्षित सिग्नेचर्स (Secure signatures)
आप शायद इस बिंदु पर कुछ सुरक्षित सिग्नेचर कोड चाह रहे होंगे, है ना? हम आपको solidity में सिग्नेचर बनाने और उन्हें foundry में टेस्ट करने पर हमारे ट्यूटोरियल का संदर्भ देते हैं। लेकिन यहाँ चेकलिस्ट है:
- मैलाबिलिटी हमलों (malleability attacks) और शून्य (zero) मुद्दों को रोकने के लिए openzeppelin की लाइब्रेरी का उपयोग करें।
- पासवर्ड के रूप में सिग्नेचर्स का उपयोग न करें। संदेशों में ऐसी जानकारी होनी चाहिए जिसका हमलावर आसानी से पुनः उपयोग नहीं कर सकते (उदा. msg.sender)।
- आप जो साइन कर रहे हैं उसे ऑन-चेन हैश (Hash) करें।
- रीप्ले हमलों को रोकने के लिए एक नॉन्स (nonce) का उपयोग करें। बेहतर होगा कि EIP712 का पालन करें ताकि उपयोगकर्ता देख सकें कि वे किस पर हस्ताक्षर कर रहे हैं और आप कॉन्ट्रैक्ट्स और विभिन्न चेन्स के बीच सिग्नेचर्स के पुनः उपयोग को रोक सकें।
उचित सुरक्षा उपायों के बिना सिग्नेचर जाली बनाए जा सकते हैं या तैयार किए जा सकते हैं
ऊपर दिए गए हमले को और अधिक सामान्यीकृत किया जा सकता है यदि ऑन चेन हैशिंग नहीं की जाती है। उपरोक्त उदाहरणों में, हैशिंग स्मार्ट कॉन्ट्रैक्ट में की गई थी, इसलिए उपरोक्त उदाहरण निम्नलिखित शोषण (exploit) के प्रति असुरक्षित नहीं हैं।
आइए सिग्नेचर्स को रिकवर करने वाले कोड को देखें:
// this code is vulnerable!
function recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public returns (address signer) {
require(signer == ecrecover(hash, v, r, s), "signer does not match");
// more actions
}
उपयोगकर्ता हैश और सिग्नेचर दोनों की आपूर्ति करता है। यदि हमलावर ने पहले ही हस्ताक्षरकर्ता से एक वैध सिग्नेचर देख लिया है, तो वे किसी अन्य संदेश के हैश और सिग्नेचर का पुन: उपयोग कर सकते हैं।
यही कारण है कि संदेश को स्मार्ट कॉन्ट्रैक्ट में हैश करना बहुत महत्वपूर्ण है, ऑफ-चेन नहीं।
इस शोषण को क्रियान्वित होते देखने के लिए, ट्विटर पर पोस्ट किए गए CTF को देखें।
Original Challenge:
Part 1: https://twitter.com/RareSkills_io/status/1650869999266037760
Part 2: https://twitter.com/RareSkills_io/status/1650897671543197701
Solutions:
https://twitter.com/RareSkills_io/status/1651527648676573185 https://twitter.com/RareSkills_io/status/1651224817465540611
पहचानकर्ता (identifiers) के रूप में सिग्नेचर्स
उपयोगकर्ताओं की पहचान करने के लिए सिग्नेचर्स का उपयोग नहीं किया जाना चाहिए। मैलाबिलिटी के कारण, उन्हें अद्वितीय (unique) नहीं माना जा सकता है। Msg.sender में बहुत मजबूत विशिष्टता (uniqueness) की गारंटी होती है।
कुछ Solidity कंपाइलर वर्ज़न्स में बग होते हैं
ट्विटर पर हमारे द्वारा होस्ट किया गया एक सुरक्षा अभ्यास यहाँ देखें। कोडबेस (codebase) को ऑडिट करते समय, Solidity पेज पर रिलीज़ घोषणाओं के विरुद्ध Solidity संस्करण (version) की जांच करें ताकि यह देखा जा सके कि कोई बग मौजूद तो नहीं है।
यह मान लेना कि स्मार्ट कॉन्ट्रैक्ट्स इम्यूटेबल (immutable) हैं
प्रॉक्सी पैटर्न (Proxy Pattern) (या बहुत कम मेटामोर्फिक पैटर्न) के साथ स्मार्ट कॉन्ट्रैक्ट्स को अपग्रेड किया जा सकता है। स्मार्ट कॉन्ट्रैक्ट्स को अपरिवर्तित (unchanged) रहने के लिए मनमाने (arbitrary) स्मार्ट कॉन्ट्रैक्ट की कार्यक्षमता (functionality) पर निर्भर नहीं होना चाहिए।
मल्टी-सिग्नेचर वॉलेट्स के साथ Transfer() और send() ब्रेक हो सकते हैं
solidity फ़ंक्शंस transfer और send का उपयोग नहीं किया जाना चाहिए। वे जानबूझकर ट्रांजैक्शन के साथ फॉरवर्ड की गई गैस की मात्रा को 2,300 तक सीमित करते हैं, जिससे अधिकांश ऑपरेशन्स गैस से बाहर (run out of gas) हो जाएंगे।
आमतौर पर इस्तेमाल किया जाने वाला gnosis safe मल्टी-सिग्नेचर वॉलेट fallback फंक्शन में किसी अन्य एड्रेस पर कॉल को फॉरवर्ड करने का समर्थन करता है। यदि कोई मल्टीसिग वॉलेट में ईथर भेजने के लिए transfer या send का उपयोग करता है, तो fallback फंक्शन गैस से बाहर हो सकता है और ट्रांसफर विफल हो सकता है। gnosis safe fallback फंक्शन का स्क्रीनशॉट नीचे दिया गया है। पाठक स्पष्ट रूप से देख सकते हैं कि 2300 गैस का उपयोग करने के लिए पर्याप्त से अधिक ऑपरेशन्स हैं।
// @notice Forwards all calls to the fallback handler if set. Returns 0 if no handler is set.
// @dev Appends the non-padded caller address to the calldata to be optionally used in the handler
// The handler can make use of 'HandlerContext.sol' to extract the address.
// This is done because in the next call frame the `msg.sender` will be FallbackManager's address
// and having the original caller address may enable additional verification scenarios.
// @solhint-disable-next-line payable-fallback, no-complex-fallback
fallback() external {
bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT;
// @solhint-disable-next-line no-inline-assembly
assembly {
let handler := sload(slot)
if iszero(handler) {
return(0, 0)
}
calldatacopy(0, 0, calldatasize())
// The msg.sender address is shifted to the left by 12 bytes to remove the padding
// Then the address without padding is stored right after the calldata
mstore(calldatasize(), shl(96, caller()))
// Add 20 bytes for the address appended add the end
let success := call(gas(), handler, 0, 0, add(calldatasize(), 20), 0, 0)
returndatacopy(0, 0, returndatasize())
if iszero(success) {
revert(0, returndatasize())
}
return(0, returndatasize())
}
}
Gnosis Safe Fallback Function
यदि आपको किसी ऐसे कॉन्ट्रैक्ट के साथ इंटरेक्ट करने की आवश्यकता है जो transfer और send का उपयोग करता है, तो Ethereum access list transactions पर हमारा लेख देखें जो आपको स्टोरेज और कॉन्ट्रैक्ट एक्सेस ऑपरेशन्स की गैस लागत को कम करने की अनुमति देता है।
क्या अरिथमेटिक ओवरफ्लो अभी भी प्रासंगिक है?
Solidity 0.8.0 में ओवरफ्लो और अंडरफ्लो सुरक्षा (overflow and underflow protection) अंतर्निहित (built in) है। इसलिए जब तक कोई unchecked ब्लॉक मौजूद न हो, या Yul में लो लेवल कोड का उपयोग न किया जाए, ओवरफ्लो का कोई खतरा नहीं है। इस प्रकार, SafeMath लाइब्रेरीज़ का उपयोग नहीं किया जाना चाहिए क्योंकि वे अतिरिक्त जाँचों पर गैस बर्बाद करते हैं।
block.timestamp के बारे में क्या?
कुछ साहित्य प्रलेखित करते हैं कि block.timestamp एक भेद्यता वेक्टर (vulnerability vector) है क्योंकि माइनर्स इसमें हेरफेर कर सकते हैं। यह आमतौर पर रैंडमनेस के स्रोत के रूप में टाइमस्टैम्प का उपयोग करने पर लागू होता है, जो वैसे भी नहीं किया जाना चाहिए जैसा कि पहले प्रलेखित किया गया है। पोस्ट-मर्ज Ethereum ठीक 12 सेकंड (या 12 सेकंड के गुणक) अंतराल में टाइमस्टैम्प अपडेट करता है। हालाँकि, सेकंड-स्तर की ग्रैन्युलैरिटी (granularity) में समय मापना एक एंटी-पैटर्न है। एक मिनट के पैमाने पर, त्रुटि (error) की काफी गुंजाइश होती है यदि कोई वैलिडेटर अपना ब्लॉक स्लॉट चूक जाता है और ब्लॉक उत्पादन में 24 सेकंड का अंतराल होता है।
कॉर्नर केसेस, एज केसेस और ऑफ-बाय-वन एरर्स
कॉर्नर केसेस (Corner cases) को आसानी से परिभाषित नहीं किया जा सकता है, लेकिन एक बार जब आप उनमें से पर्याप्त देख लेते हैं, तो आप उनके लिए एक अंतर्ज्ञान (intuition) विकसित करना शुरू कर देते हैं। कॉर्नर केस ऐसा हो सकता है जैसे कोई रिवॉर्ड का दावा करने की कोशिश कर रहा हो, लेकिन उसके पास कुछ भी स्टेक (stake) पर न हो। यह वैध (valid) है, हमें बस उन्हें शून्य रिवॉर्ड देना चाहिए। इसी तरह, हम आम तौर पर रिवॉर्ड्स को समान रूप से विभाजित करना चाहते हैं, लेकिन क्या होगा यदि केवल एक प्राप्तकर्ता (recipient) है, और तकनीकी रूप से कोई विभाजन (division) नहीं होना चाहिए?
कॉर्नर केस: उदाहरण 1
यह उदाहरण Akshay Srivastav के ट्विटर थ्रेड से लिया गया है और संशोधित किया गया है। उस मामले पर विचार करें जहाँ कोई एक विशेषाधिकार प्राप्त (privileged) कार्रवाई कर सकता है यदि विशेषाधिकार प्राप्त पतों (addresses) का एक सेट इसके लिए सिग्नेचर प्रदान करता है।
contract VulnerableMultisigAuthorization {
struct Authorization {
bytes signature;
address authorizer;
bytes32 hashOfAction;
// more fields
}
// more code
function takeAction(Authorization[] calldata auths, bytes calldata action) public {
// logic for avoiding replay attacks
for (uint256 i; i < auths.length; ++i) {
require(validateSignature(auths[i].signature, auths[i].authorizer), "invalid signature");
require(authorizers[auths[i].authorizer], "address is not an authorizer");
}
doTheAction(action)
}
}
यदि कोई भी सिग्नेचर वैध (valid) नहीं है, या सिग्नेचर किसी वैध एड्रेस से मेल नहीं खाते हैं, तो रिवर्ट (revert) होगा। लेकिन क्या होगा अगर एरे खाली हो? उस मामले में, यह किसी भी सिग्नेचर की आवश्यकता के बिना सीधे doTheAction पर जंप कर जाएगा।
ऑफ-बाय-वन (Off-By-One): उदाहरण 2
contract ProportionalRewards {
mapping(address => uint256) originalId;
address[] stakers;
function stake(uint256 id) public {
nft.transferFrom(msg.sender, address(this), id);
stakers.append(msg.sender);
}
function unstake(uint256 id) public {
require(originalId[id] == msg.sender, "not the owner");
removeFromArray(msg.sender, stakers);
sendRewards(msg.sender, totalRewardsSinceLastclaim() / stakers.length());
nft.transferFrom(address(this), msg.sender, id);
}
}
यद्यपि ऊपर दिया गया कोड सभी फंक्शन इम्प्लीमेंटेशन्स को नहीं दिखाता है, भले ही फंक्शन्स उनके नामों के वर्णन के अनुसार व्यवहार करते हों, फिर भी एक बग है। क्या आप इसे पहचान सकते हैं? नीचे स्क्रॉल करने से पहले उत्तर न देखने के लिए आपको कुछ जगह देने के लिए यहाँ एक चित्र दिया गया है।

स्क्रॉल करना बंद करें
removeFromArray और sendRewards फंक्शन गलत क्रम (wrong order) में हैं। यदि stakers एरे में केवल एक उपयोगकर्ता है, तो डिवाइड बाय ज़ीरो (divide by zero) त्रुटि (error) होगी, और उपयोगकर्ता अपना NFT वापस नहीं ले पाएगा। इसके अलावा, रिवॉर्ड्स शायद उस तरह से विभाजित नहीं होते हैं जैसा लेखक चाहता है। यदि मूल रूप से चार स्टेकर्स (stakers) थे, और एक व्यक्ति विथड्रॉ करता है, तो उसे रिवॉर्ड्स का एक तिहाई हिस्सा मिलेगा क्योंकि विथड्रॉल के समय एरे की लंबाई 3 है।
कॉर्नर केस उदाहरण 3: Compound Finance Reward Miscalculation
आइए एक वास्तविक उदाहरण का उपयोग करें जिसने कुछ अनुमानों के अनुसार $100 मिलियन डॉलर से अधिक का नुकसान किया। चिंता न करें यदि आप Compound प्रोटोकॉल को पूरी तरह से नहीं समझते हैं, हम केवल प्रासंगिक भागों पर ध्यान केंद्रित करेंगे। (इसके अलावा Compound प्रोटोकॉल DeFi के इतिहास में सबसे महत्वपूर्ण और परिणामी प्रोटोकॉल में से एक है, हम इसे अपने DeFi बूटकैंप में पढ़ाते हैं, इसलिए यदि यह प्रोटोकॉल के बारे में आपकी पहली धारणा है, तो गुमराह न हों)।
वैसे भी, Compound का उद्देश्य उपयोगकर्ताओं को अन्य ट्रेडर्स (traders) को उनकी निष्क्रिय (idle) क्रिप्टोकरेंसी उधार देने के लिए पुरस्कृत करना है, जिन्हें इसका उपयोग हो सकता है। ऋणदाताओं (lenders) को ब्याज और COMP टोकन दोनों में भुगतान किया जाता है (उधारकर्ता COMP टोकन रिवॉर्ड का भी दावा कर सकते हैं, लेकिन हम अभी उस पर ध्यान केंद्रित नहीं करेंगे)।
Compound Comptroller एक प्रॉक्सी कॉन्ट्रैक्ट है जो कॉल को ऐसे कार्यान्वयन (implementations) को सौंपता है जिन्हें Compound Governance द्वारा सेट किया जा सकता है।
30 सितंबर, 2021 को गवर्नेंस प्रस्ताव 62 (proposal 62) पर, कार्यान्वयन कॉन्ट्रैक्ट (implementation contract) को एक कार्यान्वयन कॉन्ट्रैक्ट में सेट किया गया था जिसमें भेद्यता (vulnerability) थी। उसी दिन यह लाइव हो गया, यह देखा गया कि कुछ ट्रांजैक्शन्स शून्य टोकन स्टेक करने के बावजूद COMP रिवॉर्ड्स का दावा कर रहे थे।
कमजोर (vulnerable) फंक्शन distributeSupplierComp()
यहाँ मूल कोड है:
/**
* @notice Calculate COMP accrued by a supplier and possibly transfer it to them
* @param cToken The market in which the supplier is interacting
* @param supplier The address of the supplier to distribute COMP to
*/
function distributeSupplierComp(address cToken, address supplier) internal {
// TODO: Don't distribute supplier COMP if the user is not in the supplier market.
// This check should be as gas efficient as possible as distributeSupplierComp is called in many places.
// - We really don't want to call an external contract as that's quite expensive.
CompMarketState storage supplyState = compSupplyState[cToken];
uint supplyIndex = supplyState.index;
uint supplierIndex = compSupplierIndex[cToken][supplier];
// Update supplier's index to the current index since we are distributing accrued COMP
compSupplierIndex[cToken][supplier] = supplyIndex;
if (supplierIndex == 0 && supplyIndex > compInitialIndex) {
// Covers the case where users supplied tokens before the market's supply state index was set.
// Rewards the user with COMP accrued from the start of when supplier rewards were first
// set for the market.
supplierIndex = compInitialIndex;
}
// Calculate change in the cumulative sum of the COMP per cToken accrued
Double memory deltaIndex = Double({mantissa: sub_(supplyIndex, supplierIndex)});
uint supplierTokens = CToken(cToken).balanceOf(supplier);
// Calculate COMP accrued: cTokenAmount * accruedPerCToken
uint supplierDelta = mul_(supplierTokens, deltaIndex);
uint supplierAccrued = add_(compAccrued[supplier], supplierDelta);
compAccrued[supplier] = supplierAccrued;
emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex);
}
विडंबना यह है कि बग TODO कमेंट में है। “यदि उपयोगकर्ता सप्लायर मार्केट में नहीं है तो सप्लायर COMP वितरित न करें (Don’t distribute supplier COMP if the user is not in the supplier market.)” लेकिन इसके लिए कोड में कोई जांच (check) नहीं है। जब तक उपयोगकर्ता के बटुए (CToken(cToken).balanceOf(supplier);) में स्टेकिंग टोकन होता है, तब तक…
प्रस्ताव 64 (Proposal 64) ने 9 अक्टूबर, 2021 को बग को ठीक कर दिया।
यद्यपि इसे एक इनपुट वैलिडेशन बग कहा जा सकता है, उपयोगकर्ताओं ने कोई दुर्भावनापूर्ण चीज़ सबमिट नहीं की थी। यदि कोई बिना कुछ स्टेक किए रिवॉर्ड का दावा करने का प्रयास करता है, तो सही गणना शून्य होनी चाहिए। यकीनन, यह बिज़नेस लॉजिक या कॉर्नर केस एरर अधिक है।
वास्तविक दुनिया के हैक्स (Real World Hacks)
वास्तविक दुनिया में होने वाले DeFi हैक्स अक्सर ऊपर दी गई अच्छी श्रेणियों में नहीं आते हैं।
Pairity Wallet Freeze (नवंबर 2017)
parity वॉलेट का सीधे उपयोग करने का इरादा नहीं था। यह एक संदर्भ कार्यान्वयन (reference implementation) था जिसे स्मार्ट कॉन्ट्रैक्ट क्लोन इंगित करेंगे। कार्यान्वयन ने आवश्यकता होने पर क्लोन को selfdestruct करने की अनुमति दी, लेकिन इसके लिए सभी वॉलेट मालिकों को इस पर हस्ताक्षर (sign off) करने की आवश्यकता थी।
// throw unless the contract is not yet initialized.
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
वॉलेट ओनर (owners) घोषित किए गए हैं:
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
कुछ साहित्य इसे “असुरक्षित selfdestruct” यानी एक्सेस कंट्रोल विफलता के रूप में वर्णित करते हैं, लेकिन यह पूरी तरह से सटीक नहीं है। समस्या यह थी कि इम्प्लीमेंटेशन कॉन्ट्रैक्ट (implementation contract) पर initWallet फंक्शन को कॉल नहीं किया गया था और इससे किसी को भी खुद से initWallet फंक्शन को कॉल करने और खुद को ओनर बनाने की अनुमति मिल गई। इससे उन्हें kill फंक्शन को कॉल करने का अधिकार मिल गया। मूल कारण यह था कि कार्यान्वयन को इनिशियलाइज़ (initialize) नहीं किया गया था। इसलिए, बग दोषपूर्ण solidity कोड के कारण नहीं, बल्कि दोषपूर्ण डिप्लॉयमेंट प्रक्रिया के कारण पेश किया गया था।
Badger DAO हैक (दिसंबर 2021)
इस हैक में किसी भी Solidity कोड का शोषण नहीं किया गया था। इसके बजाय, हमलावरों ने Cloudflare API कुंजी (key) प्राप्त की और वेबसाइट फ्रंटएंड (frontend) में एक स्क्रिप्ट इंजेक्ट की जिसने उपयोगकर्ता के ट्रांजैक्शन्स को बदल दिया ताकि विथड्रॉल्स को हमलावर के एड्रेस पर निर्देशित किया जा सके। इस लेख में अधिक पढ़ें।
वॉलेट्स के लिए अटैक वेक्टर्स
अपर्याप्त रैंडमनेस वाली प्राइवेट कीज़
बहुत सारे अग्रणी शून्यों (leading zeros) वाले एड्रेसेस को खोजने की प्रेरणा यह है कि वे उपयोग करने के लिए गैस के प्रति अधिक कुशल (gas efficient) हैं। एक Ethereum ट्रांजैक्शन पर ट्रांजैक्शन डेटा में शून्य बाइट (zero byte) के लिए 4 गैस और गैर-शून्य बाइट के लिए 16 गैस का शुल्क लिया जाता है। इसी तरह,
Wintermute को हैक कर लिया गया क्योंकि उसने profanity एड्रेस का उपयोग किया था (writeup)। यहाँ 1inch का राइटअप दिया गया है कि profanity एड्रेस जनरेटर से कैसे समझौता (compromise) किया गया।
ट्रस्ट वॉलेट (trust wallet) में इस लेख में प्रलेखित एक समान भेद्यता थी (https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been-stolen/)
ध्यान दें कि यह create2 में साल्ट (salt) को बदलकर खोजे गए अग्रणी शून्यों (leading zeros) वाले स्मार्ट कॉन्ट्रैक्ट्स पर लागू नहीं होता है, क्योंकि स्मार्ट कॉन्ट्रैक्ट्स में प्राइवेट कीज़ नहीं होती हैं।
पुन: उपयोग किए गए नॉन्स (nonces) या अपर्याप्त रूप से रैंडम नॉन्स
अण्डाकार वक्र (Elliptic Curve) सिग्नेचर पर “r” और “s” बिंदु निम्नानुसार उत्पन्न होता है:
r = k * G (mod N)
s = k^-1 * (h + r * privateKey) (mod N)
G, r, s, h, और N सभी सार्वजनिक रूप से ज्ञात हैं। यदि “k” सार्वजनिक हो जाता है, तो “privateKey” ही एकमात्र अज्ञात वेरिएबल है, और इसे हल किया जा सकता है। इसके कारण वॉलेट्स को k को पूरी तरह से रैंडम तरीके से उत्पन्न करने की आवश्यकता होती है और इसका कभी भी पुन: उपयोग नहीं किया जाना चाहिए। यदि रैंडमनेस पूरी तरह से रैंडम नहीं है, तो k का अनुमान लगाया जा सकता है। Java लाइब्रेरी में असुरक्षित रैंडमनेस जनरेशन ने 2013 में बहुत सारे Android बिटकॉइन वॉलेट्स को असुरक्षित छोड़ दिया था। (Bitcoin उसी सिग्नेचर एल्गोरिथ्म का उपयोग करता है जिसका उपयोग Ethereum करता है।) (https://arstechnica.com/information-technology/2013/08/all-android-created-bitcoin-wallets-vulnerable-to-theft/)।
अधिकांश कमजोरियां एप्लिकेशन-विशिष्ट होती हैं
इस सूची में एंटी-पैटर्न्स (anti-patterns) को जल्दी से पहचानने के लिए खुद को प्रशिक्षित करने से आप एक अधिक प्रभावी स्मार्ट कॉन्ट्रैक्ट प्रोग्रामर बन जाएंगे, लेकिन अधिकांश स्मार्ट कॉन्ट्रैक्ट बग्स इच्छित बिज़नेस लॉजिक और कोड वास्तव में क्या करता है, के बीच बेमेल होने के कारण होते हैं।
अन्य क्षेत्र जहाँ बग उत्पन्न हो सकते हैं:
- खराब टोकनॉमिक इंसेंटिव्स (tokenomic incentives)
- ऑफ-बाय-वन एरर्स (off by one errors)
- टाइपोग्राफिकल एरर्स (typographical errors)
- एडमिन्स या उपयोगकर्ताओं की प्राइवेट कीज़ का चोरी हो जाना
यूनिट टेस्ट्स की मदद से कई कमजोरियों को पकड़ा जा सकता था
स्मार्ट कॉन्ट्रैक्ट यूनिट टेस्टिंग यकीनन स्मार्ट कॉन्ट्रैक्ट के लिए सबसे बुनियादी सुरक्षा उपाय है, लेकिन एक चौंका देने वाली संख्या में स्मार्ट कॉन्ट्रैक्ट्स में या तो इनकी कमी होती है या उनका टेस्ट कवरेज अपर्याप्त होता है।
लेकिन यूनिट टेस्ट्स केवल कॉन्ट्रैक्ट्स के “हैप्पी पाथ” (अपेक्षित/डिज़ाइन किए गए व्यवहार) का परीक्षण करने की प्रवृत्ति रखते हैं। आश्चर्यजनक मामलों का परीक्षण करने के लिए, अतिरिक्त परीक्षण पद्धतियों (methodologies) को लागू किया जाना चाहिए।
ऑडिट के लिए स्मार्ट कॉन्ट्रैक्ट भेजे जाने से पहले, निम्नलिखित कार्य पहले किए जाने चाहिए:
- Slither जैसे उपकरणों के साथ स्टेटिक एनालिसिस (Static analysis) ताकि यह सुनिश्चित हो सके कि बुनियादी गलतियों को अनदेखा नहीं किया गया है।
- यूनिट टेस्टिंग के माध्यम से 100% लाइन और ब्रांच कवरेज (line and branch coverage)।
- म्यूटेशन टेस्टिंग (Mutation testing) यह सुनिश्चित करने के लिए कि यूनिट टेस्ट में मज़बूत असर्ट (assert) स्टेटमेंट्स हैं।
- फज़ टेस्टिंग (Fuzz testing), विशेष रूप से अंकगणित (arithmetic) के लिए।
- स्टेटफुल प्रॉपर्टीज़ (stateful properties) के लिए इनवेरिएंट टेस्टिंग (Invariant testing)।
- जहाँ उपयुक्त हो वहाँ फॉर्मल वेरिफिकेशन (Formal verification)।
जो लोग यहाँ कुछ कार्यप्रणालियों (methodologies) से अपरिचित हैं, उनके लिए Cyfrin Audits के Patrick Collins के पास अपने वीडियो में स्टेटफुल (stateful) और स्टेटलेस (stateless) फज़िंग का एक हास्यपूर्ण परिचय है। इन कार्यों को पूरा करने के उपकरण तेज़ी से अधिक व्यापक (widespread) और उपयोग में आसान होते जा रहे हैं।
अधिक संसाधन (More resources)
कुछ लेखकों ने इन Repos में पिछले DeFi हैक्स की एक सूची संकलित (compiled) की है:
- https://github.com/coinspect/learn-evm-attacks
- https://github.com/SunWeb3Sec/DeFiHackLabs
- https://rekt.news/
Secureum का उपयोग सुरक्षा के अध्ययन और अभ्यास के लिए व्यापक रूप से किया गया है, लेकिन ध्यान रखें कि repo को 2 वर्षों से काफी हद तक अपडेट नहीं किया गया है:
आप हमारे Solidity Riddles रिपॉजिटरी के साथ solidity की कमजोरियों का फायदा उठाने का अभ्यास कर सकते हैं।
DamnVulnerableDeFi एक क्लासिक वॉरगेम (wargame) है जिसका हर डेवलपर को अभ्यास करना चाहिए:
Capture The Ether और Ethernaut क्लासिक्स हैं, लेकिन ध्यान रखें कि कुछ समस्याएं अवास्तविक रूप से आसान हैं या पुराने Solidity कॉन्सेप्ट्स सिखाती हैं:
कुछ प्रतिष्ठित क्राउडसोर्सड (crowdsourced) सुरक्षा फर्मों के पास अध्ययन करने के लिए पिछले ऑडिट्स की एक उपयोगी सूची है।
स्मार्ट कॉन्ट्रैक्ट ऑडिटर बनना
यदि आप Solidity में धाराप्रवाह (fluent) नहीं हैं, तो कोई तरीका नहीं है कि आप Ethereum स्मार्ट कॉन्ट्रैक्ट्स का ऑडिट कर पाएंगे। यदि आप अभी शुरुआत कर रहे हैं तो हमारा मुफ्त Solidity ट्यूटोरियल देखें।
स्मार्ट कॉन्ट्रैक्ट ऑडिटर बनने के लिए कोई उद्योग मान्यता प्राप्त प्रमाणन (certification) नहीं है। कोई भी व्यक्ति स्मार्ट कॉन्ट्रैक्ट ऑडिटर होने का दावा करते हुए एक वेबसाइट और सोशल मीडिया प्रोफाइल बना सकता है और सेवाएं बेचना शुरू कर सकता है, और कई लोगों ने ऐसा किया भी है। इसलिए, किसी को काम पर रखने से पहले सावधानी बरतें और रेफरल (referrals) प्राप्त करें।
एक स्मार्ट कॉन्ट्रैक्ट ऑडिटर बनने के लिए, आपको बग पहचानने में औसत solidity डेवलपर से काफी बेहतर होना होगा। इस प्रकार, एक ऑडिटर बनने का “रोडमैप” महीनों और महीनों के निरंतर और जानबूझकर किए गए अभ्यास से ज्यादा कुछ नहीं है जब तक कि आप अधिकांश की तुलना में बेहतर स्मार्ट कॉन्ट्रैक्ट बग कैचर (bug catcher) न बन जाएं।
यदि आपमें कमजोरियों (vulnerabilities) की पहचान करने में अपने साथियों से बेहतर प्रदर्शन करने के दृढ़ संकल्प (determination) की कमी है, तो यह संभावना नहीं है कि उच्च प्रशिक्षित और प्रेरित अपराधियों द्वारा ऐसा करने से पहले आप महत्वपूर्ण मुद्दों को पहचान पाएंगे।
स्मार्ट कॉन्ट्रैक्ट सिक्योरिटी ऑडिटर बनने की सफलता की संभावनाओं के बारे में कड़वा सच
हाल ही में स्मार्ट कॉन्ट्रैक्ट ऑडिटिंग को एक आकर्षक क्षेत्र माना जाने लगा है क्योंकि ऐसी धारणा है कि यह आकर्षक (lucrative) है। वास्तव में, कुछ बग बाउंटी (bug bounty) भुगतान 1 मिलियन डॉलर से अधिक हो गए हैं, लेकिन यह अत्यंत दुर्लभ अपवाद (exception) है, न कि आदर्श (norm)।
Code4rena के पास अपनी ऑडिट प्रतियोगिताओं में प्रतिस्पर्धियों से भुगतान का एक सार्वजनिक लीडरबोर्ड है, जो हमें सफलता दर के बारे में कुछ डेटा देता है।
बोर्ड पर 1171 नाम हैं, फिर भी:
- केवल 29 प्रतिस्पर्धियों की आजीवन कमाई $100,000 से अधिक है (2.4%)
- केवल 57 की आजीवन कमाई $50,000 से अधिक है (4.9%)
- केवल 170 की आजीवन कमाई $10,000 से अधिक है (14.5%)
इस पर भी विचार करें, जब OpenZeppelin ने सुरक्षा अनुसंधान फेलोशिप (एक नौकरी नहीं, नौकरी से पहले की स्क्रीनिंग और प्रशिक्षण) के लिए आवेदन खोला, तो उन्हें 300 से अधिक आवेदन प्राप्त हुए, जिसमें से केवल 10 से कम उम्मीदवारों का चयन किया गया, जिनमें से और भी कम लोगों को पूर्णकालिक (full time) नौकरी मिलेगी।

https://twitter.com/David_Bessin/status/1625167906328944640
यह हार्वर्ड (Harvard) की तुलना में कम प्रवेश दर (admission rate) है।
स्मार्ट कॉन्ट्रैक्ट ऑडिटिंग एक प्रतिस्पर्धी जीरो-सम गेम (zero-sum game) है। ऑडिट करने के लिए केवल कुछ ही प्रोजेक्ट्स हैं, सुरक्षा के लिए केवल इतना ही बजट है, और खोजने के लिए केवल कुछ ही बग हैं। यदि आप अभी सुरक्षा का अध्ययन शुरू करते हैं, तो दर्जनों अत्यधिक प्रेरित व्यक्ति और टीमें हैं जिनके पास आपसे भारी बढ़त (headstart) है। अधिकांश प्रोजेक्ट एक बिना परखे (untested) गए नए ऑडिटर के बजाय प्रतिष्ठा वाले ऑडिटर के लिए प्रीमियम का भुगतान करने को तैयार हैं।
इस लेख में, हमने कमजोरियों की कम से कम 20 विभिन्न श्रेणियों को सूचीबद्ध किया है। यदि आपने प्रत्येक में महारत हासिल करने में एक सप्ताह बिताया (जो कुछ हद तक आशावादी है), तो आप केवल यह समझना शुरू कर रहे हैं कि अनुभवी ऑडिटर्स के लिए सामान्य ज्ञान क्या है। हमने इस लेख में गैस ऑप्टिमाइज़ेशन या टोकनॉमिक्स को कवर नहीं किया है, ये दोनों ही ऑडिटर के लिए समझने योग्य महत्वपूर्ण विषय हैं। गणित करें और आप देखेंगे कि यह कोई छोटी यात्रा नहीं है।
यह कहा जा सकता है कि, समुदाय आम तौर पर नए लोगों के अनुकूल और मददगार है और टिप्स और ट्रिक्स की भरमार है। लेकिन जो लोग स्मार्ट कॉन्ट्रैक्ट सुरक्षा में करियर बनाने की उम्मीद में इस लेख को पढ़ रहे हैं, उनके लिए यह स्पष्ट रूप से समझना महत्वपूर्ण है कि एक आकर्षक करियर प्राप्त करने की संभावनाएं आपके पक्ष में नहीं हैं। सफलता डिफ़ॉल्ट परिणाम नहीं है।
बेशक यह किया जा सकता है, और काफी कुछ लोग ऐसे हैं जो Solidity को न जानने से लेकर ऑडिटिंग में एक आकर्षक करियर बनाने तक पहुँचे हैं। यकीनन लॉ स्कूल में प्रवेश पाने और बार परीक्षा (bar exam) पास करने की तुलना में दो साल की समय सीमा में स्मार्ट कॉन्ट्रैक्ट ऑडिटर के रूप में नौकरी पाना आसान है। कई अन्य करियर विकल्पों की तुलना में निश्चित रूप से इसमें अधिक अवसर (upside) हैं।
लेकिन फिर भी आगे के तेज़ी से विकसित हो रहे ज्ञान के पहाड़ में महारत हासिल करने और बग्स को पहचानने के लिए अपने अंतर्ज्ञान (intuition) को निखारने के लिए आपकी ओर से हरक्यूलियन (अत्यधिक) दृढ़ता की आवश्यकता होगी।
इसका मतलब यह नहीं है कि स्मार्ट कॉन्ट्रैक्ट सुरक्षा सीखना एक सार्थक प्रयास नहीं है। यह बिल्कुल है। लेकिन यदि आप अपनी आँखों में डॉलर के निशान के साथ क्षेत्र में आ रहे हैं, तो अपनी अपेक्षाओं (expectations) को नियंत्रण में रखें।
निष्कर्ष (Conclusion)
ज्ञात एंटी-पैटर्न्स के बारे में जागरूक होना महत्वपूर्ण है। हालाँकि, अधिकांश वास्तविक दुनिया के बग एप्लिकेशन-विशिष्ट होते हैं। भेद्यता (vulnerabilities) की किसी भी श्रेणी की पहचान करने के लिए निरंतर और जानबूझकर अभ्यास की आवश्यकता होती है।
हमारे उद्योग-अग्रणी solidity ट्रेनिंग के साथ स्मार्ट कॉन्ट्रैक्ट सुरक्षा, और कई अन्य Ethereum विकास विषय सीखें।
मूल रूप से 5 मई, 2023 को प्रकाशित