添加稳定币三明治和组捆绑功能以优化我们的三明治机器人

本文介绍了如何在 sandwich bot 中添加稳定币(USDT/USDC)支持,并且通过分组打包多个交易来最大化利润。文章详细讲解了代码的更新和实现逻辑,特别是如何在不同币种之间进行转换和计算利润。

让我们这次加入稳定币三明治!

Fotor AI: “一只芝娃娃在三明治上”

大家好!本文是我们三明治机器人系列的第三篇。还没有阅读之前文章的读者可以参考以下文章以获取一些背景信息:

  1. 设置三明治机器人:

构建三明治机器人所需的100小时

2. 发送三明治捆绑包:

让我们看看我们的三明治机器人是否真的有效

本系列中的所有代码将在Github上开源:

GitHub - solidquant/sandooo: 一款三明治机器人

✅ 在本文中,我们将添加非-WETH三明治捆绑包——特别是使用USDT/USDC的稳定币三明治——并看看它们是否能帮助改善我们的系统。

✅ 做完这一步后,我们将尝试合并多个三明治的捆绑包,并努力最大化利润,从而增加我们的获胜机会。

有了这些新特性,我们的系统将进一步接近生产就绪状态。不过不要忘记,我们目前只在做Uniswap V2三明治,下周还会加入V3订单。最终,我们将拥有一个完整的系统,可以完成以下任务:

  • Uniswap V2 三明治
  • Uniswap V3 三明治
  • 稳定币 三明治
  • 多个三明治的组合捆绑

添加更多货币

到目前为止,我们一直使用WETH代币作为我们的主要货币。但今天我们希望改变这一点。我们希望让我们的系统更灵活,所以它也能交易USDT对、USDC对等。

我们将立即开始推动一些更新到我们的代码,以实现这一目标。

现在,我们处于我们的路线图的phase2阶段:

GitHub - solidquant/sandooo在phase2上

现在我们将转移到phase3

GitHub - solidquant/sandooo在phase3上

我们首先添加希望使用的其他货币,而不是WETH代币。我们需要更改sandooo/src/common/constants.rs文件:

sandooo/src/common/constants.rs在phase3上 · solidquant/sandooo

之前:

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

之后:

pub static WETH: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
pub static USDT: &str = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
pub static USDC: &str = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

pub static WETH_BALANCE_SLOT: i32 = 3;
pub static USDT_BALANCE_SLOT: i32 = 2;
pub static USDC_BALANCE_SLOT: i32 = 9;

pub static WETH_DECIMALS: u8 = 18;
pub static USDT_DECIMALS: u8 = 6;
pub static USDC_DECIMALS: u8 = 6;

我们会添加USDT和USDC代币,并为每种代币指定余额槽值。这些代币的余额槽值可以通过调用get_balance_slot方法的EvmSimulator (sandooo/src/common/evm.rs)找到:

sandooo/src/common/evm.rs在phase3上 · solidquant/sandooo

pub fn get_balance_slot(&mut self, token_address: H160) -> Result<i32> {
    let calldata = self.abi.token.encode("balanceOf", token_address)?;
    self.evm.env.tx.caller = self.owner.into();
    self.evm.env.tx.transact_to = TransactTo::Call(token_address.into());
    self.evm.env.tx.data = calldata.0;
    let result = match self.evm.transact_ref() {
        Ok(result) => result,
        Err(e) => return Err(anyhow!("EVM ref call failed: {e:?}")),
    };
    let token_b160: B160 = token_address.into();
    let token_acc = result.state.get(&token_b160).unwrap();
    let token_touched_storage = token_acc.storage.clone();
    for i in 0..30 {
        let slot = keccak256(&abi::encode(&[\
            abi::Token::Address(token_address.into()),\
            abi::Token::Uint(U256::from(i)),\
        ]));
        let slot: rU256 = U256::from(slot).into();
        match token_touched_storage.get(&slot) {
            Some(_) => {
                return Ok(i);
            }
            None => {}
        }
    }

    Ok(-1)
}

