Построение арбитражного бота

  • Введение в боты MEV
  • Создание бота для арбитражной торговли MEV с нуля

Введение в боты MEV

Предполагается, что это художественное изображение бота MEV в стиле Ван Гога

Эта статья служит учебником для серии о разработке ботов MEV. В этой серии мы разработаем MEV бота средней сложности. Большая часть предоставленной информации относится только к блокчейнам, совместимым с Ethereum и EVM (BSC, Polygon и т. д.) Некоторые приемы и методы, представленные в серии, все еще могут обеспечить небольшое конкурентное преимущество, но ожидайте, что для получения какого-либо значительного дохода потребуются значительные изменения в стратегии. Прежде чем мы начнем, давайте определимся, что такое MEV.

Извлекаемая ценность в блокчейне

MEV изначально обозначал значение, которое может быть извлечено майнером из блока. Это значение представляет собой разницу между ожидаемым значением блока и значением блока для майнера по мере его добычи. Математическое ожидание блока — это сумма комиссий за все транзакции, включенные в блок. Было реализовано, что, изменяя порядок транзакций или вставляя частные транзакции в блок непосредственно перед его добычей, майнер может увеличить стоимость блока. Это называется извлекаемым значением майнера (MEV). В настоящее время с распространением PoS майнеры были заменены валидаторами (хотя термин MEV все еще используется). Хотя майнеры и валидаторы имеют преимущественную позицию для извлечения ценности из блока, у них может не быть оптимальной стратегии для извлечения всей ценности. Существуют механизмы, которые позволяют любому агенту с более совершенными стратегиями делиться прибылью с валидаторами. Эти агенты повсеместно используют автоматизированные программы, называемые ботами MEV.

Самые популярные типы MEV ботов

Децентрализованные финансы (DeFi), естественно, являются игровой площадкой для ботов MEV: не требующий разрешения характер блокчейнов и большие суммы денег на кону делают их очень привлекательной целью. Очевидные области действий для ботов привели к тому, что их можно классифицировать по нескольким категориям. О самых популярных из них будет рассказано в следующих параграфах.

Боты-ликвидаторы

В DeFi появились блестящие финансовые услуги, такие как протоколы кредитования (Compound, Aave, MakerDAO и т. д.). Эти протоколы позволяют пользователям одалживать свои активы другим пользователям в обмен на доходность. Заемщики обязаны предоставить залог сверх суммы, которую они заимствуют. Если со временем стоимость залога падает ниже определенного порога, залог ликвидируется, а кредитор получает возмещение. Это оказалось простым способом предложить заемщикам кредитное плечо через систему без доверия, как и ожидается во всех частях экосистемы DeFi. Блестящая часть большинства этих протоколов заключается в том, что процесс ликвидации открыт для любого субъекта, не относящегося к протоколу. Любой желающий может следить за состоянием протокола и инициировать ликвидацию позиции с недостаточным обеспечением. Смарт-контракт проверяет, действительно ли залог стоит меньше определенного порога, и стимулирует ликвидатора, предоставляя ему часть залога. Это именно то, что делают ликвидационные боты: ликвидируют позиции автоматически. В какой-то момент это была очень прибыльная стратегия, но по мере того, как все больше и больше людей начали это делать, прибыль резко сократилась.

Арбитражные боты

Очень большая часть экосистемы DeFi состоит из децентрализованных бирж (DEX). Эти биржи позволяют пользователям торговать активами без необходимости в доверенной третьей стороне. Самой популярной DEX на Ethereum является Uniswap. Текущая версия Uniswap — V3. Именно здесь происходит большая часть активности, но Uniswap V2 все еще имеет некоторый объем, отчасти потому, что LP никогда не удосужились перевести свои средства в V3. Uniswap V2 очень прост для понимания и был разветвлен многими другими конкурирующими DEX, такими как Sushiswap, Pancakeswap и т. Д. В любой момент времени цена актива на DEX определяется соотношением резервов двух активов в пуле ликвидности. Если цена актива на DEX отличается от цены того же актива на другой DEX, то есть возможность купить актив на более дешевой DEX и продать его на более дорогой. Это называется арбитражем. Арбитражные боты — самый популярный тип ботов MEV. Они также доступны новичкам, так как примитивные боты могут быть разработаны за ограниченное количество времени. Простые боты вряд ли будут прибыльными, но они являются хорошей отправной точкой для изучения торговли. Простые стратегии могут быть улучшены во многих отношениях, ограниченных только творчеством разработчика. По этим причинам эта серия будет посвящена арбитражным ботам. Обратите внимание, что на практике большая часть арбитражной прибыли генерируется ботами, которые торгуют между централизованными и децентрализованными биржами.

Сэндвич-боты

Сэндвич — еще один очень популярный тип бота MEV. Он использует тот факт, что транзакции, отправленные в блокчейн неискушенными пользователями, часто публикуются в пуле транзакций (часто называемом мемпулом). У ботов есть способы прочитать мемпул, найти целевую транзакцию для использования и подделать транзакцию, которая будет добыта до целевой транзакции. Своп-транзакции в DeFi являются основными целями сэндвич-ботов из-за необходимости пользователям DeFi устанавливать некоторое проскальзывание, то есть они принимают немного худшую цену исполнения для своей транзакции. Они делают это, потому что цена AMM может меняться между моментом отправки транзакции и временем ее добычи, что может привести к сбою транзакции, что приведет к потенциально значительным затратам на газ. Каждая покупка/продажа AMM изменяет цену пропорционально количеству токенов, предоставленных свопом, и обратно пропорционально количеству токенов, ранее предоставленных в пул поставщиками ликвидности. Боты могут разместить вредоносный ордер на покупку прямо перед законным заказом пользователя на покупку. Законный ордер все еще может быть выполнен до тех пор, пока цена не увеличилась слишком сильно с момента размещения ордера (это относительное движение цены называется проскальзыванием). Это еще больше сдвинет цену для оператора бота, который затем сможет продать ранее купленные токены по более высокой цене. Эта операция называется сэндвичингом, потому что оператор бота размещает ордер на покупку до и ордер на продажу после законного ордера, фактически помещая его между двумя ордерами. Также используется термин «фронтраннинг» с тем же значением, которое он передает в традиционных финансах. Сэндвич-боты более сложны в разработке, чем арбитражные боты, но они также, как правило, более прибыльны, поскольку они также могут реализовывать арбитражные стратегии в одном пакете транзакций. Обратите внимание, что сэндвичинг, в отличие от арбитража и ликвидации, можно рассматривать как аморальный, поскольку он вынуждает транзакцию законного пользователя выполняться по более низкой цене, чем ожидалось. Опережающий бег на самом деле незаконен на традиционных финансовых рынках. Морально приемлемый, но менее прибыльный вариант этой стратегии называется бэкраннингом. Он заключается только в размещении транзакции после целевой транзакции. Более сложный вид ботов с обратной связью также реализует полную арбитражную стратегию, часто занимая большую часть пирога, доступного обычным арбитражным ботам.

Более маргинальный тип: снайпинг

Бот-снайпер попытается приобрести цифровой актив сразу после того, как он станет общедоступным. Например, приобретение NFT из коллекции, недавно открытой для чеканки, или покупка токенов на недавно запущенной продаже токенов. Простые боты, которые покупали токены сразу после их листинга на Uniswap, раньше были прибыльными, но по мере того, как количество бесполезных токенов резко возросло, все боты истощали свои ETH, если их запускать в настоящее время. Боты-снайперы могут быть созданы для одного использования/цели, но это требует много времени и усилий для одного варианта использования, а также хорошего общего знания экосистемы блокчейна.

Архитектура MEV бота

Боты MEV, как правило, имеют схожую архитектуру, независимо от их типа. Они выполняют три основные задачи:

  • Чтение блокчейна
  • Поиск возможностей
  • Выполнение ончейн-действий

Ниже приводится общее описание содержания каждой из этих задач.

Чтение блокчейна

Поскольку боты MEV предназначены для взаимодействия с блокчейном, который производит новые блоки транзакций каждые несколько секунд (12 для Ethereum), они должны иметь возможность непрерывно и эффективно считывать состояние блокчейна. Боты Sandwhich также должны уметь читать мемпул, который представляет собой пул транзакций, которые еще предстоит включить в блок.

Чтобы взаимодействовать с блокчейном, боты должны подключаться к узлу. Узлы — это компьютеры, между которыми распределен блокчейн в точных копиях. Эти узлы связаны друг с другом и обновляют свое внутреннее состояние при создании нового блока. Они также несут ответственность за хранение ожидающих транзакций в своем собственном мемпуле и распространение их на другие узлы. Самым популярным программным обеспечением для запуска узла Ethereum является Go-Ethereum, более известный как Geth. Он написан на Go и был эталонной реализацией протокола Ethereum. В настоящее время это наиболее часто используемый клиент Ethereum. Другой, менее популярный, — Parity, написанный на Rust. Многие провайдеры предлагают бесплатный доступ к узлу, например Infura, Alchemy или Quicknode. Необходимо время, чтобы сравнить их предложения, так как некоторые узлы более надежны, чем другие, а некоторые предлагают больше возможностей, чем другие. Например, Infura не разрешает доступ к мемпулу, который необходим для сэндвич-ботов. Программное обеспечение узла можно запрашивать удаленно через API JSON-RPC. Этот API позволяет получать информацию о состоянии блокчейна и отправлять транзакции на узел. Вот пример кода Python, который извлекает текущий номер блока цепочки основной сети Ethereum через JSON-RPC API узла, размещенного в Infura:import requests
import json

def get_block_number():
url = «https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID»
headers = {‘content-type’: ‘application/json’}
data = {
«jsonrpc»: «2.0»,
«method»: «eth_blockNumber»,
«params»: [],
«id»: 1
}
response = requests.post(url, data=json.dumps(data), headers=headers)
return int(response.json()[‘result’], 16)

print(get_block_number())

Результат:17388115

Такие библиотеки, как Web3.py или Web3.js могут значительно упростить эту часть. Они также предлагают множество других распространенных функций, таких как подписание транзакций или анализ данных, отправленных/полученных узлом. Обратите внимание, что более сложные операторы ботов будут запускать свой собственный узел, чтобы избежать задержек в сети. Это может легко стоить несколько сотен долларов в месяц, поэтому новичку это может не стоить того. Запросы JSON-RPC могут занять несколько секунд, чтобы вернуть ответ, поэтому он значительно ограничивает объем информации, которая может быть получена между каждым блоком. Чтобы избежать этого узкого места, большинство операторов ботов развертывают смарт-контракт, который будет считывать всю необходимую им информацию в цепочке и возвращать ее за одну транзакцию. Очень искушенные поисковики MEV могут даже вообще избежать этой части, содержа большую часть кода своего бота в модифицированной версии программного обеспечения своего узла, избегая каких-либо узких мест связи или преобразования представления данных, но за счет более сложного процесса разработки и более высоких затрат на эксплуатацию / обслуживание.

Поиск возможностей

После того, как бот извлек всю необходимую информацию из блокчейна, он может начать искать возможности. Это самая важная часть бота, так как она будет определять его прибыльность. Стратегии уникальны для каждого бота. Для простого сэндвич-бота он будет заключаться в том, чтобы найти, какие транзакции являются действительными своп-сделками, и рассчитать максимальную прибыль, которую можно получить, сэндвич-сэндвич. Для арбитражного бота он будет заключаться в нахождении максимальной прибыли, которую можно получить, обменивая заданное количество токенов между двумя биржами, которые имеют разные цены на один и тот же токен.

Ончейн-действия

