सुरक्षा उपायांसह ERC-20
परिचय
इथेरियमची एक उत्तम गोष्ट म्हणजे तुमचे व्यवहार बदलू किंवा रद्द करू शकेल असा कोणताही केंद्रीय प्राधिकरण (central authority) नाही. इथेरियमची एक मोठी समस्या ही आहे की वापरकर्त्यांच्या चुका किंवा बेकायदेशीर व्यवहार रद्द करण्याचा अधिकार असलेल्या कोणत्याही केंद्रीय प्राधिकरणाचा अभाव आहे. या लेखात तुम्ही वापरकर्ते ERC-20 टोकन वापरताना करत असलेल्या काही सामान्य चुकांबद्दल शिकाल, तसेच वापरकर्त्यांना त्या चुका टाळण्यास मदत करणारे किंवा केंद्रीय प्राधिकरणाला काही अधिकार (उदा. खाती गोठवणे) देणारे ERC-20 कॉन्ट्रॅक्ट कसे तयार करावे हे शिकाल.
लक्षात घ्या की आम्ही ओपनझेपलिन ERC-20 टोकन कॉन्ट्रॅक्ट (नवीन टॅबमध्ये उघडते) वापरणार असलो तरी, हा लेख त्याचे सविस्तर स्पष्टीकरण देत नाही. तुम्ही ही माहिती येथे शोधू शकता.
जर तुम्हाला संपूर्ण सोर्स कोड पाहायचा असेल:
- Remix IDE (नवीन टॅबमध्ये उघडते) उघडा.
- क्लोन GitHub आयकॉनवर क्लिक करा (
). https://github.com/qbzzt/20220815-erc20-safety-railsहे GitHub रिपॉझिटरी क्लोन करा.- contracts > erc20-safety-rails.sol उघडा.
ERC-20 कॉन्ट्रॅक्ट तयार करणे
सुरक्षा उपायांची कार्यक्षमता जोडण्यापूर्वी आपल्याला ERC-20 कॉन्ट्रॅक्टची आवश्यकता आहे. या लेखात आम्ही ओपनझेपलिन कॉन्ट्रॅक्ट्स विझार्ड (नवीन टॅबमध्ये उघडते) वापरू. ते दुसऱ्या ब्राउझरमध्ये उघडा आणि या सूचनांचे पालन करा:
-
ERC20 निवडा.
-
या सेटिंग्ज प्रविष्ट करा:
पॅरामीटर मूल्य Name SafetyRailsToken Symbol SAFE Premint 1000 Features None Access Control Ownable Upgradability None -
वर स्क्रोल करा आणि Open in Remix (Remix साठी) किंवा वेगळे वातावरण वापरण्यासाठी Download वर क्लिक करा. मी असे गृहीत धरत आहे की तुम्ही Remix वापरत आहात, जर तुम्ही दुसरे काही वापरत असाल तर योग्य ते बदल करा.
-
आता आपल्याकडे पूर्णपणे कार्यशील ERC-20 कॉन्ट्रॅक्ट आहे. आयात केलेला कोड पाहण्यासाठी तुम्ही
.deps>npmविस्तृत करू शकता. -
कॉन्ट्रॅक्ट ERC-20 कॉन्ट्रॅक्ट म्हणून कार्य करते हे पाहण्यासाठी ते संकलित (compile) करा, प्रस्थापित करा आणि त्याच्यासोबत प्रयोग करा. जर तुम्हाला Remix कसे वापरायचे हे शिकायचे असेल, तर हे ट्युटोरियल वापरा (नवीन टॅबमध्ये उघडते).
सामान्य चुका
चुका
वापरकर्ते कधीकधी चुकीच्या पत्त्यावर टोकन पाठवतात. त्यांना नेमके काय करायचे होते हे जाणून घेण्यासाठी आपण त्यांचे मन वाचू शकत नसलो तरी, दोन प्रकारच्या चुका खूप वेळा घडतात आणि त्या शोधणे सोपे आहे:
-
कॉन्ट्रॅक्टच्या स्वतःच्या पत्त्यावर टोकन पाठवणे. उदाहरणार्थ, ऑप्टिमिझम् च्या OP टोकनने (नवीन टॅबमध्ये उघडते) दोन महिन्यांपेक्षा कमी कालावधीत 120,000 पेक्षा जास्त (नवीन टॅबमध्ये उघडते) OP टोकन जमा केले. हे संपत्तीचे एक महत्त्वपूर्ण प्रमाण दर्शवते जे बहुधा लोकांनी गमावले आहे.
-
रिकाम्या पत्त्यावर टोकन पाठवणे, जो बाह्य मालकीच्या खात्याशी (externally owned account) किंवा स्मार्ट कॉन्ट्रॅक्टशी संबंधित नाही. हे किती वेळा घडते याची आकडेवारी माझ्याकडे नसली तरी, एका घटनेत 20,000,000 टोकन्सचे नुकसान होऊ शकले असते (नवीन टॅबमध्ये उघडते).
हस्तांतरण रोखणे
ओपनझेपलिन ERC-20 कॉन्ट्रॅक्टमध्ये एक हूक, _beforeTokenTransfer (नवीन टॅबमध्ये उघडते) समाविष्ट आहे, जो टोकन हस्तांतरित होण्यापूर्वी कॉल केला जातो. डीफॉल्टनुसार हा हूक काहीही करत नाही, परंतु आपण त्यावर आपली स्वतःची कार्यक्षमता जोडू शकतो, जसे की काही समस्या असल्यास पूर्ववत करणारे (revert) तपासणी (checks).
हा हूक वापरण्यासाठी, कन्स्ट्रक्टर नंतर हे फंक्शन जोडा:
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal virtual
override(ERC20)
{
super._beforeTokenTransfer(from, to, amount);
}
जर तुम्हाला Solidity ची फारशी माहिती नसेल तर या फंक्शनचे काही भाग नवीन असू शकतात:
internal virtual
virtual कीवर्डचा अर्थ असा आहे की ज्याप्रमाणे आपण ERC20 कडून कार्यक्षमता वारशाने मिळवली (inherited) आणि हे फंक्शन ओव्हरराइड केले, त्याचप्रमाणे इतर कॉन्ट्रॅक्ट्स आपल्याकडून वारसा मिळवू शकतात आणि हे फंक्शन ओव्हरराइड करू शकतात.
override(ERC20)
आपण _beforeTokenTransfer ची ERC20 टोकन व्याख्या ओव्हरराइड (नवीन टॅबमध्ये उघडते) करत आहोत हे आपल्याला स्पष्टपणे नमूद करावे लागेल. सर्वसाधारणपणे, सुरक्षिततेच्या दृष्टिकोनातून, अस्पष्ट (implicit) व्याख्यांपेक्षा स्पष्ट (explicit) व्याख्या खूप चांगल्या असतात - जर एखादी गोष्ट तुमच्या समोरच असेल तर तुम्ही ती केली आहे हे तुम्ही विसरू शकत नाही. हेच कारण आहे की आपण कोणत्या सुपरक्लासचे _beforeTokenTransfer ओव्हरराइड करत आहोत हे नमूद करणे आवश्यक आहे.
super._beforeTokenTransfer(from, to, amount);
ही ओळ त्या कॉन्ट्रॅक्टच्या किंवा कॉन्ट्रॅक्ट्सच्या _beforeTokenTransfer फंक्शनला कॉल करते ज्यांच्याकडून आपण वारसा मिळवला आहे आणि ज्यांच्याकडे ते आहे. या प्रकरणात, ते फक्त ERC20 आहे, Ownable कडे हा हूक नाही. जरी सध्या ERC20._beforeTokenTransfer काहीही करत नसले तरी, भविष्यात कार्यक्षमता जोडली गेल्यास आपण त्याला कॉल करतो (आणि नंतर आपण कॉन्ट्रॅक्ट पुन्हा प्रस्थापित करण्याचे ठरवतो, कारण प्रस्थापनेनंतर कॉन्ट्रॅक्ट्स बदलत नाहीत).
आवश्यकतांचे कोडिंग करणे
आम्हाला फंक्शनमध्ये या आवश्यकता जोडायच्या आहेत:
toपत्ताaddress(this)च्या समान असू शकत नाही, जो स्वतः ERC-20 कॉन्ट्रॅक्टचा पत्ता आहे.toपत्ता रिकामा असू शकत नाही, तो यापैकी एक असणे आवश्यक आहे:- बाह्य मालकीचे खाते (EOA). एखादा पत्ता EOA आहे की नाही हे आपण थेट तपासू शकत नाही, परंतु आपण पत्त्याची ETH शिल्लक तपासू शकतो. EOA मध्ये जवळजवळ नेहमीच शिल्लक असते, जरी ते यापुढे वापरले जात नसले तरीही - त्यांना शेवटच्या Wei पर्यंत साफ करणे कठीण आहे.
- एक स्मार्ट कॉन्ट्रॅक्ट. एखादा पत्ता स्मार्ट कॉन्ट्रॅक्ट आहे की नाही हे तपासणे थोडे कठीण आहे. बाह्य कोडची लांबी तपासणारा एक ऑपकोड आहे, ज्याला
EXTCODESIZE(नवीन टॅबमध्ये उघडते) म्हणतात, परंतु तो थेट Solidity मध्ये उपलब्ध नाही. त्यासाठी आपल्याला Yul (नवीन टॅबमध्ये उघडते) वापरावे लागेल, जे EVM असेंब्ली आहे. आपण Solidity मधून इतर मूल्ये वापरू शकतो (<address>.codeआणि<address>.codehash(नवीन टॅबमध्ये उघडते)), परंतु त्यासाठी जास्त खर्च येतो.
चला नवीन कोड ओळीनुसार पाहूया:
require(to != address(this), "Can't send tokens to the contract address");
ही पहिली आवश्यकता आहे, to आणि this(address) समान नाहीत हे तपासा.
bool isToContract;
assembly {
isToContract := gt(extcodesize(to), 0)
}
अशा प्रकारे आपण एखादा पत्ता कॉन्ट्रॅक्ट आहे की नाही हे तपासतो. आपण Yul कडून थेट आउटपुट प्राप्त करू शकत नाही, म्हणून त्याऐवजी आपण निकाल ठेवण्यासाठी एक व्हेरिएबल परिभाषित करतो (या प्रकरणात isToContract). Yul ज्या प्रकारे कार्य करते ते म्हणजे प्रत्येक ऑपकोड एक फंक्शन मानले जाते. म्हणून प्रथम आपण कॉन्ट्रॅक्टचा आकार मिळवण्यासाठी EXTCODESIZE (नवीन टॅबमध्ये उघडते) ला कॉल करतो आणि नंतर ते शून्य नाही हे तपासण्यासाठी GT (नवीन टॅबमध्ये उघडते) वापरतो (आपण अनसाईन्ड इंटिजर्स हाताळत आहोत, त्यामुळे अर्थातच ते नकारात्मक असू शकत नाही). त्यानंतर आपण निकाल isToContract मध्ये लिहितो.
require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
आणि शेवटी, आपल्याकडे रिकाम्या पत्त्यांसाठी प्रत्यक्ष तपासणी आहे.
प्रशासकीय प्रवेश
कधीकधी चुका पूर्ववत करू शकणारा प्रशासक असणे उपयुक्त ठरते. गैरवापराची शक्यता कमी करण्यासाठी, हा प्रशासक मल्टीसिग (नवीन टॅबमध्ये उघडते) असू शकतो जेणेकरून एका कृतीवर अनेक लोकांना सहमत व्हावे लागेल. या लेखात आपल्याकडे दोन प्रशासकीय वैशिष्ट्ये असतील:
-
खाती गोठवणे (Freezing) आणि पूर्ववत करणे (unfreezing). हे उपयुक्त ठरू शकते, उदाहरणार्थ, जेव्हा एखाद्या खात्याशी तडजोड (compromised) झाली असेल.
-
मालमत्ता स्वच्छता (Asset cleanup).
कधीकधी फसवणूक करणारे कायदेशीरपणा मिळवण्यासाठी खऱ्या टोकनच्या कॉन्ट्रॅक्टवर फसव्या टोकन्स पाठवतात. उदाहरणार्थ, येथे पहा (नवीन टॅबमध्ये उघडते). कायदेशीर ERC-20 कॉन्ट्रॅक्ट 0x4200....0042 (नवीन टॅबमध्ये उघडते) आहे. जो घोटाळा तसे असल्याचे भासवतो तो 0x234....bbe (नवीन टॅबमध्ये उघडते) आहे.
असेही होऊ शकते की लोक चुकून आपल्या कॉन्ट्रॅक्टवर कायदेशीर ERC-20 टोकन पाठवतात, जे त्यांना बाहेर काढण्याचा मार्ग हवा असण्याचे आणखी एक कारण आहे.
प्रशासकीय प्रवेश सक्षम करण्यासाठी ओपनझेपलिन दोन यंत्रणा प्रदान करते:
Ownable(नवीन टॅबमध्ये उघडते) कॉन्ट्रॅक्ट्सचा एकच मालक असतो. ज्या फंक्शन्समध्येonlyOwnerमॉडिफायर (नवीन टॅबमध्ये उघडते) असतो त्यांना फक्त तोच मालक कॉल करू शकतो. मालक मालकी दुसऱ्या कोणाकडे हस्तांतरित करू शकतात किंवा ती पूर्णपणे सोडून देऊ शकतात. इतर सर्व खात्यांचे अधिकार सामान्यतः समान असतात.AccessControl(नवीन टॅबमध्ये उघडते) कॉन्ट्रॅक्ट्समध्ये भूमिका आधारित प्रवेश नियंत्रण (role based access control - RBAC) (नवीन टॅबमध्ये उघडते) असते.
सोपेपणासाठी, या लेखात आपण Ownable वापरतो.
कॉन्ट्रॅक्ट्स गोठवणे आणि पूर्ववत करणे (thawing)
कॉन्ट्रॅक्ट्स गोठवण्यासाठी आणि पूर्ववत करण्यासाठी अनेक बदलांची आवश्यकता असते:
-
कोणते पत्ते गोठवले आहेत याचा मागोवा ठेवण्यासाठी पत्त्यांपासून बुलियन्सपर्यंत (नवीन टॅबमध्ये उघडते) एक मॅपिंग (नवीन टॅबमध्ये उघडते). सर्व मूल्ये सुरुवातीला शून्य असतात, ज्याचा बुलियन मूल्यांसाठी अर्थ 'खोटे' (false) असा होतो. आपल्याला हेच हवे आहे कारण डीफॉल्टनुसार खाती गोठवलेली नसतात.
mapping(address => bool) public frozenAccounts; -
जेव्हा एखादे खाते गोठवले जाते किंवा पूर्ववत केले जाते तेव्हा स्वारस्य असलेल्या कोणालाही माहिती देण्यासाठी घटना (नवीन टॅबमध्ये उघडते). तांत्रिकदृष्ट्या या कृतींसाठी घटना आवश्यक नाहीत, परंतु साखळीबाह्य (offchain) कोडला या घटना ऐकण्यास आणि काय घडत आहे हे जाणून घेण्यास मदत होते. जेव्हा दुसऱ्या कोणासाठी तरी संबंधित असू शकेल असे काहीतरी घडते तेव्हा स्मार्ट कॉन्ट्रॅक्टने त्यांना उत्सर्जित (emit) करणे हे चांगले शिष्टाचार मानले जाते.
घटना अनुक्रमित (indexed) केल्या आहेत जेणेकरून खाते किती वेळा गोठवले गेले किंवा पूर्ववत केले गेले हे शोधणे शक्य होईल.
// जेव्हा खाती गोठवली जातात किंवा पूर्ववत केली जातात event AccountFrozen(address indexed _addr); event AccountThawed(address indexed _addr); -
खाती गोठवण्यासाठी आणि पूर्ववत करण्यासाठी फंक्शन्स. ही दोन फंक्शन्स जवळजवळ सारखीच आहेत, त्यामुळे आपण फक्त फ्रीझ फंक्शन पाहू.
function freezeAccount(address addr) public onlyOwnerpublic(नवीन टॅबमध्ये उघडते) चिन्हांकित केलेली फंक्शन्स इतर स्मार्ट कॉन्ट्रॅक्ट्समधून किंवा थेट व्यवहाराद्वारे कॉल केली जाऊ शकतात.{ 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 (नवीन टॅबमध्ये उघडते) किंवा approve (नवीन टॅबमध्ये उघडते). या प्रकरणात भत्त्यांवर (allowances) गॅस वाया घालवण्यात काही अर्थ नाही, आपण थेट हस्तांतरण करू शकतो.
function cleanupERC20(
address erc20,
address dest
)
public
onlyOwner
{
IERC20 token = IERC20(erc20);
जेव्हा आपल्याला पत्ता प्राप्त होतो तेव्हा कॉन्ट्रॅक्टसाठी ऑब्जेक्ट तयार करण्याचा हा सिंटॅक्स आहे. आपण हे करू शकतो कारण आपल्याकडे सोर्स कोडचा भाग म्हणून ERC20 टोकन्सची व्याख्या आहे (ओळ 4 पहा), आणि त्या फाईलमध्ये IERC20 ची व्याख्या (नवीन टॅबमध्ये उघडते) समाविष्ट आहे, जो ओपनझेपलिन ERC-20 कॉन्ट्रॅक्टसाठी इंटरफेस आहे.
uint balance = token.balanceOf(address(this));
token.transfer(dest, balance);
}
हे एक स्वच्छता (cleanup) फंक्शन आहे, त्यामुळे बहुधा आपल्याला कोणतेही टोकन सोडायचे नाहीत. वापरकर्त्याकडून मॅन्युअली शिल्लक मिळवण्याऐवजी, आपण ही प्रक्रिया स्वयंचलित करू शकतो.
निष्कर्ष
हा एक परिपूर्ण उपाय नाही - "वापरकर्त्याने चूक केली" या समस्येवर कोणताही परिपूर्ण उपाय नाही. तथापि, या प्रकारच्या तपासण्या वापरल्याने किमान काही चुका टाळता येऊ शकतात. खाती गोठवण्याची क्षमता, जरी धोकादायक असली तरी, हॅकरला चोरीला गेलेला निधी नाकारून काही हॅक्सचे नुकसान मर्यादित करण्यासाठी वापरली जाऊ शकते.