如下面调用此函数:

let mut simulator = EvmSimulator::new(provider.clone(), None, block_number);

let usdt = H160::from_str(USDT).unwrap();
let balance_slot = simulator.get_balance_slot(usdt).unwrap();
println!("USDT balance slot: {}", balance_slot); // 2

将得到我们想要的结果。这个函数将通过静态调用ERC-20函数balanceOf来推断余额槽值,该函数必须引用代币合约的余额映射。

然而,这种方法并不是解决余额槽的完美方案。它对那些具有独立实现和存储合约的代币(即代理代币)不会有效。但这在目前足够用了,因为它适用于所有WETH、USDT、USDC代币。

接下来更新我们的sandooo/src/common/utils.rs

sandooo/src/common/utils.rs在phase3上 · solidquant/sandooo

##[derive(Debug, Clone)]
pub enum MainCurrency {
    WETH,
    USDT,
    USDC,

    Default, // 不是WETH/稳定币对的对。暂时默认使用WETH
}

impl MainCurrency {
    pub fn new(address: H160) -> Self {
        if address == to_h160(WETH) {
            MainCurrency::WETH
        } else if address == to_h160(USDT) {
            MainCurrency::USDT
        } else if address == to_h160(USDC) {
            MainCurrency::USDC
        } else {
            MainCurrency::Default
        }
    }

    pub fn decimals(&self) -> u8 {
        match self {
            MainCurrency::WETH => WETH_DECIMALS,
            MainCurrency::USDT => USDC_DECIMALS,
            MainCurrency::USDC => USDC_DECIMALS,
            MainCurrency::Default => WETH_DECIMALS,
        }
    }

    pub fn balance_slot(&self) -> i32 {
        match self {
            MainCurrency::WETH => WETH_BALANCE_SLOT,
            MainCurrency::USDT => USDT_BALANCE_SLOT,
            MainCurrency::USDC => USDC_BALANCE_SLOT,
            MainCurrency::Default => WETH_BALANCE_SLOT,
        }
    }

    /*
    我们根据重要性对货币进行评分
    WETH具有最高的重要性,USDT、USDC依次排序
    */
    pub fn weight(&self) -> u8 {
        match self {
            MainCurrency::WETH => 3,
            MainCurrency::USDT => 2,
            MainCurrency::USDC => 1,
            MainCurrency::Default => 3, // 默认是WETH
        }
    }
}

我们添加了对我们的新主要货币的处理。它是一个简单的枚举数据类型,可以返回主要货币的小数点和余额槽值。但你会注意到我们有一个额外的字段叫“weight”。这个新字段在另一个新函数中使用(仍然在sandooo/src/common/utils.rs):

pub fn return_main_and_target_currency(token0: H160, token1: H160) -> Option<(H160, H160)> {
    let token0_supported = is_main_currency(token0);
    let token1_supported = is_main_currency(token1);

    if !token0_supported && !token1_supported {
        return None;
    }

    if token0_supported && token1_supported {
        let mc0 = MainCurrency::new(token0);
        let mc1 = MainCurrency::new(token1);

        let token0_weight = mc0.weight();
        let token1_weight = mc1.weight();

        if token0_weight > token1_weight {
            return Some((token0, token1));
        } else {
            return Some((token1, token0));
        }
    }

    if token0_supported {
        return Some((token0, token1));
    } else {
        return Some((token1, token0));
    }
}

我们使用权重值来确定在试图交易两个主要货币的对(如WETH-USDT、USDT-USDC对)时,应该优先使用哪主要货币。

使我们的模拟器支持多货币

为了支持稳定币三明治,我们接下来必须更新我们模拟器中的代码,在sandooo/src/sandwich/simulation.rs中。我们将从extract_swap_info函数开始。

sandooo/src/sandwich/simulation.rs在phase3上 · solidquant/sandooo

之前:

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

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

// 只筛选WETH对
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 token0 = pool.token0;
let token1 = pool.token1;

