इस लेख के माध्यम से हमारी मंशा शुरुआती डेवलपर्स की उनकी यात्रा के आरंभ में नीचा दिखाने की नहीं है। कई Solidity डेवलपर्स के कोड की समीक्षा करने के बाद, हमने देखा है कि कुछ गलतियाँ अधिक बार होती हैं और हम उन्हें यहाँ सूचीबद्ध कर रहे हैं।
किसी भी तरह से यह उन गलतियों की कोई संपूर्ण सूची नहीं है जो एक Solidity डेवलपर कर सकता है। इंटरमीडिएट (Intermediate) और यहाँ तक कि अनुभवी डेवलपर्स भी ये गलतियाँ कर सकते हैं।
हालाँकि, सीखने की शुरुआती यात्रा में इन गलतियों के होने की अधिक संभावना होती है, इसलिए इन्हें सूचीबद्ध करना उचित है।
1. गुणा (multiplication) से पहले भाग (division) करना
Solidity में, भाग (division) का ऑपरेशन हमेशा अंतिम ऑपरेशन होना चाहिए क्योंकि भाग संख्याओं को नीचे की ओर पूर्णांकित (round down) कर देता है।
उदाहरण के लिए, यदि हम गणना करना चाहते हैं कि हमें किसी को 33.33% ब्याज देना चाहिए, तो इसे करने का गलत तरीका है:
interest = principal / 3_333 * 10_000;
यदि मूलधन (principal) 3,333 से कम है, तो ब्याज राउंड डाउन होकर शून्य हो जाएगा। इसके बजाय, ब्याज की गणना इस प्रकार की जानी चाहिए:
interest = principal * 10_000 / 3_333;
यहाँ वह गणित है जो बताता है कि पहले उदाहरण में राउंडिंग कैसे विफल होती है और दूसरे में कैसे सफल होती है:
**// Wrong way:**
If principal = 3000,
interest = principal / 3333 * 10000
interest = 3000 / 3333 * 10000
interest = 0 * 10000 (rounding down in division)
interest = 0
// **Correct Calculation:**
If principal = 3000,
interest = principal * 10000 / 3333
interest = 3000 * 10000 / 3333
interest = 30000000 / 3333 interest approx 9000
Slither के साथ समस्या को पकड़ना
Slither Trail of Bits का एक स्टैटिक एनालिसिस टूल (static analysis tool) है जो सामान्य गलतियों के पैटर्न से मिलान करने के लिए कोडबेस को पार्स करता है।
यदि हम निम्नलिखित (दोषपूर्ण) कॉन्ट्रैक्ट interest.sol बनाते हैं:
contract Interest {
// 1 basis point is 0.01% or 1/10_000
function calculateInterest(uint256 principal, uint256 interestBasisPoints) public pure returns (uint256 interest){
interest = principal / 10_000 * interestBasisPoints;
}
}
और टर्मिनल में रन करते हैं:
slither interest.sol
तो हमें निम्नलिखित चेतावनी (warning) मिलती है:

इस मामले में, यह कह रहा है कि हम गुणा करने से पहले भाग कर रहे हैं, जो सामान्य तौर पर एक ऐसी चीज़ है जिससे बचना चाहिए।
2. check-effects-interaction पैटर्न का पालन न करना
Solidity में, re-entrancy हमलों को रोकने के लिए “check-effects-interaction” पैटर्न का पालन करना बहुत महत्वपूर्ण है। इसका मतलब यह है कि किसी अन्य कॉन्ट्रैक्ट को कॉल करना या किसी अन्य पते (address) पर ETH भेजना किसी भी फंक्शन में अंतिम ऑपरेशन होना चाहिए। ऐसा न करने पर कॉन्ट्रैक्ट दुर्भावनापूर्ण हमलों (malicious attacks) के प्रति संवेदनशील हो सकता है।
निम्नलिखित कॉन्ट्रैक्ट BadBank check-effects-interaction का पालन नहीं करता है और इस प्रकार इससे ETH निकाला जा सकता है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
// DO NOT USE
contract BadBank {
mapping(address => uint256) public balances;
constructor()
payable {
require(msg.value == 10 ether, "deposit 10 eth");
}
function deposit()
external
payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
(bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
require(ok, "transfer failed");
balances[msg.sender] = 0;
}
}
बैंक को खाली करने के लिए निम्नलिखित अटैक कॉन्ट्रैक्ट का उपयोग किया जा सकता है:
contract BankDrainer {
function steal(BadBank bank) external payable {
require(msg.value == 1 ether, "send deposit 1 eth");
bank.deposit{value: 1 ether}();
bank.withdraw();
}
receive() external payable {
// msg.sender is the BadBank because the BadBank
// called `receive()` when it transfered either
while (msg.sender.balance >= 1 ether) {
BadBank(msg.sender).withdraw();
}
}
}
आप Remix में कोड का परीक्षण यहाँ कर सकते हैं। निम्नलिखित वीडियो इस हैक को प्रदर्शित करता है।
यह हैक इसलिए संभव है क्योंकि BadBank का withdraw() फंक्शन बैलेंस अपडेट करने से पहले BankDrainer में receive() फंक्शन को कॉल करता है। ईथर भेजना किसी अन्य कॉन्ट्रैक्ट पर receive() या fallback() फंक्शन को कॉल करने के बराबर है।
इसलिए, हमेशा किसी अन्य स्मार्ट कॉन्ट्रैक्ट के फंक्शन को कॉल करें या Ether को सबसे अंत में भेजें। इस हमले की श्रेणी को re-entrancy कहा जाता है। आप इस हमले के बारे में हमारे re-entrancy लेख में अधिक जान सकते हैं।
जब हम उपरोक्त कोड पर Slither चलाते हैं, तो Slither हमें दो चेतावनियाँ देता है:

पहली चेतावनी, कि यह “एक मनमाने उपयोगकर्ता को eth भेजता है (sends eth to an arbitrary user)” एक फॉल्स पॉजिटिव (false positive) है। यह सच है कि कोई भी withdraw को कॉल कर सकता है, लेकिन वे जो राशि निकाल सकते हैं वह उनके बैलेंस तक सीमित है (कम से कम शुरुआत में!)।
हालाँकि, Slither सही ढंग से re-entrancy भेद्यता (vulnerability) का पता लगा लेता है।
3. transfer या send का उपयोग करना
Solidity में कॉन्ट्रैक्ट से डेस्टिनेशन तक Ether भेजने के लिए दो सुविधाजनक फंक्शन्स transfer() और send() हैं। हालाँकि, आपको इन फंक्शन्स का उपयोग नहीं करना चाहिए।
आपको transfer या send का उपयोग क्यों नहीं करना चाहिए, इस पर Consensys का ब्लॉग एक क्लासिक लेख है जिसे हर Solidity डेवलपर को कभी न कभी जरूर पढ़ना चाहिए।
ये फंक्शन्स क्यों मौजूद हैं?
DAO हैक के बाद, जिसने Ethereum को Ethereum और Ethereum Classic में विभाजित कर दिया, डेवलपर्स re-entrancy हमलों से बहुत डर गए थे। ऐसे हमलों से बचने के लिए, transfer() और send() पेश किए गए क्योंकि वे प्राप्तकर्ता (recipient) को उपलब्ध गैस की मात्रा को सीमित कर देते हैं। इसके परिणामस्वरूप प्राप्तकर्ता को आगे कोड निष्पादित (execute) करने के लिए आवश्यक गैस से वंचित करके re-entrancy को रोका जाता है।
उदाहरण परिदृश्य (Example scenario):
आप पहले वाले उदाहरण में इस कोड को:
(bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
require(ok, "transfer failed");
इसके साथ payable(msg.sender).transfer(balances[msg.sender]); बदल सकते हैं और आप देखेंगे कि बैंक अब असुरक्षित नहीं है।
हालाँकि, यह उन इंटिग्रेशन्स को तोड़ देगा जब कॉन्ट्रैक्ट आने वाले Ether का जवाब देने के लिए पर्याप्त गैस प्राप्त करने की उम्मीद कर रहा हो। उदाहरण के लिए, यदि लक्ष्य (target) कॉन्ट्रैक्ट प्रेषक (sender) को ETH क्रेडिट करने का प्रयास करता है, तो यह विफल हो जाएगा क्योंकि बुककीपिंग को पूरा करने के लिए इसके पास पर्याप्त गैस नहीं है।
निम्नलिखित उदाहरण पर विचार करें:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract GoodBank {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 balance = balances[msg.sender];
balances[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: balance}("");
require(ok, "transfer failed");
}
receive() external payable {
balances[msg.sender] += msg.value;
}
}
contract SendToBank {
address owner;
constructor() {
owner = msg.sender;
}
function depositInBank(
address bank
) external payable {
require(msg.sender == owner, "not owner");
// THIS LINE WILL FAIL
payable(bank).transfer(msg.value);
}
function withdrawBank(
address payable bank
) external {
require(msg.sender == owner, "not owner");
// this triggers the receive function
GoodBank(bank).withdraw();
// the receive function has completed
// and now this contract has a balance
// send it to the owner
(bool ok, ) = msg.sender.call{value: address(this).balance}("");
require(ok, "transfer failed");
}
// we need this to receive Ether from the bank
receive() external payable {
}
}
आप उपरोक्त कोड का Remix में यहाँ परीक्षण कर सकते हैं। और यहाँ विफल ट्रांसफर (failed transfer) को प्रदर्शित करने वाला एक वीडियो है।
ट्रांजेक्शन विफल हो जाता है क्योंकि प्रेषक का बैलेंस बढ़ाते समय receive() की गैस खत्म हो जाती है।
इसलिए transfer या send का उपयोग न करें और re-entrant कोड न लिखें। पहला विकल्प यह है कि transfer या send को address(receiver).call{value: amountToSend}("") से बदल दिया जाए। वैकल्पिक रूप से, कोई भी वही काम करने के लिए OpenZeppelin Address लाइब्रेरी का उपयोग कर सकता है। दोनों तरीके नीचे दिखाए गए हैं:
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
contract SendEthExample {
using Address for address payable;
// both these functions do the same thing. Note that OZ requires
// payable addresses, but a low-level call does not
function sendSomeEthV1(address receiver, uint256 amount) external payable {
payable(receiver).sendValue(amount);
}
function sendSomeEthV2(address receiver, uint256 amount) external payable {
(bool ok, ) = receiver.call{value: amount}("");
require(ok, "transfer failed");
}
}
Slither transfer या send का उपयोग करने के बारे में कोई चेतावनी प्रदान नहीं करता है, लेकिन फिर भी आपको इनका उपयोग करने से बचना चाहिए।
4. msg.sender के बजाय tx.origin का उपयोग करना
Solidity थोड़ी भ्रमित करने वाली है क्योंकि कॉन्ट्रैक्ट के दृष्टिकोण से “मुझे कौन कॉल कर रहा है” यह निर्धारित करने के दो तरीके हैं: एक tx.origin है और दूसरा msg.sender है।
tx.origin वह वॉलेट है जिसने ट्रांजेक्शन पर हस्ताक्षर (sign) किए हैं। msg.sender डायरेक्ट कॉलर है। यदि कोई वॉलेट सीधे किसी कॉन्ट्रैक्ट को कॉल करता है
wallet → contract
तो कॉन्ट्रैक्ट के दृष्टिकोण से, वॉलेट msg.sender और tx.origin दोनों है।
अब विचार करें कि यदि वॉलेट एक मध्यवर्ती (intermediate) कॉन्ट्रैक्ट को कॉल करता है जो फिर अंतिम कॉन्ट्रैक्ट को कॉल करता है:
wallet → intermediate contract → final contract
अंतिम कॉन्ट्रैक्ट के दृष्टिकोण से, वॉलेट tx.origin है और मध्यवर्ती कॉन्ट्रैक्ट msg.sender है।
कॉलर की पहचान करने के लिए tx.origin का उपयोग करने से एक सुरक्षा भेद्यता (security vulnerability) उत्पन्न होती है। मान लीजिए कि उपयोगकर्ता को धोखे (phishing) से एक दुर्भावनापूर्ण (malicious) मध्यवर्ती कॉन्ट्रैक्ट को कॉल करने के लिए प्रेरित किया जाता है
wallet → malicious intermediate contract → final contract
इस स्थिति में, दुर्भावनापूर्ण मध्यवर्ती कॉन्ट्रैक्ट वॉलेट के सभी विशेषाधिकार प्राप्त कर लेता है, जिससे वह कोई भी ऐसा कार्य कर सकता है जिसे करने के लिए वॉलेट अधिकृत है — जैसे कि धन (funds) को स्थानांतरित करना।
msg.sender और tx.origin के बीच के अंतर के बारे में अधिक जानने के लिए कोई पता (address) स्मार्ट कॉन्ट्रैक्ट है या नहीं, इसका पता लगाना पर हमारा लेख देखें।
Slither tx.origin के संबंध में कोई चेतावनी प्रदान नहीं करता है।
5. ERC-20 के साथ safeTransfer का उपयोग न करना
ERC-20 मानक केवल यह कहता है कि यदि उपयोगकर्ता अपने बैलेंस से अधिक ट्रांसफर करने का प्रयास करता है तो टोकन को एक त्रुटि (error) फेंकनी चाहिए। हालाँकि, यदि ट्रांसफर किसी अन्य कारण से विफल हो जाता है, तो मानक स्पष्ट रूप से यह नहीं बताता है कि क्या होना चाहिए।
ERC-20 transfer के लिए फंक्शन सिग्नेचर है:
function transfer(address _to, uint256 _value) public returns (bool success);
जो यह इंगित करता है कि ERC-20 टोकन को विफलता पर false रिटर्न करना चाहिए।
व्यवहार में, ERC-20 टोकन को असंगत (inconsistent) तरीकों से लागू किया गया है: कुछ विफलता पर रिवर्ट (revert) करते हैं, और अन्य कोई भी बूलियन (Boolean) रिटर्न नहीं करते हैं (अर्थात फंक्शन सिग्नेचर का सम्मान नहीं करते हैं)।
लाइब्रेरी SafeERC20 दोनों प्रकार के ERC-20 टोकन को संभालती है। विशेष रूप से, यह एड्रेस पर एक transfer कॉल करती है, और
- यदि रिवर्ट होता है, तो
SafeERC20रिवर्ट को ऊपर की ओर बबल्स (bubbles up) कर देता है। यह उन टोकन्स को संभालता है जो विफलता पर रिवर्ट करते हैं, लेकिन जरूरी नहीं कि वे एक बूलियन रिटर्न करें। - यदि कोई रिवर्ट नहीं होता है, तो यह जाँचता है कि क्या डेटा बिल्कुल भी रिटर्न किया गया था
- यदि कोई डेटा रिटर्न नहीं किया गया था और टोकन एड्रेस स्मार्ट कॉन्ट्रैक्ट के बजाय खाली पता (empty address) निकलता है, तो लाइब्रेरी रिवर्ट हो जाती है।
- यदि डेटा रिटर्न किया गया था, और रिटर्न एक फाल्स (false) मान है, तो SafeERC20 रिवर्ट हो जाता है।
- अन्यथा, लाइब्रेरी रिवर्ट नहीं करती है, जो एक सफल ट्रांसफर का संकेत देती है।
यहाँ बताया गया है कि OpenZeppelin की SafeERC20 लाइब्रेरी का उपयोग कैसे किया जाना चाहिए:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC20/ERC20.sol";
contract SafeTransferDemo {
using SafeERC20 for IERC20;
function deposit(
IERC20 token,
uint256 amount)
external {
token.safeTransferFrom(msg.sender, address(this), amount);
}
// withdraw function not shown
}
contract MyToken is ERC20("MyToken", "MT") {
constructor() {
// mint the supply of 10_000 tokens
// to the deployer
_mint(msg.sender, 10_000 * 1e18);
}
}
6. Solidity 0.8.0 (या उच्चतर) के साथ safeMath का उपयोग करना
Solidity 0.8.0 से पहले, यदि कोई गणितीय ऑपरेशन होता था जिसके परिणामस्वरूप मान उस वेरिएबल की क्षमता से अधिक हो जाता था, तो वेरिएबल ओवरफ़्लो (overflow) कर सकते थे। इसके जवाब में, OpenZeppelin की SafeMath लाइब्रेरी लोकप्रिय हो गई। यहाँ बताया गया है कि लाइब्रेरी ने जोड़ने (addition) में ओवरफ़्लो को कैसे रोका:
function add(uint256 x, uint256 y) internal pure returns (uint256) {
uint256 sum = x + y;
require(sum >= x || sum >= y, "overflow");
return sum;
}
योग (sum) हमेशा x या y से बड़ा होना चाहिए। यदि ऐसा नहीं है, तो एक ओवरफ़्लो हुआ है और फंक्शन रिवर्ट हो जाता है।
पुराने कोडबेस में, आप अक्सर यह लाइन देखेंगे:
using SafeMath for uint256;
और गणित इस तरीके से किया जा रहा होगा:
uint256 sum = x.add(y);
हालाँकि, आपको Solidity 0.8.0 या उच्चतर में ऐसा नहीं करना चाहिए क्योंकि कंपाइलर परदे के पीछे एक अंतर्निहित (built-in) ओवरफ़्लो चेक जोड़ देता है। इसलिए, बुनियादी अंकगणितीय ऑपरेशन्स के लिए SafeMath लाइब्रेरी का उपयोग करने से कोड कम पठनीय (readable) और अक्षम (inefficient) हो जाता है, और सुरक्षा में कोई अतिरिक्त लाभ भी नहीं मिलता है।
7. एक्सेस कंट्रोल (access control) भूल जाना
आइए एक न्यूनतम उदाहरण का उपयोग करें। क्या आप समस्या को पहचान सकते हैं?
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
contract NFTSale is ERC721("MyTok", "MT") {
uint256 public price;
uint256 public currentId;
function setPrice(
uint256 price_
) public {
price = price_;
}
function buyNFT() external payable {
require(msg.value == price, "wrong price");
currentId++;
_mint(msg.sender, currentId);
}
}
कोई भी setPrice() को कॉल कर सकता है और buyNFT() को कॉल करने से पहले इसे शून्य पर सेट कर सकता है।
जब भी आप कोई ऐसा फंक्शन लिखते हैं जो public या external हो, तो खुद से पूछें कि क्या फंक्शन को कॉल करने वाले पर कोई प्रतिबंध होना चाहिए। यहाँ ऊपर दी गई समस्या का एक सूक्ष्म रूप (subtle variation) है:
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
contract NFTSale is ERC721("MyTok", "MT") {
uint256 public price;
address owner;
uint256 public currentId;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "onlyOwner");
_;
}
function setPrice(
uint256 price_
) public onlyOwner {
price = price_;
}
function buyNFT() external payable {
require(msg.value == price, "wrong price");
currentId++;
_mint(msg.sender, currentId);
}
}
यहाँ, डेवलपर ने एक onlyOwner मॉडिफायर (modifier) जोड़ा है, जो केवल निर्दिष्ट उपयोगकर्ता (designated user) को एक्सेस प्रदान करता है। उपरोक्त उदाहरणों में, एक्सेस कंट्रोल मॉडिफायर यह सुनिश्चित करता है कि केवल कॉन्ट्रैक्ट का मालिक ही मूल्य निर्धारित कर सकता है, जैसा कि setPrice फंक्शन में देखा गया है।
8. लूप (loop) में महंगे ऑपरेशन्स (Expensive operations)
ऐसी ऐरे (Arrays) जो बिना किसी सीमा के बढ़ सकती हैं, समस्याग्रस्त होती हैं क्योंकि उन पर लूप (loop) चलाने की ट्रांजेक्शन लागत अत्यधिक अधिक हो सकती है।
निम्नलिखित कॉन्ट्रैक्ट Ether में दान (donations) लेता है और दाताओं (donors) को एक ऐरे में जोड़ता है। बाद में, मालिक distributeNFTs() को कॉल करेगा और सभी दाताओं के लिए एक NFT मिंट करेगा। हालाँकि, यदि बहुत सारे दाता हैं, तो मालिक के लिए दान की प्रक्रिया को पूरा करना बहुत महंगा हो सकता है।
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@5.0.0/access/Ownable.sol";
contract GiveNFTToDonors is ERC721("MyTok", "MT"), Ownable(msg.sender) {
address[] donors;
uint256 currentId;
receive() external payable {
require(msg.value >= 0.1 ether, "donation too small");
donors.push(msg.sender);
}
function distributeNFTs() external onlyOwner {
for (uint256 i = 0; i < donors.length; i++) {
currentId++;
_mint(msg.sender, currentId);
}
}
}
फंक्शन distributeNFTs() पूरी डोनर ऐरे पर पुनरावृति (iterate) करने का प्रयास करेगा। हालाँकि, यदि ऐरे में डोनर सूची बड़ी है, तो इस लूप के परिणामस्वरूप बहुत अधिक गैस लागत आएगी, जिससे ट्रांजेक्शन अव्यावहारिक (unfeasible) हो जाएगा। Slither आपको इस स्थिति के बारे में निम्नलिखित जैसी एक चेतावनी देगा:

इस समस्या का समाधान “pull over push” के रूप में जाना जाता है। प्रत्येक प्राप्तकर्ता को उनका NFT भेजने के बजाय, आप उनसे एक फंक्शन कॉल करवाते हैं जो NFT को उस एड्रेस पर ट्रांसफर कर देता है, यदि वह एड्रेस फंक्शन को कॉल करता है।
9. फंक्शन इनपुट्स पर सैनिटी चेक (sanity checks) का न होना
जब भी आप कोई पब्लिक (public) फंक्शन लिखते हैं, तो स्पष्ट रूप से उन मानों (values) को लिखें जिन्हें आप फंक्शन के तर्कों (arguments) में पास किए जाने की उम्मीद करते हैं और यह सुनिश्चित करें कि require स्टेटमेंट्स इसे लागू करते हैं। उदाहरण के लिए, लोगों को अपने बैलेंस से अधिक निकालने में सक्षम नहीं होना चाहिए। लोगों को ऐसी संपत्ति निकालने में सक्षम नहीं होना चाहिए जिसे उन्होंने जमा नहीं किया है।
निम्नलिखित उदाहरणों पर विचार करें:
contract LendingProtocol is Ownable {
function offerLoan(
uint256 amount,
uint256 interest,
uint256 duration)
external {}
function setProtocolFee(
uint256 feeInBasisPoints)
external
onlyOwner {}
}
डिज़ाइनर को सोचना चाहिए कि यहाँ कौन से पैरामीटर्स उचित हैं। 1000% से अधिक की ब्याज दर अनुचित है। 1 घंटे जैसी अत्यंत छोटी अवधि भी अनुचित है।
इसी तरह, setProtocolFee फंक्शन में एक समझदारी वाली ऊपरी सीमा (upper bound) होनी चाहिए कि मालिक कितनी फीस निर्धारित कर सकता है, अन्यथा यदि प्रोटोकॉल का उपयोग करने की फीस अचानक से अनुचित स्तर तक बढ़ जाती है तो उपयोगकर्ता आश्चर्यचकित हो सकते हैं।
सैनिटी चेक को लागू करने के लिए, हम बस require स्टेटमेंट्स जोड़ते हैं जो इनपुट्स की स्वीकार्य सीमा (acceptable range) को बांधते हैं।
सार्वजनिक फंक्शन को डिज़ाइन करते समय हमेशा इस बात पर विचार करें कि फंक्शन के तर्कों के लिए पैरामीटर्स की कौन सी सीमा (range) उपयुक्त है।
10. मिसिंग कोड (Missing code)
Solidity में कुछ बग्स (bugs) खराब कोड के बजाय मिसिंग (missing) कोड के कारण होते हैं। निम्नलिखित NFT मिंटिंग कॉन्ट्रैक्ट मालिक को यह निर्दिष्ट (specify) करने की अनुमति देता है कि किसे NFT मिंट करने की अनुमति है और कितनी। (यह इसे करने का गैस-कुशल तरीका नहीं है, लेकिन हम मौजूदा सिद्धांत पर ध्यान केंद्रित करना चाहते हैं)।
यहाँ कोड दिया गया है, क्या आप पहचान सकते हैं कि क्या गायब है?
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@5.0.0/access/Ownable2Step.sol";
contract MissingCode is ERC721("MissingCode", "MC"), Ownable(msg.sender) {
uint256 id;
mapping(address => uint256) public amountAllowedToMint;
function mint(
uint256 amount
) external {
require(amount < amountAllowedToMint[msg.sender],
"not enough allocation");
for (uint256 i = 0; i < amount; i++) {
id++;
_mint(msg.sender, id);
}
}
function setAmountAllowedToMint(
address[] calldata minters,
uint256[] calldata amounts
) external onlyOwner {
require(minters.length == amounts.length,
"length mismatch");
for (uint256 i = 0; i < minters.length; i++) {
amountAllowedToMint[minters[i]] = amounts[i];
}
}
}
समस्या यह है कि एक खरीदार जितनी मात्रा (amount) मिंट करता है उसे amountAllowedToMint से नहीं घटाया जाता है, इसलिए “सीमा (limit)” वास्तव में लागू नहीं होती है। मैपिंग में मौजूद कोई भी एड्रेस जितनी बार चाहे mint() को कॉल कर सकता है।
_mint() फंक्शन के बाद एक अतिरिक्त लाइन amountAllowedToMint[msg.sender] -= amount होनी चाहिए।
11. Solidity pragma को फिक्स (fix) न करना
जब आप Solidity लाइब्रेरीज़ का कोड पढ़ते हैं, तो आप अक्सर कुछ इस तरह देखेंगे:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
शीर्ष (top) पर। इस वजह से, नए डेवलपर्स आँख मूंदकर इस पैटर्न की नकल करने लगते हैं।
हालाँकि, ^0.8.0 के साथ Solidity संस्करण (version) सेट करना केवल लाइब्रेरीज़ के लिए उपयुक्त है। लाइब्रेरी वितरित करने वाले लेखक को यह नहीं पता होता है कि कोई बाद का प्रोग्रामर इसे किस सटीक संस्करण के साथ संकलित (compile) करेगा, इसलिए वे केवल न्यूनतम संस्करण निर्धारित करते हैं।
एप्लिकेशन तैनात (deploy) करने वाले डेवलपर के रूप में, आप जानते हैं कि कोड को संकलित करने के लिए आप कंपाइलर के किस संस्करण का उपयोग कर रहे हैं। इसलिए आपको संस्करण को उस सटीक संस्करण पर लॉक कर देना चाहिए जिसका आपने उपयोग किया था ताकि कोड का ऑडिट करने वाले अन्य लोगों के लिए यह स्पष्ट हो जाए कि आपने Solidity कंपाइलर के किस संस्करण का उपयोग किया है। उदाहरण के लिए, pragma solidity ^0.8.0 डालने के बजाय सटीक संस्करण pragma solidity 0.8.26 लिखें। यह निर्दिष्ट संस्करण के साथ कोड का ऑडिट करने वाले अन्य लोगों के लिए चीजें स्पष्ट कर देगा।
12. स्टाइल गाइड (style guide) का पालन न करना
हमने Solidity स्टाइल गाइड को एक अलग ब्लॉग पोस्ट में प्रलेखित (documented) किया है।
यहाँ इसके मुख्य बिंदु (highlights) हैं:
- constructor पहला फंक्शन होता है
- फिर
fallback()औरreceive()(यदि कॉन्ट्रैक्ट में वे हैं) - फिर
externalफंक्शन्स,publicफंक्शन्स,internalफंक्शन्स, औरpureफंक्शन्स - प्रत्येक समूह के भीतर
payableफंक्शन्स पहले आते हैं- इसके बाद नॉन-
payableनॉन-viewफंक्शन्स - और
viewफंक्शन्स सबसे अंत में आते हैं
13. लॉग्स (logs) का न होना या गलत तरीके से इंडेक्स किए गए लॉग्स
Ethereum में, किसी विशिष्ट स्मार्ट कॉन्ट्रैक्ट पर भेजे गए सभी ट्रांजेक्शन को सूचीबद्ध करने का कोई नेटिव तरीका नहीं है, सिवाय इसके कि ब्लॉक एक्सप्लोरर्स में इस जानकारी की खोज की जाए। हालाँकि, यह कॉन्ट्रैक्ट से इवेंट्स (events) एमिट (emit) करवाकर हासिल किया जा सकता है।
यहाँ इवेंट्स के बारे में कुछ सामान्य नियम दिए गए हैं:
- कोई भी फंक्शन जो स्टोरेज वेरिएबल (storage variable) को बदल सकता है, उसे एक इवेंट एमिट करना चाहिए।
- इवेंट में इतनी जानकारी होनी चाहिए कि लॉग का ऑडिट करने वाला कोई व्यक्ति यह निर्धारित कर सके कि उस समय स्टोरेज वेरिएबल ने क्या मान लिया था।
- इवेंट में कोई भी
addressपैरामीटरindexedहोना चाहिए ताकि किसी विशेष वॉलेट की गतिविधि पर बारीकी से नज़र डालना आसान हो। - View और pure फंक्शन्स में इवेंट्स नहीं होने चाहिए क्योंकि वे स्टेट (state) को नहीं बदलते हैं।
आप इसके बारे में हमारे लेख Solidity और Ethereum में इवेंट्स में अधिक पढ़ सकते हैं।
सामान्य तौर पर, यदि आप कोई स्टोरेज वेरिएबल बदलते हैं या कॉन्ट्रैक्ट में Ether अंदर-बाहर करते हैं, तो आपको एक इवेंट एमिट करना चाहिए।
14. यूनिट टेस्ट (unit tests) न लिखना
आप कैसे जान सकते हैं कि कॉन्ट्रैक्ट हर उस संभावित परिदृश्य (scenario) में काम करेगा जिसका वह सामना करेगा, जब तक कि इसका वास्तव में परीक्षण न किया गया हो?
हमारे विचार में यह कुछ हद तक आश्चर्यजनक है कि स्मार्ट कॉन्ट्रैक्ट्स बिना यूनिट टेस्ट के तैनात (deploy) कर दिए जाते हैं। ऐसा नहीं होना चाहिए।
Solidity यूनिट टेस्ट पर हमारा ट्यूटोरियल यहाँ देखें।
15. गलत दिशा में राउंडिंग (Rounding) करना
यदि आप 100/3 को भाग देते हैं तो आपको 33 मिलेगा भले ही “सही” उत्तर 33.33333 है क्योंकि Solidity फ्लोट्स (floats) का समर्थन नहीं करती है। उस स्थिति में, आप जिस भी इकाई (unit) को माप रहे हैं उसका 0.3333 गायब हो गया है, क्योंकि जब भाग का उपयोग किया जाता है तो आपको “राउंड डाउन” करने के लिए मजबूर किया जाता है। भाग का सुनहरा नियम (golden rule) यहाँ है:
हमेशा इस तरह राउंड करें कि उपयोगकर्ता को नुकसान हो या प्रोटोकॉल को लाभ हो।
उदाहरण के लिए, यदि आप यह गणना कर रहे हैं कि किसी चीज़ के लिए उपयोगकर्ता को कितना भुगतान करने की आवश्यकता है, तो भाग करने से अनुमान (estimate) उस राशि से कम हो जाएगा जो होना चाहिए। उपरोक्त उदाहरण में, उपयोगकर्ता को 0.3333 की छूट मिलती है।
स्थिति 1: यह गणना करना कि प्रोटोकॉल कितना भुगतान करता है
यदि हम यह निर्धारित करने के लिए 100/3 की गणना कर रहे हैं कि स्मार्ट कॉन्ट्रैक्ट उपयोगकर्ता को कितना भुगतान करता है, तो स्मार्ट कॉन्ट्रैक्ट उपयोगकर्ता को कम भुगतान (underpay) करेगा। ऐसा करने का यही सही तरीका है। उपयोगकर्ता प्रोटोकॉल से मूल्य निकालने (bleed value) में सक्षम नहीं होगा।
स्थिति 2: यह गणना करना कि उपयोगकर्ता कितना भुगतान करता है
दूसरी ओर, यदि हम यह निर्धारित करने के लिए 100/3 की गणना कर रहे हैं कि उपयोगकर्ता को स्मार्ट कॉन्ट्रैक्ट को कितना भुगतान करना चाहिए, तो हमारे पास एक समस्या है, क्योंकि उपयोगकर्ता को जितना भुगतान करना चाहिए उससे 0.333 कम भुगतान करता है। यदि उपयोगकर्ता उस संपत्ति को 0.333 लाभ पर बेचने में सक्षम है, तो वे इस प्रक्रिया को तब तक दोहरा सकते हैं जब तक कि वे प्रोटोकॉल को खाली न कर दें!
इस परिस्थिति में सही काम यह है कि भागफल (division) में एक जोड़ दिया जाए ताकि हमने दशमलव (decimals) में जो खोया है वह वापस मिल जाए। अर्थात्, हमें यह गणना करनी चाहिए कि उपयोगकर्ता 100/3 + 1 के रूप में कितना भुगतान करता है, इसलिए उपयोगकर्ता को 33.333 मूल्य की संपत्ति के लिए 34 का भुगतान करना होगा। मूल्य की जो छोटी सी राशि उन्हें नहीं मिलेगी वह उन्हें स्मार्ट कॉन्ट्रैक्ट को लूटने से रोकेगी।
भिन्न (fractions) को ठीक से कैसे संभालना है, इसके बारे में हमारे फिक्स्ड-पॉइंट मैथ (fixed-point math) लेख में अधिक जानें।
16. फॉर्मेटर (formatter) का उपयोग न करना
Solidity कोड को फॉर्मेट करने के लिए पहिए का फिर से आविष्कार करने (reinvent the wheel) की कोई आवश्यकता नहीं है। आप Foundry में forge fmt का उपयोग कर सकते हैं या solfmt टूल का उपयोग कर सकते हैं। यह आपके कोड को समीक्षक (reviewer) के पढ़ने के लिए आसान बना देगा।
निम्नलिखित कोड पढ़ना अनावश्यक रूप से कठिन है:
contract GoodBank {
mapping(address=>uint256) public balances;
function withdraw () external {
uint256 balance=balances[msg.sender];
balances[msg.sender] = 0;
(bool ok,) =msg.sender.call{value: balance}("");
require(ok,"transfer failed");
}
receive() external payable {
balances[msg.sender]+=msg.value;
}
}
इसे फॉर्मेटर के माध्यम से चलाया जाना चाहिए ताकि स्पेसिंग अधिक समान (uniform) हो:
contract GoodBank {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 balance = balances[msg.sender];
balances[msg.sender] = 0;
(bool ok,) = msg.sender.call{value: balance}("");
require(ok, "transfer failed");
}
receive() external payable {
balances[msg.sender] += msg.value;
}
}
17. उन कॉन्ट्रैक्ट्स में _msgSender() का उपयोग करना जो मेटाट्रांजेक्शन (metatransactions) का समर्थन नहीं करते हैं
नए Solidity डेवलपर्स अक्सर OpenZeppelin कॉन्ट्रैक्ट्स में _msgSender() के बार-बार उपयोग से भ्रमित हो जाते हैं। उदाहरण के लिए, यहाँ _msgSender() का उपयोग करते हुए OpenZeppelin ERC-20 लाइब्रेरी है:

जब तक आप कोई ऐसा कॉन्ट्रैक्ट नहीं बना रहे हैं जो गैसलैस (gasless) या मेटाट्रांजेक्शन का समर्थन करता हो, तब तक _msgSender() के बजाय नियमित msg.sender का उपयोग करें।
_msgSender() OpenZeppelin कॉन्ट्रैक्ट Context.sol द्वारा बनाया गया एक फंक्शन है:

इसका उपयोग केवल उन कॉन्ट्रैक्ट्स में किया जाता है जो मेटाट्रांजेक्शन का समर्थन करते हैं।
मेटाट्रांजेक्शन या गैसलैस ट्रांजेक्शन वह होता है जहाँ एक रिलेयर (relayer) उपयोगकर्ता की ओर से ट्रांजेक्शन भेजता है और उनके लिए गैस का भुगतान करता है। चूँकि ट्रांजेक्शन एक रिलेयर से आया है, इसलिए msg.sender “मूल (original)” प्रेषक नहीं होगा। मेटाट्रांजेक्शन का उपयोग करने वाले स्मार्ट कॉन्ट्रैक्ट ट्रांजेक्शन में कहीं और “वास्तविक (true)” msg.sender को एन्कोड (encode) करते हैं और _msgSender() फंक्शन को ओवरराइड करके “वास्तविक” msg.sender को दर्शाते हैं।
यदि आप इनमें से कोई भी चीज़ नहीं कर रहे हैं, तो _msgSender() का उपयोग करने का कोई कारण नहीं है। इसके बजाय msg.sender का उपयोग करें।
18. गलती से Github पर API कीज़ (keys) या प्राइवेट कीज़ कमिट (commit) कर देना
हालाँकि हमने ऐसा बहुत बार होते नहीं देखा है, लेकिन ऐसा जितनी भी बार होता है, उसके परिणाम अत्यंत विनाशकारी होते हैं। यदि आप API कीज़ या प्राइवेट कीज़ को .env फ़ाइल में रखते हैं, तो .env फ़ाइल को हमेशा .gitignore फ़ाइल में जोड़ें।
19. फ्रंटरनिंग (frontrunning), स्लिपेज (slippage), या ट्रांजेक्शन साइनिंग और एग्जीक्यूशन के बीच की देरी को ध्यान में न रखना
Solidity कॉन्ट्रैक्ट्स के साथ फ्रंटरनिंग एक काउंटरइंट्यूटिव (counterintuitive) समस्या है क्योंकि इसके समरूप (analogs) वेब2 (web2) प्रोग्रामिंग में बहुत कम ही देखने को मिलते हैं।
उदाहरण 1: खरीदारी का ट्रांजेक्शन लंबित (pending) होने के दौरान कीमत बदलना
निम्नलिखित कॉन्ट्रैक्ट पर विचार करें, जो एक NFT के विक्रेता (seller) को एक ही ट्रांजेक्शन में USDC के लिए खरीदार के साथ स्वैप (swap) करने की अनुमति देता है। सैद्धांतिक रूप से इसका यह लाभ है कि किसी भी पक्ष (party) को पहले अपना टोकन नहीं भेजना पड़ता है और यह भरोसा नहीं करना पड़ता है कि प्रतिपक्ष (counterparty) अपना टोकन भेजेगा।
हालाँकि, इसमें फ्रंटरनिंग भेद्यता (vulnerability) है। जब स्वैप ट्रांजेक्शन लंबित (pending) होता है तो विक्रेता स्वैप की कीमत बदल सकता है।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
contract BadSwapERC20ForNFT is Ownable(msg.sender) {
using SafeERC20 for IERC20;
uint256 price;
IERC20 token;
IERC721 nft;
address public seller;
constructor(IERC721 nft_, IERC20 token_) {
nft = nft_;
token = token_;
seller = msg.sender;
}
function setPrice(uint256 price_) external {
require(msg.sender == seller, "only seller");
price = price_;
}
// the buyer calls this function
function atomicSwap(uint256 nftId) external
// requires both the seller and buyer
// to approve their tokens first
token.safeTransferFrom(msg.sender, owner(), price);
nft.transferFrom(owner(), msg.sender, nftId);
}
}
जब भी किसी उपयोगकर्ता से टोकन ट्रांसफर किए जा रहे हों, तो उपयोगकर्ता को हमेशा डेटा पास करने की आवश्यकता होनी चाहिए जो उस अधिकतम मात्रा को निर्दिष्ट करता हो जिसे वे भेजने के इच्छुक हैं ताकि खरीद ट्रांजेक्शन के लंबित होने के दौरान विक्रेता कीमत में बदलाव न कर सके।
उदाहरण 2: NFT जिसकी कीमत प्रत्येक खरीद के साथ बढ़ती है
निम्नलिखित NFT बिक्री (sales) को प्रत्येक खरीद के साथ कीमत को 5% तक बढ़ाने के लिए प्रोग्राम किया गया है। इसमें भी ऊपर वाले के समान ही एक समस्या है। जिस समय खरीदार ट्रांजेक्शन पर हस्ताक्षर करता है, वह कीमत ट्रांजेक्शन की पुष्टि (confirm) होने के समय की कीमत के समान नहीं हो सकती है। यदि 10 खरीदार एक ही समय में खरीदारी का ट्रांजेक्शन भेजते हैं, तो उनमें से 9 को अपनी अपेक्षा से अधिक कीमत चुकानी पड़ेगी।
जब कोई कॉन्ट्रैक्ट गणना करता है कि उपयोगकर्ता से कितने टोकन ट्रांसफर करने हैं, तो उपयोगकर्ता को उनके अकाउंट से ट्रांसफर की जाने वाली अधिकतम सीमा निर्दिष्ट करनी चाहिए।
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@5.0.0/access/Ownable2Step.sol";
contract BadNFTSale is ERC721("BadNFT", "BNFT"), Ownable(msg.sender) {
using SafeERC20 for IERC20;
uint256 price = 100e6; // USDC / USDT have 6 decimals
IERC20 immutable token;
uint256 id;
constructor(IERC20 token_) {
token = token_;
}
function buyNFT() external {
token.safeTransferFrom(msg.sender, owner(), price);
price = price * 105 / 100;
id++;
_mint(msg.sender, id);
}
}
यहाँ एक और सूक्ष्म (subtle) समस्या है: खरीदार का ट्रांजेक्शन अभी भी लंबित रहने के दौरान मालिक टोकन बदल सकता है! अब, यह असंभव है कि खरीदार ने नए टोकन के लिए कॉन्ट्रैक्ट को मंजूरी दी हो, इसलिए transferFrom संभवतः विफल हो जाएगा। लेकिन अधिक जटिल कॉन्ट्रैक्ट में जहाँ यथार्थ रूप से कई अप्रूवल्स हो सकते हैं, यह ध्यान रखने योग्य एक समस्या होगी।
20. ऐसे फंक्शन जो यूजर्स द्वारा एक ही ट्रांजेक्शन को कई बार करने की संभावना पर विचार नहीं करते
स्मार्ट कॉन्ट्रैक्ट्स को इस संभावना का ध्यान रखना चाहिए कि कोई उपयोगकर्ता एक ही ट्रांजेक्शन को एक से अधिक बार कर सकता है। निम्नलिखित उदाहरण पर विचार करें:
contract DepositAndWithdraw {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] = msg.value;
}
function withdraw(
uint256 amount
) external {
require(
amount <= balances[msg.sender],
"insufficient balance"
);
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
}
यदि deposit को दो बार कॉल किया जाता है, तो पहला बैलेंस दूसरे ट्रांजेक्शन द्वारा ओवरराइट (overwrite) कर दिया जाएगा और वह पैसा खो जाएगा। उदाहरण के लिए, यदि उपयोगकर्ता 1 ETH के मान के साथ deposit() को कॉल करता है, फिर 2 ETH के मान के साथ फिर से deposit() को कॉल करता है, तो उस एड्रेस का बैलेंस 2 ETH होगा, भले ही उसने 3 ETH जमा किए हों। इसे ठीक करने का तरीका बैलेंस को बढ़ाना (increment) है, यानी, balances[msg.sender] += msg.value;।