Fotor AI: «Чихуахуа ест бутерброд» после 16 итераций
  • Создания сэндвич-бота
  • Проверка работы сэндвич-бота

Создания сэндвич-бота

От А до Я: Создайте своего собственного сэндвич-бота правильным способом

Сегодня мы вместе создадим сэндвич-бота. 🥪

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

И вот опять. В Интернете уже есть множество ресурсов, из которых можно узнать об этом. Зачем еще один скучный урок по sando bot от кого-то вроде вас?

Хороший вопрос. 🤔

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

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

Если вы еще не пробовали создать MEV-бота с нуля, сейчас у вас есть шанс. Сразу оговоримся, что этот проект не начнет приносить прибыль с первого дня. Если бы это было так, это не было бы открыто опубликовано. Но это будет довольно близко к реальной сделке, и по мере того, как мы будем добавлять больше функций, мы начнем добавлять новые функции. В следующем месяце мы будем дорабатывать код для максимальной производительности (а затем перейдем к снайпингу и Solana).

Вот краткий обзор итогового результата:

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

GitHub — solidquant/sandooo: сэндвич-бот

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

Эта серия будет разделена на две части:

  1. (Часть #1): Идентификация
  2. (Часть #2): Исполнение

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

Содержание:

  1. Сэндвич-бот 101
  2. Сэндвич-смарт-контракт с использованием Solidity/Yul
  3. Механизм сэндвич-моделирования с использованием REVM

Знакомство

Большинство из нас, вероятно, знакомы с ботом Subway, написанным на JS libevm:

GitHub — libevm/subway: Практический пример того, как выполнять сэндвич-атаки на Ethereum

Практический пример того, как выполнять сэндвич-атаки на Ethereum — GitHub — libevm/subway: Практический пример того, как…

github.com

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

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

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

👾 Присоединяйтесь к нашей команде в Discord, где тысячи людей каждый день общаются о MEV. Заниматься самостоятельным исследованием может быть немного одиноко, поэтому загляните и расскажите, в чем дело 🏄🏄. Посмотрите, как другие справляются с этой задачей:

Присоединяйтесь к серверу Solid Quant в Discord!

Квантовые любители, занимающиеся квантовыми вещами — в настоящее время в основном связанные с MEV. | 1380 участников

discord.com

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

Если вы еще не слишком знакомы с конструкторами блоков, взгляните на мою предыдущую статью:

Меня не волнует, что я попаду в затруднительное положение, в MEV грядут более важные вещи

Внутренняя работа MEV и то, как меняется отрасль

medium.com

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

Сначала мы создадим наш проект.

✋✋ Обратите внимание, что запуск производственного кода с использованием полного узла обеспечит наилучшую производительность. Стратегии MEV, как правило, очень интенсивно используют сеть, поэтому лучше всего устранить любые задержки, связанные с ними, и самый простой способ сделать это — запустить полный узел. Я лично управляю Geth + Lighthouse.

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

Настройка проекта

Во-первых, создайте новый проект Rust на вашем локальном компьютере, выполнив:cargo new sandooo

это создаст скелет шаблона Rust в новом каталоге с именем sandooo.

Откройте этот каталог с помощью IDE по вашему выбору, скопируйте и вставьте его в файл Cargo.toml (sandooo/Cargo.toml):

sandooo/Cargo.toml на главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com[package]
name = «sandooo»
version = «0.1.0»
edition = «2021»

[dependencies]
dotenv = «0.15.0»
anyhow = «1.0.70»
itertools = «0.11.0»
serde = «1.0.188»
serde_json = «1.0.107»
bounded-vec-deque = «0.1.1»

# Telegram
teloxide = { version = «0.12», features = [«macros»] }

futures = «0.3.5»
futures-util = «*»
tokio = { version = «1.29.0», features = [«full»] }
tokio-stream = { version = «0.1», features = [‘sync’] }
tokio-tungstenite = «*»
async-trait = «0.1.74»

ethers-core = «2.0»
ethers-providers = «2.0»
ethers-contract = «2.0»
ethers = { version = «2.0», features = [«abigen», «ws», «ipc»] }

ethers-flashbots = { git = «https://github.com/onbjerg/ethers-flashbots» }

eth-encode-packed = «0.1.0»
rlp = { version = «0.5», features = [«derive»] }

foundry-evm-mini = { git = «https://github.com/solidquant/foundry-evm-mini.git» }

revm = { version = «3», default-features = false, features = [
«std»,
«serde»,
«memory_limit»,
«optional_eip3607»,
«optional_block_gas_limit»,
«optional_no_base_fee»,
] }

csv = «1.2.2»
colored = «2.0.0»
log = «0.4.17»
fern = { version = «0.6.2», features = [«colored»] }
chrono = «0.4.23»
indicatif = «0.17.5»

[patch.crates-io]
revm = { git = «https://github.com/bluealloy/revm/», rev = «80c909d6f242886cb26e6103a01d1a4bf9468426» }

[profile.release]
codegen-units = 1
lto = «fat»

Как только это будет сделано, создайте новый каталог в каталоге src и назовите его common. И создайте новый файл с именем: constants.rs (sandooo/src/common/constants.rs):

sandooo/src/common/constants.rs на главной странице · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.compub static PROJECT_NAME: &str = «sandooo»;

// Function that will load the environment variable as a String value
pub fn get_env(key: &str) -> String {
std::env::var(key).unwrap_or(String::from(«»))
}

#[derive(Debug, Clone)]
pub struct Env {
pub https_url: String,
pub wss_url: String,
pub bot_address: String,
pub private_key: String,
pub identity_key: String,
pub telegram_token: String,
pub telegram_chat_id: String,
pub use_alert: bool,
pub debug: bool,
}

// Creating a new Env struct will automatically load the environment variables
impl Env {
pub fn new() -> Self {
Env {
https_url: get_env(«HTTPS_URL»),
wss_url: get_env(«WSS_URL»),
bot_address: get_env(«BOT_ADDRESS»),
private_key: get_env(«PRIVATE_KEY»),
identity_key: get_env(«IDENTITY_KEY»),
telegram_token: get_env(«TELEGRAM_TOKEN»),
telegram_chat_id: get_env(«TELEGRAM_CHAT_ID»),
use_alert: get_env(«USE_ALERT»).parse::<bool>().unwrap(),
debug: get_env(«DEBUG»).parse::<bool>().unwrap(),
}
}
}

pub static COINBASE: &str = «0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5»; // Flashbots Builder

pub static WETH: &str = «0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2»;
pub static WETH_BALANCE_SLOT: i32 = 3;
pub static WETH_DECIMALS: u8 = 18;

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

sandooo/.env.example в главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.comHTTPS_URL=http://localhost:8545
WSS_URL=ws://localhost:8546
BOT_ADDRESS=»…»
PRIVATE_KEY=»…»
IDENTITY_KEY=»…»
TELEGRAM_TOKEN=»…»
TELEGRAM_CHAT_ID=»…»
USE_ALERT=false
DEBUG=true

RUST_BACKTRACE=1

  • PRIVATE_KEY: это ваш фактический приватный ключ, если вы собираетесь запускать своего сэндвич-бота с помощью реального кошелька
  • IDENTITY_KEY: это может быть любой приватный ключ по вашему выбору. Строители, получившие идентификационные ключи, будут использовать их для приоритизации определенных пакетов на основе репутации пользователя. Подробнее о репутации пользователя можно узнать здесь: https://docs.flashbots.net/flashbots-auction/advanced/reputation
  • TELEGRAM_TOKEN / TELEGRAM_CHAT_ID: вы можете оставить эти поля пустыми, если не хотите использовать оповещения Telegram.
  • DEBUG: мы будем использовать этот флаг позже для поддержки обоих режимов разработки/продакшена. Если DEBUG установлено в true, мы будем запускать только симуляции и не отправлять реальные пакеты.

Мы также хотим приукрасить наши консольные журналы, поэтому мы добавляем utils.rs файл в наш каталог src/common (sandooo/src/common/utils.rs) :

sandooo/src/common/utils.rs в главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.comuse anyhow::Result;
use ethers::core::rand::thread_rng;
use ethers::prelude::*;
use ethers::{
self,
types::{
transaction::eip2930::{AccessList, AccessListItem},
U256,
},
};
use fern::colors::{Color, ColoredLevelConfig};
use foundry_evm_mini::evm::utils::{b160_to_h160, h160_to_b160, ru256_to_u256, u256_to_ru256};
use log::LevelFilter;
use rand::Rng;
use revm::primitives::{B160, U256 as rU256};
use std::str::FromStr;
use std::sync::Arc;

use crate::common::constants::{PROJECT_NAME, WETH};

// Function to format our console logs
pub fn setup_logger() -> Result<()> {
let colors = ColoredLevelConfig {
trace: Color::Cyan,
debug: Color::Magenta,
info: Color::Green,
warn: Color::Red,
error: Color::BrightRed,
..ColoredLevelConfig::new()
};

fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
«{}[{}] {}»,
chrono::Local::now().format(«[%H:%M:%S]»),
colors.color(record.level()),
message
))
})
.chain(std::io::stdout())
.level(log::LevelFilter::Error)
.level_for(PROJECT_NAME, LevelFilter::Info)
.apply()?;

Ok(())
}

// Calculates the next block base fee given the previous block’s gas usage / limits
// Refer to: https://www.blocknative.com/blog/eip-1559-fees
pub fn calculate_next_block_base_fee(
gas_used: U256,
gas_limit: U256,
base_fee_per_gas: U256,
) -> U256 {
let gas_used = gas_used;

let mut target_gas_used = gas_limit / 2;
target_gas_used = if target_gas_used == U256::zero() {
U256::one()
} else {
target_gas_used
};

let new_base_fee = {
if gas_used > target_gas_used {
base_fee_per_gas
+ ((base_fee_per_gas * (gas_used — target_gas_used)) / target_gas_used)
/ U256::from(8u64)
} else {
base_fee_per_gas
— ((base_fee_per_gas * (target_gas_used — gas_used)) / target_gas_used)
/ U256::from(8u64)
}
};

let seed = rand::thread_rng().gen_range(0..9);
new_base_fee + seed
}

pub fn access_list_to_ethers(access_list: Vec<(B160, Vec<rU256>)>) -> AccessList {
AccessList::from(
access_list
.into_iter()
.map(|(address, slots)| AccessListItem {
address: b160_to_h160(address),
storage_keys: slots
.into_iter()
.map(|y| H256::from_uint(&ru256_to_u256(y)))
.collect(),
})
.collect::<Vec<AccessListItem>>(),
)
}

pub fn access_list_to_revm(access_list: AccessList) -> Vec<(B160, Vec<rU256>)> {
access_list
.0
.into_iter()
.map(|x| {
(
h160_to_b160(x.address),
x.storage_keys
.into_iter()
.map(|y| u256_to_ru256(y.0.into()))
.collect(),
)
})
.collect()
}

abigen!(
IERC20,
r#»[
function balanceOf(address) external view returns (uint256)
]»#,
);