let (main_currency, target_token, token0_is_main) =
    match return_main_and_target_currency(token0, token1) {
        Some(out) => (out.0, out.1, out.0 == token0),
        None => continue,
    };

我们不再过滤掉WETH对,而是使用我们新增的函数return_main_and_target_currency来确定主要货币。

👆 执行非-WETH对三明治的最棘手之处在于确定捆绑包的盈利能力。

因为我们将以这种方式进行交换:

  • 抢跑交易:USDT/USDC → 目标代币
  • 尾随交易:目标代币 → USDT/USDC

你可以看到,我们的利润将以USDT/USDC表示,而不是WETH,因此很难扣除Gas费用,并评估我们的捆绑包是否盈利。

我们添加一个聪明的技巧来解决这个问题。

如果我们以这种方式考虑非-WETH三明治会怎样:

  • 抢跑交易:USDT/USDC → 目标代币
  • 尾随交易:目标代币 → USDT/USDC
  • USDT/USDC → WETH(额外的交换步骤将输出代币金额转换为WETH)

当然,我们实际上不会进行交换,因为那是我们交易中的额外步骤,会消耗更多Gas,最终导致我们的三明治捆绑包无法竞争。

但我们会通过添加一个转换函数来假设我们这样做,以将USDT/USDC金额以WETH值进行考虑。这些函数如下(sandooo/src/sandwich/simulation.rs):

pub fn convert_usdt_to_weth(
    simulator: &mut EvmSimulator<Provider<Ws>>,
    amount: U256,
) -> Result<U256> {
    let conversion_pair = H160::from_str("0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852").unwrap();
    // token0: WETH / token1: USDT
    let reserves = simulator.get_pair_reserves(conversion_pair)?;
    let (reserve_in, reserve_out) = (reserves.1, reserves.0);
    let weth_out = get_v2_amount_out(amount, reserve_in, reserve_out);
    Ok(weth_out)
}

pub fn convert_usdc_to_weth(
    simulator: &mut EvmSimulator<Provider<Ws>>,
    amount: U256,
) -> Result<U256> {
    let conversion_pair = H160::from_str("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc").unwrap();
    // token0: USDC / token1: WETH
    let reserves = simulator.get_pair_reserves(conversion_pair)?;
    let (reserve_in, reserve_out) = (reserves.0, reserves.1);
    let weth_out = get_v2_amount_out(amount, reserve_in, reserve_out);
    Ok(weth_out)
}

我们将使用两个受欢迎且流动性充足的V2池:

来在这些代币之间进行转换。

phase2中,我们计算三明治的盈利能力如下( BatchSandwich.simulate 函数):

  let eth_balance_after = simulator.get_eth_balance_of(simulator.owner);
  let weth_balance_after = simulator.get_token_balance(weth, bot_address)?;

  let eth_used_as_gas = eth_balance_before
      .checked_sub(eth_balance_after)
      .unwrap_or(eth_balance_before);
  let eth_used_as_gas_i256 = I256::from_dec_str(&eth_used_as_gas.to_string())?;

  let weth_balance_before_i256 = I256::from_dec_str(&weth_balance_before.to_string())?;
  let weth_balance_after_i256 = I256::from_dec_str(&weth_balance_after.to_string())?;

  let profit = (weth_balance_after_i256 - weth_balance_before_i256).as_i128();
  let gas_cost = eth_used_as_gas_i256.as_i128();
  let revenue = profit - gas_cost;

这假设所有捆绑包最终都以WETH代币结束。

但我们将更改为如下:

let eth_balance_after = simulator.get_eth_balance_of(simulator.owner);
// 获取所有主要货币余额:WETH/USDT/USDC
let mut mc_balances_after = HashMap::new();
for (main_currency, _) in &starting_mc_values {
    let balance_after = simulator.get_token_balance(*main_currency, bot_address)?;
    mc_balances_after.insert(main_currency, balance_after);
}

let eth_used_as_gas = eth_balance_before
    .checked_sub(eth_balance_after)
    .unwrap_or(eth_balance_before);
