本文介绍了如何在 sandwich bot 中添加稳定币(USDT/USDC)支持,并且通过分组打包多个交易来最大化利润。文章详细讲解了代码的更新和实现逻辑,特别是如何在不同币种之间进行转换和计算利润。
Fotor AI: “一只芝娃娃在三明治上”
大家好!本文是我们三明治机器人系列的第三篇。还没有阅读之前文章的读者可以参考以下文章以获取一些背景信息:
2. 发送三明治捆绑包:
本系列中的所有代码将在Github上开源:
GitHub - solidquant/sandooo: 一款三明治机器人
✅ 在本文中,我们将添加非-WETH三明治捆绑包——特别是使用USDT/USDC的稳定币三明治——并看看它们是否能帮助改善我们的系统。
✅ 做完这一步后,我们将尝试合并多个三明治的捆绑包,并努力最大化利润,从而增加我们的获胜机会。
有了这些新特性,我们的系统将进一步接近生产就绪状态。不过不要忘记,我们目前只在做Uniswap V2三明治,下周还会加入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表示,而不是WETH,因此很难扣除Gas费用,并评估我们的捆绑包是否盈利。
我们添加一个聪明的技巧来解决这个问题。
如果我们以这种方式考虑非-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(ð_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(ð_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_weth和convert_usdc_to_weth函数将结果USDT/USDC余额转换为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
};
我们将使用以下值运行模拟:
代币。
而且,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、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中的所有三明治,如下所示:
并尽可能多地投入资金,通过取主要货币余额和优化额的最小值来实现。
假设我们的余额情况如下:
而我们正在考虑的三种三明治机会为:
由于投资这三者需要我们拥有90 USDT、2.2 WETH,这在WETH的一侧我们是短缺的,因此我们采取以下方式:
首先取当前USDT余额的最小值,即100与优化金额90的最小值,这是90 USDT。
其次,我们取当前WETH余额的最小值,即2 WETH与1.2 WETH,这是1.2 WETH。此后,我们从WETH余额中减去1.2 WETH,并更新余额为0.8 WETH。
第三,我们取更新后的WETH余额,即0.8 WETH与优化金额1 WETH之间的最小值,结果为0.8 WETH。
总之,我们总共投资了90 USDT, 2 WETH。
请注意,投资每个三明治捆绑包将是最有利可图的。上述方法只是为了支持在小资金运行的机器人而进行的有趣练习。
有了这些更新,我们现在可以捕捉到稳定币三明治并组合多个三明治捆绑。
希望本文阅读愉快。😃
- 原文链接: medium.com/@solidquant/a...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!