После того, как оператор определил возможности для использования, он должен отправлять транзакции, чтобы действовать в соответствии с ними в цепочке. Это всегда делается путем отправки транзакции в смарт-контракт, содержащий эту логику в цепочке. Эта логика может заключаться в том, чтобы просто выполнить несколько свопов и проверить, что сырая прибыль больше, чем затраты на газ. Операторы должны проявлять осторожность при разработке контракта, который будет управлять их стратегией, чтобы гарантировать, что транзакции потребляют как можно меньше газа для максимизации их прибыли. Смарт-контракты могут быть написаны на разных языках, которые будут компилироваться в байт-код EVM. Самым популярным из них является Solidity, который в значительной степени вдохновлен Javascript. Также можно писать смарт-контракты на Vyper, который является языком, похожим на Python. Операторы более сложных ботов очень часто пишут на сборке Yul, чтобы иметь полный контроль над байт-кодом, который будет выполняться в цепочке. Большинство ботов на самом деле конкурируют за одни и те же возможности. Единственное конкурентное преимущество, которое операторы имеют перед другими, — это эффективность их кода смарт-контракта, который в конечном итоге диктует, какие транзакции будут включены в следующий блок.

Исполнение транзакций

Блоки Ethereum состоят из транзакций, которые выполняются одна после заказа. Этот порядок определяется майнером/валидатором, который производит следующий блок. Именно этот ордер приносит майнеру наибольшую прибыль. Вплоть до 2020 года операторам ботов приходилось конкурировать за включение, отправляя публичные транзакции. Теперь вместо этого они могут отправлять свои транзакции напрямую валидаторам через аукционную систему Flashbots. Эти два механизма будут сравнены в следующем разделе, чтобы лучше понять преимущества системы Flashbots.

Как это делалось раньше: приоритетные газовые аукционы (PGA)

При отправке публичной транзакции, если цена на газ, установленная оператором бота, была ниже, чем у какого-либо конкурента, транзакция не удалась и повлекла бы за собой очень высокие затраты для оператора. Это связано с тем, что валидаторы хотят максимизировать свою прибыль и всегда будут включать транзакции, которые платят больше всего газа в первую очередь. Чтобы повысить вероятность включения, операторы будут устанавливать очень высокую цену на газ, что приведет к очень высокой стоимости газа для оператора, даже если сделка не будет включена в блок. Боты должны были постоянно следить за мемпулом, чтобы убедиться, что ни один конкурент не установил более высокую цену. Если это так, то на замену предыдущей, но с более высокой ценой на газ, нужно было отправить еще одну транзакцию с тем же номером. Эта система была очень неэффективной, так как приводила к большому количеству потраченного впустую газа и делала конструкцию бота намного сложнее, чем нужно.

Связки Flashbots

В 2020 году появилась новая система под названием Flashbots. Это позволяет любому отправлять транзакции напрямую майнерам, без необходимости конкурировать с другими участниками мемпула. Эта система называется пакетным аукционом. Он имеет большие преимущества для операторов ботов:

  • Транзакции, которые завершаются сбоем, не включаются в следующий блок, что снижает стоимость неудачных транзакций. Любой может извлечь выгоду из этой системы, даже если он не запускает бота. Неудачные транзакции теперь ушли в прошлое.
  • Транзакции могут быть отправлены майнерам в виде пакета транзакций. Майнеры могут гарантировать, что никакая другая транзакция не будет включена между транзакциями сэндвич-пакета, что делает сэндвичинг менее рискованным. Если одна транзакция не удалась, весь пакет отбрасывается, предоставляя те же гарантии, что и одна транзакция.
  • Устранение рисков, связанных с опережением. Поскольку транзакции не являются публичными, они не могут выполняться другими ботами.
  • Программный платеж: смарт-контракт может выбрать количество газа, которое он хочет заплатить майнеру, в зависимости, например, от прибыли, которую он получит (которая не всегда известна заранее). Это максимизирует прибыль бота, и позволяет им не переплачивать за газ. Транзакции, которые в итоге недоплачивают (или не платят вообще), майнер просто отбрасывает.

После The Merge (парижское обновление) Ethereum является блокчейном Proof-of-Stake. Это означает, что консенсус достигается уже не майнерами, а валидаторами. Система Flashbots также была адаптирована к этому новому механизму консенсуса, и команда предложила промежуточное программное обеспечение под названием mev-boost, которое позволяет отправлять пакеты валидаторам. Эта новая система также отделяет производителя блока от валидатора. Теперь поисковики MEV и другие пользователи, желающие отправлять приватные пакеты предлагаемому валидатору, делают это через посредника. Это, однако, не сильно меняет способ отправки пакетов транзакций раньше.

Примеры кода с открытым исходным кодом

Прежде чем приступить к написанию бота, было бы разумно взглянуть на существующих ботов с открытым исходным кодом, чтобы лучше понять, как они работают. Вот пара примеров:

  • Flashbots simple-arbitrage: репозиторий TypeScript, созданный командой Flashbots, который создает простого арбитражного бота, отправляющего пакеты… к Flashbots.
  • Alciviades Capital mev_bundle_generator: Этот репозиторий был написан на Rust. Смарт-контракты Yul могут показаться вам особенно интересными. Смарт-контракты, которые будут представлены в следующих статьях, еще больше улучшили их.

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

Заключение

В этой статье представлены основы MEV, но не вдавались в технические подробности. В следующих статьях этой серии будет объяснено все, что нужно знать, с примерами кода полной реализации Python, сопровождаемой смарт-контрактами Solidity и Yul.

Часть 1. Automated Market Makers и Uniswap V2

Эта статья является частью серии статей о разработке MEV bot. Цель этой серии — предоставить пошаговое руководство по разработке арбитражного бота. В этой статье мы расскажем об основах автоматизированных маркет-мейкеров (AMM), особенно Uniswap V2, и о том, как взаимодействовать с ними с помощью Python и Web3py.

Также будет предоставлен код Solidity для выполнения свопов в цепочке. В последнем разделе объясняется, как выполнять Flash-swaps с помощью Uniswap V2

Общее введение в AMM

Что такое AMM?

В DeFi автоматизированный маркет-мейкер (AMM) — это смарт-контракт, который хранит ликвидность в пуле и позволяет пользователям обмениваться между двумя активами.

Любой пользователь может финансировать ликвидность в обмен на долю комиссий, взимаемых с каждого свопа. Движение цены, указанной пользователям, желающим выполнить своп, определяется математической формулой, что позволяет ее предсказать. Эту формулу иногда называют кривой связи AMM.

Варианты использования

AMM являются ключевым компонентом экосистемы DeFi. Они могут обеспечить ликвидность для любой пары активов без необходимости в централизованной организации для обеспечения ценового потока. Пользователи могут взаимодействовать с протоколом без разрешений и переключаться между любыми двумя активами.

Создатели новых токенов также могут использовать AMM для начальной загрузки ликвидности для своего токена, предоставляя ликвидность самостоятельно, также без разрешения.

Это один из немногих способов получения устойчивой доходности в DeFi, поскольку сборы будут взиматься до тех пор, пока пользователи взаимодействуют с протоколом.

Первым протоколом, внедрившим AMM, был Bancor в 2017 году. С тех пор многие другие протоколы реализовали аналогичные AMM, такие как Uniswap, Curve, Balancer, Sushiswap и т. Д. Однако Uniswap является самым популярным, и на нем мы сосредоточимся в этой статье.

Юнисвап

На сегодняшний день Uniswap выпустила 3 версии своего протокола. V3 является последним, с немного более сложной системой, чем V2, но с большим объемом.

Uniswap V2 по-прежнему имеет значительный объем, несмотря на свой устаревший статус, поскольку он по-прежнему обладает большой ликвидностью. С ним очень просто взаимодействовать, и их код был разветвлен сотнями других протоколов, которые почти всегда используют один и тот же интерфейс. Это делает его хорошей отправной точкой для нашего арбитражного бота.

Uniswap V2 значительно упростил логику парных контрактов, позволив торговать любыми 2 парами токенов ERC20, а не только токенами ETH и ERC20.

Если кто-то хочет финансировать ликвидность между $RANDOM токеном и Eth, он может просто сделать это, создав пару между $RANDOM и $WETH.

WETH (Wrapped ETH) — это токен ERC20, который привязан к ETH в соотношении 1:1. Это смарт-контракт, который блокирует нативные эфиры и выдает соответствующее количество WETH. WETH можно конвертировать обратно в ETH в любое время, отправив его обратно в смарт-контракт. Действия по упаковке и распаковке эфира часто называют депозитом и снятием средств соответственно.

Вы можете получить доступ к контракту WETH здесь.

На вкладке Contract/Write Contract вы можете увидеть функции депозита и вывода, которые позволяют обернуть и развернуть эфир соответственно (обратите внимание, что deposit() принимает десятичное число эфиров в качестве аргумента. withdraw(), однако, принимает целое число вэй. 1 эфир равен 10^18 вэй).

Упаковка и распаковка WETH на etherscan.io

Uniswap V2 AMM

Uniswap V2 находится под капотом заводского контракта, который позволяет любому создать новую торговую пару для любых двух токенов ERC20.

Это развертывает новый смарт-контракт, который удерживает ликвидность для этой пары. Этот контракт называется парным контрактом. (Посмотреть транзакцию по созданию пары WETH/USDC).

Поставщики ликвидности (LP) могут затем финансировать парный контракт любым количеством обоих токенов и получать долю комиссий, взимаемых с каждого свопа.

При предоставлении ликвидности LP должен предоставить равную стоимость обоих токенов, согласно текущей цене пары. Они, в свою очередь, получают долю пула ликвидности, которая представлена токеном, называемым токеном LP. Этот токен может быть выкуплен в любое время за базовые активы, а сумма полученных активов будет пропорциональна доле пула, принадлежащей LP.

Пользователи могут переключаться между двумя токенами, отправляя один из двух токенов в парный контракт и получая другой взамен. Протокол взимает комиссию в процентах от суммы отправленного входного токена и добавляет его в пул ликвидности. На Uniswap V2 эта комиссия составляет 0,3%. В Uniswap V3 есть несколько уровней комиссий от 0,05% до 1%. Команда Uniswap оставила себе возможность переключать «переключатель платы за протокол». Это дополнительная плата, которая будет взиматься с каждого обмена. Сумма ограничена в смарт-контракте разумным значением.

Обычные пользователи часто взаимодействуют не напрямую с контрактом пары, а скорее с контрактом маршрутизатора, который будет выполнять обмен от их имени. Маршрутизатор пытается получить лучшую цену для пользователя, разделив своп между несколькими парами или даже биржами, если это необходимо.

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

Формула продукта Contant

Описание

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

Формула постоянного продукта:

x * y = k (0)

Где x и y — сумма резервов каждого токена в пуле, а k — константа. Эта константа меняется, когда провайдеры ликвидности добавляют или удаляют ликвидность из пула. Во время операции свопа это k никогда не должно изменяться, и поэтому, зная, насколько изменяется один из резервов, мы можем определить, насколько изменится другой, чтобы сохранить k постоянным.

Результаты формулы

При обмене некоторого токена x на некоторый токен y, если мы обозначим dx прибавление к резерву x, а dy результирующее изменение к резерву y, мы можем написать следующее:

(x + dx) * (y + dy) = k

Затем мы можем решить для dy:

y + dy = k / (x + dx)

dy = k/(x + dx) - y

dy = y * (x/(x + dx) - 1) (1)

Если бы пользователь вместо этого поменял какой-то токен y на какой-то токен x, мы бы получили следующее выражение для dx:

dx = x * (y/(y + dy) - 1) (2)

Обратите внимание, что dx и dy — это изменения в запасах, а не строго выход свопа. В случае свопа эти значения отрицательны, так как запасы продукции уменьшаются.

Мы также можем графически увидеть, что происходит, когда происходит своп, построив график функции y(x) = k/x. Наклон функции в любой точке равен цене пары в этой точке.

