1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::U256;
4use num_bigint::BigUint;
5use num_traits::Zero;
6use serde::{Deserialize, Serialize};
7use tycho_common::{
8 dto::ProtocolStateDelta,
9 models::token::Token,
10 simulation::{
11 errors::{SimulationError, TransitionError},
12 protocol_sim::{
13 Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
14 SwapConstraint,
15 },
16 },
17 Bytes,
18};
19
20use crate::evm::protocol::{
21 cpmm::protocol::{
22 cpmm_fee, cpmm_get_amount_out, cpmm_get_limits, cpmm_spot_price, cpmm_swap_to_price,
23 ProtocolFee,
24 },
25 safe_math::{safe_add_u256, safe_div_u256, safe_mul_u256, safe_sub_u256},
26 u256_num::{biguint_to_u256, u256_to_biguint},
27 utils::add_fee_markup,
28};
29
30const SWAP_BASE_GAS: u64 = 90_000;
33const RING_SWAP_V2_FEE_BPS: u32 = 30;
34const FEE_PRECISION: U256 = U256::from_limbs([10_000, 0, 0, 0]);
35const FEE_NUMERATOR: U256 = U256::from_limbs([9_970, 0, 0, 0]);
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub struct RingSwapV2State {
43 pub component_id: String,
44 pub reserve0: U256,
45 pub reserve1: U256,
46 pub backing0: U256,
47 pub backing1: U256,
48 pub token0: Bytes,
49 pub token1: Bytes,
50}
51
52impl RingSwapV2State {
53 pub fn new(
54 component_id: String,
55 reserve0: U256,
56 reserve1: U256,
57 backing0: U256,
58 backing1: U256,
59 token0: Bytes,
60 token1: Bytes,
61 ) -> Self {
62 Self { component_id, reserve0, reserve1, backing0, backing1, token0, token1 }
63 }
64
65 fn zero_to_one(&self, token_in: &Token, token_out: &Token) -> bool {
66 token_in.address < token_out.address
67 }
68
69 fn output_backing(&self, zero_to_one: bool) -> U256 {
70 if zero_to_one {
71 self.backing1
72 } else {
73 self.backing0
74 }
75 }
76
77 fn protocol_fee() -> ProtocolFee {
78 ProtocolFee::new(FEE_NUMERATOR, FEE_PRECISION)
79 }
80
81 fn max_input_for_backing(
82 reserve_in: U256,
83 reserve_out: U256,
84 output_backing: U256,
85 ) -> Result<U256, SimulationError> {
86 if output_backing >= reserve_out {
87 return Ok(U256::MAX);
88 }
89 let first_unexecutable_output = safe_add_u256(output_backing, U256::from(1))?;
90 if first_unexecutable_output == reserve_out {
91 return Ok(U256::MAX);
92 }
93
94 let numerator =
98 safe_mul_u256(safe_mul_u256(first_unexecutable_output, reserve_in)?, FEE_PRECISION)?;
99 let denominator =
100 safe_mul_u256(FEE_NUMERATOR, safe_sub_u256(reserve_out, first_unexecutable_output)?)?;
101
102 safe_div_u256(safe_sub_u256(numerator, U256::from(1))?, denominator)
103 }
104
105 fn capped_limits(
106 &self,
107 sell_token: Bytes,
108 buy_token: Bytes,
109 ) -> Result<(BigUint, BigUint), SimulationError> {
110 let (soft_input, _) = cpmm_get_limits(
111 sell_token.clone(),
112 buy_token.clone(),
113 self.reserve0,
114 self.reserve1,
115 RING_SWAP_V2_FEE_BPS,
116 )?;
117 let zero_to_one = sell_token < buy_token;
118 let (reserve_in, reserve_out) = if zero_to_one {
119 (self.reserve0, self.reserve1)
120 } else {
121 (self.reserve1, self.reserve0)
122 };
123 let soft_input_u256 = biguint_to_u256(&soft_input);
124 let soft_output =
125 cpmm_get_amount_out(soft_input_u256, reserve_in, reserve_out, Self::protocol_fee())?;
126 let output_backing = self.output_backing(zero_to_one);
127 if output_backing == U256::ZERO {
128 return Ok((BigUint::ZERO, BigUint::ZERO));
129 }
130 if soft_output <= output_backing {
131 return Ok((soft_input, u256_to_biguint(soft_output)));
132 }
133
134 let capped_input = Self::max_input_for_backing(reserve_in, reserve_out, output_backing)?
135 .min(soft_input_u256);
136 if capped_input == U256::ZERO {
137 return Ok((BigUint::ZERO, BigUint::ZERO));
138 }
139 let output =
140 cpmm_get_amount_out(capped_input, reserve_in, reserve_out, Self::protocol_fee())?;
141 Ok((u256_to_biguint(capped_input), u256_to_biguint(output)))
142 }
143
144 fn apply_component_balance_updates(&mut self, balances: &Balances) {
145 let Some(component_balances) = balances
146 .component_balances
147 .get(&self.component_id)
148 else {
149 return;
150 };
151 if let Some(balance) = component_balances.get(&self.token0) {
152 self.backing0 = U256::from_be_slice(balance);
153 }
154 if let Some(balance) = component_balances.get(&self.token1) {
155 self.backing1 = U256::from_be_slice(balance);
156 }
157 }
158}
159
160#[typetag::serde]
161impl ProtocolSim for RingSwapV2State {
162 fn fee(&self) -> f64 {
163 cpmm_fee(RING_SWAP_V2_FEE_BPS)
164 }
165
166 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
167 let price = cpmm_spot_price(base, quote, self.reserve0, self.reserve1)?;
168 Ok(add_fee_markup(price, self.fee()))
169 }
170
171 fn get_amount_out(
172 &self,
173 amount_in: BigUint,
174 token_in: &Token,
175 token_out: &Token,
176 ) -> Result<GetAmountOutResult, SimulationError> {
177 let amount_in = biguint_to_u256(&amount_in);
178 let zero_to_one = self.zero_to_one(token_in, token_out);
179 let (reserve_in, reserve_out) = if zero_to_one {
180 (self.reserve0, self.reserve1)
181 } else {
182 (self.reserve1, self.reserve0)
183 };
184 let amount_out =
185 cpmm_get_amount_out(amount_in, reserve_in, reserve_out, Self::protocol_fee())?;
186 let output_backing = self.output_backing(zero_to_one);
187 if output_backing == U256::ZERO || amount_out > output_backing {
188 return Err(SimulationError::InvalidInput(
189 "RingSwapV2 output exceeds FewToken underlying backing".to_string(),
190 None,
191 ));
192 }
193
194 let mut new_state = self.clone();
195 if zero_to_one {
196 new_state.reserve0 = safe_add_u256(self.reserve0, amount_in)?;
197 new_state.reserve1 = safe_sub_u256(self.reserve1, amount_out)?;
198 new_state.backing0 = safe_add_u256(self.backing0, amount_in)?;
199 new_state.backing1 = safe_sub_u256(self.backing1, amount_out)?;
200 } else {
201 new_state.reserve0 = safe_sub_u256(self.reserve0, amount_out)?;
202 new_state.reserve1 = safe_add_u256(self.reserve1, amount_in)?;
203 new_state.backing0 = safe_sub_u256(self.backing0, amount_out)?;
204 new_state.backing1 = safe_add_u256(self.backing1, amount_in)?;
205 }
206
207 Ok(GetAmountOutResult::new(
208 u256_to_biguint(amount_out),
209 BigUint::from(SWAP_BASE_GAS),
210 Box::new(new_state),
211 ))
212 }
213
214 fn get_limits(
215 &self,
216 sell_token: Bytes,
217 buy_token: Bytes,
218 ) -> Result<(BigUint, BigUint), SimulationError> {
219 self.capped_limits(sell_token, buy_token)
220 }
221
222 fn delta_transition(
223 &mut self,
224 delta: ProtocolStateDelta,
225 _tokens: &HashMap<Bytes, Token>,
226 balances: &Balances,
227 ) -> Result<(), TransitionError> {
228 if delta
229 .updated_attributes
230 .contains_key("reserve0") ||
231 delta
232 .updated_attributes
233 .contains_key("reserve1")
234 {
235 self.reserve0 = U256::from_be_slice(
236 delta
237 .updated_attributes
238 .get("reserve0")
239 .ok_or_else(|| TransitionError::MissingAttribute("reserve0".to_string()))?,
240 );
241 self.reserve1 = U256::from_be_slice(
242 delta
243 .updated_attributes
244 .get("reserve1")
245 .ok_or_else(|| TransitionError::MissingAttribute("reserve1".to_string()))?,
246 );
247 }
248 self.apply_component_balance_updates(balances);
249 Ok(())
250 }
251
252 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
253 match params.swap_constraint() {
254 SwapConstraint::PoolTargetPrice {
255 target: price,
256 tolerance: _,
257 min_amount_in: _,
258 max_amount_in: _,
259 } => {
260 let zero_to_one = self.zero_to_one(params.token_in(), params.token_out());
261 let (reserve_in, reserve_out) = if zero_to_one {
262 (self.reserve0, self.reserve1)
263 } else {
264 (self.reserve1, self.reserve0)
265 };
266 let (target_input, _) =
267 cpmm_swap_to_price(reserve_in, reserve_out, price, Self::protocol_fee())?;
268 let (max_input, _) = self.get_limits(
269 params.token_in().address.clone(),
270 params.token_out().address.clone(),
271 )?;
272 let amount_in = target_input.min(max_input);
273 if amount_in.is_zero() {
274 return Ok(PoolSwap::new(
275 BigUint::ZERO,
276 BigUint::ZERO,
277 Box::new(self.clone()),
278 None,
279 ));
280 }
281
282 let result =
283 self.get_amount_out(amount_in.clone(), params.token_in(), params.token_out())?;
284 Ok(PoolSwap::new(amount_in, result.amount, result.new_state, None))
285 }
286 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
287 "RingSwapV2State does not support TradeLimitPrice constraint in query_pool_swap"
288 .to_string(),
289 None,
290 )),
291 }
292 }
293
294 fn clone_box(&self) -> Box<dyn ProtocolSim> {
295 Box::new(self.clone())
296 }
297
298 fn as_any(&self) -> &dyn Any {
299 self
300 }
301
302 fn as_any_mut(&mut self) -> &mut dyn Any {
303 self
304 }
305
306 fn eq(&self, other: &dyn ProtocolSim) -> bool {
307 other
308 .as_any()
309 .downcast_ref::<Self>()
310 .is_some_and(|other| self == other)
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use std::collections::HashMap;
317
318 use alloy::primitives::U256;
319 use num_bigint::BigUint;
320 use tycho_common::{
321 dto::ProtocolStateDelta,
322 models::{token::Token, Chain},
323 simulation::{
324 errors::SimulationError,
325 protocol_sim::{Balances, ProtocolSim},
326 },
327 Bytes,
328 };
329
330 use super::*;
331
332 fn address(value: u8) -> Bytes {
333 Bytes::from(vec![value; 20])
334 }
335
336 fn token(value: u8) -> Token {
337 Token::new(&address(value), "T", 18, 0, &[Some(10_000)], Chain::Ethereum, 100)
338 }
339
340 fn state_with_id(component_id: &str, backing1: u64) -> RingSwapV2State {
341 RingSwapV2State::new(
342 component_id.to_string(),
343 U256::from(1_000),
344 U256::from(1_000),
345 U256::from(1_000),
346 U256::from(backing1),
347 address(1),
348 address(2),
349 )
350 }
351
352 fn state(backing1: u64) -> RingSwapV2State {
353 state_with_id("ring-pool", backing1)
354 }
355
356 #[test]
357 fn rejects_quotes_that_exceed_output_wrapper_backing() {
358 let result = state(10).get_amount_out(BigUint::from(100_u64), &token(1), &token(2));
359
360 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
361 }
362
363 #[test]
364 fn quotes_within_backing_update_reserves_and_wrapper_balances() {
365 let result = state(10)
366 .get_amount_out(BigUint::from(10_u64), &token(1), &token(2))
367 .unwrap();
368 let updated = result
369 .new_state
370 .as_any()
371 .downcast_ref::<RingSwapV2State>()
372 .unwrap();
373
374 assert_eq!(result.amount, BigUint::from(9_u64));
375 assert_eq!(updated.reserve0, U256::from(1_010));
376 assert_eq!(updated.reserve1, U256::from(991));
377 assert_eq!(updated.backing0, U256::from(1_010));
378 assert_eq!(updated.backing1, U256::from(1));
379 }
380
381 #[test]
382 fn limits_are_capped_by_output_wrapper_backing() {
383 let state = state(10);
384 let (max_input, max_output) = state
385 .get_limits(address(1), address(2))
386 .unwrap();
387
388 assert_eq!(max_input, BigUint::from(11_u64));
389 assert_eq!(max_output, BigUint::from(10_u64));
390 assert!(state
391 .get_amount_out(max_input.clone(), &token(1), &token(2))
392 .is_ok());
393 assert!(matches!(
394 state.get_amount_out(max_input + BigUint::from(1_u64), &token(1), &token(2)),
395 Err(SimulationError::InvalidInput(_, None))
396 ));
397 }
398
399 #[test]
400 fn zero_backing_has_no_executable_limits() {
401 let state = state(0);
402 let (max_input, max_output) = state
403 .get_limits(address(1), address(2))
404 .unwrap();
405
406 assert_eq!(max_input, BigUint::ZERO);
407 assert_eq!(max_output, BigUint::ZERO);
408 assert!(matches!(
409 state.get_amount_out(BigUint::from(1_u64), &token(1), &token(2)),
410 Err(SimulationError::InvalidInput(_, None))
411 ));
412 }
413
414 #[test]
415 fn closed_form_backing_limit_is_exact_at_boundary() {
416 for (reserve_in, reserve_out, backing) in [
417 (1_000_u64, 1_000_u64, 0_u64),
418 (1_000, 1_000, 10),
419 (10_000, 25_000, 1_000),
420 (25_000, 10_000, 9_000),
421 ] {
422 let max_input = RingSwapV2State::max_input_for_backing(
423 U256::from(reserve_in),
424 U256::from(reserve_out),
425 U256::from(backing),
426 )
427 .unwrap();
428 let amount_out = cpmm_get_amount_out(
429 max_input,
430 U256::from(reserve_in),
431 U256::from(reserve_out),
432 RingSwapV2State::protocol_fee(),
433 )
434 .unwrap();
435 let next_amount_out = cpmm_get_amount_out(
436 max_input + U256::from(1),
437 U256::from(reserve_in),
438 U256::from(reserve_out),
439 RingSwapV2State::protocol_fee(),
440 )
441 .unwrap();
442
443 assert!(amount_out <= U256::from(backing));
444 assert!(next_amount_out > U256::from(backing));
445 }
446 }
447
448 #[test]
449 fn backing_at_or_above_reserve_is_uncapped_without_overflow() {
450 assert_eq!(
451 RingSwapV2State::max_input_for_backing(
452 U256::from(1_000),
453 U256::from(1_000),
454 U256::from(1_000),
455 )
456 .unwrap(),
457 U256::MAX
458 );
459 assert_eq!(
460 RingSwapV2State::max_input_for_backing(
461 U256::from(1_000),
462 U256::from(1_000),
463 U256::MAX,
464 )
465 .unwrap(),
466 U256::MAX
467 );
468 }
469
470 #[test]
471 fn uncapped_limits_report_the_exact_cpmm_output() {
472 let state = state(10_000);
473 let (max_input, max_output) = state
474 .get_limits(address(1), address(2))
475 .unwrap();
476 let expected_output = cpmm_get_amount_out(
477 biguint_to_u256(&max_input),
478 state.reserve0,
479 state.reserve1,
480 RingSwapV2State::protocol_fee(),
481 )
482 .unwrap();
483
484 assert_eq!(max_output, u256_to_biguint(expected_output));
485 }
486
487 #[test]
488 fn component_balance_only_delta_updates_without_reserve_attributes() {
489 let mut state = state(10);
490 let balances = Balances {
491 component_balances: HashMap::from([(
492 "ring-pool".to_string(),
493 HashMap::from([(address(2), Bytes::from(vec![42]))]),
494 )]),
495 account_balances: HashMap::new(),
496 };
497
498 state
499 .delta_transition(ProtocolStateDelta::default(), &HashMap::new(), &balances)
500 .unwrap();
501
502 assert_eq!(state.reserve0, U256::from(1_000));
503 assert_eq!(state.reserve1, U256::from(1_000));
504 assert_eq!(state.backing1, U256::from(42));
505 }
506
507 #[test]
508 fn shared_wrapper_updates_each_pool_from_its_component_balance() {
509 let mut first_pool = state_with_id("first-pool", 10);
510 let mut second_pool = state_with_id("second-pool", 20);
511 let balances = Balances {
512 component_balances: HashMap::from([
513 ("first-pool".to_string(), HashMap::from([(address(2), Bytes::from(vec![42]))])),
514 ("second-pool".to_string(), HashMap::from([(address(2), Bytes::from(vec![21]))])),
515 ]),
516 account_balances: HashMap::new(),
517 };
518
519 first_pool
520 .delta_transition(ProtocolStateDelta::default(), &HashMap::new(), &balances)
521 .unwrap();
522 second_pool
523 .delta_transition(ProtocolStateDelta::default(), &HashMap::new(), &balances)
524 .unwrap();
525
526 assert_eq!(first_pool.backing1, U256::from(42));
527 assert_eq!(second_pool.backing1, U256::from(21));
528 }
529
530 #[test]
531 fn unrelated_component_balance_does_not_update_state() {
532 let mut state = state(10);
533 let balances = Balances {
534 component_balances: HashMap::from([(
535 "other-pool".to_string(),
536 HashMap::from([(address(2), Bytes::from(vec![99]))]),
537 )]),
538 account_balances: HashMap::new(),
539 };
540
541 state
542 .delta_transition(ProtocolStateDelta::default(), &HashMap::new(), &balances)
543 .unwrap();
544
545 assert_eq!(state.backing1, U256::from(10));
546 }
547
548 #[test]
549 fn backing_cap_is_applied_in_both_directions() {
550 let mut state = state(1_000);
551 state.backing0 = U256::from(10);
552
553 let result = state.get_amount_out(BigUint::from(100_u64), &token(2), &token(1));
554
555 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
556 }
557}