---
title: "Реверс-інжиніринг контракту"
description: "Як зрозуміти контракт, коли у вас немає вихідного коду"
author: "Орі Померанц"
lang: uk
tags: ["evm", "опкоди"]
skill: advanced
breadcrumb: "Реверс-інжиніринг"
published: 2021-12-30
---
## Вступ {#introduction}

_У блокчейні немає секретів_, усе, що відбувається, є послідовним, таким, що можна перевірити, і загальнодоступним. В ідеалі [контракти повинні мати свій вихідний код опублікованим і верифікованим на Etherscan](https://etherscan.io/address/0xb8901acb165ed027e32754e0ffe830802919727f#code). Однак [це не завжди так](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f#code). У цій статті ви дізнаєтеся, як здійснювати реверс-інжиніринг контрактів, розглядаючи контракт без вихідного коду, [`0x2510c039cc3b061d79e564b38836da87e31b342f`](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f).

Існують декомпілятори, але вони не завжди дають [придатні для використання результати](https://etherscan.io/bytecode-decompiler?a=0x2510c039cc3b061d79e564b38836da87e31b342f). У цій статті ви дізнаєтеся, як вручну здійснювати реверс-інжиніринг і розуміти контракт за [опкодами](https://github.com/wolflo/evm-opcodes), а також як інтерпретувати результати декомпілятора.

Щоб зрозуміти цю статтю, ви вже повинні знати основи EVM і бути принаймні трохи знайомими з асемблером EVM. [Ви можете прочитати про ці теми тут](https://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e).

## Підготовка виконуваного коду {#prepare-the-executable-code}

Ви можете отримати опкоди, перейшовши на Etherscan для контракту, натиснувши вкладку **Contract** (Контракт), а потім **Switch to Opcodes View** (Перейти до перегляду опкодів). Ви отримаєте вигляд, де на кожному рядку розміщено один опкод.

![Opcode View from Etherscan](opcode-view.png)

Однак, щоб зрозуміти переходи (jumps), вам потрібно знати, де саме в коді знаходиться кожен опкод. Один зі способів зробити це — відкрити Google Таблиці та вставити опкоди у стовпець C. [Ви можете пропустити наступні кроки, зробивши копію цієї вже підготовленої таблиці](https://docs.google.com/spreadsheets/d/1tKmTJiNjUwHbW64wCKOSJxHjmh0bAUapt6btUYE7kDA/edit?usp=sharing).

Наступний крок — отримати правильні розташування коду, щоб ми могли розуміти переходи. Ми помістимо розмір опкоду у стовпець B, а розташування (у шістнадцятковому форматі) — у стовпець A. Введіть цю функцію в клітинку `B1`, а потім скопіюйте та вставте її для решти стовпця B до кінця коду. Після цього ви можете приховати стовпець B.

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

Спочатку ця функція додає один байт для самого опкоду, а потім шукає `PUSH`. Опкоди PUSH є особливими, оскільки вони потребують додаткових байтів для значення, що додається (push). Якщо опкод — це `PUSH`, ми витягуємо кількість байтів і додаємо її.

У `A1` введіть перше зміщення — нуль. Потім у `A2` введіть цю функцію і знову скопіюйте та вставте її для решти стовпця A:

```
=dec2hex(hex2dec(A1)+B1)
```

Нам потрібно, щоб ця функція повертала шістнадцяткове значення, оскільки значення, які додаються перед переходами (`JUMP` та `JUMPI`), надаються нам у шістнадцятковому форматі.

## Точка входу (0x00) {#the-entry-point-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        | Порожньо                    |

Цей код виконує дві дії:

1. Записує 0x80 як 32-байтове значення в комірки пам'яті 0x40-0x5F (0x80 зберігається в 0x5F, а 0x40-0x5E заповнені нулями).
2. Зчитує розмір даних виклику. Зазвичай дані виклику для контракту Ethereum відповідають [ABI (двійковому інтерфейсу застосунку)](https://docs.soliditylang.org/en/v0.8.10/abi-spec.html), який вимагає щонайменше чотири байти для селектора функції. Якщо розмір даних виклику менший за чотири, відбувається перехід до 0x5E.

![Flowchart for this portion](flowchart-entry.png)

### Обробник за адресою 0x5E (для даних виклику не у форматі ABI) {#the-handler-at-0x5e-for-non-abi-call-data}

| Зміщення | Опкод       |
| -----: | ------------ |
|     5E | JUMPDEST     |
|     5F | CALLDATASIZE |
|     60 | PUSH2 0x007c |
|     63 | JUMPI        |

Цей фрагмент починається з `JUMPDEST`. Програми EVM (віртуальної машини Ethereum) видають виняток, якщо ви переходите до опкоду, який не є `JUMPDEST`. Потім він перевіряє CALLDATASIZE, і якщо значення є «істинним» (тобто не дорівнює нулю), переходить до 0x7C. Ми розглянемо це нижче.

| Зміщення | Опкод     | Стек (після опкоду)                                                       |
| -----: | ---------- | -------------------------------------------------------------------------- |
|     64 | CALLVALUE  | [Wei](/glossary/#wei), надані викликом. У 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`. Фактично, [найперша транзакція, яку отримав контракт](https://etherscan.io/tx/0xeec75287a583c36bcc7ca87685ab41603494516a0f5986d18de96c8e630762e7), є переказом.

Якщо ми подивимося на цю транзакцію і натиснемо **Click to see More** (Натисніть, щоб побачити більше), ми побачимо, що дані виклику, які називаються вхідними даними (input data), дійсно порожні (`0x`). Зверніть також увагу, що значення становить 1.559 ETH, це буде важливо пізніше.

![The call data is empty](calldata-empty.png)

Далі перейдіть на вкладку **State** (Стан) і розгорніть контракт, який ми досліджуємо (0x2510...). Ви можете побачити, що `Storage[6]` дійсно змінилося під час транзакції, і якщо ви зміните Hex на **Number** (Число), то побачите, що воно стало 1,559,000,000,000,000,000 — це переказане значення у wei (я додав коми для ясності), що відповідає наступному значенню контракту.

![Зміна в Storage[6]](storage6.png)

Якщо ми подивимося на зміни стану, викликані [іншими транзакціями `Transfer` з того ж періоду](https://etherscan.io/tx/0xf708d306de39c422472f43cb975d97b66fd5d6a6863db627067167cbf93d84d1#statechange), ми побачимо, що `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 на момент написання статті становить менше двохсот мільйонів](https://etherscan.io/stat/supply).

| Зміщення | Опкод   | Стек                                     |
| -----: | -------- | ----------------------------------------- |
|    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   |

Нарешті, очищаємо стек (що не є обов'язковим) і сигналізуємо про успішне завершення транзакції.

Підсумовуючи все це, ось блок-схема початкового коду.

![Entry point flowchart](flowchart-entry.png)

## Обробник за зміщенням 0x7C {#the-handler-at-0x7c}

Я навмисно не вказав у заголовку, що робить цей обробник. Мета полягає не в тому, щоб навчити вас, як працює цей конкретний контракт, а в тому, як здійснювати зворотну розробку контрактів. Ви дізнаєтеся, що він робить, так само, як і я — слідуючи за кодом.

Ми потрапляємо сюди з кількох місць:

- Якщо є дані виклику розміром 1, 2 або 3 байти (зі зміщення 0x63)
- Якщо підпис методу невідомий (зі зміщень 0x42 та 0x5D)

| Зміщення | Опкод        | Стек                 |
| -------: | ------------ | -------------------- |
|       7C | JUMPDEST     |
|       7D | PUSH1 0x00   | 0x00                 |
|       7F | PUSH2 0x009d | 0x9D 0x00            |
|       82 | PUSH1 0x03   | 0x03 0x9D 0x00       |
|       84 | SLOAD        | Storage[3] 0x9D 0x00 |

Це ще одна комірка сховища, яку я не зміг знайти в жодній транзакції, тому важче зрозуміти, що вона означає. Наведений нижче код зробить це зрозумілішим.

| Зміщення | Опкод                                             | Стек                            |
| -------: | ------------------------------------------------- | ------------------------------- |
|       85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 |
|       9A | AND                                               | Storage[3]-як-адреса 0x9D 0x00  |

Ці опкоди усікають значення, яке ми зчитуємо зі Storage[3], до 160 біт — довжини адреси Ethereum.

| Зміщення | Опкод  | Стек                           |
| -------: | ------ | ------------------------------ |
|       9B | SWAP1  | 0x9D Storage[3]-як-адреса 0x00 |
|       9C | JUMP   | Storage[3]-як-адреса 0x00      |

Цей перехід є зайвим, оскільки ми переходимо до наступного опкоду. Цей код далеко не такий ефективний з точки зору використання газу, яким міг би бути.

| Зміщення | Опкод      | Стек                           |
| -------: | ---------- | ------------------------------ |
|       9D | JUMPDEST   | Storage[3]-як-адреса 0x00      |
|       9E | SWAP1      | 0x00 Storage[3]-як-адреса      |
|       9F | POP        | Storage[3]-як-адреса           |
|       A0 | PUSH1 0x40 | 0x40 Storage[3]-як-адреса      |
|       A2 | MLOAD      | Mem[0x40] Storage[3]-як-адреса |

На самому початку коду ми встановлюємо Mem[0x40] на 0x80. Якщо ми пошукаємо 0x40 пізніше, то побачимо, що ми його не змінюємо — тому можемо припустити, що це 0x80.

| Зміщення | Опкод        | Стек                                             |
| -------: | ------------ | ------------------------------------------------ |
|       A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-як-адреса           |
|       A4 | PUSH1 0x00   | 0x00 CALLDATASIZE 0x80 Storage[3]-як-адреса      |
|       A6 | DUP3         | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-як-адреса |
|       A7 | CALLDATACOPY | 0x80 Storage[3]-як-адреса                        |

Копіюємо всі дані виклику в пам'ять, починаючи з 0x80.

| Зміщення | Опкод         | Стек                                                                           |
| -------: | ------------- | ------------------------------------------------------------------------------ |
|       A8 | PUSH1 0x00    | 0x00 0x80 Storage[3]-як-адреса                                                 |
|       AA | DUP1          | 0x00 0x00 0x80 Storage[3]-як-адреса                                            |
|       AB | CALLDATASIZE  | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-як-адреса                               |
|       AC | DUP4          | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-як-адреса                          |
|       AD | DUP6          | Storage[3]-як-адреса 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-як-адреса     |
|       AE | GAS           | GAS Storage[3]-як-адреса 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-як-адреса |
|       AF | DELEGATE_CALL |

Тепер усе набагато зрозуміліше. Цей контракт може діяти як [проксі](https://blog.openzeppelin.com/proxy-patterns/), викликаючи адресу в Storage[3] для виконання реальної роботи. `DELEGATE_CALL` викликає окремий контракт, але залишається в тому ж сховищі. Це означає, що делегований контракт, для якого ми є проксі, отримує доступ до того ж простору сховища. Параметри для виклику такі:

- _Газ_: Увесь газ, що залишився
- _Викликана адреса_: Storage[3]-як-адреса
- _Дані виклику_: Байти CALLDATASIZE, починаючи з 0x80, куди ми помістили початкові дані виклику
- _Дані повернення_: Немає (0x00 - 0x00). Ми отримаємо дані повернення іншим способом (див. нижче)

| Зміщення | Опкод          | Стек                                                                                          |
| -------: | -------------- | --------------------------------------------------------------------------------------------- |
|       B0 | RETURNDATASIZE | RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса                          |
|       B1 | DUP1           | RETURNDATASIZE RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса           |
|       B2 | PUSH1 0x00     | 0x00 RETURNDATASIZE RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса      |
|       B4 | DUP5           | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса |
|       B5 | RETURNDATACOPY | RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса                          |

Тут ми копіюємо всі дані повернення в буфер пам'яті, починаючи з 0x80.

| Зміщення | Опкод        | Стек                                                                                                                         |
| -------: | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|       B6 | DUP2         | (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса                             |
|       B7 | DUP1         | (((успіх/невдача виклику))) (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса |
|       B8 | ISZERO       | (((чи завершився виклик невдачею))) (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса |
|       B9 | PUSH2 0x00c0 | 0xC0 (((чи завершився виклик невдачею))) (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса |
|       BC | JUMPI        | (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса                             |
|       BD | DUP2         | RETURNDATASIZE (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса              |
|       BE | DUP5         | 0x80 RETURNDATASIZE (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса         |
|       BF | RETURN       |                                                                                                                              |

Отже, після виклику ми копіюємо дані повернення в буфер 0x80 - 0x80+RETURNDATASIZE, і якщо виклик успішний, ми виконуємо `RETURN` саме з цим буфером.

### Помилка DELEGATECALL {#delegatecall-failed}

Якщо ми потрапляємо сюди, на 0xC0, це означає, що викликаний нами контракт скасував виконання. Оскільки ми є лише проксі для цього контракту, ми хочемо повернути ті самі дані, а також скасувати виконання.

| Зміщення | Опкод    | Стек                                                                                                                |
| -------: | -------- | ------------------------------------------------------------------------------------------------------------------- |
|       C0 | JUMPDEST | (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса                    |
|       C1 | DUP2     | RETURNDATASIZE (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса     |
|       C2 | DUP5     | 0x80 RETURNDATASIZE (((успіх/невдача виклику))) RETURNDATASIZE (((успіх/невдача виклику))) 0x80 Storage[3]-як-адреса|
|       C3 | REVERT   |

Тому ми виконуємо `REVERT` з тим самим буфером, який ми використовували для `RETURN` раніше: 0x80 - 0x80+RETURNDATASIZE

![Call to proxy flowchart](flowchart-proxy.png)

## Виклики ABI {#abi-calls}

Якщо розмір даних виклику становить чотири байти або більше, це може бути дійсний виклик ABI.

| Зміщення | Опкод        | Стек                                              |
| -------: | ------------ | ------------------------------------------------- |
|        D | PUSH1 0x00   | 0x00                                              |
|        F | CALLDATALOAD | (((Перше слово (256 біт) даних виклику)))         |
|       10 | PUSH1 0xe0   | 0xE0 (((Перше слово (256 біт) даних виклику)))    |
|       12 | SHR          | (((перші 32 біти (4 байти) даних виклику)))       |

Etherscan повідомляє нам, що `1C` є невідомим опкодом, оскільки [його було додано після того, як Etherscan написав цю функцію](https://eips.ethereum.org/EIPS/eip-145), і вони її не оновили. [Актуальна таблиця опкодів](https://github.com/wolflo/evm-opcodes) показує нам, що це зсув праворуч

| Зміщення | Опкод            | Стек                                                                                                     |
| -------: | ---------------- | -------------------------------------------------------------------------------------------------------- |
|       13 | DUP1             | (((перші 32 біти (4 байти) даних виклику))) (((перші 32 біти (4 байти) даних виклику)))                  |
|       14 | PUSH4 0x3cd8045e | 0x3CD8045E (((перші 32 біти (4 байти) даних виклику))) (((перші 32 біти (4 байти) даних виклику)))       |
|       19 | GT               | 0x3CD8045E>перші-32-біти-даних-виклику (((перші 32 біти (4 байти) даних виклику)))                       |
|       1A | PUSH2 0x0043     | 0x43 0x3CD8045E>перші-32-біти-даних-виклику (((перші 32 біти (4 байти) даних виклику)))                  |
|       1D | JUMPI            | (((перші 32 біти (4 байти) даних виклику)))                                                              |

Розділення тестів на збіг підпису методу на дві частини таким чином економить у середньому половину тестів. Код, що йде відразу за цим, і код за адресою 0x43 дотримуються того ж шаблону: `DUP1` перші 32 біти даних виклику, `PUSH4 (((method signature>`, виконують `EQ` для перевірки на рівність, а потім `JUMPI`, якщо підпис методу збігається. Ось підписи методів, їхні адреси та, якщо відомо, [відповідне визначення методу](https://www.4byte.directory/):

| Метод                                                                                  | Підпис методу    | Зміщення для переходу |
| -------------------------------------------------------------------------------------- | ---------------- | --------------------- |
| [splitter()](https://www.4byte.directory/signatures/?bytes4_signature=0x3cd8045e)      | 0x3cd8045e       | 0x0103                |
| ???                                                                                    | 0x81e580d3       | 0x0138                |
| [currentWindow()](https://www.4byte.directory/signatures/?bytes4_signature=0xba0bafb4) | 0xba0bafb4       | 0x0158                |
| ???                                                                                    | 0x1f135823       | 0x00C4                |
| [merkleRoot()](https://www.4byte.directory/signatures/?bytes4_signature=0x2eb4a7ab)    | 0x2eb4a7ab       | 0x00ED                |

Якщо збігів не знайдено, код переходить до [обробника проксі за адресою 0x7C](#the-handler-at-0x7c), сподіваючись, що контракт, для якого ми є проксі, має збіг.

![ABI calls flowchart](flowchart-abi.png)

## splitter() {#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`](https://solidity-by-example.org/payable/). Якщо хтось надіслав нам 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 {#the-e4-code}

Ми вперше бачимо ці рядки, але вони спільні з іншими методами (див. нижче). Тому ми назвемо значення в стеку 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() {#currentwindow}

Код у зміщеннях 0x158-0x163 ідентичний тому, що ми бачили в 0x103-0x10E у `splitter()` (за винятком місця призначення `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 {#the-da-code}

Цей код також використовується спільно з іншими методами. Тому ми назвемо значення в стеку 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      |

А решта вже пояснювалася [вище](#the-e4-code). Отже, переходи до 0xDA записують вершину стека (Y) у 0x80-0x9F і повертають це значення. У випадку з `currentWindow()` він повертає Storage[1].

## merkleRoot() {#merkleroot}

Код у зміщеннях 0xED-0xF8 ідентичний тому, що ми бачили в 0x103-0x10E у `splitter()` (за винятком місця призначення `JUMPI`), тому ми знаємо, що `merkleRoot()` також не є `payable`.

| Зміщення | Опкод        | Стек                 |
| -------: | ------------ | -------------------- |
|       F9 | JUMPDEST     |
|       FA | POP          |
|       FB | PUSH2 0x00da | 0xDA                 |
|       FE | PUSH1 0x00   | 0x00 0xDA            |
|      100 | SLOAD        | Storage[0] 0xDA      |
|      101 | DUP2         | 0xDA Storage[0] 0xDA |
|      102 | JUMP         | Storage[0] 0xDA      |

Що відбувається після переходу, [ми вже з'ясували](#the-da-code). Отже, `merkleRoot()` повертає Storage[0].

## 0x81e580d3 {#0x81e580d3}

Код у зміщеннях 0x138-0x143 ідентичний тому, що ми бачили в 0x103-0x10E у `splitter()` (за винятком призначення `JUMPI`), тому ми знаємо, що ця функція також не є `payable`.

| Зміщення | Опкод        | Стек                                                         |
| -------: | ------------ | ------------------------------------------------------------ |
|      144 | JUMPDEST     |
|      145 | POP          |
|      146 | PUSH2 0x00da | 0xDA                                                         |
|      149 | PUSH2 0x0153 | 0x0153 0xDA                                                  |
|      14C | CALLDATASIZE | CALLDATASIZE 0x0153 0xDA                                     |
|      14D | PUSH1 0x04   | 0x04 CALLDATASIZE 0x0153 0xDA                                |
|      14F | PUSH2 0x018f | 0x018F 0x04 CALLDATASIZE 0x0153 0xDA                         |
|      152 | JUMP         | 0x04 CALLDATASIZE 0x0153 0xDA                                |
|      18F | JUMPDEST     | 0x04 CALLDATASIZE 0x0153 0xDA                                |
|      190 | PUSH1 0x00   | 0x00 0x04 CALLDATASIZE 0x0153 0xDA                           |
|      192 | PUSH1 0x20   | 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA                      |
|      194 | DUP3         | 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA                 |
|      195 | DUP5         | CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA    |
|      196 | SUB          | CALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA       |
|      197 | SLT          | CALLDATASIZE-4\<32 0x00 0x04 CALLDATASIZE 0x0153 0xDA         |
|      198 | ISZERO       | CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA        |
|      199 | PUSH2 0x01a0 | 0x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
|      19C | JUMPI        | 0x00 0x04 CALLDATASIZE 0x0153 0xDA                           |

Схоже, що ця функція приймає щонайменше 32 байти (одне слово) даних виклику.

| Зміщення | Опкод  | Стек                                         |
| -------: | ------ | -------------------------------------------- |
|      19D | DUP1   | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA      |
|      19E | DUP2   | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
|      19F | REVERT |

Якщо вона не отримує дані виклику, транзакція скасовується без будь-яких даних повернення.

Давайте подивимося, що станеться, якщо функція _дійсно_ отримає необхідні їй дані виклику.

| Зміщення | Опкод        | Стек                                     |
| -------: | ------------ | ---------------------------------------- |
|      1A0 | JUMPDEST     | 0x00 0x04 CALLDATASIZE 0x0153 0xDA       |
|      1A1 | POP          | 0x04 CALLDATASIZE 0x0153 0xDA            |
|      1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA |

`calldataload(4)` — це перше слово даних виклику _після_ підпису методу

| Зміщення | Опкод        | Стек                                                                         |
| -------: | ------------ | ---------------------------------------------------------------------------- |
|      1A3 | SWAP2        | 0x0153 CALLDATASIZE calldataload(4) 0xDA                                     |
|      1A4 | SWAP1        | CALLDATASIZE 0x0153 calldataload(4) 0xDA                                     |
|      1A5 | POP          | 0x0153 calldataload(4) 0xDA                                                  |
|      1A6 | JUMP         | calldataload(4) 0xDA                                                         |
|      153 | JUMPDEST     | calldataload(4) 0xDA                                                         |
|      154 | PUSH2 0x016e | 0x016E calldataload(4) 0xDA                                                  |
|      157 | JUMP         | calldataload(4) 0xDA                                                         |
|      16E | JUMPDEST     | calldataload(4) 0xDA                                                         |
|      16F | PUSH1 0x04   | 0x04 calldataload(4) 0xDA                                                    |
|      171 | DUP2         | calldataload(4) 0x04 calldataload(4) 0xDA                                    |
|      172 | DUP2         | 0x04 calldataload(4) 0x04 calldataload(4) 0xDA                               |
|      173 | SLOAD        | Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA                         |
|      174 | DUP2         | calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA         |
|      175 | LT           | calldataload(4)\<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA         |
|      176 | PUSH2 0x017e | 0x017EC calldataload(4)\<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
|      179 | JUMPI        | calldataload(4) 0x04 calldataload(4) 0xDA                                    |

Якщо перше слово не менше за Storage[4], функція завершується помилкою. Вона скасовується без будь-якого поверненого значення:

| Зміщення | Опкод      | Стек          |
| -------: | ---------- | ------------- |
|      17A | PUSH1 0x00 | 0x00 ...      |
|      17C | DUP1       | 0x00 0x00 ... |
|      17D | REVERT     |

Якщо calldataload(4) менше за Storage[4], ми отримуємо цей код:

| Зміщення | Опкод      | Стек                                                |
| -------: | ---------- | --------------------------------------------------- |
|      17E | JUMPDEST   | calldataload(4) 0x04 calldataload(4) 0xDA           |
|      17F | PUSH1 0x00 | 0x00 calldataload(4) 0x04 calldataload(4) 0xDA      |
|      181 | SWAP2      | 0x04 calldataload(4) 0x00 calldataload(4) 0xDA      |
|      182 | DUP3       | 0x00 0x04 calldataload(4) 0x00 calldataload(4) 0xDA |
|      183 | MSTORE     | calldataload(4) 0x00 calldataload(4) 0xDA           |

А комірки пам'яті 0x00-0x1F тепер містять дані 0x04 (0x00-0x1E — усі нулі, 0x1F — це чотири)

| Зміщення | Опкод      | Стек                                                                    |
| -------: | ---------- | ----------------------------------------------------------------------- |
|      184 | PUSH1 0x20 | 0x20 calldataload(4) 0x00 calldataload(4) 0xDA                          |
|      186 | SWAP1      | calldataload(4) 0x20 0x00 calldataload(4) 0xDA                          |
|      187 | SWAP2      | 0x00 0x20 calldataload(4) calldataload(4) 0xDA                          |
|      188 | SHA3       | (((SHA3 of 0x00-0x1F))) calldataload(4) calldataload(4) 0xDA            |
|      189 | ADD        | (((SHA3 of 0x00-0x1F)))+calldataload(4) calldataload(4) 0xDA            |
|      18A | SLOAD      | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] calldataload(4) 0xDA |

Отже, у сховищі є таблиця пошуку, яка починається з SHA3 від 0x000...0004 і має запис для кожного допустимого значення даних виклику (значення, меншого за 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](#the-da-code): він повертає верхнє значення стека тому, хто його викликав. Отже, ця функція повертає значення з таблиці пошуку тому, хто її викликав.

## 0x1f135823 {#0x1f135823}

Код у зміщеннях 0xC4-0xCF ідентичний тому, що ми бачили в 0x103-0x10E у `splitter()` (за винятком місця призначення `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](#the-da-code): він повертає верхнє значення стека тому, хто його викликав. Отже, ця функція повертає `Value*`.

### Підсумок методів {#method-summary}

Чи відчуваєте ви, що розумієте контракт на цьому етапі? Я — ні. Поки що ми маємо такі методи:

| Метод                             | Значення                                                                             |
| --------------------------------- | ------------------------------------------------------------------------------------ |
| Переказ                           | Приймає значення, надане викликом, і збільшує `Value*` на цю суму           |
| [splitter()](#splitter)           | Повертає Storage[3], адресу проксі-контракту                                                 |
| [currentWindow()](#currentwindow) | Повертає Storage[1]                                                                    |
| [merkleRoot()](#merkleroot)        | Повертає Storage[0]                                                                    |
| [0x81e580d3](#0x81e580d3)         | Повертає значення з таблиці пошуку за умови, що параметр менший за Storage[4] |
| [0x1f135823](#0x1f135823)         | Повертає Storage[6], тобто Value\*                                                    |

Але ми знаємо, що будь-яка інша функціональність забезпечується контрактом у Storage[3]. Можливо, якби ми знали, що це за контракт, це дало б нам підказку. На щастя, це блокчейн, і тут усе відомо, принаймні теоретично. Ми не бачили жодних методів, які встановлюють Storage[3], тому він, мабуть, був встановлений конструктором.

## Конструктор {#the-constructor}

Коли ми [розглядаємо контракт](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f), ми також можемо побачити транзакцію, яка його створила.

![Click the create transaction](create-tx.png)

Якщо ми натиснемо на цю транзакцію, а потім на вкладку **Стан**, ми зможемо побачити початкові значення параметрів. Зокрема, ми бачимо, що Storage[3] містить [0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761](https://etherscan.io/address/0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761). Цей контракт повинен містити відсутній функціонал. Ми можемо розібратися в ньому, використовуючи ті ж інструменти, що й для контракту, який ми досліджуємо.

## Проксі-контракт {#the-proxy-contract}

Використовуючи ті самі методи, що й для оригінального контракту вище, ми бачимо, що контракт скасовується, якщо:

- До виклику додано будь-яку кількість ETH (0x05-0x0F)
- Розмір даних виклику менший за чотири (0x10-0x19 та 0xBE-0xC2)

А також, що він підтримує такі методи:

| Метод                                                                                                           | Підпис методу                | Зміщення для переходу |
| --------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------------------- |
| [scaleAmountByPercentage(uint256,uint256)](https://www.4byte.directory/signatures/?bytes4_signature=0x8ffb5c97)          | 0x8ffb5c97                   | 0x0135              |
| [isClaimed(uint256,address)](https://www.4byte.directory/signatures/?bytes4_signature=0xd2ef0795)               | 0xd2ef0795                   | 0x0151              |
| [claim(uint256,address,uint256,bytes32[])](https://www.4byte.directory/signatures/?bytes4_signature=0x2e7ba6ef) | 0x2e7ba6ef                   | 0x00F4              |
| [incrementWindow()](https://www.4byte.directory/signatures/?bytes4_signature=0x338b1d31)                        | 0x338b1d31                   | 0x0110              |
| ???                                                                                                             | 0x3f26479e                   | 0x0118              |
| ???                                                                                                             | 0x1e7df9d3                   | 0x00C3              |
| [currentWindow()](https://www.4byte.directory/signatures/?bytes4_signature=0xba0bafb4)                          | [0xba0bafb4](#currentwindow) | 0x0148              |
| [merkleRoot()](https://www.4byte.directory/signatures/?bytes4_signature=0x2eb4a7ab)                             | [0x2eb4a7ab](#merkleroot)    | 0x0107              |
| ???                                                                                                             | [0x81e580d3](#0x81e580d3)    | 0x0122              |
| ???                                                                                                             | [0x1f135823](#0x1f135823)    | 0x00D8              |

Ми можемо проігнорувати чотири останні методи, оскільки ми ніколи до них не дійдемо. Їхні підписи такі, що наш оригінальний контракт обробляє їх самостійно (ви можете натиснути на підписи, щоб побачити деталі вище), тому це, мабуть, [перевизначені методи](https://medium.com/upstate-interactive/solidity-override-vs-virtual-functions-c0a5dfb83aaf).

Один із методів, що залишилися, — це `claim(<params>)`, а інший — `isClaimed(<params>)`, тому це схоже на контракт для ейрдропу. Замість того, щоб переглядати решту опкод за опкодом, ми можемо [спробувати декомпілятор](https://etherscan.io/bytecode-decompiler?a=0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761), який дає придатні для використання результати для трьох функцій із цього контракту. Зворотна розробка інших залишається як вправа для читача.

### scaleAmountByPercentage {#scaleamountbypercentage}

Ось що декомпілятор видає для цієї функції:

```python
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` перевіряє, що дані виклику мають, окрім чотирьох байтів підпису функції, щонайменше 64 байти, чого достатньо для двох параметрів. Якщо ні, то очевидно щось не так.

Оператор `if`, схоже, перевіряє, що `_param1` не дорівнює нулю, а `_param1 * _param2` не є від'ємним. Ймовірно, це зроблено для запобігання випадкам переповнення.

Нарешті, функція повертає масштабоване значення.

### claim {#claim}

Код, який створює декомпілятор, є складним, і не весь він релевантний для нас. Я пропущу частину з нього, щоб зосередитися на рядках, які, на мою думку, містять корисну інформацію

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

Тут ми бачимо дві важливі речі:

- `_param2`, хоча й оголошено як `uint256`, насправді є адресою
- `_param1` — це вікно, що затребується, яке має бути `currentWindow` або ранішим.

```python
  ...
  if stor5[_claimWindow][addr(_claimFor)]:
      revert with 0, 'Account already claimed the given window'
```

Отже, тепер ми знаємо, що Storage[5] — це масив вікон та адрес, а також інформація про те, чи затребувала адреса винагороду для цього вікна.

```python
  ...
  idx = 0
  s = 0
  while idx < _param4.length:
  ...
      if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]:
          mem[mem[64] + 32] = mem[(32 * idx) + 296]
          ...
          s = sha3(mem[_62 + 32 len mem[_62]])
          continue
      ...
      s = sha3(mem[_66 + 32 len mem[_66]])
      continue
  if unknown2eb4a7ab != s:
      revert with 0, 'Invalid proof'
```

Ми знаємо, що `unknown2eb4a7ab` насправді є функцією `merkleRoot()`, тому цей код виглядає так, ніби він перевіряє [доказ Меркла](https://medium.com/crypto-0-nite/merkle-proofs-explained-6dd429623dc5). Це означає, що `_param4` є доказом Меркла.

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

Ось як контракт переказує власні ETH на іншу адресу (контракту або зовнішню). Він викликає її зі значенням, яке є сумою для переказу. Тож схоже, що це ейрдроп ETH.

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

Два останні рядки кажуть нам, що Storage[2] — це також контракт, який ми викликаємо. Якщо ми [поглянемо на транзакцію конструктора](https://etherscan.io/tx/0xa1ea0549fb349eb7d3aff90e1d6ce7469fdfdcd59a2fd9b8d1f5e420c0d05b58#statechange), то побачимо, що цей контракт — [0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2), контракт обгорнутого ефіру (WETH), [вихідний код якого було завантажено на Etherscan](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code).

Отже, схоже, що контракт намагається надіслати ETH на `_param2`. Якщо йому це вдається — чудово. Якщо ні, він намагається надіслати [WETH](https://weth.tkn.eth.limo/). Якщо `_param2` є зовнішнім акаунтом (EOA), то він завжди може отримувати ETH, але контракти можуть відмовитися від отримання ETH. Однак WETH — це ERC-20, і контракти не можуть відмовитися від його прийняття.

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

Наприкінці функції ми бачимо, що генерується запис логу. [Погляньте на згенеровані записи логів](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f#events) та відфільтруйте за темою, яка починається з `0xdbd5...`. Якщо ми [натиснемо на одну з транзакцій, яка згенерувала такий запис](https://etherscan.io/tx/0xe7d3b7e00f645af17dfbbd010478ef4af235896c65b6548def1fe95b3b7d2274), то побачимо, що це дійсно схоже на затребування — акаунт надіслав повідомлення до контракту, який ми піддаємо зворотній розробці, і натомість отримав ETH.

![A claim transaction](claim-tx.png)

### 1e7df9d3 {#1e7df9d3}

Ця функція дуже схожа на [`claim`](#claim) вище. Вона також перевіряє доказ Меркла, намагається переказати ETH першому і створює такий самий тип запису логу.

```python
def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable:
  ...
  idx = 0
  s = 0
  while idx < _param3.length:
      if idx >= mem[96]:
          revert with 0, 50
      _55 = mem[(32 * idx) + 128]
      if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]:
          ...
          s = sha3(mem[_58 + 32 len mem[_58]])
          continue
      mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]])
  ...
  if unknown2eb4a7ab != s:
      revert with 0, 'Invalid proof'
  ...
  call addr(_param1) with:
     value s wei
       gas 30000 wei
  if not return_data.size:
      if not ext_call.success:
          require ext_code.size(stor2)
          call stor2.deposit() with:
             value s wei
               gas gas_remaining wei
  ...
  log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)
```

Головна відмінність полягає в тому, що першого параметра, вікна для зняття, тут немає. Натомість є цикл по всіх вікнах, які можна було б затребувати.

```python
  idx = 0
  s = 0
  while idx < currentWindow:
      ...
      if stor5[mem[0]]:
          if idx == -1:
              revert with 0, 17
          idx = idx + 1
          s = s
          continue
      ...
      stor5[idx][addr(_param1)] = 1
      if idx >= unknown81e580d3.length:
          revert with 0, 50
      mem[0] = 4
      if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:
          revert with 0, 17
      if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):
          revert with 0, 17
      if idx == -1:
          revert with 0, 17
      idx = idx + 1
      s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)
      continue
```

Тож це схоже на варіант `claim`, який затребує всі вікна.

## Висновок {#conclusion}

На цьому етапі ви вже повинні знати, як розуміти контракти, вихідний код яких недоступний, використовуючи або опкоди, або (коли це працює) декомпілятор. Як видно з обсягу цієї статті, реверс-інжиніринг контракту не є тривіальним, але в системі, де безпека має вирішальне значення, вміння перевіряти, чи працюють контракти так, як обіцяно, є важливою навичкою.

[Більше моїх робіт можна знайти тут](https://cryptodocguy.pro/).