ایک کنٹریکٹ کی ریورس انجینئرنگ
تعارف
بلاک چین پر کوئی راز نہیں ہوتے، جو کچھ بھی ہوتا ہے وہ مستقل، قابل تصدیق اور عوامی طور پر دستیاب ہوتا ہے۔ مثالی طور پر، کنٹریکٹس کا سورس کوڈ Etherscan پر شائع اور تصدیق شدہ ہونا چاہیے (opens in a new tab)۔ تاہم، ہمیشہ ایسا نہیں ہوتا (opens in a new tab)۔ اس مضمون میں آپ سیکھیں گے کہ سورس کوڈ کے بغیر ایک کنٹریکٹ، 0x2510c039cc3b061d79e564b38836da87e31b342f (opens in a new tab) کو دیکھ کر کنٹریکٹس کی ریورس انجینئرنگ کیسے کی جاتی ہے۔
ریورس کمپائلرز موجود ہیں، لیکن وہ ہمیشہ قابل استعمال نتائج (opens in a new tab) پیدا نہیں کرتے۔ اس مضمون میں آپ سیکھیں گے کہ opcodes (opens in a new tab) سے دستی طور پر ریورس انجینئرنگ کر کے کنٹریکٹ کو کیسے سمجھا جائے، اور ساتھ ہی ڈی کمپائلر کے نتائج کی تشریح کیسے کی جائے۔
اس مضمون کو سمجھنے کے لیے آپ کو پہلے سے ہی EVM کی بنیادی باتوں کا علم ہونا چاہیے، اور EVM اسمبلر سے کم از کم کچھ حد تک واقف ہونا چاہیے۔ آپ ان موضوعات کے بارے میں یہاں پڑھ سکتے ہیں (opens in a new tab)۔
ایگزیکیوٹیبل کوڈ تیار کریں
آپ کنٹریکٹ کے لیے Etherscan پر جا کر، Contract ٹیب پر کلک کر کے اور پھر Switch to Opcodes View پر کلک کر کے opcodes حاصل کر سکتے ہیں۔ آپ کو ایک ایسا منظر ملے گا جس میں فی لائن ایک opcode ہوگا۔
تاہم، jumps کو سمجھنے کے لیے، آپ کو یہ جاننے کی ضرورت ہے کہ کوڈ میں ہر opcode کہاں واقع ہے۔ ایسا کرنے کا ایک طریقہ یہ ہے کہ ایک Google Spreadsheet کھولیں اور opcodes کو کالم C میں پیسٹ کریں۔ آپ اس پہلے سے تیار شدہ اسپریڈشیٹ کی کاپی بنا کر درج ذیل مراحل کو چھوڑ سکتے ہیں (opens in a new tab)۔
اگلا قدم کوڈ کے درست مقامات حاصل کرنا ہے تاکہ ہم jumps کو سمجھ سکیں۔ ہم opcode کا سائز کالم B میں، اور مقام (hexadecimal میں) کالم A میں رکھیں گے۔ اس فنکشن کو سیل B1 میں ٹائپ کریں اور پھر اسے کوڈ کے اختتام تک باقی کالم B کے لیے کاپی اور پیسٹ کریں۔ ایسا کرنے کے بعد آپ کالم B کو چھپا سکتے ہیں۔
1=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)پہلے یہ فنکشن خود opcode کے لیے ایک بائٹ کا اضافہ کرتا ہے، اور پھر PUSH کو تلاش کرتا ہے۔ Push opcodes خاص ہوتے ہیں کیونکہ انہیں پش کی جانے والی ویلیو کے لیے اضافی بائٹس کی ضرورت ہوتی ہے۔ اگر opcode ایک PUSH ہے، تو ہم بائٹس کی تعداد نکالتے ہیں اور اسے شامل کرتے ہیں۔
سیل A1 میں پہلا آفسیٹ، صفر (zero) رکھیں۔ پھر، A2 میں، یہ فنکشن رکھیں اور اسے دوبارہ باقی کالم A کے لیے کاپی اور پیسٹ کریں:
1=dec2hex(hex2dec(A1)+B1)ہمیں اس فنکشن کی ضرورت ہے تاکہ یہ ہمیں hexadecimal ویلیو دے کیونکہ jumps (JUMP اور JUMPI) سے پہلے پش کی جانے والی ویلیوز ہمیں hexadecimal میں دی جاتی ہیں۔
انٹری پوائنٹ (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 پر 0x80 کو 32 بائٹ ویلیو کے طور پر لکھیں (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 | 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 پر کانٹریکٹ ریورٹ ہو جاتا ہے اگر اوور فلو کا پتہ چلتا ہے، جو کہ نارمل رویہ ہے۔
نوٹ کریں کہ اس طرح کا اوور فلو انتہائی غیر متوقع ہے، کیونکہ اس کے لیے کال ویلیو پلس 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]-as-address 0x9D 0x00 |
یہ اوپ کوڈز (opcodes) اس ویلیو کو جو ہم Storage[3] سے پڑھتے ہیں، 160 بٹس تک محدود (truncate) کر دیتے ہیں، جو کہ ایک Ethereum ایڈریس کی لمبائی ہے۔
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| 9B | SWAP1 | 0x9D Storage[3]-as-address 0x00 |
| 9C | JUMP | Storage[3]-as-address 0x00 |
یہ جمپ (jump) غیر ضروری ہے، کیونکہ ہم اگلے اوپ کوڈ پر جا رہے ہیں۔ یہ کوڈ گیس کے لحاظ سے اتنا موثر (gas-efficient) نہیں ہے جتنا یہ ہو سکتا تھا۔
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| 9D | JUMPDEST | Storage[3]-as-address 0x00 |
| 9E | SWAP1 | 0x00 Storage[3]-as-address |
| 9F | POP | Storage[3]-as-address |
| A0 | PUSH1 0x40 | 0x40 Storage[3]-as-address |
| A2 | MLOAD | Mem[0x40] Storage[3]-as-address |
کوڈ کے بالکل شروع میں ہم نے Mem[0x40] کو 0x80 پر سیٹ کیا تھا۔ اگر ہم بعد میں 0x40 کو تلاش کریں، تو ہم دیکھتے ہیں کہ ہم اسے تبدیل نہیں کرتے - لہذا ہم فرض کر سکتے ہیں کہ یہ 0x80 ہے۔
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-as-address |
| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 Storage[3]-as-address |
| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-as-address |
| A7 | CALLDATACOPY | 0x80 Storage[3]-as-address |
تمام کال ڈیٹا کو میموری میں کاپی کریں، جس کی شروعات 0x80 سے ہوتی ہے۔
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| A8 | PUSH1 0x00 | 0x00 0x80 Storage[3]-as-address |
| AA | DUP1 | 0x00 0x00 0x80 Storage[3]-as-address |
| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AD | DUP6 | Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AE | GAS | GAS Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AF | DELEGATE_CALL |
اب چیزیں بہت زیادہ واضح ہیں۔ یہ کنٹریکٹ ایک پراکسی (proxy) (opens in a new tab) کے طور پر کام کر سکتا ہے، جو اصل کام کرنے کے لیے Storage[3] میں موجود ایڈریس کو کال کرتا ہے۔ DELEGATE_CALL ایک الگ کنٹریکٹ کو کال کرتا ہے، لیکن اسی اسٹوریج میں رہتا ہے۔ اس کا مطلب یہ ہے کہ ڈیلیگیٹڈ کنٹریکٹ، جس کے لیے ہم ایک پراکسی ہیں، اسی اسٹوریج اسپیس تک رسائی حاصل کرتا ہے۔ کال کے پیرامیٹرز یہ ہیں:
- گیس (Gas): باقی تمام گیس
- کال کیا گیا ایڈریس (Called address): Storage[3]-as-address
- کال ڈیٹا (Call data): 0x80 سے شروع ہونے والے CALLDATASIZE بائٹس، جہاں ہم نے اصل کال ڈیٹا رکھا تھا
- ریٹرن ڈیٹا (Return data): کوئی نہیں (0x00 - 0x00) ہم ریٹرن ڈیٹا دوسرے ذرائع سے حاصل کریں گے (نیچے دیکھیں)
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| B0 | RETURNDATASIZE | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
یہاں ہم تمام ریٹرن ڈیٹا کو 0x80 سے شروع ہونے والے میموری بفر میں کاپی کرتے ہیں۔
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| B6 | DUP2 | (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| B7 | DUP1 | (((کال کی کامیابی/ناکامی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| B8 | ISZERO | (((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| B9 | PUSH2 0x00c0 | 0xC0 (((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| BC | JUMPI | (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| BD | DUP2 | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| BE | DUP5 | 0x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| BF | RETURN |
لہذا کال کے بعد ہم ریٹرن ڈیٹا کو بفر 0x80 - 0x80+RETURNDATASIZE میں کاپی کرتے ہیں، اور اگر کال کامیاب ہو جاتی ہے تو ہم بالکل اسی بفر کے ساتھ RETURN کرتے ہیں۔
DELEGATECALL ناکام ہو گیا
اگر ہم یہاں 0xC0 پر آتے ہیں، تو اس کا مطلب ہے کہ جس کنٹریکٹ کو ہم نے کال کیا تھا وہ ریورٹ (revert) ہو گیا۔ چونکہ ہم اس کنٹریکٹ کے لیے صرف ایک پراکسی ہیں، ہم وہی ڈیٹا واپس کرنا چاہتے ہیں اور ساتھ ہی ریورٹ بھی کرنا چاہتے ہیں۔
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| C0 | JUMPDEST | (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| C1 | DUP2 | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address |
| C3 | REVERT |
لہذا ہم اسی بفر کے ساتھ REVERT کرتے ہیں جو ہم نے پہلے RETURN کے لیے استعمال کیا تھا: 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 بائٹس)))) |
طریقہ کار (method) کے دستخط (signature) کی مماثلت کے ٹیسٹ کو اس طرح دو حصوں میں تقسیم کرنے سے اوسطاً آدھے ٹیسٹ بچ جاتے ہیں۔ وہ کوڈ جو فوراً اس کے بعد آتا ہے اور 0x43 میں موجود کوڈ اسی طرز کی پیروی کرتے ہیں: کال ڈیٹا کے پہلے 32 بٹس کو DUP1 کریں، PUSH4 (((طریقہ کار کے دستخط>، برابری چیک کرنے کے لیے EQ چلائیں، اور پھر اگر طریقہ کار کے دستخط مماثل ہوں تو JUMPI کریں۔ یہاں طریقہ کار کے دستخط، ان کے پتے، اور اگر معلوم ہو تو متعلقہ طریقہ کار کی تعریف (opens in a new tab) دی گئی ہے:
| طریقہ کار (Method) | طریقہ کار کے دستخط | جمپ کرنے کا آفسیٹ |
|---|---|---|
| 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 بھیجا ہے تو یہ یقیناً ایک غلطی ہوگی اور ہم REVERT کرنا چاہتے ہیں تاکہ وہ ETH ایسی جگہ نہ پھنس جائے جہاں سے وہ اسے واپس نہ لے سکیں۔
| آفسیٹ | اوپ کوڈ | اسٹیک |
|---|---|---|
| 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 کوڈ
یہ پہلی بار ہے جب ہم ان لائنوں کو دیکھ رہے ہیں، لیکن یہ دوسرے میتھڈز کے ساتھ شیئر کی گئی ہیں (نیچے دیکھیں)۔ لہذا ہم اسٹیک میں موجود ویلیو کو 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) میں ایک میموری پوائنٹر وصول کرتا ہے، اور کنٹریکٹ کو ایک بفر کے ساتھ RETURN کرنے کا سبب بنتا ہے جو 0x80 - X ہے۔
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 نہیں ہے۔
| آف سیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 |
اگر اسے کال ڈیٹا نہیں ملتا ہے تو ٹرانزیکشن بغیر کسی ریٹرن ڈیٹا کے ریورٹ (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* واپس کرتا ہے۔
میتھڈ کا خلاصہ
کیا آپ کو لگتا ہے کہ آپ اس مقام پر کنٹریکٹ کو سمجھ گئے ہیں؟ مجھے تو نہیں لگتا۔ اب تک ہمارے پاس یہ میتھڈز ہیں:
| میتھڈ | مطلب |
|---|---|
| Transfer | کال کے ذریعے فراہم کردہ ویلیو کو قبول کریں اور اس رقم سے 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) تو ہم وہ ٹرانزیکشن بھی دیکھ سکتے ہیں جس نے اسے بنایا تھا۔
اگر ہم اس ٹرانزیکشن پر کلک کریں، اور پھر State ٹیب پر، تو ہم پیرامیٹرز کی ابتدائی ویلیوز دیکھ سکتے ہیں۔ خاص طور پر، ہم دیکھ سکتے ہیں کہ Storage[3] میں 0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761 (opens in a new tab) شامل ہے۔ اس کنٹریکٹ میں لازمی طور پر وہ غائب فنکشنلٹی موجود ہونی چاہیے۔ ہم اسے انہی ٹولز کا استعمال کرتے ہوئے سمجھ سکتے ہیں جو ہم نے اس کنٹریکٹ کے لیے استعمال کیے تھے جس کی ہم تحقیق کر رہے ہیں۔
پراکسی کنٹریکٹ
اوپر اصل کنٹریکٹ کے لیے استعمال کی گئی انہی تکنیکوں کا استعمال کرتے ہوئے ہم دیکھ سکتے ہیں کہ کنٹریکٹ ریورٹ (revert) ہو جاتا ہے اگر:
- کال کے ساتھ کوئی ETH منسلک ہو (0x05-0x0F)
- کال ڈیٹا کا سائز چار سے کم ہو (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 |
ہم نیچے والے چار میتھڈز کو نظر انداز کر سکتے ہیں کیونکہ ہم کبھی ان تک نہیں پہنچ پائیں گے۔ ان کے سگنیچرز ایسے ہیں کہ ہمارا اصل کنٹریکٹ خود ہی ان کو سنبھال لیتا ہے (آپ اوپر تفصیلات دیکھنے کے لیے سگنیچرز پر کلک کر سکتے ہیں)، اس لیے یہ لازمی طور پر اوور رائیڈ کیے گئے میتھڈز (overridden methods) (opens in a new tab) ہوں گے۔
باقی ماندہ میتھڈز میں سے ایک claim(<params>) ہے، اور دوسرا isClaimed(<params>) ہے، لہذا یہ ایک ایئر ڈراپ (airdrop) کنٹریکٹ لگتا ہے۔ باقی ماندہ کو ایک ایک اوپ کوڈ (opcode) کر کے دیکھنے کے بجائے، ہم ڈی کمپائلر (decompiler) آزما سکتے ہیں (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 منفی نہیں ہے۔ یہ غالباً ریپ اراؤنڈ (wrap around) کے معاملات کو روکنے کے لیے ہے۔
آخر میں، فنکشن ایک اسکیلڈ (scaled) ویلیو واپس کرتا ہے۔
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'تو اب ہم جانتے ہیں کہ Storage[5] ونڈوز اور ایڈریسز کی ایک ایرے (array) ہے، اور یہ کہ آیا اس ایڈریس نے اس ونڈو کے لیے انعام کلیم کیا ہے یا نہیں۔
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() فنکشن ہے، لہذا یہ کوڈ ایسا لگتا ہے کہ یہ ایک مرکل پروف (merkle proof) (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نیچے کی دو لائنیں ہمیں بتاتی ہیں کہ Storage[2] بھی ایک کنٹریکٹ ہے جسے ہم کال کرتے ہیں۔ اگر ہم کنسٹرکٹر ٹرانزیکشن کو دیکھیں (opens in a new tab) تو ہم دیکھتے ہیں کہ یہ کنٹریکٹ 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (opens in a new tab) ہے، جو ایک Wrapped Ether کنٹریکٹ ہے جس کا سورس کوڈ Etherscan پر اپ لوڈ کیا گیا ہے (opens in a new tab)۔
لہذا ایسا لگتا ہے کہ کنٹریکٹ _param2 کو ETH بھیجنے کی کوشش کرتا ہے۔ اگر یہ ایسا کر سکتا ہے، تو بہت اچھا۔ اگر نہیں، تو یہ WETH (opens 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)فنکشن کے آخر میں ہم دیکھتے ہیں کہ ایک لاگ (log) انٹری تیار کی جا رہی ہے۔ تیار کردہ لاگ انٹریز کو دیکھیں (opens in a new tab) اور اس ٹاپک پر فلٹر کریں جو 0xdbd5... سے شروع ہوتا ہے۔ اگر ہم ایسی انٹری تیار کرنے والی ٹرانزیکشنز میں سے کسی ایک پر کلک کریں (opens in a new tab) تو ہم دیکھتے ہیں کہ واقعی یہ ایک کلیم (claim) کی طرح لگتا ہے - اکاؤنٹ نے اس کنٹریکٹ کو ایک پیغام بھیجا جس کی ہم ریورس انجینئرنگ کر رہے ہیں، اور بدلے میں 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)سب دکھائیںبنیادی فرق یہ ہے کہ پہلا پیرامیٹر، یعنی نکالنے والی ونڈو، وہاں موجود نہیں ہے۔ اس کے بجائے، ان تمام ونڈوز پر ایک لوپ (loop) ہے جنہیں کلیم کیا جا سکتا ہے۔
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 ویرینٹ (variant) کی طرح لگتا ہے جو تمام ونڈوز کو کلیم کرتا ہے۔
نتیجہ
اب تک آپ کو یہ جان لینا چاہیے کہ ان معاہدوں کو کیسے سمجھا جائے جن کا سورس کوڈ دستیاب نہیں ہے، جس کے لیے یا تو opcodes کا استعمال کیا جاتا ہے یا (جب یہ کام کرے) ڈی کمپائلر کا۔ جیسا کہ اس مضمون کی طوالت سے ظاہر ہے، کسی معاہدے کی ریورس انجینئرنگ کرنا کوئی معمولی بات نہیں ہے، لیکن ایک ایسے سسٹم میں جہاں سیکیورٹی انتہائی ضروری ہو، یہ تصدیق کرنے کے قابل ہونا ایک اہم مہارت ہے کہ معاہدے اپنے وعدے کے مطابق کام کرتے ہیں۔
میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔
صفحہ کی آخری اپ ڈیٹ: 22 اگست، 2025



![Storage[6] میں تبدیلی](/_next/image/?url=%2Fcontent%2Fdevelopers%2Ftutorials%2Freverse-engineering-a-contract%2Fstorage6.png&w=1920&q=75)



