মূল কন্টেন্টে যান

একটি কন্ট্রাক্ট রিভার্স ইঞ্জিনিয়ারিং করা

evm
অপকোড
উন্নত
ওরি পোমেরান্টজ
30 ডিসেম্বর, 2021
30 মিনিট পড়ার সময়
পৃষ্ঠা সম্পাদনা করুন (opens in a new tab)

ভূমিকা

ব্লকচেইনে কোনো গোপনীয়তা নেই, যা কিছু ঘটে তা সামঞ্জস্যপূর্ণ, যাচাইযোগ্য এবং সর্বজনীনভাবে উপলব্ধ। আদর্শভাবে, কন্ট্রাক্টগুলোর সোর্স কোড Etherscan-এ প্রকাশিত এবং যাচাই করা উচিত (opens in a new tab)। তবে, সবসময় এমনটা হয় না (opens in a new tab)। এই নিবন্ধে আপনি শিখবেন কীভাবে সোর্স কোড ছাড়া একটি কন্ট্রাক্ট দেখে কন্ট্রাক্টগুলো রিভার্স ইঞ্জিনিয়ার করতে হয়, 0x2510c039cc3b061d79e564b38836da87e31b342f (opens in a new tab)

রিভার্স কম্পাইলার রয়েছে, তবে সেগুলো সবসময় ব্যবহারযোগ্য ফলাফল (opens in a new tab) তৈরি করে না। এই নিবন্ধে আপনি শিখবেন কীভাবে ম্যানুয়ালি রিভার্স ইঞ্জিনিয়ার করতে হয় এবং অপকোডগুলো (opens in a new tab) থেকে একটি কন্ট্রাক্ট বুঝতে হয়, পাশাপাশি একটি ডিকম্পাইলারের ফলাফল কীভাবে ব্যাখ্যা করতে হয়।

এই নিবন্ধটি বোঝার জন্য আপনার আগে থেকেই EVM-এর বেসিক জানা উচিত এবং EVM অ্যাসেম্বলারের সাথে অন্তত কিছুটা পরিচিত হওয়া উচিত। আপনি এই বিষয়গুলো সম্পর্কে এখানে পড়তে পারেন (opens in a new tab)

এক্সিকিউটেবল কোড প্রস্তুত করুন

আপনি কন্ট্রাক্টের জন্য Etherscan-এ গিয়ে, Contract ট্যাবে ক্লিক করে এবং তারপর Switch to Opcodes View-এ ক্লিক করে অপকোডগুলো পেতে পারেন। আপনি এমন একটি ভিউ পাবেন যেখানে প্রতি লাইনে একটি করে অপকোড থাকে।

Opcode View from Etherscan

তবে, জাম্প (jumps) বুঝতে পারার জন্য, আপনাকে জানতে হবে কোডের কোথায় প্রতিটি অপকোড অবস্থিত। এটি করার একটি উপায় হলো একটি Google Spreadsheet খোলা এবং কলাম C-তে অপকোডগুলো পেস্ট করা। আপনি আগে থেকে প্রস্তুত করা এই স্প্রেডশিটের একটি কপি তৈরি করে নিচের ধাপগুলো এড়িয়ে যেতে পারেন (opens in a new tab)

পরবর্তী ধাপ হলো কোডের সঠিক অবস্থানগুলো বের করা যাতে আমরা জাম্পগুলো বুঝতে পারি। আমরা কলাম B-তে অপকোডের আকার এবং কলাম A-তে অবস্থান (হেক্সাডেসিম্যালে) রাখব। B1 সেলে এই ফাংশনটি টাইপ করুন এবং তারপর কোডের শেষ পর্যন্ত কলাম B-এর বাকি অংশের জন্য এটি কপি এবং পেস্ট করুন। এটি করার পর আপনি কলাম B লুকিয়ে রাখতে পারেন।

=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)

প্রথমে এই ফাংশনটি অপকোডের জন্য এক বাইট যোগ করে এবং তারপর PUSH খোঁজে। পুশ (Push) অপকোডগুলো বিশেষ কারণ পুশ করা ভ্যালুর জন্য এগুলোতে অতিরিক্ত বাইট থাকতে হয়। যদি অপকোডটি একটি PUSH হয়, তবে আমরা বাইটের সংখ্যা বের করি এবং সেটি যোগ করি।