Представление формулы постоянного продукта. В выбранной точке пул с k=1 показывает цену 4/0,25 = 16. Свопы заставляют точку скользить по кривой

Чтобы лучше понять математику, мы собираемся смоделировать некоторые свопы. Помните, что большую часть времени в этой статье мы не учитываем комиссию, взимаемую протоколом.

Мы будем использовать формулу (1) с x = 2 WETH, y = 2000 USDC и k = 2 * 2000 WETHUSDC. Мы смоделируем своп 0.1 WETH на USDC. (Обратите внимание, что мы игнорируем преобразование единиц измерения. 1 WETH = 10^18; 1 USDC = 10^6)

dy = 2000 * (2/(2 + 0.1) - 1) 
dy = 2000 * (2/2.1 - 1)
dy = 2000 * (-1/21)
dy = -95.24

В результате свопа пользователь получит 95,24 USDC в обмен на 0,1 WETH. Пул остается с резервами в размере 2,1 WETH и 1904,76 USDC.

Обратите внимание, что цена до исполнения была 2000/2 = 1000 USDC/WETH Цена после исполнения составляет 1904,76/2,1 = 907,98 USDC/WETH. Цена была перемещена сделкой, и пользователь получил худшую цену, чем та, которая отображается в пользовательском интерфейсе. Это называется влиянием на цену.

Средняя цена исполнения свопа составляет p = выход/вход = 95,24/0,1 = 952,4 USDC/WETH.

Если сразу после обмена 0.1 WETH на USDC, мы получим следующее:

dy = 1904.76 * (2.1/(2.1 + 0.1) - 1)
dy = -86.58

На этот раз мы получили 86,58 USDC вместо 95,24 USDC.

Средняя цена исполнения стала p = 86,58/0,1 = 865,8 USDC/WETH.

Мы можем наблюдать, что цена пары не меняется линейно на протяжении всего свопа.

Цена всегда движется в том направлении, что делает своп в том же направлении более дорогим. Ввод большого количества токенов приведет к худшей цене, чем ввод небольшого количества токенов.

Разделение свопа на несколько меньших последовательных свопов не приведет к лучшей общей цене. Если бы мы обменяли 0.2 WETH на USDC, мы бы получили:

dy = 2000 * (2/(2 + 0.2) - 1)
dy = -181.82

Мы получили 181.82 USDC. Ранее мы получили 95,24 + 86,58 = 181,82 USDC, разделив своп на два меньших свопа, с тем же результатом.

Таким образом, постоянные AMM продукта проявляют независимость от траектории.

Разделение свопа на несколько более мелких свопов, разделенных другими свопами, определенно приведет к лучшей цене, поскольку мы можем ожидать, что арбитражеры сбросят цену до значения, близкого к стартовой цене. Сумма, подлежащая обмену, должна быть достаточно большой, чтобы дополнительные расходы на газ того стоили.

Последнее замечание заключается в том, что всякий раз, когда цена одного из токенов в пуле сильно меняется по сравнению с другим (что очень часто случается в криптовалюте), пул будет постепенно истощаться наиболее ценными токенами, поскольку пользователи захотят приобрести его через свопы с токеном с более низкой стоимостью. Когда провайдеры ликвидности удаляют свою ликвидность (игнорируя начисленные комиссии), стоимость токенов, которые они получат, будет иметь тенденцию состоять в большей степени из токенов с более низкой стоимостью, что приведет к потере капитала. Это называется непостоянной потерей.

Стоимость позиции в зависимости от относительной дивергенции цен токенов

Вы можете найти краткий вывод формулы непостоянных потерь в этой статье Питериса Эринса.

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

Считывание цены бассейна

Теперь, когда мы знаем, что цена пары (или, взаимозаменяемо, пула) равна стоимости резервов одного токена, деленной на другой, все, что нам нужно сделать, это получить эти значения из парного контракта.

Контракты V2 предоставляют функцию getReserves(), которая возвращает резервы двух токенов в паре (вместе с меткой времени последнего изменения резервов, но мы не будем ее использовать).

Мы будем использовать Python и web3.py для чтения резервов пары в основной сети Ethereum. Обратите внимание, что для выполнения этого кода требуется конечная точка RPC узла. Infura, Alchemy и QuickNode предоставляют бесплатные конечные точки.

Чтобы легко взаимодействовать с развернутым кодом, инструменты/библиотеки смарт-контрактов используют ABI (двоичные интерфейсы приложений). Это JSON-файлы, которые описывают функции и события смарт-контракта. Эти ABI генерируются компилятором Solidity для облегчения взаимодействия. Продвинутые пользователи могут взаимодействовать без ABI, но это делает процесс немного более утомительным.

Простой способ получить ABI проверенного контракта — использовать Etherscan:

ABI контракта UniswapV2Pair

Вы можете найти ABI контракта UniswapV2Pair на паре WETH-USDT, раздел ABI контракта.

Следующий фрагмент кода выведет два резерва:

from web3 import Web3
import json

# Connect to a node. If using Infura, it should look like this:
w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/<YOUR_INFURA_PROJECT_ID>'))

# Load the ABI of the UniswapV2Pair contract
with open('UniswapV2Pair.json', 'r') as f:
pairABI = json.load(f)

# Create a contract object with the pair address.
pair = w3.eth.contract(address='0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852', abi=pairABI)

# Call the getReserves() function
reserves = pair.functions.getReserves().call()

# Print the reserves
print(reserves)

Получаем следующий вывод:

[16213967662142758453773, 30340153518332, 1685519663]

Первые два значения — это резервы WETH и USDT соответственно, а последнее значение — это временная метка последнего изменения резервов. Помните, что смарт-контракты всегда рассуждают в wei при ведении учета токенов, а не в обычных единицах.

Для любого токена, чтобы преобразовать это значение в обычную единицу, нам нужно разделить на 10^десятичные дроби. Для WETH десятичные дроби = 18, поэтому нам нужно разделить на 10¹⁸. Для USDT десятичные дроби = 6, поэтому нам нужно разделить на 10⁶. В обычных подразделениях резервы составляют:

WETH: 16213967662142758453773 / 10^18 = 16,213.97 $WETH
USDT: 30340153518332 / 10^6 = 30,340,153.52 $USDT

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

Вы можете получить атрибут decimals любого токена ERC20, вызвав функцию decimals() контракта токена:

# [...]

# Load the ABI of the ERC20 contract
with open('ERC20ABI.json', 'r') as f:
ercABI = json.load(f)

# Create a contract object with the token address. Here we used USDT.
token = w3.eth.contract(address='0xdAC17F958D2ee523a2206206994597C13D831ec7', abi=ercABI)

# Call the decimals() function
decimals = token.functions.decimals().call()

# Print the decimals
print(decimals)

Получаем следующий вывод:

6

Как и ожидалось, десятичные дроби USDT равны 6.

Последняя информация, которую следует запомнить, заключается в том, какой токен является token0 и token1, имея соответственно reserve0 и reserve1.

Глядя на код создания пары заводских контрактов, мы видим следующее:

function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
// [...]
}

Мы видим, что токен с самым младшим/наименьшим адресом — token0, а другой — token1. В нашем случае WETH имеет самый низкий адрес, поэтому это token0, а USDT — token1.

Выполнение свопа

Ядро арбитражного бота заключается в выполнении свопов нужного размера между нужными пулами токенов.

Мы увидим, как выполнить своп на Uniswap V2 из контракта, развернутого с помощью Remix, эталонной веб-среды Solidity IDE. Транзакция будет отправлена на локальный форк основной сети Ethereum с использованием Anvil, локального узла тестовой сети Foundry.

Код Python будет использоваться для взаимодействия с узлом.

Своп в Solidity

Прежде чем взаимодействовать со смарт-контрактом, всегда полезно взглянуть на код контракта. Переходя к размещенному на github коду контракта UniswapV2Pair, мы видим следующую функцию:

// this low-level function should be called from a contract which performs important safety checks
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}

_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

Не вдаваясь в подробности, можно увидеть, что функция принимает 4 аргумента:

  • amount0Out: количество token0, которое будет отправлено пулом получателю toкак всегда в wei)
  • amount1Out: количество token1, которое будет отправлено пулом получателю to
  • to: адрес получателя выходных токенов свопа
  • data: необязательный параметр (чтобы игнорировать, передайте пустой массив байтов). Используется для подкачки флэш-памяти, которую мы увидим далее в статье.

Обратите внимание, что параметры amount0In и amount1In отсутствуют. Входные значения должны быть вычислены вызывающей стороной, все, что делает пул, это следит за тем, чтобы соблюдалась формула постоянного произведения (на самом деле, она допускает своп, если результирующая ликвидность k в пуле больше или равна, чем раньше).

Входные токены должны быть отправлены в пул перед вызовом функции swap() Затем пул отправит выходные маркеры получателю еще до того, как он проверитto было отправлено достаточное количество входных маркеров. Это необходимо для того, чтобы разрешить замену флэш-памяти.

Наивный разработчик может попытаться сначала отправить входные токены в транзакции, а затем вызвать функцию swap() в другой транзакции. Однако это почти наверняка приведет к потере средств, так как не будет никакой гарантии, что ни один внешний субъект не вызовет функцию swap() до того, как будет добыта вторая транзакция. Функция swap() должна вызываться в той же транзакции, что и та, в которой входные токены отправляются в пул.

Следующий смарт-контракт должен быть вызван с пересылкой входного Eth в качестве значения вызывающей транзакции. Когда функция startSwap()() вызывается с предварительно вычисленной суммой вывода, она упаковывает полученный в транзакции ETH в WETH, отправляет его в пул и вызывает функцию swap() с правильной суммой вывода. Затем пул отправит выходные токены USDT на счет, отправивший транзакцию.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; // Any Solidity 0.8.x version