let eth_used_as_gas_i256 = I256::from_dec_str(&eth_used_as_gas.to_string())?;

let usdt = H160::from_str(USDT).unwrap();
let usdc = H160::from_str(USDC).unwrap();

let mut weth_before_i256 = I256::zero();
let mut weth_after_i256 = I256::zero();

for (main_currency, _) in &starting_mc_values {
    let mc_balance_before = *mc_balances_before.get(&main_currency).unwrap();
    let mc_balance_after = *mc_balances_after.get(&main_currency).unwrap();

    // 将USDT/USDC转换为WETH,以便我们可以用WETH值进行考虑
    let (mc_balance_before, mc_balance_after) = if *main_currency == usdt {
        let before =
            convert_usdt_to_weth(&mut simulator, mc_balance_before).unwrap_or_default();
        let after =
            convert_usdt_to_weth(&mut simulator, mc_balance_after).unwrap_or_default();
        (before, after)
    } else if *main_currency == usdc {
        let before =
            convert_usdc_to_weth(&mut simulator, mc_balance_before).unwrap_or_default();
        let after =
            convert_usdc_to_weth(&mut simulator, mc_balance_after).unwrap_or_default();
        (before, after)
    } else {
        (mc_balance_before, mc_balance_after)
    };

    let mc_balance_before_i256 = I256::from_dec_str(&mc_balance_before.to_string())?;
    let mc_balance_after_i256 = I256::from_dec_str(&mc_balance_after.to_string())?;

    weth_before_i256 += mc_balance_before_i256;
    weth_after_i256 += mc_balance_after_i256;
}

let profit = (weth_after_i256 - weth_before_i256).as_i128();
let gas_cost = eth_used_as_gas_i256.as_i128();
let revenue = profit - gas_cost;

你可以看到,我们使用convert_usdt_to_wethconvert_usdc_to_weth函数将结果USDT/USDC余额转换为WETH,并且我们将所有主要货币余额加在一起,以计算盈利能力。

非-WETH对开胃菜 🥗

如果你已经为非-WETH对模拟添加了支持,现在我们来看看我们的开胃菜。

**sandooo/src/sandwich/appetizer.rs在phase3上 · solidquant/sandooo

我们不再为decimals变量设定硬编码值:

之前:

let decimals = WETH_DECIMALS;

之后:

let main_currency = info.main_currency;
let mc = MainCurrency::new(main_currency);
let decimals = mc.decimals();

我们将获取所有受支持主要货币的小数点值。

同样,不再像在phase2中以0.01 WETH进行三明治捆绑包的测试:

let small_amount_in = U256::from(10).pow(U256::from(decimals - 2)); // 0.01 WETH

我们将其更改为:

let small_amount_in = if is_weth(main_currency) {
    U256::from(10).pow(U256::from(decimals - 2)) // 0.01 WETH
} else {
    U256::from(10) * U256::from(10).pow(U256::from(decimals)) // 10 USDT, 10 USDC
};

我们将使用以下值运行模拟:

  • WETH: 0.01
  • USDT/USDC: 10

代币。

而且,ceiling_amount_in的值现在将是:

let ceiling_amount_in = if is_weth(main_currency) {
    U256::from(100) * U256::from(10).pow(U256::from(18)) // 100 ETH
} else {
    U256::from(300000) * U256::from(10).pow(U256::from(decimals)) // 300000 USDT/USDC(均为6位小数)
};

我们将使用这个值来优化三明治捆绑包,假设没有三明治大于100 ETH、300000 USDT/USDC。

组合捆绑 + 主菜 🥪🍛

我们将要在这一部分上上主菜。我们将探讨如何将多个三明治组合在一起。现在我们的三明治捆绑包看起来像这样:

  • 抢跑交易:WETH → 目标代币 #1, USDT → 目标代币 #2
  • 受害者交易 #1:购买目标代币 #1
  • 受害者交易 #2:购买目标代币 #2
  • 尾随交易:目标代币 #1 → WETH, 目标代币 #2 → USDT

