kabu_types_swap/
tips.rs

1use std::collections::HashMap;
2use std::fmt::{Display, Formatter};
3use std::sync::Arc;
4
5use crate::Swap;
6use alloy_primitives::utils::format_units;
7use alloy_primitives::{Address, U256};
8use eyre::{OptionExt, Result, eyre};
9use kabu_evm_utils::NWETH;
10use kabu_types_market::Token;
11use lazy_static::lazy_static;
12use rand::random;
13use tracing::{error, info};
14
15#[derive(Clone, Debug)]
16pub struct Tips {
17    pub token_in: Arc<Token>,
18    pub profit: U256,
19    pub profit_eth: U256,
20    pub tips: U256,
21    pub min_change: U256,
22}
23
24impl Display for Tips {
25    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26        write!(
27            f,
28            "{} : tips {} min_change {} profit : {} eth : {} ",
29            self.token_in.get_symbol(),
30            format_units(self.tips, "ether").unwrap_or_default(),
31            self.token_in.to_float(self.min_change),
32            self.token_in.to_float(self.profit),
33            format_units(self.profit_eth, "ether").unwrap_or_default(),
34        )
35    }
36}
37
38lazy_static! {
39    static ref START_PCT : U256 = U256::from(9900);
40
41    static ref SLOPES : Vec<(U256,U256)> = vec![
42//x        (U256::from(0), U256::from(10000)),
43        (U256::from(10).pow(U256::from(19)), U256::from(7000)),
44        (U256::from(10).pow(U256::from(19))*U256::from(5), U256::from(5000))
45    ];
46}
47pub fn tips_pct_advanced(profit: &U256) -> u32 {
48    let mut start_point = U256::ZERO;
49    let mut start_pct = *START_PCT;
50    for (x, y) in SLOPES.iter() {
51        if x > profit {
52            return (start_pct - ((start_pct - y) * (profit - start_point) / (x - start_point))).to::<u32>();
53        }
54        start_point = *x;
55        start_pct = *y;
56    }
57    start_pct.to()
58}
59
60pub fn randomize_tips_pct(tips_pct: u32) -> u32 {
61    let rnd: u32 = random::<u32>() % 50;
62    tips_pct - rnd
63}
64
65pub fn tips_and_value_for_swap_type(
66    swap: &Swap,
67    tips_pct: Option<u32>,
68    gas_cost: Option<U256>,
69    eth_balance: U256,
70) -> Result<(Vec<Tips>, U256)> {
71    let total_profit_eth = swap.arb_profit_eth();
72    info!("Total profit eth : {}", format_units(total_profit_eth, "ether").unwrap_or_default());
73    let tips_pct = randomize_tips_pct(tips_pct.unwrap_or(tips_pct_advanced(&total_profit_eth)));
74
75    if let Some(gas_cost) = gas_cost
76        && total_profit_eth < gas_cost
77    {
78        info!(
79            "total_profit_eth={} < {}",
80            format_units(total_profit_eth, "ether").unwrap_or_default(),
81            format_units(gas_cost, "ether").unwrap_or_default()
82        );
83        return Err(eyre!("NOT_ENOUGH_PROFIT"));
84    }
85
86    match swap {
87        Swap::BackrunSwapLine(_) | Swap::BackrunSwapSteps(_) => {
88            let profit = swap.arb_profit();
89            if profit.is_zero() {
90                error!(profit = NWETH::to_float(profit), %swap, "Zero profit");
91                return Err(eyre!("NO_PROFIT"));
92            }
93            let token_in = swap.get_first_token().ok_or_eyre("NO_FIRST_TOKEN")?.clone();
94            let profit_eth = token_in.calc_eth_value(profit).ok_or_eyre("CALC_ETH_VALUE_FAILED")?;
95
96            if let Some(gas_cost) = gas_cost
97                && profit_eth < gas_cost
98            {
99                error!(
100                    profit_eth = NWETH::to_float(profit_eth),
101                    gas_cost = NWETH::to_float(gas_cost),
102                    %swap,
103                    "Profit doesn't exceed the gas cost"
104                );
105                return Err(eyre!("NO_PROFIT_EXCEEDING_GAS"));
106            }
107
108            let mut tips = profit_eth.checked_sub(gas_cost.unwrap_or_default()).ok_or_eyre("SUBTRACTION_OVERFLOWN")? * U256::from(tips_pct)
109                / U256::from(10000);
110            let min_change = token_in.calc_token_value_from_eth(gas_cost.unwrap_or_default() + tips).unwrap();
111            let mut value = if token_in.is_weth() { U256::ZERO } else { tips };
112
113            if !token_in.is_weth() && (tips > ((eth_balance * U256::from(9000)) / U256::from(10000))) {
114                tips = (eth_balance * U256::from(9000)) / U256::from(10000);
115                value = tips;
116            }
117
118            Ok((vec![Tips { token_in, profit, profit_eth, tips, min_change }], value))
119        }
120        Swap::Multiple(swap_vec) => {
121            let mut tips_hashset: HashMap<Address, Tips> = HashMap::new();
122
123            let profit_eth = swap.arb_profit_eth();
124
125            if let Some(gas_cost) = gas_cost
126                && profit_eth < gas_cost
127            {
128                error!(
129                    profit_eth = NWETH::to_float(profit_eth),
130                    gas_cost = NWETH::to_float(gas_cost),
131                    %swap,
132                    "Profit doesn't exceed the gas cost"
133                );
134                return Err(eyre!("NO_PROFIT_EXCEEDING_GAS"));
135            }
136
137            let gas_cost_per_record = gas_cost.unwrap_or_default() / U256::from(swap_vec.len());
138
139            for swap_record in swap_vec.iter() {
140                let token_in = swap_record.get_first_token().ok_or_eyre("NO_FIRST_TOKEN")?.clone();
141
142                let profit = swap_record.arb_profit();
143                if profit.is_zero() {
144                    error!(profit = NWETH::to_float(profit), %swap, "Zero profit");
145                    return Err(eyre!("NO_PROFIT"));
146                }
147
148                let profit_eth = token_in.calc_eth_value(profit).ok_or_eyre("CALC_ETH_VALUE_FAILED")?;
149
150                let tips = profit_eth.checked_sub(gas_cost_per_record).ok_or_eyre("SUBTRACTION_OVERFLOWN")? * U256::from(tips_pct)
151                    / U256::from(10000);
152                let min_change = token_in.calc_token_value_from_eth(tips + gas_cost_per_record).unwrap();
153
154                let entry = tips_hashset.entry(token_in.get_address()).or_insert(Tips {
155                    token_in,
156                    profit: U256::ZERO,
157                    profit_eth: U256::ZERO,
158                    tips: U256::ZERO,
159                    min_change: U256::ZERO,
160                });
161
162                entry.profit += profit;
163                entry.profit_eth += profit_eth;
164                entry.tips += tips;
165                entry.min_change += min_change;
166            }
167
168            let mut value = U256::ZERO;
169
170            if tips_hashset.iter().any(|(_, x)| x.token_in.is_weth()) {
171                let total_tips_eth: U256 = tips_hashset.iter().filter(|(_, x)| x.token_in.is_weth()).map(|(_, x)| x.tips).sum();
172                let total_tips_non_eth: U256 = tips_hashset.iter().filter(|(_, x)| !x.token_in.is_weth()).map(|(_, x)| x.tips).sum();
173                let total_profit_eth: U256 = tips_hashset.iter().filter(|(_, x)| x.token_in.is_weth()).map(|(_, x)| x.profit_eth).sum();
174                for (_, token_tips) in tips_hashset.iter_mut() {
175                    if token_tips.token_in.is_weth() {
176                        token_tips.tips = total_tips_eth + total_tips_non_eth;
177                        if total_tips_eth + total_tips_non_eth > total_profit_eth {
178                            value = total_tips_eth + total_tips_non_eth - total_profit_eth;
179                        }
180
181                        if value > eth_balance {
182                            token_tips.tips = token_tips.tips.checked_sub(value).ok_or_eyre("SUBTRACTION_OVERFLOWN")?;
183                            value = eth_balance * U256::from(9000) / U256::from(10000);
184                            token_tips.tips += value;
185                        }
186                    } else {
187                        token_tips.tips = U256::ZERO;
188                    }
189                }
190            } else {
191                let total_tips = tips_hashset.values().map(|x| x.tips).sum::<U256>();
192                value = if total_tips >= eth_balance { eth_balance * U256::from(9000) / U256::from(10000) } else { total_tips };
193
194                for (idx, (_, token_tips)) in tips_hashset.iter_mut().enumerate() {
195                    token_tips.tips = if idx == 0 { value } else { U256::ZERO };
196                }
197            }
198            let tips_vec = tips_hashset.values().cloned().collect();
199
200            Ok((tips_vec, value + U256::from(100)))
201        }
202
203        _ => Err(eyre!("NOT_IMPLEMENTED")),
204    }
205}