kabu_types_entities/
tips.rs

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