1use std::{any::Any, collections::HashMap, fmt::Debug};
2
3use alloy::primitives::U256;
4use num_bigint::{BigUint, ToBigUint};
5use serde::{Deserialize, Serialize};
6use tycho_common::{
7 dto::ProtocolStateDelta,
8 models::token::Token,
9 simulation::{
10 errors::{SimulationError, TransitionError},
11 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
12 },
13 Bytes,
14};
15
16use crate::evm::protocol::{
17 cowamm::{
18 bmath::*,
19 constants::{BONE, MAX_IN_RATIO},
20 error::CowAMMError,
21 },
22 safe_math::{safe_add_u256, safe_div_u256, safe_mul_u256, safe_sub_u256},
23 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
24};
25
26const COWAMM_FEE: f64 = 0.0; type TokenInfo = (Bytes, U256, U256);
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32pub struct CowAMMState {
33 pub address: Bytes,
35 pub token_a: TokenInfo,
37 pub token_b: TokenInfo,
39 pub fee: u64,
41 pub lp_token: Bytes,
43 pub lp_token_supply: U256,
45}
46
47impl CowAMMState {
48 #[allow(clippy::too_many_arguments)]
64 pub fn new(
65 address: Bytes,
66 token_a_addr: Bytes,
67 token_b_addr: Bytes,
68 liquidity_a: U256,
69 liquidity_b: U256,
70 lp_token: Bytes,
71 lp_token_supply: U256,
72 weight_a: U256,
73 weight_b: U256,
74 fee: u64,
75 ) -> Self {
76 Self {
77 address,
78 token_a: (token_a_addr, liquidity_a, weight_a),
79 token_b: (token_b_addr, liquidity_b, weight_b),
80 lp_token,
81 lp_token_supply,
82 fee,
83 }
84 }
85 fn token_a_addr(&self) -> &Bytes {
87 &self.token_a.0
88 }
89
90 fn liquidity_a(&self) -> U256 {
91 self.token_a.1
92 }
93
94 fn liquidity_b(&self) -> U256 {
95 self.token_b.1
96 }
97
98 fn weight_a(&self) -> U256 {
99 self.token_a.2
100 }
101
102 fn weight_b(&self) -> U256 {
103 self.token_b.2
104 }
105
106 pub fn calc_tokens_out_given_exact_lp_token_in(
123 &self,
124 lp_token_in: U256,
125 ) -> Result<(U256, U256), SimulationError> {
126 let liquidity_a = self.liquidity_a();
128 let liquidity_b = self.liquidity_b();
129
130 let balances = [liquidity_a, liquidity_b];
131
132 let total_lp_token = self.lp_token_supply;
133
134 let lp_token_ratio = bdiv(lp_token_in, total_lp_token).map_err(|err| {
136 SimulationError::FatalError(format!("Error in calculating LP token ratio {err:?}"))
137 })?;
138
139 let mut amounts_out = Vec::with_capacity(balances.len());
141
142 for balance in balances.iter() {
143 let amount = bmul(*balance, lp_token_ratio).map_err(|err| {
144 SimulationError::FatalError(format!("Error in calculating amount out {err:?}"))
145 })?;
146 amounts_out.push(amount);
147 }
148 Ok((amounts_out[0], amounts_out[1]))
149 }
150
151 pub fn join_pool(
154 &self,
155 new_state: &mut CowAMMState,
156 pool_amount_out: U256,
157 max_amounts_in: &[U256],
158 ) -> Result<(), CowAMMError> {
159 let pool_total = new_state.lp_token_supply;
160 let ratio = bdiv(pool_amount_out, pool_total)?;
161
162 if ratio.is_zero() {
163 return Err(CowAMMError::InvalidPoolRatio);
164 }
165
166 let balances = vec![new_state.liquidity_a(), new_state.liquidity_b()];
167
168 for (i, bal) in balances.into_iter().enumerate() {
169 let token_amount_in = bmul(ratio, bal)?;
170 if token_amount_in.is_zero() {
171 return Err(CowAMMError::InvalidTokenAmountIn);
172 }
173 if token_amount_in > max_amounts_in[i] {
174 return Err(CowAMMError::TokenAmountInAboveMax);
175 }
176
177 if i == 0 {
179 new_state.token_a.1 = badd(new_state.token_a.1, token_amount_in)?;
180 } else {
181 new_state.token_b.1 = badd(new_state.token_b.1, token_amount_in)?;
182 }
183 }
184
185 new_state.lp_token_supply = badd(new_state.lp_token_supply, pool_amount_out)?;
187 Ok(())
188 }
189
190 pub fn exit_pool(
192 &self,
193 new_state: &mut CowAMMState,
194 pool_amount_in: U256,
195 min_amounts_out: &[U256],
196 exit_fee: U256,
197 ) -> Result<(), CowAMMError> {
198 let pool_total = self.lp_token_supply;
199
200 let fee = bmul(pool_amount_in, exit_fee)?;
202
203 let pai_after_fee = bsub(pool_amount_in, fee)?;
204 let ratio = bdiv(pai_after_fee, pool_total)?;
205
206 if ratio.is_zero() {
207 return Err(CowAMMError::InvalidPoolRatio);
208 }
209
210 new_state.lp_token_supply = bsub(self.lp_token_supply, pai_after_fee)?;
212
213 let balances = vec![self.liquidity_a(), self.liquidity_b()];
214 for (i, bal) in balances.into_iter().enumerate() {
215 let token_amount_out = bmul(ratio, bal)?;
216
217 if token_amount_out.is_zero() {
218 return Err(CowAMMError::InvalidTokenAmountOut);
219 }
220
221 if token_amount_out < min_amounts_out[i] {
222 return Err(CowAMMError::TokenAmountOutBelowMinAmountOut);
223 }
224
225 if i == 0 {
227 new_state.token_a.1 = bsub(self.token_a.1, token_amount_out)?;
228 } else {
229 new_state.token_b.1 = bsub(self.token_b.1, token_amount_out)?;
230 }
231 }
232
233 Ok(())
234 }
235
236 fn get_lp_swap_limits(
256 &self,
257 is_lp_buy: bool,
258 sell_token: Bytes,
259 buy_token: Bytes,
260 ) -> Result<(BigUint, BigUint), SimulationError> {
261 if is_lp_buy {
264 let is_token_a_in = sell_token == *self.token_a_addr();
266
267 let (bal_in, weight_in, bal_out, weight_out) = if is_token_a_in {
268 (self.liquidity_a(), self.weight_a(), self.liquidity_b(), self.weight_b())
269 } else {
270 (self.liquidity_b(), self.weight_b(), self.liquidity_a(), self.weight_a())
271 };
272
273 let max_token_in = bmul(bal_in, MAX_IN_RATIO)
275 .map_err(|err| SimulationError::FatalError(format!("max_in error: {err:?}")))?;
276
277 if max_token_in.is_zero() || bal_in.is_zero() || bal_out.is_zero() {
278 return Ok((BigUint::ZERO, BigUint::ZERO));
279 }
280
281 let mut lo = U256::ZERO;
283 let mut hi = max_token_in;
284 let mut best_x = U256::ZERO;
285 for _ in 0..128 {
286 let x = safe_div_u256(safe_add_u256(lo, hi)?, U256::from(2u8))?;
287 let out = calculate_out_given_in(
288 bal_in,
289 weight_in,
290 bal_out,
291 weight_out,
292 x,
293 U256::from(self.fee),
294 )
295 .map_err(|e| SimulationError::FatalError(format!("amount_out error: {e:?}")))?;
296
297 let in_remaining = safe_sub_u256(max_token_in, x)?;
298 let bal_in_after = safe_add_u256(bal_in, x)?;
299 let bal_out_after = safe_sub_u256(bal_out, out)?;
300
301 let left = safe_mul_u256(in_remaining, bal_out_after)?;
303 let right = safe_mul_u256(out, bal_in_after)?;
304
305 if left > right {
306 lo = safe_add_u256(x, U256::from(1u8))?;
307 } else {
308 best_x = x;
309 if x.is_zero() {
310 break;
311 }
312 hi = safe_sub_u256(x, U256::from(1u8))?;
313 }
314 }
315
316 let x = best_x;
317 let out = calculate_out_given_in(
318 bal_in,
319 weight_in,
320 bal_out,
321 weight_out,
322 x,
323 U256::from(self.fee),
324 )
325 .map_err(|e| SimulationError::FatalError(format!("amount_out error: {e:?}")))?;
326
327 let in_remaining = safe_sub_u256(max_token_in, x)?;
328 let bal_in_after = safe_add_u256(bal_in, x)?;
329 let bal_out_after = safe_sub_u256(bal_out, out)?;
330
331 let pool_total = self.lp_token_supply;
333 let lp_from_in = safe_div_u256(safe_mul_u256(in_remaining, pool_total)?, bal_in_after)?;
334 let lp_from_out = safe_div_u256(safe_mul_u256(out, pool_total)?, bal_out_after)?;
335 let max_lp_out = if lp_from_in < lp_from_out { lp_from_in } else { lp_from_out };
336
337 Ok((u256_to_biguint(max_token_in), u256_to_biguint(max_lp_out)))
338 } else {
339 let is_token_a_out = buy_token == *self.token_a_addr();
341
342 let (unwanted_liquidity, unwanted_weight, wanted_liquidity, wanted_weight) =
343 if is_token_a_out {
344 (self.liquidity_b(), self.weight_b(), self.liquidity_a(), self.weight_a())
346 } else {
347 (self.liquidity_a(), self.weight_a(), self.liquidity_b(), self.weight_b())
349 };
350
351 if unwanted_liquidity.is_zero() {
352 return Ok((BigUint::ZERO, BigUint::ZERO));
353 }
354
355 let max_intermediate_swap_in = bmul(unwanted_liquidity, MAX_IN_RATIO)
357 .map_err(|err| SimulationError::FatalError(format!("max_in error: {err:?}")))?;
358
359 let ratio = bdiv(max_intermediate_swap_in, unwanted_liquidity)
360 .map_err(|err| SimulationError::FatalError(format!("ratio error: {err:?}")))?;
361
362 let max_lp_in = bmul(self.lp_token_supply, ratio)
364 .map_err(|err| SimulationError::FatalError(format!("max_lp_in error: {err:?}")))?;
365
366 let exit_wanted = bmul(wanted_liquidity, ratio).map_err(|err| {
368 SimulationError::FatalError(format!("exit_wanted error: {err:?}"))
369 })?;
370
371 let swap_out = calculate_out_given_in(
373 unwanted_liquidity,
374 unwanted_weight,
375 wanted_liquidity,
376 wanted_weight,
377 max_intermediate_swap_in,
378 U256::from(self.fee),
379 )
380 .map_err(|err| SimulationError::FatalError(format!("amount_out error: {err:?}")))?;
381
382 let max_token_out = safe_add_u256(exit_wanted, swap_out)?;
384
385 Ok((u256_to_biguint(max_lp_in), u256_to_biguint(max_token_out)))
386 }
387 }
388}
389
390#[typetag::serde]
391impl ProtocolSim for CowAMMState {
392 fn fee(&self) -> f64 {
393 COWAMM_FEE
394 }
395 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
407 let (bal_in, weight_in) = if base.address == *self.token_a_addr() {
408 (self.liquidity_a(), self.weight_a())
409 } else if base.address == self.token_b.0 {
410 (self.liquidity_b(), self.weight_b())
411 } else {
412 return Err(SimulationError::FatalError(
413 "spot_price base token not in pool".to_string(),
414 ));
415 };
416
417 let (bal_out, weight_out) = if quote.address == *self.token_a_addr() {
418 (self.liquidity_a(), self.weight_a())
419 } else if quote.address == self.token_b.0 {
420 (self.liquidity_b(), self.weight_b())
421 } else {
422 return Err(SimulationError::FatalError(
423 "spot_price quote token not in pool".to_string(),
424 ));
425 };
426
427 let numer = bdiv(bal_in, weight_in).map_err(|err| {
428 SimulationError::FatalError(format!(
429 "Error in numerator bdiv(balance_base / weight_base): {err:?}"
430 ))
431 })?;
432 let denom = bdiv(bal_out, weight_out).map_err(|err| {
433 SimulationError::FatalError(format!(
434 "Error in denominator bdiv(balance_quote / weight_quote): {err:?}"
435 ))
436 })?;
437
438 let ratio = bmul(
439 bdiv(numer, denom).map_err(|err| {
440 SimulationError::FatalError(format!("Error in (numer / denom): {err:?}"))
441 })?,
442 BONE,
443 )
444 .map_err(|err| {
445 SimulationError::FatalError(format!("Error in bmul(ratio * scale): {err:?}"))
446 })?;
447
448 u256_to_f64(ratio)
449 }
450
451 fn get_amount_out(
452 &self,
453 amount_in: BigUint,
454 token_in: &Token,
455 token_out: &Token,
456 ) -> Result<GetAmountOutResult, SimulationError> {
457 let amount_in = biguint_to_u256(&amount_in);
458 if amount_in.is_zero() {
459 return Err(SimulationError::InvalidInput("Amount in cannot be zero".to_string(), None));
460 }
461
462 let is_lp_in = token_in.address == self.address;
463 let is_lp_out = token_out.address == self.address;
464
465 if is_lp_in && is_lp_out {
466 return Err(SimulationError::InvalidInput(
467 "Cannot swap LP token for LP token".into(),
468 None,
469 ));
470 }
471
472 let mut new_state = self.clone();
473
474 if is_lp_in && !is_lp_out {
478 let (proportional_token_amount_a, proportional_token_amount_b) = self
479 .calc_tokens_out_given_exact_lp_token_in(amount_in)
480 .map_err(|e| {
481 SimulationError::FatalError(format!(
482 "failed to calculate token proportions out error: {e:?}"
483 ))
484 })?;
485 self.exit_pool(
486 &mut new_state,
487 amount_in,
488 &[proportional_token_amount_a, proportional_token_amount_b],
489 U256::from(self.fee),
490 )
491 .map_err(|err| SimulationError::FatalError(format!("exit_pool error: {err:?}")))?;
492
493 let (amount_to_swap, is_token_a_swap_in) = if token_out.address == *self.token_a_addr()
494 {
495 (proportional_token_amount_b, false)
496 } else {
497 (proportional_token_amount_a, true)
498 };
499
500 let amount_out = if is_token_a_swap_in {
501 calculate_out_given_in(
502 new_state.liquidity_a(),
503 new_state.weight_a(),
504 new_state.liquidity_b(),
505 new_state.weight_b(),
506 amount_to_swap,
507 U256::from(self.fee),
508 )
509 } else {
510 calculate_out_given_in(
511 new_state.liquidity_b(),
512 new_state.weight_b(),
513 new_state.liquidity_a(),
514 new_state.weight_a(),
515 amount_to_swap,
516 U256::from(self.fee),
517 )
518 }
519 .map_err(|e| SimulationError::FatalError(format!("amount_out error: {e:?}")))?;
520
521 if is_token_a_swap_in {
522 new_state.token_a.1 = safe_sub_u256(new_state.liquidity_a(), amount_to_swap)?;
523 new_state.token_b.1 = safe_add_u256(new_state.liquidity_b(), amount_out)?;
524 } else {
525 new_state.token_b.1 = safe_sub_u256(new_state.liquidity_b(), amount_to_swap)?;
526 new_state.token_a.1 = safe_add_u256(new_state.liquidity_a(), amount_out)?;
527 }
528
529 let total_trade_amount = if is_token_a_swap_in {
530 safe_add_u256(amount_out, proportional_token_amount_b)?
531 } else {
532 safe_add_u256(amount_out, proportional_token_amount_a)?
533 };
534
535 return Ok(GetAmountOutResult {
536 amount: u256_to_biguint(total_trade_amount),
537 gas: 194_140u64.to_biguint().unwrap(),
538 new_state: Box::new(new_state),
539 });
540 }
541
542 if is_lp_out && !is_lp_in {
546 let fee = U256::from(self.fee);
549 let (bal_in, weight_in, bal_out, weight_out, is_token_a_in) =
550 if token_in.address == *new_state.token_a_addr() {
551 (
552 new_state.liquidity_a(),
553 new_state.weight_a(),
554 new_state.liquidity_b(),
555 new_state.weight_b(),
556 true,
557 )
558 } else {
559 (
560 new_state.liquidity_b(),
561 new_state.weight_b(),
562 new_state.liquidity_a(),
563 new_state.weight_a(),
564 false,
565 )
566 };
567
568 let mut lo = U256::ZERO;
571 let mut hi = amount_in;
572 let mut best_x = U256::ZERO;
573 for _ in 0..128 {
574 let x = safe_div_u256(safe_add_u256(lo, hi)?, U256::from(2u8))?;
576 let out = calculate_out_given_in(bal_in, weight_in, bal_out, weight_out, x, fee)
577 .map_err(|e| SimulationError::FatalError(format!("amount_out error: {e:?}")))?;
578
579 let in_remaining = safe_sub_u256(amount_in, x)?;
581 let bal_in_after = safe_add_u256(bal_in, x)?;
582 let bal_out_after = safe_sub_u256(bal_out, out)?;
583
584 let left = safe_mul_u256(in_remaining, bal_out_after)?;
586 let right = safe_mul_u256(out, bal_in_after)?;
587
588 if left > right {
589 lo = safe_add_u256(x, U256::from(1u8))?;
591 } else {
592 best_x = x;
594 if x.is_zero() {
595 break;
596 }
597 hi = safe_sub_u256(x, U256::from(1u8))?;
598 }
599 }
600
601 let x = best_x;
603 let out = calculate_out_given_in(bal_in, weight_in, bal_out, weight_out, x, fee)
604 .map_err(|e| SimulationError::FatalError(format!("amount_out error: {e:?}")))?;
605
606 let in_remaining = safe_sub_u256(amount_in, x)?;
608 let bal_in_after = safe_add_u256(bal_in, x)?;
609 let bal_out_after = safe_sub_u256(bal_out, out)?;
610
611 if bal_in_after.is_zero() || bal_out_after.is_zero() {
612 return Err(SimulationError::FatalError(
613 "join_pool balance is zero after swap".to_string(),
614 ));
615 }
616
617 let pool_total = new_state.lp_token_supply;
620 let lp_from_in = safe_div_u256(safe_mul_u256(in_remaining, pool_total)?, bal_in_after)?;
621 let lp_from_out = safe_div_u256(safe_mul_u256(out, pool_total)?, bal_out_after)?;
622 let mut lp_out = if lp_from_in < lp_from_out { lp_from_in } else { lp_from_out };
623
624 if lp_out.is_zero() {
625 return Err(SimulationError::FatalError(
626 "join_pool produces zero lp_out".to_string(),
627 ));
628 }
629 if is_token_a_in {
631 new_state.token_a.1 = bal_in_after;
632 new_state.token_b.1 = bal_out_after;
633 } else {
634 new_state.token_b.1 = bal_in_after;
635 new_state.token_a.1 = bal_out_after;
636 }
637
638 let (max_a, max_b) =
640 if is_token_a_in { (in_remaining, out) } else { (out, in_remaining) };
641
642 loop {
646 let ratio = bdiv(lp_out, pool_total).map_err(|err| {
647 SimulationError::FatalError(format!("join_pool ratio error: {err:?}"))
648 })?;
649 let required_a = bmul(ratio, new_state.liquidity_a()).map_err(|err| {
650 SimulationError::FatalError(format!("join_pool amount_a error: {err:?}"))
651 })?;
652 let required_b = bmul(ratio, new_state.liquidity_b()).map_err(|err| {
653 SimulationError::FatalError(format!("join_pool amount_b error: {err:?}"))
654 })?;
655
656 if required_a <= max_a && required_b <= max_b {
657 break;
658 }
659 if lp_out.is_zero() {
660 return Err(SimulationError::FatalError(
661 "join_pool lp_out underflow while applying rounding tolerance".to_string(),
662 ));
663 }
664 lp_out = safe_sub_u256(lp_out, U256::from(1u8))?;
665 }
666
667 self.join_pool(&mut new_state, lp_out, &[max_a, max_b])
669 .map_err(|err| SimulationError::FatalError(format!("join_pool error: {err:?}")))?;
670
671 return Ok(GetAmountOutResult {
672 amount: u256_to_biguint(lp_out),
673 gas: 120_000u64.to_biguint().unwrap(),
674 new_state: Box::new(new_state),
675 });
676 }
677
678 let is_token_a_in = token_in.address == *self.token_a_addr();
682 let (bal_in, weight_in, bal_out, weight_out) = if is_token_a_in {
683 (self.liquidity_a(), self.weight_a(), self.liquidity_b(), self.weight_b())
684 } else {
685 (self.liquidity_b(), self.weight_b(), self.liquidity_a(), self.weight_a())
686 };
687
688 let amount_out = calculate_out_given_in(
689 bal_in,
690 weight_in,
691 bal_out,
692 weight_out,
693 amount_in,
694 U256::from(self.fee),
695 )
696 .map_err(|e| SimulationError::FatalError(format!("amount_out error: {e:?}")))?;
697
698 if is_token_a_in {
699 new_state.token_a.1 = safe_sub_u256(new_state.liquidity_a(), amount_in)?;
700 new_state.token_b.1 = safe_add_u256(new_state.liquidity_b(), amount_out)?;
701 } else {
702 new_state.token_b.1 = safe_sub_u256(new_state.liquidity_b(), amount_in)?;
703 new_state.token_a.1 = safe_add_u256(new_state.liquidity_a(), amount_out)?;
704 }
705
706 Ok(GetAmountOutResult {
707 amount: u256_to_biguint(amount_out),
708 gas: 120_000u64.to_biguint().unwrap(),
709 new_state: Box::new(new_state),
710 })
711 }
712
713 fn get_limits(
714 &self,
715 sell_token: Bytes,
716 buy_token: Bytes,
717 ) -> Result<(BigUint, BigUint), SimulationError> {
718 if self.liquidity_a().is_zero() || self.liquidity_b().is_zero() {
719 return Ok((BigUint::ZERO, BigUint::ZERO));
720 }
721
722 let is_lp_sell = sell_token == self.address;
724 let is_lp_buy = buy_token == self.address;
725
726 if is_lp_sell || is_lp_buy {
728 return self.get_lp_swap_limits(is_lp_buy, sell_token, buy_token);
729 }
730
731 if sell_token == *self.token_a_addr() {
732 let max_in = bmul(self.liquidity_a(), MAX_IN_RATIO)
734 .map_err(|err| SimulationError::FatalError(format!("max_in error: {err:?}")))?;
735
736 let max_out = calculate_out_given_in(
737 self.liquidity_a(),
738 self.weight_a(),
739 self.liquidity_b(),
740 self.weight_b(),
741 max_in,
742 U256::from(self.fee),
743 )
744 .map_err(|err| SimulationError::FatalError(format!("max_out error: {err:?}")))?;
745
746 Ok((u256_to_biguint(max_in), u256_to_biguint(max_out)))
747 } else {
748 let max_in = bmul(self.liquidity_b(), MAX_IN_RATIO)
750 .map_err(|err| SimulationError::FatalError(format!("max_in error: {err:?}")))?;
751
752 let max_out = calculate_out_given_in(
753 self.liquidity_b(),
754 self.weight_b(),
755 self.liquidity_a(),
756 self.weight_a(),
757 max_in,
758 U256::from(self.fee),
759 )
760 .map_err(|err| SimulationError::FatalError(format!("max_out error: {err:?}")))?;
761
762 Ok((u256_to_biguint(max_in), u256_to_biguint(max_out)))
763 }
764 }
765
766 fn delta_transition(
767 &mut self,
768 delta: ProtocolStateDelta,
769 _tokens: &HashMap<Bytes, Token>,
770 _balances: &Balances,
771 ) -> Result<(), TransitionError> {
772 let liquidity_a = U256::from_be_slice(
775 delta
776 .updated_attributes
777 .get("liquidity_a")
778 .ok_or(TransitionError::MissingAttribute("liquidity_a".to_string()))?,
779 );
780
781 let liquidity_b = U256::from_be_slice(
782 delta
783 .updated_attributes
784 .get("liquidity_b")
785 .ok_or(TransitionError::MissingAttribute("liquidity_b".to_string()))?,
786 );
787
788 let lp_token_supply = U256::from_be_slice(
789 delta
790 .updated_attributes
791 .get("lp_token_supply")
792 .ok_or(TransitionError::MissingAttribute("lp_token_supply".to_string()))?,
793 );
794
795 self.token_a.1 = liquidity_a;
796 self.token_b.1 = liquidity_b;
797 self.lp_token_supply = lp_token_supply;
798
799 Ok(())
800 }
801
802 fn query_pool_swap(
803 &self,
804 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
805 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
806 crate::evm::query_pool_swap::query_pool_swap(self, params)
807 }
808
809 fn clone_box(&self) -> Box<dyn ProtocolSim> {
810 Box::new(self.clone())
811 }
812
813 fn as_any(&self) -> &dyn Any {
814 self
815 }
816
817 fn as_any_mut(&mut self) -> &mut dyn Any {
818 self
819 }
820
821 fn eq(&self, other: &dyn ProtocolSim) -> bool {
822 other
823 .as_any()
824 .downcast_ref::<CowAMMState>()
825 .is_some_and(|other_state| self == other_state)
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use std::{
832 collections::{HashMap, HashSet},
833 str::FromStr,
834 };
835
836 use alloy::primitives::U256;
837 use approx::assert_ulps_eq;
838 use num_bigint::BigUint;
839 use num_traits::{One, ToPrimitive, Zero};
840 use rstest::rstest;
841 use tycho_common::{
842 dto::ProtocolStateDelta,
843 models::{token::Token, Chain},
844 simulation::errors::{SimulationError, TransitionError},
845 Bytes,
846 };
847
848 use super::*;
849 use crate::evm::protocol::{
850 cowamm::state::{CowAMMState, ProtocolSim},
851 u256_num::biguint_to_u256,
852 };
853 pub fn wei_to_eth(amount: &BigUint) -> f64 {
855 let divisor = 1e18_f64;
857 amount.to_f64().unwrap_or(0.0) / divisor
858 }
859
860 fn create_test_tokens() -> (Token, Token, Token, Token, Token, Token, Token) {
861 let t0 = Token::new(
862 &Bytes::from_str("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB").unwrap(),
863 "COW",
864 18,
865 0,
866 &[Some(10_000)],
867 Chain::Ethereum,
868 100,
869 );
870
871 let t1 = Token::new(
872 &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
873 "wstETH",
874 18,
875 0,
876 &[Some(10_000)],
877 Chain::Ethereum,
878 100,
879 );
880
881 let t2 = Token::new(
882 &Bytes::from_str("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1").unwrap(),
883 "BCoW-50CoW-50wstETH",
884 18,
885 0,
886 &[Some(199_999_999_999_999_990)],
887 Chain::Ethereum,
888 100,
889 );
890 let t3 = Token::new(
891 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
892 "WETH",
893 18,
894 0,
895 &[Some(1_000_000)],
896 Chain::Ethereum,
897 100,
898 );
899 let t4 = Token::new(
900 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
901 "USDC",
902 6,
903 0,
904 &[Some(10_000_000)],
905 Chain::Ethereum,
906 100,
907 );
908 let t5 = Token::new(
909 &Bytes::from_str("0xBAac2B4491727D78D2b78815144570b9f2Fe8899").unwrap(),
910 "DOG",
911 18,
912 0,
913 &[Some(10_000_000)],
914 Chain::Ethereum,
915 100,
916 );
917 let t6 = Token::new(
918 &Bytes::from_str("0x9d0e8cdf137976e03ef92ede4c30648d05e25285").unwrap(),
919 "wstETH-DOG-LP-Token",
920 18,
921 0,
922 &[Some(10_000_000)],
923 Chain::Ethereum,
924 100,
925 );
926 (t0, t1, t2, t3, t4, t5, t6)
927 }
928
929 #[rstest]
930 #[case::same_dec(
931 U256::from_str("1547000000000000000000").unwrap(), U256::from_str("100000000000000000").unwrap(), 0, 1, BigUint::from_str("5205849666").unwrap(), BigUint::from_str("336513").unwrap(), )]
937 #[case::test_dec(
938 U256::from_str("81297577909021519893").unwrap(), U256::from_str("332162411254631243300976822").unwrap(), 1, 5, BigUint::from_str("1000000000000000000").unwrap(), BigUint::from_str("4036114059417772362872299").unwrap(), )]
944 #[case::diff_dec(
945 U256::from_str("170286779513658066185").unwrap(), U256::from_str("413545982676").unwrap(), 3, 4, BigUint::from_str("217679081735374278").unwrap(), BigUint::from_str("527964550").unwrap(), )]
951 fn test_get_amount_out(
952 #[case] liq_a: U256,
953 #[case] liq_b: U256,
954 #[case] token_in_idx: usize,
955 #[case] token_out_idx: usize,
956 #[case] amount_in: BigUint,
957 #[case] expected_out: BigUint,
958 ) {
959 let (t0, t1, t2, t3, t4, t5, t6) = create_test_tokens();
960 let tokens = [&t0, &t1, &t2, &t3, &t4, &t5, &t6];
961 let token_in = tokens[token_in_idx];
962 let token_out = tokens[token_out_idx];
963
964 let state = CowAMMState::new(
965 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
966 token_in.address.clone(),
967 token_out.address.clone(),
968 liq_a, liq_b, Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
971 U256::from_str("128375712183366405029").unwrap(),
972 U256::from_str("1000000000000000000").unwrap(),
973 U256::from_str("1000000000000000000").unwrap(),
974 0,
975 );
976
977 let res = state
978 .get_amount_out(amount_in.clone(), token_in, token_out)
979 .unwrap();
980
981 assert_eq!(res.amount, expected_out);
982
983 let new_state = res
984 .new_state
985 .as_any()
986 .downcast_ref::<CowAMMState>()
987 .unwrap();
988
989 assert_eq!(
990 new_state.liquidity_a(),
991 safe_sub_u256(liq_a, biguint_to_u256(&amount_in)).unwrap()
992 );
993 assert_eq!(
994 new_state.liquidity_b(),
995 safe_add_u256(liq_b, biguint_to_u256(&expected_out)).unwrap()
996 );
997
998 assert_eq!(state.liquidity_a(), liq_a);
1000 assert_eq!(state.liquidity_b(), liq_b);
1001 }
1002
1003 #[rstest]
1004 #[case::buy_lp_token( U256::from_str("81297577909021519893").unwrap(), U256::from_str("332162411254631243300976822").unwrap(), 1, 6, BigUint::from_str("1000000000000000000").unwrap(), BigUint::from_str("787128927353433245").unwrap(), )]
1011 fn test_get_amount_out_buy_lp_token(
1012 #[case] liq_a: U256,
1013 #[case] liq_b: U256,
1014 #[case] token_in_idx: usize,
1015 #[case] token_out_idx: usize,
1016 #[case] amount_in: BigUint,
1017 #[case] expected_out: BigUint,
1018 ) {
1019 let (t0, t1, t2, t3, t4, t5, t6) = create_test_tokens();
1020 let tokens = [&t0, &t1, &t2, &t3, &t4, &t5, &t6];
1021
1022 let token_a = tokens[token_in_idx];
1023 let token_b = tokens[token_out_idx];
1024
1025 let state = CowAMMState::new(
1026 Bytes::from("0x9d0e8cdf137976e03ef92ede4c30648d05e25285"),
1027 t1.address.clone(), t5.address.clone(), liq_a, liq_b, Bytes::from("0x9d0e8cdf137976e03ef92ede4c30648d05e25285"),
1032 U256::from_str("128375712183366405029").unwrap(),
1033 U256::from_str("1000000000000000000").unwrap(),
1034 U256::from_str("1000000000000000000").unwrap(),
1035 0,
1036 );
1037
1038 let res = state
1039 .get_amount_out(amount_in.clone(), token_a, token_b)
1040 .unwrap();
1041
1042 assert_eq!(res.amount, expected_out);
1043 let new_state = res
1045 .new_state
1046 .as_any()
1047 .downcast_ref::<CowAMMState>()
1048 .unwrap();
1049
1050 assert!(
1051 new_state.lp_token_supply > state.lp_token_supply,
1052 "LP token supply did not increase"
1053 );
1054
1055 assert_eq!(state.liquidity_a(), liq_a);
1057 assert_eq!(state.liquidity_b(), liq_b);
1058 }
1059
1060 #[rstest]
1061 #[case::sell_lp_token( U256::from_str("1547000000000000000000").unwrap(),
1063 U256::from_str("100000000000000000").unwrap(),
1064 2, 0, BigUint::from_str("1000000000000000000").unwrap(), BigUint::from_str("15431325000000000000").unwrap(), )]
1068 fn test_get_amount_out_sell_lp_token(
1069 #[case] liq_a: U256,
1070 #[case] liq_b: U256,
1071 #[case] token_in_idx: usize,
1072 #[case] token_out_idx: usize,
1073 #[case] amount_in: BigUint,
1074 #[case] expected_out: BigUint,
1075 ) {
1076 let (t0, t1, t2, t3, t4, t5, t6) = create_test_tokens();
1077 let tokens = [&t0, &t1, &t2, &t3, &t4, &t5, &t6];
1078
1079 let token_a = tokens[token_in_idx];
1080 let token_b = tokens[token_out_idx];
1081
1082 let state = CowAMMState::new(
1083 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1084 t0.address.clone(), t1.address.clone(), liq_a, liq_b, Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1089 U256::from_str("199999999999999999990").unwrap(),
1090 U256::from_str("1000000000000000000").unwrap(),
1091 U256::from_str("1000000000000000000").unwrap(),
1092 0,
1093 );
1094
1095 let res = state
1096 .get_amount_out(amount_in.clone(), token_a, token_b)
1097 .unwrap();
1098
1099 assert_eq!(res.amount, expected_out);
1100 let new_state = res
1102 .new_state
1103 .as_any()
1104 .downcast_ref::<CowAMMState>()
1105 .unwrap();
1106
1107 if token_a.address == t2.address {
1108 assert!(
1109 new_state.lp_token_supply < state.lp_token_supply,
1110 "LP token supply did not reduce"
1111 );
1112 } else {
1113 assert!(
1114 new_state.lp_token_supply > state.lp_token_supply,
1115 "LP token supply did not reduce"
1116 );
1117 }
1118
1119 assert_eq!(state.liquidity_a(), liq_a);
1121 assert_eq!(state.liquidity_b(), liq_b);
1122 }
1123
1124 #[test]
1125 fn test_get_amount_out_overflow() {
1126 let max = (BigUint::one() << 256) - BigUint::one();
1127
1128 let (t0, t1, _, _, _, _, _) = create_test_tokens();
1129
1130 let state = CowAMMState::new(
1131 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1132 t0.address.clone(),
1133 t1.address.clone(),
1134 U256::from_str("886800000000000000").unwrap(),
1135 U256::from_str("50000000000000000").unwrap(),
1136 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1137 U256::from_str("100000000000000000000").unwrap(),
1138 U256::from_str("1000000000000000000").unwrap(),
1139 U256::from_str("1000000000000000000").unwrap(),
1140 0,
1141 );
1142
1143 let res = state.get_amount_out(max, &t0.clone(), &t1.clone());
1144 assert!(res.is_err());
1145 let err = res.err().unwrap();
1146 assert!(matches!(err, SimulationError::FatalError(_)));
1147 }
1148
1149 #[rstest]
1150 #[case(244752492017f64)]
1151 fn test_spot_price(#[case] expected: f64) {
1152 let (t0, _, _, _, _, t5, _) = create_test_tokens();
1153 let state = CowAMMState::new(
1154 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1155 t0.address.clone(),
1156 t5.address.clone(),
1157 U256::from_str("81297577909021519893").unwrap(),
1158 U256::from_str("332162411254631243300976822").unwrap(),
1159 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1160 U256::from_str("128375712183366405029").unwrap(),
1161 U256::from_str("1000000000000000000").unwrap(),
1162 U256::from_str("1000000000000000000").unwrap(),
1163 0,
1164 );
1165
1166 let price = state.spot_price(&t0, &t5).unwrap();
1167 assert_ulps_eq!(price, expected);
1168 }
1169
1170 #[test]
1171 fn test_fee() {
1172 let (t0, t1, _, _, _, _, _) = create_test_tokens();
1173
1174 let state = CowAMMState::new(
1175 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1176 t0.address.clone(),
1177 t1.address.clone(),
1178 U256::from_str("36925554990922").unwrap(),
1179 U256::from_str("30314846538607556521556").unwrap(),
1180 Bytes::from("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"),
1181 U256::from_str("36925554990922").unwrap(),
1182 U256::from_str("30314846538607556521556").unwrap(),
1183 U256::from_str("30314846538607556521556").unwrap(),
1184 0,
1185 );
1186
1187 let res = state.fee();
1188
1189 assert_ulps_eq!(res, 0.0);
1190 }
1191 #[test]
1192 fn test_delta_transition() {
1193 let (t0, t1, _, _, _, _, _) = create_test_tokens();
1194
1195 let mut state = CowAMMState::new(
1196 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1197 t0.address.clone(),
1198 t1.address.clone(),
1199 U256::from_str("36925554990922").unwrap(),
1200 U256::from_str("30314846538607556521556").unwrap(),
1201 Bytes::from("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"),
1202 U256::from_str("36925554990922").unwrap(),
1203 U256::from_str("30314846538607556521556").unwrap(),
1204 U256::from_str("30314846538607556521556").unwrap(),
1205 0,
1206 );
1207 let attributes: HashMap<String, Bytes> = vec![
1208 ("liquidity_a".to_string(), Bytes::from(15000_u64.to_be_bytes().to_vec())),
1209 ("liquidity_b".to_string(), Bytes::from(20000_u64.to_be_bytes().to_vec())),
1210 ("lp_token_supply".to_string(), Bytes::from(250000_u64.to_be_bytes().to_vec())),
1211 ]
1212 .into_iter()
1213 .collect();
1214 let delta = ProtocolStateDelta {
1215 component_id: "State1".to_owned(),
1216 updated_attributes: attributes,
1217 deleted_attributes: HashSet::new(),
1218 };
1219
1220 let res = state.delta_transition(delta, &HashMap::new(), &Balances::default());
1221
1222 assert!(res.is_ok());
1223 assert_eq!(state.liquidity_a(), U256::from_str("15000").unwrap());
1224 assert_eq!(state.liquidity_b(), U256::from_str("20000").unwrap());
1225 assert_eq!(state.lp_token_supply, U256::from_str("250000").unwrap());
1226 }
1227
1228 #[test]
1229 fn test_delta_transition_missing_attribute() {
1230 let mut state = CowAMMState::new(
1231 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1232 Bytes::from("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB"),
1233 Bytes::from("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"),
1234 U256::from_str("36925554990922").unwrap(),
1235 U256::from_str("30314846538607556521556").unwrap(),
1236 Bytes::from("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"),
1237 U256::from_str("36928554990972").unwrap(),
1238 U256::from_str("30314846538607556521556").unwrap(),
1239 U256::from_str("30314846538607556521556").unwrap(),
1240 0,
1241 );
1242 let attributes: HashMap<String, Bytes> = vec![(
1243 "liquidity_a".to_string(),
1244 Bytes::from(
1245 1500000000000000_u64
1246 .to_be_bytes()
1247 .to_vec(),
1248 ),
1249 )]
1250 .into_iter()
1251 .collect();
1252
1253 let delta = ProtocolStateDelta {
1254 component_id: "State1".to_owned(),
1255 updated_attributes: attributes,
1256 deleted_attributes: HashSet::new(),
1257 };
1258
1259 let res = state.delta_transition(delta, &HashMap::new(), &Balances::default());
1260
1261 assert!(res.is_err());
1262 match res {
1263 Err(e) => {
1264 assert!(matches!(e, TransitionError::MissingAttribute(ref x) if x== "liquidity_b"))
1265 }
1266 _ => panic!("Test failed: was expecting an Err value"),
1267 };
1268 }
1269
1270 #[test]
1271 fn test_get_limits_price_impact() {
1272 let (t0, t1, _, _, _, _, _) = create_test_tokens();
1273
1274 let state = CowAMMState::new(
1275 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1276 t0.address.clone(),
1277 t1.address.clone(),
1278 U256::from_str("886800000000000000").unwrap(),
1279 U256::from_str("50000000000000000").unwrap(),
1280 Bytes::from("0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1"),
1281 U256::from_str("100000000000000000000").unwrap(),
1282 U256::from_str("1000000000000000000").unwrap(),
1283 U256::from_str("1000000000000000000").unwrap(),
1284 0,
1285 );
1286
1287 let (amount_in, _) = state
1288 .get_limits(
1289 Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1290 Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1291 )
1292 .unwrap();
1293
1294 let t0 = Token::new(
1295 &Bytes::from_str("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB").unwrap(),
1296 "COW",
1297 18,
1298 0,
1299 &[Some(10_000)],
1300 Chain::Ethereum,
1301 100,
1302 );
1303
1304 let t1 = Token::new(
1305 &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
1306 "wstETH",
1307 18,
1308 0,
1309 &[Some(10_000)],
1310 Chain::Ethereum,
1311 100,
1312 );
1313
1314 let result = state
1315 .get_amount_out(amount_in.clone(), &t0, &t1)
1316 .unwrap();
1317 let new_state = result
1318 .new_state
1319 .as_any()
1320 .downcast_ref::<CowAMMState>()
1321 .unwrap();
1322
1323 let initial_price = state.spot_price(&t0, &t1).unwrap();
1324 println!("Initial spot price (t0 -> t1): {}", initial_price);
1325
1326 let new_price = new_state.spot_price(&t0, &t1).unwrap();
1327
1328 println!("New spot price (t0 -> t1), floored: {}", new_price);
1329
1330 assert!(new_price < initial_price);
1331 }
1332 #[test]
1333 fn test_arb_weth_lp_limits_calculation() {
1334 let arb = Token::new(
1335 &Bytes::from_str("0xb50721bcf8d664c30412cfbc6cf7a15145234ad1").unwrap(),
1336 "ARB",
1337 18,
1338 0,
1339 &[Some(10_000)],
1340 Chain::Ethereum,
1341 100,
1342 );
1343 let weth = Token::new(
1344 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1345 "WETH",
1346 18,
1347 0,
1348 &[Some(10_000)],
1349 Chain::Ethereum,
1350 100,
1351 );
1352 let lp_token = Token::new(
1353 &Bytes::from_str("0x4359a8ea4c353d93245c0b6b8608a28bb48a05e2").unwrap(),
1354 "BCoW-50ARB-50WETH",
1355 18,
1356 0,
1357 &[Some(60_502_268_657_704_388)],
1358 Chain::Ethereum,
1359 100,
1360 );
1361
1362 let state = CowAMMState::new(
1363 Bytes::from_str("0x4359a8ea4c353d93245c0b6b8608a28bb48a05e2").unwrap(),
1364 arb.address.clone(),
1365 weth.address.clone(),
1366 U256::from_str("286275852074040134274570").unwrap(), U256::from_str("61694306956323018369").unwrap(), Bytes::from_str("0x4359a8ea4c353d93245c0b6b8608a28bb48a05e2").unwrap(),
1369 U256::from_str("60502268657704388057834").unwrap(),
1370 U256::from_str("1000000000000000000").unwrap(),
1371 U256::from_str("1000000000000000000").unwrap(),
1372 0,
1373 );
1374
1375 let (max_lp_in, max_weth_out) = state
1378 .get_limits(lp_token.address.clone(), weth.address.clone())
1379 .unwrap();
1380
1381 println!("LP → WETH limits:");
1382 println!(" Max LP in: {}", max_lp_in);
1383 println!(" Max WETH out: {:.6} WETH", wei_to_eth(&max_weth_out));
1384
1385 let amount_in = max_lp_in.clone() / BigUint::from(10u64);
1387 println!("\nTesting with 10% of safe max: {}", amount_in);
1388
1389 let res = state
1390 .get_amount_out(amount_in.clone(), &lp_token, &weth)
1391 .expect("Should succeed with safe limit");
1392 assert!(!res.amount.is_zero(), "Amount out should be non-zero for a valid LP redemption");
1394 }
1395}