尽管捆绑包现在看起来像是我们上一轮三明治捆绑包的两倍大,但其实并不复杂,所以不要担心。🙏

我们只需要找到所有可以使用WETH、USDT、USDC代币进行的潜在三明治。我们已经在我们的模拟器中做到了这一点。

我们将模拟的结果保存在sandooo/src/sandwich/strategy.rs文件中,特别是在promising_sandwiches中:

let mut promising_sandwiches: HashMap<H256, Vec<Sandwich>> = HashMap::new();

你会注意到我们在优化完成后在开胃菜函数中使用这个HashMap:

if optimized_sandwich.max_revenue > U256::zero() {
    // 将优化后的三明治添加到promising_sandwiches
    if !promising_sandwiches.contains_key(&tx_hash) {
        promising_sandwiches.insert(tx_hash, vec![sandwich.clone()]);
    } else {
        let sandwiches = promising_sandwiches.get_mut(&tx_hash).unwrap();
        sandwiches.push(sandwich.clone());
    }
}

如果优化后的三明治的最大收益大于0,那么我们知道这个三明治捆绑包在扣除Gas费用后也是有利可图的。因此我们将其保存到我们的promising_sandwiches HashMap中。

📍 三明治策略是资本密集型策略,因为它们不是原子的,所以不能进行闪电贷/闪电兑换。

因此,如果你比较两个搜索者,分别以1 ETH和100 ETH开始,后者将获胜。

有些三明治的规模较小,因此你甚至可以与0.5 ETH竞争,但大多数三明治的规模将大于1 ~ 2 ETH,并且当有多个三明治时,我们理想情况下希望获取所有这些,以便我们的捆绑包是最有利可图的。

那么,如何知道在有多个三明治机会时如何分配我们的资本?

你会回忆起在phase2中,我们遍历所有可能的三明治并将单个三明治捆绑包发送给构建者。

但更新的代码将为所有三明治捆绑包打分,并按此评分排序并根据排名分配资金。

我们现在在sandooo/src/sandwich/main_dish.rs文件中的main_dish函数。

**sandooo/src/sandwich/main_dish.rs在phase3上 · solidquant/sandooo

首先,我们将从我们的机器人合约中获取WETH、USDT、USDC余额:

let weth = H160::from_str(WETH).unwrap();
let usdt = H160::from_str(USDT).unwrap();
let usdc = H160::from_str(USDC).unwrap();

let bot_balances = if env.debug {
    // 假设你在调试时有无限资金
    let mut bot_balances = HashMap::new();
    bot_balances.insert(weth, U256::MAX);
    bot_balances.insert(usdt, U256::MAX);
    bot_balances.insert(usdc, U256::MAX);
    bot_balances
} else {
    let bot_balances =
        get_token_balances(&provider, bot_address, &vec![weth, usdt, usdc]).await;
    bot_balances
};

接下来,我们创建一个plate向量,并将一种叫Ingredients的新数据类型存储到plate中。

##[derive(Debug, Clone)]
pub struct Ingredients {
    pub tx_hash: H256,
    pub pair: H160,
    pub main_currency: H160,
    pub amount_in: U256,
    pub max_revenue: U256,
    pub score: f64,
    pub sandwich: Sandwich,
}

let mut plate = Vec::new();
for (promising_tx_hash, sandwiches) in promising_sandwiches {
    for sandwich in sandwiches {
        let optimized_sandwich = sandwich.optimized_sandwich.as_ref().unwrap();
        let amount_in = optimized_sandwich.amount_in;
        let max_revenue = optimized_sandwich.max_revenue;
        let score = (max_revenue.as_u128() as f64) / (amount_in.as_u128() as f64);
        let clean_sandwich = Sandwich {
            amount_in,
            swap_info: sandwich.swap_info.clone(),
            victim_tx: sandwich.victim_tx.clone(),
            optimized_sandwich: None,
        };
        let ingredients = Ingredients {
            tx_hash: *promising_tx_hash,
            pair: sandwich.swap_info.target_pair,
            main_currency: sandwich.swap_info.main_currency,
            amount_in,
            max_revenue,
            score,
            sandwich: clean_sandwich,
        };
        plate.push(ingredients);
    }
}