// Utility functions

pub async fn get_token_balance(
provider: Arc<Provider<Ws>>,
owner: H160,
token: H160,
) -> Result<U256> {
let contract = IERC20::new(token, provider);
let token_balance = contract.balance_of(owner).call().await?;
Ok(token_balance)
}

pub fn create_new_wallet() -> (LocalWallet, H160) {
let wallet = LocalWallet::new(&mut thread_rng());
let address = wallet.address();
(wallet, address)
}

pub fn to_h160(str_address: &’static str) -> H160 {
H160::from_str(str_address).unwrap()
}

pub fn is_weth(token_address: H160) -> bool {
token_address == to_h160(WETH)
}

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

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

Для этого:

  1. Создание sandooo/src/lib.rs:

pub mod common;

2. Создайте sandooo/src/common/mod.rs:pub mod constants;
pub mod utils;

Настройка проекта завершена. Теперь мы можем перейти к более интересным аспектам сэндвич-бота.

Потоки мемпула

Например, кто-то отправляет транзакцию ордера Uniswap в публичный мемпул. Любая транзакция, отправленная непосредственно в блокчейн, как правило, попадает в публичный мемпул.

И любой желающий может получить доступ к этим данным, установив подключение websocket к провайдеру узла. Для этого мы создадим новый файл в sandooo/src/common/streams.rs:

sandooo/src/common/streams.rs на главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.comuse ethers::{
providers::{Middleware, Provider, Ws},
types::*,
};
use std::sync::Arc;
use tokio::sync::broadcast::Sender;
use tokio_stream::StreamExt;

use crate::common::utils::calculate_next_block_base_fee;

#[derive(Default, Debug, Clone)]
pub struct NewBlock {
pub block_number: U64,
pub base_fee: U256,
pub next_base_fee: U256,
}

#[derive(Debug, Clone)]
pub struct NewPendingTx {
pub added_block: Option<U64>,
pub tx: Transaction,
}

impl Default for NewPendingTx {
fn default() -> Self {
Self {
added_block: None,
tx: Transaction::default(),
}
}
}

#[derive(Debug, Clone)]
pub enum Event {
Block(NewBlock),
PendingTx(NewPendingTx),
}

// A websocket connection made to get newly created blocks
pub async fn stream_new_blocks(provider: Arc<Provider<Ws>>, event_sender: Sender<Event>) {
let stream = provider.subscribe_blocks().await.unwrap();
let mut stream = stream.filter_map(|block| match block.number {
Some(number) => Some(NewBlock {
block_number: number,
base_fee: block.base_fee_per_gas.unwrap_or_default(),
next_base_fee: U256::from(calculate_next_block_base_fee(
block.gas_used,
block.gas_limit,
block.base_fee_per_gas.unwrap_or_default(),
)),
}),
None => None,
});

while let Some(block) = stream.next().await {
match event_sender.send(Event::Block(block)) {
Ok(_) => {}
Err(_) => {}
}
}
}

// A websocket connection made to get new pending transactions
pub async fn stream_pending_transactions(provider: Arc<Provider<Ws>>, event_sender: Sender<Event>) {
let stream = provider.subscribe_pending_txs().await.unwrap();
let mut stream = stream.transactions_unordered(256).fuse();

while let Some(result) = stream.next().await {
match result {
Ok(tx) => match event_sender.send(Event::PendingTx(NewPendingTx {
added_block: None,
tx,
})) {
Ok(_) => {}
Err(_) => {}
},
Err(_) => {}
};
}
}

