June 12, 2022

MEV 1.2

Всем привет! С вами Тёма!

Сегодня мы продолжим погружаться в MEV и попытаемся понять что это такое и почему оно на слуху

Перевод данной статьи - ТЫК

MEV 1.1 - ТЫК

Начнем!

Боты MEV это верхушка, а мы хотим узнать более подробно, что эти боты делают

Чтобы найти эти секреты, нам нужно изучить инструменты и методы, связанные с декомпиляцией байткода контракта. Эта статья проведет вас через них и в процессе декомпилирует не верефнутый контракт бота MEV

Это вторая часть серии, в которой подробно рассказывается о первом набеге MevAlphaLeak на арену MEV. Эта статья следует непосредственно из первой части, поэтому, если вы еще не читали ее, то я рекомендую вам сделать это, прежде чем продолжить

Наш единственный способ продвинуться вперед — декомпилировать эти контракты, чтобы посмотреть, что происходит

ApeBot Contract Calldata

На изображении ниже показана calldata исходного вызова wfjizxua() и то, как они разделяются для использования в качестве входных данных для последующих вызовов. Это представляет собой большинство наших выводов из первой части

Эти 3 вызова нацелены на адреса 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 и 0xf4863028b093fdac9cf7fd67c0df6866ac3c7a60, оба из которых являются не верефнутыми контрактами

Прежде чем мы начнем, давайте быстро проясним, что такое не верефнутый контракт. В Etherscan при создании контракта у вас есть возможность верефнуть его, отправив код, создавший этот контракт

Это позволяет пользователям видеть код Solidity, который создал контракт. Контракты крупных компаний всегда будут верифицированными. Если контракт не верефнут, вместо этого вы увидите это

0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 байткод

У вас будет доступ только к байткоду контракта, который хранится в сети. У вас не будет кода Solidity, на который можно ссылаться. Таким образом, может быть очень трудно понять, что происходит в рамках контракта

Наша задача — декомпилировать байткод не верефнутых контрактов, чтобы понять, что произошло в транзакции ApeBot

Инструменты и методы

Наше исследование будет использовать следующие инструменты

Первым шагом в декомпиляции байткода является использование онлайн-декомпилятора для получения грубой схемы кода. Обратите внимание, что все, что дает нам декомпилятор, скорее всего, не будет на 100% точным. Бывают случаи, когда декомпилятор даст вам неверную информацию. Вот почему вы должны перекрестно ссылаться на дубликат опкодов

Первая задача — прогнать декомпилированный код построчно и попытаться связать значения и смысл любых объявленных переменных. Вот список вещей на которые следует обратить внимание/делать при прогонке кода

  • Если мы видим переменную, которая представляет байты 32–64 calldata, то точно найдите эти байты и запишите их в комментарии рядом с этой строкой, чтобы вы знали фактическое значение
  • Если мы видим всплывающий адрес в одной из переменных, то поищите его на Etherscan, чтобы узнать, известен ли этот адрес, есть ли у него верефнутый контракт и т. д.
  • Если переменные передаются в функции верефнутого контракта, то мы должны быть в состоянии понять, что представляют собой эти переменные. Найдите их входные данные в коде
  • Можем ли мы определить, что представляет собой каждый фрагмент calldata
  • Если контракт вызывается несколько раз, то мы должны проверить несколько транзакций — иногда calldata не будет иметь смысла, пока вы не просмотрите несколько транзакций
  • С какими другими контрактами взаимодействует контракт, верефнутые ли они. Если нет, мы должны повторить все эти шаги и для этих контрактов

Теперь давайте рассмотрим эти инструменты и методы один за другим на примере ApeBot

Декомпилятор Dedaub

Начнем с декомпилятора, в этом примере я использую декомпилятор Dedaub. Ниже приведен фрагмент декомпилированного контракта

0xd380f7e1dc7408aa007744ed3af390f8a47f9b75, который участвует в Call[1] и Call[2]

Декомпилятор Dedaub использует таблицу поиска сигнатур функций для поиска любых известных вызываемых функций. Мы видим, что адрес 0x8d12a197cb00d4747a1fe03395095ce2a5cc6819 имеет функцию «orderFills» и «balanceOf». Давайте посмотрим на это поближе