需要注意的是这里引入了新的score变量。

分数的计算如下:

score = max_revenue / amount_in

你可以把这个值理解为我们可以期待与我们投资的amount_in相比预期的利润。

我们将按降序对Ingredients进行排序:

plate.sort_by(|x, y| y.score.partial_cmp(&x.score).unwrap());

❗ ️然而,在这样做时我们必须谨慎。你应该理解这样做会给这个系统引入什么bug。

回忆一下我们USDT/USDC三明治捆绑包的模拟。amount_in值将以USDT/USDC代币表示。max_revenue将表示为WETH(我们故意将USDT/USDC值转换为WETH)。

这意味着USDT/USDC三明治的分数总会高于WETH三明治。

但这可能是好事,因为如果我们相信稳定币三明治会更具竞争力,我们希望在WETH三明治之前考虑稳定币三明治。并且在当前的排序机制下,我们将总是先进入稳定币三明治,然后再考虑WETH三明治。因此我们暂时保持这个排序系统,如果我们想要不同的资产配置技术再修复。

接下来,我们将使用plate向量中的Ingredients创建批量三明治:

for i in 0..plate.len() {
    let mut balances = bot_balances.clone();
    let mut sandwiches = Vec::new();

    for j in 0..(i + 1) {
        let ingredient = &plate[j];
        let main_currency = ingredient.main_currency;
        let balance = *balances.get(&main_currency).unwrap();
        let optimized = ingredient.amount_in;
        let amount_in = std::cmp::min(balance, optimized);

        let mut final_sandwich = ingredient.sandwich.clone();
        final_sandwich.amount_in = amount_in;

        let new_balance = balance - amount_in;
        balances.insert(main_currency, new_balance);

        sandwiches.push(final_sandwich);
    }

    let final_batch_sandwich = BatchSandwich { sandwiches };

    // ...
}

我们将遍历我们plate中的所有三明治,如下所示:

  • 三明治 #1
  • 三明治 #1, 三明治 #2
  • 三明治 #1, 三明治 #2, 三明治 #2

并尽可能多地投入资金,通过取主要货币余额和优化额的最小值来实现。

假设我们的余额情况如下:

  • WETH: 2
  • USDT: 100

而我们正在考虑的三种三明治机会为:

  • 三明治 #1 优化金额:90 USDT
  • 三明治 #2 优化金额:1.2 WETH
  • 三明治 #2 优化金额:1 WETH

由于投资这三者需要我们拥有90 USDT、2.2 WETH,这在WETH的一侧我们是短缺的,因此我们采取以下方式:

  • 三明治 #1: min(100, 90) = 90 USDT

首先取当前USDT余额的最小值,即100与优化金额90的最小值,这是90 USDT。

  • 三明治 #2: min(2, 1.2) = 1.2 WETH

其次,我们取当前WETH余额的最小值,即2 WETH与1.2 WETH,这是1.2 WETH。此后,我们从WETH余额中减去1.2 WETH,并更新余额为0.8 WETH。

  • 三明治 #2: min(0.8, 1) = 0.8 WETH

第三,我们取更新后的WETH余额,即0.8 WETH与优化金额1 WETH之间的最小值,结果为0.8 WETH。

总之,我们总共投资了90 USDT, 2 WETH

请注意,投资每个三明治捆绑包将是最有利可图的。上述方法只是为了支持在小资金运行的机器人而进行的有趣练习。

有了这些更新,我们现在可以捕捉到稳定币三明治并组合多个三明治捆绑。

希望本文阅读愉快。😃

  • 原文链接: medium.com/@solidquant/a...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Solid Quant
Solid Quant
江湖只有他的大名,没有他的介绍。