И обновите sandooo/src/common/mod.rs:pub mod constants;
pub mod streams;
pub mod utils;

чтобы мы могли использовать функции в streams.rs.

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

Для этого мы создадим новый каталог в каталоге src, назовем его: sandooo/src/sandwich. Создайте два новых файла в этом каталоге:

  • sandooo/src/sandwich/strategy.rs:

use bounded_vec_deque::BoundedVecDeque;
use ethers::signers::{LocalWallet, Signer};
use ethers::{
providers::{Middleware, Provider, Ws},
types::{BlockNumber, H160, H256, U256, U64},
};
use log::{info, warn};
use std::{collections::HashMap, str::FromStr, sync::Arc};
use tokio::sync::broadcast::Sender;

// we’ll update this part later, for now just import the necessary components
use crate::common::constants::{Env, WETH};
use crate::common::streams::{Event, NewBlock};
use crate::common::utils::{calculate_next_block_base_fee, to_h160};

pub async fn run_sandwich_strategy(provider: Arc<Provider<Ws>>, event_sender: Sender<Event>) {
let mut event_receiver = event_sender.subscribe();

loop {
match event_receiver.recv().await {
Ok(event) => match event {
Event::Block(block) => {
info!(«{:?}», block);
}
Event::PendingTx(mut pending_tx) => {
info!(«{:?}», pending_tx);
}
},
_ => {}
}
}
}

  • sandooo/src/sandwich/mod.rs:

pub mod strategy;

  • sandooo/src/lib.rs:

pub mod common;
pub mod sandwich;

Надеюсь, теперь вы понимаете, что каждый раз, когда мы добавляем новый каталог в каталог src, мы обновляем его в нашем sandooo/src/lib.rs, и в этих каталогах должен быть mod.rs файл. И каждый раз, когда мы добавляем новый файл в этот каталог, мы должны добавить его в mod.rs файл. Пожалуйста, не забывайте делать это с этого момента, потому что я не буду описывать этот процесс с этого момента, и предполагаю, что это всегда делается.

Перейдите к main.rs и обновите код:use anyhow::Result;
use ethers::providers::{Provider, Ws};
use log::info;
use std::sync::Arc;
use tokio::sync::broadcast::{self, Sender};
use tokio::task::JoinSet;

use sandooo::common::constants::Env;
use sandooo::common::streams::{stream_new_blocks, stream_pending_transactions, Event};
use sandooo::common::utils::setup_logger;
use sandooo::sandwich::strategy::run_sandwich_strategy;

#[tokio::main]
async fn main() -> Result<()> {
dotenv::dotenv().ok();
setup_logger().unwrap();

info!(«Starting Sandooo»);

let env = Env::new();

let ws = Ws::connect(env.wss_url.clone()).await.unwrap();
let provider = Arc::new(Provider::new(ws));

let (event_sender, _): (Sender<Event>, _) = broadcast::channel(512);

let mut set = JoinSet::new();

set.spawn(stream_new_blocks(provider.clone(), event_sender.clone()));
set.spawn(stream_pending_transactions(
provider.clone(),
event_sender.clone(),
));

set.spawn(run_sandwich_strategy(
provider.clone(),
event_sender.clone(),
));

while let Some(res) = set.join_next().await {
info!(«{:?}», res);
}

Ok(())
}

Функция main является нашей точкой входа для всей системы, и она будет выполнять три асинхронные функции, используя JoinSet Tokio.

Запустите текущую программу Rust, выполнив следующие действия:cargo run

выдаст вам поток ожидающих транзакций на вашем терминале:

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

🔎 На какие незавершенные транзакции стоит обратить внимание?

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

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

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

Но мы не можем фиксировать транзакции, которые намного сложнее, но могут быть сэндвичевыми. Это могут быть транзакции от агрегаторов, таких как 1inch и 0x, или смарт-контракты, вызывающие свопы извне в Uniswap в целом.

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

Вот почему нам нужен более масштабируемый метод.

❓ Знаете ли вы, что можно выяснить, что транзакция будет делать в состоянии блокчейна, еще до того, как она будет подтверждена в следующем блоке?

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

Мы попробуем использовать метод eth_traceCall на Geth, чтобы выяснить, с какими пулами Uniswap V2 соприкасаются ожидающие транзакции — под «касанием» мы подразумеваем, какие состояния пулов изменяются в результате вызова.

debug_traceCall | Эфириум

Метод API Ethereum, который отслеживает выполнение eth_call в контексте выполнения конкретного блока.

docs.chainstack.com

Давайте создадим еще один файл в каталоге сэндвичаsandooo/src/sandwich/simulation.rs:use anyhow::Result;
use eth_encode_packed::ethabi::ethereum_types::{H160 as eH160, U256 as eU256};
use eth_encode_packed::{SolidityDataType, TakeLastXBytes};
use ethers::abi::ParamType;
use ethers::prelude::*;
use ethers::providers::{Provider, Ws};
use ethers::types::{transaction::eip2930::AccessList, Bytes, H160, H256, I256, U256, U64};
use log::info;
use revm::primitives::{Bytecode, U256 as rU256};
use std::{collections::HashMap, default::Default, str::FromStr, sync::Arc};

use crate::common::constants::{WETH, WETH_BALANCE_SLOT};
use crate::common::streams::{NewBlock, NewPendingTx};
use crate::common::utils::{create_new_wallet, is_weth, to_h160};

#[derive(Debug, Clone, Default)]
pub struct PendingTxInfo {
pub pending_tx: NewPendingTx,
pub touched_pairs: Vec<SwapInfo>,
}

#[derive(Debug, Clone)]
pub enum SwapDirection {
Buy,
Sell,
}

#[derive(Debug, Clone)]
pub struct SwapInfo {
pub tx_hash: H256,
pub target_pair: H160,
pub main_currency: H160,
pub target_token: H160,
pub version: u8,
pub token0_is_main: bool,
pub direction: SwapDirection,
}

pub static V2_SWAP_EVENT_ID: &str = «0xd78ad95f»;

pub async fn debug_trace_call(
provider: &Arc<Provider<Ws>>,
new_block: &NewBlock,
pending_tx: &NewPendingTx,
) -> Result<Option<CallFrame>> {
let mut opts = GethDebugTracingCallOptions::default();
let mut call_config = CallConfig::default();
call_config.with_log = Some(true); // 👈 make sure we are getting logs

opts.tracing_options.tracer = Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
));
opts.tracing_options.tracer_config = Some(GethDebugTracerConfig::BuiltInTracer(
GethDebugBuiltInTracerConfig::CallTracer(call_config),
));

let block_number = new_block.block_number;
let mut tx = pending_tx.tx.clone();
let nonce = provider
.get_transaction_count(tx.from, Some(block_number.into()))
.await
.unwrap_or_default();
tx.nonce = nonce;

let trace = provider
.debug_trace_call(&tx, Some(block_number.into()), opts)
.await;

match trace {
Ok(trace) => match trace {
GethTrace::Known(call_tracer) => match call_tracer {
GethTraceFrame::CallTracer(frame) => Ok(Some(frame)),
_ => Ok(None),
},
_ => Ok(None),
},
_ => Ok(None),
}

