Skip to main content

tycho_simulation/evm/protocol/cowamm/
state.rs

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; // 0% fee
27
28// Token 3 tuple: (address, liquidity, weight)
29type TokenInfo = (Bytes, U256, U256);
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32pub struct CowAMMState {
33    /// The Pool Address
34    pub address: Bytes,
35    /// Token A information: (address, liquidity, weight)
36    pub token_a: TokenInfo,
37    /// Token B information: (address, liquidity, weight)
38    pub token_b: TokenInfo,
39    /// The Swap Fee on the Pool
40    pub fee: u64,
41    ///The lp token of the pool
42    pub lp_token: Bytes,
43    /// The Supply of the lp token
44    pub lp_token_supply: U256,
45}
46
47impl CowAMMState {
48    /// Creates a new `CowAMMState` instance.
49    ///
50    /// # Arguments
51    /// - `token_a`: The address of the first token in the pair.
52    /// - `token_b`: The address of the second token in the pair.
53    /// - `liquidity_a`: Liquidity of the first token in the pair.
54    /// - `liquidity_b`: Liquidity of the second token in the pair.
55    /// - `lp_token`: The pool address (usually the LP token address).
56    /// - `lp_token_supply`: The supply of the lp_token in the pool
57    /// - `weight_a`: The denormalized weight of `token_a`.
58    /// - `weight_b`: The denormalized weight of `token_b`.
59    /// - `fee`: The swap fee for the pool.
60    ///
61    /// # Returns
62    /// A new `CowAMMState` with token states initialized.
63    #[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    /// Helper methods
86    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    /// Calculates the proportion of tokens a user receives when exiting the pool by burning LP
107    /// tokens,
108    ///
109    ///
110    /// Solidity reference:
111    /// https://github.com/balancer/balancer-v2-monorepo/blob/master/pkg/core/contracts/pools/weighted/WeightedMath.sol#L299
112    ///
113    /// Formula:
114    /// amountOut[i] = balances[i] * (lpTokenAmountIn / totalLpToken)
115    ///
116    /// # Arguments
117    /// * `pool` - Reference to the pool containing balances and LP supply
118    /// * `lp_token_in` - Amount of LP tokens to burn
119    ///
120    /// # Returns
121    /// Tuple of `(amountOut_tokenA, amountOut_tokenB)`
122    pub fn calc_tokens_out_given_exact_lp_token_in(
123        &self,
124        lp_token_in: U256,
125    ) -> Result<(U256, U256), SimulationError> {
126        // Collect balances
127        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        // lpTokenRatio = lp_token_in / total_lp_token
135        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        // amountsOut[i] = balances[i] * lp_token_ratio
140        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    //https://github.com/balancer/cow-amm/blob/main/src/contracts/BPool.sol#L174
152    /// joinPool
153    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            // equivalent to _pullUnderlying
178            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        // mint LP shares
186        new_state.lp_token_supply = badd(new_state.lp_token_supply, pool_amount_out)?;
187        Ok(())
188    }
189
190    /// exitPool
191    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        // calculate fee
201        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        // burn LP shares
211        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            // Update new_state balances
226            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    /// Calculates swap limits for operations involving LP tokens.
237    ///
238    /// # Parameters
239    /// - `is_lp_buy`:
240    ///   - `false` → **LP → Token** (redeeming LP tokens)
241    ///   - `true`  → **Token → LP** (minting or acquiring LP tokens)
242    /// - `sell_token`: Token being provided by the user
243    /// - `buy_token`: Token being received by the user
244    ///
245    /// # Returns
246    /// - When `is_lp_buy == false` (**LP → Token**):
247    ///   - `(max_lp_in, max_token_out)`
248    ///     - `max_lp_in`: Maximum LP tokens that can be redeemed without exceeding swap limits
249    ///     - `max_token_out`: Maximum amount of the underlying token that can be received
250    ///
251    /// - When `is_lp_buy == true` (**Token → LP**):
252    ///   - `(max_token_in, max_lp_out)`
253    ///     - `max_token_in`: Maximum amount of the underlying token that can be provided
254    ///     - `max_lp_out`: Maximum LP tokens that can be minted or received
255    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        // LP swaps are modeled as: swap part of the input to the other leg, then join/exit.
262        // Limits cap the intermediate swap via MAX_IN_RATIO; dust is allowed.
263        if is_lp_buy {
264            // Buy LP: swap part of the sell token into the other leg, then join.
265            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            // Cap total input by MAX_IN_RATIO of the input token balance.
274            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            // Find a split (x to swap, max_token_in - x to keep) that matches pool proportions.
282            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                // Target: in_remaining / bal_in_after == out / bal_out_after
302                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            // LP minting is limited by the smaller proportional contribution.
332            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            // Sell LP: exit to both tokens, then swap the unwanted leg into the desired token.
340            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                    // Want token_a; swap token_b into token_a after exit.
345                    (self.liquidity_b(), self.weight_b(), self.liquidity_a(), self.weight_a())
346                } else {
347                    // Want token_b; swap token_a into token_b after exit.
348                    (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            // Cap the exit ratio so the unwanted leg can be fully swapped under MAX_IN_RATIO.
356            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            // LP in upper bound implied by the ratio cap.
363            let max_lp_in = bmul(self.lp_token_supply, ratio)
364                .map_err(|err| SimulationError::FatalError(format!("max_lp_in error: {err:?}")))?;
365
366            // Wanted token from the exit leg at the capped ratio.
367            let exit_wanted = bmul(wanted_liquidity, ratio).map_err(|err| {
368                SimulationError::FatalError(format!("exit_wanted error: {err:?}"))
369            })?;
370
371            // Wanted token from swapping the unwanted leg at MAX_IN_RATIO.
372            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            // Total upper bound: wanted from exit + wanted from swap.
383            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    /// Calculates a f64 representation of base token price in the AMM.
396    /// ********************************************************************************************
397    /// ** calcSpotPrice
398    /// // sP = spotPrice
399    /// // bI = tokenBalanceIn                ( bI / wI )         1
400    /// // bO = tokenBalanceOut         sP =  -----------  *  ----------
401    /// // wI = tokenWeightIn                 ( bO / wO )     ( 1 - sF )
402    /// // wO = tokenWeightOut
403    /// // sF = swapFee
404    /// // *************************************************************************************
405    /// *********/
406    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        // ============================
475        // EXIT POOL (LP → TOKEN) //
476        // ============================
477        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        // ============================
543        // JOIN POOL (TOKEN → LP)
544        // ============================
545        if is_lp_out && !is_lp_in {
546            // This is the TOKEN -> LP flow: we will split the input token into
547            // (1) a swap leg to balance pool proportions and (2) a join leg to mint LP.
548            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            // Binary search the swap amount (x) that makes the post-swap ratio
569            // match the join ratio, minimizing dust on the join leg.
570            let mut lo = U256::ZERO;
571            let mut hi = amount_in;
572            let mut best_x = U256::ZERO;
573            for _ in 0..128 {
574                // Midpoint in integer space (floor division).
575                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                // Split: x is swapped, the remainder joins as the original token.
580                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                // Compare cross-products to decide which side of the ratio we are on.
585                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                    // Too much remaining input relative to swap output: increase swap amount.
590                    lo = safe_add_u256(x, U256::from(1u8))?;
591                } else {
592                    // Swap is large enough; record candidate and try smaller to reduce dust.
593                    best_x = x;
594                    if x.is_zero() {
595                        break;
596                    }
597                    hi = safe_sub_u256(x, U256::from(1u8))?;
598                }
599            }
600
601            // Final swap leg using the best candidate x.
602            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            // Post-swap balances that will be used for the join leg.
607            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            // Compute the LP shares implied by each side, then take the minimum
618            // to avoid over-minting relative to either asset.
619            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            // Update the local state with the post-swap balances before joining.
630            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            // These are the maximum amounts we are allowed to contribute to the join.
639            let (max_a, max_b) =
640                if is_token_a_in { (in_remaining, out) } else { (out, in_remaining) };
641
642            // join_pool uses BONE-based rounding (bdiv/bmul), which can require slightly
643            // more input than the floor-based amounts above. Reduce lp_out until the
644            // implied required amounts fit within max_a/max_b.
645            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            // Perform the join with the adjusted lp_out so the pool accepts the inputs.
668            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        // ============================
679        // NORMAL SWAP
680        // ============================
681        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        // Check if this is an LP token swap
723        let is_lp_sell = sell_token == self.address;
724        let is_lp_buy = buy_token == self.address;
725
726        // Use special LP swap limits that account for intermediate swaps
727        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            // Sell token A for token B
733            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            // Sell token B for token A
749            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        // liquidity_a, liquidity_b and lp_token_supply are considered required attributes and are
773        // expected in every delta we process
774        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    /// Converts a `BigUint` amount in wei to f64 ETH/WETH
854    pub fn wei_to_eth(amount: &BigUint) -> f64 {
855        // 1 ETH = 10^18 wei
856        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(), //COW balance
932        U256::from_str("100000000000000000").unwrap(), //wstETH balance
933        0, 1, // token indices: t0 -> t1
934        BigUint::from_str("5205849666").unwrap(),  //COW in
935        BigUint::from_str("336513").unwrap(), //wstETH out
936    )]
937    #[case::test_dec(
938        U256::from_str("81297577909021519893").unwrap(), //wstETH balance
939        U256::from_str("332162411254631243300976822").unwrap(), //DOG balance
940        1, 5, // token indices: t1 -> t5
941        BigUint::from_str("1000000000000000000").unwrap(),  //wstETH in
942        BigUint::from_str("4036114059417772362872299").unwrap(), //DOG out
943    )]
944    #[case::diff_dec(
945        U256::from_str("170286779513658066185").unwrap(), //WETH balance
946        U256::from_str("413545982676").unwrap(), //USDC balance
947        3, 4, // token indices: t3 -> t4
948        BigUint::from_str("217679081735374278").unwrap(), //WETH in
949        BigUint::from_str("527964550").unwrap(), // USDC out (≈ 0.2177 WETH) for 527964550 USDC (≈ 527.96 USDC)
950    )]
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, //wstETH
969            liq_b, //DOG
970            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        // Original state unchanged
999        assert_eq!(state.liquidity_a(), liq_a);
1000        assert_eq!(state.liquidity_b(), liq_b);
1001    }
1002
1003    #[rstest]
1004    #[case::buy_lp_token( //buying lp token with COW == buying amounts of wstETH needed to join pool with COW, then joining pool with both tokens
1005        U256::from_str("81297577909021519893").unwrap(),      // wstETH balance
1006        U256::from_str("332162411254631243300976822").unwrap(), //DOG balance
1007        1, 6, // token indices: wstETH -> wstETH-DOG-LP-Token
1008        BigUint::from_str("1000000000000000000").unwrap(), // wstETH in
1009        BigUint::from_str("787128927353433245").unwrap(), // Amount of wstETH-DOG-LP-Token we buy from join the pool
1010    )]
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(), //wstETH
1028            t5.address.clone(), //DOG
1029            liq_a,              //wstETH Liquidity
1030            liq_b,              //DOG Liquidity
1031            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        //lp token supply reduced
1044        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        // Original state unchanged
1056        assert_eq!(state.liquidity_a(), liq_a);
1057        assert_eq!(state.liquidity_b(), liq_b);
1058    }
1059
1060    #[rstest]
1061    #[case::sell_lp_token( //selling (redeeming) lp_token for COW == exiting pool and converting excess COW to wstETH
1062        U256::from_str("1547000000000000000000").unwrap(),
1063        U256::from_str("100000000000000000").unwrap(),
1064        2, 0, // token indices: t2 -> t0
1065        BigUint::from_str("1000000000000000000").unwrap(), //Amount of lp_token being sold (redeeemd)
1066        BigUint::from_str("15431325000000000000").unwrap(), //COW as output, verify that we sent this amount to the pool in total
1067    )]
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(), //COW
1085            t1.address.clone(), //wstETH
1086            liq_a,              //COW
1087            liq_b,              //wstETH
1088            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        //lp token supply reduced
1101        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        // Original state unchanged
1120        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(), //ARB
1367            U256::from_str("61694306956323018369").unwrap(),     //WETH
1368            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        //Get limits for LP → WETH
1376        //uses lp_limits
1377        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        // Test with 10% of the SAFE max
1386        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        // Basic sanity assertions
1393        assert!(!res.amount.is_zero(), "Amount out should be non-zero for a valid LP redemption");
1394    }
1395}