1use std::{any::Any, collections::HashMap};
2
3use lunarbase_pmm_math::{
4 curve_pmm::{quote_x_to_y_with_multiplier, quote_y_to_x_with_multiplier},
5 sqrt_price_x96_to_price, PoolParams, 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::{Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams},
14 },
15 Bytes,
16};
17
18use super::decoder::apply_delta;
19
20pub type Address = [u8; 20];
21const DEFAULT_GAS: u64 = 180_000;
22
23#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
24pub struct LunarBaseState {
25 pub pool: Address,
26 pub token_x: Address,
27 pub token_y: Address,
28 pub anchor_price_x96: u128,
29 pub fee_ask_x24: u32,
30 pub fee_bid_x24: u32,
31 pub latest_update_block: u64,
32 pub reserve_x: u128,
33 pub reserve_y: u128,
34 pub concentration_k: u32,
35 pub block_delay: u64,
36 pub paused: bool,
37 pub head_block: u64,
38}
39
40impl LunarBaseState {
41 pub fn pool_params(&self) -> PoolParams {
42 PoolParams {
43 sqrt_price_x96: self.anchor_price_x96,
44 fee_ask_x24: self.fee_ask_x24,
45 fee_bid_x24: self.fee_bid_x24,
46 reserve_x: self.reserve_x,
47 reserve_y: self.reserve_y,
48 concentration_k: self.concentration_k,
49 }
50 }
51
52 pub fn is_fresh(&self) -> bool {
53 self.head_block <
54 self.latest_update_block
55 .saturating_add(self.block_delay)
56 }
57
58 fn quote_exact_in(
59 &self,
60 token_in: Address,
61 token_out: Address,
62 amount_in: U256,
63 ) -> Result<(U256, Self), QuoteError> {
64 if self.paused {
65 return Err(QuoteError::Paused);
66 }
67
68 if !self.is_fresh() {
69 return Err(QuoteError::Stale {
70 block_number: self.head_block,
71 latest_update_block: self.latest_update_block,
72 block_delay: self.block_delay,
73 });
74 }
75
76 let params = self.pool_params();
77 if token_in == self.token_x && token_out == self.token_y {
78 let math_result = quote_x_to_y_with_multiplier(¶ms, amount_in, U256::from(1u64));
79 if math_result.amount_out.is_zero() {
80 return Err(QuoteError::Rejected);
81 }
82
83 let input = u256_to_u128(amount_in)?;
84 let gross_output = u256_to_u128(
85 math_result
86 .amount_out
87 .checked_add(math_result.fee)
88 .ok_or(QuoteError::ReserveOverflow)?,
89 )?;
90 let mut next = self.clone();
91 next.reserve_x = next
92 .reserve_x
93 .checked_add(input)
94 .ok_or(QuoteError::ReserveOverflow)?;
95 next.reserve_y = next
96 .reserve_y
97 .checked_sub(gross_output)
98 .ok_or(QuoteError::ReserveUnderflow)?;
99 return Ok((math_result.amount_out, next));
100 }
101
102 if token_in == self.token_y && token_out == self.token_x {
103 let math_result = quote_y_to_x_with_multiplier(¶ms, amount_in, U256::from(1u64));
104 if math_result.amount_out.is_zero() {
105 return Err(QuoteError::Rejected);
106 }
107
108 let input = u256_to_u128(amount_in)?;
109 let gross_output = u256_to_u128(
110 math_result
111 .amount_out
112 .checked_add(math_result.fee)
113 .ok_or(QuoteError::ReserveOverflow)?,
114 )?;
115 let mut next = self.clone();
116 next.reserve_y = next
117 .reserve_y
118 .checked_add(input)
119 .ok_or(QuoteError::ReserveOverflow)?;
120 next.reserve_x = next
121 .reserve_x
122 .checked_sub(gross_output)
123 .ok_or(QuoteError::ReserveUnderflow)?;
124 return Ok((math_result.amount_out, next));
125 }
126
127 Err(QuoteError::InvalidTokenPair)
128 }
129}
130
131#[typetag::serde]
132impl ProtocolSim for LunarBaseState {
133 fn fee(&self) -> f64 {
134 0.0
135 }
136
137 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
138 let token_in = address_from_bytes(base.address.as_ref())?;
139 let token_out = address_from_bytes(quote.address.as_ref())?;
140 if token_in == self.token_x && token_out == self.token_y {
141 return Ok(apply_fee_discount(
142 anchor_price(self.anchor_price_x96, base, quote),
143 self.fee_bid_x24,
144 ));
145 }
146 if token_in == self.token_y && token_out == self.token_x {
147 return Ok(apply_fee_discount(
148 1.0 / anchor_price(self.anchor_price_x96, quote, base),
149 self.fee_ask_x24,
150 ));
151 }
152 Err(SimulationError::InvalidInput("invalid LunarBase token pair".to_owned(), None))
153 }
154
155 fn get_amount_out(
156 &self,
157 amount_in: BigUint,
158 token_in: &Token,
159 token_out: &Token,
160 ) -> Result<GetAmountOutResult, SimulationError> {
161 if amount_in == BigUint::ZERO {
162 return Ok(GetAmountOutResult::new(
163 BigUint::ZERO,
164 BigUint::from(DEFAULT_GAS),
165 Box::new(self.clone()),
166 ));
167 }
168
169 let (amount_out, next_state) = self
170 .quote_exact_in(
171 address_from_bytes(token_in.address.as_ref())?,
172 address_from_bytes(token_out.address.as_ref())?,
173 biguint_to_u256(&amount_in)?,
174 )
175 .map_err(map_quote_error)?;
176
177 Ok(GetAmountOutResult::new(
178 u256_to_biguint(amount_out),
179 BigUint::from(DEFAULT_GAS),
180 Box::new(next_state),
181 ))
182 }
183
184 fn get_limits(
185 &self,
186 sell_token: Bytes,
187 buy_token: Bytes,
188 ) -> Result<(BigUint, BigUint), SimulationError> {
189 let sell = address_from_bytes(sell_token.as_ref())?;
190 let buy = address_from_bytes(buy_token.as_ref())?;
191 if sell == self.token_x && buy == self.token_y {
192 return quote_limit(self, sell, buy, soft_limit(self.reserve_x));
193 }
194 if sell == self.token_y && buy == self.token_x {
195 return quote_limit(self, sell, buy, soft_limit(self.reserve_y));
196 }
197 Err(SimulationError::InvalidInput("invalid LunarBase token pair".to_owned(), None))
198 }
199
200 fn delta_transition(
201 &mut self,
202 delta: ProtocolStateDelta,
203 _tokens: &HashMap<Bytes, Token>,
204 _balances: &Balances,
205 ) -> Result<(), TransitionError> {
206 if let Some(name) = delta.deleted_attributes.iter().next() {
207 return Err(TransitionError::DecodeError(format!(
208 "LunarBase does not support deleted attributes: {name}"
209 )));
210 }
211
212 let head_block = delta
213 .updated_attributes
214 .get("block_number")
215 .map(|value| u64::from(value.clone()));
216
217 let updated_attributes = delta
218 .updated_attributes
219 .into_iter()
220 .filter(|(key, _)| key != "block_number" && key != "block_timestamp")
221 .collect();
222 apply_delta(self, updated_attributes)
223 .map_err(|err| TransitionError::DecodeError(format!("{err:?}")))?;
224 if let Some(head_block) = head_block {
225 self.head_block = head_block;
226 }
227 Ok(())
228 }
229
230 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
231 crate::evm::query_pool_swap::query_pool_swap(self, params)
232 }
233
234 fn clone_box(&self) -> Box<dyn ProtocolSim> {
235 Box::new(self.clone())
236 }
237
238 fn as_any(&self) -> &dyn Any {
239 self
240 }
241
242 fn as_any_mut(&mut self) -> &mut dyn Any {
243 self
244 }
245
246 fn eq(&self, other: &dyn ProtocolSim) -> bool {
247 other.as_any().downcast_ref::<Self>() == Some(self)
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq)]
252enum QuoteError {
253 Paused,
254 Stale { block_number: u64, latest_update_block: u64, block_delay: u64 },
255 InvalidTokenPair,
256 Rejected,
257 ReserveOverflow,
258 ReserveUnderflow,
259}
260
261fn u256_to_u128(value: U256) -> Result<u128, QuoteError> {
262 if value.bit_len() > 128 {
263 return Err(QuoteError::ReserveOverflow);
264 }
265 let limbs = value.as_limbs();
266 Ok(((limbs[1] as u128) << 64) | limbs[0] as u128)
267}
268
269fn anchor_price(anchor_price_x96: u128, token_in: &Token, token_out: &Token) -> f64 {
270 let decimals_adjustment = 10f64.powi(token_in.decimals as i32 - token_out.decimals as i32);
271 sqrt_price_x96_to_price(anchor_price_x96) * decimals_adjustment
272}
273
274fn apply_fee_discount(price: f64, fee_x24: u32) -> f64 {
275 price * (1.0 - fee_x24 as f64 / (1u64 << 24) as f64)
276}
277
278fn soft_limit(reserve_in: u128) -> BigUint {
287 BigUint::from(reserve_in) * 2162u32 / 1000u32
288}
289
290fn quote_limit(
291 state: &LunarBaseState,
292 token_in: Address,
293 token_out: Address,
294 mut amount_in: BigUint,
295) -> Result<(BigUint, BigUint), SimulationError> {
296 if amount_in == BigUint::ZERO {
297 return Ok((BigUint::ZERO, BigUint::ZERO));
298 }
299
300 loop {
301 match state.quote_exact_in(token_in, token_out, biguint_to_u256(&amount_in)?) {
302 Ok((amount_out, _)) => return Ok((amount_in, u256_to_biguint(amount_out))),
303 Err(
304 QuoteError::Rejected | QuoteError::ReserveOverflow | QuoteError::ReserveUnderflow,
305 ) => {
306 amount_in >>= 1;
307 if amount_in == BigUint::ZERO {
308 return Ok((BigUint::ZERO, BigUint::ZERO));
309 }
310 }
311 Err(err) => return Err(map_quote_error(err)),
312 }
313 }
314}
315
316fn address_from_bytes(value: &[u8]) -> Result<Address, SimulationError> {
317 value.try_into().map_err(|_| {
318 SimulationError::InvalidInput(
319 format!("expected 20-byte address, got {}", value.len()),
320 None,
321 )
322 })
323}
324
325fn biguint_to_u256(value: &BigUint) -> Result<U256, SimulationError> {
326 let bytes = value.to_bytes_be();
327 if bytes.len() > 32 {
328 return Err(SimulationError::InvalidInput("amount_in exceeds uint256".to_owned(), None));
329 }
330 Ok(U256::from_be_slice(&bytes))
331}
332
333fn u256_to_biguint(value: U256) -> BigUint {
334 BigUint::from_bytes_be(&value.to_be_bytes::<32>())
335}
336
337fn map_quote_error(err: QuoteError) -> SimulationError {
338 SimulationError::InvalidInput(format!("LunarBase quote rejected: {err:?}"), None)
339}
340
341#[cfg(test)]
342mod tests {
343 use tycho_common::models::Chain;
344
345 use super::*;
346
347 fn addr(byte: u8) -> [u8; 20] {
348 [byte; 20]
349 }
350
351 fn token(address: Address, symbol: &str, decimals: u32) -> Token {
352 Token::new(
353 &Bytes::from(address.to_vec()),
354 symbol,
355 decimals,
356 100,
357 &[Some(100_000)],
358 Chain::Base,
359 100,
360 )
361 }
362
363 fn state() -> LunarBaseState {
364 LunarBaseState {
365 pool: addr(9),
366 token_x: addr(1),
367 token_y: addr(2),
368 anchor_price_x96: 1u128 << 96,
369 fee_ask_x24: 0,
370 fee_bid_x24: 0,
371 latest_update_block: 100,
372 reserve_x: 1_000_000,
373 reserve_y: 1_000_000,
374 concentration_k: 0,
375 block_delay: 2,
376 paused: false,
377 head_block: 100,
378 }
379 }
380
381 #[test]
382 fn quotes_x_to_y_and_transitions_reserves() {
383 let state = state();
384 let (amount_out, next_state) = state
385 .quote_exact_in(state.token_x, state.token_y, U256::from(1_000u64))
386 .unwrap();
387
388 assert_eq!(amount_out, U256::from(1_000u64));
389 assert_eq!(next_state.reserve_x, 1_001_000);
390 assert_eq!(next_state.reserve_y, 999_000);
391 assert_eq!(next_state.anchor_price_x96, state.anchor_price_x96);
392 assert_eq!(next_state.head_block, state.head_block);
393 }
394
395 #[test]
396 fn zero_amount_out_returns_zero_without_transition() {
397 let state = state();
398 let token_x = token(state.token_x, "ETH", 18);
399 let token_y = token(state.token_y, "USDC", 6);
400
401 let quote = state
402 .get_amount_out(BigUint::ZERO, &token_x, &token_y)
403 .unwrap();
404 let next_state = quote
405 .new_state
406 .as_any()
407 .downcast_ref::<LunarBaseState>()
408 .unwrap();
409
410 assert_eq!(quote.amount, BigUint::ZERO);
411 assert_eq!(next_state, &state);
412 }
413
414 #[test]
415 fn spot_price_uses_anchor_price() {
416 let mut state = state();
417 state.anchor_price_x96 = lunarbase_pmm_math::price_to_sqrt_price_x96(2_000.0 / 1e12);
418 state.reserve_x = 1_000_000_000_000_000_000;
419 state.reserve_y = 1_000_000;
420
421 let token_x = token(state.token_x, "ETH", 18);
422 let token_y = token(state.token_y, "USDC", 6);
423
424 let price = state
425 .spot_price(&token_x, &token_y)
426 .unwrap();
427 let inverse = state
428 .spot_price(&token_y, &token_x)
429 .unwrap();
430
431 assert!((price - 2_000.0).abs() < 1e-6);
432 assert!((inverse - 0.0005).abs() < 1e-12);
433 }
434
435 #[test]
436 fn spot_price_applies_directional_base_fee() {
437 let mut state = state();
438 state.anchor_price_x96 = lunarbase_pmm_math::price_to_sqrt_price_x96(2_000.0 / 1e12);
439 state.fee_ask_x24 = (1u32 << 24) / 100;
440 state.fee_bid_x24 = (2 * (1u32 << 24)) / 100;
441
442 let token_x = token(state.token_x, "ETH", 18);
443 let token_y = token(state.token_y, "USDC", 6);
444
445 let price = state
446 .spot_price(&token_x, &token_y)
447 .unwrap();
448 let inverse = state
449 .spot_price(&token_y, &token_x)
450 .unwrap();
451
452 assert!((price - (2_000.0 * 0.98)).abs() < 1e-3);
453 assert!((inverse - (0.0005 * 0.99)).abs() < 1e-9);
454 }
455
456 #[test]
457 fn rejects_stale_state() {
458 let mut state = state();
459 state.head_block = 102;
460
461 let err = state
462 .quote_exact_in(state.token_x, state.token_y, U256::from(1_000u64))
463 .unwrap_err();
464
465 assert_eq!(
466 err,
467 QuoteError::Stale { block_number: 102, latest_update_block: 100, block_delay: 2 }
468 );
469 }
470}