مرکزی مواد پر جائیں

ایک کنٹریکٹ کی ریورس انجینئرنگ

evm
opcodes
ایڈوانسڈ
اوری پومرانٹز
30 دسمبر، 2021
36 منٹ کی پڑھائی

تعارف

بلاک چین پر کوئی راز نہیں ہوتے، جو کچھ بھی ہوتا ہے وہ مستقل، قابل تصدیق اور عوامی طور پر دستیاب ہوتا ہے۔ مثالی طور پر، کنٹریکٹس کا سورس کوڈ 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 ہوگا۔

Etherscan سے 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)

کانٹریکٹس ہمیشہ پہلی بائٹ سے ایگزیکیوٹ ہوتے ہیں۔ یہ کوڈ کا ابتدائی حصہ ہے:

آفسیٹآپ کوڈاسٹیک (آپ کوڈ کے بعد)
0PUSH1 0x800x80
2PUSH1 0x400x40, 0x80
4MSTOREخالی
5PUSH1 0x040x04
7CALLDATASIZECALLDATASIZE 0x04
8LTCALLDATASIZE<4
9PUSH2 0x005e0x5E CALLDATASIZE<4
CJUMPIخالی

یہ کوڈ دو کام کرتا ہے:

  1. میموری لوکیشنز 0x40-0x5F پر 0x80 کو 32 بائٹ ویلیو کے طور پر لکھیں (0x80 کو 0x5F میں اسٹور کیا جاتا ہے، اور 0x40-0x5E سب صفر ہیں)۔
  2. کال ڈیٹا کا سائز پڑھیں۔ عام طور پر ایتھیریم کانٹریکٹ کا کال ڈیٹا ABI (ایپلیکیشن بائنری انٹرفیس) (opens in a new tab) کی پیروی کرتا ہے، جس کے لیے فنکشن سلیکٹر کے لیے کم از کم چار بائٹس درکار ہوتے ہیں۔ اگر کال ڈیٹا کا سائز چار سے کم ہے، تو 0x5E پر جمپ کریں۔

اس حصے کا فلو چارٹ

0x5E پر ہینڈلر (نان-ABI کال ڈیٹا کے لیے)

آفسیٹآپ کوڈ
5EJUMPDEST
5FCALLDATASIZE
60PUSH2 0x007c
63JUMPI

یہ اسنیپٹ JUMPDEST سے شروع ہوتا ہے۔ EVM (ایتھیریم ورچوئل مشین) پروگرامز ایک ایکسیپشن تھرو کرتے ہیں اگر آپ کسی ایسے آپ کوڈ پر جمپ کرتے ہیں جو JUMPDEST نہیں ہے۔ پھر یہ CALLDATASIZE کو دیکھتا ہے، اور اگر یہ "true" ہے (یعنی صفر نہیں ہے) تو 0x7C پر جمپ کرتا ہے۔ ہم اس پر نیچے بات کریں گے۔