A1-এ প্রথম অফসেট, শূন্য রাখুন। তারপর, A2-এ এই ফাংশনটি রাখুন এবং আবার কলাম A-এর বাকি অংশের জন্য এটি কপি এবং পেস্ট করুন:

=dec2hex(hex2dec(A1)+B1)

আমাদের এই ফাংশনটি প্রয়োজন যাতে এটি আমাদের হেক্সাডেসিম্যাল ভ্যালু দেয়, কারণ জাম্পের আগে পুশ করা ভ্যালুগুলো (JUMP এবং JUMPI) আমাদের হেক্সাডেসিম্যালে দেওয়া হয়।

এন্ট্রি পয়েন্ট (0x00)

কন্ট্রাক্টগুলো সর্বদা প্রথম বাইট থেকে এক্সিকিউট করা হয়। এটি কোডের প্রাথমিক অংশ:

অফসেটঅপকোডস্ট্যাক (অপকোডের পরে)
0PUSH1 0x800x80
2PUSH1 0x400x40, 0x80
4MSTOREখালি
5PUSH1 0x040x04
7CALLDATASIZECALLDATASIZE 0x04
8LTCALLDATASIZE<4
9PUSH2 0x005e0x5E CALLDATASIZE<4
CJUMPIখালি

এই কোডটি দুটি কাজ করে:

  1. মেমরি লোকেশন 0x40-0x5F-এ 32 বাইট মান হিসেবে 0x80 লেখে (0x80 সংরক্ষিত হয় 0x5F-এ, এবং 0x40-0x5E এর সবগুলো শূন্য)।
  2. কল ডেটার আকার পড়ে। সাধারণত একটি ইথেরিয়াম কন্ট্রাক্টের কল ডেটা ABI (অ্যাপ্লিকেশন বাইনারি ইন্টারফেস) (opens in a new tab) অনুসরণ করে, যার জন্য ফাংশন সিলেক্টরের জন্য ন্যূনতম চার বাইট প্রয়োজন হয়। যদি কল ডেটার আকার চারের কম হয়, তবে 0x5E-তে জাম্প করে।

Flowchart for this portion

0x5E-তে হ্যান্ডলার (নন-ABI কল ডেটার জন্য)

অফসেটঅপকোড
5EJUMPDEST
5FCALLDATASIZE
60PUSH2 0x007c
63JUMPI

এই স্নিপেটটি একটি JUMPDEST দিয়ে শুরু হয়। EVM (ইথেরিয়াম ভার্চুয়াল মেশিন) প্রোগ্রামগুলো একটি এক্সেপশন থ্রো করে যদি আপনি এমন কোনো অপকোডে জাম্প করেন যা JUMPDEST নয়। এরপর এটি CALLDATASIZE দেখে, এবং যদি এটি "সত্য" (অর্থাৎ, শূন্য নয়) হয় তবে 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, যা পরবর্তীতে প্রাসঙ্গিক হবে।

The call data is empty

এরপর, 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

অবশেষে, স্ট্যাকটি পরিষ্কার করুন (যা প্রয়োজনীয় নয়) এবং ট্রানজ্যাকশনের সফল সমাপ্তির সংকেত দিন।

সব মিলিয়ে, এখানে প্রাথমিক কোডের জন্য একটি ফ্লোচার্ট দেওয়া হলো।

Entry point flowchart

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]-ঠিকানা-হিসেবে 0x9D 0x00

এই অপকোডগুলো Storage[3] থেকে পড়া মানটিকে 160 বিটে কেটে ছোট করে, যা একটি ইথেরিয়াম ঠিকানার দৈর্ঘ্য।

অফসেটঅপকোডস্ট্যাক
9BSWAP10x9D Storage[3]-ঠিকানা-হিসেবে 0x00
9CJUMPStorage[3]-ঠিকানা-হিসেবে 0x00

এই জাম্পটি অপ্রয়োজনীয়, কারণ আমরা পরবর্তী অপকোডে যাচ্ছি। এই কোডটি যতটা গ্যাস-সাশ্রয়ী হতে পারত, ততটা নয়।

