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 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 LunarBaseTychoState {
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 LunarBaseTychoState {
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 LunarBaseTychoState {
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 spot_from_reserves(self.reserve_x, self.reserve_y, base, quote);
142 }
143 if token_in == self.token_y && token_out == self.token_x {
144 return spot_from_reserves(self.reserve_y, self.reserve_x, base, quote);
145 }
146 Err(SimulationError::InvalidInput("invalid LunarBase token pair".to_owned(), None))
147 }
148
149 fn get_amount_out(
150 &self,
151 amount_in: BigUint,
152 token_in: &Token,
153 token_out: &Token,
154 ) -> Result<GetAmountOutResult, SimulationError> {
155 let (amount_out, next_state) = self
156 .quote_exact_in(
157 address_from_bytes(token_in.address.as_ref())?,
158 address_from_bytes(token_out.address.as_ref())?,
159 biguint_to_u256(&amount_in)?,
160 )
161 .map_err(map_quote_error)?;
162
163 Ok(GetAmountOutResult::new(
164 u256_to_biguint(amount_out),
165 BigUint::from(DEFAULT_GAS),
166 Box::new(next_state),
167 ))
168 }
169
170 fn get_limits(
171 &self,
172 sell_token: Bytes,
173 buy_token: Bytes,
174 ) -> Result<(BigUint, BigUint), SimulationError> {
175 let sell = address_from_bytes(sell_token.as_ref())?;
176 let buy = address_from_bytes(buy_token.as_ref())?;
177 if sell == self.token_x && buy == self.token_y {
178 return quote_limit(self, sell, buy, soft_limit(self.reserve_x));
179 }
180 if sell == self.token_y && buy == self.token_x {
181 return quote_limit(self, sell, buy, soft_limit(self.reserve_y));
182 }
183 Err(SimulationError::InvalidInput("invalid LunarBase token pair".to_owned(), None))
184 }
185
186 fn delta_transition(
187 &mut self,
188 delta: ProtocolStateDelta,
189 _tokens: &HashMap<Bytes, Token>,
190 _balances: &Balances,
191 ) -> Result<(), TransitionError> {
192 if let Some(name) = delta.deleted_attributes.iter().next() {
193 return Err(TransitionError::DecodeError(format!(
194 "LunarBase does not support deleted attributes: {name}"
195 )));
196 }
197
198 let head_block = delta
199 .updated_attributes
200 .get("block_number")
201 .map(|value| u64::from(value.clone()));
202
203 let updated_attributes = delta
204 .updated_attributes
205 .into_iter()
206 .filter(|(key, _)| key != "block_number" && key != "block_timestamp")
207 .collect();
208 apply_delta(self, updated_attributes)
209 .map_err(|err| TransitionError::DecodeError(format!("{err:?}")))?;
210 if let Some(head_block) = head_block {
211 self.head_block = head_block;
212 }
213 Ok(())
214 }
215
216 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
217 crate::evm::query_pool_swap::query_pool_swap(self, params)
218 }
219
220 fn clone_box(&self) -> Box<dyn ProtocolSim> {
221 Box::new(self.clone())
222 }
223
224 fn as_any(&self) -> &dyn Any {
225 self
226 }
227
228 fn as_any_mut(&mut self) -> &mut dyn Any {
229 self
230 }
231
232 fn eq(&self, other: &dyn ProtocolSim) -> bool {
233 other.as_any().downcast_ref::<Self>() == Some(self)
234 }
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
238enum QuoteError {
239 Paused,
240 Stale { block_number: u64, latest_update_block: u64, block_delay: u64 },
241 InvalidTokenPair,
242 Rejected,
243 ReserveOverflow,
244 ReserveUnderflow,
245}
246
247fn u256_to_u128(value: U256) -> Result<u128, QuoteError> {
248 if value.bit_len() > 128 {
249 return Err(QuoteError::ReserveOverflow);
250 }
251 let limbs = value.as_limbs();
252 Ok(((limbs[1] as u128) << 64) | limbs[0] as u128)
253}
254
255fn spot_from_reserves(
256 reserve_in: u128,
257 reserve_out: u128,
258 token_in: &Token,
259 token_out: &Token,
260) -> Result<f64, SimulationError> {
261 if reserve_in == 0 || reserve_out == 0 {
262 return Err(SimulationError::RecoverableError("zero LunarBase reserve".to_owned()));
263 }
264 let decimals_adjustment = 10f64.powi(token_in.decimals as i32 - token_out.decimals as i32);
265 Ok((reserve_out as f64 / reserve_in as f64) * decimals_adjustment)
266}
267
268fn soft_limit(reserve_in: u128) -> BigUint {
277 BigUint::from(reserve_in) * 2162u32 / 1000u32
278}
279
280fn quote_limit(
281 state: &LunarBaseTychoState,
282 token_in: Address,
283 token_out: Address,
284 mut amount_in: BigUint,
285) -> Result<(BigUint, BigUint), SimulationError> {
286 if amount_in == BigUint::ZERO {
287 return Ok((BigUint::ZERO, BigUint::ZERO));
288 }
289
290 loop {
291 match state.quote_exact_in(token_in, token_out, biguint_to_u256(&amount_in)?) {
292 Ok((amount_out, _)) => return Ok((amount_in, u256_to_biguint(amount_out))),
293 Err(
294 QuoteError::Rejected | QuoteError::ReserveOverflow | QuoteError::ReserveUnderflow,
295 ) => {
296 amount_in >>= 1;
297 if amount_in == BigUint::ZERO {
298 return Ok((BigUint::ZERO, BigUint::ZERO));
299 }
300 }
301 Err(err) => return Err(map_quote_error(err)),
302 }
303 }
304}
305
306fn address_from_bytes(value: &[u8]) -> Result<Address, SimulationError> {
307 value.try_into().map_err(|_| {
308 SimulationError::InvalidInput(
309 format!("expected 20-byte address, got {}", value.len()),
310 None,
311 )
312 })
313}
314
315fn biguint_to_u256(value: &BigUint) -> Result<U256, SimulationError> {
316 let bytes = value.to_bytes_be();
317 if bytes.len() > 32 {
318 return Err(SimulationError::InvalidInput("amount_in exceeds uint256".to_owned(), None));
319 }
320 Ok(U256::from_be_slice(&bytes))
321}
322
323fn u256_to_biguint(value: U256) -> BigUint {
324 BigUint::from_bytes_be(&value.to_be_bytes::<32>())
325}
326
327fn map_quote_error(err: QuoteError) -> SimulationError {
328 SimulationError::InvalidInput(format!("LunarBase quote rejected: {err:?}"), None)
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 fn addr(byte: u8) -> [u8; 20] {
336 [byte; 20]
337 }
338
339 fn state() -> LunarBaseTychoState {
340 LunarBaseTychoState {
341 pool: addr(9),
342 token_x: addr(1),
343 token_y: addr(2),
344 anchor_price_x96: 1u128 << 96,
345 fee_ask_x24: 0,
346 fee_bid_x24: 0,
347 latest_update_block: 100,
348 reserve_x: 1_000_000,
349 reserve_y: 1_000_000,
350 concentration_k: 0,
351 block_delay: 2,
352 paused: false,
353 head_block: 100,
354 }
355 }
356
357 #[test]
358 fn quotes_x_to_y_and_transitions_reserves() {
359 let state = state();
360 let (amount_out, next_state) = state
361 .quote_exact_in(state.token_x, state.token_y, U256::from(1_000u64))
362 .unwrap();
363
364 assert_eq!(amount_out, U256::from(1_000u64));
365 assert_eq!(next_state.reserve_x, 1_001_000);
366 assert_eq!(next_state.reserve_y, 999_000);
367 assert_eq!(next_state.anchor_price_x96, state.anchor_price_x96);
368 assert_eq!(next_state.head_block, state.head_block);
369 }
370
371 #[test]
372 fn rejects_stale_state() {
373 let mut state = state();
374 state.head_block = 102;
375
376 let err = state
377 .quote_exact_in(state.token_x, state.token_y, U256::from(1_000u64))
378 .unwrap_err();
379
380 assert_eq!(
381 err,
382 QuoteError::Stale { block_number: 102, latest_update_block: 100, block_delay: 2 }
383 );
384 }
385}