1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, FixedPoint};
4
5use crate::{calculate_rate_given_fixed_price, State, YieldSpace};
6
7impl State {
8 pub fn calculate_open_short(
50 &self,
51 bond_amount: FixedPoint<U256>,
52 open_vault_share_price: FixedPoint<U256>,
53 ) -> Result<FixedPoint<U256>> {
54 if bond_amount < self.minimum_transaction_amount() {
57 return Err(eyre!(
58 "MinimumTransactionAmount: Input amount too low. bond_amount = {:#?} must be >= {:#?}",
59 bond_amount,
60 self.minimum_transaction_amount()
61 ));
62 }
63
64 let open_vault_share_price = if open_vault_share_price == fixed!(0) {
68 self.vault_share_price()
69 } else {
70 open_vault_share_price
71 };
72
73 let share_reserves_delta = self.calculate_short_principal(bond_amount)?;
76
77 if share_reserves_delta.mul_up(self.vault_share_price()) > bond_amount {
83 return Err(eyre!(
84 "InsufficientLiquidity: Negative Interest.
85 expected bond_amount={} <= share_reserves_delta_in_shares={}",
86 bond_amount,
87 share_reserves_delta
88 ));
89 }
90
91 let curve_fee_shares = self
97 .open_short_curve_fee(bond_amount)?
98 .div_up(self.vault_share_price());
99 if share_reserves_delta < curve_fee_shares {
100 return Err(eyre!(format!(
101 "The transaction curve fee = {}, computed with coefficient = {},
102 is too high. It must be less than share reserves delta = {}",
103 curve_fee_shares,
104 self.curve_fee(),
105 share_reserves_delta
106 )));
107 }
108
109 let close_vault_share_price = open_vault_share_price.max(self.vault_share_price());
114
115 let base_proceeds = self
125 .calculate_short_proceeds_up(
126 bond_amount,
127 share_reserves_delta - curve_fee_shares,
128 open_vault_share_price,
129 close_vault_share_price,
130 )
131 .mul_up(self.vault_share_price());
132
133 Ok(base_proceeds)
134 }
135
136 pub fn calculate_open_short_derivative(
171 &self,
172 bond_amount: FixedPoint<U256>,
173 open_vault_share_price: FixedPoint<U256>,
174 maybe_initial_spot_price: Option<FixedPoint<U256>>,
175 ) -> Result<FixedPoint<U256>> {
176 let close_vault_share_price = open_vault_share_price.max(self.vault_share_price());
181
182 if self.calculate_short_proceeds_up(
184 bond_amount,
185 self.calculate_short_principal(bond_amount)?
186 - self
187 .open_short_curve_fee(bond_amount)?
188 .div_up(self.vault_share_price()),
189 open_vault_share_price,
190 close_vault_share_price,
191 ) == fixed!(0)
192 {
193 return Ok(fixed!(0));
194 }
195
196 let spot_price = match maybe_initial_spot_price {
197 Some(spot_price) => spot_price,
198 None => self.calculate_spot_price()?,
199 };
200
201 let share_adjustment_derivative =
203 close_vault_share_price.div_up(open_vault_share_price) + self.flat_fee();
204 let short_principal_derivative = self
205 .calculate_short_principal_derivative(bond_amount)?
206 .mul_up(self.vault_share_price());
207 let curve_fee_derivative = self.curve_fee().mul_up((fixed!(1e18) - spot_price));
208
209 Ok(share_adjustment_derivative - short_principal_derivative + curve_fee_derivative)
211 }
212
213 pub fn calculate_short_principal(
230 &self,
231 bond_amount: FixedPoint<U256>,
232 ) -> Result<FixedPoint<U256>> {
233 self.calculate_shares_out_given_bonds_in_down(bond_amount)
234 }
235
236 pub fn calculate_short_principal_derivative(
248 &self,
249 bond_amount: FixedPoint<U256>,
250 ) -> Result<FixedPoint<U256>> {
251 let lhs = fixed!(1e18).div_up(
253 self.vault_share_price()
254 .mul_up((self.bond_reserves() + bond_amount).pow(self.time_stretch())?),
255 );
256 let rhs = (self
257 .initial_vault_share_price()
258 .div_up(self.vault_share_price())
259 .mul_up(
260 self.k_up()?
261 - (self.bond_reserves() + bond_amount)
262 .pow(fixed!(1e18) - self.time_stretch())?,
263 ))
264 .pow(
265 self.time_stretch()
266 .div_up(fixed!(1e18) - self.time_stretch()),
267 )?;
268 Ok(lhs.mul_up(rhs))
269 }
270
271 pub fn calculate_pool_state_after_open_short(
278 &self,
279 bond_amount: FixedPoint<U256>,
280 maybe_share_amount: Option<FixedPoint<U256>>,
281 ) -> Result<Self> {
282 let share_amount = match maybe_share_amount {
283 Some(share_amount) => share_amount,
284 None => self.calculate_pool_share_delta_after_open_short(bond_amount)?,
285 };
286 let mut state = self.clone();
287 state.info.bond_reserves += bond_amount.into();
288 state.info.share_reserves -= share_amount.into();
289 Ok(state)
290 }
291
292 pub fn calculate_pool_share_delta_after_open_short(
294 &self,
295 bond_amount: FixedPoint<U256>,
296 ) -> Result<FixedPoint<U256>> {
297 let curve_fee_base = self.open_short_curve_fee(bond_amount)?;
298 let curve_fee_shares = curve_fee_base.div_up(self.vault_share_price());
299 let gov_curve_fee_shares = self
300 .open_short_governance_fee(bond_amount, Some(curve_fee_base))?
301 .div_up(self.vault_share_price());
302 let short_principal = self.calculate_short_principal(bond_amount)?;
303 if short_principal.mul_up(self.vault_share_price()) > bond_amount {
304 return Err(eyre!("InsufficientLiquidity: Negative Interest"));
305 }
306 if short_principal < (curve_fee_shares - gov_curve_fee_shares) {
307 return Err(eyre!(
308 "short_principal={:#?} is too low to account for fees={:#?}",
309 short_principal,
310 curve_fee_shares - gov_curve_fee_shares
311 ));
312 }
313 Ok(short_principal - (curve_fee_shares - gov_curve_fee_shares))
314 }
315
316 pub fn calculate_spot_price_after_short(
319 &self,
320 bond_amount: FixedPoint<U256>,
321 maybe_base_amount: Option<FixedPoint<U256>>,
322 ) -> Result<FixedPoint<U256>> {
323 let share_amount = match maybe_base_amount {
324 Some(base_amount) => base_amount / self.vault_share_price(),
325 None => self.calculate_pool_share_delta_after_open_short(bond_amount)?,
326 };
327 let updated_state =
328 self.calculate_pool_state_after_open_short(bond_amount, Some(share_amount))?;
329 updated_state.calculate_spot_price()
330 }
331
332 pub fn calculate_spot_rate_after_short(
346 &self,
347 bond_amount: FixedPoint<U256>,
348 maybe_base_amount: Option<FixedPoint<U256>>,
349 ) -> Result<FixedPoint<U256>> {
350 let price = self.calculate_spot_price_after_short(bond_amount, maybe_base_amount)?;
351 Ok(calculate_rate_given_fixed_price(
352 price,
353 self.position_duration(),
354 ))
355 }
356
357 pub fn calculate_implied_rate(
398 &self,
399 bond_amount: FixedPoint<U256>,
400 open_vault_share_price: FixedPoint<U256>,
401 variable_apy: FixedPoint<U256>,
402 ) -> Result<I256> {
403 let full_base_paid = self.calculate_open_short(bond_amount, open_vault_share_price)?;
404 let backpaid_interest = bond_amount
405 .mul_div_down(self.vault_share_price(), open_vault_share_price)
406 - bond_amount;
407 let base_paid = full_base_paid - backpaid_interest;
408 let tpy =
409 (fixed!(1e18) + variable_apy).pow(self.annualized_position_duration())? - fixed!(1e18);
410 let base_proceeds = bond_amount * tpy;
411 if base_proceeds > base_paid {
412 Ok(I256::try_from(
413 (base_proceeds - base_paid) / (base_paid * self.annualized_position_duration()),
414 )?)
415 } else {
416 Ok(-I256::try_from(
417 (base_paid - base_proceeds) / (base_paid * self.annualized_position_duration()),
418 )?)
419 }
420 }
421
422 pub fn calculate_approximate_short_bonds_given_base_deposit(
448 &self,
449 base_deposit: FixedPoint<U256>,
450 open_vault_share_price: FixedPoint<U256>,
451 ) -> Result<FixedPoint<U256>> {
452 let close_vault_share_price = open_vault_share_price.max(self.vault_share_price());
453 let shares_deposit = base_deposit / self.vault_share_price();
454 let minimum_short_principal =
455 self.calculate_short_principal(self.minimum_transaction_amount())?;
456 let price_adjustment_with_fees = close_vault_share_price / open_vault_share_price
457 + self.flat_fee()
458 + self.curve_fee() * (fixed!(1e18) - self.calculate_spot_price()?);
459 let approximate_bond_amount = (self.vault_share_price() / price_adjustment_with_fees)
460 * (shares_deposit + minimum_short_principal);
461 Ok(approximate_bond_amount)
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use std::panic;
468
469 use ethers::types::{U128, U256};
470 use fixedpointmath::{fixed, fixed_u256, int256, FixedPointValue};
471 use hyperdrive_test_utils::{
472 chain::TestChain,
473 constants::{FAST_FUZZ_RUNS, FUZZ_RUNS, SLOW_FUZZ_RUNS},
474 };
475 use hyperdrive_wrappers::wrappers::ihyperdrive::{Checkpoint, Options};
476 use rand::{thread_rng, Rng, SeedableRng};
477 use rand_chacha::ChaCha8Rng;
478
479 use super::*;
480 use crate::test_utils::{
481 agent::HyperdriveMathAgent,
482 preamble::{get_max_short, initialize_pool_with_random_state},
483 };
484
485 #[tokio::test]
486 async fn fuzz_calculate_pool_state_after_open_short() -> Result<()> {
487 let share_adjustment_test_tolerance = fixed_u256!(0);
489 let bond_reserves_test_tolerance = fixed!(0);
490 let share_reserves_test_tolerance = fixed!(1);
491 let chain = TestChain::new().await?;
493 let mut alice = chain.alice().await?;
494 let mut bob = chain.bob().await?;
495 let mut celine = chain.celine().await?;
496 let mut rng = {
500 let mut rng = thread_rng();
501 let seed = rng.gen();
502 ChaCha8Rng::seed_from_u64(seed)
503 };
504 for _ in 0..*SLOW_FUZZ_RUNS {
505 let id = chain.snapshot().await?;
507 initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
508 alice.advance_time(fixed!(0), fixed!(0)).await?;
510 let original_state = alice.get_state().await?;
511 let checkpoint_exposure = alice
513 .get_checkpoint_exposure(original_state.to_checkpoint(alice.now().await?))
514 .await?;
515 let max_short_amount = original_state.calculate_max_short(
516 U256::MAX,
517 original_state.vault_share_price(),
518 checkpoint_exposure,
519 None,
520 None,
521 )?;
522 let bond_amount =
523 rng.gen_range(original_state.minimum_transaction_amount()..=max_short_amount);
524 let rust_state =
526 original_state.calculate_pool_state_after_open_short(bond_amount, None)?;
527 bob.fund(bond_amount * fixed!(1.5e18)).await?;
529 bob.open_short(bond_amount, None, None).await?;
530 let sol_state = alice.get_state().await?;
531 let rust_share_adjustment = rust_state.share_adjustment();
533 let sol_share_adjustment = sol_state.share_adjustment();
534 let share_adjustment_error = if rust_share_adjustment < sol_share_adjustment {
535 FixedPoint::try_from(sol_share_adjustment - rust_share_adjustment)?
536 } else {
537 FixedPoint::try_from(rust_share_adjustment - sol_share_adjustment)?
538 };
539 assert!(
540 share_adjustment_error <= share_adjustment_test_tolerance,
541 "expected abs(rust_share_adjustment={}-sol_share_adjustment={}) <= test_tolerance={}",
542 rust_share_adjustment, sol_share_adjustment, share_adjustment_test_tolerance
543 );
544 let rust_bond_reserves = rust_state.bond_reserves();
545 let sol_bond_reserves = sol_state.bond_reserves();
546 let bond_reserves_error = if rust_bond_reserves < sol_bond_reserves {
547 sol_bond_reserves - rust_bond_reserves
548 } else {
549 rust_bond_reserves - sol_bond_reserves
550 };
551 assert!(
552 bond_reserves_error <= bond_reserves_test_tolerance,
553 "expected abs(rust_bond_reserves={}-sol_bond_reserves={}) <= test_tolerance={}",
554 rust_bond_reserves,
555 sol_bond_reserves,
556 bond_reserves_test_tolerance
557 );
558 let rust_share_reserves = rust_state.share_reserves();
559 let sol_share_reserves = sol_state.share_reserves();
560 let share_reserves_error = if rust_share_reserves < sol_share_reserves {
561 sol_share_reserves - rust_share_reserves
562 } else {
563 rust_share_reserves - sol_share_reserves
564 };
565 assert!(
566 share_reserves_error <= share_reserves_test_tolerance,
567 "expected abs(rust_share_reserves={}-sol_share_reserves={}) <= test_tolerance={}",
568 rust_share_reserves,
569 sol_share_reserves,
570 share_reserves_test_tolerance
571 );
572 chain.revert(id).await?;
574 alice.reset(Default::default()).await?;
575 bob.reset(Default::default()).await?;
576 celine.reset(Default::default()).await?;
577 }
578 Ok(())
579 }
580
581 #[tokio::test]
582 async fn test_sol_calculate_pool_share_delta_after_open_short() -> Result<()> {
583 let test_tolerance = fixed!(10);
584
585 let chain = TestChain::new().await?;
586 let mut rng = thread_rng();
587 for _ in 0..*FAST_FUZZ_RUNS {
588 let state = rng.gen::<State>();
589 let checkpoint_exposure = {
590 let value = rng.gen_range(fixed_u256!(0)..=fixed!(10_000_000e18));
591 if rng.gen() {
592 -I256::try_from(value).unwrap()
593 } else {
594 I256::try_from(value).unwrap()
595 }
596 };
597 let max_bond_amount = match panic::catch_unwind(|| {
599 state.calculate_absolute_max_short(
600 state.calculate_spot_price()?,
601 checkpoint_exposure,
602 None,
603 )
604 }) {
605 Ok(max_bond_amount) => match max_bond_amount {
606 Ok(max_bond_amount) => max_bond_amount,
607 Err(_) => continue, },
609 Err(_) => continue, };
611 if max_bond_amount < state.minimum_transaction_amount() + fixed!(1) {
612 continue;
613 }
614 let bond_amount = rng.gen_range(state.minimum_transaction_amount()..=max_bond_amount);
615 let rust_pool_delta = state.calculate_pool_share_delta_after_open_short(bond_amount);
616 let curve_fee_base = state.open_short_curve_fee(bond_amount)?;
617 let gov_fee_base =
618 state.open_short_governance_fee(bond_amount, Some(curve_fee_base))?;
619 let fees = curve_fee_base.div_up(state.vault_share_price())
620 - gov_fee_base.div_up(state.vault_share_price());
621 match chain
622 .mock_hyperdrive_math()
623 .calculate_open_short(
624 state.effective_share_reserves()?.into(),
625 state.bond_reserves().into(),
626 bond_amount.into(),
627 state.time_stretch().into(),
628 state.vault_share_price().into(),
629 state.initial_vault_share_price().into(),
630 )
631 .call()
632 .await
633 {
634 Ok(sol_pool_delta) => {
635 let sol_pool_delta_with_fees = FixedPoint::from(sol_pool_delta) - fees;
636 let rust_pool_delta_unwrapped = rust_pool_delta.unwrap();
637 let result_equal = sol_pool_delta_with_fees
638 <= rust_pool_delta_unwrapped + test_tolerance
639 && sol_pool_delta_with_fees >= rust_pool_delta_unwrapped - test_tolerance;
640 assert!(result_equal, "Should be equal.");
641 }
642 Err(_) => {
643 assert!(rust_pool_delta.is_err())
644 }
645 };
646 }
647 Ok(())
648 }
649
650 #[tokio::test]
651 async fn test_sol_calculate_short_principal() -> Result<()> {
652 let chain = TestChain::new().await?;
655 let mut rng = thread_rng();
656 let state = rng.gen::<State>();
657 let bond_amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18));
658 let actual = state.calculate_short_principal(bond_amount);
659 match chain
660 .mock_yield_space_math()
661 .calculate_shares_out_given_bonds_in_down_safe(
662 state.effective_share_reserves()?.into(),
663 state.bond_reserves().into(),
664 bond_amount.into(),
665 (fixed!(1e18) - state.time_stretch()).into(),
666 state.vault_share_price().into(),
667 state.initial_vault_share_price().into(),
668 )
669 .call()
670 .await
671 {
672 Ok((expected, expected_status)) => {
673 assert_eq!(actual.is_ok(), expected_status);
674 assert_eq!(actual.unwrap_or(fixed!(0)), expected.into());
675 }
676 Err(_) => assert!(actual.is_err()),
677 }
678 Ok(())
679 }
680
681 #[tokio::test]
685 async fn fuzz_calculate_short_principal_derivative() -> Result<()> {
686 let empirical_derivative_epsilon = fixed!(1e14);
689 let test_tolerance = fixed!(1e14);
690
691 let mut rng = thread_rng();
692 for _ in 0..*FAST_FUZZ_RUNS {
693 let state = rng.gen::<State>();
694
695 let bond_amount = rng.gen_range(fixed!(1e18)..=fixed!(10_000_000e18));
697
698 let f_x = match panic::catch_unwind(|| state.calculate_short_principal(bond_amount)) {
700 Ok(result) => match result {
701 Ok(result) => result,
702 Err(_) => continue, },
704 Err(_) => continue, };
706 let f_x_plus_delta = match panic::catch_unwind(|| {
707 state.calculate_short_principal(bond_amount + empirical_derivative_epsilon)
708 }) {
709 Ok(result) => match result {
710 Ok(result) => result,
711 Err(_) => continue, },
713 Err(_) => continue, };
715
716 assert!(f_x_plus_delta > f_x);
718
719 let empirical_derivative = (f_x_plus_delta - f_x) / empirical_derivative_epsilon;
721 let short_principal_derivative =
722 state.calculate_short_principal_derivative(bond_amount)?;
723
724 let derivative_diff = if short_principal_derivative >= empirical_derivative {
726 short_principal_derivative - empirical_derivative
727 } else {
728 empirical_derivative - short_principal_derivative
729 };
730 assert!(
731 derivative_diff < test_tolerance,
732 "expected abs(derivative_diff={}) < test_tolerance={};
733 calculated_derivative={}, emperical_derivative={}",
734 derivative_diff,
735 test_tolerance,
736 short_principal_derivative,
737 empirical_derivative
738 );
739 }
740
741 Ok(())
742 }
743
744 #[tokio::test]
748 async fn fuzz_calculate_open_short_derivative() -> Result<()> {
749 let empirical_derivative_epsilon = fixed!(1e14);
752 let test_tolerance = fixed!(1e14);
753
754 let mut rng = thread_rng();
755 for _ in 0..*FAST_FUZZ_RUNS {
756 let state = rng.gen::<State>();
757 let bond_amount = rng.gen_range(fixed!(1e18)..=fixed!(10_000_000e18));
759
760 let f_x = match panic::catch_unwind(|| {
762 state.calculate_open_short(bond_amount, state.vault_share_price())
763 }) {
764 Ok(result) => match result {
765 Ok(result) => result,
766 Err(_) => continue, },
768 Err(_) => continue, };
770 let f_x_plus_delta = match panic::catch_unwind(|| {
771 state.calculate_open_short(
772 bond_amount + empirical_derivative_epsilon,
773 state.vault_share_price(),
774 )
775 }) {
776 Ok(result) => match result {
777 Ok(result) => result,
778 Err(_) => continue, },
780 Err(_) => continue, };
782
783 assert!(f_x_plus_delta > f_x);
785
786 let empirical_derivative = (f_x_plus_delta - f_x) / empirical_derivative_epsilon;
789 let short_deposit_derivative = state.calculate_open_short_derivative(
790 bond_amount,
791 state.vault_share_price(),
792 Some(state.calculate_spot_price()?),
793 )?;
794
795 let derivative_diff = if short_deposit_derivative >= empirical_derivative {
797 short_deposit_derivative - empirical_derivative
798 } else {
799 empirical_derivative - short_deposit_derivative
800 };
801 assert!(
802 derivative_diff < test_tolerance,
803 "expected abs(derivative_diff={}) < test_tolerance={};
804 calculated_derivative={}, emperical_derivative={}",
805 derivative_diff,
806 test_tolerance,
807 short_deposit_derivative,
808 empirical_derivative
809 );
810 }
811
812 Ok(())
813 }
814
815 #[tokio::test]
816 async fn fuzz_sol_calculate_spot_price_after_short() -> Result<()> {
817 let test_tolerance = fixed!(1e3);
818
819 let mut rng = thread_rng();
825 let chain = TestChain::new().await?;
826 let mut alice = chain.alice().await?;
827 let mut bob = chain.bob().await?;
828
829 for _ in 0..*FUZZ_RUNS {
830 let id = chain.snapshot().await?;
832
833 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
835 let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
836 let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
837 alice.fund(contribution).await?;
838 bob.fund(budget).await?;
839
840 alice.initialize(fixed_rate, contribution, None).await?;
842
843 let mut state = alice.get_state().await?;
845 let bond_amount = rng.gen_range(
846 state.minimum_transaction_amount()..=bob.calculate_max_short(None).await?,
847 );
848
849 bob.open_short(bond_amount, None, None).await?;
851
852 let new_state = alice.get_state().await?;
857 let new_vault_share_price = new_state.vault_share_price();
858 state.info.vault_share_price = new_vault_share_price.into();
859
860 let expected_spot_price = state.calculate_spot_price_after_short(bond_amount, None)?;
863 let actual_spot_price = new_state.calculate_spot_price()?;
864 let abs_spot_price_diff = if actual_spot_price >= expected_spot_price {
865 actual_spot_price - expected_spot_price
866 } else {
867 expected_spot_price - actual_spot_price
868 };
869 assert!(
870 abs_spot_price_diff <= test_tolerance,
871 "expected abs(spot_price_diff={}) <= test_tolerance={};
872 calculated_spot_price={}, actual_spot_price={}",
873 abs_spot_price_diff,
874 test_tolerance,
875 expected_spot_price,
876 actual_spot_price,
877 );
878 chain.revert(id).await?;
880 alice.reset(Default::default()).await?;
881 bob.reset(Default::default()).await?;
882 }
883
884 Ok(())
885 }
886
887 #[tokio::test]
888 async fn test_defaults_calculate_spot_price_after_short() -> Result<()> {
889 let mut rng = thread_rng();
890 let mut num_checks = 0;
891 for _ in 0..*SLOW_FUZZ_RUNS {
894 let state = rng.gen::<State>();
898 let checkpoint_exposure = rng
899 .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
900 .raw()
901 .flip_sign_if(rng.gen());
902 let max_bond_amount = match panic::catch_unwind(|| {
904 state.calculate_absolute_max_short(
905 state.calculate_spot_price()?,
906 checkpoint_exposure,
907 Some(3),
908 )
909 }) {
910 Ok(max_bond_amount) => match max_bond_amount {
911 Ok(max_bond_amount) => max_bond_amount,
912 Err(_) => continue, },
914 Err(_) => continue, };
916 if max_bond_amount == fixed!(0) {
917 continue;
918 }
919 let bond_amount = rng.gen_range(state.minimum_transaction_amount()..=max_bond_amount);
921 let price_with_default = state.calculate_spot_price_after_short(bond_amount, None)?;
922
923 let base_amount = match state.calculate_pool_share_delta_after_open_short(bond_amount) {
925 Ok(share_amount) => Some(share_amount * state.vault_share_price()),
926 Err(_) => continue,
927 };
928 let price_with_base_amount =
929 state.calculate_spot_price_after_short(bond_amount, base_amount)?;
930
931 assert_eq!(
933 price_with_default, price_with_base_amount,
934 "`calculate_spot_price_after_short` is not handling default base_amount correctly."
935 );
936 num_checks += 1
937 }
938 assert!(num_checks > 0);
940 Ok(())
941 }
942
943 #[tokio::test]
944 async fn fuzz_calculate_implied_rate() -> Result<()> {
945 let tolerance = int256!(1e12);
946
947 let mut rng = thread_rng();
949 let chain = TestChain::new().await?;
950 let mut alice = chain.alice().await?;
951 let mut bob = chain.bob().await?;
952
953 for _ in 0..*FUZZ_RUNS {
954 let id = chain.snapshot().await?;
956
957 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
959 let contribution = rng.gen_range(fixed!(100_000e18)..=fixed!(100_000_000e18));
960 let budget = fixed!(100_000_000e18);
961 alice.fund(contribution).await?;
962 bob.fund(budget).await?;
963
964 alice.initialize(fixed_rate, contribution, None).await?;
967
968 let variable_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(1e18));
970 alice.advance_time(variable_rate, 12.into()).await?;
971
972 let bond_amount = rng.gen_range(
975 FixedPoint::from(bob.get_config().minimum_transaction_amount)
976 ..=bob.calculate_max_short(None).await? * fixed!(0.9e18),
977 );
978 let implied_rate = bob.get_state().await?.calculate_implied_rate(
979 bond_amount,
980 bob.get_state().await?.vault_share_price(),
981 variable_rate,
982 )?;
983 let (maturity_time, base_paid) = bob.open_short(bond_amount, None, None).await?;
984
985 chain
987 .increase_time(bob.get_config().position_duration.low_u128())
988 .await?;
989
990 let base_proceeds = bob.close_short(maturity_time, bond_amount, None).await?;
992 let annualized_position_duration =
993 bob.get_state().await?.annualized_position_duration();
994
995 let realized_rate = if base_proceeds > base_paid {
998 I256::try_from(
999 (base_proceeds - base_paid) / (base_paid * annualized_position_duration),
1000 )?
1001 } else {
1002 -I256::try_from(
1003 (base_paid - base_proceeds) / (base_paid * annualized_position_duration),
1004 )?
1005 };
1006 let error = (implied_rate - realized_rate).abs();
1007 let scaled_tolerance = if implied_rate > int256!(1e18) {
1008 I256::from(tolerance * implied_rate)
1009 } else {
1010 tolerance
1011 };
1012 assert!(
1013 error < scaled_tolerance,
1014 "error {:?} exceeds tolerance of {} (scaled to {})",
1015 error,
1016 tolerance,
1017 scaled_tolerance
1018 );
1019
1020 chain.revert(id).await?;
1022 alice.reset(Default::default()).await?;
1023 bob.reset(Default::default()).await?;
1024 }
1025
1026 Ok(())
1027 }
1028
1029 #[tokio::test]
1031 async fn test_error_open_short_min_txn_amount() -> Result<()> {
1032 let min_bond_delta = fixed!(1);
1033
1034 let mut rng = thread_rng();
1035 let state = rng.gen::<State>();
1036 let result = state.calculate_open_short(
1037 state.minimum_transaction_amount() - min_bond_delta,
1038 state.vault_share_price(),
1039 );
1040 assert!(result.is_err());
1041 Ok(())
1042 }
1043
1044 #[tokio::test]
1046 async fn fuzz_error_open_short_max_txn_amount() -> Result<()> {
1047 let max_bond_delta = fixed!(1_000_000_000e18);
1052
1053 let mut rng = thread_rng();
1054 for _ in 0..*FAST_FUZZ_RUNS {
1055 let state = rng.gen::<State>();
1056 let checkpoint_exposure = rng
1057 .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
1058 .raw()
1059 .flip_sign_if(rng.gen());
1060 let max_iterations = 7;
1061 let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
1062 let max_trade = panic::catch_unwind(|| {
1064 state.calculate_absolute_max_short(
1065 state.calculate_spot_price()?,
1066 checkpoint_exposure,
1067 Some(max_iterations),
1068 )
1069 });
1070 match max_trade {
1073 Ok(max_trade) => match max_trade {
1074 Ok(max_trade) => {
1075 let bond_amount = max_trade + max_bond_delta;
1076 let base_amount = panic::catch_unwind(|| {
1077 state.calculate_open_short(bond_amount, open_vault_share_price)
1078 });
1079 match base_amount {
1080 Ok(result) => match result {
1081 Ok(base_amount) => {
1082 return Err(eyre!(format!(
1083 "calculate_open_short on bond_amount={:#?} > max_bond_amount={:#?} \
1084 returned base_amount={:#?}, but should have failed.",
1085 bond_amount,
1086 max_trade,
1087 base_amount,
1088 )));
1089 }
1090 Err(_) => continue, },
1092 Err(_) => continue, }
1094 }
1095 Err(_) => continue, },
1097 Err(_) => continue, }
1099 }
1100
1101 Ok(())
1102 }
1103
1104 #[tokio::test]
1105 pub async fn fuzz_sol_calculate_open_short() -> Result<()> {
1106 let mut rng = {
1110 let mut rng = thread_rng();
1111 let seed = rng.gen();
1112 ChaCha8Rng::seed_from_u64(seed)
1113 };
1114
1115 let chain = TestChain::new().await?;
1117 let mut alice = chain.alice().await?;
1118 let mut bob = chain.bob().await?;
1119 let mut celine = chain.celine().await?;
1120
1121 for _ in 0..*FUZZ_RUNS {
1122 let id = chain.snapshot().await?;
1124
1125 initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
1127
1128 let mut state = alice.get_state().await?;
1130 let min_txn_amount = state.minimum_transaction_amount();
1131 let max_short = celine.calculate_max_short(None).await?;
1132 let bond_amount = rng.gen_range(min_txn_amount..=max_short);
1133
1134 celine.fund(bond_amount).await?;
1136
1137 match celine
1139 .hyperdrive()
1140 .open_short(
1141 bond_amount.into(),
1142 FixedPoint::from(U256::MAX).into(),
1143 fixed!(0).into(),
1144 Options {
1145 destination: celine.address(),
1146 as_base: true,
1147 extra_data: [].into(),
1148 },
1149 )
1150 .call()
1151 .await
1152 {
1153 Ok((_, sol_base)) => {
1154 let vault_share_price = alice.get_state().await?.vault_share_price();
1161 state.info.vault_share_price = vault_share_price.into();
1162
1163 let Checkpoint {
1165 weighted_spot_price: _,
1166 last_weighted_spot_price_update_time: _,
1167 vault_share_price: open_vault_share_price,
1168 } = alice
1169 .get_checkpoint(state.to_checkpoint(alice.now().await?))
1170 .await?;
1171
1172 let rust_base =
1174 state.calculate_open_short(bond_amount, open_vault_share_price.into());
1175
1176 let rust_base_unwrapped = rust_base.unwrap();
1178 let sol_base_fp = FixedPoint::from(sol_base);
1179 assert_eq!(
1180 rust_base_unwrapped, sol_base_fp,
1181 "expected rust_base={:#?} == sol_base={:#?}",
1182 rust_base_unwrapped, sol_base_fp
1183 );
1184 }
1185 Err(sol_err) => {
1186 let vault_share_price = alice.get_state().await?.vault_share_price();
1188 state.info.vault_share_price = vault_share_price.into();
1189
1190 let Checkpoint {
1192 weighted_spot_price: _,
1193 last_weighted_spot_price_update_time: _,
1194 vault_share_price: open_vault_share_price,
1195 } = alice
1196 .get_checkpoint(state.to_checkpoint(alice.now().await?))
1197 .await?;
1198
1199 let rust_base =
1201 state.calculate_open_short(bond_amount, open_vault_share_price.into());
1202
1203 assert!(
1205 rust_base.is_err(),
1206 "sol_err={:#?}, but rust_base={:#?} did not error",
1207 sol_err,
1208 rust_base,
1209 );
1210 }
1211 }
1212
1213 chain.revert(id).await?;
1215 alice.reset(Default::default()).await?;
1216 bob.reset(Default::default()).await?;
1217 celine.reset(Default::default()).await?;
1218 }
1219
1220 Ok(())
1221 }
1222
1223 #[tokio::test]
1224 async fn fuzz_calculate_approximate_short_bonds_given_deposit() -> Result<()> {
1225 let mut rng = thread_rng();
1226 for _ in 0..*FAST_FUZZ_RUNS {
1227 let state = rng.gen::<State>();
1228 let open_vault_share_price = rng.gen_range(fixed!(1e5)..=state.vault_share_price());
1229 let checkpoint_exposure = {
1230 let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX)));
1231 if rng.gen() {
1232 -I256::try_from(value)?
1233 } else {
1234 I256::try_from(value)?
1235 }
1236 };
1237 match get_max_short(state.clone(), checkpoint_exposure, None) {
1238 Ok(max_short_bonds) => {
1239 let bond_amount =
1240 rng.gen_range(state.minimum_transaction_amount()..=max_short_bonds);
1241 let base_amount =
1242 state.calculate_open_short(bond_amount, open_vault_share_price)?;
1243 let approximate_bond_amount = state
1246 .calculate_approximate_short_bonds_given_base_deposit(
1247 base_amount,
1248 open_vault_share_price,
1249 )?;
1250 assert!(
1253 approximate_bond_amount <= bond_amount,
1254 "approximate_bond_amount={:#?} not <= bond_amount={:#?}",
1255 approximate_bond_amount,
1256 bond_amount
1257 );
1258 match state.calculate_open_short(approximate_bond_amount, open_vault_share_price) {
1262 Ok(approximate_base_amount) => {
1263 assert!(
1264 approximate_base_amount <= base_amount,
1265 "approximate_base_amount={:#?} not <= base_amount={:#?}",
1266 approximate_base_amount,
1267 base_amount
1268 );
1269 }
1270 Err(_) => assert!(
1271 false,
1272 "Failed to run calculate_open_short with the approximate bond amount = {:#?}.",
1273 approximate_bond_amount
1274 ),
1275 };
1276 }
1277 Err(_) => continue,
1278 }
1279 }
1280 Ok(())
1281 }
1282}