कॉन्ट्रॅक्टचे रिव्हर्स इंजिनिअरिंग
परिचय
ब्लॉकचेनवर कोणतीही गुपिते नसतात, जे काही घडते ते सुसंगत, पडताळणीयोग्य आणि सार्वजनिकरित्या उपलब्ध असते. आदर्शपणे, कॉन्ट्रॅक्ट्सचा सोर्स कोड Etherscan वर प्रकाशित आणि सत्यापित केलेला असावा (opens in a new tab). तथापि, नेहमीच असे नसते (opens in a new tab). या लेखामध्ये तुम्ही सोर्स कोड नसलेले कॉन्ट्रॅक्ट पाहून कॉन्ट्रॅक्ट्सचे रिव्हर्स इंजिनिअरिंग कसे करायचे ते शिकाल, 0x2510c039cc3b061d79e564b38836da87e31b342f (opens in a new tab).
रिव्हर्स कंपायलर उपलब्ध आहेत, परंतु ते नेहमीच वापरण्यायोग्य परिणाम (opens in a new tab) देत नाहीत. या लेखामध्ये तुम्ही ऑपकोड्सवरून (opens in a new tab) मॅन्युअली रिव्हर्स इंजिनिअरिंग कसे करायचे आणि कॉन्ट्रॅक्ट कसे समजून घ्यायचे, तसेच डीकंपायलरच्या परिणामांचा अर्थ कसा लावायचा हे शिकाल.
हा लेख समजून घेण्यासाठी तुम्हाला EVM च्या मूलभूत गोष्टी आधीच माहित असाव्यात आणि EVM असेंबलरची किमान थोडीफार माहिती असावी. तुम्ही या विषयांबद्दल येथे वाचू शकता (opens in a new tab).
एक्झिक्युटेबल कोड तयार करा
तुम्ही कॉन्ट्रॅक्टसाठी Etherscan वर जाऊन, Contract टॅबवर क्लिक करून आणि नंतर Switch to Opcodes View वर क्लिक करून ऑपकोड्स मिळवू शकता. तुम्हाला असे दृश्य मिळते ज्यामध्ये प्रति ओळ एक ऑपकोड असतो.
तथापि, जंप्स समजून घेण्यासाठी, तुम्हाला प्रत्येक ऑपकोड कोडमध्ये कुठे स्थित आहे हे माहित असणे आवश्यक आहे. हे करण्यासाठी, एक मार्ग म्हणजे Google Spreadsheet उघडणे आणि स्तंभ C मध्ये ऑपकोड्स पेस्ट करणे. तुम्ही या आधीच तयार केलेल्या स्प्रेडशीटची प्रत बनवून पुढील पायऱ्या वगळू शकता (opens in a new tab).
पुढची पायरी म्हणजे योग्य कोड लोकेशन्स मिळवणे जेणेकरून आपण जंप्स समजून घेऊ शकू. आपण ऑपकोडचा आकार स्तंभ B मध्ये आणि लोकेशन (हेक्साडेसिमलमध्ये) स्तंभ A मध्ये ठेवू. सेल B1 मध्ये हे फंक्शन टाइप करा आणि नंतर कोडच्या शेवटपर्यंत, उर्वरित स्तंभ B साठी ते कॉपी आणि पेस्ट करा. तुम्ही हे केल्यानंतर स्तंभ B लपवू शकता.
=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)
प्रथम हे फंक्शन स्वतः ऑपकोडसाठी एक बाइट जोडते, आणि नंतर PUSH शोधते. Push ऑपकोड्स विशेष असतात कारण पुश केल्या जाणाऱ्या मूल्यासाठी त्यांच्याकडे अतिरिक्त बाइट्स असणे आवश्यक असते. जर ऑपकोड PUSH असेल, तर आपण बाइट्सची संख्या काढतो आणि ती जोडतो.
A1 मध्ये पहिला ऑफसेट, शून्य ठेवा. त्यानंतर, A2 मध्ये, हे फंक्शन ठेवा आणि पुन्हा उर्वरित स्तंभ A साठी ते कॉपी आणि पेस्ट करा:
=dec2hex(hex2dec(A1)+B1)
आपल्याला हेक्साडेसिमल मूल्य देण्यासाठी या फंक्शनची आवश्यकता आहे कारण जंप्सच्या आधी पुश केलेली मूल्ये (JUMP आणि JUMPI) आपल्याला हेक्साडेसिमलमध्ये दिली जातात.
एंट्री पॉईंट (0x00)
कॉन्ट्रॅक्ट्स नेहमी पहिल्या बाइटपासून कार्यान्वित केले जातात. हा कोडचा सुरुवातीचा भाग आहे:
| ऑफसेट | ऑपकोड | स्टॅक (ऑपकोडनंतर) |
|---|---|---|
| 0 | PUSH1 0x80 | 0x80 |
| 2 | PUSH1 0x40 | 0x40, 0x80 |
| 4 | MSTORE | रिक्त |
| 5 | PUSH1 0x04 | 0x04 |
| 7 | CALLDATASIZE | CALLDATASIZE 0x04 |
| 8 | LT | CALLDATASIZE<4 |
| 9 | PUSH2 0x005e | 0x5E CALLDATASIZE<4 |
| C | JUMPI | रिक्त |
हा कोड दोन गोष्टी करतो:
- मेमरी लोकेशन्स 0x40-0x5F वर 32 बाइट मूल्य म्हणून 0x80 लिहा (0x80 हे 0x5F मध्ये साठवले जाते, आणि 0x40-0x5E सर्व शून्य असतात).
- कॉल डेटाचा आकार वाचा. साधारणपणे Ethereum कॉन्ट्रॅक्टसाठी कॉल डेटा ABI (अॅप्लिकेशन बायनरी इंटरफेस) (opens in a new tab) चे अनुसरण करतो, ज्यासाठी फंक्शन सिलेक्टरसाठी किमान चार बाइट्स आवश्यक असतात. जर कॉल डेटाचा आकार चारापेक्षा कमी असेल, तर 0x5E वर जा.
0x5E वरील हँडलर (नॉन-ABI कॉल डेटासाठी)
| ऑफसेट | ऑपकोड |
|---|---|
| 5E | JUMPDEST |
| 5F | CALLDATASIZE |
| 60 | PUSH2 0x007c |
| 63 | JUMPI |
हा स्निपेट JUMPDEST ने सुरू होतो. जर तुम्ही JUMPDEST नसलेल्या ऑपकोडवर उडी मारली तर EVM (इथेरियम व्हर्च्युअल मशीन) प्रोग्राम्स अपवाद (exception) थ्रो करतात. त्यानंतर ते CALLDATASIZE पाहते, आणि जर ते "true" असेल (म्हणजेच, शून्य नसेल) तर 0x7C वर उडी मारते. आपण त्याबद्दल खाली पाहू.
| ऑफसेट | ऑपकोड | स्टॅक (ऑपकोडनंतर) |
|---|---|---|
| 64 | CALLVALUE | कॉलद्वारे प्रदान केलेले . Solidity मध्ये याला msg.value म्हटले जाते |
| 65 | PUSH1 0x06 | 6 CALLVALUE |
| 67 | PUSH1 0x00 | 0 6 CALLVALUE |
| 69 | DUP3 | CALLVALUE 0 6 CALLVALUE |
| 6A | DUP3 | 6 CALLVALUE 0 6 CALLVALUE |
| 6B | SLOAD | Storage[6] CALLVALUE 0 6 CALLVALUE |
म्हणून जेव्हा कोणताही कॉल डेटा नसतो तेव्हा आपण Storage[6] चे मूल्य वाचतो. हे मूल्य काय आहे हे आपल्याला अद्याप माहित नाही, परंतु आपण अशा व्यवहारांचा शोध घेऊ शकतो जे कॉन्ट्रॅक्टला कोणत्याही कॉल डेटाशिवाय प्राप्त झाले आहेत. जे व्यवहार कोणत्याही कॉल डेटाशिवाय (आणि त्यामुळे कोणत्याही पद्धतीशिवाय) फक्त ETH चे हस्तांतरण करतात, त्यांच्यासाठी Etherscan मध्ये Transfer ही पद्धत असते. किंबहुना, कॉन्ट्रॅक्टला प्राप्त झालेला अगदी पहिला व्यवहार (opens in a new tab) हे एक हस्तांतरण आहे.
जर आपण त्या व्यवहारामध्ये पाहिले आणि Click to see More वर क्लिक केले, तर आपल्याला दिसेल की कॉल डेटा, ज्याला इनपुट डेटा म्हटले जाते, तो खरोखरच रिक्त आहे (0x). हे देखील लक्षात घ्या की मूल्य 1.559 ETH आहे, जे नंतर संबंधित असेल.
पुढे, State टॅबवर क्लिक करा आणि आपण ज्या कॉन्ट्रॅक्टचे रिव्हर्स इंजिनिअरिंग करत आहोत (0x2510...) त्याचा विस्तार करा. तुम्ही पाहू शकता की व्यवहारादरम्यान Storage[6] बदलले आहे, आणि जर तुम्ही Hex ला Number मध्ये बदलले, तर तुम्हाला दिसेल की ते 1,559,000,000,000,000,000 झाले आहे, जे wei मध्ये हस्तांतरित केलेले मूल्य आहे (मी स्पष्टतेसाठी स्वल्पविराम जोडले आहेत), जे पुढील कॉन्ट्रॅक्ट मूल्याशी संबंधित आहे.
जर आपण त्याच कालावधीतील इतर Transfer व्यवहारांमुळे (opens in a new tab) झालेल्या स्थितीतील बदलांमध्ये पाहिले तर आपल्याला दिसेल की Storage[6] ने काही काळासाठी कॉन्ट्रॅक्टच्या मूल्याचा मागोवा घेतला. सध्या आपण याला Value* म्हणू. अस्टरिस्क (*) आपल्याला आठवण करून देतो की हे व्हेरिएबल काय करते हे आपल्याला अद्याप माहित नाही, परंतु ते केवळ कॉन्ट्रॅक्टच्या मूल्याचा मागोवा घेण्यासाठी असू शकत नाही कारण जेव्हा तुम्ही ADDRESS BALANCE वापरून तुमच्या खात्यातील शिल्लक मिळवू शकता, तेव्हा स्टोरेज वापरण्याची कोणतीही आवश्यकता नाही, जे खूप महाग आहे. पहिला ऑपकोड कॉन्ट्रॅक्टचा स्वतःचा पत्ता पुश करतो. दुसरा ऑपकोड स्टॅकच्या शीर्षस्थानी असलेला पत्ता वाचतो आणि त्या पत्त्याच्या शिल्लक रकमेने तो बदलतो.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 6C | PUSH2 0x0075 | 0x75 Value* CALLVALUE 0 6 CALLVALUE |
| 6F | SWAP2 | CALLVALUE Value* 0x75 0 6 CALLVALUE |
| 70 | SWAP1 | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 71 | PUSH2 0x01a7 | 0x01A7 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 74 | JUMP |
आपण जंप डेस्टिनेशनवर या कोडचा मागोवा घेणे सुरू ठेवू.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 1A7 | JUMPDEST | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1A8 | PUSH1 0x00 | 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AA | DUP3 | CALLVALUE 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AB | NOT | 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
NOT हे बिटवाइज आहे, त्यामुळे ते कॉल व्हॅल्यूमधील प्रत्येक बिटचे मूल्य उलट करते.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 1AC | DUP3 | Value* 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AD | GT | Value*>2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AE | ISZERO | Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AF | PUSH2 0x01df | 0x01DF Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1B2 | JUMPI |
जर Value* हे 2^256-CALLVALUE-1 पेक्षा लहान किंवा त्याच्या समान असेल तर आपण उडी मारतो. हे ओव्हरफ्लो टाळण्यासाठी लॉजिक असल्यासारखे दिसते. आणि खरोखरच, आपण पाहतो की काही निरर्थक ऑपरेशन्स नंतर (उदाहरणार्थ, मेमरीमध्ये लिहिलेले हटवले जाणार आहे) ऑफसेट 0x01DE वर जर ओव्हरफ्लो आढळला तर कॉन्ट्रॅक्ट पूर्ववत होते (reverts), जे सामान्य वर्तन आहे.
लक्षात घ्या की असा ओव्हरफ्लो होण्याची शक्यता अत्यंत कमी आहे, कारण त्यासाठी कॉल व्हॅल्यू अधिक Value* हे 2^256 wei च्या तुलनेत असणे आवश्यक आहे, जे सुमारे 10^59 ETH आहे. लेखनाच्या वेळी, एकूण ETH पुरवठा दोनशे दशलक्षांपेक्षा कमी आहे (opens in a new tab).
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 1DF | JUMPDEST | 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1E0 | POP | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1E1 | ADD | Value*+CALLVALUE 0x75 0 6 CALLVALUE |
| 1E2 | SWAP1 | 0x75 Value*+CALLVALUE 0 6 CALLVALUE |
| 1E3 | JUMP |
जर आपण येथे पोहोचलो, तर Value* + CALLVALUE मिळवा आणि ऑफसेट 0x75 वर उडी मारा.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 75 | JUMPDEST | Value*+CALLVALUE 0 6 CALLVALUE |
| 76 | SWAP1 | 0 Value*+CALLVALUE 6 CALLVALUE |
| 77 | SWAP2 | 6 Value*+CALLVALUE 0 CALLVALUE |
| 78 | SSTORE | 0 CALLVALUE |
जर आपण येथे पोहोचलो (ज्यासाठी कॉल डेटा रिक्त असणे आवश्यक आहे) तर आपण Value* मध्ये कॉल व्हॅल्यू जोडतो. हे आपण Transfer व्यवहार काय करतात याबद्दल जे म्हणतो त्याच्याशी सुसंगत आहे.
| ऑफसेट | ऑपकोड |
|---|---|
| 79 | POP |
| 7A | POP |
| 7B | STOP |
शेवटी, स्टॅक साफ करा (जे आवश्यक नाही) आणि व्यवहाराच्या यशस्वी समाप्तीचा संकेत द्या.
थोडक्यात सांगायचे तर, सुरुवातीच्या कोडसाठी येथे एक फ्लोचार्ट आहे.
0x7C वरील हँडलर
या हँडलरचे काम काय आहे हे मी मुद्दाम शीर्षकात दिलेले नाही. हा विशिष्ट कॉन्ट्रॅक्ट कसा काम करतो हे शिकवणे हा उद्देश नाही, तर कॉन्ट्रॅक्ट्स रिव्हर्स इंजिनिअर कसे करायचे हे शिकवणे हा आहे. मी जसे कोड फॉलो करून शिकलो, तसेच तुम्हीही ते काय करते हे शिकाल.
आपण येथे अनेक ठिकाणांहून पोहोचतो:
- जर 1, 2, किंवा 3 बाइट्सचा कॉल डेटा असेल (ऑफसेट 0x63 वरून)
- जर मेथडची स्वाक्षरी अज्ञात असेल (ऑफसेट 0x42 आणि 0x5D वरून)
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 7C | JUMPDEST | |
| 7D | PUSH1 0x00 | 0x00 |
| 7F | PUSH2 0x009d | 0x9D 0x00 |
| 82 | PUSH1 0x03 | 0x03 0x9D 0x00 |
| 84 | SLOAD | Storage[3] 0x9D 0x00 |
हा आणखी एक स्टोरेज सेल आहे, जो मला कोणत्याही व्यवहारांमध्ये सापडला नाही त्यामुळे त्याचा अर्थ काय आहे हे जाणून घेणे कठीण आहे. खालील कोड हे अधिक स्पष्ट करेल.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 |
| 9A | AND | Storage[3]-पत्ता-म्हणून 0x9D 0x00 |
हे ऑपकोड्स आपण Storage[3] मधून वाचलेले मूल्य 160 बिट्सपर्यंत ट्रंकेट करतात, जी एका Ethereum पत्त्याची लांबी असते.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 9B | SWAP1 | 0x9D Storage[3]-पत्ता-म्हणून 0x00 |
| 9C | JUMP | Storage[3]-पत्ता-म्हणून 0x00 |
ही जंप अनावश्यक आहे, कारण आपण पुढच्या ऑपकोडवर जात आहोत. हा कोड जितका गॅस-कार्यक्षम असू शकतो तितका नाही.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 9D | JUMPDEST | Storage[3]-पत्ता-म्हणून 0x00 |
| 9E | SWAP1 | 0x00 Storage[3]-पत्ता-म्हणून |
| 9F | POP | Storage[3]-पत्ता-म्हणून |
| A0 | PUSH1 0x40 | 0x40 Storage[3]-पत्ता-म्हणून |
| A2 | MLOAD | Mem[0x40] Storage[3]-पत्ता-म्हणून |
कोडच्या अगदी सुरुवातीला आपण Mem[0x40] ला 0x80 वर सेट करतो. जर आपण नंतर 0x40 शोधले, तर आपल्याला दिसेल की आपण ते बदलत नाही - त्यामुळे आपण असे गृहीत धरू शकतो की ते 0x80 आहे.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-पत्ता-म्हणून |
| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 Storage[3]-पत्ता-म्हणून |
| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-पत्ता-म्हणून |
| A7 | CALLDATACOPY | 0x80 Storage[3]-पत्ता-म्हणून |
सर्व कॉल डेटा मेमरीमध्ये कॉपी करा, 0x80 पासून सुरू करून.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| A8 | PUSH1 0x00 | 0x00 0x80 Storage[3]-पत्ता-म्हणून |
| AA | DUP1 | 0x00 0x00 0x80 Storage[3]-पत्ता-म्हणून |
| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-पत्ता-म्हणून |
| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-पत्ता-म्हणून |
| AD | DUP6 | Storage[3]-पत्ता-म्हणून 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-पत्ता-म्हणून |
| AE | GAS | GAS Storage[3]-पत्ता-म्हणून 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-पत्ता-म्हणून |
| AF | DELEGATE_CALL |
आता गोष्टी अधिक स्पष्ट झाल्या आहेत. हा कॉन्ट्रॅक्ट एक प्रॉक्सी (opens in a new tab) म्हणून काम करू शकतो, जो खरे काम करण्यासाठी Storage[3] मधील पत्त्याला कॉल करतो. DELEGATE_CALL एका वेगळ्या कॉन्ट्रॅक्टला कॉल करतो, परंतु त्याच स्टोरेजमध्ये राहतो. याचा अर्थ असा की डेलिगेटेड कॉन्ट्रॅक्ट, ज्यासाठी आपण प्रॉक्सी आहोत, तो त्याच स्टोरेज स्पेसमध्ये प्रवेश करतो. कॉलसाठी पॅरामीटर्स खालीलप्रमाणे आहेत:
- गॅस: सर्व उर्वरित गॅस
- कॉल केलेला पत्ता: Storage[3]-पत्ता-म्हणून
- कॉल डेटा: 0x80 पासून सुरू होणारे CALLDATASIZE बाइट्स, जिथे आपण मूळ कॉल डेटा ठेवला होता
- रिटर्न डेटा: काहीही नाही (0x00 - 0x00) आपल्याला रिटर्न डेटा इतर मार्गांनी मिळेल (खाली पहा)
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| B0 | RETURNDATASIZE | RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
येथे आपण सर्व रिटर्न डेटा 0x80 पासून सुरू होणाऱ्या मेमरी बफरमध्ये कॉपी करतो.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| B6 | DUP2 | (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| B7 | DUP1 | (((कॉल यश/अपयश))) (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| B8 | ISZERO | (((कॉल अयशस्वी झाला का))) (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| B9 | PUSH2 0x00c0 | 0xC0 (((कॉल अयशस्वी झाला का))) (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| BC | JUMPI | (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| BD | DUP2 | RETURNDATASIZE (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| BE | DUP5 | 0x80 RETURNDATASIZE (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| BF | RETURN |
त्यामुळे कॉलनंतर आपण रिटर्न डेटा 0x80 - 0x80+RETURNDATASIZE या बफरमध्ये कॉपी करतो, आणि जर कॉल यशस्वी झाला तर आपण नेमक्या त्याच बफरसह RETURN करतो.
DELEGATECALL अयशस्वी
जर आपण येथे, 0xC0 वर पोहोचलो, तर याचा अर्थ असा की आपण कॉल केलेला कॉन्ट्रॅक्ट पूर्ववत (revert) झाला. आपण त्या कॉन्ट्रॅक्टसाठी फक्त एक प्रॉक्सी असल्यामुळे, आपल्याला तोच डेटा परत करायचा आहे आणि पूर्ववत देखील करायचे आहे.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| C0 | JUMPDEST | (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| C1 | DUP2 | RETURNDATASIZE (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((कॉल यश/अपयश))) RETURNDATASIZE (((कॉल यश/अपयश))) 0x80 Storage[3]-पत्ता-म्हणून |
| C3 | REVERT |
त्यामुळे आपण आधी RETURN साठी वापरलेल्या त्याच बफरसह REVERT करतो: 0x80 - 0x80+RETURNDATASIZE
ABI कॉल्स
जर कॉल डेटाचा आकार चार बाइट्स किंवा त्याहून अधिक असेल, तर हा एक वैध ABI कॉल असू शकतो.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| D | PUSH1 0x00 | 0x00 |
| F | CALLDATALOAD | (((कॉल डेटाचा पहिला शब्द (256 बिट्स)))) |
| 10 | PUSH1 0xe0 | 0xE0 (((कॉल डेटाचा पहिला शब्द (256 बिट्स)))) |
| 12 | SHR | (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
Etherscan आपल्याला सांगते की 1C हा एक अज्ञात ऑपकोड आहे, कारण Etherscan ने हे वैशिष्ट्य लिहिल्यानंतर तो जोडला गेला होता (opens in a new tab) आणि त्यांनी तो अद्यतनित केलेला नाही. एक अद्ययावत ऑपकोड तक्ता (opens in a new tab) आपल्याला दाखवतो की हे शिफ्ट राईट (shift right) आहे
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 13 | DUP1 | (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 14 | PUSH4 0x3cd8045e | 0x3CD8045E (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 19 | GT | 0x3CD8045E>first-32-bits-of-the-call-data (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 1A | PUSH2 0x0043 | 0x43 0x3CD8045E>first-32-bits-of-the-call-data (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 1D | JUMPI | (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
पद्धत स्वाक्षरी जुळणी चाचण्यांना अशा प्रकारे दोन भागांत विभागल्याने सरासरी निम्म्या चाचण्या वाचतात. यानंतर लगेच येणारा कोड आणि 0x43 मधील कोड समान पॅटर्न फॉलो करतात: कॉल डेटाचे पहिले 32 बिट्स DUP1 करा, PUSH4 (((method signature> करा, समानतेची तपासणी करण्यासाठी EQ चालवा, आणि नंतर पद्धत स्वाक्षरी जुळत असल्यास JUMPI करा. येथे पद्धत स्वाक्षऱ्या, त्यांचे पत्ते आणि ज्ञात असल्यास संबंधित पद्धत व्याख्या (opens in a new tab) दिली आहे:
| पद्धत | पद्धत स्वाक्षरी | जंप करण्यासाठी ऑफसेट |
|---|---|---|
| splitter() (opens in a new tab) | 0x3cd8045e | 0x0103 |
| ??? | 0x81e580d3 | 0x0138 |
| currentWindow() (opens in a new tab) | 0xba0bafb4 | 0x0158 |
| ??? | 0x1f135823 | 0x00C4 |
| merkleRoot() (opens in a new tab) | 0x2eb4a7ab | 0x00ED |
जर कोणतीही जुळणी आढळली नाही, तर कोड 0x7C वरील प्रॉक्सी हँडलरकडे जंप करतो, या आशेने की आपण ज्या कॉन्ट्रॅक्टसाठी प्रॉक्सी आहोत त्यात जुळणी असेल.
splitter()
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 103 | JUMPDEST | |
| 104 | CALLVALUE | CALLVALUE |
| 105 | DUP1 | CALLVALUE CALLVALUE |
| 106 | ISZERO | CALLVALUE==0 CALLVALUE |
| 107 | PUSH2 0x010f | 0x010F CALLVALUE==0 CALLVALUE |
| 10A | JUMPI | CALLVALUE |
| 10B | PUSH1 0x00 | 0x00 CALLVALUE |
| 10D | DUP1 | 0x00 0x00 CALLVALUE |
| 10E | REVERT |
हे फंक्शन सर्वात आधी हे तपासते की कॉलने कोणतेही ETH पाठवले नाही. हे फंक्शन payable (opens in a new tab) नाही. जर कोणी आपल्याला ETH पाठवले असेल तर ती नक्कीच चूक असावी आणि ते ETH अशा ठिकाणी अडकून राहू नये जिथून ते परत मिळवता येणार नाही, हे टाळण्यासाठी आपण REVERT करू इच्छितो.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 10F | JUMPDEST | |
| 110 | POP | |
| 111 | PUSH1 0x03 | 0x03 |
| 113 | SLOAD | (((Storage[3] म्हणजेच ते कॉन्ट्रॅक्ट ज्यासाठी आपण प्रॉक्सी आहोत))) |
| 114 | PUSH1 0x40 | 0x40 (((Storage[3] म्हणजेच ते कॉन्ट्रॅक्ट ज्यासाठी आपण प्रॉक्सी आहोत))) |
| 116 | MLOAD | 0x80 (((Storage[3] म्हणजेच ते कॉन्ट्रॅक्ट ज्यासाठी आपण प्रॉक्सी आहोत))) |
| 117 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xFF...FF 0x80 (((Storage[3] म्हणजेच ते कॉन्ट्रॅक्ट ज्यासाठी आपण प्रॉक्सी आहोत))) |
| 12C | SWAP1 | 0x80 0xFF...FF (((Storage[3] म्हणजेच ते कॉन्ट्रॅक्ट ज्यासाठी आपण प्रॉक्सी आहोत))) |
| 12D | SWAP2 | (((Storage[3] म्हणजेच ते कॉन्ट्रॅक्ट ज्यासाठी आपण प्रॉक्सी आहोत))) 0xFF...FF 0x80 |
| 12E | AND | ProxyAddr 0x80 |
| 12F | DUP2 | 0x80 ProxyAddr 0x80 |
| 130 | MSTORE | 0x80 |
आणि 0x80 मध्ये आता प्रॉक्सी पत्ता आहे
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 131 | PUSH1 0x20 | 0x20 0x80 |
| 133 | ADD | 0xA0 |
| 134 | PUSH2 0x00e4 | 0xE4 0xA0 |
| 137 | JUMP | 0xA0 |
E4 कोड
आपण या ओळी पहिल्यांदाच पाहत आहोत, परंतु त्या इतर पद्धतींसोबत (methods) सामायिक केल्या आहेत (खाली पहा). म्हणून आपण स्टॅकमधील मूल्याला X म्हणूया, आणि फक्त एवढे लक्षात ठेवा की splitter() मध्ये या X चे मूल्य 0xA0 आहे.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| E4 | JUMPDEST | X |
| E5 | PUSH1 0x40 | 0x40 X |
| E7 | MLOAD | 0x80 X |
| E8 | DUP1 | 0x80 0x80 X |
| E9 | SWAP2 | X 0x80 0x80 |
| EA | SUB | X-0x80 0x80 |
| EB | SWAP1 | 0x80 X-0x80 |
| EC | RETURN |
तर हा कोड स्टॅकमध्ये (X) मेमरी पॉइंटर प्राप्त करतो, आणि कॉन्ट्रॅक्टला 0x80 - X असलेल्या बफरसह RETURN करण्यास प्रवृत्त करतो.
splitter() च्या बाबतीत, हे तो पत्ता परत करते ज्यासाठी आपण प्रॉक्सी आहोत. RETURN 0x80-0x9F मधील बफर परत करते, जिथे आपण हा डेटा लिहिला होता (वरील ऑफसेट 0x130).
currentWindow()
ऑफसेट्स 0x158-0x163 मधील कोड splitter() मधील 0x103-0x10E मध्ये आपण जे पाहिले त्याच्यासारखाच आहे (JUMPI डेस्टिनेशन व्यतिरिक्त), म्हणून आपल्याला माहित आहे की currentWindow() हे देखील payable नाही.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 164 | JUMPDEST | |
| 165 | POP | |
| 166 | PUSH2 0x00da | 0xDA |
| 169 | PUSH1 0x01 | 0x01 0xDA |
| 16B | SLOAD | Storage[1] 0xDA |
| 16C | DUP2 | 0xDA Storage[1] 0xDA |
| 16D | JUMP | Storage[1] 0xDA |
DA कोड
हा कोड इतर मेथड्ससोबत देखील सामायिक केला आहे. म्हणून आपण स्टॅकमधील मूल्याला Y म्हणू, आणि फक्त हे लक्षात ठेवा की currentWindow() मध्ये या Y चे मूल्य Storage[1] आहे.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| DA | JUMPDEST | Y 0xDA |
| DB | PUSH1 0x40 | 0x40 Y 0xDA |
| DD | MLOAD | 0x80 Y 0xDA |
| DE | SWAP1 | Y 0x80 0xDA |
| DF | DUP2 | 0x80 Y 0x80 0xDA |
| E0 | MSTORE | 0x80 0xDA |
Y ला 0x80-0x9F वर लिहा.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| E1 | PUSH1 0x20 | 0x20 0x80 0xDA |
| E3 | ADD | 0xA0 0xDA |
आणि उर्वरित भाग आधीच वर स्पष्ट केला आहे. म्हणून 0xDA वरील जंप्स स्टॅकच्या वरच्या भागाला (Y) 0x80-0x9F वर लिहितात आणि ते मूल्य परत करतात. currentWindow() च्या बाबतीत, ते Storage[1] परत करते.
merkleRoot()
ऑफसेट 0xED-0xF8 मधील कोड आपण splitter() मधील 0x103-0x10E मध्ये जे पाहिले त्याच्या अगदी समान आहे (JUMPI डेस्टिनेशन व्यतिरिक्त), त्यामुळे आपल्याला माहित आहे की merkleRoot() हे देखील payable नाही.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| F9 | JUMPDEST | |
| FA | POP | |
| FB | PUSH2 0x00da | 0xDA |
| FE | PUSH1 0x00 | 0x00 0xDA |
| 100 | SLOAD | Storage[0] 0xDA |
| 101 | DUP2 | 0xDA Storage[0] 0xDA |
| 102 | JUMP | Storage[0] 0xDA |
जंपनंतर काय होते हे आपण आधीच शोधून काढले आहे. त्यामुळे merkleRoot() Storage[0] परत करते.
0x81e580d3
ऑफसेट्स 0x138-0x143 मधील कोड splitter() मध्ये 0x103-0x10E मध्ये आपण जे पाहिले त्याच्या अगदी समान आहे (JUMPI डेस्टिनेशन व्यतिरिक्त), त्यामुळे आपल्याला माहित आहे की हे फंक्शन देखील payable नाही.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 144 | JUMPDEST | |
| 145 | POP | |
| 146 | PUSH2 0x00da | 0xDA |
| 149 | PUSH2 0x0153 | 0x0153 0xDA |
| 14C | CALLDATASIZE | CALLDATASIZE 0x0153 0xDA |
| 14D | PUSH1 0x04 | 0x04 CALLDATASIZE 0x0153 0xDA |
| 14F | PUSH2 0x018f | 0x018F 0x04 CALLDATASIZE 0x0153 0xDA |
| 152 | JUMP | 0x04 CALLDATASIZE 0x0153 0xDA |
| 18F | JUMPDEST | 0x04 CALLDATASIZE 0x0153 0xDA |
| 190 | PUSH1 0x00 | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 192 | PUSH1 0x20 | 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 194 | DUP3 | 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 195 | DUP5 | CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 196 | SUB | CALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 197 | SLT | CALLDATASIZE-4<32 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 198 | ISZERO | CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 199 | PUSH2 0x01a0 | 0x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19C | JUMPI | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
असे दिसते की हे फंक्शन कॉल डेटाचे किमान 32 बाइट्स (एक वर्ड) घेते.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 19D | DUP1 | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19E | DUP2 | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19F | REVERT |
जर त्याला कॉल डेटा मिळाला नाही तर कोणताही रिटर्न डेटा न देता व्यवहार पूर्ववत केला जातो.
फंक्शनला आवश्यक असलेला कॉल डेटा मिळाल्यास काय होते ते पाहूया.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 1A0 | JUMPDEST | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A1 | POP | 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA |
calldataload(4) हा मेथड स्वाक्षरीच्या नंतरचा कॉल डेटाचा पहिला वर्ड आहे
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 1A3 | SWAP2 | 0x0153 CALLDATASIZE calldataload(4) 0xDA |
| 1A4 | SWAP1 | CALLDATASIZE 0x0153 calldataload(4) 0xDA |
| 1A5 | POP | 0x0153 calldataload(4) 0xDA |
| 1A6 | JUMP | calldataload(4) 0xDA |
| 153 | JUMPDEST | calldataload(4) 0xDA |
| 154 | PUSH2 0x016e | 0x016E calldataload(4) 0xDA |
| 157 | JUMP | calldataload(4) 0xDA |
| 16E | JUMPDEST | calldataload(4) 0xDA |
| 16F | PUSH1 0x04 | 0x04 calldataload(4) 0xDA |
| 171 | DUP2 | calldataload(4) 0x04 calldataload(4) 0xDA |
| 172 | DUP2 | 0x04 calldataload(4) 0x04 calldataload(4) 0xDA |
| 173 | SLOAD | Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 174 | DUP2 | calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 175 | LT | calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 176 | PUSH2 0x017e | 0x017EC calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 179 | JUMPI | calldataload(4) 0x04 calldataload(4) 0xDA |
जर पहिला वर्ड Storage[4] पेक्षा कमी नसेल, तर फंक्शन अयशस्वी होते. ते कोणत्याही रिटर्न मूल्याशिवाय पूर्ववत होते:
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 17A | PUSH1 0x00 | 0x00 ... |
| 17C | DUP1 | 0x00 0x00 ... |
| 17D | REVERT |
जर calldataload(4) हे Storage[4] पेक्षा कमी असेल, तर आपल्याला हा कोड मिळतो:
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 17E | JUMPDEST | calldataload(4) 0x04 calldataload(4) 0xDA |
| 17F | PUSH1 0x00 | 0x00 calldataload(4) 0x04 calldataload(4) 0xDA |
| 181 | SWAP2 | 0x04 calldataload(4) 0x00 calldataload(4) 0xDA |
| 182 | DUP3 | 0x00 0x04 calldataload(4) 0x00 calldataload(4) 0xDA |
| 183 | MSTORE | calldataload(4) 0x00 calldataload(4) 0xDA |
आणि मेमरी लोकेशन्स 0x00-0x1F मध्ये आता 0x04 हा डेटा आहे (0x00-0x1E सर्व शून्य आहेत, 0x1F हे चार आहे)
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 184 | PUSH1 0x20 | 0x20 calldataload(4) 0x00 calldataload(4) 0xDA |
| 186 | SWAP1 | calldataload(4) 0x20 0x00 calldataload(4) 0xDA |
| 187 | SWAP2 | 0x00 0x20 calldataload(4) calldataload(4) 0xDA |
| 188 | SHA3 | (((SHA3 of 0x00-0x1F))) calldataload(4) calldataload(4) 0xDA |
| 189 | ADD | (((SHA3 of 0x00-0x1F)))+calldataload(4) calldataload(4) 0xDA |
| 18A | SLOAD | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] calldataload(4) 0xDA |
म्हणून स्टोरेजमध्ये एक लुकअप टेबल आहे, जे 0x000...0004 च्या SHA3 पासून सुरू होते आणि प्रत्येक वैध कॉल डेटा मूल्यासाठी (Storage[4] च्या खालील मूल्य) एक नोंद असते.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 18B | SWAP1 | calldataload(4) Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18C | POP | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18D | DUP2 | 0xDA Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18E | JUMP | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
आपल्याला आधीच माहित आहे की ऑफसेट 0xDA वरील कोड काय करतो, तो कॉलरला स्टॅकचे सर्वोच्च मूल्य परत करतो. त्यामुळे हे फंक्शन लुकअप टेबलमधील मूल्य कॉलरला परत करते.
0x1f135823
0xC4-0xCF ऑफसेट्समधील कोड आपण splitter() मध्ये 0x103-0x10E मध्ये जे पाहिले त्याच्यासारखाच आहे (JUMPI डेस्टिनेशन व्यतिरिक्त), त्यामुळे आपल्याला माहित आहे की हे फंक्शन देखील payable नाही.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| D0 | JUMPDEST | |
| D1 | POP | |
| D2 | PUSH2 0x00da | 0xDA |
| D5 | PUSH1 0x06 | 0x06 0xDA |
| D7 | SLOAD | Value* 0xDA |
| D8 | DUP2 | 0xDA Value* 0xDA |
| D9 | JUMP | Value* 0xDA |
0xDA ऑफसेटवरील कोड काय करतो हे आपल्याला आधीच माहित आहे, तो कॉलरला स्टॅकचे सर्वोच्च मूल्य परत करतो. त्यामुळे हे फंक्शन Value* परत करते.
मेथड सारांश
या टप्प्यावर तुम्हाला कॉन्ट्रॅक्ट समजले आहे असे वाटते का? मला वाटत नाही. आतापर्यंत आपल्याकडे या मेथड्स आहेत:
| मेथड | अर्थ |
|---|---|
| हस्तांतरण | कॉलद्वारे प्रदान केलेले मूल्य स्वीकारा आणि Value* त्या रकमेने वाढवा |
| splitter() | Storage[3], प्रॉक्सी पत्ता परत करा |
| currentWindow() | Storage[1] परत करा |
| merkleRoot() | Storage[0] परत करा |
| 0x81e580d3 | पॅरामीटर Storage[4] पेक्षा कमी असल्यास, लुकअप टेबलमधून मूल्य परत करा |
| 0x1f135823 | Storage[6], म्हणजेच Value* परत करा |
परंतु आपल्याला माहित आहे की इतर कोणतीही कार्यक्षमता Storage[3] मधील कॉन्ट्रॅक्टद्वारे प्रदान केली जाते. कदाचित ते कॉन्ट्रॅक्ट काय आहे हे आपल्याला माहित असल्यास आपल्याला एक संकेत मिळेल. सुदैवाने, ही ब्लॉकचेन आहे आणि किमान सैद्धांतिकदृष्ट्या सर्व काही ज्ञात आहे. आपल्याला Storage[3] सेट करणारी कोणतीही मेथड दिसली नाही, त्यामुळे ती कन्स्ट्रक्टरद्वारे सेट केली गेली असावी.
कन्स्ट्रक्टर
जेव्हा आपण एखाद्या कॉन्ट्रॅक्टकडे पाहतो (opens in a new tab) तेव्हा आपण तो व्यवहार देखील पाहू शकतो ज्याने ते तयार केले आहे.
जर आपण त्या व्यवहारावर आणि त्यानंतर स्थिती टॅबवर क्लिक केले, तर आपण पॅरामीटर्सची प्रारंभिक मूल्ये पाहू शकतो. विशेषतः, आपण पाहू शकतो की Storage[3] मध्ये 0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761 (opens in a new tab) आहे. त्या कॉन्ट्रॅक्टमध्ये गहाळ असलेली कार्यक्षमता असली पाहिजे. आपण ज्या कॉन्ट्रॅक्टची तपासणी करत आहोत त्यासाठी वापरलेल्या त्याच साधनांचा वापर करून आपण ते समजून घेऊ शकतो.
प्रॉक्सी कॉन्ट्रॅक्ट
वर मूळ कॉन्ट्रॅक्टसाठी आपण वापरलेल्या त्याच तंत्रांचा वापर करून आपण पाहू शकतो की जर खालील गोष्टी घडल्या तर कॉन्ट्रॅक्ट पूर्ववत होते (reverts):
- कॉलला कोणताही ETH जोडलेला असेल (0x05-0x0F)
- कॉल डेटाचा आकार 4 पेक्षा कमी असेल (0x10-0x19 आणि 0xBE-0xC2)
आणि ते समर्थन करत असलेल्या पद्धती (methods) खालीलप्रमाणे आहेत:
| पद्धत | पद्धतीची स्वाक्षरी | उडी मारण्यासाठी ऑफसेट |
|---|---|---|
| scaleAmountByPercentage(uint256,uint256) (opens in a new tab) | 0x8ffb5c97 | 0x0135 |
| isClaimed(uint256,address) (opens in a new tab) | 0xd2ef0795 | 0x0151 |
| claim(uint256,address,uint256,bytes32[]) (opens in a new tab) | 0x2e7ba6ef | 0x00F4 |
| incrementWindow() (opens in a new tab) | 0x338b1d31 | 0x0110 |
| ??? | 0x3f26479e | 0x0118 |
| ??? | 0x1e7df9d3 | 0x00C3 |
| currentWindow() (opens in a new tab) | 0xba0bafb4 | 0x0148 |
| merkleRoot() (opens in a new tab) | 0x2eb4a7ab | 0x0107 |
| ??? | 0x81e580d3 | 0x0122 |
| ??? | 0x1f135823 | 0x00D8 |
आपण तळाच्या 4 पद्धतींकडे दुर्लक्ष करू शकतो कारण आपण त्यांच्यापर्यंत कधीही पोहोचणार नाही. त्यांच्या स्वाक्षऱ्या अशा आहेत की आपले मूळ कॉन्ट्रॅक्ट स्वतःच त्यांची काळजी घेते (तुम्ही वरील तपशील पाहण्यासाठी स्वाक्षऱ्यांवर क्लिक करू शकता), त्यामुळे त्या ओव्हरराइड केलेल्या पद्धती (opens in a new tab) असल्या पाहिजेत.
उर्वरित पद्धतींपैकी एक claim(<params>) आहे, आणि दुसरी isClaimed(<params>) आहे, त्यामुळे हे एक एअरड्रॉप कॉन्ट्रॅक्ट असल्याचे दिसते. उर्वरित ऑपकोड एक-एक करून तपासण्याऐवजी, आपण डीकंपायलर वापरून पाहू शकतो (opens in a new tab), जे या कॉन्ट्रॅक्टमधील 3 फंक्शन्ससाठी उपयुक्त परिणाम देते. इतरांचे रिव्हर्स इंजिनिअरिंग करणे वाचकांसाठी एक सराव म्हणून सोडले आहे.
scaleAmountByPercentage
या फंक्शनसाठी डीकंपायलर आपल्याला हे देते:
def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable:
require calldata.size - 4 >=′ 64
if _param1 and _param2 > -1 / _param1:
revert with 0, 17
return (_param1 * _param2 / 100 * 10^6)
पहिली require हे तपासते की कॉल डेटामध्ये, फंक्शन स्वाक्षरीच्या 4 बाइट्स व्यतिरिक्त, किमान 64 बाइट्स आहेत, जे दोन पॅरामीटर्ससाठी पुरेसे आहेत. तसे नसल्यास नक्कीच काहीतरी चुकीचे आहे.
if विधान हे तपासत असल्याचे दिसते की _param1 शून्य नाही, आणि _param1 * _param2 नकारात्मक नाही. हे बहुधा रॅप अराउंडची (wrap around) प्रकरणे टाळण्यासाठी आहे.
शेवटी, फंक्शन एक स्केल केलेले मूल्य (scaled value) परत करते.
claim
डीकंपायलरने तयार केलेला कोड गुंतागुंतीचा आहे, आणि त्यातील सर्वच आपल्यासाठी संबंधित नाही. मला उपयुक्त माहिती देणाऱ्या ओळींवर लक्ष केंद्रित करण्यासाठी मी त्यातील काही भाग वगळणार आहे
def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable:
...
require _param2 == addr(_param2)
...
if currentWindow <= _param1:
revert with 0, 'cannot claim for a future window'
आपण येथे दोन महत्त्वाच्या गोष्टी पाहतो:
_param2, जरी तेuint256म्हणून घोषित केले असले तरी, प्रत्यक्षात तो एक पत्ता आहे_param1ही दावा केली जाणारी विंडो आहे, जीcurrentWindowकिंवा त्यापूर्वीची असली पाहिजे.
...
if stor5[_claimWindow][addr(_claimFor)]:
revert with 0, 'Account already claimed the given window'
तर आता आपल्याला माहित आहे की Storage[5] हा विंडोज आणि पत्त्यांचा एक अॅरे (array) आहे, आणि त्या पत्त्याने त्या विंडोसाठी बक्षीसाचा दावा केला आहे की नाही हे दर्शवतो.
...
idx = 0
s = 0
while idx < _param4.length:
...
if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]:
mem[mem[64] + 32] = mem[(32 * idx) + 296]
...
s = sha3(mem[_62 + 32 len mem[_62]])
continue
...
s = sha3(mem[_66 + 32 len mem[_66]])
continue
if unknown2eb4a7ab != s:
revert with 0, 'Invalid proof'
आपल्याला माहित आहे की unknown2eb4a7ab हे प्रत्यक्षात merkleRoot() फंक्शन आहे, त्यामुळे हा कोड मर्केल पुरावा (opens in a new tab) पडताळत असल्याचे दिसते. याचा अर्थ असा की _param4 हा एक मर्केल पुरावा आहे.
call addr(_param2) with:
value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei
gas 30000 wei
अशा प्रकारे एखादे कॉन्ट्रॅक्ट स्वतःचा ETH दुसऱ्या पत्त्यावर (कॉन्ट्रॅक्ट किंवा बाह्य मालकीचे) हस्तांतरित करते. ते हस्तांतरित करावयाच्या रकमेच्या मूल्यासह त्याला कॉल करते. त्यामुळे हे ETH चे एअरड्रॉप असल्याचे दिसते.
if not return_data.size:
if not ext_call.success:
require ext_code.size(stor2)
call stor2.deposit() with:
value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei
तळाच्या दोन ओळी आपल्याला सांगतात की Storage[2] हे देखील एक कॉन्ट्रॅक्ट आहे ज्याला आपण कॉल करतो. जर आपण कन्स्ट्रक्टर व्यवहाराकडे पाहिले (opens in a new tab) तर आपल्याला दिसेल की हे कॉन्ट्रॅक्ट 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (opens in a new tab) आहे, जे एक रॅप्ड इथर (weth) कॉन्ट्रॅक्ट आहे ज्याचा सोर्स कोड Etherscan वर अपलोड केला गेला आहे (opens in a new tab).
त्यामुळे असे दिसते की कॉन्ट्रॅक्ट्स _param2 ला ETH पाठवण्याचा प्रयत्न करतात. जर ते तसे करू शकले, तर उत्तम. तसे नसल्यास, ते WETH (opens in a new tab) पाठवण्याचा प्रयत्न करतात. जर _param2 हे बाह्य मालकीचे खाते (EOA) असेल तर ते नेहमी ETH प्राप्त करू शकते, परंतु कॉन्ट्रॅक्ट्स ETH प्राप्त करण्यास नकार देऊ शकतात. तथापि, WETH हे ERC-20 आहे आणि कॉन्ट्रॅक्ट्स ते स्वीकारण्यास नकार देऊ शकत नाहीत.
...
log 0xdbd5389f: addr(_param2), unknown81e580d3[_param1] * _param3 / 100 * 10^6, bool(ext_call.success)
फंक्शनच्या शेवटी आपण एक नोंद (log) तयार होताना पाहतो. तयार केलेल्या नोंदी पहा (opens in a new tab) आणि 0xdbd5... ने सुरू होणाऱ्या विषयावर फिल्टर करा. जर आपण अशी नोंद तयार करणाऱ्या व्यवहारांपैकी एकावर क्लिक केले (opens in a new tab) तर आपल्याला दिसेल की खरोखरच तो एक दावा (claim) असल्यासारखे दिसते - खात्याने आपण रिव्हर्स इंजिनिअरिंग करत असलेल्या कॉन्ट्रॅक्टला एक संदेश पाठवला, आणि बदल्यात ETH मिळवला.
1e7df9d3
हे फंक्शन वरील claim च्या अगदी समान आहे. हे देखील मर्केल पुरावा तपासते, पहिल्याला ETH हस्तांतरित करण्याचा प्रयत्न करते, आणि त्याच प्रकारची नोंद तयार करते.
def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable:
...
idx = 0
s = 0
while idx < _param3.length:
if idx >= mem[96]:
revert with 0, 50
_55 = mem[(32 * idx) + 128]
if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]:
...
s = sha3(mem[_58 + 32 len mem[_58]])
continue
mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]])
...
if unknown2eb4a7ab != s:
revert with 0, 'Invalid proof'
...
call addr(_param1) with:
value s wei
gas 30000 wei
if not return_data.size:
if not ext_call.success:
require ext_code.size(stor2)
call stor2.deposit() with:
value s wei
gas gas_remaining wei
...
log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)
मुख्य फरक हा आहे की पहिला पॅरामीटर, काढण्यासाठीची (withdraw) विंडो, तिथे नाही. त्याऐवजी, दावा केल्या जाऊ शकणाऱ्या सर्व विंडोजवर एक लूप आहे.
idx = 0
s = 0
while idx < currentWindow:
...
if stor5[mem[0]]:
if idx == -1:
revert with 0, 17
idx = idx + 1
s = s
continue
...
stor5[idx][addr(_param1)] = 1
if idx >= unknown81e580d3.length:
revert with 0, 50
mem[0] = 4
if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:
revert with 0, 17
if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):
revert with 0, 17
if idx == -1:
revert with 0, 17
idx = idx + 1
s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)
continue
त्यामुळे हे claim चे एक प्रकार (variant) असल्यासारखे दिसते जे सर्व विंडोजवर दावा करते.
निष्कर्ष
आतापर्यंत तुम्हाला समजले असेल की ज्या कॉन्ट्रॅक्ट्सचा सोर्स कोड उपलब्ध नाही ते ऑपकोड्स किंवा (जेव्हा ते काम करते तेव्हा) डीकंपायलर वापरून कसे समजून घ्यायचे. या लेखाच्या लांबीवरून हे स्पष्ट होते की, एखाद्या कॉन्ट्रॅक्टचे रिव्हर्स इंजिनिअरिंग करणे सोपे नाही, परंतु ज्या प्रणालीमध्ये सुरक्षा आवश्यक आहे तिथे कॉन्ट्रॅक्ट्स वचन दिल्याप्रमाणे काम करतात की नाही हे पडताळून पाहता येणे हे एक महत्त्वाचे कौशल्य आहे.



![Storage[6] मधील बदल](/_next/image/?url=%2Fcontent%2Fdevelopers%2Ftutorials%2Freverse-engineering-a-contract%2Fstorage6.png&w=1920&q=75)



