कॉन्ट्रॅक्टचे रिव्हर्स इंजिनिअरिंग
प्रस्तावना
ब्लॉकचेनवर कोणतीही गुपिते नाहीत, जे काही घडते ते सुसंगत, पडताळणीयोग्य आणि सार्वजनिकरित्या उपलब्ध असते. आदर्शपणे, कॉन्ट्रॅक्ट्सचा सोर्स कोड इथरस्कॅनवर प्रकाशित आणि सत्यापित केला गेला पाहिजेopens in a new tab. तथापि, ते नेहमीच असे नसतेopens in a new tab. या लेखात आपण सोर्स कोडशिवाय कॉन्ट्रॅक्टचे रिव्हर्स इंजिनिअर कसे करायचे हे शिकाल, 0x2510c039cc3b061d79e564b38836da87e31b342fopens 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 स्प्रेडशीट उघडून स्तंभ C मध्ये ऑपकोड पेस्ट करणे. तुम्ही या आधीच तयार केलेल्या स्प्रेडशीटची प्रत बनवून पुढील पायऱ्या वगळू शकताopens in a new tab.
पुढील पायरी म्हणजे योग्य कोड लोकेशन्स मिळवणे जेणेकरून आपण जंप्स समजून घेऊ शकू. आपण स्तंभ B मध्ये ऑपकोडचा आकार आणि स्तंभ A मध्ये स्थान (हेक्साडेसिमलमध्ये) ठेवू. B1 सेलमध्ये हे फंक्शन टाइप करा आणि नंतर ते कोडच्या शेवटपर्यंत स्तंभ B च्या उर्वरित भागासाठी कॉपी आणि पेस्ट करा. हे केल्यानंतर तुम्ही स्तंभ B लपवू शकता.
1=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)प्रथम हे फंक्शन स्वतः ऑपकोडसाठी एक बाइट जोडते, आणि नंतर PUSH शोधते. पुश ऑपकोड्स विशेष आहेत कारण त्यांना पुश केल्या जाणाऱ्या व्हॅल्यूसाठी अतिरिक्त बाइट्सची आवश्यकता असते. जर ऑपकोड PUSH असेल, तर आपण बाइट्सची संख्या काढतो आणि ती जोडतो.
A1 मध्ये पहिला ऑफसेट, शून्य टाका. नंतर, A2 मध्ये, हे फंक्शन टाका आणि पुन्हा स्तंभ A च्या उर्वरित भागासाठी कॉपी आणि पेस्ट करा:
1=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 सर्व शून्य आहेत).
- कॉलडेटाचा आकार वाचा. सामान्यतः इथेरियम कॉन्ट्रॅक्टसाठी कॉल डेटा ABI (ॲप्लिकेशन बायनरी इंटरफेस)opens in a new tab चे पालन करतो, ज्यासाठी फंक्शन सिलेक्टरसाठी किमान चार बाइट्सची आवश्यकता असते. जर कॉल डेटाचा आकार चार पेक्षा कमी असेल, तर 0x5E वर जंप करा.
0x5E वरील हँडलर (नॉन-ABI कॉल डेटासाठी)
| ऑफसेट | ऑपकोड |
|---|---|
| 5E | JUMPDEST |
| 5F | CALLDATASIZE |
| 60 | PUSH2 0x007c |
| 63 | JUMPI |
हा स्निपेट JUMPDEST ने सुरू होतो. EVM (इथेरियम व्हर्च्युअल मशीन) प्रोग्राम्स JUMPDEST नसलेल्या ऑपकोडवर जंप केल्यास अपवाद टाकतात. मग ते 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 | स्टोरेज[6] CALLVALUE 0 6 CALLVALUE |
म्हणून जेव्हा कोणताही कॉल डेटा नसतो तेव्हा आपण स्टोरेज[6] चे व्हॅल्यू वाचतो. हे व्हॅल्यू काय आहे हे आम्हाला अद्याप माहित नाही, परंतु आम्ही कॉन्ट्रॅक्टला कोणताही कॉल डेटा नसताना मिळालेल्या व्यवहारांसाठी पाहू शकतो. ज्या व्यवहारांमध्ये फक्त ETH हस्तांतरित केले जाते आणि कोणताही कॉल डेटा नसतो (आणि म्हणून कोणतीही पद्धत नाही) त्यामध्ये Etherscan मध्ये Transfer ही पद्धत असते. खरं तर, कॉन्ट्रॅक्टला मिळालेला पहिला व्यवहारopens in a new tab एक हस्तांतरण आहे.
जर आपण त्या व्यवहारात पाहिले आणि अधिक पाहण्यासाठी क्लिक करा वर क्लिक केले, तर आपल्याला दिसेल की कॉल डेटा, ज्याला इनपुट डेटा म्हणतात, खरोखरच रिकामा आहे (0x). हे देखील लक्षात घ्या की व्हॅल्यू 1.559 ETH आहे, जे नंतर संबंधित असेल.
पुढे, स्टेट टॅबवर क्लिक करा आणि आपण रिव्हर्स इंजिनिअरिंग करत असलेल्या कॉन्ट्रॅक्टचा विस्तार करा (0x2510...). तुम्ही पाहू शकता की व्यवहारादरम्यान स्टोरेज[6] बदलले आहे, आणि जर तुम्ही हेक्स (Hex) बदलून नंबर (Number) केले, तर तुम्हाला दिसेल की ते 1,559,000,000,000,000,000 झाले आहे, जे wei मध्ये हस्तांतरित केलेले मूल्य आहे (मी स्पष्टतेसाठी स्वल्पविराम जोडले आहेत), जे पुढील कॉन्ट्रॅक्ट मूल्याशी संबंधित आहे.
जर आपण त्याच कालावधीतील इतर Transfer व्यवहारांमुळेopens in a new tab झालेल्या स्टेट बदलांकडे पाहिले तर आपल्याला दिसेल की स्टोरेज[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 वर ओव्हरफ्लो आढळल्यास कॉन्ट्रॅक्ट परत येतो, जे सामान्य वर्तन आहे.
लक्षात घ्या की असा ओव्हरफ्लो अत्यंत अशक्य आहे, कारण त्यासाठी कॉल व्हॅल्यू आणि 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 | स्टोरेज[3] 0x9D 0x00 |
हा आणखी एक स्टोरेज सेल आहे, जो मला कोणत्याही व्यवहारात सापडला नाही, त्यामुळे त्याचा अर्थ काय आहे हे जाणून घेणे अधिक कठीण आहे. खालील कोड ते अधिक स्पष्ट करेल.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff स्टोरेज[3] 0x9D 0x00 |
| 9A | AND | स्टोरेज[3]-as-address 0x9D 0x00 |
हे ऑपकोड्स आम्ही स्टोरेज[3] मधून वाचलेले व्हॅल्यू 160 बिट्सपर्यंत कमी करतात, जे इथेरियम पत्त्याची लांबी आहे.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 9B | SWAP1 | 0x9D स्टोरेज[3]-as-address 0x00 |
| 9C | JUMP | स्टोरेज[3]-as-address 0x00 |
ही जंप अनावश्यक आहे, कारण आपण पुढील ऑपकोडवर जात आहोत. हा कोड गॅस-कार्यक्षमतेच्या बाबतीत जितका असू शकला असता तितका नाही.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 9D | JUMPDEST | स्टोरेज[3]-as-address 0x00 |
| 9E | SWAP1 | 0x00 स्टोरेज[3]-as-address |
| 9F | POP | स्टोरेज[3]-as-address |
| A0 | PUSH1 0x40 | 0x40 स्टोरेज[3]-as-address |
| A2 | MLOAD | Mem[0x40] स्टोरेज[3]-as-address |
कोडच्या अगदी सुरुवातीला आम्ही Mem[0x40] हे 0x80 वर सेट केले. जर आपण नंतर 0x40 शोधले, तर आपल्याला दिसेल की आपण ते बदलत नाही - म्हणून आपण ते 0x80 आहे असे गृहीत धरू शकतो.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| A3 | CALLDATASIZE | CALLDATASIZE 0x80 स्टोरेज[3]-as-address |
| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 स्टोरेज[3]-as-address |
| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 स्टोरेज[3]-as-address |
| A7 | CALLDATACOPY | 0x80 स्टोरेज[3]-as-address |
सर्व कॉल डेटा मेमरीमध्ये कॉपी करा, 0x80 पासून सुरू करून.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| A8 | PUSH1 0x00 | 0x00 0x80 स्टोरेज[3]-as-address |
| AA | DUP1 | 0x00 0x00 0x80 स्टोरेज[3]-as-address |
| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 स्टोरेज[3]-as-address |
| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 स्टोरेज[3]-as-address |
| AD | DUP6 | स्टोरेज[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 स्टोरेज[3]-as-address |
| AE | GAS | GAS स्टोरेज[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 स्टोरेज[3]-as-address |
| AF | DELEGATE_CALL |
आता गोष्टी अधिक स्पष्ट झाल्या आहेत. हा कॉन्ट्रॅक्ट प्रॉक्सीopens in a new tab म्हणून काम करू शकतो, खरे काम करण्यासाठी स्टोरेज[3] मधील पत्त्यावर कॉल करून. DELEGATE_CALL एका वेगळ्या कॉन्ट्रॅक्टला कॉल करतो, परंतु त्याच स्टोरेजमध्ये राहतो. याचा अर्थ असा की डेलीगेटेड कॉन्ट्रॅक्ट, ज्यासाठी आम्ही प्रॉक्सी आहोत, त्याच स्टोरेज स्पेसमध्ये प्रवेश करतो. कॉलसाठी पॅरामीटर्स आहेत:
- Gas: उर्वरित सर्व गॅस
- कॉल केलेला पत्ता: स्टोरेज[3]-as-address
- कॉल डेटा: 0x80 पासून सुरू होणारे CALLDATASIZE बाइट्स, जिथे आम्ही मूळ कॉल डेटा ठेवला
- रिटर्न डेटा: काहीही नाही (0x00 - 0x00) आम्ही इतर मार्गांनी रिटर्न डेटा मिळवू (खाली पहा)
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| B0 | RETURNDATASIZE | RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
येथे आम्ही सर्व रिटर्न डेटा 0x80 पासून सुरू होणाऱ्या मेमरी बफरमध्ये कॉपी करतो.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| B6 | DUP2 | (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| B7 | DUP1 | (((कॉल यशस्वी/अयशस्वी))) (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| B8 | ISZERO | (((कॉल अयशस्वी झाला का))) (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| B9 | PUSH2 0x00c0 | 0xC0 (((कॉल अयशस्वी झाला का))) (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| BC | JUMPI | (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| BD | DUP2 | RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| BE | DUP5 | 0x80 RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| BF | RETURN |
म्हणून कॉल नंतर आम्ही रिटर्न डेटा 0x80 - 0x80+RETURNDATASIZE बफरमध्ये कॉपी करतो, आणि जर कॉल यशस्वी झाला तर आम्ही त्याच बफरसह RETURN करतो.
DELEGATECALL अयशस्वी
जर आपण येथे, 0xC0 वर आलो, तर याचा अर्थ असा की आम्ही कॉल केलेला कॉन्ट्रॅक्ट रिव्हर्ट झाला. आम्ही त्या कॉन्ट्रॅक्टसाठी फक्त एक प्रॉक्सी असल्याने, आम्हाला समान डेटा परत करायचा आहे आणि रिव्हर्ट देखील करायचा आहे.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| C0 | JUMPDEST | (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| C1 | DUP2 | RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) RETURNDATASIZE (((कॉल यशस्वी/अयशस्वी))) 0x80 स्टोरेज[3]-as-address |
| 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 आम्हाला दर्शविते की हे उजवीकडे शिफ्ट आहे
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 13 | DUP1 | (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 14 | PUSH4 0x3cd8045e | 0x3CD8045E (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 19 | GT | 0x3CD8045E>कॉल-डेटा-चे-पहिले-32-बिट्स (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 1A | PUSH2 0x0043 | 0x43 0x3CD8045E>कॉल-डेटा-चे-पहिले-32-बिट्स (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
| 1D | JUMPI | (((कॉल डेटाचे पहिले 32 बिट्स (4 बाइट्स)))) |
मेथड सिग्नेचर मॅचिंग टेस्ट्स अशा प्रकारे दोन भागात विभागल्याने सरासरी अर्ध्या टेस्ट्स वाचतात. या नंतरचा कोड आणि 0x43 मधील कोड समान पॅटर्नचे अनुसरण करतो: कॉल डेटाचे पहिले 32 बिट्स DUP1 करा, PUSH4 (((मेथड सिग्नेचर>, समानतेची तपासणी करण्यासाठी 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 पाठवला नाही. हे फंक्शन payableopens in a new tab नाही. जर कोणी आम्हाला ETH पाठवले असेल तर ती चूक असावी आणि आम्ही REVERT करू इच्छितो जेणेकरून ते ETH अशा ठिकाणी राहणार नाही जिथे ते परत मिळवू शकत नाहीत.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 10F | JUMPDEST | |
| 110 | POP | |
| 111 | PUSH1 0x03 | 0x03 |
| 113 | SLOAD | (((स्टोरेज[3] म्हणजेच ज्या कॉन्ट्रॅक्टसाठी आम्ही प्रॉक्सी आहोत))) |
| 114 | PUSH1 0x40 | 0x40 (((स्टोरेज[3] म्हणजेच ज्या कॉन्ट्रॅक्टसाठी आम्ही प्रॉक्सी आहोत))) |
| 116 | MLOAD | 0x80 (((स्टोरेज[3] म्हणजेच ज्या कॉन्ट्रॅक्टसाठी आम्ही प्रॉक्सी आहोत))) |
| 117 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xFF...FF 0x80 (((स्टोरेज[3] म्हणजेच ज्या कॉन्ट्रॅक्टसाठी आम्ही प्रॉक्सी आहोत))) |
| 12C | SWAP1 | 0x80 0xFF...FF (((स्टोरेज[3] म्हणजेच ज्या कॉन्ट्रॅक्टसाठी आम्ही प्रॉक्सी आहोत))) |
| 12D | SWAP2 | (((स्टोरेज[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 कोड
ही ओळी आपण पहिल्यांदाच पाहत आहोत, परंतु त्या इतर मेथड्ससह शेअर केल्या आहेत (खाली पहा). म्हणून आम्ही स्टॅकमधील व्हॅल्यूला 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 | स्टोरेज[1] 0xDA |
| 16C | DUP2 | 0xDA स्टोरेज[1] 0xDA |
| 16D | JUMP | स्टोरेज[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() च्या बाबतीत, ते स्टोरेज[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 | स्टोरेज[0] 0xDA |
| 101 | DUP2 | 0xDA स्टोरेज[0] 0xDA |
| 102 | JUMP | स्टोरेज[0] 0xDA |
जंप नंतर काय होते ते आपण आधीच शोधले आहे. म्हणून merkleRoot() स्टोरेज[0] परत करते.
0x81e580d3
0x138-0x143 ऑफसेटमधील कोड splitter() मध्ये 0x103-0x10E मध्ये पाहिलेल्या कोडसारखाच आहे (JUMPI डेस्टिनेशन वगळता), म्हणून आम्हाला माहित आहे की हे फंक्शन देखील payable नाही.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 99.63+40*1.1018 = 143.702 | 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 |
| १९० | 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 |
| _mintFee | 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 | स्टोरेज[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 174 | DUP2 | calldataload(4) स्टोरेज[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 175 | LT | calldataload(4)<स्टोरेज[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 176 | PUSH2 0x017e | 0x017EC calldataload(4)<स्टोरेज[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 179 | JUMPI | calldataload(4) 0x04 calldataload(4) 0xDA |
जर पहिला शब्द स्टोरेज[4] पेक्षा कमी नसेल, तर फंक्शन अयशस्वी होते. ते कोणत्याही परत केलेल्या व्हॅल्यूशिवाय रिव्हर्ट होते:
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 17A | PUSH1 0x00 | 0x00 ... |
| 17C | DUP1 | 0x00 0x00 ... |
| 17D | REVERT |
जर calldataload(4) स्टोरेज[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 | (((0x00-0x1F चे SHA3))) calldataload(4) calldataload(4) 0xDA |
| 189 | ADD | (((0x00-0x1F चे SHA3)))+calldataload(4) calldataload(4) 0xDA |
| 18A | SLOAD | स्टोरेज[(((0x00-0x1F चे SHA3))) + calldataload(4)] calldataload(4) 0xDA |
म्हणून स्टोरेजमध्ये एक लुकअप टेबल आहे, जे 0x000...0004 च्या SHA3 पासून सुरू होते आणि प्रत्येक वैध कॉल डेटा व्हॅल्यूसाठी (स्टोरेज[4] पेक्षा कमी व्हॅल्यू) एक एंट्री आहे.
| ऑफसेट | ऑपकोड | स्टॅक |
|---|---|---|
| 18B | SWAP1 | calldataload(4) स्टोरेज[(((0x00-0x1F चे SHA3))) + calldataload(4)] 0xDA |
| 18C | POP | स्टोरेज[(((0x00-0x1F चे SHA3))) + calldataload(4)] 0xDA |
| 18D | DUP2 | 0xDA स्टोरेज[(((0x00-0x1F चे SHA3))) + calldataload(4)] 0xDA |
| 18E | JUMP | स्टोरेज[(((0x00-0x1F चे SHA3))) + 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() | स्टोरेज[3] परत करा, प्रॉक्सी पत्ता |
| currentWindow() | स्टोरेज[1] परत करा |
| merkleRoot() | स्टोरेज[0] परत करा |
| 0x81e580d3 | पॅरामीटर स्टोरेज[4] पेक्षा कमी असल्यास, लुकअप टेबलमधून व्हॅल्यू परत करा |
| 0x1f135823 | स्टोरेज[6] परत करा, म्हणजेच Value* |
परंतु आम्हाला माहित आहे की इतर कोणतीही कार्यक्षमता स्टोरेज[3] मधील कॉन्ट्रॅक्टद्वारे प्रदान केली जाते. कदाचित जर आम्हाला माहित असेल की तो कॉन्ट्रॅक्ट काय आहे तर आम्हाला काहीतरी सुगावा लागेल. सुदैवाने, हे ब्लॉकचेन आहे आणि सर्व काही ज्ञात आहे, किमान सिद्धांतानुसार. आम्ही स्टोरेज[3] सेट करणाऱ्या कोणत्याही मेथड्स पाहिल्या नाहीत, म्हणून ते कन्स्ट्रक्टरद्वारे सेट केले गेले असावे.
कन्स्ट्रक्टर
जेव्हा आपण एका कॉन्ट्रॅक्टकडे पाहतोopens in a new tab तेव्हा आपण तो तयार करणारा व्यवहार देखील पाहू शकतो.
जर आपण त्या व्यवहारावर क्लिक केले, आणि नंतर स्टेट टॅबवर, तर आपण पॅरामीटर्सची प्रारंभिक व्हॅल्यू पाहू शकतो. विशेषतः, आपण पाहू शकतो की स्टोरेज[3] मध्ये 0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761opens in a new tab आहे. त्या कॉन्ट्रॅक्टमध्ये गहाळ कार्यक्षमता असली पाहिजे. आपण ज्या कॉन्ट्रॅक्टची तपासणी करत आहोत त्यासाठी वापरलेल्या त्याच साधनांचा वापर करून आपण ते समजू शकतो.
प्रॉक्सी कॉन्ट्रॅक्ट
वर दिलेल्या मूळ कॉन्ट्रॅक्टसाठी वापरलेल्या त्याच तंत्रांचा वापर करून आपण पाहू शकतो की कॉन्ट्रॅक्ट परत येतो जर:
- कॉलसोबत कोणताही ETH जोडलेला असेल (0x05-0x0F)
- कॉल डेटाचा आकार चार पेक्षा कमी असेल (0x10-0x19 आणि 0xBE-0xC2)
आणि ते समर्थन देत असलेल्या मेथड्स आहेत:
| मेथड | मेथड सिग्नेचर | जंप करण्यासाठी ऑफसेट |
|---|---|---|
| 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 |
आपण खालील चार मेथड्स दुर्लक्षित करू शकतो कारण आपण त्यांच्यापर्यंत कधीच पोहोचणार नाही. त्यांचे सिग्नेचर्स असे आहेत की आपला मूळ कॉन्ट्रॅक्ट स्वतः त्यांची काळजी घेतो (तुम्ही वरील तपशील पाहण्यासाठी सिग्नेचर्सवर क्लिक करू शकता), म्हणून त्या ओव्हरराइड केलेल्या मेथड्सopens in a new tab असाव्यात.
उर्वरित मेथड्सपैकी एक claim(<params>) आहे आणि दुसरी isClaimed(<params>) आहे, म्हणून तो एअरड्रॉप कॉन्ट्रॅक्टसारखा दिसतो. बाकीचे ऑपकोड बाय ऑपकोड तपासण्याऐवजी, आपण डीकंपाइलरचा प्रयत्न करू शकतोopens in a new tab, जो या कॉन्ट्रॅक्टमधून तीन फंक्शन्ससाठी वापरण्यायोग्य परिणाम देतो. इतरांचे रिव्हर्स इंजिनिअरिंग वाचकांसाठी अभ्यास म्हणून सोडले आहे.
scaleAmountByPercentage
या फंक्शनसाठी डीकंपाइलर आपल्याला हे देतो:
1def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable:2 require calldata.size - 4 >=′ 643 if _param1 and _param2 > -1 / _param1:4 revert with 0, 175 return (_param1 * _param2 / 100 * 10^6)पहिले require तपासते की कॉल डेटामध्ये, फंक्शन सिग्नेचरच्या चार बाइट्स व्यतिरिक्त, किमान 64 बाइट्स आहेत, जे दोन पॅरामीटर्ससाठी पुरेसे आहेत. जर नसेल तर अर्थातच काहीतरी चूक आहे.
if स्टेटमेंट असे दिसते की _param1 शून्य नाही आणि _param1 * _param2 ऋण नाही हे तपासते. हे कदाचित रॅप अराउंडचे प्रकार टाळण्यासाठी आहे.
शेवटी, फंक्शन एक स्केल्ड व्हॅल्यू परत करते.
claim
डीकंपाइलर तयार करत असलेला कोड गुंतागुंतीचा आहे आणि त्यापैकी सर्वच आपल्यासाठी संबंधित नाहीत. मी उपयुक्त माहिती पुरवतात असे मला वाटणाऱ्या ओळींवर लक्ष केंद्रित करण्यासाठी त्यापैकी काही वगळणार आहे
1def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable:2 ...3 require _param2 == addr(_param2)4 ...5 if currentWindow <= _param1:6 revert with 0, 'cannot claim for a future window'येथे आपण दोन महत्त्वाच्या गोष्टी पाहतो:
_param2, जरी तेuint256म्हणून घोषित केले असले तरी, प्रत्यक्षात एक पत्ता आहे_param1ही दावा केलेली विंडो आहे, जीcurrentWindowकिंवा त्यापूर्वीची असावी.
1 ...2 if stor5[_claimWindow][addr(_claimFor)]:3 revert with 0, 'Account already claimed the given window'तर आता आपल्याला माहित आहे की स्टोरेज[5] हे विंडोज आणि पत्त्यांचे अॅरे आहे आणि पत्त्याने त्या विंडोसाठी बक्षीस दावा केला आहे की नाही हे दर्शवते.
1 ...2 idx = 03 s = 04 while idx < _param4.length:5 ...6 if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]:7 mem[mem[64] + 32] = mem[(32 * idx) + 296]8 ...9 s = sha3(mem[_62 + 32 len mem[_62]])10 continue11 ...12 s = sha3(mem[_66 + 32 len mem[_66]])13 continue14 if unknown2eb4a7ab != s:15 revert with 0, 'Invalid proof'सर्व दाखवाआम्हाला माहित आहे की unknown2eb4a7ab प्रत्यक्षात merkleRoot() फंक्शन आहे, म्हणून हा कोड मर्कल प्रूफopens in a new tab पडताळणी करत असल्यासारखा दिसतो. याचा अर्थ _param4 हा एक मर्कल प्रूफ आहे.
1 call addr(_param2) with:2 value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei3 gas 30000 weiएक कॉन्ट्रॅक्ट स्वतःचा ETH दुसऱ्या पत्त्यावर (कॉन्ट्रॅक्ट किंवा बाह्य मालकीचा) कसा हस्तांतरित करतो हे असे आहे. ते हस्तांतरित करायच्या रकमेच्या व्हॅल्यूने त्याला कॉल करते. तर असे दिसते की हा ETH चा एअरड्रॉप आहे.
1 if not return_data.size:2 if not ext_call.success:3 require ext_code.size(stor2)4 call stor2.deposit() with:5 value unknown81e580d3[_param1] * _param3 / 100 * 10^6 weiखालील दोन ओळी आपल्याला सांगतात की स्टोरेज[2] हा देखील एक कॉन्ट्रॅक्ट आहे ज्याला आपण कॉल करतो. जर आपण कन्स्ट्रक्टर व्यवहाराकडे पाहिलेopens in a new tab तर आपल्याला दिसेल की हा कॉन्ट्रॅक्ट 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2opens in a new tab आहे, एक रॅप्ड इथर कॉन्ट्रॅक्ट ज्याचा सोर्स कोड Etherscan वर अपलोड केला गेला आहेopens in a new tab.
तर असे दिसते की कॉन्ट्रॅक्ट्स _param2 ला ETH पाठवण्याचा प्रयत्न करतात. जर ते करू शकले, तर छान. जर नाही, तर ते WETHopens in a new tab पाठवण्याचा प्रयत्न करते. जर _param2 एक बाह्य मालकीचे खाते (EOA) असेल तर ते नेहमी ETH प्राप्त करू शकते, परंतु कॉन्ट्रॅक्ट्स ETH प्राप्त करण्यास नकार देऊ शकतात. तथापि, WETH हे ERC-20 आहे आणि कॉन्ट्रॅक्ट्स ते स्वीकारण्यास नकार देऊ शकत नाहीत.
1 ...2 log 0xdbd5389f: addr(_param2), unknown81e580d3[_param1] * _param3 / 100 * 10^6, bool(ext_call.success)फंक्शनच्या शेवटी आपण एक लॉग एंट्री तयार होताना पाहतो. तयार झालेल्या लॉग एंट्रीज पहाopens in a new tab आणि 0xdbd5... ने सुरू होणाऱ्या टॉपिकवर फिल्टर करा. जर आपण अशी एंट्री तयार करणाऱ्या व्यवहारांपैकी एकावर क्लिक केलेopens in a new tab तर आपल्याला दिसेल की खरोखरच तो एक दावा असल्यासारखा दिसतो - खात्याने आपण रिव्हर्स इंजिनिअरिंग करत असलेल्या कॉन्ट्रॅक्टला एक संदेश पाठवला, आणि बदल्यात त्याला ETH मिळाले.
1e7df9d3
हे फंक्शन वरील claim सारखेच आहे. ते देखील मर्कल प्रूफ तपासते, पहिल्याला ETH हस्तांतरित करण्याचा प्रयत्न करते, आणि त्याच प्रकारची लॉग एंट्री तयार करते.
1def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable:2 ...3 idx = 04 s = 05 while idx < _param3.length:6 if idx >= mem[96]:7 revert with 0, 508 _55 = mem[(32 * idx) + 128]9 if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]:10 ...11 s = sha3(mem[_58 + 32 len mem[_58]])12 continue13 mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]])14 ...15 if unknown2eb4a7ab != s:16 revert with 0, 'Invalid proof'17 ...18 call addr(_param1) with:19 value s wei20 gas 30000 wei21 if not return_data.size:22 if not ext_call.success:23 require ext_code.size(stor2)24 call stor2.deposit() with:25 value s wei26 gas gas_remaining wei27 ...28 log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)सर्व दाखवामुख्य फरक असा आहे की पहिला पॅरामीटर, काढण्यासाठीची विंडो, तेथे नाही. त्याऐवजी, दावा करता येणाऱ्या सर्व विंडोजवर एक लूप आहे.
1 idx = 02 s = 03 while idx < currentWindow:4 ...5 if stor5[mem[0]]:6 if idx == -1:7 revert with 0, 178 idx = idx + 19 s = s10 continue11 ...12 stor5[idx][addr(_param1)] = 113 if idx >= unknown81e580d3.length:14 revert with 0, 5015 mem[0] = 416 if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:17 revert with 0, 1718 if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):19 revert with 0, 1720 if idx == -1:21 revert with 0, 1722 idx = idx + 123 s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)24 continueसर्व दाखवातर ते claim चे एक प्रकार वाटते जे सर्व विंडोजचा दावा करते.
निष्कर्ष
आतापर्यंत तुम्हाला माहित झाले असेल की ज्या कॉन्ट्रॅक्ट्सचा सोर्स कोड उपलब्ध नाही ते कसे समजून घ्यायचे, एकतर ऑपकोड्स वापरून किंवा (जेव्हा ते काम करते) डीकंपाइलर वापरून. या लेखाच्या लांबीवरून हे स्पष्ट होते की कॉन्ट्रॅक्टचे रिव्हर्स इंजिनिअरिंग करणे सोपे नाही, परंतु ज्या सिस्टीममध्ये सुरक्षितता आवश्यक आहे तेथे कॉन्ट्रॅक्ट्स वचन दिल्याप्रमाणे काम करतात याची पडताळणी करण्याची क्षमता असणे हे एक महत्त्वाचे कौशल्य आहे.
माझ्या कामाबद्दल अधिक माहितीसाठी येथे पहाopens in a new tab.
पृष्ठ अखेरचे अद्यतन: २२ ऑगस्ट, २०२५



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