Оказывается, 0x8d12a197cb00d4747a1fe03395095ce2a5cc6819 — это контракт EtherDelta. Это верефнутый контракт, что означает, что мы можем посмотреть код для каждой из этих функций

Огромная часть понимания не верефнутых контрактов — это понимание сущностей, с которыми они взаимодействуют. Таким образом, нам нужно сделать небольшой экскурс, чтобы получить общее представление о том, что такое EtherDelta и как она работает

EtherDelta 2

Etherdelta 2 — это ончейн биржа с ордербуком, которая отличается от «Automated Market Maker» бирж (AMM)

Ончейн биржа с ордербуком действует так же, как обычная оффчейн биржа с ордербуком, например Binance. Есть Makers & Takers, а не пулы ликвидности. Ордера Maker определяют актив для продажи/покупки за другой актив по определенной цене

В случае с EtherDelta эти ордера «Maker» могут быть сделаны ончейн через транзакцию или путем выполнения чужого ордера на сайте EtherDelta

Заметьте, что EtherDelta больше не работает, но я предполагаю, что эти ордера должны были храниться где-то в автономной базе данных, и запросы «Taker» могли запросить ордера «Maker», который они хотят выполнить

Если заказ «Maker» сделан ончейн, то генерируется событие. Я просмотрел событие для адреса, связанного с этой транзакцией ApeBot в сети, и ничего не нашел. В результате мы можем сделать вывод, что ордер Maker, связанный с транзакцией ApeBot, был сделан с помощью сигнатуры функции

Теперь у нас есть общее представление о том, как работает EtherDelta, давайте вернемся к коду. Вот полный декомпилированный контракт для 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75

Начнем с первого вызова (Call [1]), сигнатуры функции 0x78789120

Функция 0x78789120 - Контракт 0xd3…75 - Call [1]

Ниже приведен декомпилированный код функции 0x78789120. Фрагменты контракта EtherDelta 2 также были включены вместе с parity traces для внутренних вызовов (orderFills и balanceOf). Давайте пробежимся по нему по частям

1) Мы начинаем с вызова функции orderFills для адреса 0x8d12a197cb00d4747a1fe03395095ce2a5cc6819, который, как мы знаем, является EtherDelta 2. OrderFills принимает varg4, varg5 и возвращает v0 и v1. v0 показывает, был ли вызов успешным, а v1 — фактический возвращенный результат. Требование v0 после этого вызова подтверждает, что вызов был успешным. Мы будем использовать parity traces для определения varg4 и varg5, но мы можем видеть, что они исходят из calldata

2) Parity trace для этого внутреннего вызова, здесь мы можем видеть входные данные (calldata) для этого вызова

  1. 0x19774d43
    • Сигнатура функции
  2. 000000000000000000000000aac971235706aa7b49dd3cc2e42a9695d2060da0
    • Это varg4, он выглядит как адрес Ethereum. Если мы посмотрим на Etherscan, то мы увидим, что это адрес EOA
  3. 6ce7751bd5b9723aa3fa7e4fc4d97865cf912fce880a7797dc0a0da9508f31d9
    • Это varg5, на данный момент мы не знаем, что представляет собой значение, но, поскольку это входные данные для функции, мы можем найти его в верефнутом контракте EtherDelta.

3) Если мы проверим код orderFills в контракте EtherDelta, то увидим, что orderFills — маппинг маппинга. «Учетная запись пользователя» маппается с «Хэшем заказа», который маппается с uint. Этот uint показывает, какая часть заказа была выполнена. Используя эту новую информацию, мы можем определить, что 0xaac971235706aa7b49dd3cc2e42a9695d2060da0 на самом деле является «адресом учетной записи пользователя Maker», а 0x6ce7751bd5b9723aa3fa7e4fc4d97865cf912fce880a7797dc0a0da9508f31d9 на самом деле является ордером EtherDelta

4) varg1 - v1, из предыдущей функции мы знаем, что v1 представляет собой сумму выполненного ордера (обратите внимание, что это в ETH). Мы пока не можем быть уверены, но вполне вероятно, что varg1 — это общая сумма ордера в ETH. Общая сумма ордера за вычетом заполненной суммы даст оставшуюся сумму ордера. VARG1 равен 0x00000000000000000000000000000000000000000000000000F0142DD1C52700 из нашего CallData и V1 можно найти с вывода из parity trace [2, 1, 0, 0, 2, 1] (OrderFills), которые равны 0x0000000000000000000000000000000000000000000000000000000000000000