Функция debug_trace_call будет возвращать кадр вызова, возвращенный после трассировки ожидающих транзакций. Мы можем попробовать запустить его после того, как немного настроим функцию стратегии (sandooo/src/sandwich/strategy.rs):// … imports

pub async fn run_sandwich_strategy(provider: Arc<Provider<Ws>>, event_sender: Sender<Event>) {
let block = provider
.get_block(BlockNumber::Latest)
.await
.unwrap()
.unwrap();
let mut new_block = NewBlock {
block_number: block.number.unwrap(),
base_fee: block.base_fee_per_gas.unwrap(),
next_base_fee: calculate_next_block_base_fee(
block.gas_used,
block.gas_limit,
block.base_fee_per_gas.unwrap(),
),
};

let mut event_receiver = event_sender.subscribe();

loop {
match event_receiver.recv().await {
Ok(event) => match event {
Event::Block(block) => {
new_block = block;
info!(«[Block #{:?}]», new_block.block_number);
}
Event::PendingTx(mut pending_tx) => {
let frame = debug_trace_call(&provider, &new_block, &pending_tx).await;
match frame {
Ok(frame) => info!(«{:?}», frame),
Err(e) => info!(«{e:?}»),
}
}
},
_ => {}
}
}
}

Бег:cargo run

выдаст вам кадр вызова, который выглядит следующим образом:

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

Для рекурсивного извлечения журналов из кадров вызова мы используем еще одну вспомогательную функцию, которую мы определим в sandooo/src/sandwich/simulation.rs:pub fn extract_logs(call_frame: &CallFrame, logs: &mut Vec<CallLogFrame>) {
if let Some(ref logs_vec) = call_frame.logs {
logs.extend(logs_vec.iter().cloned());
}

if let Some(ref calls_vec) = call_frame.calls {
for call in calls_vec {
extract_logs(call, logs);
}
}
}

С помощью этой новой функции мы можем легко свести бревна в один вектор. Вы можете попробовать обновить функцию strategy.rs еще раз:loop {
match event_receiver.recv().await {
Ok(event) => match event {
Event::Block(block) => {
new_block = block;
info!(«[Block #{:?}]», new_block.block_number);
}
// just update this part 👇
Event::PendingTx(mut pending_tx) => {
let frame = debug_trace_call(&provider, &new_block, &pending_tx).await;
match frame {
Ok(frame) => match frame {
Some(frame) => {
let mut logs = Vec::new();
extract_logs(&frame, &mut logs);
info!(«{:?}», logs);
}
_ => {}
},
Err(e) => info!(«{e:?}»),
}
}
},
_ => {}
}
}

Попробуйте выполнить это, и вы увидите, что все журналы сведены в один вектор:

Некоторые трассы, конечно же, не будут иметь журналов.

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

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

Перейдя в Etherscan, вы можете сказать, что события Swap имеют селектор размером 4 байта 0xd78ad95f, что видно из topic0:

Uniswap V2: USDT | Адрес 0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 | Эфирскан

Страница 0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 адреса контракта позволяет пользователям просматривать исходный код…

etherscan.io

Поэтому мы добавим еще одну функцию в sandooo/src/sandwich/simulation.rs:pub async fn extract_swap_info(
provider: &Arc<Provider<Ws>>,
new_block: &NewBlock,
pending_tx: &NewPendingTx,
pools_map: &HashMap<H160, Pool>,
) -> Result<Vec<SwapInfo>> {
let tx_hash = pending_tx.tx.hash;
let mut swap_info_vec = Vec::new();

let frame = debug_trace_call(provider, new_block, pending_tx).await?;
if frame.is_none() {
return Ok(swap_info_vec);
}
let frame = frame.unwrap();

let mut logs = Vec::new();
extract_logs(&frame, &mut logs);

for log in &logs {
match &log.topics {
Some(topics) => {
if topics.len() > 1 {
let selector = &format!(«{:?}», topics[0])[0..10];
let is_v2_swap = selector == V2_SWAP_EVENT_ID;
if is_v2_swap {
let pair_address = log.address.unwrap();

// filter out the pools we have in memory only
let pool = pools_map.get(&pair_address);
if pool.is_none() {
continue;
}
let pool = pool.unwrap();

let token0 = pool.token0;
let token1 = pool.token1;

let token0_is_weth = is_weth(token0);
let token1_is_weth = is_weth(token1);

// filter WETH pairs only
if !token0_is_weth && !token1_is_weth {
continue;
}

let (main_currency, target_token, token0_is_main) = if token0_is_weth {
(token0, token1, true)
} else {
(token1, token0, false)
};

let (in0, _, _, out1) = match ethers::abi::decode(
&[
ParamType::Uint(256),
ParamType::Uint(256),
ParamType::Uint(256),
ParamType::Uint(256),
],
log.data.as_ref().unwrap(),
) {
Ok(input) => {
let uints: Vec<U256> = input
.into_iter()
.map(|i| i.to_owned().into_uint().unwrap())
.collect();
(uints[0], uints[1], uints[2], uints[3])
}
_ => {
let zero = U256::zero();
(zero, zero, zero, zero)
}
};

let zero_for_one = (in0 > U256::zero()) && (out1 > U256::zero());

let direction = if token0_is_main {
if zero_for_one {
SwapDirection::Buy
} else {
SwapDirection::Sell
}
} else {
if zero_for_one {
SwapDirection::Sell
} else {
SwapDirection::Buy
}
};

let swap_info = SwapInfo {
tx_hash,
target_pair: pair_address,
main_currency,
target_token,
version: 2,
token0_is_main,
direction,
};
swap_info_vec.push(swap_info);
}
}
}
_ => {}
}
}

Ok(swap_info_vec)
}

Мы отфильтровываем журналы в два этапа:

  1. Во-первых, отфильтровывая пулы, которые мы храним только в наших pools_map. Мы еще не добавили это, но мы добавим это в следующем разделе.
  2. Во-вторых, отфильтровывая только пулы пар WETH.

Как только мы это сделаем, мы расшифруем данные журнала, выполнив следующие действия:ethers::abi::decode(
&[
ParamType::Uint(256),
ParamType::Uint(256),
ParamType::Uint(256),
ParamType::Uint(256),
],
log.data.as_ref().unwrap(),
)

и извлеките значения amount0In, amount1In, amount0Out, amount1Out.

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

Теперь добавим способ обновления пулов Uniswap V2 и связанных с ними токенов ERC-20 при запуске программы, чтобы трассировка + извлечение логов работали.

Добавьте два новых файла в sandooo/src/common:

  1. pools.rs

sandooo/src/common/pools.rs на главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

2. tokens.rs

sandooo/src/common/tokens.rs на главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

3. bytecode.rs

sandooo/src/common/bytecode.rs на главной странице · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

После того, как вы это сделаете, давайте снова обновим strategy.rs файл в sandooo/src/sandwich/strategy.rs:pub async fn run_sandwich_strategy(provider: Arc<Provider<Ws>>, event_sender: Sender<Event>) {
let env = Env::new();

// load_all_pools:
// this will load all Uniswap V2 pools that was deployed after the block #10000000
let (pools, prev_pool_id) = load_all_pools(env.wss_url.clone(), 10000000, 50000)
.await
.unwrap();

// load_all_tokens:
// this will get all the token information including: name, symbol, symbol, totalSupply
let block_number = provider.get_block_number().await.unwrap();
let tokens_map = load_all_tokens(&provider, block_number, &pools, prev_pool_id)
.await
.unwrap();
info!(«Tokens map count: {:?}», tokens_map.len());

// filter pools that don’t have both token0 / token1 info
let pools_vec: Vec<Pool> = pools
.into_iter()
.filter(|p| {
let token0_exists = tokens_map.contains_key(&p.token0);
let token1_exists = tokens_map.contains_key(&p.token1);
token0_exists && token1_exists
})
.collect();
info!(«Filtered pools by tokens count: {:?}», pools_vec.len());

let pools_map: HashMap<H160, Pool> = pools_vec
.clone()
.into_iter()
.map(|p| (p.address, p))
.collect();

let block = provider
.get_block(BlockNumber::Latest)
.await
.unwrap()
.unwrap();
let mut new_block = NewBlock {
block_number: block.number.unwrap(),
base_fee: block.base_fee_per_gas.unwrap(),
next_base_fee: calculate_next_block_base_fee(
block.gas_used,
block.gas_limit,
block.base_fee_per_gas.unwrap(),
),
};

let mut event_receiver = event_sender.subscribe();

loop {
match event_receiver.recv().await {
Ok(event) => match event {
Event::Block(block) => {
new_block = block;
info!(«[Block #{:?}]», new_block.block_number);
}
Event::PendingTx(mut pending_tx) => {
let swap_info =
extract_swap_info(&provider, &new_block, &pending_tx, &pools_map).await;
info!(«{:?}», swap_info);
}
},
_ => {}
}
}
}

👏👏 👏 Теперь создайте новый каталог из корневого каталога: sandooo/cache. Эта часть важна, потому что pools.rs и tokens.rs создадим файловый кеш всех существующих пулов и токенов Uniswap V2 в кэшированном csv-файле для быстрой загрузки при перезапуске системы.

Когда вы запустите систему после создания каталога кэша, вы увидите, что программа начнет загружать пулы, используя конечную точку узла RPC:

После того, как программа поработает некоторое время (это заняло у меня около 30 минут при использовании полного узла), она начнет печатать информацию о подкачке, которую мы извлекли из трассировок гетов.

Как видите, мы получаем target_pair, main_currency, target_token и правильное направление свопа отложенных сделок.

Наконец-то мы готовы перейти к более интересной части нашего анализа: пониманию структуры прибыли и затрат на сэндвич-пакеты.

🥪 Какие из этих транзакций можно сэндвичировать?

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

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

  • Транзакция с опережением: WETH → Целевой токен (КУПИТЬ)
  • Транзакция жертвы: WETH → Целевой токен (КУПИТЬ)
  • Обратная транзакция: Целевой токен → WETH (ПРОДАВАТЬ)

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

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

Поскольку теперь мы можем отслеживать все транзакции покупки и продажи из пулов Uniswap V2 с помощью системы, которую мы настроили в предыдущем разделе, мы можем попробовать сгруппировать их в пакеты транзакций front-run, victim-run и back-run и выяснить следующее:

  1. максимальное количество токенов, которое мы можем купить до жертвы, чтобы убедиться, что транзакция жертвы не откатится (транзакции могут быть отменены из-за уровней допуска проскальзывания, которые пользователь устанавливает при использовании контракта Uniswap V2 Router)
  2. Максимальная сумма прибыли, которую мы можем ожидать, если все три транзакции пройдут без возврата

с помощью нашего механизма моделирования.

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

Сэндвич-смарт-контракт с использованием Solidity/Yul

Торговля в сети отличается от торговли вне сети на CEX, таких как Binance, Bybit и т. д. Я не буду говорить о том, что сложнее, потому что это субъективный вопрос. Некоторые утверждают, что строить стратегии на CEX сложнее из-за быстрых движений цен, в то время как другие утверждают, что ончейн-торговля более сложна из-за увеличения времени создания блоков, что создает новые проблемы. Обе точки зрения имеют свои достоинства, и достижение прибыльности на любой платформе — непростая задача.

Тем не менее, одно можно сказать наверняка: аспект исполнения MEV значительно сложнее, чем торговля на CEX.

Если вы стремитесь быть прибыльным в MEV, очень важно иметь четкое представление о разработке безопасного и эффективного смарт-контракта.

Читатели, незнакомые с Юлом, могут обратиться к моей предыдущей статье на эту тему:

Как использовать Yul в MEV-проекте

Руководство от А до Я по снижению затрат на газ и использованию сборки для обработки ошибок, передачи токенов, обмена токенов и многого другого

medium.com

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

Договор предоставляется здесь:

sandooo/contracts/src/Sandooo.sol в главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

Это очень простой контракт, который написан на Yul с использованием Foundry.

Мы просто вкратце рассмотрим резервную функцию:fallback() external payable {
// We check that the msg.sender is the owner who deployed the contract
require(msg.sender == owner, «NOT_OWNER»);

assembly {
let ptr := mload(0x40)
let end := calldatasize()

// the first 8 bytes (64 bits, uint64) of the calldata is the block_number
// we want to make sure that our transactions are valid only on
// the block that we’ve specified
let block_number := shr(192, calldataload(0))
if iszero(eq(block_number, number())) {
revert(0, 0)
}

// we can pass in multiple swap instructions
// which we’ll use later when we group multiple sandwiches together
for {
let offset := 8
} lt(offset, end) {

} {
let zeroForOne := shr(248, calldataload(offset)) // 1 byte
let pair := shr(96, calldataload(add(offset, 1))) // 20 bytes
let tokenIn := shr(96, calldataload(add(offset, 21))) // 20 bytes
let amountIn := calldataload(add(offset, 41)) // 32 bytes
let amountOut := calldataload(add(offset, 73)) // 32 bytes
offset := add(offset, 105) // 1 + 20 + 20 + 32 + 32

// transfer tokenIn to pair contract first
mstore(ptr, TOKEN_TRANSFER_ID)
mstore(add(ptr, 4), pair)
mstore(add(ptr, 36), amountIn)

if iszero(call(gas(), tokenIn, 0, ptr, 68, 0, 0)) {
revert(0, 0)
}

// call swap function in UniswapV2Pair contract
// zeroForOne means the transaction is a swap going from token0 to token1
// Uniswap V2 swap function expects us to pass it in the amountOut value
// so if zeroForOne == 1 (true), the out token is token1
// and if zeroForOne == 0 (false), the out token is token0
mstore(ptr, V2_SWAP_ID)
switch zeroForOne
case 0 {
mstore(add(ptr, 4), amountOut)
mstore(add(ptr, 36), 0)
}
case 1 {
mstore(add(ptr, 4), 0)
mstore(add(ptr, 36), amountOut)
}
mstore(add(ptr, 68), address())
mstore(add(ptr, 100), 0x80)

if iszero(call(gas(), pair, 0, ptr, 164, 0, 0)) {
revert(0, 0)
}
}
}
}

Механизм сэндвич-моделирования с использованием REVM

Теперь, когда контракт готов, мы наконец-то можем провести реальное моделирование.

Всего мы выполним три этапа моделирования:

  1. Симуляция закуски
  2. Моделирование оптимизации входных сумм
  3. Симуляция основного блюда

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

Симуляция введение

sandooo/src/sandwich/appetizer.rs на главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

В симуляции закуски мы пытаемся передать 0,1 WETH в качестве amountIn нашей первой сделки на покупку (front-run tx) и пытаемся понять, прибыльна ли она.

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

Моделирование оптимизации

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

Проверить процесс оптимизации можно в этом файле:

sandooo/src/sandwich/simulation.rs на главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

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

Главная симуляция

sandooo/src/sandwich/main_dish.rs в главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

Когда мы закончим этап оптимизации, мы используем этот результат для запуска еще одного моделирования и вычисления точного значения дохода.

Выручка рассчитывается следующим образом:

  • Прибыль = Баланс WETH контракта после — баланс WETH до
  • Стоимость = Баланс ETH пользователя после — баланс ETH до
  • Выручка = Прибыль — Затраты

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

Запуск системы со всеми этими компонентами приведет к следующей гифке. Попробуйте выполнить:cargo run

Проверка работы сэндвич-бота

Есть ли у нас преимущество на рынке MEV

Fotor AI: «Богатые чихуахуа на вечеринке» Извините за отрезанное ухо чихуахуа #1

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

Точно так же создание MEV-ботов также доставляет удовольствие, и это также включает в себя шлифовку и понимание того, почему наш бот работает или нет. Но это немного отличается от ИИ, потому что мы можем потерять деньги, пока тестируем наши стратегии. Поэтому мы должны очень тщательно проанализировать наши конкурентные преимущества и протестировать все, прежде чем мы начнем запускать нашу систему.

Сегодня мы продолжим с того места, на котором остановились:

100 часов создания сэндвич-бота

От А до Я: Создайте своего собственного сэндвич-бота правильным способом

medium.com

Мы создали систему, которая могла обнаруживать сэндвич-возможности от разветвленных DEX Uniswap V2. И мы хотели бы посмотреть, сможем ли мы действительно получить прибыль от отправки пакетов в сети.

Для читателей, которые еще не прочитали код, вот код:

GitHub — solidquant/sandooo: сэндвич-бот

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

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

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

  1. Вы научитесь разбирать механизм симуляции, чтобы его можно было применить и к стейблкоинам.
  2. Вы также научитесь группировать свои бутерброды (сэндвичи с несколькими жертвами) и максимизировать прибыль.
  3. Наконец, вы узнаете, как распространить эту модель на пулы Uniswap V3.

Интеграция этих дополнительных функций значительно повысит вероятность успеха системы в реальных сделках.

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

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

Прежде чем мы продолжим, я хотел бы отметить одну вещь.

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

Является ли бот прибыльным?

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

Есть персонаж, который мне очень нравился в One Piece, Роб Луччи.

One Piece: Роб Луччи, CP9

Для тех из вас, кто не знаком. Роб Луччи тренировал свое тело, чтобы оно само стало оружием. А его навык, Шиган 👉, способен смертельно поразить вооруженного человека одним только указательным пальцем.

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

Содержание

  1. Анализ конкурентоспособности сэндвич-бота
  2. Широковещательная рассылка пакетов нескольким сборщикам
  3. Дальнейшие действия по оптимизации

В сегодняшней статье мы рассмотрим несколько интересных тем.

И если вы когда-нибудь где-нибудь застрянете, пожалуйста, не стесняйтесь обращаться к другим и спрашивать, сталкиваются ли они с подобными проблемами на сервере 🙏 Discord:

Присоединяйтесь к серверу Solid Quant в Discord!

Квантовые любители, занимающиеся квантовыми вещами — в настоящее время в основном связанные с MEV. | 1535 участников

discord.com

Давайте начнем! 🏎

Анализ конкурентоспособности сэндвич-бота

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

Снимок кода находится в другой ветке репозитория Github, для этого можно перейти в ветку phase1:

GitHub — solidquant/sandooo на этапе 1

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

Попробуйте запустить код, выполнив следующие действия:cargo run

Программа обновит новые пулы и токены, которые были запущены на Uniswap V2 с момента нашего последнего запуска, и начнет мониторинг сэндвич-возможностей.

Мы даем ему поработать некоторое время 😴.

И мы обнаруживаем наш первый сэндвич через 5 блоков:

Мы видим, что транзакция жертвы выглядит следующим образом:

0xd1a41244a9aab38f41ce5fb54ce5ba3bcd20e07afb439bc968b720f5031feb80

Детали хеша транзакции Ethereum (Txhash) | Эфирскан

Ethereum (ETH) подробная информация о транзакциях для txhash 0xd1a41244a9aab38f41ce5fb54ce5ba3bcd20e07afb439bc968b720f5031feb80…

etherscan.io

Это транзакция, которая совершает сделку с помощью универсального маршрутизатора.

Оптимизированный токен по объему составил 2.0 WETH, и мы могли рассчитывать на прибыль в размере 0.0516 WETH от сэндвич-пакета.

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

  • Использование газа для транзакций Frontrun: 116 956
  • Использование газа для транзакций обратного выполнения: 106 769

А базовая плата составляет 22,27 GWEI, поэтому наши общие затраты на газ в конечном итоге находятся в диапазоне:

0.00475 ~ 0.00498 ETH

Может быть небольшая разница в стоимости газа из-за возврата газа и того, как foundry-evm рассчитывает это, поэтому мы будем использовать значение, которое мы получаем от нашего механизма моделирования, которое составляет 0,00475 ETH.

Тогда ожидается, что наша выручка составит:

  • 0.0516 — 0.00475 = 0.0469 WETH

Давайте посмотрим, конкурентоспособен ли наш набор сэндвичей.

Используя сервис Auction Stats от Gambit Labs, мы можем выяснить, сколько людей отправляли наборы, чтобы воспользоваться этой возможностью, и увидеть, сколько взяток они отправляют строителям.

Вставьте хеш транзакции из вкладки «Статистика аукциона» и давайте посмотрим, насколько мы были конкурентоспособны:

Аукцион — Gambit Labs

Статистика аукционов поисковиков

www.gmbit.co

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

Наш доход прямо сейчас будет на уровне 15~16

Главный взяточник Gambit Labs использовал в качестве взятки 0,058102 ETH. А наш доход с учетом затрат на газ составляет 0,0469 ETH, поэтому мы видим, что нам нужно еще немного оптимизировать наш контракт, чтобы выиграть.

На этот раз давайте отправимся в Eigenphi и посмотрим, кто из поисковиков на самом деле выиграл эту сделку:

Блок-схема транзакции |…

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

eigenphi.io

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

Его опережающая транзакция выглядит следующим образом:

и его обратная транзакция следующим образом:

Во-первых, мы проверяем, правильно ли были сделаны наши оптимизации. И кажется, что так оно и есть:

Джаред получил значение 2 с чем-то WETH в качестве оптимизированной суммы в стоимости.

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

Доход, на который мы могли рассчитывать, составляет:

0,0469 ETH * 2 300 USD (текущая цена ETH) = 107,87 USD

в то время как Джаред способен генерировать $160.

Это связано с тем, что Джаред также занимается арбитражем в своих сделках frontrun/backrun.

Давайте вкратце рассмотрим, что он делает в опережающей транзакции:

Вы можете видеть, что Джаред делает 2-хоповый арбитраж между Uniswap V3 и V2. Он смог заметить возможность арбитража на пулах dogwifhat в двух DEX и добавить это в свою транзакцию frontrun.

Откуда мы знаем, что это возможность арбитража?

Потому что Джаред торгует на двух пулах:

  • Uniswap V3: 0xB9aD117834579543Ed5E79f2a32476d50D7cE35F
  • Uniswap V2: 0x11C20A3b83FF206e4aB6b5935D766564925b8B2b

Они оба сочетаются с:

Ну, это облом… потому что мы никогда не сможем победить Джареда сейчас, если не внедрим арбитраж в нашу систему.

Пока не нужно разочаровываться, потому что все станет только хуже, когда вы увидите, как он выходит из своих первоначальных токенов RSTK (то, чем жертва пыталась торговать на Universal Router).

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

Вот это странно. Я думал, что нам нужно выйти только из позиции RSTK, используя первоначальный пул Uniswap V2, верно?

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

И это именно то, что делает Джаред. Бот Джареда обнаруживает расхождение в цене между пулами Uniswap V3 и V2 RSTK и проводит арбитраж, получая от этого дополнительную прибыль.

Благодаря этому он может заработать как минимум на 50 долларов больше на той же возможности, которую мы заметили.

📍 На самом деле, у Джареда есть чему поучиться. Он также занимается:
1. JIT-обеспечение ликвидности в его опережающих сделках, 2. подбирает несколько транзакций жертвы, чтобы сжать их, 3. покупает мемкоины и выполняет сэндвич-стратегии, отличные от WETH, используя эти токены. Но это после того, как мы овладеем искусством простых стратегий сэндвич + арбитраж.

Я надеюсь, что это может дать вам представление о том, насколько конкурентоспособными вы должны быть, чтобы победить на рынке сэндвичей Ethereum в настоящее время.

Давайте попробуем запустить нашего сэндвич-бота немного дольше:

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

  • Gambit Labs: краткий обзор того, сколько людей соревнуется и сколько они подкупают
  • Eigenphi: кто на самом деле выигрывает бандлы и какую стратегию использует

Это даст вам очень четкое представление о том, сколько еще нам нужно оптимизировать наш код.

Широковещательная рассылка пакетов нескольким сборщикам

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

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

  1. Как быстро мы их подаем,
  2. Сколько мы можем подкупить строителей.

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

Перед тем, как мы действительно развернем данный контракт здесь:

sandooo/contracts/src/Sandooo.sol в главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

Мы быстро проведем несколько тестов и посмотрим, все ли функции работают так, как задумано.

👉 Во-первых, давайте посмотрим, сможем ли мы отправить токены ETH и ERC-20 на наш контракт и вернуть их обратно. Одна ошибка, которую я допустил, когда только начинал, заключалась в том, что я забыл добавить эту функцию, и мне пришлось посмотреть, как мой ETH заблокирован в контракте. Надеюсь, мы знаем, что с контрактом Sandooo этого не произойдет.

Запустите процесс Anvil, выполнив:anvil —fork-url http://localhost:8545 —port 2000

Я запущу форк основной сети и запущу Anvil на порту 2000.

Далее запишем тестовую функцию для контракта следующим образом в sandooo/contracts/test/Sandooo.t.sol:

sandooo/contracts/test/Sandooo.t.sol на главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.compragma solidity 0.8.20;

import «forge-std/Test.sol»;
import «forge-std/console.sol»;

import «../src/Sandooo.sol»;

contract SandoooTest is Test {
Sandooo bot;
IWETH weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

receive() external payable {}

function test() public {
console.log(«Sandooo bot test starting»);

// Create Sandooo instance
bot = new Sandooo();

uint256 amountIn = 100000000000000000; // 0.1 ETH

// Wrap 0.1 ETH to 0.1 WETH and send to Sandooo contract
weth.deposit{value: amountIn}();
weth.transfer(address(bot), amountIn);

// Check if WETH is properly sent
uint256 botBalance = weth.balanceOf(address(bot));
console.log(«Bot WETH balance: %s», botBalance);

// Check if we can recover WETH
bot.recoverToken(address(weth), botBalance);
uint256 botBalanceAfterRecover = weth.balanceOf(address(bot));
console.log(
«Bot WETH balance after recover: %s»,
botBalanceAfterRecover
); // should be 0

// Check if we can recover ETH
(bool s, ) = address(bot).call{value: amountIn}(«»);
console.log(«ETH transfer: %s», s);
uint256 testEthBal = address(this).balance;
uint256 botEthBal = address(bot).balance;
console.log(«Curr ETH balance: %s», testEthBal);
console.log(«Bot ETH balance: %s», botEthBal);

// Send zero address to retrieve ETH
bot.recoverToken(address(0), botEthBal);

uint256 testEthBalAfterRecover = address(this).balance;
uint256 botEthBalAfterRecover = address(bot).balance;
console.log(«ETH balance after recover: %s», testEthBalAfterRecover);
console.log(«Bot ETH balance after recover: %s», botEthBalAfterRecover);

console.log(«============================»);
}
}

и выполните:forge test —fork-url http://localhost:2000 —match-contract SandoooTest -vv

и проверяем логи, которые мы получаем:[PASS] test() (gas: 265096)
Logs:
Sandooo bot test starting
Bot WETH balance: 100000000000000000
Bot WETH balance after recover: 0
ETH transfer: true
Curr ETH balance: 79228162514064337593543950335
Bot ETH balance: 100000000000000000
ETH balance after recover: 79228162514164337593543950335
Bot ETH balance after recover: 0
============================

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

👉 Далее мы попробуем сделать симуляцию свопа на паре Uniswap V2 и подтвердим, что наш контракт действительно работает.

Попробуйте добавить это в тестовую функцию, которую мы писали ранее:// Transfer WETH to contract again
weth.transfer(address(bot), amountIn);
uint256 startingWethBalance = weth.balanceOf(address(bot));
console.log(«Starting WETH balance: %s», startingWethBalance);

address usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address wethUsdtV2 = 0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852;

IUniswapV2Pair pair = IUniswapV2Pair(wethUsdtV2);
address token0 = pair.token0();
address token1 = pair.token1();

// We will be testing WETH —> USDT
// So it’s zeroForOne if WETH is token0
uint8 zeroForOne = address(weth) == token0 ? 1 : 0;

// Calculate the amountOut using reserves
(uint112 reserve0, uint112 reserve1, ) = IUniswapV2Pair(address(pair))
.getReserves();

uint256 reserveIn;
uint256 reserveOut;

if (zeroForOne == 1) {
reserveIn = reserve0;
reserveOut = reserve1;
} else {
reserveIn = reserve1;
reserveOut = reserve0;
}

uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
uint256 targetAmountOut = numerator / denominator;

console.log(«Amount in: %s», amountIn);
console.log(«Target amount out: %s», targetAmountOut);

bytes memory data = abi.encodePacked(
uint64(block.number), // blockNumber
uint8(zeroForOne), // zeroForOne
address(pair), // pair
address(weth), // tokenIn
uint256(amountIn), // amountIn
uint256(targetAmountOut) // amountOut
);
console.log(«Calldata:»);
console.logBytes(data);

uint gasBefore = gasleft();
(bool success, ) = address(bot).call(data);
uint gasAfter = gasleft();
uint gasUsed = gasBefore — gasAfter;
console.log(«Swap success: %s», success);
console.log(«Gas used: %s», gasUsed);

uint256 usdtBalance = IERC20(usdt).balanceOf(address(bot));
console.log(«Bot USDT balance: %s», usdtBalance);

require(success, «FAILED»);

Мы попробуем купить немного USDT с помощью WETH.

Попробуйте запустить тест с помощью команды:forge test —fork-url http://localhost:2000 —match-contract SandoooTest -vv

и получим:[PASS] test() (gas: 348846)
Logs:
Sandooo bot test starting
Bot WETH balance: 100000000000000000
Bot WETH balance after recover: 0
ETH transfer: true
Curr ETH balance: 79228162514064337593543950335
Bot ETH balance: 100000000000000000
ETH balance after recover: 79228162514164337593543950335
Bot ETH balance after recover: 0
============================
Starting WETH balance: 100000000000000000
Amount in: 100000000000000000
Target amount out: 229783289
Calldata:
0x0000000001244bcc010d4a11d5eeaac28ec3f61d100daf4d40471f1852c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000000000000000000000000000000000000db236f9
Swap success: true
Gas used: 82214
Bot USDT balance: 229783289

Мы видим, что тест удался.

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

К счастью, этот контракт очень прост, поэтому скомпилированный байт-код сводится к следующему:

0x6080604052600436101561001e575b361561001c5761001c61012d565b005b6000803560e01c80638da5cb5b146100d05763b29a814014610040575061000e565b3461009e57604036600319011261009e57806001600160a01b0360043581811681036100cc576100776024359284541633146100f5565b82811591826000146100a157505060011461008f5750f35b81808092335af11561009e5780f35b80fd5b60449250908093916040519263a9059cbb60e01b845233600485015260248401525af11561009e5780f35b5050fd5b503461009e578060031936011261009e57546001600160a01b03166080908152602090f35b156100fc57565b60405162461bcd60e51b81526020600482015260096024820152682727aa2fa7aba722a960b91b6044820152606490fd5b60008054610145906001600160a01b031633146100f5565b60405143823560c01c03610209576008600482019160248101925b36831061016e575050505050565b823560f81c926060906001810135821c916015820135901c9487806044878260298701359a6069604989013598019b63a9059cbb60e01b8452898b528d525af1156102055784888094819460a49463022c0d9f60e01b8552806000146101f9576001146101ee575b50306044840152608060648401525af1610160578480fd5b8288528a52386101d6565b508752818a52386101d6565b8780fd5b5080fdfea264697066735822122070cd8d8a51fe625e0f10f1ea26f94679859661cf1936f171d337a6616cfb19ad64736f6c63430008140033

Это очень коротко, не находите?

Я попробовал развернуть это в основной сети с помощью команды:forge create —rpc-url <your_rpc_url> —private-key <your_private_key> src/Sandooo.sol:Sandooo

и я использовал 181 016 в газе, и в:

  • Базовая плата: 10.6 GWEI
  • Максимальная стоимость: 21.57 GWEI
  • Максимальная плата за приоритет: 3 GWEI

использовал 0.00246 ETH для развертывания контракта. Это $5.67.

Попробуйте выполнить извлечение из Github, и вы увидите, что ветка phase2 была объединена с нашей основной веткой.

И есть execution.rs файл в каталоге sandooo/src:

sandooo/src/common/execution.rs в главной · Солидквант/Санду

Сэндвич-бот. Внесите свой вклад в разработку solidquant/sandooo, создав учетную запись на GitHub.

github.com

Если вы планируете протестировать это, пожалуйста, перепроверьте всю логику и будьте осторожны! Существует так много причин, по которым развертывание по контракту может пойти не так, что я всегда предпринимаю дополнительные шаги, чтобы ничего не упустить. (🛑 ( Кроме того, не доверяйте тому, что я говорю вам в этой статье, убедитесь в логике сами, прежде чем вы сможете попробовать что-то в основной сети. На самом деле, никому 🛑 не доверяйте)

Измените поле DEBUG в файле .env, чтобы код мог выполняться с использованием реального WETH в контракте:HTTPS_URL=http://localhost:8545
WSS_URL=ws://localhost:8546
BOT_ADDRESS=
PRIVATE_KEY=
IDENTITY_KEY=
TELEGRAM_TOKEN=
TELEGRAM_CHAT_ID=
USE_ALERT=false
DEBUG=false // <— change this to false to run with real bot
RUST_BACKTRACE=1

Обязательно добавьте реальный BOT_ADDRESS и PRIVATE_KEY адреса, который использовался для развертывания контракта бота.

Я попробовал отправить 0.5 WETH на контракт, чтобы проверить свою логику.

После этого мы можем попробовать запустить нашего бота:cargo run

Теперь ждем и молимся, чтобы все прошло хорошо. 🙏

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

На этот раз мы хотим сосредоточиться на части нашего журнала «Bundle sent»:Bundle sent:
{
«gambit»: SendBundleResponse { bundle_hash: 0x9b728ef4bb79af616b2aa9f49d703e2b2f1e28ce8a8115b6cb3622db4ec8ccaa },
«flashbots»: SendBundleResponse { bundle_hash: 0x9b728ef4bb79af616b2aa9f49d703e2b2f1e28ce8a8115b6cb3622db4ec8ccaa },
«rsync»: SendBundleResponse { bundle_hash: 0x0000000000000000000000000000000000000000000000000000000000000000 },
«penguinbuild»: SendBundleResponse { bundle_hash: 0x0000000000000000000000000000000000000000000000000000000000000000 },
«titanbuilder»: SendBundleResponse { bundle_hash: 0x63c4bbb135785635c9dde5b571a082800ede504597c09967d84614df7150f321 },
«builder0x69»: SendBundleResponse { bundle_hash: 0x0000000000000000000000000000000000000000000000000000000000000000 },
«beaverbuild»: SendBundleResponse { bundle_hash: 0x0d0a698ee45dae9a516582651e853f8426fb41ac74fe787f7b4b1059dcec5d95 }
}

Мы видим, что успешно отправили в Gambit Labs, Flashbots, Rsync, Penguin Build, Titan Builder, Builder0x69 и Beaverbuild.

Нас интересует Gambit Labs, поэтому давайте зайдем на их веб-сайт, проверим хеш транзакции и посмотрим, какова конкуренция:

0x1924235bfe061560fd9725320cf4c26825422ea1aff42ec913a76e53370d7199

Вот хэш транзакции жертвы:

Аукцион — Gambit Labs

Статистика аукционов поисковиков

www.gmbit.co

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

Сравните сумму взятки с суммой взятки, предложившей самую высокую цену, которая заплатила 0,009547 ETH в качестве взятки. Тот, кто предложит самую высокую цену, платит в два раза больше, чем мы.

Чтобы понять, почему это может быть так. На этот раз отправляйтесь в Eigenphi и посмотрите, какова была оптимизированная сумма в стоимости. Наша стоимость 0.39728 WETH.

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

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

Это то же самое, что и у Джареда:

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

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

Источник

Источник