آفسیٹآپ کوڈاسٹیک (آپ کوڈ کے بعد)
64CALLVALUEکال کے ذریعے فراہم کردہ ۔ Solidity میں اسے msg.value کہا جاتا ہے
65PUSH1 0x066 CALLVALUE
67PUSH1 0x000 6 CALLVALUE
69DUP3CALLVALUE 0 6 CALLVALUE
6ADUP36 CALLVALUE 0 6 CALLVALUE
6BSLOADStorage[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 میں ٹرانسفر کی گئی ویلیو ہے (میں نے وضاحت کے لیے کوما شامل کیے ہیں)، جو اگلی کانٹریکٹ ویلیو کے مساوی ہے۔

Storage[6] میں تبدیلی

اگر ہم اسی عرصے کی دیگر Transfer ٹرانزیکشنز (opens in a new tab) کی وجہ سے ہونے والی اسٹیٹ تبدیلیوں کو دیکھیں تو ہم دیکھتے ہیں کہ Storage[6] نے کچھ عرصے تک کانٹریکٹ کی ویلیو کو ٹریک کیا۔ فی الحال ہم اسے Value* کہیں گے۔ ایسٹرسک (*) ہمیں یاد دلاتا ہے کہ ہم ابھی تک نہیں جانتے کہ یہ ویری ایبل کیا کرتا ہے، لیکن یہ صرف کانٹریکٹ کی ویلیو کو ٹریک کرنے کے لیے نہیں ہو سکتا کیونکہ اسٹوریج استعمال کرنے کی کوئی ضرورت نہیں ہے، جو کہ بہت مہنگا ہے، جب آپ ADDRESS BALANCE کا استعمال کر کے اپنے اکاؤنٹس کا بیلنس حاصل کر سکتے ہیں۔ پہلا آپ کوڈ کانٹریکٹ کا اپنا ایڈریس پش کرتا ہے۔ دوسرا اسٹیک کے اوپری حصے پر موجود ایڈریس کو پڑھتا ہے اور اسے اس ایڈریس کے بیلنس سے بدل دیتا ہے۔

آفسیٹآپ کوڈاسٹیک
6CPUSH2 0x00750x75 Value* CALLVALUE 0 6 CALLVALUE
6FSWAP2CALLVALUE Value* 0x75 0 6 CALLVALUE
70SWAP1Value* CALLVALUE 0x75 0 6 CALLVALUE
71PUSH2 0x01a70x01A7 Value* CALLVALUE 0x75 0 6 CALLVALUE
74JUMP

ہم جمپ ڈیسٹینیشن پر اس کوڈ کو ٹریس کرنا جاری رکھیں گے۔

آفسیٹآپ کوڈاسٹیک
1A7JUMPDESTValue* CALLVALUE 0x75 0 6 CALLVALUE
1A8PUSH1 0x000x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1AADUP3CALLVALUE 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1ABNOT2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE

NOT بٹ وائز ہے، لہذا یہ کال ویلیو میں ہر بٹ کی ویلیو کو الٹ دیتا ہے۔

آفسیٹآپ کوڈاسٹیک
1ACDUP3Value* 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1ADGTValue*>2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1AEISZEROValue*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1AFPUSH2 0x01df0x01DF Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1B2JUMPI

ہم جمپ کرتے ہیں اگر Value* 2^256-CALLVALUE-1 سے چھوٹا ہے یا اس کے برابر ہے۔ یہ اوور فلو کو روکنے کے لیے لاجک کی طرح لگتا ہے۔ اور واقعی، ہم دیکھتے ہیں کہ کچھ بے معنی آپریشنز کے بعد (مثال کے طور پر میموری میں لکھنا ڈیلیٹ ہونے والا ہے) آفسیٹ 0x01DE پر کانٹریکٹ ریورٹ ہو جاتا ہے اگر اوور فلو کا پتہ چلتا ہے، جو کہ نارمل رویہ ہے۔

نوٹ کریں کہ اس طرح کا اوور فلو انتہائی غیر متوقع ہے، کیونکہ اس کے لیے کال ویلیو پلس Value* کا 2^256 wei کے برابر ہونا ضروری ہوگا، جو تقریباً 10^59 ETH بنتا ہے۔ لکھتے وقت، کل ETH سپلائی دو سو ملین سے کم ہے (opens in a new tab)۔

آفسیٹآپ کوڈاسٹیک
1DFJUMPDEST0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1E0POPValue* CALLVALUE 0x75 0 6 CALLVALUE
1E1ADDValue*+CALLVALUE 0x75 0 6 CALLVALUE
1E2SWAP10x75 Value*+CALLVALUE 0 6 CALLVALUE
1E3JUMP

اگر ہم یہاں پہنچ گئے ہیں، تو Value* + CALLVALUE حاصل کریں اور آفسیٹ 0x75 پر جمپ کریں۔

آفسیٹآپ کوڈاسٹیک
75JUMPDESTValue*+CALLVALUE 0 6 CALLVALUE
76SWAP10 Value*+CALLVALUE 6 CALLVALUE
77SWAP26 Value*+CALLVALUE 0 CALLVALUE
78SSTORE0 CALLVALUE

اگر ہم یہاں پہنچتے ہیں (جس کے لیے کال ڈیٹا کا خالی ہونا ضروری ہے) تو ہم Value* میں کال ویلیو شامل کرتے ہیں۔ یہ اس بات سے مطابقت رکھتا ہے جو ہم کہتے ہیں کہ Transfer ٹرانزیکشنز کرتی ہیں۔

آفسیٹآپ کوڈ
79POP
7APOP
7BSTOP

آخر میں، اسٹیک کو کلیئر کریں (جو ضروری نہیں ہے) اور ٹرانزیکشن کے کامیاب اختتام کا سگنل دیں۔

مختصراً، یہاں ابتدائی کوڈ کے لیے ایک فلو چارٹ ہے۔

انٹری پوائنٹ فلو چارٹ

0x7C پر ہینڈلر

میں نے جان بوجھ کر ہیڈنگ میں یہ نہیں لکھا کہ یہ ہینڈلر کیا کرتا ہے۔ مقصد آپ کو یہ سکھانا نہیں ہے کہ یہ مخصوص کنٹریکٹ کیسے کام کرتا ہے، بلکہ یہ سکھانا ہے کہ کنٹریکٹس کو ریورس انجینئر کیسے کیا جاتا ہے۔ آپ اسی طرح سیکھیں گے کہ یہ کیا کرتا ہے جیسے میں نے سیکھا، یعنی کوڈ کو فالو کر کے۔

ہم یہاں کئی جگہوں سے آتے ہیں:

  • اگر 1، 2، یا 3 بائٹس کا کال ڈیٹا ہو (آفسیٹ 0x63 سے)
  • اگر میتھڈ سگنیچر نامعلوم ہو (آفسیٹس 0x42 اور 0x5D سے)
آفسیٹاوپ کوڈاسٹیک
7CJUMPDEST
7DPUSH1 0x000x00
7FPUSH2 0x009d0x9D 0x00
82PUSH1 0x030x03 0x9D 0x00
84SLOADStorage[3] 0x9D 0x00

یہ ایک اور اسٹوریج سیل ہے، جو مجھے کسی ٹرانزیکشن میں نہیں مل سکا اس لیے یہ جاننا مشکل ہے کہ اس کا کیا مطلب ہے۔ نیچے دیا گیا کوڈ اسے مزید واضح کر دے گا۔

آفسیٹاوپ کوڈاسٹیک
85PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xff....ff Storage[3] 0x9D 0x00
9AANDStorage[3]-as-address 0x9D 0x00

یہ اوپ کوڈز (opcodes) اس ویلیو کو جو ہم Storage[3] سے پڑھتے ہیں، 160 بٹس تک محدود (truncate) کر دیتے ہیں، جو کہ ایک Ethereum ایڈریس کی لمبائی ہے۔

آفسیٹاوپ کوڈاسٹیک
9BSWAP10x9D Storage[3]-as-address 0x00
9CJUMPStorage[3]-as-address 0x00

یہ جمپ (jump) غیر ضروری ہے، کیونکہ ہم اگلے اوپ کوڈ پر جا رہے ہیں۔ یہ کوڈ گیس کے لحاظ سے اتنا موثر (gas-efficient) نہیں ہے جتنا یہ ہو سکتا تھا۔

آفسیٹاوپ کوڈاسٹیک
9DJUMPDESTStorage[3]-as-address 0x00
9ESWAP10x00 Storage[3]-as-address
9FPOPStorage[3]-as-address
A0PUSH1 0x400x40 Storage[3]-as-address
A2MLOADMem[0x40] Storage[3]-as-address

کوڈ کے بالکل شروع میں ہم نے Mem[0x40] کو 0x80 پر سیٹ کیا تھا۔ اگر ہم بعد میں 0x40 کو تلاش کریں، تو ہم دیکھتے ہیں کہ ہم اسے تبدیل نہیں کرتے - لہذا ہم فرض کر سکتے ہیں کہ یہ 0x80 ہے۔

آفسیٹاوپ کوڈاسٹیک
A3CALLDATASIZECALLDATASIZE 0x80 Storage[3]-as-address
A4PUSH1 0x000x00 CALLDATASIZE 0x80 Storage[3]-as-address
A6DUP30x80 0x00 CALLDATASIZE 0x80 Storage[3]-as-address
A7CALLDATACOPY0x80 Storage[3]-as-address

تمام کال ڈیٹا کو میموری میں کاپی کریں، جس کی شروعات 0x80 سے ہوتی ہے۔

آفسیٹاوپ کوڈاسٹیک
A8PUSH1 0x000x00 0x80 Storage[3]-as-address
AADUP10x00 0x00 0x80 Storage[3]-as-address
ABCALLDATASIZECALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address
ACDUP40x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address
ADDUP6Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address
AEGASGAS Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address
AFDELEGATE_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) ہم ریٹرن ڈیٹا دوسرے ذرائع سے حاصل کریں گے (نیچے دیکھیں)
آفسیٹاوپ کوڈاسٹیک
B0RETURNDATASIZERETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
B1DUP1RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
B2PUSH1 0x000x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
B4DUP50x80 0x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
B5RETURNDATACOPYRETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address