5) Вызов адреса EtherDelta 2 в функции balanceOf, принимает varg2 и varg4. Мы снова будем использовать parity traces

6) Parity trace для этого внутреннего вызова, calldata

  1. 0xf7888aec
    • Сигнатура функции
  2. 000000000000000000000000bb49a51ee5a66ca3a8cbe529379ba44ba67e6771
  3. 000000000000000000000000aac971235706aa7b49dd3cc2e42a9695d2060da0

7) Функция balanceOf в контракте EtherDelta взаимодействует с маппингом токенов. Функция возвращает нам баланс конкретного токена для конкретного адреса. Итак, мы проверяем баланс CST «Учетной записи пользователя Maker». Глядя на вывод parity trace, мы видим, что v5 равно 0x0000000000000000000000000000000000000000000000005b95329a965952e83d

8) Мы можем увидеть некоторый расчет, v5 * varg1 / varg3 = баланс CST * (сумма сделки в ETH / varg3). На данном этапе мы не уверены, что представляет собой varg3. В результате нам трудно определить, что он пытается вычислить

  1. Это хороший пример того, как следующий вызов (Call [2]) поможет нам определить этот расчет. А пока я расскажу вам, что такое varg3, но будьте осторожны, когда мы обнаружим эту информацию в следующем вызове. На самом деле нам пришлось бы вернуться к этому элементу, как только мы получили бы новую информацию
  2. varg3 сообщает нам сумму CST в заказе Maker. Создатель готов обменять X CST на Y ETH. varg3 — это X. В приведенном выше расчете используются X и Y, чтобы получить коэффициент торговли 1/2500 ETH/CST. Это значение умножается на баланс Makers CST. Таким образом, результатом этого расчета, v5, является баланс Makers CST в ETH с использованием коэффициента обмена в качестве коэффициента конверсии

9) v3 < v6 = 0xf0142dd1c52700 < 0xf0142dd1dcb4be проверяет, меньше ли сумма сделки в ETH, чем баланс Maker в ETH. Это гарантирует, что Maker готов выполнить сделку. В нашем случае это возвращает True. Мы видим, что в этом операторе If нет кода, при просмотре опкодов вы можете увидеть JUMPI, основанный на этом результате. Вполне вероятно, что если это условие не будет выполнено, то исполнение будет отменено. Вы можете подтвердить это с помощью Foundry с --debug & --fork-block-number для создания транзакций, которые будут исследовать этот раздел кода и просматривать опкоды, которые вызываются в различных сценариях

10) v2 < varg6 = 0xf0142dd1c52700 < 0x05 проверяет, меньше ли сумма сделки Maker в ETH, чем сумма, которую мы хотим обменять в ETH. Обратите внимание, что v2 ссылается на свое определение в строке 15, а не в строке 21, еще одна вещь, на которую следует обратить внимание при декомпилировании кода. Эта проверка имеет смысл, она подтверждает, что сумма открытой сделки достаточно велика для исполнения нашего ордера. В нашем случае это возвращает false, поэтому мы переходим к оператору else

11) Строка 27 не вызывается в потоке транзакции ApeBot, однако интересно отметить, что байт 2 элемента calldata [0] в нашем случае 0x4b является некоторой пользовательской кодировкой. Он используется, когда баланс суммы сделки мейкера меньше суммы, которую мы хотели бы обменять

12) v2 установлено на 4200, не зная, что представляет собой это значение. Возвращаемое значение не используется ни в каких последующих вызовах транзакции ApeBot

13) Возвращает 2 значения. Два v2 снова представляют разные значения, мы всегда можем проверить возвращаемые значения с помощью parity traces

  • v2, v2 = 0x05, 0x1068

Это показывает нам, что этот первый call (call[1]) в конечном счете касается проверки действительности сделки, т.е. возможно ли это

Некоторые элементы calldata не использовались в этом вызове функции или использовались без контекста, например, varg3. Таким образом, мы не смогли определить их значение

