Skip to main content

tycho_simulation/evm/protocol/lunarbase/
state.rs

1use std::{any::Any, collections::HashMap};
2
3use lunarbase_pmm_math::{
4    sqrt_price_x96_to_price, try_simulate_successful_swap, Direction, MathError, PoolParams,
5    RollbackReason, SimulationStatus, MAX_U112, MAX_U24, U256,
6};
7use num_bigint::BigUint;
8use tycho_common::{
9    dto::ProtocolStateDelta,
10    models::token::Token,
11    simulation::{
12        errors::{SimulationError, TransitionError},
13        protocol_sim::{
14            Balances, BlockContext, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
15            SwapConstraint,
16        },
17    },
18    Bytes,
19};
20
21use super::decoder::apply_delta;
22
23pub type Address = [u8; 20];
24// Pool-call estimate for routing, not a transaction gas limit.
25const DEFAULT_GAS: u64 = 100_000;
26
27/// Native simulation of the linear anchor and immediate punishment mechanism.
28///
29/// Tycho partner quotes always use fee multiplier one.
30/// Active-reserve transitions assume standard token transfers and fully credited
31/// fees with sufficient fee-bucket capacity; caller-specific fee accounting and
32/// unsynced donations are not indexed.
33#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
34pub struct LunarBaseState {
35    pub pool: Address,
36    pub token_x: Address,
37    pub token_y: Address,
38    pub anchor_price_x96: U256,
39    pub fee_ask_x24: u32,
40    pub fee_bid_x24: u32,
41    pub latest_update_block: u64,
42    pub reserve_x: u128,
43    pub reserve_y: u128,
44    pub max_punishment_x24: u32,
45    pub block_delay: u64,
46    pub paused: bool,
47    pub head_block: u64,
48}
49
50impl LunarBaseState {
51    pub fn pool_params(&self) -> PoolParams {
52        PoolParams {
53            sqrt_price_x96: self.anchor_price_x96,
54            fee_ask_x24: self.fee_ask_x24,
55            fee_bid_x24: self.fee_bid_x24,
56            reserve_x: self.reserve_x,
57            reserve_y: self.reserve_y,
58            max_punishment_x24: self.max_punishment_x24,
59        }
60    }
61
62    pub fn is_fresh(&self) -> bool {
63        self.head_block <
64            self.latest_update_block
65                .saturating_add(self.block_delay)
66    }
67
68    fn quote_exact_in(
69        &self,
70        token_in: Address,
71        token_out: Address,
72        amount_in: U256,
73    ) -> Result<(U256, Self), QuoteError> {
74        if self.paused {
75            return Err(QuoteError::Paused);
76        }
77
78        if !self.is_fresh() {
79            return Err(QuoteError::Stale {
80                block_number: self.head_block,
81                latest_update_block: self.latest_update_block,
82                block_delay: self.block_delay,
83            });
84        }
85
86        let direction = if token_in == self.token_x && token_out == self.token_y {
87            Direction::XToY
88        } else if token_in == self.token_y && token_out == self.token_x {
89            Direction::YToX
90        } else {
91            return Err(QuoteError::InvalidTokenPair);
92        };
93        let simulation = try_simulate_successful_swap(
94            &self.pool_params(),
95            amount_in,
96            direction,
97            U256::from(1u64),
98        )
99        .map_err(QuoteError::Math)?;
100        match simulation.status {
101            SimulationStatus::Applied => {}
102            SimulationStatus::RolledBack(RollbackReason::SwapImpossible) => {
103                return Err(QuoteError::Rejected);
104            }
105            SimulationStatus::RolledBack(RollbackReason::ReserveTransitionOverflow) => {
106                return Err(QuoteError::ReserveOverflow);
107            }
108            SimulationStatus::RolledBack(RollbackReason::LaterRevert) => {
109                return Err(QuoteError::Rejected);
110            }
111        }
112
113        let mut next = self.clone();
114        next.reserve_x = simulation.post_swap.reserve_x;
115        next.reserve_y = simulation.post_swap.reserve_y;
116        next.fee_ask_x24 = simulation.post_swap.fee_ask_x24;
117        next.fee_bid_x24 = simulation.post_swap.fee_bid_x24;
118        Ok((simulation.quote.amount_out, next))
119    }
120}
121
122#[typetag::serde]
123impl ProtocolSim for LunarBaseState {
124    fn fee(&self) -> f64 {
125        0.0
126    }
127
128    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
129        self.pool_params()
130            .validate()
131            .map_err(|err| map_quote_error(QuoteError::Math(err)))?;
132        let token_in = address_from_bytes(base.address.as_ref())?;
133        let token_out = address_from_bytes(quote.address.as_ref())?;
134        if token_in == self.token_x && token_out == self.token_y {
135            return Ok(apply_fee_discount(
136                anchor_price(self.anchor_price_x96, base, quote),
137                self.fee_bid_x24,
138            ));
139        }
140        if token_in == self.token_y && token_out == self.token_x {
141            if self.anchor_price_x96.is_zero() {
142                return Ok(0.0);
143            }
144            return Ok(apply_fee_discount(
145                1.0 / anchor_price(self.anchor_price_x96, quote, base),
146                self.fee_ask_x24,
147            ));
148        }
149        Err(SimulationError::InvalidInput("invalid LunarBase token pair".to_owned(), None))
150    }
151
152    fn get_amount_out(
153        &self,
154        amount_in: BigUint,
155        token_in: &Token,
156        token_out: &Token,
157    ) -> Result<GetAmountOutResult, SimulationError> {
158        if amount_in == BigUint::ZERO {
159            return Ok(GetAmountOutResult::new(
160                BigUint::ZERO,
161                BigUint::from(DEFAULT_GAS),
162                Box::new(self.clone()),
163            ));
164        }
165
166        let (amount_out, next_state) = self
167            .quote_exact_in(
168                address_from_bytes(token_in.address.as_ref())?,
169                address_from_bytes(token_out.address.as_ref())?,
170                biguint_to_u256(&amount_in)?,
171            )
172            .map_err(map_quote_error)?;
173
174        Ok(GetAmountOutResult::new(
175            u256_to_biguint(amount_out),
176            BigUint::from(DEFAULT_GAS),
177            Box::new(next_state),
178        ))
179    }
180
181    fn get_limits(
182        &self,
183        sell_token: Bytes,
184        buy_token: Bytes,
185    ) -> Result<(BigUint, BigUint), SimulationError> {
186        let sell = address_from_bytes(sell_token.as_ref())?;
187        let buy = address_from_bytes(buy_token.as_ref())?;
188        if sell == self.token_x && buy == self.token_y {
189            return quote_limit(self, sell, buy, Direction::XToY);
190        }
191        if sell == self.token_y && buy == self.token_x {
192            return quote_limit(self, sell, buy, Direction::YToX);
193        }
194        Err(SimulationError::InvalidInput("invalid LunarBase token pair".to_owned(), None))
195    }
196
197    fn delta_transition(
198        &mut self,
199        delta: ProtocolStateDelta,
200        _tokens: &HashMap<Bytes, Token>,
201        _balances: &Balances,
202    ) -> Result<(), TransitionError> {
203        if let Some(name) = delta.deleted_attributes.iter().next() {
204            return Err(TransitionError::DecodeError(format!(
205                "LunarBase does not support deleted attributes: {name}"
206            )));
207        }
208
209        apply_delta(self, delta.updated_attributes)
210            .map_err(|err| TransitionError::DecodeError(format!("{err:?}")))?;
211        Ok(())
212    }
213
214    fn apply_block(&mut self, block: &BlockContext) -> bool {
215        let was_fresh = self.is_fresh();
216        self.head_block = block.number();
217        was_fresh != self.is_fresh()
218    }
219
220    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
221        let (price, tolerance, min_amount, max_amount, trade_price) = match params.swap_constraint()
222        {
223            SwapConstraint::TradeLimitPrice { limit, tolerance, min_amount_in, max_amount_in } => {
224                (limit, *tolerance, min_amount_in, max_amount_in, true)
225            }
226            SwapConstraint::PoolTargetPrice { target, tolerance, min_amount_in, max_amount_in } => {
227                (target, *tolerance, min_amount_in, max_amount_in, false)
228            }
229        };
230        if price.numerator == BigUint::ZERO ||
231            price.denominator == BigUint::ZERO ||
232            !tolerance.is_finite() ||
233            tolerance < 0.0 ||
234            matches!((min_amount, max_amount), (Some(min), Some(max)) if min > max)
235        {
236            return Err(SimulationError::InvalidInput(
237                "invalid LunarBase price query bounds".into(),
238                None,
239            ));
240        }
241        let token_in = params.token_in();
242        let token_out = params.token_out();
243        let (mut maximum, _) =
244            self.get_limits(token_in.address.clone(), token_out.address.clone())?;
245        if let Some(max) = max_amount {
246            maximum = maximum.min(max.clone());
247        }
248        let zero = || PoolSwap::new(BigUint::ZERO, BigUint::ZERO, Box::new(self.clone()), None);
249        if maximum == BigUint::ZERO {
250            return Ok(zero());
251        }
252        let direction = if token_in.address.as_ref() == self.token_x {
253            Direction::XToY
254        } else {
255            Direction::YToX
256        };
257        let (minimum, _) = quote_input_bounds(self, direction);
258        if maximum < minimum ||
259            min_amount
260                .as_ref()
261                .is_some_and(|min| maximum < *min)
262        {
263            return Ok(zero());
264        }
265        let (amount_out, next) = self
266            .quote_exact_in(
267                address_from_bytes(token_in.address.as_ref())?,
268                address_from_bytes(token_out.address.as_ref())?,
269                biguint_to_u256(&maximum)?,
270            )
271            .map_err(map_quote_error)?;
272        let amount_out = u256_to_biguint(amount_out);
273        let full_fill_allowed = if trade_price {
274            // Compare actual rounded output in raw token units, without f64/decimal loss.
275            &amount_out * &price.denominator >= &maximum * &price.numerator
276        } else {
277            // The post-swap marginal price includes the updated directional punishment.
278            let anchor_squared = u256_to_biguint(next.anchor_price_x96).pow(2);
279            let q192 = BigUint::from(1u32) << 192;
280            let (numerator, denominator, fee) = match direction {
281                Direction::XToY => (anchor_squared, q192, next.fee_bid_x24),
282                Direction::YToX => (q192, anchor_squared, next.fee_ask_x24),
283            };
284            let discount = if fee == MAX_U24 { 0 } else { (1u32 << 24) - fee };
285            numerator * discount * &price.denominator >=
286                denominator * (1u32 << 24) * &price.numerator
287        };
288        if full_fill_allowed {
289            // A liquidity/amount cap can prevent reaching the target even though every
290            // tradable amount remains above it. Return the capped fill instead of an error.
291            return Ok(PoolSwap::new(maximum, amount_out, Box::new(next), None));
292        }
293        // Keep the existing numerical search for an interior price crossing. Its search
294        // currently ignores optional amount bounds, so never return an out-of-bounds fill.
295        let result = crate::evm::query_pool_swap::query_pool_swap(self, params)?;
296        if result.amount_in() > &maximum ||
297            min_amount
298                .as_ref()
299                .is_some_and(|min| result.amount_in() < min)
300        {
301            return Err(SimulationError::InvalidInput(
302                "LunarBase price crossing outside amount bounds".into(),
303                None,
304            ));
305        }
306        // The numerical search can round distinct rational prices to the same f64.
307        if trade_price &&
308            result.amount_out() * &price.denominator < result.amount_in() * &price.numerator
309        {
310            return Err(SimulationError::InvalidInput(
311                "LunarBase fill below trade limit price".into(),
312                None,
313            ));
314        }
315        Ok(result)
316    }
317
318    fn clone_box(&self) -> Box<dyn ProtocolSim> {
319        Box::new(self.clone())
320    }
321
322    fn as_any(&self) -> &dyn Any {
323        self
324    }
325
326    fn as_any_mut(&mut self) -> &mut dyn Any {
327        self
328    }
329
330    fn eq(&self, other: &dyn ProtocolSim) -> bool {
331        other.as_any().downcast_ref::<Self>() == Some(self)
332    }
333}
334
335#[derive(Clone, Debug, PartialEq, Eq)]
336enum QuoteError {
337    Paused,
338    Stale { block_number: u64, latest_update_block: u64, block_delay: u64 },
339    InvalidTokenPair,
340    Rejected,
341    ReserveOverflow,
342    Math(MathError),
343}
344
345fn anchor_price(anchor_price_x96: U256, token_in: &Token, token_out: &Token) -> f64 {
346    let decimals_adjustment = 10f64.powi(token_in.decimals as i32 - token_out.decimals as i32);
347    sqrt_price_x96_to_price(anchor_price_x96) * decimals_adjustment
348}
349
350fn apply_fee_discount(price: f64, fee_x24: u32) -> f64 {
351    if fee_x24 == MAX_U24 {
352        return 0.0;
353    }
354    price * (1.0 - fee_x24 as f64 / (1u64 << 24) as f64)
355}
356
357/// Invert both gross-output floors to bound all executable inputs. A positive gross
358/// quote stays positive unless the effective fee reaches the MAX_U24 sentinel.
359fn quote_input_bounds(state: &LunarBaseState, direction: Direction) -> (BigUint, BigUint) {
360    let (reserve_in, reserve_out) = match direction {
361        Direction::XToY => (state.reserve_x, state.reserve_y),
362        Direction::YToX => (state.reserve_y, state.reserve_x),
363    };
364    if reserve_out == 0 || state.anchor_price_x96.is_zero() {
365        return (BigUint::ZERO, BigUint::ZERO);
366    }
367    let anchor = u256_to_biguint(state.anchor_price_x96);
368    let q96 = BigUint::from(1u64) << 96;
369    let (numerator, denominator) = match direction {
370        Direction::XToY => (q96, anchor),
371        Direction::YToX => (anchor, q96),
372    };
373    // floor(x * d / n) <= r iff x <= ((r + 1) * n - 1) / d.
374    let intermediate = ((BigUint::from(reserve_out) + 1u32) * &numerator - 1u32) / &denominator;
375    let maximum = ((intermediate + 1u32) * &numerator - 1u32) / &denominator;
376    // Inverse ceilings find the first input with at least one gross output unit.
377    let intermediate = (&numerator + &denominator - 1u32) / &denominator;
378    let minimum = (intermediate * &numerator + &denominator - 1u32) / denominator;
379    (minimum, maximum.min(BigUint::from(MAX_U112 - reserve_in)))
380}
381
382fn quote_limit(
383    state: &LunarBaseState,
384    token_in: Address,
385    token_out: Address,
386    direction: Direction,
387) -> Result<(BigUint, BigUint), SimulationError> {
388    if state.paused || !state.is_fresh() {
389        return Ok((BigUint::ZERO, BigUint::ZERO));
390    }
391    state
392        .pool_params()
393        .validate()
394        .map_err(|err| map_quote_error(QuoteError::Math(err)))?;
395    let (mut low, mut high) = quote_input_bounds(state, direction);
396    if high == BigUint::ZERO || low > high {
397        return Ok((BigUint::ZERO, BigUint::ZERO));
398    }
399    match state.quote_exact_in(token_in, token_out, biguint_to_u256(&high)?) {
400        Ok((output, _)) => return Ok((high, u256_to_biguint(output))),
401        Err(QuoteError::Rejected | QuoteError::ReserveOverflow) => {}
402        Err(err) => return Err(map_quote_error(err)),
403    }
404    let mut low_output = match state.quote_exact_in(token_in, token_out, biguint_to_u256(&low)?) {
405        Ok((output, _)) => u256_to_biguint(output),
406        Err(QuoteError::Rejected | QuoteError::ReserveOverflow) => {
407            return Ok((BigUint::ZERO, BigUint::ZERO));
408        }
409        Err(err) => return Err(map_quote_error(err)),
410    };
411    // Gross output and punishment are nondecreasing. Between the positive-output
412    // minimum and the reserve bounds, only fee saturation can stop execution.
413    // Keep low executable and high rejected; at most 112 bisections are needed.
414    while &high - &low > BigUint::from(1u32) {
415        let middle = (&low + &high) >> 1;
416        match state.quote_exact_in(token_in, token_out, biguint_to_u256(&middle)?) {
417            Ok((output, _)) => {
418                low = middle;
419                low_output = u256_to_biguint(output);
420            }
421            Err(QuoteError::Rejected | QuoteError::ReserveOverflow) => high = middle,
422            Err(err) => return Err(map_quote_error(err)),
423        }
424    }
425    Ok((low, low_output))
426}
427
428fn address_from_bytes(value: &[u8]) -> Result<Address, SimulationError> {
429    value.try_into().map_err(|_| {
430        SimulationError::InvalidInput(
431            format!("expected 20-byte address, got {}", value.len()),
432            None,
433        )
434    })
435}
436
437fn biguint_to_u256(value: &BigUint) -> Result<U256, SimulationError> {
438    let bytes = value.to_bytes_be();
439    if bytes.len() > 32 {
440        return Err(SimulationError::InvalidInput("amount_in exceeds uint256".to_owned(), None));
441    }
442    Ok(U256::from_be_slice(&bytes))
443}
444
445fn u256_to_biguint(value: U256) -> BigUint {
446    BigUint::from_bytes_be(&value.to_be_bytes::<32>())
447}
448
449fn map_quote_error(err: QuoteError) -> SimulationError {
450    SimulationError::InvalidInput(format!("LunarBase quote rejected: {err:?}"), None)
451}
452
453#[cfg(test)]
454mod tests {
455    use tycho_common::models::Chain;
456
457    use super::*;
458
459    fn addr(byte: u8) -> [u8; 20] {
460        [byte; 20]
461    }
462
463    fn token(address: Address, symbol: &str, decimals: u32) -> Token {
464        Token::new(
465            &Bytes::from(address.to_vec()),
466            symbol,
467            decimals,
468            100,
469            &[Some(100_000)],
470            Chain::Base,
471            100,
472        )
473    }
474
475    fn state() -> LunarBaseState {
476        LunarBaseState {
477            pool: addr(9),
478            token_x: addr(1),
479            token_y: addr(2),
480            anchor_price_x96: U256::from(1u128 << 96),
481            fee_ask_x24: 0,
482            fee_bid_x24: 0,
483            latest_update_block: 100,
484            reserve_x: 1_000_000,
485            reserve_y: 1_000_000,
486            max_punishment_x24: 0,
487            block_delay: 2,
488            paused: false,
489            head_block: 100,
490        }
491    }
492
493    #[test]
494    fn limits_use_output_liquidity_for_asymmetric_reserves() {
495        for x_to_y in [true, false] {
496            let mut state = state();
497            let (sell, buy) = if x_to_y {
498                state.reserve_x = 1;
499                state.reserve_y = 100;
500                (state.token_x, state.token_y)
501            } else {
502                state.reserve_x = 100;
503                state.reserve_y = 1;
504                (state.token_y, state.token_x)
505            };
506            assert_eq!(
507                state
508                    .get_limits(sell.into(), buy.into())
509                    .unwrap(),
510                (100u32.into(), 100u32.into())
511            );
512        }
513    }
514
515    #[test]
516    fn limits_find_the_last_fill_before_punishment_saturates() {
517        let mut state = state();
518        state.reserve_x = 100;
519        state.reserve_y = 100;
520        state.fee_bid_x24 = 1 << 23;
521        state.fee_ask_x24 = 1 << 23;
522        state.max_punishment_x24 = MAX_U24;
523        for (sell, buy) in [(state.token_x, state.token_y), (state.token_y, state.token_x)] {
524            let expected = (1..=200u64)
525                .rev()
526                .find_map(|input| {
527                    state
528                        .quote_exact_in(sell, buy, U256::from(input))
529                        .ok()
530                        .map(|(output, _)| (BigUint::from(input), u256_to_biguint(output)))
531                })
532                .unwrap();
533            assert_eq!(
534                state
535                    .get_limits(sell.into(), buy.into())
536                    .unwrap(),
537                expected
538            );
539        }
540    }
541
542    #[test]
543    fn limits_use_remaining_input_reserve_capacity_exactly() {
544        let mut state = state();
545        state.reserve_x = MAX_U112 - 7;
546        state.reserve_y = 100;
547        assert_eq!(
548            state
549                .get_limits(state.token_x.into(), state.token_y.into())
550                .unwrap(),
551            (7u32.into(), 7u32.into())
552        );
553    }
554
555    #[test]
556    fn price_queries_fill_flat_pools_in_both_directions() {
557        use tycho_common::simulation::protocol_sim::{Price, SwapConstraint};
558        for x_to_y in [true, false] {
559            for trade_price in [true, false] {
560                let state = state();
561                let (sell, buy) = if x_to_y {
562                    (state.token_x, state.token_y)
563                } else {
564                    (state.token_y, state.token_x)
565                };
566                for max in [None, Some(BigUint::from(123u32))] {
567                    let price = Price::new(999u32.into(), 1000u32.into());
568                    let constraint = if trade_price {
569                        SwapConstraint::TradeLimitPrice {
570                            limit: price,
571                            tolerance: 1e-6,
572                            min_amount_in: None,
573                            max_amount_in: max.clone(),
574                        }
575                    } else {
576                        SwapConstraint::PoolTargetPrice {
577                            target: price,
578                            tolerance: 1e-6,
579                            min_amount_in: None,
580                            max_amount_in: max.clone(),
581                        }
582                    };
583                    // Raw-unit prices do not depend on display decimals.
584                    let params = QueryPoolSwapParams::new(
585                        token(sell, "IN", 18),
586                        token(buy, "OUT", 6),
587                        constraint,
588                    );
589                    let swap = state.query_pool_swap(&params).unwrap();
590                    let expected = max.unwrap_or(1_000_000u32.into());
591                    assert_eq!(swap.amount_in(), &expected);
592                    assert_eq!(swap.amount_out(), &expected);
593                }
594            }
595        }
596    }
597
598    #[test]
599    fn price_queries_distinguish_rounded_trade_and_post_swap_prices() {
600        use tycho_common::simulation::protocol_sim::{Price, SwapConstraint};
601        let mut state = state();
602        state.reserve_y = 2;
603        state.fee_bid_x24 = 1 << 22;
604        let x = token(state.token_x, "X", 18);
605        let y = token(state.token_y, "Y", 18);
606        let params = QueryPoolSwapParams::new(
607            x.clone(),
608            y.clone(),
609            SwapConstraint::TradeLimitPrice {
610                limit: Price::new(9u32.into(), 10u32.into()),
611                tolerance: 0.0,
612                min_amount_in: None,
613                max_amount_in: None,
614            },
615        );
616        let result = state.query_pool_swap(&params).unwrap();
617        assert_eq!(result.amount_in(), &BigUint::from(2u32));
618        assert_eq!(result.amount_out(), &BigUint::from(2u32));
619        let params = QueryPoolSwapParams::new(
620            x,
621            y,
622            SwapConstraint::PoolTargetPrice {
623                target: Price::new(9u32.into(), 10u32.into()),
624                tolerance: 0.0,
625                min_amount_in: None,
626                max_amount_in: None,
627            },
628        );
629        assert!(state.query_pool_swap(&params).is_err());
630    }
631
632    #[test]
633    fn price_queries_preserve_bounds_zero_capacity_and_exact_equality() {
634        use tycho_common::simulation::protocol_sim::{Price, SwapConstraint};
635        for trade in [true, false] {
636            for (min, max, expected) in [
637                (None, None, 1_000_000u32),
638                (Some(12u32), Some(20u32), 20),
639                (None, Some(0), 0),
640                (Some(1_000_001), None, 0),
641            ] {
642                let mut state = state();
643                let constraint = if trade {
644                    SwapConstraint::TradeLimitPrice {
645                        limit: Price::new(1u32.into(), 1u32.into()),
646                        tolerance: 0.0,
647                        min_amount_in: min.map(BigUint::from),
648                        max_amount_in: max.map(BigUint::from),
649                    }
650                } else {
651                    SwapConstraint::PoolTargetPrice {
652                        target: Price::new(1u32.into(), 1u32.into()),
653                        tolerance: 0.0,
654                        min_amount_in: min.map(BigUint::from),
655                        max_amount_in: max.map(BigUint::from),
656                    }
657                };
658                let params = QueryPoolSwapParams::new(
659                    token(state.token_x, "X", 18),
660                    token(state.token_y, "Y", 18),
661                    constraint,
662                );
663                assert_eq!(
664                    state
665                        .query_pool_swap(&params)
666                        .unwrap()
667                        .amount_in(),
668                    &BigUint::from(expected)
669                );
670                state.paused = true;
671                assert_eq!(
672                    state
673                        .query_pool_swap(&params)
674                        .unwrap()
675                        .amount_in(),
676                    &BigUint::ZERO
677                );
678            }
679        }
680        let state = rounding_boundary_state(true, 1);
681        let params = QueryPoolSwapParams::new(
682            token(state.token_x, "X", 18),
683            token(state.token_y, "Y", 18),
684            SwapConstraint::TradeLimitPrice {
685                limit: Price::new(1u32.into(), 100u32.into()),
686                tolerance: 0.0,
687                min_amount_in: None,
688                max_amount_in: Some(2u32.into()),
689            },
690        );
691        assert_eq!(
692            state
693                .query_pool_swap(&params)
694                .unwrap()
695                .amount_in(),
696            &BigUint::ZERO
697        );
698    }
699
700    #[test]
701    fn price_queries_keep_interior_punishment_search() {
702        use tycho_common::simulation::protocol_sim::{Price, SwapConstraint};
703        let mut state = state();
704        state.max_punishment_x24 = 1 << 20;
705        let x = token(state.token_x, "X", 18);
706        let y = token(state.token_y, "Y", 6);
707        for trade in [true, false] {
708            let constraint = if trade {
709                SwapConstraint::TradeLimitPrice {
710                    limit: Price::new(99u32.into(), 100u32.into()),
711                    tolerance: 1e-6,
712                    min_amount_in: Some(100u32.into()),
713                    max_amount_in: Some(900_000u32.into()),
714                }
715            } else {
716                SwapConstraint::PoolTargetPrice {
717                    target: Price::new(99u32.into(), 100u32.into()),
718                    tolerance: 1e-6,
719                    min_amount_in: Some(100u32.into()),
720                    max_amount_in: Some(900_000u32.into()),
721                }
722            };
723            let params = QueryPoolSwapParams::new(x.clone(), y.clone(), constraint);
724            let result = state.query_pool_swap(&params).unwrap();
725            assert!(result.amount_in() > &BigUint::from(100u32));
726            assert!(result.amount_in() < &BigUint::from(900_000u32));
727            assert_eq!(
728                &state
729                    .get_amount_out(result.amount_in().clone(), &x, &y)
730                    .unwrap()
731                    .amount,
732                result.amount_out()
733            );
734            if trade {
735                assert!(result.amount_out() * 100u32 >= result.amount_in() * 99u32);
736            }
737        }
738    }
739
740    #[test]
741    fn price_queries_reject_fallback_below_exact_trade_limit() {
742        use tycho_common::simulation::protocol_sim::{Price, SwapConstraint};
743        let mut state = state();
744        state.max_punishment_x24 = 1 << 20;
745        let scale = BigUint::from(10u32).pow(30);
746        let params = QueryPoolSwapParams::new(
747            token(state.token_x, "X", 18),
748            token(state.token_y, "Y", 18),
749            SwapConstraint::TradeLimitPrice {
750                limit: Price::new(&scale * 99u32 + 1u32, &scale * 100u32),
751                tolerance: 1e-6,
752                min_amount_in: None,
753                max_amount_in: None,
754            },
755        );
756        // The generic f64 search rounds this limit to 0.99 and returns 320000 -> 316800.
757        // That fill is strictly below the requested rational price.
758        assert!(matches!(
759            state.query_pool_swap(&params),
760            Err(SimulationError::InvalidInput(message, _))
761                if message == "LunarBase fill below trade limit price"
762        ));
763    }
764
765    #[test]
766    fn randomized_limits_are_executable_and_maximal() {
767        // Fixed-seed xorshift keeps property coverage reproducible without a new dependency.
768        let mut seed = 0x1445_cafe_7813_u64;
769        let mut random = || {
770            seed ^= seed << 13;
771            seed ^= seed >> 7;
772            seed ^= seed << 17;
773            seed
774        };
775        for case in 0..6000 {
776            let mut state = state();
777            state.anchor_price_x96 = U256::from(random() % 17) << 94usize;
778            state.reserve_x = (random() % 32) as u128;
779            state.reserve_y = (random() % 32) as u128;
780            state.fee_ask_x24 = (random() % (1 << 24)) as u32;
781            state.fee_bid_x24 = (random() % (1 << 24)) as u32;
782            state.max_punishment_x24 = (random() % (1 << 24)) as u32;
783            if case % 5 == 0 {
784                state.max_punishment_x24 = MAX_U24;
785            }
786            if case % 7 == 0 {
787                state.fee_bid_x24 = MAX_U24;
788            }
789            if case % 11 == 0 {
790                state.reserve_x = MAX_U112 - state.reserve_x;
791            }
792            if case % 13 == 0 {
793                state.reserve_y = MAX_U112 - state.reserve_y;
794            }
795            if case % 17 == 0 {
796                state.paused = true;
797            }
798            if case % 19 == 0 {
799                state.head_block = 102;
800            }
801            for (sell, buy) in [(state.token_x, state.token_y), (state.token_y, state.token_x)] {
802                let token_in = token(sell, "IN", 18);
803                let token_out = token(buy, "OUT", 18);
804                let (input, output) = state
805                    .get_limits(sell.into(), buy.into())
806                    .unwrap();
807                if input != BigUint::ZERO {
808                    let quoted = state
809                        .get_amount_out(input.clone(), &token_in, &token_out)
810                        .unwrap();
811                    assert_eq!(quoted.amount, output, "case={case}");
812                    assert_ne!(output, BigUint::ZERO, "case={case}");
813                    assert!(
814                        state
815                            .get_amount_out(&input + 1u32, &token_in, &token_out)
816                            .is_err(),
817                        "case={case}"
818                    );
819                } else {
820                    assert_eq!(output, BigUint::ZERO);
821                }
822                if state.reserve_x < 32 && state.reserve_y < 32 {
823                    // At these anchors all possible fills fit in 512 input units.
824                    // Exhaustive search independently checks zero limits as well as maxima.
825                    let expected = (1..=512u64)
826                        .rev()
827                        .find_map(|amount| {
828                            state
829                                .get_amount_out(BigUint::from(amount), &token_in, &token_out)
830                                .ok()
831                                .map(|quote| (BigUint::from(amount), quote.amount))
832                        })
833                        .unwrap_or((BigUint::ZERO, BigUint::ZERO));
834                    assert_eq!(
835                        (input, output),
836                        expected,
837                        "case={case}, sell={sell:?}, state={state:?}"
838                    );
839                }
840            }
841        }
842    }
843
844    #[test]
845    fn quotes_x_to_y_and_transitions_reserves() {
846        let state = state();
847        let (amount_out, next_state) = state
848            .quote_exact_in(state.token_x, state.token_y, U256::from(1_000u64))
849            .unwrap();
850
851        assert_eq!(amount_out, U256::from(1_000u64));
852        assert_eq!(next_state.reserve_x, 1_001_000);
853        assert_eq!(next_state.reserve_y, 999_000);
854        assert_eq!(next_state.anchor_price_x96, state.anchor_price_x96);
855        assert_eq!(next_state.head_block, state.head_block);
856    }
857
858    #[test]
859    fn zero_amount_out_returns_zero_without_transition() {
860        let state = state();
861        let token_x = token(state.token_x, "ETH", 18);
862        let token_y = token(state.token_y, "USDC", 6);
863
864        let quote = state
865            .get_amount_out(BigUint::ZERO, &token_x, &token_y)
866            .unwrap();
867        let next_state = quote
868            .new_state
869            .as_any()
870            .downcast_ref::<LunarBaseState>()
871            .unwrap();
872
873        assert_eq!(quote.amount, BigUint::ZERO);
874        assert_eq!(next_state, &state);
875    }
876
877    #[test]
878    fn spot_price_uses_anchor_price() {
879        let mut state = state();
880        state.anchor_price_x96 = lunarbase_pmm_math::price_to_sqrt_price_x96(2_000.0 / 1e12);
881        state.reserve_x = 1_000_000_000_000_000_000;
882        state.reserve_y = 1_000_000;
883
884        let token_x = token(state.token_x, "ETH", 18);
885        let token_y = token(state.token_y, "USDC", 6);
886
887        let price = state
888            .spot_price(&token_x, &token_y)
889            .unwrap();
890        let inverse = state
891            .spot_price(&token_y, &token_x)
892            .unwrap();
893
894        assert!((price - 2_000.0).abs() < 1e-6);
895        assert!((inverse - 0.0005).abs() < 1e-12);
896    }
897
898    #[test]
899    fn spot_price_applies_directional_base_fee() {
900        let mut state = state();
901        state.anchor_price_x96 = lunarbase_pmm_math::price_to_sqrt_price_x96(2_000.0 / 1e12);
902        state.fee_ask_x24 = (1u32 << 24) / 100;
903        state.fee_bid_x24 = (2 * (1u32 << 24)) / 100;
904
905        let token_x = token(state.token_x, "ETH", 18);
906        let token_y = token(state.token_y, "USDC", 6);
907
908        let price = state
909            .spot_price(&token_x, &token_y)
910            .unwrap();
911        let inverse = state
912            .spot_price(&token_y, &token_x)
913            .unwrap();
914
915        assert!((price - (2_000.0 * 0.98)).abs() < 1e-3);
916        assert!((inverse - (0.0005 * 0.99)).abs() < 1e-9);
917    }
918
919    #[test]
920    fn rejects_stale_state() {
921        let mut state = state();
922        state.head_block = 102;
923
924        let err = state
925            .quote_exact_in(state.token_x, state.token_y, U256::from(1_000u64))
926            .unwrap_err();
927
928        assert_eq!(
929            err,
930            QuoteError::Stale { block_number: 102, latest_update_block: 100, block_delay: 2 }
931        );
932    }
933
934    #[test]
935    fn unavailable_pools_have_zero_limits_but_still_reject_swaps() {
936        for (paused, head_block) in [(true, 100), (false, 102), (true, 103)] {
937            let mut state = state();
938            state.paused = paused;
939            state.head_block = head_block;
940            for (sell, buy) in [(state.token_x, state.token_y), (state.token_y, state.token_x)] {
941                assert_eq!(
942                    state
943                        .get_limits(Bytes::from(sell), Bytes::from(buy))
944                        .unwrap(),
945                    (BigUint::ZERO, BigUint::ZERO)
946                );
947                assert!(state
948                    .get_amount_out(
949                        BigUint::from(1_000u64),
950                        &token(sell, "IN", 18),
951                        &token(buy, "OUT", 18),
952                    )
953                    .is_err());
954            }
955            assert!(state
956                .get_limits(Bytes::from(addr(3)), Bytes::from(state.token_y))
957                .is_err());
958        }
959    }
960
961    #[test]
962    fn limits_recover_after_unpause_or_fresh_operator_update() {
963        let mut state = state();
964        let sell = Bytes::from(state.token_x);
965        let buy = Bytes::from(state.token_y);
966        let available_limits = state
967            .get_limits(sell.clone(), buy.clone())
968            .unwrap();
969        assert!(available_limits.0 > BigUint::ZERO);
970        assert!(available_limits.1 > BigUint::ZERO);
971
972        state.paused = true;
973        assert_eq!(
974            state
975                .get_limits(sell.clone(), buy.clone())
976                .unwrap()
977                .0,
978            BigUint::ZERO
979        );
980        apply_delta(&mut state, HashMap::from([("paused".to_owned(), Bytes::from([0u8]))]))
981            .unwrap();
982        assert_eq!(
983            state
984                .get_limits(sell.clone(), buy.clone())
985                .unwrap(),
986            available_limits
987        );
988
989        state.apply_block(&BlockContext::new(102, 0));
990        assert_eq!(
991            state
992                .get_limits(sell.clone(), buy.clone())
993                .unwrap()
994                .0,
995            BigUint::ZERO
996        );
997        apply_delta(
998            &mut state,
999            HashMap::from([("latest_update_block".to_owned(), Bytes::from(102u64))]),
1000        )
1001        .unwrap();
1002        assert_eq!(state.get_limits(sell, buy).unwrap(), available_limits);
1003    }
1004
1005    #[test]
1006    fn charges_immediate_punishment_and_accumulates_only_the_traded_direction() {
1007        let mut state = state();
1008        state.max_punishment_x24 = 16_778;
1009        let amount = U256::from(100_000u64);
1010
1011        let (first_out, first) = state
1012            .quote_exact_in(state.token_x, state.token_y, amount)
1013            .unwrap();
1014        assert_eq!(first_out, U256::from(99_995u64));
1015        assert_eq!((first.reserve_x, first.reserve_y), (1_100_000, 900_000));
1016        assert_eq!((first.fee_ask_x24, first.fee_bid_x24), (0, 839));
1017        assert_eq!(first.anchor_price_x96, state.anchor_price_x96);
1018        assert_eq!(first.latest_update_block, state.latest_update_block);
1019
1020        let (second_out, second) = first
1021            .quote_exact_in(state.token_x, state.token_y, amount)
1022            .unwrap();
1023        assert_eq!(second_out, U256::from(99_990u64));
1024        assert_eq!((second.fee_ask_x24, second.fee_bid_x24), (0, 1_678));
1025
1026        let (reverse_out, reverse) = second
1027            .quote_exact_in(state.token_y, state.token_x, amount)
1028            .unwrap();
1029        assert_eq!(reverse_out, U256::from(99_995u64));
1030        assert_eq!((reverse.fee_ask_x24, reverse.fee_bid_x24), (839, 1_678));
1031        assert_eq!((state.fee_ask_x24, state.fee_bid_x24), (0, 0));
1032    }
1033
1034    #[test]
1035    fn operator_update_replaces_accumulated_fees_and_restores_freshness() {
1036        let mut initial = state();
1037        initial.max_punishment_x24 = 16_778;
1038        let (_, mut state) = initial
1039            .quote_exact_in(initial.token_x, initial.token_y, U256::from(100_000u64))
1040            .unwrap();
1041        state.apply_block(&BlockContext::new(102, 0));
1042        let reserves = (state.reserve_x, state.reserve_y);
1043        state
1044            .delta_transition(
1045                ProtocolStateDelta {
1046                    updated_attributes: HashMap::from([
1047                        (
1048                            "anchor_price_x96".to_owned(),
1049                            Bytes::from(
1050                                (U256::from(2u64) << 96usize).to_be_bytes::<32>()[12..].to_vec(),
1051                            ),
1052                        ),
1053                        ("fee_ask_x24".to_owned(), Bytes::from(11u32)),
1054                        ("fee_bid_x24".to_owned(), Bytes::from(22u32)),
1055                        ("latest_update_block".to_owned(), Bytes::from(102u64)),
1056                        ("block_number".to_owned(), Bytes::from(102u64)),
1057                    ]),
1058                    ..Default::default()
1059                },
1060                &HashMap::new(),
1061                &Balances::default(),
1062            )
1063            .unwrap();
1064
1065        assert!(state.is_fresh());
1066        assert_eq!((state.fee_ask_x24, state.fee_bid_x24), (11, 22));
1067        assert_eq!(state.max_punishment_x24, 16_778);
1068        assert_eq!((state.reserve_x, state.reserve_y), reserves);
1069    }
1070
1071    #[test]
1072    fn full_fee_and_punishment_saturation_reject_without_changing_state() {
1073        let mut state = state();
1074        state.fee_bid_x24 = MAX_U24;
1075        let before = state.clone();
1076        assert_eq!(
1077            state.quote_exact_in(state.token_x, state.token_y, U256::from(1u64)),
1078            Err(QuoteError::Rejected)
1079        );
1080        assert_eq!(state, before);
1081        assert_eq!(
1082            state
1083                .spot_price(&token(state.token_x, "X", 18), &token(state.token_y, "Y", 18))
1084                .unwrap(),
1085            0.0
1086        );
1087
1088        state.fee_bid_x24 = MAX_U24 - 1;
1089        state.max_punishment_x24 = 1;
1090        let before = state.clone();
1091        assert_eq!(
1092            state.quote_exact_in(state.token_x, state.token_y, U256::from(1u64)),
1093            Err(QuoteError::Rejected)
1094        );
1095        assert_eq!(state, before);
1096    }
1097
1098    #[test]
1099    fn max_punishment_sentinel_uses_conceptual_one_hundred_percent() {
1100        let mut state = state();
1101        state.reserve_x = 0;
1102        state.reserve_y = 1_000_000;
1103        state.max_punishment_x24 = MAX_U24;
1104        assert_eq!(
1105            state.quote_exact_in(state.token_x, state.token_y, U256::from(1_000_000u64)),
1106            Err(QuoteError::Rejected)
1107        );
1108    }
1109
1110    #[test]
1111    fn rejects_uint112_reserve_overflow_and_invalid_math_state() {
1112        let mut state = state();
1113        state.reserve_x = lunarbase_pmm_math::MAX_U112;
1114        let before = state.clone();
1115        assert_eq!(
1116            state.quote_exact_in(state.token_x, state.token_y, U256::from(1u64)),
1117            Err(QuoteError::ReserveOverflow)
1118        );
1119        assert_eq!(state, before);
1120
1121        state.max_punishment_x24 = 1 << 24;
1122        assert_eq!(
1123            state.quote_exact_in(state.token_x, state.token_y, U256::from(1u64)),
1124            Err(QuoteError::Math(MathError::MaxPunishmentExceedsUint24))
1125        );
1126    }
1127
1128    #[test]
1129    fn quotes_anchor_above_uint128_and_checks_gross_liquidity() {
1130        let mut state = state();
1131        state.anchor_price_x96 = U256::from(1u64) << 129usize;
1132        state.reserve_y = 1u128 << 80;
1133        let expected_gross = U256::from(1u64) << 66usize;
1134        let (out, next) = state
1135            .quote_exact_in(state.token_x, state.token_y, U256::from(1u64))
1136            .unwrap();
1137        assert_eq!(out, expected_gross);
1138        assert_eq!(next.reserve_y, state.reserve_y - (1u128 << 66));
1139        state.reserve_y = (1u128 << 66) - 1;
1140        state.fee_bid_x24 = (1 << 24) / 2;
1141        assert_eq!(
1142            state.quote_exact_in(state.token_x, state.token_y, U256::from(1u64)),
1143            Err(QuoteError::Rejected)
1144        );
1145    }
1146
1147    #[test]
1148    fn execution_block_hook_expires_idle_pools_and_handles_reorgs() {
1149        let mut state = state();
1150        assert!(!state.apply_block(&BlockContext::new(101, 0)));
1151        assert_eq!(state.head_block, 101);
1152        assert!(state.is_fresh());
1153
1154        assert!(state.apply_block(&BlockContext::new(102, 0)));
1155        assert!(!state.is_fresh());
1156        assert!(!state.apply_block(&BlockContext::new(102, 0)));
1157        assert!(!state.apply_block(&BlockContext::new(103, 0)));
1158        assert!(matches!(
1159            state.quote_exact_in(state.token_x, state.token_y, U256::from(1u64)),
1160            Err(QuoteError::Stale { block_number: 103, .. })
1161        ));
1162
1163        assert!(state.apply_block(&BlockContext::new(101, 0)));
1164        assert!(state.is_fresh());
1165    }
1166
1167    #[test]
1168    fn zero_anchor_never_returns_infinite_spot_price() {
1169        let mut state = state();
1170        state.anchor_price_x96 = U256::ZERO;
1171        let x = token(state.token_x, "X", 18);
1172        let y = token(state.token_y, "Y", 18);
1173        assert_eq!(state.spot_price(&x, &y).unwrap(), 0.0);
1174        assert_eq!(state.spot_price(&y, &x).unwrap(), 0.0);
1175        assert_eq!(
1176            state.quote_exact_in(state.token_y, state.token_x, U256::from(1u64)),
1177            Err(QuoteError::Rejected)
1178        );
1179    }
1180
1181    #[test]
1182    fn limits_allow_both_directions_with_an_empty_input_reserve() {
1183        let mut state = state();
1184        state.anchor_price_x96 = U256::from(2u64) << 96usize;
1185        state.max_punishment_x24 = 16_778;
1186
1187        state.reserve_x = 0;
1188        state.reserve_y = 4_000_000;
1189        let (input, output) = state
1190            .get_limits(Bytes::from(state.token_x), Bytes::from(state.token_y))
1191            .unwrap();
1192        assert_eq!(input, BigUint::from(1_000_000u64));
1193        assert_eq!(output, BigUint::from(3_996_000u64));
1194
1195        state.reserve_x = 1_000_000;
1196        state.reserve_y = 0;
1197        let (input, output) = state
1198            .get_limits(Bytes::from(state.token_y), Bytes::from(state.token_x))
1199            .unwrap();
1200        // The last three input units still round down to the same gross output.
1201        assert_eq!(input, BigUint::from(4_000_003u64));
1202        assert_eq!(output, BigUint::from(999_000u64));
1203    }
1204
1205    #[test]
1206    fn limits_recover_minimum_trade_for_empty_and_small_input_reserves() {
1207        for x_to_y in [true, false] {
1208            // Reserve 1 starts below the minimum; reserve 4 halves 8 -> 4 -> 2,
1209            // skipping the only valid input (3). Reserve 3 already finds it.
1210            for reserve_in in [0, 1, 3, 4] {
1211                let state = rounding_boundary_state(x_to_y, reserve_in);
1212                let (sell, buy) = if x_to_y {
1213                    (state.token_x, state.token_y)
1214                } else {
1215                    (state.token_y, state.token_x)
1216                };
1217                assert_eq!(
1218                    state
1219                        .quote_exact_in(sell, buy, U256::from(3u64))
1220                        .unwrap()
1221                        .0,
1222                    U256::from(1u64)
1223                );
1224                assert_eq!(
1225                    state
1226                        .get_limits(Bytes::from(sell), Bytes::from(buy))
1227                        .unwrap(),
1228                    (BigUint::from(3u64), BigUint::from(1u64)),
1229                    "x_to_y={x_to_y}, reserve_in={reserve_in}"
1230                );
1231            }
1232        }
1233    }
1234
1235    fn rounding_boundary_state(x_to_y: bool, reserve_in: u128) -> LunarBaseState {
1236        let mut state = state();
1237        let q96 = U256::from(1u64) << 96usize;
1238        if x_to_y {
1239            state.anchor_price_x96 = q96 * U256::from(3u64) / U256::from(4u64);
1240            state.reserve_x = reserve_in;
1241            state.reserve_y = 1;
1242        } else {
1243            state.anchor_price_x96 = q96 * U256::from(4u64) / U256::from(3u64);
1244            state.reserve_x = 1;
1245            state.reserve_y = reserve_in;
1246        }
1247        state
1248    }
1249
1250    #[test]
1251    fn minimum_trade_limits_apply_fees_and_punishment() {
1252        for x_to_y in [true, false] {
1253            for (fee, punishment, available) in
1254                [(1 << 22, 1 << 22, true), (MAX_U24, 0, false), (MAX_U24 - 1, MAX_U24, false)]
1255            {
1256                let mut state = rounding_boundary_state(x_to_y, 1);
1257                state.fee_ask_x24 = fee;
1258                state.fee_bid_x24 = fee;
1259                state.max_punishment_x24 = punishment;
1260                let (sell, buy) = if x_to_y {
1261                    (state.token_x, state.token_y)
1262                } else {
1263                    (state.token_y, state.token_x)
1264                };
1265                let expected = if available {
1266                    (BigUint::from(3u64), BigUint::from(1u64))
1267                } else {
1268                    (BigUint::ZERO, BigUint::ZERO)
1269                };
1270                assert_eq!(
1271                    state
1272                        .get_limits(Bytes::from(sell), Bytes::from(buy))
1273                        .unwrap(),
1274                    expected
1275                );
1276            }
1277        }
1278    }
1279
1280    #[test]
1281    fn minimum_trade_limits_preserve_unavailable_states_and_math_errors() {
1282        for x_to_y in [true, false] {
1283            for case in [
1284                "paused",
1285                "stale",
1286                "no liquidity",
1287                "reserve overflow",
1288                "zero anchor",
1289                "minimum exceeds capacity",
1290                "output quantum exceeds liquidity",
1291                "invalid math",
1292            ] {
1293                let mut state = rounding_boundary_state(x_to_y, 1);
1294                let (sell, buy) = if x_to_y {
1295                    (state.token_x, state.token_y)
1296                } else {
1297                    (state.token_y, state.token_x)
1298                };
1299                match case {
1300                    "paused" => state.paused = true,
1301                    "stale" => state.head_block = 102,
1302                    "no liquidity" if x_to_y => state.reserve_y = 0,
1303                    "no liquidity" => state.reserve_x = 0,
1304                    "reserve overflow" if x_to_y => state.reserve_x = MAX_U112 - 2,
1305                    "reserve overflow" => state.reserve_y = MAX_U112 - 2,
1306                    "zero anchor" => state.anchor_price_x96 = U256::ZERO,
1307                    "minimum exceeds capacity" => {
1308                        state.anchor_price_x96 =
1309                            if x_to_y { U256::from(1u64) } else { U256::from(1u64) << 159usize };
1310                    }
1311                    "output quantum exceeds liquidity" => {
1312                        state.anchor_price_x96 =
1313                            U256::from(1u64) << if x_to_y { 97usize } else { 95usize };
1314                    }
1315                    "invalid math" => state.max_punishment_x24 = 1 << 24,
1316                    _ => unreachable!(),
1317                }
1318                let limits = state.get_limits(Bytes::from(sell), Bytes::from(buy));
1319                if case == "invalid math" {
1320                    assert!(limits.is_err(), "x_to_y={x_to_y}, {case}");
1321                } else {
1322                    assert_eq!(
1323                        limits.unwrap(),
1324                        (BigUint::ZERO, BigUint::ZERO),
1325                        "x_to_y={x_to_y}, {case}"
1326                    );
1327                }
1328            }
1329        }
1330    }
1331
1332    #[test]
1333    fn empty_input_reserve_limit_inverts_both_rounding_steps() {
1334        for x_to_y in [true, false] {
1335            let mut state = state();
1336            let (sell, buy) = if x_to_y {
1337                state.anchor_price_x96 = (U256::from(3u64) << 96usize) / U256::from(4u64);
1338                state.reserve_x = 0;
1339                state.reserve_y = 1;
1340                (state.token_x, state.token_y)
1341            } else {
1342                state.anchor_price_x96 = (U256::from(3u64) << 96usize) / U256::from(2u64);
1343                state.reserve_x = 1;
1344                state.reserve_y = 0;
1345                (state.token_y, state.token_x)
1346            };
1347            assert_eq!(
1348                state
1349                    .quote_exact_in(sell, buy, U256::from(3u64))
1350                    .unwrap()
1351                    .0,
1352                U256::from(1u64)
1353            );
1354            let (input, output) = state
1355                .get_limits(Bytes::from(sell), Bytes::from(buy))
1356                .unwrap();
1357            assert_eq!(input, BigUint::from(if x_to_y { 3u64 } else { 4u64 }));
1358            assert_eq!(output, BigUint::from(1u64));
1359            assert_eq!(
1360                state.quote_exact_in(sell, buy, biguint_to_u256(&(input + 1u32)).unwrap()),
1361                Err(QuoteError::Rejected)
1362            );
1363        }
1364    }
1365
1366    #[test]
1367    fn empty_input_reserve_limits_match_exhaustive_small_quotes() {
1368        for anchor_quarters in 1..=16u64 {
1369            for reserve_out in 1..=8u128 {
1370                for x_to_y in [true, false] {
1371                    let mut state = state();
1372                    state.anchor_price_x96 = U256::from(anchor_quarters) << 94usize;
1373                    let (sell, buy) = if x_to_y {
1374                        state.reserve_x = 0;
1375                        state.reserve_y = reserve_out;
1376                        (state.token_x, state.token_y)
1377                    } else {
1378                        state.reserve_x = reserve_out;
1379                        state.reserve_y = 0;
1380                        (state.token_y, state.token_x)
1381                    };
1382                    // These prices/reserves exhaust output liquidity below 256 input units.
1383                    let expected = (1..=256u64)
1384                        .rev()
1385                        .find_map(|input| {
1386                            state
1387                                .quote_exact_in(sell, buy, U256::from(input))
1388                                .ok()
1389                                .map(|(output, _)| (BigUint::from(input), u256_to_biguint(output)))
1390                        })
1391                        .unwrap_or((BigUint::ZERO, BigUint::ZERO));
1392                    assert_eq!(
1393                        state.get_limits(Bytes::from(sell), Bytes::from(buy)).unwrap(),
1394                        expected,
1395                        "anchor_quarters={anchor_quarters}, reserve_out={reserve_out}, x_to_y={x_to_y}"
1396                    );
1397                }
1398            }
1399        }
1400    }
1401
1402    #[test]
1403    fn empty_input_reserve_limit_respects_uint112_capacity() {
1404        let mut state = state();
1405        state.anchor_price_x96 = U256::from(2u64) << 96usize;
1406        state.reserve_x = MAX_U112;
1407        state.reserve_y = 0;
1408        let (input, output) = state
1409            .get_limits(Bytes::from(state.token_y), Bytes::from(state.token_x))
1410            .unwrap();
1411        assert_eq!(input, BigUint::from(MAX_U112));
1412        assert_eq!(output, BigUint::from(MAX_U112 / 4));
1413    }
1414
1415    #[test]
1416    fn legacy_caller_policy_does_not_change_partner_quotes() {
1417        let mut state = state();
1418        state.max_punishment_x24 = 16_778;
1419        state.fee_bid_x24 = (1 << 24) / 4;
1420        let x = token(state.token_x, "X", 18);
1421        let y = token(state.token_y, "Y", 18);
1422        let amount = U256::from(100_000u64);
1423        let expected = state
1424            .quote_exact_in(state.token_x, state.token_y, amount)
1425            .unwrap();
1426        assert_eq!(expected.0, U256::from(74_995u64));
1427        assert_eq!(state.spot_price(&x, &y).unwrap(), 0.75);
1428
1429        apply_delta(
1430            &mut state,
1431            HashMap::from([
1432                ("quote_caller_whitelisted".to_owned(), Bytes::from([0u8])),
1433                ("blacklist_fee_multiplier".to_owned(), Bytes::from(U256::MAX.to_be_bytes::<32>())),
1434            ]),
1435        )
1436        .unwrap();
1437        assert_eq!(
1438            state
1439                .quote_exact_in(state.token_x, state.token_y, amount)
1440                .unwrap(),
1441            expected
1442        );
1443        assert_eq!(state.spot_price(&x, &y).unwrap(), 0.75);
1444    }
1445
1446    #[test]
1447    fn matches_base_partner_quotes_from_execution_fixture() {
1448        let capture: serde_json::Value = serde_json::from_str(include_str!(concat!(
1449            env!("CARGO_MANIFEST_DIR"),
1450            "/../../protocols/substreams/base-lunarbase/bootstrap/base-execution.json"
1451        )))
1452        .unwrap();
1453        let mut state = state();
1454        let address = |name: &str| {
1455            address_from_bytes(
1456                &hex::decode(
1457                    capture[name]
1458                        .as_str()
1459                        .unwrap()
1460                        .trim_start_matches("0x"),
1461                )
1462                .unwrap(),
1463            )
1464            .unwrap()
1465        };
1466        state.pool = address("pool");
1467        state.token_x = address("token_x");
1468        state.token_y = address("token_y");
1469        let snapshot = &capture["execution_state"];
1470        apply_delta(
1471            &mut state,
1472            snapshot["attributes"]
1473                .as_object()
1474                .unwrap()
1475                .iter()
1476                .map(|(name, value)| {
1477                    (
1478                        name.clone(),
1479                        value
1480                            .as_str()
1481                            .unwrap()
1482                            .parse::<Bytes>()
1483                            .unwrap(),
1484                    )
1485                })
1486                .collect(),
1487        )
1488        .unwrap();
1489        state.head_block = snapshot["block_number"]
1490            .as_u64()
1491            .unwrap();
1492        let mut directions = [0; 2];
1493        for quote in capture["observed_quotes"]
1494            .as_array()
1495            .unwrap()
1496        {
1497            let forward = match quote["signature"].as_str().unwrap() {
1498                "quoteXToY(uint256)" => true,
1499                "quoteYToX(uint256)" => false,
1500                other => panic!("unexpected quote: {other}"),
1501            };
1502            directions[usize::from(forward)] += 1;
1503            let amount = U256::from_str_radix(quote["arguments"][0].as_str().unwrap(), 10).unwrap();
1504            let expected: Vec<U256> = quote["result"]
1505                .as_array()
1506                .unwrap()
1507                .iter()
1508                .map(|value| U256::from_str_radix(value.as_str().unwrap(), 10).unwrap())
1509                .collect();
1510            let (token_in, token_out) = if forward {
1511                (state.token_x, state.token_y)
1512            } else {
1513                (state.token_y, state.token_x)
1514            };
1515            let (output, next) = state
1516                .quote_exact_in(token_in, token_out, amount)
1517                .unwrap();
1518            assert!(output > U256::ZERO);
1519            assert_eq!(output, expected[0]);
1520            assert_eq!(next.anchor_price_x96, expected[1]);
1521            let gross = if forward {
1522                state.reserve_y - next.reserve_y
1523            } else {
1524                state.reserve_x - next.reserve_x
1525            };
1526            assert_eq!(U256::from(gross) - output, expected[2]);
1527        }
1528        assert_eq!(directions, [3, 3]);
1529    }
1530
1531    #[test]
1532    fn derives_partner_quotes_from_base_capture_at_block_51297469() {
1533        // Recorded eth_call results at Base block 51,297,469, pool
1534        // 0x0000efc4ec03a7c47d3a38a9be7ff1d52dd01b99, with `from` set to
1535        // TychoRouter 0xAbA5B53b03eAfaD1C5fc8BD5Fc765fC85Bb3de67.
1536        // The historical caller paid multiplier 100 at this block.
1537        // Normalize the recorded fee (floor(gross * fee_x24 / Q24) * 100) to
1538        // multiplier one. This checks partner math, not historical caller parity.
1539        let mut state = state();
1540        state.pool =
1541            address_from_bytes(&hex::decode("0000efc4ec03a7c47d3a38a9be7ff1d52dd01b99").unwrap())
1542                .unwrap();
1543        state.token_x = [0; 20];
1544        state.token_y =
1545            address_from_bytes(&hex::decode("833589fcd6edb6e08f4c7c32d4f71b54bda02913").unwrap())
1546                .unwrap();
1547        state.anchor_price_x96 = U256::from(3_971_029_714_873_270_134_636_544u128);
1548        state.fee_ask_x24 = 1_835;
1549        state.fee_bid_x24 = 8_475;
1550        state.latest_update_block = 51_297_469;
1551        state.head_block = 51_297_469;
1552        state.max_punishment_x24 = 160_000;
1553        state.block_delay = 3;
1554        state.reserve_x = 9_683_337_552_179_635_497;
1555        state.reserve_y = 23_231_346_797;
1556
1557        // Direction, input, observed net output, observed output-token fee.
1558        let quotes = [
1559            (true, 1_000_000_000_000_000u128, 2_385_158u128, 127_000u128),
1560            (true, 100_000_000_000_000_000, 237_258_990, 13_956_900),
1561            (true, 1_000_000_000_000_000_000, 2_258_700_200, 253_458_700),
1562            (false, 1_000_000, 393_700_695_234_017, 4_363_296_513_600),
1563            (false, 100_000_000, 39_291_060_402_299_481, 515_338_772_582_000),
1564            (false, 1_000_000_000, 385_726_231_079_417_225, 12_337_760_669_557_200),
1565        ];
1566        for (x_to_y, input, observed_out, observed_fee) in quotes {
1567            assert_eq!(observed_fee % 100, 0);
1568            let expected_fee = observed_fee / 100;
1569            let expected_out = observed_out + observed_fee - expected_fee;
1570            let (token_in, token_out) = if x_to_y {
1571                (state.token_x, state.token_y)
1572            } else {
1573                (state.token_y, state.token_x)
1574            };
1575            let (out, next) = state
1576                .quote_exact_in(token_in, token_out, U256::from(input))
1577                .unwrap();
1578            assert_eq!(out, U256::from(expected_out), "direction={x_to_y}, input={input}");
1579            let gross = if x_to_y {
1580                state.reserve_y - next.reserve_y
1581            } else {
1582                state.reserve_x - next.reserve_x
1583            };
1584            assert_eq!(gross - expected_out, expected_fee);
1585            assert_eq!(next.anchor_price_x96, state.anchor_price_x96);
1586        }
1587    }
1588
1589    #[test]
1590    fn matches_bsc_router_quotes_observed_at_block_121835898() {
1591        // Recorded eth_call results at BSC block 121,835,898, pool
1592        // 0x00007904d186680c709519e71f4dc3e2df8f1b99, with `from` set to
1593        // 0x7f3d12bbafb8955e51b3ab9588b34c8ad95bda4e. Both tokens use 18
1594        // decimals; the observed caller multiplier was 1.
1595        let mut state = state();
1596        state.pool =
1597            address_from_bytes(&hex::decode("00007904d186680c709519e71f4dc3e2df8f1b99").unwrap())
1598                .unwrap();
1599        state.token_x = [0; 20];
1600        state.token_y =
1601            address_from_bytes(&hex::decode("55d398326f99059ff775485246999027b3197955").unwrap())
1602                .unwrap();
1603        state.anchor_price_x96 = U256::from(2_129_209_909_442_387_799_622_216_056_832u128);
1604        state.fee_ask_x24 = 3_092;
1605        state.fee_bid_x24 = 552;
1606        state.latest_update_block = 121_835_890;
1607        state.head_block = 121_835_898;
1608        state.max_punishment_x24 = 125_000;
1609        state.block_delay = 25;
1610        state.reserve_x = 40_982_882_360_761_506_608;
1611        state.reserve_y = 37_489_098_599_819_531_485_143;
1612
1613        // Direction, input, observed net output, observed output-token fee.
1614        let quotes: [(bool, u128, u128, u128); 6] = [
1615            (true, 1_000_000_000_000_000, 722_209_919_138_098_675, 23_848_861_901_283),
1616            (true, 100_000_000_000_000_000, 72_220_419_368_930_290_024, 2_957_431_069_707_870),
1617            (true, 1_000_000_000_000_000_000, 722_152_061_971_320_131_445, 81_706_028_679_847_714),
1618            (false, 1_000_000_000_000_000_000, 1_384_337_907_652_450, 255_342_216_199),
1619            (false, 100_000_000_000_000_000_000, 138_432_263_993_751_885, 27_060_993_113_036),
1620            (false, 1_000_000_000_000_000_000_000, 1_384_184_240_164_873_034, 409_009_703_776_182),
1621        ];
1622        for (x_to_y, input, expected_out, expected_fee) in quotes {
1623            let (token_in, token_out) = if x_to_y {
1624                (state.token_x, state.token_y)
1625            } else {
1626                (state.token_y, state.token_x)
1627            };
1628            let (out, next) = state
1629                .quote_exact_in(token_in, token_out, U256::from(input))
1630                .unwrap();
1631            assert_eq!(out, U256::from(expected_out), "direction={x_to_y}, input={input}");
1632            let gross = if x_to_y {
1633                state.reserve_y - next.reserve_y
1634            } else {
1635                state.reserve_x - next.reserve_x
1636            };
1637            assert_eq!(gross - expected_out, expected_fee);
1638            assert_eq!(next.anchor_price_x96, state.anchor_price_x96);
1639        }
1640    }
1641}