interface IUniswapV2Pair { // Using interfaces has the same role as using ABIs in web3.py
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
interface IWETH { // To simply the code, we only include the functions we use in the interface declaration
function deposit() external payable;
}
interface IERC20 {
function transfer(address recipient, uint256 amount) external returns (bool);
}
contract TestSwap {
// Store the addresses used as constants for readability. This does not cost any gas.
address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant UNI_PAIR = 0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852;
// Define the function the will be called by web3.py
function startSwap(uint usdtOut) external payable {// External specifies that this function is called from outside this contract. The payable keyword makes the function capable of receive ETH when called.
// Wrap the ETH received in the transaction into WETH. The amount of ETH received is available in msg.value
// We call the WETH contract's deposit() function and forward the same amount of ETH we received in the transaction
IWETH(WETH).deposit{value: msg.value}(); // The {value: msg.value} syntax is used to forward ETH to a contract when calling it. The parameters are still between parentheses (here there are none).
// Now that we have WETH, we can send it to the Uniswap Pair contract.
IERC20(WETH).transfer(UNI_PAIR, msg.value); // We use the ERC20 transfer() function. The amount of WETH is the same as the ETH received.
// Just like with the WETH contract, we enclose the address with the interface we want to use, and call the function of interest.
// Remember that USDT is the token1 of the pair. We don't want to swap out any ETH, so we pass 0 as the first parameter.
// To specify the current contract as the recipient of the output, we could have used address(this) instead of msg.sender. Here we sent the output to the address that called the current transaction.
IUniswapV2Pair(UNI_PAIR).swap(0, usdtOut, msg.sender, new bytes(0)); // The last parameter is the data parameter, which we don't use here. We pass an empty bytes array.
}
}

Обратите внимание, что контракт бота всегда должен иметь необходимый WETH в контракте заранее (или использовать флэш-свопы/займы), но не оборачиваться одновременно с выполнением свопов. Это сделано для того, чтобы избежать оплаты затрат на газ для обертывания Eth в арбитражную транзакцию, что делает бота неконкурентоспособным.

Этот смарт-контракт будет развернут на локальном разветвленном узле. Это узел, который имитирует параллельный блокчейн Ethereum локально на вашем компьютере. Ему необходимо подключиться к внешнему «реальному» узлу, чтобы получить внешние данные, такие как контракт WETH или контракт Uniswap Pair. Локальный узел выбирает конкретный блок, после чего в него не будут входить данные из реальных транзакций, а только локальные, которые вы произвели. Это блестящий инструмент для тестирования смарт-контрактов, которые взаимодействуют с внешними контрактами, поскольку вы можете имитировать любое состояние блокчейна, которое захотите.

В настоящее время рекомендуемым способом создания локального разветвленного узла будет использование Anvil.

Anvil поставляется в комплекте с Foundry, полным набором инструментов разработки для блокчейнов, совместимых с Ethereum (совместимых с EVM).

Чтобы установить Foundry в Windows, необходимо включить WSL или установить Git Bash. Затем в терминале Git Bash или терминале WSL выполните следующие команды:

curl -L https://foundry.paradigm.xyz | bash

foundryup

Теперь вы сможете выполнить следующую команду, чтобы запустить локальный разветвленный узел:

anvil --fork-url https://mainnet.infura.io/v3/<YOUR_INFURA_PROJECT_ID>

Вы должны увидеть это в своей консоли:

Стартовое послание Наковальни

Обратите внимание, что узлы Infura не поддерживают режим архивации для учетных записей уровня free. Это означает, что вы не сможете получать данные из блоков старше 128 блоков назад. Чтобы решить эту проблему, просто перезапустите локальный узел. Он будет использовать последний блок в качестве отправной точки.

Для развертывания смарт-контракта, который мы только что написали, мы будем использовать Remix. Создайте новый файл кода, вставьте приведенный выше код и скомпилируйте его, чтобы проверить наличие ошибок.

После успешной компиляции перейдите на вкладку «Развертывание и запуск». Чтобы развернуть контракт на локальном разветвленном узле, а не на виртуальной машине JavaScript (у которой нет контрактов Uniswap), выберите Dev - Foundry Provider в качестве среды. Введите URL-адрес (вероятно, он http://localhost:8545).

Список выбора узлов
Параметры узла литейного производства

Теперь вы сможете развернуть контракт. Консоль Remix должна показать данные о транзакции.

Следующий код Python можно использовать для вызова функции startSwap() смарт-контракта, приведенного выше. Он использует web3.py, которая является эквивалентом web3.js на Python. На счете, используемом для подписания транзакции, должно быть достаточно ETH для финансирования свопа и оплаты газа.

Web3.py отправляет транзакцию узлу, указанному в объекте w3. Здесь мы используем локальный узел Anvil, но это может быть любой узел, включая Infura. Для тестирования рекомендуется использовать локальный узел, так как он быстрее и не стоит газа.

Комментарии предоставляются по всему коду, чтобы объяснить, что он делает.

Убедитесь, что вы установили необходимые библиотеки с pip install web3 eth_account.

Измените параметры в верхней части скрипта в соответствии с вашими настройками.

from web3 import Web3
from eth_account import Account
import json

##### Parameters/constants of the script #####
# Url of the local Anvil node
NODE_URL = "http://localhost:8545"

# Private key of the account that will sign the transaction.
# Anvil gives private keys of test accounts that have a lot of ETH in them already.
# This example PK probably has 10000 ETH in it.
SENDER_PK = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

# Address of the WETH contract. Don't forget to use checksum addresses (Have capital letters in them. You can use Web3.toChecksumAddress() to convert an address to checksum format, or copy it from EtherScan)
WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"

# USDT ERC20 contract address
USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7"

# Address of the Uniswap WETH-USDT pair
UNI_PAIR = "0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852"

# Input amount of ETH to swap. 1 ETH = 10^18 wei
ETH_INPUT = 10**18 # Python returns an integer. Careful not to use floats, as will be the case with other languages.

# Fee taken by Uniswap
UNI_FEE = 0.003 # 0.3%

# TestSwap contract address, as given by Remix after deployment, or in the Anvil console log.
TESTSWAP_ADDRESS = "0x9D3DA37d36BB0B825CD319ed129c2872b893f538"
##############################################

# Connect to the local Anvil node
w3 = Web3(Web3.HTTPProvider(NODE_URL))

# The account used to sign the transaction. Created from the private key given. Anvil gives private keys of test accounts that have a lot of ETH.
signer = Account.from_key(SENDER_PK)
address = signer.address # Calculate the address from the private key
print("Using address:", address)

# First we will find the reserves of the Uniswap pair contract. We need to do this because we need to know how much USDT we will get for the ETH we send.
# Load the ABI. You can save it as a JSON file, or you can copy it directly, as we do here. Note that we use an incomplete ABI, as only need the getReserves() function.
# pairABI = json.load(open("UniswapV2Pair.json", "r"))
pairABI = [
{
"constant": True,
"inputs": [],
"name": "getReserves",
"outputs": [
{
"internalType": "uint112",
"name": "_reserve0",
"type": "uint112"
},
{
"internalType": "uint112",
"name": "_reserve1",
"type": "uint112"
},
{
"internalType": "uint32",
"name": "_blockTimestampLast",
"type": "uint32"
}
],
"payable": True,
"stateMutability": "view",
"type": "function"
}
]

# Create a contract object for the Uniswap pair contract.
pairContract = w3.eth.contract(address=UNI_PAIR, abi=pairABI)

# Call the getReserves() function of the Uniswap pair contract.
pairReserves = pairContract.functions.getReserves().call()

# Use the formula (1) to calculate dy, the amount of USDT we will get for the ETH we send.
# Remember that Uniswap V2 takes a 0.3% fee on the input amount.
# dy = y * (x/(x + dx) - 1) (1)
x = pairReserves[0] # x is the amount of WETH in the pair
y = pairReserves[1] # y is the amount of USDT in the pair
dx = ETH_INPUT * (1 - UNI_FEE) # dx is the amount of WETH that will be used by the constant product formula
usdtOutput = y * (1 - x/(x + dx)) # dy is the amount of USDT we will get for the ETH we send
usdtOutput = int(usdtOutput) # Truncate into integer, as the formula returns a float and we must never overestimate the amount, as it would make the swap fail. We use abs() to make the number positive as the formula returns the reserve difference for the pool.
print("Expected USDT output:", usdtOutput)

# Get the ABI of the TestSwap contract. The ABI can be copied from Remix.
# contractABI = json.load(open("TestSwapABI.json", "r")
contractABI = [
{
"inputs": [
{
"internalType": "uint256",
"name": "usdtOut",
"type": "uint256"
}
],
"name": "startSwap",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
]
# Create a contract object from the ABI and address of the TestSwap contract. The ABI can be copied from Remix.
testswap = w3.eth.contract(address=TESTSWAP_ADDRESS, abi=contractABI)

# Create a transaction object from the function we want to call and its parameters.
# Here we call the startSwap() function, and pass the output usdt amount we calculated above as a parameter.
txn = testswap.functions.startSwap(usdtOutput).buildTransaction({
'chainId': 1, # Chain ID of the Node. Ganache uses 1337, Anvil uses 1.
'gas': 500000, # Gas limit.
'gasPrice': w3.eth.gas_price, # Gas price. We use the gas price of the node.
'nonce': w3.eth.get_transaction_count(address), # Nonce is the number of transactions sent from the account. It is used to prevent replay attacks.
'value': ETH_INPUT # The amount of ETH we send to the contract. We use the same amount as the amount of ETH we want to swap.
})
# Sign the transaction with the private key of the account.
signed_txn = w3.eth.account.sign_transaction(txn, private_key=SENDER_PK)

# Send the transaction to the node.
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
print("Tx hash:", tx_hash.hex())
print("Waiting for transaction to be mined...")
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print("Transaction mined in block", tx_receipt.blockNumber)
print("Transaction status:", tx_receipt.status, "(Success)" if tx_receipt.status == 1 else "(Failure)")
print("Gas used:", tx_receipt.gasUsed)

Когда все настроено правильно, вы должны увидеть следующий вывод:

Using address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Expected USDT output: 1855484230
Tx hash: 0xefa0800fdb19cc823c5705e12729a9f18e8df8919b4240b6eb2cfee52af36eb5
Waiting for transaction to be mined...
Transaction mined in block 17381684
Transaction status: 1 (Success)
Gas used: 106897

Интересная функция: Flash swaps

Читателей с небольшим опытом может заинтересовать интересная функция Uniswap V2: Flash swaps. Обратите внимание, что функцию swap() контракта UniswapV2Pair можно разложить на 4 шага:

  1. Перечисление запрошенных выходных сумм получателю
  2. Обратный вызов контракта отправителя и передача параметра data
  3. Проверка инварианта
  4. Обновление состояния парного контракта

На первый взгляд это может показаться небезопасным, так как средства отправляются до проверки инварианта. Имейте в виду, что несоблюдение инварианта приведет к отмене транзакции. Когда транзакция отменяется, все изменения состояния, включая перевод средств, отменяются, гарантируя безопасность средств.

Этот механизм называется оптимистическим переносом. Вызывая функцию обратного вызова по адресу вызывающего абонента, Uniswap V2 разрешает так называемые флэш-кредиты (или флэш-свопы): выходные средства ссужаются вызываемому, который волен делать с ними все, что захочет, при условии, что они возвращаются до инвариантной проверки. Примером использования такой функции является арбитраж больших сумм средств между различными биржами, когда у контракта бота недостаточно средств для выполнения арбитража.

Обратите внимание, что это потребляет больше газа и может сделать вашего бота неконкурентоспособным.

Вот упрощенный пример Solidity о том, как реализовать флэш-своп в вашем смарт-контракте на Uniswap V2. Код предполагает, что арбитраж между двумя пулами возможен, и выполняет его без необходимости иметь средства в контракте.

// [...]

// The modifications to your contract must conform to this interface.
interface IUniswapV2Callee {
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
}

// The "is" keyword means that the contract inherits from the IUniswapV2Callee interface.
contract TestSwap is UniswapV2Callee {
// We provide more parameters to the startSwap() function, to be able to pass them to the swap() function, which in turn will pass them to the callback function. PairB is the address of the second V2-compatible pair.
function startSwap(address pairA, address pairB, uint wethInPairA, uint usdtOutPairA, uint wethOutPairB) external payable {
// [...]
// We convert the parameters into a bytes array, to be able to pass them to the swap() function. Note that using abi.encode() is easy but note very gas efficient; abi.encodePacked() is more efficient but must be decoded manually.
bytes memory data = abi.encode(pairB, wethInPairA, wehtOutPairB);
// We send the USDT funds to the second pair, so that we can demand the WETH output, that will repay the USDT loan of the first swap.
IUniswapV2Pair(pairA).swap(0, usdtOutPairA, pairB, data);
// The invariant should be respected at this point.
// We should have profited of wethOutPairB - wethInPairA WETH.
}
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external override {
// This function is called by the UniswapV2Pair contract, in the middle of the swap function.
// When in the callback function, there is no way to access information transmitted to the startSwap() function, the info accessible is:
// - The address of the sender of the swap() function. Note that here, msg.sender is the UniswapV2Pair contract, not the address of the user who called startSwap().
// - The values token0Out and token1Out are transmitted as is, under the names amount0 and amount1.
// - The data parameter is the same as the one passed to the swap() function. It can contain arbitrary information.
// We unpack the data needed to perform the second swap
(address pairB, uint wethInPairA, uint wethOutPairB) = abi.decode(data, (address, uint, uint));

// We perform the second swap. We receive the WETH on this contract, and repay the USDT loan in WETH before the invariant check of the first pool (which will happen right when we return from the current function).
IUniswapV2Pair(pairB).swap(wethOutPairB, 0, address(this), new bytes(0)); // No data to pass, no need to loan the funds of this swap.
// Transfer the WETH needed to repay the flash loan, and keep the difference. The address of the first pool is the caller of the current function (msg.sender).
IERC20(WETH).transfer(msg.sender, wethInPairA);
}
}

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

Заключение

Эта длинная статья охватывала много информации, необходимой для выполнения свопов на Uniswap V2. Мы видели, как получить резервы и цену пула, как использовать формулу постоянного продукта для вычисления количества токенов, полученных из свопа V2 при определенной входной сумме, как выполнить своп с помощью контракта UniswapV2Pair и как флэш-свопы могут быть реализованы в Uniswap V2.

Часть 2. Эффективное считывание цен пула

Эта статья является частью серии статей о построении арбитражного бота. В этой статье будет представлен реалистичный и практичный способ чтения цен на пулы V2-совместимых DEX, таких как Uniswap V2 и SushiSwap.

Чтение фабричных пар

В предыдущей статье мы видели, как читать резервы одной пары Uniswap V2. Тем не менее, на Uniswap есть 100 000 пар и еще больше на других V2-совместимых DEX. Считывать цены одну за другой невозможно, нужен способ прочитать их все сразу.

И еще до того, как читать резервы пары, нам нужно знать адрес парного контракта.

Заводской контракт События

К счастью, команда Uniswap подумала об этом и сделала заводской контракт V2 событием, когда создается новая пара. Вот фрагмент кода Solidity заводского контракта:

function createPair(address tokenA, address tokenB) external returns (address pair) {
// [...]
// Deploy a new UniswaV2Pair contract, store its address in `pair`
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
// [...]
// Emit a PairCreated event
emit PairCreated(token0, token1, pair, allPairs.length);
}

Нам просто нужно получить события PairCreate смарт-контракта.

Считывать события из смарт-контракта очень просто. Ниже приведен пример кода, который может считывать события заводского контракта на Python с библиотекой web3.py:

from web3 import Web3

# Connect to a local node
w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/<YOUR_INFURA_PROJECT_ID>'))

# Define the contract address
contract_address = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' # Uniswap V2 factory

# Define the contract ABI
factory_abi = [
{
"anonymous": False,
"inputs": [
{
"indexed": True,
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"indexed": True,
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"indexed": False,
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"indexed": False,
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "PairCreated",
"type": "event"
}
]

# Instantiate the contract
factory_contract = w3.eth.contract(address=contract_address, abi=factory_abi)

# Get events from the contract
events = factory_contract.events.PairCreated().createFilter(fromBlock='0x0', toBlock='latest').get_all_entries()
print(f'Found {len(events)} events')

Однако из-за ограничения свободных узлов Infura этот код не будет работать, если будет возвращено более 10 000 событий. Это проблема, потому что заводской контракт выдал более 200 тысяч событий.

Чтобы решить эту проблему, мы можем реализовать рекурсивную функцию, которая будет разбивать запрос на несколько меньших подзапросов каждый раз, когда один запрос завершается сбоем, и объединять результаты. Вот пример такой функции:

# Dichotomic recursive function to fetch events between two givenpy blocks
def getEventsRecursive(contract, _from, _to):
# Infura throws an error if we query too many blocks at once (10k), so when that happens, we split the query in 2 sub-queries.
try:
events = contract.events.PairCreated().createFilter(fromBlock=_from, toBlock=_to).get_all_entries()
print("Found ", len(events), " events between blocks ", _from, " and ", _to)
return events
except ValueError:
print("Too many events found between blocks ", _from, " and ", _to)
midBlock = (_from + _to) // 2
return getEventsRecursive(contract, _from, midBlock) + getEventsRecursive(contract, midBlock + 1, _to)

Эта функция может быть вызвана так:

events = getEventsRecursive(factory_contract, 0, w3.eth.blockNumber)

Он вернет следующие выходные данные:

Too many events found between blocks  0  and  17414678
Found 0 events between blocks 0 and 8707339
Too many events found between blocks 8707340 and 17414678
[...]
Found 7005 events between blocks 17278627 and 17346652
Found 5996 events between blocks 17346653 and 17414678
194427

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

Обратите внимание, что эта функция запускается примерно за 5 минут! Хорошей новостью является то, что нет необходимости запускать эту функцию очень часто. Одного раза в 24 часа, в фоновом режиме, более чем достаточно.

V2-совместимые DEX

Теперь, когда мы знаем, как читать пары фабрики Uniswap V2, чтение пар любой V2-совместимой DEX тривиально: почти все V2-совместимые DEX дословно скопировали большую часть кода Uniswap V2, включая заводской контракт.

Чтобы получить пары на другой DEX, нам просто нужно изменить адрес заводского контракта и ABI. Вот пример для SushiSwap:

# [...]
contract_address = '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac' # SushiSwap factory ethereum mainnet
# [...]
events = factory_contract.events.PairCreated().createFilter(fromBlock='0x0', toBlock='latest').get_all_entries()
print(f'Found {len(events)} events')

Это возвращает следующие выходные данные через 5 секунд:

Found  3692  events

Обратите внимание, что количество пар намного меньше, чем на Uniswap. Это верно для любой другой DEX. Нам не нужно было выполнять рекурсивные дихотомические запросы для SushiSwap.

Существует гораздо больше V2-совместимых DEX. Вот стартовый список адресов заводских контрактов некоторых других DEX:

{
"uniV2": {
"uniswapV2": {
"factory": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
},
"sushiswapV2": {
"factory": "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac"
},
"shibaswapV2": {
"factory": "0x115934131916C8b277DD010Ee02de363c09d037c"
},
"croswapV2": {
"factory": "0x9DEB29c9a4c7A88a3C0257393b7f3335338D9A9D"
},
"convergenceV2": {
"factory": "0x4eef5746ED22A2fD368629C1852365bf5dcb79f1"
},
[...]
}
}

Другие перечислены в этой github gist.

Рединг Резервы

Теперь, когда у нас есть огромный список пар для отслеживания, нам нужно прочитать их резервы токенов. Резервы, к сожалению, хранятся не в заводском контракте, а в самих парных контрактах. Это означает, что нам нужен способ эффективного вызова getReserves() каждого парного контракта.

Вспомогательные контракты

Вместо того, чтобы выполнять тысячи вызовов RPC, мы можем сделать один большой вызов специального смарт-контракта, который мы развернули сами заранее.

В вызове мы предоставляем массив адресов пар, и во время выполнения контракта он сам будет делать вызовы getReserves() каждого парного контракта, и возвращать результаты в массиве. Это намного эффективнее, поскольку позволяет избежать накладных расходов на сеть, поскольку вся логика упакована в контекст EVM.

Договор заключается в следующем:

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8;

// Define an interface for the pair contracts, that contains the function we want to call
interface IUniswapV2Pair {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}

// Batch query contract
contract FlashBotsUniswapQuery {
function getReservesByPairs(IUniswapV2Pair[] calldata _pairs) external view returns (uint256[3][] memory) {// The calldata keyword is a small optimization that tells the compiler that the array will not be modified
uint256[3][] memory result = new uint256[3][](_pairs.length);
for (uint i = 0; i < _pairs.length; i++) {
(result[i][0], result[i][1], result[i][2]) = _pairs[i].getReserves();
}
return result;
}
}

Как видите, он принимает в качестве входных данных массив парных адресов. Одновременная передача слишком большого количества адресов может привести к сбою eth_call из-за ограничения времени ожидания (5 секунд в Infura), поэтому нам нужно разделить массив на более мелкие фрагменты. Как это сделать, мы увидим позже. Это немного подрывает эффективность пакетного запроса, но все же намного эффективнее, чем тысячи вызовов RPC.

Затем getReservesByPairs()() перебирает массив адресов пар и вызывает функцию getReserves() каждого парного контракта. Он возвращает массив массивов, где каждый подмассив содержит резервы пары.

Этот смарт-контракт можно найти почти как есть в репозитории простых арбитражных ботов Flashbots.

Версию, оптимизированную для сборки, можно найти в конце статьи. Обратите внимание, однако, что улучшение скорости незначительно, и что версия сборки возвращает сглаженный массив резервов вместо 2D-массива версии Solidity.

Контракт должен быть развернут в основной сети (например, с помощью Remix), а адрес должен быть сохранен как queryContractAddress в приведенном ниже коде.

Использовать этот контракт очень просто. Вот код, который его вызывает:

# Fetch the list of pairCreated events
# [...]
events = getEventsRecursive()

# Convert the events to a list of dictionaries
pairDataList = [
{
'token0': e['args']['token0'],
'token1': e['args']['token1'],
'pair': e['args']['pair']
} for e in events]

# Use web3py to call the getReservesByPairs() function on the deployed contract
queryContractAddress = "0x6c618c74235c70DF9F6AD47c6b5E9c8D3876432B" # The address of the contract you deployed on Remix
queryAbi = [
{
"inputs": [
{
"internalType": "contract IUniswapV2Pair[]",
"name": "_pairs",
"type": "address[]"
}
],
"name": "getReservesByPairs",
"outputs": [
{
"internalType": "uint256[3][]",
"name": "",
"type": "uint256[3][]"
}
],
"stateMutability": "view",
"type": "function"
}
]

# Create contract object
queryContract = w3.eth.contract(address=queryContractAddress, abi=queryAbi)

# Format the pairs dict into an address list
pairAddrList = [p['pair'] for p in pairDataList]

# The following will fail with the 200k Uniswap V2 pairs
reserves = queryContract.functions.getReservesByPairs(pairAddrList).call()

Этот сценарий должен возвращать результат, аналогичный следующему:

[[156362, 7, 1637535630], [1108183983306794770482713, 187818480, 1682759063],...]

Адрес контракта 0x6c618c74235c70DF9F6AD47c6b5E9c8D3876432B — это адрес контракта, который я лично развернул на Remix. Вы можете использовать его, если хотите избежать уплаты платы за газ для развертывания собственного контракта.

Обратите внимание, что этот скрипт, скорее всего, будет работать как есть только для небольших форков Uniswap V2 из-за того, что в них развернуто гораздо меньше пар токенов. Поставщики узлов RPC часто устанавливают жесткое ограничение на продолжительность вызовов контракта. Для Infura этот предел составляет 5 секунд выполнения.

Даже если бы этого предела не было, 5 секунд — это слишком большая задержка, чтобы получить резервы пар. Новый блок создается каждые 12 секунд на Ethereum. Принимая во внимание задержку поставщика ноды, задержку отправки финального пакета транзакций и время вычислений, которое потребуется алгоритму поиска возможностей (подробнее об этом в следующей статье), необходимо принять меры для оптимизации этой части.

Оптимизация запросов

Асинхронные запросы

Простой способ решить проблему задержки/тайм-аута — разрезать список адресов пар, который мы передаем узлу, на более мелкие фрагменты и отправить их асинхронно на узел параллельно.

Последние версии Web3py позволяют использовать асинхронное программирование при выполнении запросов к узлу с небольшими изменениями в исходном коде. Вот как мы могли бы это сделать:

# Add additional imports
import asyncio
from web3 import AsyncHTTPProvider
from web3.eth import AsyncEth
# If you test this code in a Jupyter notebook, sligh modifications are needed like nest_asyncio.apply() (google for more info)
# [...]

# Create a function that takes a list of pair data, and returns a list of reserves for each pair
async def getReservesAsync(pairDataList, chunkSize=1000):
# Create an async web3 provider instance
w3Async = Web3(AsyncHTTPProvider(NODE_URI), modules={'eth': (AsyncEth)})
# Create contract object
queryContract = w3Async.eth.contract(address=queryContractAddress, abi=queryAbi)
# Create a list of chunks of pair addresses
chunks = [[pair["pair"] for pair in pairDataList[i:i + chunkSize]] for i in range(0, len(pairDataList), chunkSize)]
# Gather all the async tasks
tasks = [queryContract.functions.getReservesByPairs(pairs).call() for pairs in chunks]
# Run the tasks in parallel
results = await asyncio.gather(*tasks)
# Return the results
return results

# Call the function
reserves = asyncio.run(getReservesAsync(pairDataList))

Этот код должен возвращать большой список списков, каждый из которых содержит резервы пары. Результат должен выглядеть следующим образом:

# [[reserve0, reserve1, timestamp], [...], ...]
[[807568438013832803913, 592101254, 1686247763], [...], ...]

Этот код будет работать очень хорошо, но будут соблюдены другие ограничения скорости API, такие как максимальное количество одновременных запросов к одному узлу или ограничение запросов в 24 часа.

Балансировка узлов

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

Мы отредактируем функцию getReservesAsync() чтобы отправить запросы ..call() на несколько узлов и вернуть первый полученный результат.

Узлы будут заключены в объекты Web3 AsyncHTTPProvider. Вот изменения, внесенные в начало скрипта:

# Define a list of node URIs
NODE_URIS = [
# Infura nodes
"https://mainnet.infura.io/v3/INFURA_PROJECT_ID",
"https://mainnet.infura.io/v3/INFURA_PROJECT_ID",
"https://mainnet.infura.io/v3/INFURA_PROJECT_ID",
# Alchemy nodes
"https://eth-mainnet.alchemyapi.io/v2/ALCHEMY_PROJECT_ID",
# Quiknode nodes
"https://eth-mainnet.quiknode.pro/QUICKNODE_PROJECT_ID/",
#
[...]
]
providerList = [Web3(AsyncHTTPProvider(uri), modules={'eth': (AsyncEth)}) for uri in NODE_URIS]

Затем getReservesAsync() изменяется для использования списка поставщиков:

async def getReservesParallel(pairDataList, providers, chunkSize=1000):
# Create the contract objects
contracts = [provider.eth.contract(address=queryContractAddress, abi=queryAbi) for provider in providers]

# Create a list of chunks of pair addresses
chunks = [[pair["pair"] for pair in pairDataList[i:i + chunkSize]] for i in range(0, len(pairDataList), chunkSize)]

# Assign each chunk to a provider in a round-robin fashion
tasks = [contracts[i % len(contracts)].functions.getReservesByPairs(pairs).call() for i, pairs in enumerate(chunks)]

# Run the tasks in parallel
results = await asyncio.gather(*tasks)
return results

# Call the function
reserves = asyncio.run(getReservesParallel(pairDataList, providerList))

Теперь этот код должен разделять запросы между любым количеством узлов, указанным в NODE_URIS.

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

Этот код также несет расходы, связанные с использованием библиотеки web3py, которая выполняет множество проверок и дополнительных операций. Сокращенная версия этого кода будет предоставлена позже в этой серии. Он вручную отправляет запросы JSON-RPC на узлы и обрабатывает двоичные результаты, что сокращает немалое время.

Последнее замечание заключается в том, что даже после всех этих оптимизаций масштабирования запрос 200k + пар каждого блока все еще может быть невозможен. К счастью, огромная часть этих пар включает в себя бесполезные шиткоины, которые можно безопасно отфильтровать.

Бонус: оптимизация вспомогательного контракта при сборке

Ниже приведена попытка оптимизировать вспомогательный контракт в сборке, чтобы снизить затраты на газ функции getReservesByPairs()

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8;

contract UniswapFlashQuery {
function getReservesByPairsAsm(address[] calldata _pairs) external view returns (bytes32[] memory) {
// Note that the array has been flattened, handling 2D arrays in assembly is much more complex.
bytes32[] memory result = new bytes32[](_pairs.length * 3);

assembly {
// Size of the return data (reserve0, reserve1, blockTimestampLast)
let size := 0x60 // Values are not packed, so 3 * 32 bytes = 3 * 0x20 = 0x60 bytes

// Allocate memory for the function selector
let callData := mload(0x40)
mstore(callData, 0x0902f1ac00000000000000000000000000000000000000000000000000000000) // 4-byte function selector of the getReserves() function. Note the padding to the right.
mstore(0x40, add(callData, 0x04)) // Update the free memory pointer

for { let i := 0 } lt(i, _pairs.length) { i := add(i, 1) } {
// Load the pair address from the calldata
let pair := calldataload(add(_pairs.offset, mul(i, 0x20)))

// Call the getReserves() function, write the return data to the preallocated memory for the "result" array.
let success := staticcall(gas(), pair, callData, 0x04, add(add(result, 0x20),mul(i, size)), size)
}

// Update the free memory pointer
mstore(0x40, add(mload(0x40), mul(_pairs.length, size)))
}

return result;
}
}

Этот код пытается выполнить как можно меньше операций в цикле.

Проверки удаляются, как обычно, if iszero(success) { revert() } после CALL, чтобы избежать ветвления в цикле.

Чтобы сделать сборку намного проще, возвращаемый массив больше не является 2D-массивом, а представляет собой плоский 1D-массив, где резервы каждой пары хранятся в 3 последовательных слотах. В Python требуется некоторая легкая обработка, чтобы получить тот же результат, что и с предыдущим контрактом:

# Call the contract
reserves = queryContract.functions.getReservesByPairsYul(pairs).call()
# Convert bytes32 to int
res = [int.from_bytes(elem, byteorder='big') for elem in res]
# Pack the ints into lists of 3 elements
res = [res[i:i+3] for i in range(0, len(res), 3)]

К сожалению, этот код не намного быстрее, чем предыдущий, а также гораздо менее читабельный. Если бы он был запущен в транзакции, а не в eth_call, потребление газа увеличилось бы со 109 тыс. до 87 тыс. газа (сокращение на 20%) при извлечении запасов 10 пар с использованием версии Solidity, а не версии Yul Assembly. Однако улучшенное потребление газа не приводит к повышению скорости, поскольку большая часть времени, затрачиваемого узлом на операции ввода-вывода, при получении данных хранилища парных контрактов. При получении 1000 пар время выполнения запроса увеличивается с 1,73 до 1,72 секундычто является незначительной разницей.

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

Заключение

В этой статье мы рассмотрели, как получить список пар с помощью событий заводского контракта V2 и как получить резервы этих пар с помощью функции getReserves() парного контракта. Были представлены такие оптимизации, как распараллеливание, позволяющие выполнять выборку резерва за долю времени блока в 12 секунд.

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

Часть 3.Поиск возможностей для арбитража

Если ваша настройка MEV выглядит не так, вы ngmi

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

В этой статье мы выполняем предварительный отбор интересующих пар токенов. Затем мы выводим математическую формулу для нахождения оптимального арбитража между двумя пулами одинаковых пар токенов. Наконец, мы реализуем формулу в коде и возвращаем список потенциальных возможностей арбитража.

Выбор пар токенов

Уточнения по арбитражной стратегии

Прежде чем мы начнем искать возможности для арбитража, мы должны четко определить периметр нашего арбитражного бота. В частности, по какому типу арбитража мы хотим действовать. Самый безопасный вид арбитража — между пулами, в которых задействован ETH. Поскольку ETH является активом, которым оплачивается газ наших транзакций, естественно всегда хотеть получить ETH после арбитража. Но у всех есть соблазн так думать. Имейте в виду, что в торговле пунктуальные возможности становятся все менее и менее прибыльными по мере того, как все больше людей действуют на них.

Для простоты мы сосредоточимся на возможностях арбитража между пулами, в которых задействован ETH. Мы будем искать возможности только между двумя пулами одной и той же пары токенов. Мы не будем торговать возможностями, которые включают более 2 пулов на торговом маршруте (так называемые многоскачковые возможности). Обратите внимание, что обновление этой стратегии до более рискованной — это первый шаг, который вы должны предпринять, чтобы повысить прибыльность вашего бота.

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

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

Выбор пар токенов

Теперь, когда мы определили периметр нашего арбитражного бота, нам нужно выбрать пары токенов, на которых мы хотим торговать. Вот 2 критерия выбора, которые мы будем использовать:

  • Выбранные пары должны включать ETH.
  • Пары должны торговаться как минимум на 2 разных пулах.

Повторное использование кода из статьи 2: Эффективное чтение цен пула, у нас есть следующий код, в котором перечислены все пары токенов, которые были развернуты предоставленными заводскими контрактами:

# [...]
# Load the addresses of the factory contracts
with open("FactoriesV2.json", "r") as f:
factories = json.load(f)

# [...]
# Fetch list of pools for each factory contract
pairDataList = []
for factoryName, factoryData in factories.items():
events = getPairEvents(w3.eth.contract(address=factoryData['factory'], abi=factory_abi), 0, w3.eth.block_number)
print(f'Found {len(events)} pools for {factoryName}')
for e in events:
pairDataList.append({
"token0": e["args"]["token0"],
"token1": e["args"]["token1"],
"pair": e["args"]["pair"],
"factory": factoryName
})

Мы просто инвертируем pairDataList в словарь, где ключи — это пары токенов, а значения — список пулов, которые торгуют этой парой. При просмотре списка мы игнорируем пары, которые не связаны с ETH. Когда цикл закончится, выбранные пары с не менее чем 2 пулами будут сохранены в списках, содержащих не менее 2 элементов:

# [...]
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
pair_pool_dict = {}
for pair_object in pairDataList:
# Check for ETH (WETH) in the pair.
pair = (pair_object['token0'], pair_object['token1'])
if WETH not in pair:
continue

# Make sure the pair is referenced in the dictionary.
if pair not in pair_pool_dict:
pair_pool_dict[pair] = []

# Add the pool to the list of pools that trade this pair.
pair_pool_dict[pair].append(pair_object)

# Create the final dictionnary of pools that will be traded on.
pool_dict = {}
for pair, pool_list in pair_pool_dict.items():
if len(pool_list) >= 2:
pool_dict[pair] = pool_list

Некоторые статистические данные должны быть распечатаны, чтобы лучше понять данные, с которыми мы работаем:

# Number of different pairs
print(f'We have {len(pool_dict)} different pairs.')

# Total number of pools
print(f'We have {sum([len(pool_list) for pool_list in pool_dict.values()])} pools in total.')

# Pair with the most pools
print(f'The pair with the most pools is {max(pool_dict, key=lambda k: len(pool_dict[k]))} with {len(max(pool_dict.values(), key=len))} pools.')

# Distribution of the number of pools per pair, deciles
pool_count_list = [len(pool_list) for pool_list in pool_dict.values()]
pool_count_list.sort(reverse=True)
print(f'Number of pools per pair, in deciles: {pool_count_list[::int(len(pool_count_list)/10)]}')

# Distribution of the number of pools per pair, percentiles (deciles of the first decile)
pool_count_list.sort(reverse=True)
print(f'Number of pools per pair, in percentiles: {pool_count_list[::int(len(pool_count_list)/100)][:10]}')

На момент написания статьи это выводит следующее:

We have 1431 different pairs.
We have 3081 pools in total.
The pair with the most pools is ('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xdAC17F958D2ee523a2206206994597C13D831ec7') with 16 pools.
Number of pools per pair, in deciles: [16, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
Number of pools per pair, in percentiles: [16, 5, 4, 3, 3, 3, 3, 3, 3, 3]

Выборка резервов для 3000 пулов может быть выполнена менее чем за 1 секунду с общедоступными узлами RPC. Это разумное количество времени.

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

Поиск возможностей для арбитража

Общая идея

Существует возможность арбитража всякий раз, когда есть расхождение в цене между двумя пулами, которые торгуют одной и той же парой. Однако не все ценовые различия можно использовать: стоимость газа по сделке устанавливает минимальную стоимость, которая должна быть возмещена сделкой, а ликвидность в каждом пуле ограничивает стоимость, которая может быть извлечена из данной разницы в цене.

Чтобы найти наиболее выгодную арбитражную возможность, доступную нам, нам нужно будет рассчитать потенциальную стоимость, извлекаемую из каждой ценовой разницы, учитывая резервы/ликвидность в каждом пуле, и оценить стоимость газа по сделке.

Формула оптимального размера арбитражной сделки

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

В статье 1 мы уже видели, как вычислить выход свопа через пул с учетом резервов этого пула и суммы ввода.

Чтобы найти оптимальный размер сделки, мы сначала находим формулу для вывода двух последовательных свопов с учетом некоторой суммы ввода и резервов двух пулов, участвующих в свопах.

Мы предполагаем, что входные данные первого свопа находятся в token0, а входные данные второго свопа — в token1, что в конечном итоге приводит к выходу в token0token0.

Пусть x — входная сумма, (a1, b1) — резервы первого пула, а (a1, b1) — резервы второго пула.(a2, b2) fee — это комиссия, взимаемая пулами, и предполагается, что она одинакова для обоих пулов (0,3% в большинстве случаев).

Мы определяем функцию, которая вычисляет выход свопа с учетом входных данных x и резервов (a,b):

f(x, a, b) = b * (1 - a/(a + x*(1-fee)))

Тогда мы знаем, что результат первого свопа составляет:

out1(x) = f(x, a1, b1)
out1(x) = b1 * (1 - a1/(a1 + x*(1-fee)))

Вывод второго свопа выглядит следующим образом: (обратите внимание на замененные резервные переменные)

out2(x) = f(out1(x), b2, a2)
out2(x) = f(f(x, a1, b1), b2, a2)
out2(x) = a2 * (1 - b2/(b2 + f(x, a1, b1)*(1-fee)))
out2(x) = a2 * (1 - b2/(b2 + b1 * (1 - a1/(a1 + x * (1-fee))) * (1-fee)))

Мы можем построить эту функцию с помощью desmos. Выбирая значения резервов таким образом, чтобы мы имитировали первый пул с 1 ETH и 1750 USDC, а второй пул с 1340 USDC и 1 ETH, мы получаем следующий график:

График валовой прибыли от торговли в зависимости от входной стоимости

Обратите внимание, что мы фактически построили out2(x) - x, который представляет собой прибыль от сделки за вычетом входной суммы.

Графически мы видим, что оптимальный размер сделки составляет 0,0607 ETH входа, что дает прибыль в 0.0085 ETH.0.0607 ETH Контракт должен иметь не менее 0.0607 ETH ликвидности в WETH, чтобы иметь возможность использовать эту возможность.

Это значение прибыли 0.0085 ETH (~ 16 долларов США при написании этой статьи) НЕ является окончательной прибылью сделки, так как нам все равно нужно учитывать стоимость газа по сделке. Об этом будет рассказано в следующей статье.

Мы хотим автоматически вычислить этот оптимальный размер сделки для нашего бота MEV. Это можно сделать с помощью элементарного исчисления. У нас есть функция одной переменной x, которую мы хотим максимизировать. Функция достигает максимума при значении x, где производная функции равна 0.

Для символического вычисления производной функции можно использовать различные бесплатные и онлайн-инструменты, такие как wolfram alpha.

Нахождение производной нашей функции валовой прибыли.

Найти такую производную очень просто с помощью Wolfram Alpha. Вы также можете сделать это вручную, если вы не уверены в своих математических навыках.

Wolfram Alpha дает следующую производную:

dout2(x)/dx = (a1*b1*a2*b2*(1-fee)^2)/(a1*b2 + (1-fee)*x*(b1*(1-fee)+b2))^2

Поскольку мы хотим найти значение x, которое максимизирует прибыль (которое out2(x) - x x), нам нужно найти значение x, где производная x равна 1 (а не 0).

Wolfram Alpha дает следующее решение для x в уравнении dout2(x)/dx = 1 x:

x = (sqrt(a1*b1*a2*b2*(1-fee)^4 * (b1*(1-fee)+b2)^2) - a1*b2*(1-fee)*(b1*(1-fee)+b2)) / ((1-fee) * (b1*(1-fee) + b2))^2

Со значениями резервов, которые мы использовали на графике выше, мы получаем x_optimal = 0,0607203782551, что подтверждает нашу формулу (по сравнению со значением графика x_optimal = 0.06072037825510.0607).

Хотя эта формула не очень читабельна, ее легко реализовать в коде. Вот реализация формулы на Python для вычисления вывода 2 свопов и оптимального размера сделки:

# Helper functions for calculating the optimal trade size
# Output of a single swap
def swap_output(x, a, b, fee=0.003):
return b * (1 - a/(a + x*(1-fee)))

# Gross profit of two successive swaps
def trade_profit(x, reserves1, reserves2, fee=0.003):
a1, b1 = reserves1
a2, b2 = reserves2
return swap_output(swap_output(x, a1, b1, fee), b2, a2, fee) - x

# Optimal input amount
def optimal_trade_size(reserves1, reserves2, fee=0.003):
a1, b1 = reserves1
a2, b2 = reserves2
return (math.sqrt(a1*b1*a2*b2*(1-fee)**4 * (b1*(1-fee)+b2)**2) - a1*b2*(1-fee)*(b1*(1-fee)+b2)) / ((1-fee) * (b1*(1-fee) + b2))**2

Поиск возможностей для арбитража

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

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

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

Вот код, который проходит через все пары и все пулы и сортирует возможности по прибыли:

# [...]
# Fetch the reserves of each pool in pool_dict
to_fetch = [] # List of pool addresses for which reserves need to be fetched.
for pair, pool_list in pool_dict.items():
for pair_object in pool_list:
to_fetch.append(pair_object["pair"]) # Add the address of the pool
print(f"Fetching reserves of {len(to_fetch)} pools...")

# getReservesParallel() is from article 2 in the MEV bot series
reserveList = asyncio.get_event_loop().run_until_complete(getReservesParallel(to_fetch, providersAsync))

# Build list of trading opportunities
index = 0
opps = []
for pair, pool_list in pool_dict.items():
# Store the reserves in the pool objects for later use
for pair_object in pool_list:
pair_object["reserves"] = reserveList[index]
index += 1

# Iterate over all the pools of the pair
for poolA in pool_list:
for poolB in pool_list:
# Skip if it's the same pool
if poolA["pair"] == poolB["pair"]:
continue

# Skip if one of the reserves is 0 (division by 0)
if 0 in poolA["reserves"] or 0 in poolB["reserves"]:
continue

# Re-order the reserves so that WETH is always the first token
if poolA["token0"] == WETH:
res_A = (poolA["reserves"][0], poolA["reserves"][1])
res_B = (poolB["reserves"][0], poolB["reserves"][1])
else:
res_A = (poolA["reserves"][1], poolA["reserves"][0])
res_B = (poolB["reserves"][1], poolB["reserves"][0])

# Compute value of optimal input through the formula
x = optimal_trade_size(res_A, res_B)

# Skip if optimal input is negative (the order of the pools is reversed)
if x < 0:
continue

# Compute gross profit in Wei (before gas cost)
profit = trade_profit(x, res_A, res_B)

# Store details of the opportunity. Values are in ETH. (1e18 Wei = 1 ETH)
opps.append({
"profit": profit / 1e18,
"input": x / 1e18,
"pair": pair,
"poolA": poolA,
"poolB": poolB,
})

print(f"Found {len(opps)} opportunities.")

Что приводит к следующему результату:

Fetching reserves of 3081 pools.
Found 1791 opportunities.

Теперь у нас есть список всех возможностей. Нам просто нужно оценить их прибыль. Прямо сейчас мы просто предположим постоянную стоимость газа для торговли возможностью.

Мы должны использовать нижнюю границу стоимости газа для свопа на Uniswap V2. Экспериментально мы обнаружили, что это значение близко к 43 тыс. газа.

Для использования возможности требуется 2 свопа, а выполнение транзакции на Ethereum стоит 21 тыс. газа, что в общей сложности составляет 107 тыс. газа за возможность.

Ниже приведен код, который вычисляет предполагаемую чистую прибыль каждой возможности:

# [...]
# Use the hard-coded gas cost of 107k gas per opportunity
gp = w3.eth.gas_price
for opp in opps:
opp["net_profit"] = opp["profit"] - 107000 * gp / 1e18

# Sort by estimated net profit
opps.sort(key=lambda x: x["net_profit"], reverse=True)

# Keep positive opportunities
positive_opps = [opp for opp in opps if opp["net_profit"] > 0]

### Print stats
# Positive opportunities count
print(f"Found {len(positive_opps)} positive opportunities.")

# Details on each opportunity
ETH_PRICE = 1900 # You should dynamically fetch the price of ETH
for opp in positive_opps:
print(f"Profit: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")
print(f"Input: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")
print(f"Pool A: {opp['poolA']['pair']}")
print(f"Pool B: {opp['poolB']['pair']}")
print()

Вот вывод скрипта:

Found 57 positive opportunities.

Profit: 4.936025725859028 ETH ($9378.448879132153)
Input: 1.7958289984719014 ETH ($3412.075097096613)
Pool A: 0x1498bd576454159Bb81B5Ce532692a8752D163e8
Pool B: 0x7D7E813082eF6c143277c71786e5bE626ec77b20
{'profit': 4.9374642090282865, 'input': 1.7958(...)
Profit: 4.756587769768892 ETH ($9037.516762560894)
Input: 0.32908348765283796 ETH ($625.2586265403921)
Pool A: 0x486c1609f9605fA14C28E311b7D708B0541cd2f5
Pool B: 0x5e81b946b61F3C7F73Bf84dd961dE3A0A78E8c33
{'profit': 4.7580262529381505, 'input': 0.329(...)
Profit: 0.8147203063054365 ETH ($1547.9685819803292)
Input: 0.6715171730669338 ETH ($1275.8826288271744)
Pool A: 0x1f1B4836Dde1859e2edE1C6155140318EF5931C2
Pool B: 0x1f7efDcD748F43Fc4BeAe6897e5a6DDd865DcceA
{'profit': 0.8161587894746954, 'input': 0.671(...)
(...)

Которые имеют подозрительно высокую прибыль. Первым шагом, который следует предпринять, является проверка правильности кода. Тщательно проверив код, мы обнаружили, что код правильный.

Реальна ли эта прибыль? Как выясняется, нет. Мы слишком широко раскинули нашу сеть при выборе пулов, которые следует учитывать в нашей стратегии, и получили в свои руки пулы токсичных токенов.

Стандарт токенов ERC20 описывает только интерфейс для взаимодействия. Любой может развернуть токен, реализующий этот интерфейс, и выбрать реализацию неортодоксального поведения, что и происходит здесь.

Некоторые создатели токенов создают свои ERC20 таким образом, что пулы, на которых они торгуются, не могут продать, а только купить токен. Некоторые контракты токенов даже имеют механизмы аварийного отключения, которые позволяют создателю тянуть всех своих пользователей.

В нашем боте MEV эти токсичные токены должны быть отфильтрованы. Об этом будет рассказано в одной из следующих статей.

Если мы вручную отфильтруем явно токсичные токены, у нас останутся следующие 42 возможности:

Profit: 0.004126583158496902 ETH ($7.840508001144114)
Input: 0.008369804833786892 ETH ($15.902629184195094)
Pool A: 0xdF42388059692150d0A9De836E4171c7B9c09CBf
Pool B: 0xf98fCEB2DC0Fa2B3f32ABccc5e8495E961370B23
{'profit': 0.005565066327755902, (...)

Profit: 0.004092580415474992 ETH ($7.775902789402485)
Input: 0.014696360216108083 ETH ($27.92308441060536)
Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F
Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369
{'profit': 0.005531063584733992, (...)

Profit: 0.003693235163284344 ETH ($7.017146810240254)
Input: 0.1392339178514088 ETH ($264.5444439176767)
Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1
Pool B: 0xF110783EbD020DCFBA91Cd1976b79a6E510846AA
{'profit': 0.005131718332543344, (...)

Profit: 0.003674128918827048 ETH ($6.980844945771391)
Input: 0.2719041848570484 ETH ($516.617951228392)
Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425
Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325
{'profit': 0.005112612088086048, (...)
(...)

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

Эта прибыль гораздо более разумна. Но помните, что они по-прежнему являются прибылью в лучшем случае, поскольку мы использовали очень грубую оценку стоимости газа для каждой возможности.

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

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

Заключение

Теперь у нас есть четкое определение периметра нашего арбитражного бота MEV.

Мы изучили математическую теорию, лежащую в основе арбитражной стратегии, и реализовали ее в Python.

Теперь у нас есть список потенциальных арбитражных возможностей, и нам нужно смоделировать их исполнение, чтобы получить итоговое значение прибыли. Для этого нам нужно подготовить наш торговый смарт-контракт.

В следующей статье мы разработаем такой смарт-контракт в Solidity, и смоделируем нашу первую арбитражную сделку.

Полный код можно найти в репозитории github, связанном с этой статьей. Сценарий лучше всего запускать в записной книжке Jupyter.

Создание бота для арбитражной торговли MEV с нуля

Отправляйтесь в увлекательное путешествие, чтобы создать своего собственного бота для арбитражной торговли MEV с нуля. В этом подробном руководстве вы погрузитесь в запутанный мир арбитражной торговли и исследуете увлекательное царство извлекаемой стоимости майнера (MEV). Откройте для себя основные концепции и принципы, лежащие в основе арбитража MEV, получив глубокое понимание того, как использовать потенциальные возможности получения прибыли в экосистемах децентрализованных финансов (DeFi).

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

Что такое MEV Bot?

Бот MEV (Miner Extractable Value) — это специализированный бот для алгоритмической торговли, предназначенный для извлечения выгоды из возможностей в экосистемах децентрализованных финансов (DeFi) путем использования контролируемого майнерами порядка транзакций. Майнеры имеют возможность включать или переупорядочивать транзакции в блоке, что позволяет им извлекать дополнительную ценность из транзакций. MEV-боты используют это явление, автоматизируя идентификацию и выполнение сделок, которые максимизируют прибыль в контексте поведения майнеров.

Эти боты имеют решающее значение для навигации в динамичной и конкурентной среде рынков DeFi, где оптимизация последовательности транзакций может привести к повышению прибыльности и стратегическим преимуществам. Боты MEV находятся на переднем крае алгоритмической торговли в блокчейн-средах, сочетая финансовый опыт с технической сложностью.

Как работают MEV-боты?

Боты MEV (Miner Extractable Value) — это автоматизированные программы или алгоритмы, которые используют возможности для получения прибыли в контексте транзакций блокчейна, особенно в средах децентрализованных финансов (DeFi). MEV относится к значению, которое майнеры (или валидаторы) могут извлечь из переупорядочения и включения транзакций в блок. MEV-боты нацелены на максимизацию потенциальной прибыли за счет стратегически опережающих, обратных или сэндвич-транзакций.

Вот как работают MEV-боты:

  1. Мониторинг транзакций: MEV-боты постоянно следят за мемпулом, который представляет собой временное хранилище для ожидающих транзакций, ожидающих добавления в блокчейн. Они анализируют входящие транзакции и выявляют выгодные возможности.
  2. Фронтраннинг: Опережение происходит, когда бот MEV наблюдает за ожидающей транзакцией и быстро отправляет новую транзакцию с более высокой комиссией за газ, чтобы опередить исходную транзакцию. Это часто делается в ситуациях, когда MEV-бот ожидает выгодного движения цены.
  3. Back-running: В отличие от front-running, back-running предполагает отправку транзакции после того, как известная транзакция была отправлена, но до того, как она будет подтверждена. MEV-боты могут использовать ситуации, когда они ожидают движения рынка, вызванного исходной транзакцией.
  4. Сэндвич-атаки: Сэндвич-атака включает в себя размещение двух транзакций вокруг целевой транзакции для извлечения максимальной выгоды. MEV-бот отправляет транзакцию до и после целевой транзакции, используя движения цены, вызванные целевой транзакцией.
  5. Возможности арбитража: MEV-боты также могут использовать арбитражные возможности, выявляя разницу в ценах между различными децентрализованными биржами или пулами ликвидности. Они могут быстро совершать сделки, чтобы зафиксировать расхождение в цене до того, как оно нормализуется.
  6. Флэш-кредиты: MEV-боты иногда используют флэш-кредиты, которые представляют собой займы, полученные и погашенные в рамках одной транзакции. Они могут занимать крупную сумму активов, выполнять ряд сделок, чтобы использовать рыночную неэффективность, и погашать кредит в одной и той же сделке.
  7. Манипуляции со смарт-контрактами: MEV-боты могут взаимодействовать со смарт-контрактами таким образом, чтобы извлекать дополнительную ценность. Например, они могут эксплуатировать уязвимости в коде контракта или манипулировать порядком выполнения, чтобы получить преимущество.

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

Арбитражный бот MEV

Арбитражный бот MEV — это сложная алгоритмическая торговая программа, предназначенная для сред децентрализованных финансов (DeFi). Используя извлекаемую ценность майнера (MEV), бот стратегически использует возможности, анализируя и извлекая выгоду из вариаций порядка транзакций, которые могут возникнуть из-за влияния майнеров.

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

Типы стратегий MEV-ботов

Стратегии ботов MEV (Miner Extractable Value) охватывают множество методов и подходов для использования возможностей получения прибыли в транзакциях блокчейна, особенно в экосистемах децентрализованных финансов (DeFi). Вот несколько распространенных типов стратегий MEV-ботов:

》 Фронтраннинг:

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

》 Обратный ход:

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

》 Сэндвич-атаки:

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

》 Арбитражные возможности:

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

》 Флэш-кредиты:

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

》 Манипулирование смарт-контрактами:

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

》 Вставка транзакции:

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

》 Ликвидационная снайперская стрельба:

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

》 Эксплуатация Oracle:

MEV-боты могут быть нацелены на децентрализованные приложения, полагающиеся на внешние данные или оракулы. Манипулируя сроками транзакций на основе обновлений оракула, они могут использовать расхождения и получать преимущества в торговле или другой финансовой деятельности.

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

Особенности MEV-ботов

Особенности ботов MEV (Miner Extractable Value) характеризуются их возможностями выявлять, использовать и максимизировать возможности получения прибыли в рамках транзакций блокчейна, особенно в средах децентрализованных финансов (DeFi). Вот ключевые функции, связанные с ботами MEV:

⇏ Мониторинг транзакций:

● MEV-боты постоянно отслеживают мемпул (пул ожидающих транзакций) для выявления потенциальных возможностей для получения прибыли. Они анализируют входящие транзакции, чтобы определить, какие из них могут быть стратегически использованы.

⇏ Принятие решений в режиме реального времени:

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

⇏ Оптимизация платы за газ:

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

⇏ Возможность опережающего бега:

● Опережение является ключевой особенностью разработки MEV-ботов, позволяя им размещать транзакции впереди других в очереди, чтобы воспользоваться ожидаемыми движениями рынка и обеспечить более выгодные цены.

⇏ Возможность обратного хода:

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

⇏ Выполнение сэндвич-атаки:

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

⇏ Арбитражное исполнение:

● MEV-боты выявляют и используют арбитражные возможности, быстро совершая сделки, чтобы извлечь выгоду из разницы цен на различных децентрализованных биржах, пулах ликвидности или рынках.

⇏ Использование флэш-кредита:

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

⇏ Взаимодействие со смарт-контрактом:

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

⇏ Адаптивность к условиям сети:

● MEV-боты предназначены для адаптации к меняющимся сетевым условиям, корректируя свои стратегии в зависимости от таких факторов, как цены на газ, перегруженность сети и общая рыночная среда.

⇏ Эффективное использование данных блокчейна:

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

Стоит отметить, что стратегии и функции MEV-ботов могут развиваться с течением времени по мере появления новых разработок в технологии блокчейн и экосистемах DeFi. Кроме того, предпринимаются усилия по разработке механизмов для смягчения влияния MEV на децентрализованные системы и обеспечения справедливости в транзакциях блокчейна.

Преимущества разработки MEV-ботов

В то время как боты Miner Extractable Value (MEV) были предметом споров из-за их потенциала для манипулирования децентрализованными системами, важно признать, что разработка ботов MEV имеет определенные предполагаемые преимущества для пользователей и разработчиков. Тем не менее, очень важно учитывать эти преимущества в контексте более широкой криптовалютной экосистемы:

  1. Рыночная эффективность: MEV-боты могут способствовать повышению эффективности рынка, способствуя быстрому исполнению сделок, арбитражным возможностям и другим рыночным действиям. Такая эффективность может привести к более точному ценообразованию и уменьшению спредов на различных децентрализованных биржах.
  2. Обеспечение ликвидности: MEV-боты, особенно те, которые занимаются арбитражем, могут способствовать обеспечению ликвидности на децентрализованных рынках. Используя расхождения в ценах между различными платформами, MEV-боты помогают выровнять цены и уменьшить проскальзывание для трейдеров.
  3. Инновации в экосистемах DeFi: Разработка MEV-ботов вызвала инновации в экосистемах децентрализованных финансов (DeFi). Разработчики, работающие над стратегиями MEV-ботов, вносят свой вклад в эволюцию финансовых инструментов и торговых механизмов, раздвигая границы возможного в децентрализованных системах.
  4. Использование флэш-кредита: MEV-боты, использующие флэш-кредиты, могут позволить пользователям получить доступ к большим суммам активов, не требуя значительного капитала. Это может демократизировать доступ к финансовым возможностям и обеспечить более инклюзивную среду для трейдеров, которые могут не иметь в своем распоряжении значительных средств.
  5. Оптимизированный порядок транзакций: MEV-боты, конкурируя за включение транзакций в блоки, могут косвенно привести к более оптимизированным механизмам упорядочения транзакций. Это может привести к тому, что транзакции будут обрабатываться более эффективно и с меньшей задержкой, что принесет пользу пользователям, которые отдают приоритет быстрому выполнению транзакций.

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

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

Заключение

В заключение, начало пути к созданию торгового бота для арбитража MEV с нуля предлагает больше, чем просто техническое достижение; это открывает двери к тонкому пониманию децентрализованных финансов (DeFi) и динамики блокчейна. Освоив тонкости добываемой стоимости майнера (MEV) и создав индивидуального торгового бота, вы вооружились мощным инструментом для навигации по постоянно развивающимся рынкам криптовалют.

Это не просто программа; Это путь к расширению финансовых возможностей и технологическому мастерству. Завершая этот урок, подумайте о полученных знаниях и потенциальном влиянии на ваши торговые стратегии. Участвуйте в непрерывном процессе обучения, следите за тенденциями рынка и постоянно совершенствуйте возможности своего бота. Имея в своем арсенале бота для арбитражной торговли MEV, вы готовы воспользоваться возможностями и преуспеть в динамичном ландшафте цифровых финансов. Пусть это путешествие ознаменует начало вашего расширенного исследования в области алгоритмической торговли и децентрализованных экосистем.

Источник

Источник