অফসেটঅপকোডস্ট্যাক
9DJUMPDESTStorage[3]-ঠিকানা-হিসেবে 0x00
9ESWAP10x00 Storage[3]-ঠিকানা-হিসেবে
9FPOPStorage[3]-ঠিকানা-হিসেবে
A0PUSH1 0x400x40 Storage[3]-ঠিকানা-হিসেবে
A2MLOADMem[0x40] Storage[3]-ঠিকানা-হিসেবে

কোডের একেবারে শুরুতে আমরা Mem[0x40]-কে 0x80 তে সেট করি। যদি আমরা পরে 0x40 খুঁজি, তবে দেখতে পাব যে আমরা এটি পরিবর্তন করিনি - তাই আমরা ধরে নিতে পারি এটি 0x80।

অফসেটঅপকোডস্ট্যাক
A3CALLDATASIZECALLDATASIZE 0x80 Storage[3]-ঠিকানা-হিসেবে
A4PUSH1 0x000x00 CALLDATASIZE 0x80 Storage[3]-ঠিকানা-হিসেবে
A6DUP30x80 0x00 CALLDATASIZE 0x80 Storage[3]-ঠিকানা-হিসেবে
A7CALLDATACOPY0x80 Storage[3]-ঠিকানা-হিসেবে

0x80 থেকে শুরু করে সমস্ত কল ডেটা মেমোরিতে কপি করুন।

অফসেটঅপকোডস্ট্যাক
A8PUSH1 0x000x00 0x80 Storage[3]-ঠিকানা-হিসেবে
AADUP10x00 0x00 0x80 Storage[3]-ঠিকানা-হিসেবে
ABCALLDATASIZECALLDATASIZE 0x00 0x00 0x80 Storage[3]-ঠিকানা-হিসেবে
ACDUP40x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-ঠিকানা-হিসেবে
ADDUP6Storage[3]-ঠিকানা-হিসেবে 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-ঠিকানা-হিসেবে
AEGASGAS Storage[3]-ঠিকানা-হিসেবে 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-ঠিকানা-হিসেবে
AFDELEGATE_CALL

এখন বিষয়গুলো অনেক বেশি পরিষ্কার। এই কন্ট্রাক্টটি একটি প্রক্সি (opens in a new tab) হিসেবে কাজ করতে পারে, যা আসল কাজটি করার জন্য Storage[3]-এ থাকা ঠিকানাকে কল করে। DELEGATE_CALL একটি আলাদা কন্ট্রাক্টকে কল করে, কিন্তু একই স্টোরেজে থাকে। এর মানে হলো ডেলিগেট করা কন্ট্রাক্টটি, যার জন্য আমরা একটি প্রক্সি, একই স্টোরেজ স্পেস অ্যাক্সেস করে। কলের জন্য প্যারামিটারগুলো হলো:

  • গ্যাস: অবশিষ্ট সমস্ত গ্যাস
  • কল করা ঠিকানা: Storage[3]-ঠিকানা-হিসেবে
  • কল ডেটা: 0x80 থেকে শুরু হওয়া CALLDATASIZE বাইট, যেখানে আমরা আসল কল ডেটা রেখেছিলাম
  • রিটার্ন ডেটা: কোনোটিই নয় (0x00 - 0x00) আমরা অন্য উপায়ে রিটার্ন ডেটা পাব (নিচে দেখুন)
অফসেটঅপকোডস্ট্যাক
B0RETURNDATASIZERETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
B1DUP1RETURNDATASIZE RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
B2PUSH1 0x000x00 RETURNDATASIZE RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
B4DUP50x80 0x00 RETURNDATASIZE RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
B5RETURNDATACOPYRETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে

এখানে আমরা 0x80 থেকে শুরু হওয়া মেমোরি বাফারে সমস্ত রিটার্ন ডেটা কপি করি।

