1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::U256;
4use num_bigint::BigUint;
5use num_traits::ToPrimitive;
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, Price, ProtocolSim, QueryPoolSwapParams,
14 SwapConstraint,
15 },
16 },
17 Bytes,
18};
19
20use crate::evm::{
21 protocol::{
22 safe_math::{safe_add_u256, safe_div_u256, safe_mul_u256, safe_sub_u256},
23 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
24 },
25 query_pool_swap::is_within_tolerance,
26};
27
28const WAD: u128 = 1_000_000_000_000_000_000;
29const HALTED: U256 = U256::MAX;
31
32const PSM_SWAP_GAS: u64 = 120_000;
33const WRAPPER_SWAP_GAS: u64 = 220_000;
34const CONVERTER_SWAP_GAS: u64 = 100_000;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub enum SkyComponentKind {
40 Psm,
42 PsmWrapper,
44 Converter,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct JoinEscrows {
53 pub dai: U256,
54 pub usds: U256,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub struct SkyState {
67 pub component_id: String,
68 pub kind: SkyComponentKind,
69 stable: Token,
71 gem: Token,
74 tin: U256,
76 tout: U256,
78 stable_balance: U256,
79 gem_balance: U256,
80 escrows: Option<JoinEscrows>,
82}
83
84impl SkyState {
85 #[allow(clippy::too_many_arguments)]
86 pub fn new(
87 component_id: String,
88 kind: SkyComponentKind,
89 stable: Token,
90 gem: Token,
91 tin: U256,
92 tout: U256,
93 stable_balance: U256,
94 gem_balance: U256,
95 escrows: Option<JoinEscrows>,
96 ) -> Self {
97 Self { component_id, kind, stable, gem, tin, tout, stable_balance, gem_balance, escrows }
98 }
99
100 fn conversion_factor(&self) -> U256 {
104 U256::from(10).pow(U256::from(self.stable.decimals - self.gem.decimals))
105 }
106
107 fn swap_gas(&self) -> u64 {
108 match self.kind {
109 SkyComponentKind::Psm => PSM_SWAP_GAS,
110 SkyComponentKind::PsmWrapper => WRAPPER_SWAP_GAS,
111 SkyComponentKind::Converter => CONVERTER_SWAP_GAS,
112 }
113 }
114
115 fn is_gem_to_stable(
116 &self,
117 token_in: &Bytes,
118 token_out: &Bytes,
119 ) -> Result<bool, SimulationError> {
120 if *token_in == self.gem.address && *token_out == self.stable.address {
121 Ok(true)
122 } else if *token_in == self.stable.address && *token_out == self.gem.address {
123 Ok(false)
124 } else {
125 Err(SimulationError::InvalidInput(
126 format!("invalid token pair: {token_in}, {token_out}"),
127 None,
128 ))
129 }
130 }
131
132 fn stable_out(&self, gem_in: U256) -> Result<U256, SimulationError> {
134 if self.tin == HALTED {
135 return Err(SimulationError::RecoverableError("sell gem is halted".to_string()));
136 }
137 let stable_wad = safe_mul_u256(gem_in, self.conversion_factor())?;
138 let fee = safe_mul_u256(stable_wad, self.tin)? / U256::from(WAD);
139 safe_sub_u256(stable_wad, fee)
140 }
141
142 fn gem_out(&self, stable_in: U256) -> Result<U256, SimulationError> {
145 if self.tout == HALTED {
146 return Err(SimulationError::RecoverableError("buy gem is halted".to_string()));
147 }
148 let wad = U256::from(WAD);
149 Ok(safe_mul_u256(stable_in, wad)? /
151 safe_mul_u256(self.conversion_factor(), safe_add_u256(wad, self.tout)?)?)
152 }
153
154 fn apply_component_balance_updates(&mut self, balances: &Balances) {
155 let Some(component_balances) = balances
156 .component_balances
157 .get(&self.component_id)
158 else {
159 return;
160 };
161 if let Some(balance) = component_balances.get(&self.stable.address) {
162 self.stable_balance = U256::from_be_slice(balance);
163 }
164 if let Some(balance) = component_balances.get(&self.gem.address) {
165 self.gem_balance = U256::from_be_slice(balance);
166 }
167 }
168
169 fn fee_f64(fee: U256) -> f64 {
170 if fee == HALTED {
171 return 1.0;
172 }
173 u256_to_f64(fee).unwrap_or(f64::MAX) / WAD as f64
174 }
175}
176
177#[typetag::serde]
178impl ProtocolSim for SkyState {
179 fn fee(&self) -> f64 {
180 f64::max(Self::fee_f64(self.tin), Self::fee_f64(self.tout))
181 }
182
183 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
187 if self.is_gem_to_stable(&base.address, "e.address)? {
188 if self.tout == HALTED {
189 return Err(SimulationError::RecoverableError("buy gem is halted".to_string()));
190 }
191 Ok(1.0 + Self::fee_f64(self.tout))
192 } else {
193 if self.tin == HALTED {
194 return Err(SimulationError::RecoverableError("sell gem is halted".to_string()));
195 }
196 Ok(1.0 / (1.0 - Self::fee_f64(self.tin)))
197 }
198 }
199
200 fn get_amount_out(
201 &self,
202 amount_in: BigUint,
203 token_in: &Token,
204 token_out: &Token,
205 ) -> Result<GetAmountOutResult, SimulationError> {
206 let amount_in = biguint_to_u256(&amount_in);
207 let mut new_state = self.clone();
208 let gem_to_stable = self.is_gem_to_stable(&token_in.address, &token_out.address)?;
209
210 let amount_out = if self.kind == SkyComponentKind::Converter {
216 let (sell_escrow, out) = if gem_to_stable {
217 (self.gem_balance, self.stable_out(amount_in)?)
218 } else {
219 (self.stable_balance, self.gem_out(amount_in)?)
220 };
221 if amount_in > sell_escrow {
222 return Err(SimulationError::RecoverableError(format!(
223 "amount in {amount_in} exceeds sell token escrow {sell_escrow}"
224 )));
225 }
226 if gem_to_stable {
227 new_state.gem_balance -= amount_in;
228 new_state.stable_balance += out;
229 } else {
230 new_state.stable_balance -= amount_in;
231 new_state.gem_balance += out;
232 }
233 out
234 } else if gem_to_stable {
235 let out = self.stable_out(amount_in)?;
236 if out > self.stable_balance {
237 return Err(SimulationError::RecoverableError(format!(
238 "amount out {out} exceeds stable inventory {}",
239 self.stable_balance
240 )));
241 }
242 if let Some(escrows) = &mut new_state.escrows {
246 if out > escrows.dai {
247 return Err(SimulationError::RecoverableError(format!(
248 "amount out {out} exceeds DAI join escrow {}",
249 escrows.dai
250 )));
251 }
252 escrows.dai -= out;
253 escrows.usds += out;
254 }
255 new_state.stable_balance -= out;
256 new_state.gem_balance += amount_in;
257 out
258 } else {
259 let out = self.gem_out(amount_in)?;
260 if out > self.gem_balance {
261 return Err(SimulationError::RecoverableError(format!(
262 "amount out {out} exceeds gem inventory {}",
263 self.gem_balance
264 )));
265 }
266 if let Some(escrows) = &mut new_state.escrows {
269 if amount_in > escrows.usds {
270 return Err(SimulationError::RecoverableError(format!(
271 "amount in {amount_in} exceeds USDS join escrow {}",
272 escrows.usds
273 )));
274 }
275 escrows.usds -= amount_in;
276 escrows.dai += amount_in;
277 }
278 new_state.gem_balance -= out;
279 new_state.stable_balance += amount_in;
280 out
281 };
282
283 Ok(GetAmountOutResult {
284 amount: u256_to_biguint(amount_out),
285 gas: BigUint::from(self.swap_gas()),
286 new_state: Box::new(new_state),
287 })
288 }
289
290 fn get_limits(
291 &self,
292 sell_token: Bytes,
293 buy_token: Bytes,
294 ) -> Result<(BigUint, BigUint), SimulationError> {
295 let wad = U256::from(WAD);
296 if self.is_gem_to_stable(&sell_token, &buy_token)? {
297 if self.tin == HALTED {
300 return Ok((BigUint::ZERO, BigUint::ZERO));
301 }
302 if self.kind == SkyComponentKind::Converter {
303 return Ok((u256_to_biguint(self.gem_balance), u256_to_biguint(self.gem_balance)));
304 }
305 let max_out = match &self.escrows {
308 Some(escrows) => self.stable_balance.min(escrows.dai),
309 None => self.stable_balance,
310 };
311 let max_in = safe_div_u256(
312 safe_mul_u256(max_out, wad)?,
313 safe_mul_u256(self.conversion_factor(), safe_sub_u256(wad, self.tin)?)?,
314 )?;
315 Ok((u256_to_biguint(max_in), u256_to_biguint(self.stable_out(max_in)?)))
316 } else {
317 if self.tout == HALTED {
318 return Ok((BigUint::ZERO, BigUint::ZERO));
319 }
320 if self.kind == SkyComponentKind::Converter {
321 return Ok((
322 u256_to_biguint(self.stable_balance),
323 u256_to_biguint(self.stable_balance),
324 ));
325 }
326 let max_out = self.gem_balance;
329 let max_in = safe_mul_u256(
330 safe_mul_u256(max_out, self.conversion_factor())?,
331 safe_add_u256(wad, self.tout)?,
332 )? / wad;
333 if let Some(escrows) = &self.escrows {
334 if escrows.usds < max_in {
335 return Ok((
336 u256_to_biguint(escrows.usds),
337 u256_to_biguint(self.gem_out(escrows.usds)?),
338 ));
339 }
340 }
341 Ok((u256_to_biguint(max_in), u256_to_biguint(max_out)))
342 }
343 }
344
345 fn delta_transition(
346 &mut self,
347 delta: ProtocolStateDelta,
348 _tokens: &HashMap<Bytes, Token>,
349 balances: &Balances,
350 ) -> Result<(), TransitionError> {
351 if let Some(tin) = delta.updated_attributes.get("tin") {
352 self.tin = U256::from_be_slice(tin);
353 }
354 if let Some(tout) = delta.updated_attributes.get("tout") {
355 self.tout = U256::from_be_slice(tout);
356 }
357 if let Some(escrows) = &mut self.escrows {
358 if let Some(dai) = delta
359 .updated_attributes
360 .get("dai_escrow")
361 {
362 escrows.dai = U256::from_be_slice(dai);
363 }
364 if let Some(usds) = delta
365 .updated_attributes
366 .get("usds_escrow")
367 {
368 escrows.usds = U256::from_be_slice(usds);
369 }
370 }
371 self.apply_component_balance_updates(balances);
372 Ok(())
373 }
374
375 fn clone_box(&self) -> Box<dyn ProtocolSim> {
376 Box::new(self.clone())
377 }
378
379 fn as_any(&self) -> &dyn Any {
380 self
381 }
382
383 fn as_any_mut(&mut self) -> &mut dyn Any {
384 self
385 }
386
387 fn eq(&self, other: &dyn ProtocolSim) -> bool {
388 if let Some(other_state) = other
389 .as_any()
390 .downcast_ref::<SkyState>()
391 {
392 self == other_state
393 } else {
394 false
395 }
396 }
397
398 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
410 let token_in = params.token_in();
411 let token_out = params.token_out();
412 let zero_swap =
413 || PoolSwap::new(BigUint::from(0u8), BigUint::from(0u8), self.clone_box(), None);
414
415 match params.swap_constraint() {
416 SwapConstraint::TradeLimitPrice { limit, .. } => {
417 let (max_in, _) =
419 self.get_limits(token_in.address.clone(), token_out.address.clone())?;
420 if max_in == BigUint::from(0u8) {
421 return Ok(zero_swap());
422 }
423 let execution = if self.is_gem_to_stable(&token_in.address, &token_out.address)? {
427 1.0 - Self::fee_f64(self.tin)
428 } else {
429 1.0 / (1.0 + Self::fee_f64(self.tout))
430 };
431 let limit = price_f64(limit, token_in.decimals, token_out.decimals);
432 if execution < limit {
433 return Ok(zero_swap());
434 }
435 let result = self.get_amount_out(max_in.clone(), token_in, token_out)?;
438 Ok(PoolSwap::new(max_in, result.amount, result.new_state, None))
439 }
440 SwapConstraint::PoolTargetPrice { target, tolerance, .. } => {
441 let target = price_f64(target, token_in.decimals, token_out.decimals);
442 let spot = self.spot_price(token_in, token_out)?;
443 if is_within_tolerance(spot, target, *tolerance) {
444 return Ok(zero_swap());
445 }
446 Err(SimulationError::InvalidInput(
447 format!("spot price {spot} is size-independent; cannot reach target {target}"),
448 None,
449 ))
450 }
451 }
452 }
453}
454
455fn price_f64(price: &Price, in_decimals: u32, out_decimals: u32) -> f64 {
457 (to_f64(&price.numerator, out_decimals)) / (to_f64(&price.denominator, in_decimals))
458}
459
460fn to_f64(amount: &BigUint, decimals: u32) -> f64 {
461 amount.to_f64().unwrap_or(f64::MAX) / 10f64.powi(decimals as i32)
462}
463
464#[cfg(test)]
465mod tests {
466 use std::str::FromStr;
467
468 use rstest::rstest;
469 use tycho_common::models::Chain;
470
471 use super::*;
472
473 fn dai() -> Token {
474 Token::new(
475 &Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
476 "DAI",
477 18,
478 0,
479 &[Some(50_000)],
480 Chain::Ethereum,
481 100,
482 )
483 }
484
485 fn usdc() -> Token {
486 Token::new(
487 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
488 "USDC",
489 6,
490 0,
491 &[Some(50_000)],
492 Chain::Ethereum,
493 100,
494 )
495 }
496
497 fn usds() -> Token {
498 Token::new(
499 &Bytes::from_str("0xdc035d45d973e3ec169d2276ddab16f1e407384f").unwrap(),
500 "USDS",
501 18,
502 0,
503 &[Some(50_000)],
504 Chain::Ethereum,
505 100,
506 )
507 }
508
509 fn wad(amount: u64) -> U256 {
510 U256::from(amount) * U256::from(WAD)
511 }
512
513 fn psm_state(tin: U256, tout: U256) -> SkyState {
514 SkyState::new(
515 "0xf6e72db5454dd049d0788e411b06cfaf16853042".to_string(),
516 SkyComponentKind::Psm,
517 dai(),
518 usdc(),
519 tin,
520 tout,
521 wad(1_000_000), U256::from(2_000_000_000_000u64), None,
524 )
525 }
526
527 fn wrapper_state(escrows: JoinEscrows) -> SkyState {
528 SkyState::new(
529 "0xa188eec8f81263234da3622a406892f3d630f98c".to_string(),
530 SkyComponentKind::PsmWrapper,
531 usds(),
532 usdc(),
533 U256::ZERO,
534 U256::ZERO,
535 wad(1_000_000), U256::from(2_000_000_000_000u64), Some(escrows),
538 )
539 }
540
541 fn converter_state() -> SkyState {
542 SkyState::new(
543 "0x3225737a9bbb6473cb4a45b7244aca2befdb276a".to_string(),
544 SkyComponentKind::Converter,
545 dai(),
546 usds(),
547 U256::ZERO,
548 U256::ZERO,
549 wad(3_000_000), wad(9_000_000), None,
552 )
553 }
554
555 #[test]
556 fn sell_gem_fee_free_rescales_decimals() {
557 let state = psm_state(U256::ZERO, U256::ZERO);
558 let res = state
559 .get_amount_out(BigUint::from(1_000_000u64), &usdc(), &dai())
560 .unwrap();
561 assert_eq!(res.amount, BigUint::from(WAD));
563 }
564
565 #[test]
566 fn buy_gem_fee_free_rescales_decimals() {
567 let state = psm_state(U256::ZERO, U256::ZERO);
568 let res = state
569 .get_amount_out(BigUint::from(WAD), &dai(), &usdc())
570 .unwrap();
571 assert_eq!(res.amount, BigUint::from(1_000_000u64));
573 }
574
575 #[test]
576 fn sell_gem_applies_tin() {
577 let state = psm_state(U256::from(WAD / 1000), U256::ZERO);
579 let res = state
580 .get_amount_out(BigUint::from(1_000_000u64), &usdc(), &dai())
581 .unwrap();
582 assert_eq!(res.amount, BigUint::from(WAD - WAD / 1000));
583 }
584
585 #[test]
586 fn buy_gem_applies_tout_and_rounds_down() {
587 let state = psm_state(U256::ZERO, U256::from(WAD / 1000));
589 let res = state
590 .get_amount_out(BigUint::from(WAD), &dai(), &usdc())
591 .unwrap();
592 assert_eq!(res.amount, BigUint::from(999_000u64));
593 let cost = U256::from(999_000u64) * U256::from(1_000_000_000_000u64) * (U256::from(WAD) + U256::from(WAD / 1000)) /
596 U256::from(WAD);
597 assert!(cost <= wad(1));
598 }
599
600 #[test]
601 fn swap_updates_inventory_in_new_state() {
602 let state = psm_state(U256::ZERO, U256::ZERO);
603 let res = state
604 .get_amount_out(BigUint::from(1_000_000u64), &usdc(), &dai())
605 .unwrap();
606 let new_state = res
607 .new_state
608 .as_any()
609 .downcast_ref::<SkyState>()
610 .unwrap()
611 .clone();
612 assert_eq!(new_state.stable_balance, state.stable_balance - U256::from(WAD));
613 assert_eq!(new_state.gem_balance, state.gem_balance + U256::from(1_000_000u64));
614 }
615
616 #[test]
617 fn sell_gem_bounded_by_stable_inventory() {
618 let state = psm_state(U256::ZERO, U256::ZERO);
619 let res = state.get_amount_out(BigUint::from(2_000_000_000_000u64), &usdc(), &dai());
621 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
622 }
623
624 #[rstest]
625 #[case::sell_gem_halted(HALTED, U256::ZERO, true)]
626 #[case::buy_gem_halted(U256::ZERO, HALTED, false)]
627 fn halted_direction_has_zero_limits(
628 #[case] tin: U256,
629 #[case] tout: U256,
630 #[case] gem_to_stable: bool,
631 ) {
632 let state = psm_state(tin, tout);
633 let (sell, buy) = if gem_to_stable {
634 state
635 .get_limits(usdc().address, dai().address)
636 .unwrap()
637 } else {
638 state
639 .get_limits(dai().address, usdc().address)
640 .unwrap()
641 };
642 assert_eq!(sell, BigUint::ZERO);
643 assert_eq!(buy, BigUint::ZERO);
644 }
645
646 #[test]
647 fn limits_match_inventory() {
648 let state = psm_state(U256::ZERO, U256::ZERO);
649 let (max_in, max_out) = state
650 .get_limits(usdc().address, dai().address)
651 .unwrap();
652 assert_eq!(max_out, u256_to_biguint(wad(1_000_000)));
654 assert_eq!(max_in, BigUint::from(1_000_000_000_000u64));
655
656 let (max_in, max_out) = state
657 .get_limits(dai().address, usdc().address)
658 .unwrap();
659 assert_eq!(max_out, BigUint::from(2_000_000_000_000u64));
661 assert_eq!(max_in, u256_to_biguint(wad(2_000_000)));
662 }
663
664 #[test]
665 fn spot_prices_include_fees_and_round_trip() {
666 let tin = U256::from(WAD / 1000);
667 let tout = U256::from(2 * (WAD / 1000));
668 let state = psm_state(tin, tout);
669 let usdc_in_dai = state
670 .spot_price(&usdc(), &dai())
671 .unwrap();
672 let dai_in_usdc = state
673 .spot_price(&dai(), &usdc())
674 .unwrap();
675 assert!((usdc_in_dai - 1.002).abs() < 1e-12);
678 assert!((dai_in_usdc - 1.0 / 0.999).abs() < 1e-12);
679 assert!(usdc_in_dai * dai_in_usdc >= 1.0);
680 }
681
682 #[test]
683 fn converter_is_symmetric_one_to_one() {
684 let state = converter_state();
685 let res = state
686 .get_amount_out(BigUint::from(WAD), &usds(), &dai())
687 .unwrap();
688 assert_eq!(res.amount, BigUint::from(WAD));
689 let res = state
690 .get_amount_out(BigUint::from(WAD), &dai(), &usds())
691 .unwrap();
692 assert_eq!(res.amount, BigUint::from(WAD));
693 assert_eq!(
694 state
695 .spot_price(&dai(), &usds())
696 .unwrap(),
697 1.0
698 );
699 }
700
701 #[test]
702 fn converter_mints_output_regardless_of_target_escrow() {
703 let state = SkyState::new(
706 "0x3225737a9bbb6473cb4a45b7244aca2befdb276a".to_string(),
707 SkyComponentKind::Converter,
708 dai(),
709 usds(),
710 U256::ZERO,
711 U256::ZERO,
712 wad(3_000_000),
713 U256::ZERO,
714 None,
715 );
716 let res = state
717 .get_amount_out(BigUint::from(WAD), &dai(), &usds())
718 .unwrap();
719 assert_eq!(res.amount, BigUint::from(WAD));
720 let new_state = res
721 .new_state
722 .as_any()
723 .downcast_ref::<SkyState>()
724 .unwrap();
725 assert_eq!(new_state.stable_balance, wad(3_000_000) - U256::from(WAD));
726 assert_eq!(new_state.gem_balance, U256::from(WAD));
727 }
728
729 #[test]
730 fn converter_burn_bounded_by_sell_escrow() {
731 let state = converter_state();
732 let res = state.get_amount_out(u256_to_biguint(wad(9_000_001)), &usds(), &dai());
734 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
735 }
736
737 #[test]
738 fn converter_limits_use_sell_token_escrow() {
739 let state = converter_state();
740 let (max_in, _) = state
741 .get_limits(dai().address, usds().address)
742 .unwrap();
743 assert_eq!(max_in, u256_to_biguint(wad(3_000_000)));
744 let (max_in, _) = state
745 .get_limits(usds().address, dai().address)
746 .unwrap();
747 assert_eq!(max_in, u256_to_biguint(wad(9_000_000)));
748 }
749
750 #[test]
751 fn wrapper_stable_payout_bounded_by_dai_escrow() {
752 let state = wrapper_state(JoinEscrows { dai: wad(400_000), usds: wad(9_000_000) });
754 let (max_in, max_out) = state
755 .get_limits(usdc().address, usds().address)
756 .unwrap();
757 assert_eq!(max_out, u256_to_biguint(wad(400_000)));
758 assert_eq!(max_in, BigUint::from(400_000_000_000u64));
759
760 let res = state.get_amount_out(BigUint::from(500_000_000_000u64), &usdc(), &usds());
761 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
762 }
763
764 #[test]
765 fn wrapper_stable_input_bounded_by_usds_escrow() {
766 let state = wrapper_state(JoinEscrows { dai: wad(3_000_000), usds: wad(500_000) });
768 let (max_in, max_out) = state
769 .get_limits(usds().address, usdc().address)
770 .unwrap();
771 assert_eq!(max_in, u256_to_biguint(wad(500_000)));
772 assert_eq!(max_out, BigUint::from(500_000_000_000u64));
773
774 let res = state.get_amount_out(u256_to_biguint(wad(500_001)), &usds(), &usdc());
775 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
776 }
777
778 #[test]
779 fn wrapper_unbinding_escrows_leave_mirror_limits() {
780 let state = wrapper_state(JoinEscrows { dai: wad(3_000_000), usds: wad(9_000_000) });
781 let (max_in, max_out) = state
782 .get_limits(usds().address, usdc().address)
783 .unwrap();
784 assert_eq!(max_out, BigUint::from(2_000_000_000_000u64));
786 assert_eq!(max_in, u256_to_biguint(wad(2_000_000)));
787 let (_, max_out) = state
788 .get_limits(usdc().address, usds().address)
789 .unwrap();
790 assert_eq!(max_out, u256_to_biguint(wad(1_000_000)));
792 }
793
794 #[test]
795 fn wrapper_zero_usds_escrow_zeroes_buy_gem_limits() {
796 let state = wrapper_state(JoinEscrows { dai: wad(3_000_000), usds: U256::ZERO });
798 let (max_in, max_out) = state
799 .get_limits(usds().address, usdc().address)
800 .unwrap();
801 assert_eq!(max_in, BigUint::ZERO);
802 assert_eq!(max_out, BigUint::ZERO);
803 }
804
805 #[test]
806 fn wrapper_swap_moves_escrows_in_new_state() {
807 let escrows = JoinEscrows { dai: wad(3_000_000), usds: wad(9_000_000) };
808 let state = wrapper_state(escrows);
809
810 let res = state
812 .get_amount_out(BigUint::from(1_000_000u64), &usdc(), &usds())
813 .unwrap();
814 let new_state = res
815 .new_state
816 .as_any()
817 .downcast_ref::<SkyState>()
818 .unwrap();
819 let new_escrows = new_state.escrows.unwrap();
820 assert_eq!(new_escrows.dai, escrows.dai - U256::from(WAD));
821 assert_eq!(new_escrows.usds, escrows.usds + U256::from(WAD));
822
823 let res = state
825 .get_amount_out(BigUint::from(WAD), &usds(), &usdc())
826 .unwrap();
827 let new_state = res
828 .new_state
829 .as_any()
830 .downcast_ref::<SkyState>()
831 .unwrap();
832 let new_escrows = new_state.escrows.unwrap();
833 assert_eq!(new_escrows.usds, escrows.usds - U256::from(WAD));
834 assert_eq!(new_escrows.dai, escrows.dai + U256::from(WAD));
835 }
836
837 #[test]
838 fn delta_transition_updates_escrows() {
839 let mut state = wrapper_state(JoinEscrows { dai: wad(3_000_000), usds: wad(9_000_000) });
840 let delta = ProtocolStateDelta {
841 component_id: state.component_id.clone(),
842 updated_attributes: HashMap::from([
843 ("dai_escrow".to_string(), Bytes::from(wad(5).to_be_bytes_vec())),
844 ("usds_escrow".to_string(), Bytes::from(wad(6).to_be_bytes_vec())),
845 ]),
846 deleted_attributes: Default::default(),
847 };
848 let balances =
849 Balances { component_balances: HashMap::new(), account_balances: HashMap::new() };
850 state
851 .delta_transition(delta, &HashMap::new(), &balances)
852 .unwrap();
853 assert_eq!(state.escrows.unwrap(), JoinEscrows { dai: wad(5), usds: wad(6) });
854 }
855
856 fn usdc_dai_price(dai_wei: u128) -> Price {
858 Price::new(BigUint::from(dai_wei), BigUint::from(1_000_000u32))
859 }
860
861 #[test]
862 fn query_pool_swap_trade_limit_is_all_or_nothing() {
863 let state = psm_state(U256::from(WAD / 1000), U256::ZERO);
865
866 let swap = state
868 .query_pool_swap(&QueryPoolSwapParams::new(
869 usdc(),
870 dai(),
871 SwapConstraint::TradeLimitPrice {
872 limit: usdc_dai_price(990_000_000_000_000_000), tolerance: 0.0,
874 min_amount_in: None,
875 max_amount_in: None,
876 },
877 ))
878 .unwrap();
879 let (max_in, max_out) = state
880 .get_limits(usdc().address, dai().address)
881 .unwrap();
882 assert_eq!(swap.amount_in(), &max_in);
883 assert_eq!(swap.amount_out(), &max_out);
884
885 let swap = state
887 .query_pool_swap(&QueryPoolSwapParams::new(
888 usdc(),
889 dai(),
890 SwapConstraint::TradeLimitPrice {
891 limit: usdc_dai_price(999_500_000_000_000_000), tolerance: 0.0001,
893 min_amount_in: None,
894 max_amount_in: None,
895 },
896 ))
897 .unwrap();
898 assert_eq!(swap.amount_in(), &BigUint::from(0u8));
899 assert_eq!(swap.amount_out(), &BigUint::from(0u8));
900
901 let swap = state
904 .query_pool_swap(&QueryPoolSwapParams::new(
905 usdc(),
906 dai(),
907 SwapConstraint::TradeLimitPrice {
908 limit: usdc_dai_price(999_500_000_000_000_000), tolerance: 0.01,
910 min_amount_in: None,
911 max_amount_in: None,
912 },
913 ))
914 .unwrap();
915 assert_eq!(swap.amount_in(), &BigUint::from(0u8));
916 assert_eq!(swap.amount_out(), &BigUint::from(0u8));
917 }
918
919 #[test]
920 fn query_pool_swap_target_price_only_satisfiable_at_spot() {
921 let state = psm_state(U256::ZERO, U256::from(2 * (WAD / 1000)));
923
924 let swap = state
925 .query_pool_swap(&QueryPoolSwapParams::new(
926 usdc(),
927 dai(),
928 SwapConstraint::PoolTargetPrice {
929 target: usdc_dai_price(1_002_000_000_000_000_000), tolerance: 1e-9,
931 min_amount_in: None,
932 max_amount_in: None,
933 },
934 ))
935 .unwrap();
936 assert_eq!(swap.amount_in(), &BigUint::from(0u8));
937
938 let res = state.query_pool_swap(&QueryPoolSwapParams::new(
939 usdc(),
940 dai(),
941 SwapConstraint::PoolTargetPrice {
942 target: usdc_dai_price(1_010_000_000_000_000_000), tolerance: 1e-9,
944 min_amount_in: None,
945 max_amount_in: None,
946 },
947 ));
948 assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
949 }
950
951 #[test]
952 fn delta_transition_updates_fees_and_balances() {
953 let mut state = psm_state(U256::ZERO, U256::ZERO);
954 let delta = ProtocolStateDelta {
955 component_id: state.component_id.clone(),
956 updated_attributes: HashMap::from([(
957 "tin".to_string(),
958 Bytes::from(U256::from(WAD / 100).to_be_bytes_vec()),
959 )]),
960 deleted_attributes: Default::default(),
961 };
962 let balances = Balances {
963 component_balances: HashMap::from([(
964 state.component_id.clone(),
965 HashMap::from([(dai().address, Bytes::from(wad(500_000).to_be_bytes_vec()))]),
966 )]),
967 account_balances: HashMap::new(),
968 };
969 state
970 .delta_transition(delta, &HashMap::new(), &balances)
971 .unwrap();
972 assert_eq!(state.tin, U256::from(WAD / 100));
973 assert_eq!(state.stable_balance, wad(500_000));
974 }
975
976 #[test]
977 fn invalid_pair_errors() {
978 let state = psm_state(U256::ZERO, U256::ZERO);
979 assert!(state
980 .get_amount_out(BigUint::from(1u64), &usds(), &dai())
981 .is_err());
982 }
983}