Давайте теперь посмотрим на call[2], который вызывает тот же контракт 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75, и посмотрим, может ли он пролить свет на эти неизвестные фрагменты calldata

Функция 0x401687f4 - Контракт 0xd3…75 - Call [2]

Давайте быстро вернемся к calldata из Call [1] и Call [2] в начале статьи. Вы заметите, что они одинаковы. Ниже приведен декомпилированный код функции 0x401687f4, давайте углубимся

1) Функция 0x401687f4 вызывает другую внутреннюю функцию 0x74 и передает значение 4

2) v0 - v11 объявляются с использованием calldata в 32-байтовых фрагментах со смещением в 4 байта. Смещение исходит из varg0 = 4, что представляет собой 4-байтовую сигнатуру функции в calldata

3) Выполняется проверка на наличие ненулевого адреса в элементе данных v0 = calldata[0]. Обратите внимание, что нулевой адрес 0x000…00 представляет ETH в EtherDelta, поэтому это фактически спрашивает, является ли обмен ETH или ERC20. В нашем случае у нас нулевой адрес, поэтому мы не будем вводить этот фрагмент кода. Давайте посмотрим, что он делает в любом случае. После ввода происходит проверка первого байта v0. Если он равен нулю, это означает, что токен не был одобрен для этого контракта

4) В этом разделе вызывается контракт ERC20 для данного токена и проверяется, есть ли у EtherDelta (0x8d1…19) какой-либо allowance, используя функцию allowance(). Если это не так, он вызывает функцию approve() для контракта ERC20 с адресом EtherDelta. Это позволяет EtherDelta перемещать ваш токен ERC20 без дальнейшего вашего одобрения

5) Затем он проверяет баланс контракта ApeBot (поскольку его delegate call адресса(this) является родительским коллером) для данного токена ERC20, вызывая balanceOf по адресу контракта токена. Затем он вносит полную сумму (полученную из balanceOf) в EtherDelta за вычетом 1 единицы токена. Этот минус 1 — оптимизация газа для будущих коллов. Дешевле изменить слот с ненулевого значения на другое ненулевое значение, чем изменить с нулевого значения на ненулевое значение

6) Теперь мы вернулись к процессу транзакции ApeBot, он также выполняет депозит EtherDelta, но использует v11 (calldata [11]) = 0x05 в качестве суммы депозита (5 Wei)

7) Торговая функция EtherDelta, фрагмент верефнутого контракта которой я включил, чтобы показать вам входные данные функции. Многие входные данные, которые мы используем, получены из нашей calldata, это дает нам возможность выяснить, что представляет собой каждый фрагмент calldata. Ниже приведен список входов

  1. TokenGet (calldata [0]) = это с точки зрения мэйкера, токен, который они хотели бы получить, это ETH
  2. AmountGet (calldata [1]) = сумма, которую мэйкер хотел бы получить в ETH за полную сделку
  3. TokenGive (calldata [2]) = опять же, это с точки зрения мэйкера, токен, который они хотели бы предоставить — это CST
  4. AmountGive (calldata [3]) = Сумма, которую мэйкер хотел бы отдать в CST за полную сделку
    1. На этом этапе мы можем определить стоимость сделки, взглянув на соотношение CST/ETH = 0x5b95329a8d5d289800/0xf0142dd1c52700 = 1689404535900000000000/67576181436000000 = 2500 CST → 1 ETH
    2. Это когда мы можем вернуться к varg3 в call[1], чтобы определить, что делает вычисление
  5. Expires (calldata [4]) = номер блока, в котором истекает срок действия ордера. После этого номера блока ордер больше не может торговаться
  6. Nonce (calldata [5]) = Используется EtherDelta, nonce — это число, которое вы можете включить в свой ордер, чтобы сделать его относительно уникальным. Таким образом, если вы хотите разместить два идентичных ордера, у них не будет одинакового хэша
  7. User (calldata [6]) = Ethereum адрес мэйкера
  8. V (calldata [7]) = компонент подписи ECDSA — используется для проверки того, что предлагаемая сделка является легитимной, т. е. была подписана мэйкером
  9. R (calldata [8]) = компонент подписи ECDSA — используется для проверки того, что предлагаемая сделка является легитимной, т. е. была подписана мэйкером
  10. S (calldata [9]) = компонент подписи ECDSA — используется для проверки того, что предлагаемая сделка является легитимной, т. е. была подписана мэйкером
  11. Amount = сумма, которую тейкер хотел бы обменять, обратите внимание, что сумма указана в amountGet, в нашем случае это ETH. Значение здесь 0x03, которое отличается от нашей calldata 0x05. Вы заметите, что 0xde0b6b3a7640000/0xdeb5f2f95b78000 = 100000000000000000000/10030000000000000000 = ~0,997, комиссия тейкера за сделку составляет 0,3%. Приведенный выше расчет берет ваш торговый деп и умножает его на 0,997, поэтому у вас остается достаточно, чтобы заплатить комиссию тейкера в размере 0,3%. 5 Wei * 0,997 = 4,985, но Ethereum не имеет чисел с плавающей запятой, поэтому мы округляем до 4. Затем делаем минус один, чтобы получить 3 Wei (минус один снова связан с экономией газа)

