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];
24const DEFAULT_GAS: u64 = 100_000;
26
27#[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 &amount_out * &price.denominator >= &maximum * &price.numerator
276 } else {
277 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 return Ok(PoolSwap::new(maximum, amount_out, Box::new(next), None));
292 }
293 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 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
357fn 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 let intermediate = ((BigUint::from(reserve_out) + 1u32) * &numerator - 1u32) / &denominator;
375 let maximum = ((intermediate + 1u32) * &numerator - 1u32) / &denominator;
376 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 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 let params = QueryPoolSwapParams::new(
585 token(sell, "IN", 18),
586 token(buy, "OUT", 6),
587 constraint,
588 );
589 let swap = state.query_pool_swap(¶ms).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(¶ms).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(¶ms).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(¶ms)
666 .unwrap()
667 .amount_in(),
668 &BigUint::from(expected)
669 );
670 state.paused = true;
671 assert_eq!(
672 state
673 .query_pool_swap(¶ms)
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(¶ms)
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(¶ms).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 assert!(matches!(
759 state.query_pool_swap(¶ms),
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 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 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 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 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 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 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 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 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 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}