یہاں ہم تمام ریٹرن ڈیٹا کو 0x80 سے شروع ہونے والے میموری بفر میں کاپی کرتے ہیں۔

آفسیٹاوپ کوڈاسٹیک
B6DUP2(((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
B7DUP1(((کال کی کامیابی/ناکامی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
B8ISZERO(((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
B9PUSH2 0x00c00xC0 (((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
BCJUMPI(((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
BDDUP2RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
BEDUP50x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
BFRETURN

لہذا کال کے بعد ہم ریٹرن ڈیٹا کو بفر 0x80 - 0x80+RETURNDATASIZE میں کاپی کرتے ہیں، اور اگر کال کامیاب ہو جاتی ہے تو ہم بالکل اسی بفر کے ساتھ RETURN کرتے ہیں۔

DELEGATECALL ناکام ہو گیا

اگر ہم یہاں 0xC0 پر آتے ہیں، تو اس کا مطلب ہے کہ جس کنٹریکٹ کو ہم نے کال کیا تھا وہ ریورٹ (revert) ہو گیا۔ چونکہ ہم اس کنٹریکٹ کے لیے صرف ایک پراکسی ہیں، ہم وہی ڈیٹا واپس کرنا چاہتے ہیں اور ساتھ ہی ریورٹ بھی کرنا چاہتے ہیں۔

آفسیٹاوپ کوڈاسٹیک
C0JUMPDEST(((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
C1DUP2RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
C2DUP50x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-as-address
C3REVERT

لہذا ہم اسی بفر کے ساتھ REVERT کرتے ہیں جو ہم نے پہلے RETURN کے لیے استعمال کیا تھا: 0x80 - 0x80+RETURNDATASIZE

پراکسی فلو چارٹ کو کال کریں

ABI کالز

اگر کال ڈیٹا کا سائز چار بائٹس یا اس سے زیادہ ہے تو یہ ایک درست ABI کال ہو سکتی ہے۔

آفسیٹاوپ کوڈاسٹیک
DPUSH1 0x000x00
FCALLDATALOAD(((کال ڈیٹا کا پہلا لفظ (256 بٹس))))
10PUSH1 0xe00xE0 (((کال ڈیٹا کا پہلا لفظ (256 بٹس))))
12SHR(((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))

Etherscan ہمیں بتاتا ہے کہ 1C ایک نامعلوم اوپ کوڈ ہے، کیونکہ اسے Etherscan کے اس فیچر کو لکھنے کے بعد شامل کیا گیا تھا (opens in a new tab) اور انہوں نے اسے اپ ڈیٹ نہیں کیا ہے۔ ایک اپ ٹو ڈیٹ اوپ کوڈ ٹیبل (opens in a new tab) ہمیں دکھاتا ہے کہ یہ شفٹ رائٹ (shift right) ہے

آفسیٹاوپ کوڈاسٹیک
13DUP1(((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس)))) (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
14PUSH4 0x3cd8045e0x3CD8045E (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس)))) (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
19GT0x3CD8045E>first-32-bits-of-the-call-data (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
1APUSH2 0x00430x43 0x3CD8045E>first-32-bits-of-the-call-data (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
1DJUMPI(((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))

طریقہ کار (method) کے دستخط (signature) کی مماثلت کے ٹیسٹ کو اس طرح دو حصوں میں تقسیم کرنے سے اوسطاً آدھے ٹیسٹ بچ جاتے ہیں۔ وہ کوڈ جو فوراً اس کے بعد آتا ہے اور 0x43 میں موجود کوڈ اسی طرز کی پیروی کرتے ہیں: کال ڈیٹا کے پہلے 32 بٹس کو DUP1 کریں، PUSH4 (((طریقہ کار کے دستخط>، برابری چیک کرنے کے لیے EQ چلائیں، اور پھر اگر طریقہ کار کے دستخط مماثل ہوں تو JUMPI کریں۔ یہاں طریقہ کار کے دستخط، ان کے پتے، اور اگر معلوم ہو تو متعلقہ طریقہ کار کی تعریف (opens in a new tab) دی گئی ہے:

طریقہ کار (Method)طریقہ کار کے دستخطجمپ کرنے کا آفسیٹ
splitter() (opens in a new tab)0x3cd8045e0x0103
???0x81e580d30x0138
currentWindow() (opens in a new tab)0xba0bafb40x0158
???0x1f1358230x00C4
merkleRoot() (opens in a new tab)0x2eb4a7ab0x00ED

اگر کوئی مماثلت نہیں ملتی ہے، تو کوڈ 0x7C پر موجود پراکسی ہینڈلر پر جمپ کر جاتا ہے، اس امید کے ساتھ کہ جس کنٹریکٹ کے لیے ہم پراکسی ہیں اس میں کوئی مماثلت موجود ہو۔

ABI کالز کا فلو چارٹ

splitter()

آفسیٹاوپ کوڈاسٹیک
103JUMPDEST
104CALLVALUECALLVALUE
105DUP1CALLVALUE CALLVALUE
106ISZEROCALLVALUE==0 CALLVALUE
107PUSH2 0x010f0x010F CALLVALUE==0 CALLVALUE
10AJUMPICALLVALUE
10BPUSH1 0x000x00 CALLVALUE
10DDUP10x00 0x00 CALLVALUE
10EREVERT

یہ فنکشن سب سے پہلے یہ چیک کرتا ہے کہ کال نے کوئی ETH تو نہیں بھیجا۔ یہ فنکشن payable (opens in a new tab) نہیں ہے۔ اگر کسی نے ہمیں ETH بھیجا ہے تو یہ یقیناً ایک غلطی ہوگی اور ہم REVERT کرنا چاہتے ہیں تاکہ وہ ETH ایسی جگہ نہ پھنس جائے جہاں سے وہ اسے واپس نہ لے سکیں۔

آفسیٹاوپ کوڈاسٹیک
10FJUMPDEST
110POP
111PUSH1 0x030x03
113SLOAD(((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم ایک پراکسی ہیں)))
114PUSH1 0x400x40 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم ایک پراکسی ہیں)))
116MLOAD0x80 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم ایک پراکسی ہیں)))
117PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xFF...FF 0x80 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم ایک پراکسی ہیں)))
12CSWAP10x80 0xFF...FF (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم ایک پراکسی ہیں)))
12DSWAP2(((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم ایک پراکسی ہیں))) 0xFF...FF 0x80
12EANDProxyAddr 0x80
12FDUP20x80 ProxyAddr 0x80
130MSTORE0x80

اور 0x80 میں اب پراکسی ایڈریس موجود ہے۔

آفسیٹاوپ کوڈاسٹیک
131PUSH1 0x200x20 0x80
133ADD0xA0
134PUSH2 0x00e40xE4 0xA0
137JUMP0xA0

E4 کوڈ

یہ پہلی بار ہے جب ہم ان لائنوں کو دیکھ رہے ہیں، لیکن یہ دوسرے میتھڈز کے ساتھ شیئر کی گئی ہیں (نیچے دیکھیں)۔ لہذا ہم اسٹیک میں موجود ویلیو کو X کہیں گے، اور بس یہ یاد رکھیں کہ splitter() میں اس X کی ویلیو 0xA0 ہے۔

آفسیٹاوپ کوڈاسٹیک
E4JUMPDESTX
E5PUSH1 0x400x40 X
E7MLOAD0x80 X
E8DUP10x80 0x80 X
E9SWAP2X 0x80 0x80
EASUBX-0x80 0x80
EBSWAP10x80 X-0x80
ECRETURN

لہذا یہ کوڈ اسٹیک (X) میں ایک میموری پوائنٹر وصول کرتا ہے، اور کنٹریکٹ کو ایک بفر کے ساتھ RETURN کرنے کا سبب بنتا ہے جو 0x80 - X ہے۔

splitter() کے معاملے میں، یہ وہ ایڈریس واپس کرتا ہے جس کے لیے ہم ایک پراکسی ہیں۔ RETURN بفر کو 0x80-0x9F میں واپس کرتا ہے، جو وہ جگہ ہے جہاں ہم نے یہ ڈیٹا لکھا تھا (اوپر آفسیٹ 0x130)۔

currentWindow()

آفسیٹس 0x158-0x163 میں موجود کوڈ بالکل ویسا ہی ہے جیسا ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (سوائے JUMPI کی منزل کے)، اس لیے ہم جانتے ہیں کہ currentWindow() بھی payable نہیں ہے۔

آفسیٹاوپ کوڈاسٹیک
164JUMPDEST
165POP
166PUSH2 0x00da0xDA
169PUSH1 0x010x01 0xDA
16BSLOADStorage[1] 0xDA
16CDUP20xDA Storage[1] 0xDA
16DJUMPStorage[1] 0xDA

DA کوڈ

یہ کوڈ دوسرے میتھڈز کے ساتھ بھی شیئر کیا گیا ہے۔ اس لیے ہم اسٹیک میں موجود ویلیو کو Y کہیں گے، اور بس یہ یاد رکھیں گے کہ currentWindow() میں اس Y کی ویلیو Storage[1] ہے۔

آفسیٹاوپ کوڈاسٹیک
DAJUMPDESTY 0xDA
DBPUSH1 0x400x40 Y 0xDA
DDMLOAD0x80 Y 0xDA
DESWAP1Y 0x80 0xDA
DFDUP20x80 Y 0x80 0xDA
E0MSTORE0x80 0xDA

Y کو 0x80-0x9F پر لکھیں۔

آفسیٹاوپ کوڈاسٹیک
E1PUSH1 0x200x20 0x80 0xDA
E3ADD0xA0 0xDA

اور باقی کی وضاحت پہلے ہی اوپر کی جا چکی ہے۔ لہذا 0xDA پر جمپس اسٹیک کے ٹاپ (Y) کو 0x80-0x9F پر لکھتے ہیں، اور وہ ویلیو ریٹرن کرتے ہیں۔ currentWindow() کے معاملے میں، یہ Storage[1] ریٹرن کرتا ہے۔

merkleRoot()

آف سیٹس 0xED-0xF8 میں موجود کوڈ بالکل ویسا ہی ہے جیسا ہم نے splitter() کے 0x103-0x10E میں دیکھا تھا (سوائے JUMPI کی منزل کے)، اس لیے ہم جانتے ہیں کہ merkleRoot() بھی payable نہیں ہے۔

آف سیٹOpcodeاسٹیک
F9JUMPDEST
FAPOP
FBPUSH2 0x00da0xDA
FEPUSH1 0x000x00 0xDA
100SLOADStorage[0] 0xDA
101DUP20xDA Storage[0] 0xDA
102JUMPStorage[0] 0xDA

جمپ کے بعد کیا ہوتا ہے یہ ہم پہلے ہی جان چکے ہیں۔ لہذا merkleRoot() Storage[0] واپس کرتا ہے۔

0x81e580d3

آف سیٹس 0x138-0x143 میں موجود کوڈ بالکل ویسا ہی ہے جیسا ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (سوائے JUMPI کی منزل کے)، اس لیے ہم جانتے ہیں کہ یہ فنکشن بھی payable نہیں ہے۔

آف سیٹآپ کوڈاسٹیک
144JUMPDEST
145POP
146PUSH2 0x00da0xDA
149PUSH2 0x01530x0153 0xDA
14CCALLDATASIZECALLDATASIZE 0x0153 0xDA
14DPUSH1 0x040x04 CALLDATASIZE 0x0153 0xDA
14FPUSH2 0x018f0x018F 0x04 CALLDATASIZE 0x0153 0xDA
152JUMP0x04 CALLDATASIZE 0x0153 0xDA
18FJUMPDEST0x04 CALLDATASIZE 0x0153 0xDA
190PUSH1 0x000x00 0x04 CALLDATASIZE 0x0153 0xDA
192PUSH1 0x200x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
194DUP30x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
195DUP5CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
196SUBCALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
197SLTCALLDATASIZE-4<32 0x00 0x04 CALLDATASIZE 0x0153 0xDA
198ISZEROCALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA
199PUSH2 0x01a00x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA
19CJUMPI0x00 0x04 CALLDATASIZE 0x0153 0xDA

ایسا لگتا ہے کہ یہ فنکشن کال ڈیٹا کے کم از کم 32 بائٹس (ایک ورڈ) لیتا ہے۔

آف سیٹآپ کوڈاسٹیک
19DDUP10x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA
19EDUP20x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA
19FREVERT

اگر اسے کال ڈیٹا نہیں ملتا ہے تو ٹرانزیکشن بغیر کسی ریٹرن ڈیٹا کے ریورٹ (revert) ہو جاتی ہے۔

آئیے دیکھتے ہیں کہ اگر فنکشن کو اپنی ضرورت کا کال ڈیٹا مل جائے تو کیا ہوتا ہے۔

آف سیٹآپ کوڈاسٹیک
1A0JUMPDEST0x00 0x04 CALLDATASIZE 0x0153 0xDA
1A1POP0x04 CALLDATASIZE 0x0153 0xDA
1A2CALLDATALOADcalldataload(4) CALLDATASIZE 0x0153 0xDA

میتھڈ سگنیچر کے بعد کال ڈیٹا کا پہلا ورڈ calldataload(4) ہے۔

آف سیٹآپ کوڈاسٹیک
1A3SWAP20x0153 CALLDATASIZE calldataload(4) 0xDA
1A4SWAP1CALLDATASIZE 0x0153 calldataload(4) 0xDA
1A5POP0x0153 calldataload(4) 0xDA
1A6JUMPcalldataload(4) 0xDA
153JUMPDESTcalldataload(4) 0xDA
154PUSH2 0x016e0x016E calldataload(4) 0xDA
157JUMPcalldataload(4) 0xDA
16EJUMPDESTcalldataload(4) 0xDA
16FPUSH1 0x040x04 calldataload(4) 0xDA
171DUP2calldataload(4) 0x04 calldataload(4) 0xDA
172DUP20x04 calldataload(4) 0x04 calldataload(4) 0xDA
173SLOADStorage[4] calldataload(4) 0x04 calldataload(4) 0xDA
174DUP2calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA
175LTcalldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA
176PUSH2 0x017e0x017EC calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA
179JUMPIcalldataload(4) 0x04 calldataload(4) 0xDA

اگر پہلا ورڈ Storage[4] سے کم نہیں ہے، تو فنکشن فیل ہو جاتا ہے۔ یہ بغیر کسی ریٹرن ویلیو کے ریورٹ ہو جاتا ہے:

آف سیٹآپ کوڈاسٹیک
17APUSH1 0x000x00 ...
17CDUP10x00 0x00 ...
17DREVERT

اگر calldataload(4) کی ویلیو Storage[4] سے کم ہے، تو ہمیں یہ کوڈ ملتا ہے:

آف سیٹآپ کوڈاسٹیک
17EJUMPDESTcalldataload(4) 0x04 calldataload(4) 0xDA
17FPUSH1 0x000x00 calldataload(4) 0x04 calldataload(4) 0xDA
181SWAP20x04 calldataload(4) 0x00 calldataload(4) 0xDA
182DUP30x00 0x04 calldataload(4) 0x00 calldataload(4) 0xDA
183MSTOREcalldataload(4) 0x00 calldataload(4) 0xDA

اور میموری لوکیشنز 0x00-0x1F میں اب 0x04 ڈیٹا موجود ہے (0x00-0x1E سب صفر ہیں، 0x1F چار ہے)

آف سیٹآپ کوڈاسٹیک
184PUSH1 0x200x20 calldataload(4) 0x00 calldataload(4) 0xDA
186SWAP1calldataload(4) 0x20 0x00 calldataload(4) 0xDA
187SWAP20x00 0x20 calldataload(4) calldataload(4) 0xDA
188SHA3(((SHA3 of 0x00-0x1F))) calldataload(4) calldataload(4) 0xDA
189ADD(((SHA3 of 0x00-0x1F)))+calldataload(4) calldataload(4) 0xDA
18ASLOADStorage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] calldataload(4) 0xDA

لہذا اسٹوریج میں ایک لک اپ ٹیبل موجود ہے، جو 0x000...0004 کے SHA3 سے شروع ہوتا ہے اور اس میں ہر درست کال ڈیٹا ویلیو (Storage[4] سے کم ویلیو) کے لیے ایک انٹری موجود ہے۔

آف سیٹآپ کوڈاسٹیک
18BSWAP1calldataload(4) Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA
18CPOPStorage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA
18DDUP20xDA Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA
18EJUMPStorage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA

ہم پہلے ہی جانتے ہیں کہ آف سیٹ 0xDA پر موجود کوڈ کیا کرتا ہے، یہ کالر کو اسٹیک کی ٹاپ ویلیو واپس کرتا ہے۔ لہذا یہ فنکشن لک اپ ٹیبل سے ویلیو کالر کو واپس کرتا ہے۔

0x1f135823

آف سیٹس 0xC4-0xCF میں موجود کوڈ بالکل ویسا ہی ہے جیسا ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (JUMPI کی منزل کے علاوہ)، اس لیے ہم جانتے ہیں کہ یہ فنکشن بھی payable نہیں ہے۔

آف سیٹآپ کوڈاسٹیک
D0JUMPDEST
D1POP
D2PUSH2 0x00da0xDA
D5PUSH1 0x060x06 0xDA
D7SLOADValue* 0xDA
D8DUP20xDA Value* 0xDA
D9JUMPValue* 0xDA

ہم پہلے ہی جانتے ہیں کہ آف سیٹ 0xDA پر موجود کوڈ کیا کرتا ہے، یہ کالر کو اسٹیک کی سب سے اوپری ویلیو واپس کرتا ہے۔ لہذا یہ فنکشن Value* واپس کرتا ہے۔

میتھڈ کا خلاصہ

کیا آپ کو لگتا ہے کہ آپ اس مقام پر کنٹریکٹ کو سمجھ گئے ہیں؟ مجھے تو نہیں لگتا۔ اب تک ہمارے پاس یہ میتھڈز ہیں:

میتھڈمطلب
Transferکال کے ذریعے فراہم کردہ ویلیو کو قبول کریں اور اس رقم سے Value* میں اضافہ کریں
splitter()Storage[3] واپس کریں، جو پراکسی ایڈریس ہے
currentWindow()Storage[1] واپس کریں
merkleRoot()Storage[0] واپس کریں
0x81e580d3ایک لک اپ ٹیبل سے ویلیو واپس کریں، بشرطیکہ پیرامیٹر Storage[4] سے کم ہو
0x1f135823Storage[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) کو سپورٹ کرتا ہے وہ یہ ہیں:

ہم نیچے والے چار میتھڈز کو نظر انداز کر سکتے ہیں کیونکہ ہم کبھی ان تک نہیں پہنچ پائیں گے۔ ان کے سگنیچرز ایسے ہیں کہ ہمارا اصل کنٹریکٹ خود ہی ان کو سنبھال لیتا ہے (آپ اوپر تفصیلات دیکھنے کے لیے سگنیچرز پر کلک کر سکتے ہیں)، اس لیے یہ لازمی طور پر اوور رائیڈ کیے گئے میتھڈز (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 >=64
3 if _param1 and _param2 > -1 / _param1:
4 revert with 0, 17
5 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 = 0
3 s = 0
4 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 continue
11 ...
12 s = sha3(mem[_66 + 32 len mem[_66]])
13 continue
14 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 wei
3 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 = 0
4 s = 0
5 while idx < _param3.length:
6 if idx >= mem[96]:
7 revert with 0, 50
8 _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 continue
13 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 wei
20 gas 30000 wei
21 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 wei
26 gas gas_remaining wei
27 ...
28 log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)
سب دکھائیں

بنیادی فرق یہ ہے کہ پہلا پیرامیٹر، یعنی نکالنے والی ونڈو، وہاں موجود نہیں ہے۔ اس کے بجائے، ان تمام ونڈوز پر ایک لوپ (loop) ہے جنہیں کلیم کیا جا سکتا ہے۔

1 idx = 0
2 s = 0
3 while idx < currentWindow:
4 ...
5 if stor5[mem[0]]:
6 if idx == -1:
7 revert with 0, 17
8 idx = idx + 1
9 s = s
10 continue
11 ...
12 stor5[idx][addr(_param1)] = 1
13 if idx >= unknown81e580d3.length:
14 revert with 0, 50
15 mem[0] = 4
16 if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:
17 revert with 0, 17
18 if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):
19 revert with 0, 17
20 if idx == -1:
21 revert with 0, 17
22 idx = idx + 1
23 s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)
24 continue
سب دکھائیں

لہذا یہ ایک claim ویرینٹ (variant) کی طرح لگتا ہے جو تمام ونڈوز کو کلیم کرتا ہے۔

نتیجہ

اب تک آپ کو یہ جان لینا چاہیے کہ ان معاہدوں کو کیسے سمجھا جائے جن کا سورس کوڈ دستیاب نہیں ہے، جس کے لیے یا تو opcodes کا استعمال کیا جاتا ہے یا (جب یہ کام کرے) ڈی کمپائلر کا۔ جیسا کہ اس مضمون کی طوالت سے ظاہر ہے، کسی معاہدے کی ریورس انجینئرنگ کرنا کوئی معمولی بات نہیں ہے، لیکن ایک ایسے سسٹم میں جہاں سیکیورٹی انتہائی ضروری ہو، یہ تصدیق کرنے کے قابل ہونا ایک اہم مہارت ہے کہ معاہدے اپنے وعدے کے مطابق کام کرتے ہیں۔

میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔

صفحہ کی آخری اپ ڈیٹ: 22 اگست، 2025

کیا یہ ٹیوٹوریل مددگار تھا؟