8) Наконец, после совершения сделки мы выводим наши средства. WithdrawToken() используется для вывода ERC20, а remove() используется для ETH. Поскольку v2 — это адрес CST, мы снова воспользуемся WithdrawToken(), снова здесь есть минус один для экономии газа

Теперь, когда мы прошли через call[1] и call[2], мы можем определить, что представляет calldata для исходного вызова. Ниже приведена calldata для Call[1] и [2] вместе с тем, что представляет каждое значение

На этом этап сделка EtherDelta завершена, давайте подведем итоги

EtherDelta Leg

• 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 - Call[1]

  • Убедитесь, что сделка не была заполнена
    • 0 ETH из сделки 0,067576181436 ETH было исполнено
    • Таким образом, 0,067576181436 ETH сделки все еще доступны.
  • Убедитесь, что на балансе EtherDelta адреса Maker достаточно токенов для совершения сделки (в ETH)
    • Мэйкер (0xaac971235706aa7b49dd3cc2e42a9695d2060da0) имел баланс 1689,404535938590369853 CST
    • Преобразование в ETH на основе торгового коэффициента (1:2500) дает 0,067576181437543614 ETH
    • Открыть сделку в ETH < Баланс мейкера в ETH
    • 0.067576181436 ETH < 0.067576181437543614 ETH = True
    • Следовательно, у мейкера достаточно средств для покрытия сделки
  • Убедитесь, что предлагаемая тейкером сумма сделки меньше суммы открытой сделки мейкером в ETH
    • Открытие сделки в ETH < Предлагаемая сделка покупателя в ETH
    • 0.067576181436 ETH < 0.000000000000000005 ETH = False
    • Таким образом, мы сможем совершить сделку на сумму нашего тейкера

• 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 - Call [2]

  • Проверьте, находится ли сделка тейкера в токене ERC20 или ETH, для нас это ETH
  • Депозит ETH в EtherDelta из контракта MevAlphaLeak для торговли
    • Депозит 5 Wei
  • Выполнение обмена ETH на CST
    • Предлагаемая сумма сделки 5 Wei используется в расчетах для определения суммы, вложенной в торговую функцию
      • ((5 * 1000000000000000000) / 1003000000000000000) - 1
      • Выдает 3,9850448654, без чисел с плавающей запятой, поэтому округляется до 3
    • 3 Wei обмениваются на 0.000000000000075 CST
    • Комиссия тейкера составляет (0,000000000000000003 * 3000000000000000) / 10000000000000000000, это дает 9e-21
      • 1 Wei = 1e-18 ETH, вы не можете опуститься ниже 1 Wei, и в EVM нет чисел с плавающей запятой, поэтому округляется до 0
      • Поэтому комиссия не взимается
  • Проверьте баланс CST контракта ApeBot на пост-трейде EtherDelta
    • Возвращает баланс 0.000000000000075 CST
  • Выведите баланс CST из EtherDelta на контракт MevAlphaLeak для следующего этапа арбитража
    • Выводим 0.00000000000007499 CST

Надеюсь статья была интересной и понятной!

Мой телеграмм канал - https://t.me/ortomich_crypto