सुरक्षा उपायों (Safety Rails) के साथ ERC-20
परिचय
इथेरियम (Ethereum) के बारे में एक अच्छी बात यह है कि इसमें कोई केंद्रीय प्राधिकरण नहीं है जो आपके लेन-देन को संशोधित या रद्द कर सके। इथेरियम के साथ एक बड़ी समस्या यह भी है कि इसमें कोई केंद्रीय प्राधिकरण नहीं है जिसके पास उपयोगकर्ता की गलतियों या अवैध लेन-देन को रद्द करने की शक्ति हो। इस लेख में आप उन कुछ सामान्य गलतियों के बारे में जानेंगे जो उपयोगकर्ता ERC-20 टोकन के साथ करते हैं, साथ ही यह भी जानेंगे कि ऐसे ERC-20 अनुबंध कैसे बनाए जाएं जो उपयोगकर्ताओं को उन गलतियों से बचने में मदद करें, या जो किसी केंद्रीय प्राधिकरण को कुछ शक्ति दें (उदाहरण के लिए खातों को फ्रीज करने के लिए)।
ध्यान दें कि हालांकि हम ओपनजेपेलिन (OpenZeppelin) ERC-20 टोकन अनुबंध (opens in a new tab) का उपयोग करेंगे, यह लेख इसे बहुत विस्तार से नहीं समझाता है। आप यह जानकारी यहाँ पा सकते हैं।
यदि आप पूरा सोर्स कोड देखना चाहते हैं:
- Remix IDE (opens in a new tab) खोलें।
- क्लोन GitHub आइकन (
) पर क्लिक करें। - GitHub रिपॉजिटरी
https://github.com/qbzzt/20220815-erc20-safety-railsको क्लोन करें। - contracts > erc20-safety-rails.sol खोलें।
एक ERC-20 अनुबंध बनाना
सुरक्षा उपाय (safety rail) कार्यक्षमता जोड़ने से पहले हमें एक ERC-20 अनुबंध की आवश्यकता है। इस लेख में हम ओपनजेपेलिन कॉन्ट्रैक्ट्स विज़ार्ड (OpenZeppelin Contracts Wizard) (opens in a new tab) का उपयोग करेंगे। इसे किसी अन्य ब्राउज़र में खोलें और इन निर्देशों का पालन करें:
-
ERC20 चुनें।
-
ये सेटिंग्स दर्ज करें:
पैरामीटर मान नाम SafetyRailsToken प्रतीक SAFE प्रीमिंट 1000 विशेषताएँ कोई नहीं एक्सेस कंट्रोल Ownable अपग्रेडेबिलिटी कोई नहीं -
ऊपर स्क्रॉल करें और Open in Remix (Remix के लिए) पर क्लिक करें या किसी अन्य वातावरण का उपयोग करने के लिए Download पर क्लिक करें। मैं यह मानकर चल रहा हूँ कि आप Remix का उपयोग कर रहे हैं, यदि आप किसी अन्य चीज़ का उपयोग करते हैं तो बस उचित बदलाव करें।
-
अब हमारे पास पूरी तरह से काम करने वाला ERC-20 अनुबंध है। आप आयात किए गए कोड को देखने के लिए
.deps>npmका विस्तार कर सकते हैं। -
अनुबंध को संकलित (compile) करें, तैनात करें (deploy), और इसके साथ प्रयोग करके देखें कि यह एक ERC-20 अनुबंध के रूप में कार्य करता है। यदि आपको यह सीखने की आवश्यकता है कि Remix का उपयोग कैसे करें, तो इस ट्यूटोरियल का उपयोग करें (opens in a new tab)।
सामान्य गलतियाँ
गलतियाँ
उपयोगकर्ता कभी-कभी गलत पते पर टोकन भेज देते हैं। हालांकि हम यह जानने के लिए उनका दिमाग नहीं पढ़ सकते कि वे क्या करना चाहते थे, दो प्रकार की त्रुटियां हैं जो बहुत होती हैं और जिनका पता लगाना आसान है:
-
अनुबंध के स्वयं के पते पर टोकन भेजना। उदाहरण के लिए, ऑप्टिमिज़्म (Optimism) के OP टोकन (opens in a new tab) ने दो महीने से भी कम समय में 120,000 से अधिक (opens in a new tab) OP टोकन जमा कर लिए। यह धन की एक महत्वपूर्ण राशि का प्रतिनिधित्व करता है जिसे संभवतः लोगों ने खो दिया है।
-
टोकन को एक खाली पते पर भेजना, जो किसी बाहरी रूप से स्वामित्व वाले खाते (externally owned account - EOA) या स्मार्ट अनुबंध से मेल नहीं खाता है। हालांकि मेरे पास इस बात के आंकड़े नहीं हैं कि ऐसा कितनी बार होता है, एक घटना में 20,000,000 टोकन का नुकसान हो सकता था (opens in a new tab)।
ट्रांसफर को रोकना
ओपनजेपेलिन ERC-20 अनुबंध में एक हुक, _beforeTokenTransfer (opens in a new tab) शामिल है, जिसे टोकन ट्रांसफर होने से पहले कॉल किया जाता है। डिफ़ॉल्ट रूप से यह हुक कुछ नहीं करता है, लेकिन हम इस पर अपनी कार्यक्षमता जोड़ सकते हैं, जैसे कि ऐसे चेक जो कोई समस्या होने पर रिवर्ट कर देते हैं।
हुक का उपयोग करने के लिए, कंस्ट्रक्टर के बाद इस फ़ंक्शन को जोड़ें:
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal virtual
override(ERC20)
{
super._beforeTokenTransfer(from, to, amount);
}
यदि आप Solidity से बहुत परिचित नहीं हैं तो इस फ़ंक्शन के कुछ भाग नए हो सकते हैं:
internal virtual
virtual कीवर्ड का अर्थ है कि जिस तरह हमने ERC20 से कार्यक्षमता प्राप्त की और इस फ़ंक्शन को ओवरराइड किया, उसी तरह अन्य अनुबंध हमसे इनहेरिट कर सकते हैं और इस फ़ंक्शन को ओवरराइड कर सकते हैं।
override(ERC20)
हमें स्पष्ट रूप से निर्दिष्ट करना होगा कि हम _beforeTokenTransfer की ERC20 टोकन परिभाषा को ओवरराइड (opens in a new tab) कर रहे हैं। सामान्य तौर पर, सुरक्षा के दृष्टिकोण से, स्पष्ट परिभाषाएँ अंतर्निहित परिभाषाओं की तुलना में बहुत बेहतर होती हैं - यदि कोई चीज़ आपके ठीक सामने है तो आप यह नहीं भूल सकते कि आपने कुछ किया है। यही कारण है कि हमें यह निर्दिष्ट करने की आवश्यकता है कि हम किस सुपरक्लास के _beforeTokenTransfer को ओवरराइड कर रहे हैं।
super._beforeTokenTransfer(from, to, amount);
यह लाइन उस अनुबंध या अनुबंधों के _beforeTokenTransfer फ़ंक्शन को कॉल करती है जिनसे हमने इनहेरिट किया है और जिनमें यह मौजूद है। इस मामले में, यह केवल ERC20 है, Ownable में यह हुक नहीं है। भले ही वर्तमान में ERC20._beforeTokenTransfer कुछ नहीं करता है, हम इसे इस स्थिति के लिए कॉल करते हैं कि भविष्य में कार्यक्षमता जोड़ी जाए (और फिर हम अनुबंध को फिर से तैनात करने का निर्णय लें, क्योंकि तैनाती के बाद अनुबंध नहीं बदलते हैं)।
आवश्यकताओं की कोडिंग
हम फ़ंक्शन में इन आवश्यकताओं को जोड़ना चाहते हैं:
toपताaddress(this)के बराबर नहीं हो सकता, जो कि स्वयं ERC-20 अनुबंध का पता है।toपता खाली नहीं हो सकता, यह इनमें से एक होना चाहिए:- एक बाहरी रूप से स्वामित्व वाला खाता (EOA)। हम सीधे यह जांच नहीं कर सकते कि कोई पता EOA है या नहीं, लेकिन हम किसी पते के ETH बैलेंस की जांच कर सकते हैं। EOA में लगभग हमेशा बैलेंस होता है, भले ही उनका अब उपयोग न किया जा रहा हो - उन्हें अंतिम Wei तक खाली करना मुश्किल है।
- एक स्मार्ट अनुबंध। यह जाँचना कि कोई पता स्मार्ट अनुबंध है या नहीं, थोड़ा कठिन है। एक ऑपकोड है जो बाहरी कोड की लंबाई की जांच करता है, जिसे
EXTCODESIZE(opens in a new tab) कहा जाता है, लेकिन यह सीधे Solidity में उपलब्ध नहीं है। इसके लिए हमें Yul (opens in a new tab) का उपयोग करना होगा, जो EVM असेंबली है। अन्य मान भी हैं जिनका हम Solidity से उपयोग कर सकते हैं (<address>.codeऔर<address>.codehash(opens in a new tab)), लेकिन उनकी लागत अधिक होती है।
आइए नए कोड को लाइन दर लाइन समझें:
require(to != address(this), "Can't send tokens to the contract address");
यह पहली आवश्यकता है, जांचें कि to और this(address) एक ही चीज़ नहीं हैं।
bool isToContract;
assembly {
isToContract := gt(extcodesize(to), 0)
}
इस तरह हम जांचते हैं कि कोई पता अनुबंध है या नहीं। हम सीधे Yul से आउटपुट प्राप्त नहीं कर सकते, इसलिए इसके बजाय हम परिणाम को रखने के लिए एक चर (variable) परिभाषित करते हैं (इस मामले में isToContract)। Yul जिस तरह से काम करता है वह यह है कि हर ऑपकोड को एक फ़ंक्शन माना जाता है। इसलिए पहले हम अनुबंध का आकार प्राप्त करने के लिए EXTCODESIZE (opens in a new tab) को कॉल करते हैं, और फिर यह जांचने के लिए GT (opens in a new tab) का उपयोग करते हैं कि यह शून्य नहीं है (हम अनसाइंड इंटिजर्स के साथ काम कर रहे हैं, इसलिए निश्चित रूप से यह नकारात्मक नहीं हो सकता)। फिर हम परिणाम को isToContract में लिखते हैं।
require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
और अंत में, हमारे पास खाली पतों के लिए वास्तविक जांच है।
प्रशासनिक एक्सेस
कभी-कभी एक प्रशासक का होना उपयोगी होता है जो गलतियों को सुधार सके। दुरुपयोग की संभावना को कम करने के लिए, यह प्रशासक एक मल्टीसिग (opens in a new tab) हो सकता है ताकि कई लोगों को किसी कार्रवाई पर सहमत होना पड़े। इस लेख में हमारे पास दो प्रशासनिक विशेषताएं होंगी:
-
खातों को फ्रीज और अनफ्रीज करना। यह उपयोगी हो सकता है, उदाहरण के लिए, जब किसी खाते से समझौता (compromise) किया गया हो।
-
एसेट क्लीनअप (संपत्ति की सफाई)।
कभी-कभी धोखेबाज वैधता हासिल करने के लिए असली टोकन के अनुबंध में धोखाधड़ी वाले टोकन भेजते हैं। उदाहरण के लिए, यहाँ देखें (opens in a new tab)। वैध ERC-20 अनुबंध 0x4200....0042 (opens in a new tab) है। जो घोटाला इसका दिखावा करता है वह 0x234....bbe (opens in a new tab) है।
यह भी संभव है कि लोग गलती से हमारे अनुबंध में वैध ERC-20 टोकन भेज दें, जो उन्हें बाहर निकालने का एक तरीका चाहने का एक और कारण है।
ओपनजेपेलिन प्रशासनिक एक्सेस को सक्षम करने के लिए दो तंत्र प्रदान करता है:
Ownable(opens in a new tab) अनुबंधों का एक ही मालिक होता है। जिन फ़ंक्शन्स मेंonlyOwnerमॉडिफायर (opens in a new tab) होता है, उन्हें केवल उसी मालिक द्वारा कॉल किया जा सकता है। मालिक स्वामित्व को किसी और को ट्रांसफर कर सकते हैं या इसे पूरी तरह से त्याग सकते हैं। अन्य सभी खातों के अधिकार आमतौर पर समान होते हैं।AccessControl(opens in a new tab) अनुबंधों में रोल बेस्ड एक्सेस कंट्रोल (RBAC) (opens in a new tab) होता है।
सरलता के लिए, इस लेख में हम Ownable का उपयोग करते हैं।
अनुबंधों को फ्रीज और अनफ्रीज (thaw) करना
अनुबंधों को फ्रीज और अनफ्रीज करने के लिए कई बदलावों की आवश्यकता होती है:
-
यह ट्रैक रखने के लिए कि कौन से पते फ्रीज हैं, पतों से बूलियन (booleans) (opens in a new tab) तक एक मैपिंग (opens in a new tab)। सभी मान शुरू में शून्य होते हैं, जिसे बूलियन मानों के लिए 'false' के रूप में समझा जाता है। हम यही चाहते हैं क्योंकि डिफ़ॉल्ट रूप से खाते फ्रीज नहीं होते हैं।
mapping(address => bool) public frozenAccounts; -
किसी खाते के फ्रीज या अनफ्रीज होने पर किसी भी इच्छुक व्यक्ति को सूचित करने के लिए घटनाएँ (Events) (opens in a new tab)। तकनीकी रूप से इन कार्यों के लिए घटनाओं की आवश्यकता नहीं होती है, लेकिन यह ऑफचेन कोड को इन घटनाओं को सुनने और यह जानने में मदद करता है कि क्या हो रहा है। जब कुछ ऐसा होता है जो किसी और के लिए प्रासंगिक हो सकता है, तो स्मार्ट अनुबंध के लिए उन्हें उत्सर्जित (emit) करना अच्छा शिष्टाचार माना जाता है।
घटनाओं को अनुक्रमित (indexed) किया जाता है ताकि उन सभी समयों को खोजना संभव हो सके जब किसी खाते को फ्रीज या अनफ्रीज किया गया हो।
// जब खाते फ्रीज या अनफ्रीज किए जाते हैं event AccountFrozen(address indexed _addr); event AccountThawed(address indexed _addr); -
खातों को फ्रीज और अनफ्रीज करने के लिए फ़ंक्शन्स। ये दोनों फ़ंक्शन लगभग समान हैं, इसलिए हम केवल फ्रीज फ़ंक्शन पर चर्चा करेंगे।
function freezeAccount(address addr) public onlyOwnerpublic(opens in a new tab) के रूप में चिह्नित फ़ंक्शन्स को अन्य स्मार्ट अनुबंधों से या सीधे किसी लेन-देन द्वारा कॉल किया जा सकता है।{ require(!frozenAccounts[addr], "Account already frozen"); frozenAccounts[addr] = true; emit AccountFrozen(addr); } // freezeAccountयदि खाता पहले से ही फ्रीज है, तो रिवर्ट करें। अन्यथा, इसे फ्रीज करें और एक घटना
emitकरें। -
किसी फ्रीज किए गए खाते से पैसे को स्थानांतरित होने से रोकने के लिए
_beforeTokenTransferको बदलें। ध्यान दें कि पैसे अभी भी फ्रीज किए गए खाते में ट्रांसफर किए जा सकते हैं।require(!frozenAccounts[from], "The account is frozen");
एसेट क्लीनअप
इस अनुबंध द्वारा रखे गए ERC-20 टोकन को जारी करने के लिए हमें उस टोकन अनुबंध पर एक फ़ंक्शन कॉल करने की आवश्यकता है जिससे वे संबंधित हैं, या तो transfer (opens in a new tab) या approve (opens in a new tab)। इस मामले में भत्ते (allowances) पर गैस बर्बाद करने का कोई मतलब नहीं है, हम सीधे ट्रांसफर भी कर सकते हैं।
function cleanupERC20(
address erc20,
address dest
)
public
onlyOwner
{
IERC20 token = IERC20(erc20);
जब हमें पता प्राप्त होता है तो अनुबंध के लिए एक ऑब्जेक्ट बनाने का यह सिंटैक्स है। हम ऐसा इसलिए कर सकते हैं क्योंकि हमारे पास सोर्स कोड के हिस्से के रूप में ERC20 टोकन की परिभाषा है (लाइन 4 देखें), और उस फ़ाइल में IERC20 के लिए परिभाषा (opens in a new tab) शामिल है, जो ओपनजेपेलिन ERC-20 अनुबंध के लिए इंटरफ़ेस है।
uint balance = token.balanceOf(address(this));
token.transfer(dest, balance);
}
यह एक क्लीनअप फ़ंक्शन है, इसलिए संभवतः हम कोई टोकन नहीं छोड़ना चाहते हैं। उपयोगकर्ता से मैन्युअल रूप से बैलेंस प्राप्त करने के बजाय, हम इस प्रक्रिया को स्वचालित भी कर सकते हैं।
निष्कर्ष
यह एक आदर्श समाधान नहीं है - "उपयोगकर्ता ने गलती की" समस्या का कोई आदर्श समाधान नहीं है। हालाँकि, इस प्रकार के चेक का उपयोग करने से कम से कम कुछ गलतियों को रोका जा सकता है। खातों को फ्रीज करने की क्षमता, हालांकि खतरनाक है, हैकर को चोरी किए गए फंड से वंचित करके कुछ हैक के नुकसान को सीमित करने के लिए उपयोग की जा सकती है।