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 pub amount_in: SwapAmountType,
58 pub amount_out: SwapAmountType,
60 pub calculation_results: Vec<CalculationResult>,
62 pub swap_to: Option<Address>,
64 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 pub fn contains_pool(&self, pool: &PoolWrapper) -> bool {
152 self.path.contains_pool(pool)
153 }
154
155 pub fn tokens(&self) -> &Vec<Arc<Token>> {
157 &self.path.tokens
158 }
159
160 pub fn pools(&self) -> &Vec<PoolWrapper> {
162 &self.path.pools
163 }
164
165 pub fn get_first_token(&self) -> Option<&Arc<Token>> {
167 self.tokens().first()
168 }
169
170 pub fn get_last_token(&self) -> Option<&Arc<Token>> {
172 self.tokens().last()
173 }
174
175 pub fn get_first_pool(&self) -> Option<&PoolWrapper> {
177 self.pools().first()
178 }
179
180 pub fn get_last_pool(&self) -> Option<&PoolWrapper> {
182 self.pools().last()
183 }
184
185 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 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 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 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 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 #[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 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 #[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 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 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 #[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 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 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 {
485 if first_step_change && inc_direction && current_step < denominator {
486 inc_direction = false;
487 let _next_amount = prev_in_amount;
489 current_in_amount = prev_in_amount;
490 first_step_change = true;
491 } else if first_step_change && !inc_direction {
493 inc_direction = true;
495 current_step /= U256::from(10);
496 best_profit = Some(current_profit);
497 first_step_change = true;
498 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 }
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 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}