অফসেটঅপকোডস্ট্যাক
B6DUP2(((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
B7DUP1(((কলের সফলতা/ব্যর্থতা))) (((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
B8ISZERO(((কলটি কি ব্যর্থ হয়েছে))) (((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
B9PUSH2 0x00c00xC0 (((কলটি কি ব্যর্থ হয়েছে))) (((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
BCJUMPI(((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
BDDUP2RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
BEDUP50x80 RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
BFRETURN

সুতরাং কলের পরে আমরা রিটার্ন ডেটা 0x80 - 0x80+RETURNDATASIZE বাফারে কপি করি, এবং যদি কলটি সফল হয় তবে আমরা ঠিক সেই বাফারটি দিয়ে RETURN করি।

DELEGATECALL ব্যর্থ হয়েছে

যদি আমরা এখানে, 0xC0-তে আসি, এর মানে হলো আমরা যে কন্ট্রাক্টটিকে কল করেছিলাম তা রিভার্ট হয়েছে। যেহেতু আমরা সেই কন্ট্রাক্টের জন্য কেবল একটি প্রক্সি, তাই আমরা একই ডেটা ফেরত দিতে চাই এবং রিভার্টও করতে চাই।

অফসেটঅপকোডস্ট্যাক
C0JUMPDEST(((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
C1DUP2RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
C2DUP50x80 RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) RETURNDATASIZE (((কলের সফলতা/ব্যর্থতা))) 0x80 Storage[3]-ঠিকানা-হিসেবে
C3REVERT

তাই আমরা আগে RETURN-এর জন্য যে বাফারটি ব্যবহার করেছিলাম, সেই একই বাফার দিয়ে REVERT করি: 0x80 - 0x80+RETURNDATASIZE

Call to proxy flowchart

ABI কল

যদি কল ডেটার সাইজ 4 বাইট বা তার বেশি হয়, তবে এটি একটি বৈধ ABI কল হতে পারে।

অফসেটঅপকোডস্ট্যাক
DPUSH1 0x000x00
FCALLDATALOAD(((কল ডেটার প্রথম শব্দ (256 বিট))))
10PUSH1 0xe00xE0 (((কল ডেটার প্রথম শব্দ (256 বিট))))
12SHR(((কল ডেটার প্রথম 32 বিট (4 বাইট))))

Etherscan আমাদের বলে যে 1C একটি অজানা অপকোড, কারণ Etherscan এই বৈশিষ্ট্যটি লেখার পরে এটি যোগ করা হয়েছিল (opens in a new tab) এবং তারা এটি আপডেট করেনি। একটি আপ-টু-ডেট অপকোড টেবিল (opens in a new tab) আমাদের দেখায় যে এটি শিফট রাইট

অফসেটঅপকোডস্ট্যাক
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 বাইট))))

মেথড স্বাক্ষর মেলানোর পরীক্ষাগুলোকে এভাবে দুই ভাগে ভাগ করলে গড়ে অর্ধেক পরীক্ষা বেঁচে যায়। এর ঠিক পরের কোড এবং 0x43-এর কোড একই প্যাটার্ন অনুসরণ করে: কল ডেটার প্রথম 32 বিট DUP1 করে, PUSH4 (((method signature> করে, সমতা পরীক্ষা করার জন্য EQ চালায় এবং তারপর মেথড স্বাক্ষর মিলে গেলে JUMPI করে। এখানে মেথড স্বাক্ষর, তাদের ঠিকানা এবং যদি জানা থাকে তবে সংশ্লিষ্ট মেথড সংজ্ঞা (opens in a new tab) দেওয়া হলো:

মেথডমেথড স্বাক্ষরঅফসেট যেখানে জাম্প করতে হবে
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 calls flowchart

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) একটি মেমরি পয়েন্টার গ্রহণ করে, এবং কন্ট্রাক্টটিকে 0x80 - X বাফারের সাথে RETURN করতে বাধ্য করে।

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

0x80-0x9F-এ Y লিখুন।

অফসেটঅপকোডস্ট্যাক
E1PUSH1 0x200x20 0x80 0xDA
E3ADD0xA0 0xDA

এবং বাকি অংশটি ইতিমধ্যেই উপরে ব্যাখ্যা করা হয়েছে। তাই 0xDA-তে জাম্প করলে স্ট্যাকের শীর্ষ (Y) 0x80-0x9F-এ লেখা হয় এবং সেই মানটি রিটার্ন করে। currentWindow()-এর ক্ষেত্রে, এটি Storage[1] রিটার্ন করে।

merkleRoot()

0xED-0xF8 অফসেটের কোডটি splitter()-এ 0x103-0x10E-এ আমরা যা দেখেছি তার হুবহু অনুরূপ (JUMPI গন্তব্য ছাড়া), তাই আমরা জানি যে merkleRoot()-ও payable নয়।

অফসেটঅপকোডস্ট্যাক
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

যদি এটি কল ডেটা না পায়, তবে কোনো রিটার্ন ডেটা ছাড়াই ট্রানজ্যাকশনটি রিভার্ট করা হয়।

চলুন দেখি যদি ফাংশনটি তার প্রয়োজনীয় কল ডেটা পায় তবে কী ঘটে।

অফসেটঅপকোডস্ট্যাক
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* ফেরত দেয়।

মেথড সারাংশ

আপনি কি মনে করেন যে এই পর্যায়ে আপনি কন্ট্রাক্টটি বুঝতে পেরেছেন? আমি পারিনি। এখনও পর্যন্ত আমাদের কাছে এই মেথডগুলো আছে:

মেথডঅর্থ
হস্তান্তরকলের মাধ্যমে প্রদান করা মান গ্রহণ করে এবং সেই পরিমাণ অনুযায়ী 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) তখন আমরা সেই ট্রানজ্যাকশনটিও দেখতে পারি যা এটি তৈরি করেছে।

Click the create transaction

যদি আমরা সেই ট্রানজ্যাকশনে ক্লিক করি এবং তারপর স্টেট ট্যাবে যাই, তাহলে আমরা প্যারামিটারগুলোর প্রাথমিক মান দেখতে পারি। বিশেষ করে, আমরা দেখতে পারি যে Storage[3]-এ 0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761 (opens in a new tab) রয়েছে। সেই কন্ট্রাক্টে অবশ্যই অনুপস্থিত কার্যকারিতা থাকতে হবে। আমরা যে কন্ট্রাক্টটি তদন্ত করছি তার জন্য ব্যবহৃত একই টুলগুলো ব্যবহার করে আমরা এটি বুঝতে পারি।

প্রক্সি চুক্তি

উপরে মূল কন্ট্রাক্টের জন্য আমরা যে কৌশলগুলো ব্যবহার করেছি, সেগুলো ব্যবহার করে আমরা দেখতে পারি যে কন্ট্রাক্টটি রিভার্ট করে যদি:

  • কলের সাথে কোনো ETH যুক্ত থাকে (0x05-0x0F)
  • কল ডেটার আকার 4-এর কম হয় (0x10-0x19 এবং 0xBE-0xC2)

এবং এটি যে মেথডগুলো সমর্থন করে সেগুলো হলো:

মেথডমেথড স্বাক্ষরলাফ দেওয়ার অফসেট
scaleAmountByPercentage(uint256,uint256) (opens in a new tab)0x8ffb5c970x0135
isClaimed(uint256,address) (opens in a new tab)0xd2ef07950x0151
claim(uint256,address,uint256,bytes32[]) (opens in a new tab)0x2e7ba6ef0x00F4
incrementWindow() (opens in a new tab)0x338b1d310x0110
???0x3f26479e0x0118
???0x1e7df9d30x00C3
currentWindow() (opens in a new tab)0xba0bafb40x0148
merkleRoot() (opens in a new tab)0x2eb4a7ab0x0107
???0x81e580d30x0122
???0x1f1358230x00D8

আমরা নিচের 4টি মেথড উপেক্ষা করতে পারি কারণ আমরা কখনোই সেগুলোতে পৌঁছাব না। তাদের স্বাক্ষরগুলো এমন যে আমাদের মূল কন্ট্রাক্ট নিজেই সেগুলোর যত্ন নেয় (আপনি উপরের বিস্তারিত দেখতে স্বাক্ষরগুলোতে ক্লিক করতে পারেন), তাই সেগুলো অবশ্যই ওভাররাইড করা মেথড (opens in a new tab) হতে হবে।

বাকি মেথডগুলোর মধ্যে একটি হলো claim(<params>), এবং আরেকটি হলো isClaimed(<params>), তাই এটিকে একটি এয়ারড্রপ কন্ট্রাক্ট বলে মনে হচ্ছে। বাকিগুলো অপকোড ধরে ধরে দেখার পরিবর্তে, আমরা ডিকম্পাইলার ব্যবহার করে দেখতে পারি (opens in a new tab), যা এই কন্ট্রাক্টের 3টি ফাংশনের জন্য ব্যবহারযোগ্য ফলাফল তৈরি করে। অন্যগুলোর রিভার্স ইঞ্জিনিয়ারিং পাঠকের জন্য অনুশীলন হিসেবে রেখে দেওয়া হলো।

scaleAmountByPercentage

এই ফাংশনের জন্য ডিকম্পাইলার আমাদের যা দেয় তা হলো:

def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable:
  require calldata.size - 4 >=64
  if _param1 and _param2 > -1 / _param1:
      revert with 0, 17
  return (_param1 * _param2 / 100 * 10^6)

প্রথম require পরীক্ষা করে যে কল ডেটাতে, ফাংশন স্বাক্ষরের 4 বাইট ছাড়াও, অন্তত 64 বাইট আছে, যা দুটি প্যারামিটারের জন্য যথেষ্ট। যদি না থাকে তবে স্পষ্টতই কিছু ভুল আছে।

if স্টেটমেন্টটি পরীক্ষা করে বলে মনে হয় যে _param1 শূন্য নয়, এবং _param1 * _param2 নেতিবাচক নয়। এটি সম্ভবত র‍্যাপ অ্যারাউন্ডের (wrap around) ঘটনা রোধ করার জন্য।

অবশেষে, ফাংশনটি একটি স্কেল করা মান প্রদান করে।

claim

ডিকম্পাইলার যে কোড তৈরি করে তা জটিল, এবং এর সবটাই আমাদের জন্য প্রাসঙ্গিক নয়। আমি এর কিছু অংশ এড়িয়ে গিয়ে সেই লাইনগুলোতে ফোকাস করতে যাচ্ছি যেগুলো আমার মতে দরকারী তথ্য প্রদান করে

def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable:
  ...
  require _param2 == addr(_param2)
  ...
  if currentWindow <= _param1:
      revert with 0, 'cannot claim for a future window'

আমরা এখানে 2টি গুরুত্বপূর্ণ জিনিস দেখতে পাচ্ছি:

  • _param2, যদিও এটি একটি uint256 হিসেবে ঘোষণা করা হয়েছে, আসলে এটি একটি ঠিকানা
  • _param1 হলো দাবি করা উইন্ডো, যা currentWindow বা তার আগের হতে হবে।
  ...
  if stor5[_claimWindow][addr(_claimFor)]:
      revert with 0, 'Account already claimed the given window'

সুতরাং এখন আমরা জানি যে Storage[5] হলো উইন্ডো এবং ঠিকানাগুলোর একটি অ্যারে, এবং ঠিকানাটি সেই উইন্ডোর জন্য পুরস্কার দাবি করেছে কিনা।

আমরা জানি যে unknown2eb4a7ab আসলে merkleRoot() ফাংশন, তাই এই কোডটি দেখে মনে হচ্ছে এটি একটি মার্কেল প্রমাণ (opens in a new tab) যাচাই করছে। এর মানে হলো _param4 একটি মার্কেল প্রমাণ।

  call addr(_param2) with:
     value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei
       gas 30000 wei

এভাবেই একটি কন্ট্রাক্ট তার নিজস্ব ETH অন্য ঠিকানায় (কন্ট্রাক্ট বা বাহ্যিক মালিকানাধীন) হস্তান্তর করে। এটি এমন একটি মান দিয়ে কল করে যা হস্তান্তর করার পরিমাণ। তাই এটিকে ETH-এর একটি এয়ারড্রপ বলে মনে হচ্ছে।

  if not return_data.size:
      if not ext_call.success:
          require ext_code.size(stor2)
          call stor2.deposit() with:
             value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei

নিচের 2টি লাইন আমাদের বলে যে Storage[2] হলো আরেকটি কন্ট্রাক্ট যাকে আমরা কল করি। যদি আমরা কনস্ট্রাক্টর ট্রানজ্যাকশনটি দেখি (opens in a new tab) তবে আমরা দেখতে পাই যে এই কন্ট্রাক্টটি হলো 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (opens in a new tab), একটি র‍্যাপড ইথার (weth) কন্ট্রাক্ট যার সোর্স কোড Etherscan-এ আপলোড করা হয়েছে (opens in a new tab)

তাই মনে হচ্ছে কন্ট্রাক্টগুলো _param2-এ ETH পাঠানোর চেষ্টা করে। যদি এটি করতে পারে, তবে দারুণ। যদি না পারে, তবে এটি WETH (opens in a new tab) পাঠানোর চেষ্টা করে। যদি _param2 একটি বাহ্যিক মালিকানাধীন অ্যাকাউন্ট (EOA) হয় তবে এটি সর্বদা ETH গ্রহণ করতে পারে, কিন্তু কন্ট্রাক্টগুলো ETH গ্রহণ করতে অস্বীকার করতে পারে। তবে, WETH হলো ERC-20 এবং কন্ট্রাক্টগুলো এটি গ্রহণ করতে অস্বীকার করতে পারে না।

  ...
  log 0xdbd5389f: addr(_param2), unknown81e580d3[_param1] * _param3 / 100 * 10^6, bool(ext_call.success)

ফাংশনের শেষে আমরা দেখতে পাই যে একটি লগ এন্ট্রি তৈরি হচ্ছে। তৈরি করা লগ এন্ট্রিগুলো দেখুন (opens in a new tab) এবং 0xdbd5... দিয়ে শুরু হওয়া টপিকটি ফিল্টার করুন। যদি আমরা এমন একটি এন্ট্রি তৈরি করা ট্রানজ্যাকশনগুলোর একটিতে ক্লিক করি (opens in a new tab) তবে আমরা দেখতে পাই যে এটি সত্যিই একটি দাবির মতো দেখাচ্ছে - অ্যাকাউন্টটি আমরা যে কন্ট্রাক্টটি রিভার্স ইঞ্জিনিয়ারিং করছি তাতে একটি বার্তা পাঠিয়েছে, এবং বিনিময়ে ETH পেয়েছে।

A claim transaction

1e7df9d3

এই ফাংশনটি উপরের claim-এর মতোই। এটিও একটি মার্কেল প্রমাণ পরীক্ষা করে, প্রথমটিতে ETH হস্তান্তর করার চেষ্টা করে এবং একই ধরনের লগ এন্ট্রি তৈরি করে।

প্রধান পার্থক্য হলো প্রথম প্যারামিটার, অর্থাৎ তোলার জন্য উইন্ডোটি, সেখানে নেই। এর পরিবর্তে, দাবি করা যেতে পারে এমন সমস্ত উইন্ডোর উপর একটি লুপ রয়েছে।

তাই এটিকে claim-এর একটি ভ্যারিয়েন্ট বলে মনে হচ্ছে যা সমস্ত উইন্ডো দাবি করে।

উপসংহার

এতক্ষণে আপনার জেনে যাওয়া উচিত কীভাবে অপকোড অথবা (যখন এটি কাজ করে) ডিকম্পাইলার ব্যবহার করে এমন কন্ট্রাক্টগুলো বুঝতে হয় যেগুলোর সোর্স কোড পাওয়া যায় না। এই নিবন্ধের দৈর্ঘ্য থেকেই স্পষ্ট যে, কোনো কন্ট্রাক্ট রিভার্স ইঞ্জিনিয়ারিং করা খুব সহজ কাজ নয়। তবে, যে সিস্টেমে নিরাপত্তা অপরিহার্য, সেখানে কন্ট্রাক্টগুলো প্রতিশ্রুতি অনুযায়ী কাজ করছে কিনা তা যাচাই করতে পারা একটি অত্যন্ত গুরুত্বপূর্ণ দক্ষতা।

আমার আরও কাজের জন্য এখানে দেখুন (opens in a new tab)

পেজ সর্বশেষ আপডেট করা হয়েছে: 3 এপ্রিল, 2026