kabu_types_swap/
swap_line.rs

1use crate::swap_error::SwapErrorKind;
2use crate::{CalculationResult, SwapError, SwapStep};
3use alloy_evm::EvmEnv;
4use alloy_primitives::{Address, I256, U256};
5use eyre::{Result, eyre};
6use kabu_evm_db::KabuDBError;
7use kabu_types_market::SwapPath;
8use kabu_types_market::{PoolId, PoolWrapper, Token};
9use revm::DatabaseRef;
10use std::fmt;
11use std::hash::{Hash, Hasher};
12use std::sync::Arc;
13use tracing::debug;
14
15#[derive(Debug, Clone, Default)]
16pub enum SwapAmountType {
17    #[default]
18    NotSet,
19    Set(U256),
20    Stack0,
21    RelativeStack(u32),
22    Balance(Address),
23}
24
25impl Copy for SwapAmountType {}
26
27impl SwapAmountType {
28    #[inline]
29    pub fn unwrap(&self) -> U256 {
30        match &self {
31            Self::Set(x) => *x,
32            _ => panic!("called `InAmountType::unwrap()` on a unknown value"),
33        }
34    }
35    #[inline]
36    pub fn unwrap_or_default(&self) -> U256 {
37        match &self {
38            Self::Set(x) => *x,
39            _ => U256::ZERO,
40        }
41    }
42
43    #[inline]
44    pub fn is_set(&self) -> bool {
45        matches!(self, Self::Set(_))
46    }
47    #[inline]
48    pub fn is_not_set(&self) -> bool {
49        !matches!(self, Self::Set(_))
50    }
51}
52
53#[derive(Clone, Debug, Default)]
54pub struct SwapLine {
55    pub path: SwapPath,
56    /// Input token amount of the swap
57    pub amount_in: SwapAmountType,
58    /// Output token amount of the swap
59    pub amount_out: SwapAmountType,
60    /// The in and out amounts for each swap step
61    pub calculation_results: Vec<CalculationResult>,
62    /// Output token of the swap
63    pub swap_to: Option<Address>,
64    /// Gas used for the swap
65    pub gas_used: Option<u64>,
66}
67
68impl fmt::Display for SwapLine {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        let token_in = self.tokens().first();
71        let token_out = self.tokens().last();
72
73        let profit: String = if token_in == token_out {
74            match token_in {
75                Some(t) => format!("profit={}", t.to_float_sign(self.profit().unwrap_or(I256::ZERO))),
76                _ => format!("profit={}", self.profit().unwrap_or(I256::ZERO)),
77            }
78        } else {
79            "-".to_string()
80        };
81
82        let tokens = self.tokens().iter().map(|token| token.get_symbol()).collect::<Vec<String>>().join(", ");
83        let pools =
84            self.pools().iter().map(|pool| format!("{}@{}", pool.get_protocol(), pool.get_pool_id())).collect::<Vec<String>>().join(", ");
85        let amount_in = match self.amount_in {
86            SwapAmountType::Set(x) => match token_in {
87                Some(t) => format!("{:?}", t.to_float(x)),
88                _ => format!("{x}"),
89            },
90            _ => {
91                format!("{:?}", self.amount_in)
92            }
93        };
94        let amount_out = match self.amount_out {
95            SwapAmountType::Set(x) => match token_out {
96                Some(t) => format!("{:?}", t.to_float(x)),
97                _ => format!("{x}"),
98            },
99            _ => {
100                format!("{:?}", self.amount_out)
101            }
102        };
103
104        let calculation_results =
105            self.calculation_results.iter().map(|calculation_result| format!("{calculation_result}")).collect::<Vec<String>>().join(", ");
106
107        write!(
108            f,
109            "SwapLine [{}, tokens=[{}], pools=[{}], amount_in={}, amount_out={}, calculation_results=[{}], gas_used={:?}]",
110            profit, tokens, pools, amount_in, amount_out, calculation_results, self.gas_used
111        )
112    }
113}
114
115impl Hash for SwapLine {
116    fn hash<H: Hasher>(&self, state: &mut H) {
117        self.tokens().hash(state);
118        self.pools().hash(state);
119    }
120}
121
122impl PartialEq for SwapLine {
123    fn eq(&self, other: &Self) -> bool {
124        self.tokens() == other.tokens() && self.pools() == other.pools()
125    }
126}
127
128impl From<SwapPath> for SwapLine {
129    fn from(value: SwapPath) -> Self {
130        Self { path: value, ..Default::default() }
131    }
132}
133
134impl SwapLine {
135    pub fn to_error(&self, kind: SwapErrorKind) -> SwapError {
136        SwapError {
137            kind,
138            pool: self.get_first_pool().map(|x| x.get_pool_id()).unwrap_or(PoolId::Address(Address::default())),
139            token_from: self.get_first_token().map_or(Address::default(), |x| x.get_address()),
140            token_to: self.get_last_token().map_or(Address::default(), |x| x.get_address()),
141            is_in_amount: true,
142            amount: self.amount_in.unwrap_or_default(),
143        }
144    }
145
146    pub fn new() -> Self {
147        Default::default()
148    }
149
150    /// Check if the path contains a specific pool
151    pub fn contains_pool(&self, pool: &PoolWrapper) -> bool {
152        self.path.contains_pool(pool)
153    }
154
155    /// Get all involved tokens in the swap line
156    pub fn tokens(&self) -> &Vec<Arc<Token>> {
157        &self.path.tokens
158    }
159
160    /// Get all used pools in the swap line
161    pub fn pools(&self) -> &Vec<PoolWrapper> {
162        &self.path.pools
163    }
164
165    /// Get the first token in the swap line
166    pub fn get_first_token(&self) -> Option<&Arc<Token>> {
167        self.tokens().first()
168    }
169
170    /// Get the last token in the swap line
171    pub fn get_last_token(&self) -> Option<&Arc<Token>> {
172        self.tokens().last()
173    }
174
175    /// Get the first pool in the swap line
176    pub fn get_first_pool(&self) -> Option<&PoolWrapper> {
177        self.pools().first()
178    }
179
180    /// Get the last pool in the swap line
181    pub fn get_last_pool(&self) -> Option<&PoolWrapper> {
182        self.pools().last()
183    }
184
185    /// Convert the swap line to two swap steps for flash swapping
186    pub fn to_swap_steps(&self, multicaller: Address) -> Option<(SwapStep, SwapStep)> {
187        let mut sp0: Option<SwapLine> = None;
188        let mut sp1: Option<SwapLine> = None;
189
190        for i in 1..self.path.pool_count() {
191            let (head_path, mut tail_path) = self.split(i).unwrap();
192            if head_path.can_flash_swap() || tail_path.can_flash_swap() {
193                if head_path.can_flash_swap() {
194                    tail_path.amount_in = SwapAmountType::Stack0;
195                }
196                sp0 = Some(head_path);
197                sp1 = Some(tail_path);
198                break;
199            }
200        }
201
202        if sp0.is_none() || sp1.is_none() {
203            let (head_path, tail_path) = self.split(1).unwrap();
204            sp0 = Some(head_path);
205            sp1 = Some(tail_path);
206        }
207
208        let mut step_0 = SwapStep::new(multicaller);
209        step_0.add(sp0.unwrap());
210
211        let mut step_1 = SwapStep::new(multicaller);
212        let sp1 = sp1.unwrap();
213        step_1.add(sp1);
214
215        Some((step_0, step_1))
216    }
217
218    /// Split the swap line into two swap lines at a specific pool index
219    pub fn split(&self, pool_index: usize) -> Result<(SwapLine, SwapLine)> {
220        let first = SwapLine {
221            path: SwapPath::new(self.tokens()[0..pool_index + 1].to_vec(), self.pools()[0..pool_index].to_vec()),
222            amount_in: self.amount_in,
223            amount_out: SwapAmountType::NotSet,
224            calculation_results: vec![],
225            swap_to: None,
226            gas_used: None,
227        };
228        let second = SwapLine {
229            path: SwapPath::new(self.tokens()[pool_index..].to_vec(), self.pools()[pool_index..].to_vec()),
230            amount_in: SwapAmountType::NotSet,
231            amount_out: self.amount_out,
232            calculation_results: vec![],
233            swap_to: None,
234            gas_used: None,
235        };
236        Ok((first, second))
237    }
238
239    /// Check if all pools in the swap line can be flash swapped
240    pub fn can_flash_swap(&self) -> bool {
241        for pool in self.pools().iter() {
242            if !pool.can_flash_swap() {
243                return false;
244            }
245        }
246        true
247    }
248
249    /// Calculate the absolute profit of the swap line
250    pub fn abs_profit(&self) -> U256 {
251        let Some(token_in) = self.tokens().first() else {
252            return U256::ZERO;
253        };
254        let Some(token_out) = self.tokens().last() else {
255            return U256::ZERO;
256        };
257        if token_in != token_out {
258            return U256::ZERO;
259        }
260        let SwapAmountType::Set(amount_in) = self.amount_in else {
261            return U256::ZERO;
262        };
263        let SwapAmountType::Set(amount_out) = self.amount_out else {
264            return U256::ZERO;
265        };
266        if amount_out > amount_in {
267            return amount_out - amount_in;
268        }
269
270        U256::ZERO
271    }
272
273    /// Calculate the absolute profit of the swap line in ETH
274    pub fn abs_profit_eth(&self) -> U256 {
275        let profit = self.abs_profit();
276        let Some(first_token) = self.get_first_token() else {
277            return U256::ZERO;
278        };
279        first_token.calc_eth_value(profit).unwrap_or(U256::ZERO)
280    }
281
282    pub fn profit(&self) -> Result<I256> {
283        if self.tokens().len() < 3 {
284            return Err(eyre!("NOT_ARB_PATH"));
285        }
286        if let Some(token_in) = self.tokens().first()
287            && let Some(token_out) = self.tokens().last()
288        {
289            return if token_in == token_out {
290                if let SwapAmountType::Set(amount_in) = self.amount_in
291                    && let SwapAmountType::Set(amount_out) = self.amount_out
292                {
293                    return Ok(I256::from_raw(amount_out) - I256::from_raw(amount_in));
294                }
295                Err(eyre!("AMOUNTS_NOT_SET"))
296            } else {
297                Err(eyre!("TOKENS_DONT_MATCH"))
298            };
299        }
300        Err(eyre!("CANNOT_CALCULATE"))
301    }
302
303    const MIN_VALID_OUT_AMOUNT: U256 = U256::from_limbs([0x100, 0, 0, 0]);
304
305    /// Calculate the out amount for the swap line for a given in amount
306    #[allow(clippy::result_large_err)]
307    pub fn calculate_with_in_amount<DB: DatabaseRef<Error = KabuDBError>>(
308        &self,
309        db: &DB,
310        evm_env: &EvmEnv,
311        in_amount: U256,
312    ) -> Result<(U256, u64, Vec<CalculationResult>), SwapError> {
313        let mut current_in_amount = in_amount;
314        let mut final_out_amount = U256::ZERO;
315        let mut gas_used = 0;
316        let mut calculation_results = vec![];
317
318        for (i, pool) in self.pools().iter().enumerate() {
319            let token_from = &self.tokens()[i];
320            let token_to = &self.tokens()[i + 1];
321            match pool.calculate_out_amount(db, evm_env, &token_from.get_address(), &token_to.get_address(), current_in_amount) {
322                Ok((out_amount_result, gas_result)) => {
323                    if out_amount_result.is_zero() {
324                        return Err(SwapError {
325                            kind: SwapErrorKind::ZeroOutAmount,
326                            pool: pool.get_pool_id(),
327                            token_from: token_from.get_address(),
328                            token_to: token_to.get_address(),
329                            is_in_amount: true,
330                            amount: current_in_amount,
331                        });
332                    }
333                    if out_amount_result.lt(&Self::MIN_VALID_OUT_AMOUNT) {
334                        return Err(SwapError {
335                            kind: SwapErrorKind::AlmostZeroOutAmount,
336                            pool: pool.get_pool_id(),
337                            token_from: token_from.get_address(),
338                            token_to: token_to.get_address(),
339                            is_in_amount: true,
340                            amount: current_in_amount,
341                        });
342                    }
343
344                    calculation_results.push(CalculationResult::new(current_in_amount, out_amount_result));
345                    current_in_amount = out_amount_result;
346                    final_out_amount = out_amount_result;
347                    gas_used += gas_result
348                }
349                Err(e) => {
350                    //error!("calculate_with_in_amount calculate_out_amount error {} amount {} : {}", self, in_amount, e);
351                    return Err(SwapError {
352                        kind: SwapErrorKind::from(e),
353                        pool: pool.get_pool_id(),
354                        token_from: token_from.get_address(),
355                        token_to: token_to.get_address(),
356                        is_in_amount: true,
357                        amount: current_in_amount,
358                    });
359                }
360            }
361        }
362        Ok((final_out_amount, gas_used, calculation_results))
363    }
364
365    /// Calculate the in amount for the swap line for a given out amount
366    #[allow(clippy::result_large_err)]
367    pub fn calculate_with_out_amount<DB: DatabaseRef<Error = KabuDBError>>(
368        &self,
369        db: &DB,
370        evm_env: &EvmEnv,
371        out_amount: U256,
372    ) -> Result<(U256, u64, Vec<CalculationResult>), SwapError> {
373        let mut current_out_amount = out_amount;
374        let mut final_in_amount = U256::ZERO;
375        let mut gas_used = 0;
376        let mut calculation_results = vec![];
377
378        // TODO: Check if possible without clone?
379        let mut pool_reverse = self.pools().clone();
380        pool_reverse.reverse();
381        let mut tokens_reverse = self.tokens().clone();
382        tokens_reverse.reverse();
383
384        for (i, pool) in pool_reverse.iter().enumerate() {
385            let token_from = &tokens_reverse[i + 1];
386            let token_to = &tokens_reverse[i];
387            match pool.calculate_in_amount(db, evm_env, &token_from.get_address(), &token_to.get_address(), current_out_amount) {
388                Ok((in_amount_result, gas_result)) => {
389                    if in_amount_result == U256::MAX {
390                        return Err(SwapError {
391                            kind: SwapErrorKind::MaxInAmount,
392                            pool: pool.get_pool_id(),
393                            token_from: token_from.get_address(),
394                            token_to: token_to.get_address(),
395                            is_in_amount: false,
396                            amount: current_out_amount,
397                        });
398                    }
399                    if in_amount_result == U256::ZERO {
400                        return Err(SwapError {
401                            kind: SwapErrorKind::ZeroInAmount,
402                            pool: pool.get_pool_id(),
403                            token_from: token_from.get_address(),
404                            token_to: token_to.get_address(),
405                            is_in_amount: false,
406                            amount: current_out_amount,
407                        });
408                    }
409                    calculation_results.push(CalculationResult::new(current_out_amount, in_amount_result));
410                    current_out_amount = in_amount_result;
411                    final_in_amount = in_amount_result;
412                    gas_used += gas_result;
413                }
414                Err(e) => {
415                    //error!("calculate_with_out_amount calculate_in_amount error {} amount {} : {}", self, in_amount, e);
416
417                    return Err(SwapError {
418                        kind: SwapErrorKind::from(e),
419                        pool: pool.get_pool_id(),
420                        token_from: token_from.get_address(),
421                        token_to: token_to.get_address(),
422                        is_in_amount: false,
423                        amount: current_out_amount,
424                    });
425                }
426            }
427        }
428        Ok((final_in_amount, gas_used, calculation_results))
429    }
430
431    /// Optimize the swap line for a given in amount
432    #[allow(clippy::result_large_err)]
433    pub fn optimize_with_in_amount<DB: DatabaseRef<Error = KabuDBError>>(
434        &mut self,
435        db: &DB,
436        evm_env: EvmEnv,
437        in_amount: U256,
438    ) -> Result<&mut Self, SwapError> {
439        let mut current_in_amount = in_amount;
440        let mut best_profit: Option<I256> = None;
441        let mut current_step = U256::from(10000);
442        let mut inc_direction = true;
443        let mut first_step_change = false;
444        let mut next_amount = current_in_amount;
445        let mut prev_in_amount = U256::ZERO;
446        let mut counter = 0;
447        let denominator = U256::from(1000);
448
449        loop {
450            counter += 1;
451            //let next_amount  = current_in_amount + (current_in_amount * current_step / 10000);
452
453            if counter > 30 {
454                debug!("optimize_swap_path_in_amount iterations exceeded : {self} {current_in_amount} {current_step}");
455                return Ok(self);
456            }
457
458            let (current_out_amount, current_gas_used, calculation_results) = match self.calculate_with_in_amount(db, &evm_env, next_amount)
459            {
460                Ok(ret) => ret,
461                Err(e) => {
462                    if counter == 1 {
463                        // break if first swap already fails
464                        return Err(e);
465                    }
466                    (U256::ZERO, 0, vec![])
467                }
468            };
469
470            let current_profit = I256::from_raw(current_out_amount) - I256::from_raw(next_amount);
471
472            if best_profit.is_none() {
473                best_profit = Some(current_profit);
474                self.amount_in = SwapAmountType::Set(next_amount);
475                self.amount_out = SwapAmountType::Set(current_out_amount);
476                self.gas_used = Some(current_gas_used);
477                self.calculation_results = calculation_results;
478                current_in_amount = next_amount;
479                if current_out_amount.is_zero() || current_profit.is_negative() {
480                    return Ok(self);
481                }
482            } else if best_profit.unwrap() > current_profit || current_out_amount.is_zero()
483            /*|| next_profit < current_profit*/
484            {
485                if first_step_change && inc_direction && current_step < denominator {
486                    inc_direction = false;
487                    //TODO : Check why not used
488                    let _next_amount = prev_in_amount;
489                    current_in_amount = prev_in_amount;
490                    first_step_change = true;
491                    //debug!("inc direction changed {} {} {}", next_amount, current_profit, bestprofit.unwrap());
492                } else if first_step_change && !inc_direction {
493                    //TODO : Check why is self aligned
494                    inc_direction = true;
495                    current_step /= U256::from(10);
496                    best_profit = Some(current_profit);
497                    first_step_change = true;
498                    //debug!("dec direction changed  {} {} {}", next_amount, current_profit, bestprofit.unwrap());
499
500                    if current_step == U256::from(1) {
501                        break;
502                    }
503                } else {
504                    current_step /= U256::from(10);
505                    first_step_change = true;
506                    if current_step == U256::from(1) {
507                        break;
508                    }
509                }
510            } else {
511                best_profit = Some(current_profit);
512                self.amount_in = SwapAmountType::Set(next_amount);
513                self.amount_out = SwapAmountType::Set(current_out_amount);
514                self.gas_used = Some(current_gas_used);
515                self.calculation_results = calculation_results;
516                current_in_amount = next_amount;
517                first_step_change = false;
518            }
519
520            prev_in_amount = current_in_amount;
521            if inc_direction {
522                next_amount = current_in_amount + (current_in_amount * current_step / denominator);
523            } else {
524                next_amount = current_in_amount - (current_in_amount * current_step / denominator);
525            }
526            //trace!("opt step : {} direction {} first_step {} step : {} current_in_amount : {} next_amount: {} profit : {} {}", counter, inc_direction, first_step_change,  current_step, current_in_amount , next_amount, current_profit, bestprofit.unwrap());
527        }
528
529        Ok(self)
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536    use alloy_primitives::utils::parse_units;
537    use kabu_defi_address_book::{TokenAddressEth, UniswapV2PoolAddress, UniswapV3PoolAddress};
538    use kabu_types_market::Pool;
539    use kabu_types_market::mock_pool::MockPool;
540    use std::sync::Arc;
541
542    fn default_swap_line() -> (MockPool, MockPool, SwapLine) {
543        let token0 = Arc::new(Token::new_with_data(TokenAddressEth::WETH, Some("WETH".to_string()), None, Some(18), true, false));
544        let token1 = Arc::new(Token::new_with_data(TokenAddressEth::USDT, Some("USDT".to_string()), None, Some(6), true, false));
545        let pool1 = MockPool::new(TokenAddressEth::WETH, TokenAddressEth::USDT, UniswapV3PoolAddress::WETH_USDT_3000);
546        let pool2 = MockPool::new(TokenAddressEth::WETH, TokenAddressEth::USDT, UniswapV2PoolAddress::WETH_USDT);
547
548        let swap_path =
549            SwapPath::new(vec![token0.clone(), token1.clone(), token1.clone(), token0.clone()], vec![pool1.clone(), pool2.clone()]);
550
551        let swap_line = SwapLine {
552            path: swap_path,
553            amount_in: SwapAmountType::Set(parse_units("0.01", "ether").unwrap().get_absolute()),
554            amount_out: SwapAmountType::Set(parse_units("0.03", "ether").unwrap().get_absolute()),
555            calculation_results: vec![],
556            swap_to: Some(Default::default()),
557            gas_used: Some(10000),
558        };
559
560        (pool1, pool2, swap_line)
561    }
562
563    #[test]
564    fn test_swapline_fmt() {
565        let (_, _, swap_line) = default_swap_line();
566
567        // under test
568        let formatted = format!("{swap_line}");
569        assert_eq!(
570            formatted,
571            "SwapLine [profit=0.02, tokens=[WETH, USDT, USDT, WETH], \
572            pools=[UniswapV2@0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36, UniswapV2@0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852], \
573            amount_in=0.01, amount_out=0.03, calculation_results=[], gas_used=Some(10000)]"
574        )
575    }
576
577    #[test]
578    fn test_contains_pool() {
579        let (pool1, pool2, swap_line) = default_swap_line();
580
581        assert!(swap_line.contains_pool(&PoolWrapper::from(pool1)));
582        assert!(swap_line.contains_pool(&PoolWrapper::from(pool2)));
583    }
584
585    #[test]
586    fn test_tokens() {
587        let (_, _, swap_line) = default_swap_line();
588
589        let tokens = swap_line.tokens();
590        assert_eq!(tokens.first().unwrap().get_address(), TokenAddressEth::WETH);
591        assert_eq!(tokens.get(1).unwrap().get_address(), TokenAddressEth::USDT);
592    }
593
594    #[test]
595    fn test_pools() {
596        let (pool1, pool2, swap_line) = default_swap_line();
597
598        let pools = swap_line.pools();
599        assert_eq!(pools.first().unwrap().get_address(), pool1.get_address());
600        assert_eq!(pools.get(1).unwrap().get_address(), pool2.get_address());
601    }
602
603    #[test]
604    fn test_get_first_token() {
605        let (_, _, swap_line) = default_swap_line();
606
607        let token = swap_line.get_first_token();
608        assert_eq!(token.unwrap().get_address(), TokenAddressEth::WETH);
609    }
610
611    #[test]
612    fn test_get_last_token() {
613        let (_, _, swap_line) = default_swap_line();
614
615        let token = swap_line.get_last_token();
616        assert_eq!(token.unwrap().get_address(), TokenAddressEth::WETH);
617    }
618
619    #[test]
620    fn test_get_first_pool() {
621        let (pool1, _, swap_line) = default_swap_line();
622
623        let pool = swap_line.get_first_pool();
624        assert_eq!(pool.unwrap().get_address(), pool1.get_address());
625    }
626
627    #[test]
628    fn test_get_last_pool() {
629        let (_, pool2, swap_line) = default_swap_line();
630
631        let pool = swap_line.get_last_pool();
632        assert_eq!(pool.unwrap().get_address(), pool2.get_address());
633    }
634}