1use ethers::types::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_long<F: Into<FixedPoint<U256>>>(
28 &self,
29 base_amount: F,
30 ) -> Result<FixedPoint<U256>> {
31 let base_amount = base_amount.into();
32
33 if base_amount < self.minimum_transaction_amount() {
34 return Err(eyre!("MinimumTransactionAmount: Input amount too low",));
35 }
36
37 let bond_amount =
38 self.calculate_bonds_out_given_shares_in_down(base_amount / self.vault_share_price())?;
39
40 let ending_spot_price =
42 self.calculate_spot_price_after_long(base_amount, bond_amount.into())?;
43 let max_spot_price = self.calculate_max_spot_price()?;
44 if ending_spot_price > max_spot_price {
45 return Err(eyre!("InsufficientLiquidity: Negative Interest",));
46 }
47
48 Ok(bond_amount - self.open_long_curve_fee(base_amount)?)
49 }
50
51 pub(super) fn calculate_open_long_derivative(
81 &self,
82 base_amount: FixedPoint<U256>,
83 ) -> Result<FixedPoint<U256>> {
84 let share_amount = base_amount / self.vault_share_price();
85 let inner =
86 self.initial_vault_share_price() * (self.effective_share_reserves()? + share_amount);
87 let mut derivative = fixed!(1e18) / (inner).pow(self.time_stretch())?;
88
89 let k = self.k_down()?;
93 let rhs = self.vault_share_price().mul_div_down(
94 inner.pow(self.time_stretch())?,
95 self.initial_vault_share_price(),
96 );
97 if k < rhs {
98 return Err(eyre!("Open long derivative is undefined."));
99 }
100 derivative *= (k - rhs).pow(
101 self.time_stretch()
102 .div_up(fixed!(1e18) - self.time_stretch()),
103 )?;
104
105 derivative -=
107 self.curve_fee() * ((fixed!(1e18) / self.calculate_spot_price()?) - fixed!(1e18));
108
109 Ok(derivative)
110 }
111
112 pub fn calculate_pool_state_after_open_long(
119 &self,
120 base_amount: FixedPoint<U256>,
121 maybe_bond_delta: Option<FixedPoint<U256>>,
122 ) -> Result<Self> {
123 let (share_delta, bond_delta) =
124 self.calculate_pool_deltas_after_open_long(base_amount, maybe_bond_delta)?;
125 let mut state = self.clone();
126 state.info.bond_reserves -= bond_delta.into();
127 state.info.share_reserves += share_delta.into();
128 Ok(state)
129 }
130
131 pub fn calculate_pool_deltas_after_open_long(
133 &self,
134 base_amount: FixedPoint<U256>,
135 maybe_bond_delta: Option<FixedPoint<U256>>,
136 ) -> Result<(FixedPoint<U256>, FixedPoint<U256>)> {
137 let bond_delta = match maybe_bond_delta {
138 Some(delta) => delta,
139 None => self.calculate_open_long(base_amount)?,
140 };
141 let total_gov_curve_fee_shares = self
142 .open_long_governance_fee(base_amount, None)?
143 .div_down(self.vault_share_price());
144 let share_delta =
145 base_amount.div_down(self.vault_share_price()) - total_gov_curve_fee_shares;
146 Ok((share_delta, bond_delta))
147 }
148
149 pub fn calculate_spot_price_after_long(
152 &self,
153 base_amount: FixedPoint<U256>,
154 maybe_bond_pool_delta: Option<FixedPoint<U256>>,
155 ) -> Result<FixedPoint<U256>> {
156 let state =
157 self.calculate_pool_state_after_open_long(base_amount, maybe_bond_pool_delta)?;
158 state.calculate_spot_price()
159 }
160
161 pub fn calculate_spot_rate_after_long(
177 &self,
178 base_amount: FixedPoint<U256>,
179 maybe_bond_amount: Option<FixedPoint<U256>>,
180 ) -> Result<FixedPoint<U256>> {
181 Ok(calculate_rate_given_fixed_price(
182 self.calculate_spot_price_after_long(base_amount, maybe_bond_amount)?,
183 self.position_duration(),
184 ))
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use std::panic;
191
192 use ethers::types::{I256, U256};
193 use fixedpointmath::{fixed, fixed_u256, FixedPointValue};
194 use hyperdrive_test_utils::{
195 chain::TestChain,
196 constants::{FAST_FUZZ_RUNS, FUZZ_RUNS, SLOW_FUZZ_RUNS},
197 };
198 use hyperdrive_wrappers::wrappers::ihyperdrive::Options;
199 use rand::{thread_rng, Rng, SeedableRng};
200 use rand_chacha::ChaCha8Rng;
201
202 use super::*;
203 use crate::test_utils::{
204 agent::HyperdriveMathAgent, preamble::initialize_pool_with_random_state,
205 };
206
207 #[tokio::test]
208 async fn fuzz_calculate_pool_state_after_open_long() -> Result<()> {
209 let share_adjustment_test_tolerance = fixed_u256!(0);
211 let bond_reserves_test_tolerance = fixed!(1e10);
212 let share_reserves_test_tolerance = fixed!(1e10);
213 let chain = TestChain::new().await?;
215 let mut alice = chain.alice().await?;
216 let mut bob = chain.bob().await?;
217 let mut celine = chain.celine().await?;
218 let mut rng = {
222 let mut rng = thread_rng();
223 let seed = rng.gen();
224 ChaCha8Rng::seed_from_u64(seed)
225 };
226 for _ in 0..*SLOW_FUZZ_RUNS {
227 let id = chain.snapshot().await?;
229 initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
230 alice.advance_time(fixed!(0), fixed!(0)).await?;
232 let original_state = alice.get_state().await?;
233 let checkpoint_exposure = alice
235 .get_checkpoint_exposure(original_state.to_checkpoint(alice.now().await?))
236 .await?;
237 let max_long_amount =
238 original_state.calculate_max_long(U256::MAX, checkpoint_exposure, None)?;
239 let base_amount =
240 rng.gen_range(original_state.minimum_transaction_amount()..=max_long_amount);
241 let rust_state =
243 original_state.calculate_pool_state_after_open_long(base_amount, None)?;
244 bob.fund(base_amount * fixed!(1.5e18)).await?;
246 bob.open_long(base_amount, None, None).await?;
247 let sol_state = alice.get_state().await?;
248 let rust_share_adjustment = rust_state.share_adjustment();
250 let sol_share_adjustment = sol_state.share_adjustment();
251 let share_adjustment_error = if rust_share_adjustment < sol_share_adjustment {
252 FixedPoint::try_from(sol_share_adjustment - rust_share_adjustment)?
253 } else {
254 FixedPoint::try_from(rust_share_adjustment - sol_share_adjustment)?
255 };
256 assert!(
257 share_adjustment_error <= share_adjustment_test_tolerance,
258 "expected abs(rust_share_adjustment={}-sol_share_adjustment={})={} <= test_tolerance={}",
259 rust_share_adjustment, sol_share_adjustment, share_adjustment_error, share_adjustment_test_tolerance
260 );
261 let rust_bond_reserves = rust_state.bond_reserves();
262 let sol_bond_reserves = sol_state.bond_reserves();
263 let bond_reserves_error = if rust_bond_reserves < sol_bond_reserves {
264 sol_bond_reserves - rust_bond_reserves
265 } else {
266 rust_bond_reserves - sol_bond_reserves
267 };
268 assert!(
269 bond_reserves_error <= bond_reserves_test_tolerance,
270 "expected abs(rust_bond_reserves={}-sol_bond_reserves={})={} <= test_tolerance={}",
271 rust_bond_reserves,
272 sol_bond_reserves,
273 bond_reserves_error,
274 bond_reserves_test_tolerance
275 );
276 let rust_share_reserves = rust_state.share_reserves();
277 let sol_share_reserves = sol_state.share_reserves();
278 let share_reserves_error = if rust_share_reserves < sol_share_reserves {
279 sol_share_reserves - rust_share_reserves
280 } else {
281 rust_share_reserves - sol_share_reserves
282 };
283 assert!(
284 share_reserves_error <= share_reserves_test_tolerance,
285 "expected abs(rust_share_reserves={}-sol_share_reserves={})={} <= test_tolerance={}",
286 rust_share_reserves,
287 sol_share_reserves,
288 share_reserves_error,
289 share_reserves_test_tolerance
290 );
291 chain.revert(id).await?;
293 alice.reset(Default::default()).await?;
294 bob.reset(Default::default()).await?;
295 celine.reset(Default::default()).await?;
296 }
297 Ok(())
298 }
299
300 #[tokio::test]
301 async fn fuzz_calculate_spot_price_after_long() -> Result<()> {
302 let mut rng = thread_rng();
308 let chain = TestChain::new().await?;
309 let mut alice = chain.alice().await?;
310 let mut bob = chain.bob().await?;
311
312 for _ in 0..*FUZZ_RUNS {
313 let id = chain.snapshot().await?;
315
316 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
318 let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
319 let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
320 alice.fund(contribution).await?;
321 bob.fund(budget).await?;
322
323 alice.initialize(fixed_rate, contribution, None).await?;
325
326 let base_paid = rng.gen_range(fixed!(0.1e18)..=bob.calculate_max_long(None).await?);
328 let expected_spot_price = bob
329 .get_state()
330 .await?
331 .calculate_spot_price_after_long(base_paid, None)?;
332
333 bob.open_long(base_paid, None, None).await?;
335
336 let actual_spot_price = bob.get_state().await?.calculate_spot_price()?;
340 let delta = if actual_spot_price > expected_spot_price {
341 actual_spot_price - expected_spot_price
342 } else {
343 expected_spot_price - actual_spot_price
344 };
345 let tolerance = fixed!(1e9);
346 assert!(
347 delta < tolerance,
348 "expected: delta = {} < {} = tolerance",
349 delta,
350 tolerance
351 );
352
353 chain.revert(id).await?;
355 alice.reset(Default::default()).await?;
356 bob.reset(Default::default()).await?;
357 }
358 Ok(())
359 }
360
361 #[tokio::test]
362 async fn fuzz_calculate_spot_rate_after_long() -> Result<()> {
363 let mut rng = thread_rng();
369 let chain = TestChain::new().await?;
370 let mut alice = chain.alice().await?;
371 let mut bob = chain.bob().await?;
372
373 for _ in 0..*FUZZ_RUNS {
374 let id = chain.snapshot().await?;
376
377 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
379 let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
380 let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
381 alice.fund(contribution).await?;
382 bob.fund(budget).await?;
383
384 alice.initialize(fixed_rate, contribution, None).await?;
386
387 let base_paid = rng.gen_range(
389 alice.get_state().await?.minimum_transaction_amount()
390 ..=bob.calculate_max_long(None).await?,
391 );
392 let expected_spot_rate = bob
393 .get_state()
394 .await?
395 .calculate_spot_rate_after_long(base_paid, None)?;
396
397 bob.open_long(base_paid, None, None).await?;
399
400 let actual_spot_rate = bob.get_state().await?.calculate_spot_rate()?;
404 let delta = if actual_spot_rate > expected_spot_rate {
405 actual_spot_rate - expected_spot_rate
406 } else {
407 expected_spot_rate - actual_spot_rate
408 };
409 let tolerance = fixed!(1e9);
410 assert!(
411 delta < tolerance,
412 "expected: delta = {} < {} = tolerance",
413 delta,
414 tolerance
415 );
416
417 chain.revert(id).await?;
419 alice.reset(Default::default()).await?;
420 bob.reset(Default::default()).await?;
421 }
422 Ok(())
423 }
424
425 #[tokio::test]
427 async fn test_error_open_long_min_txn_amount() -> Result<()> {
428 let mut rng = thread_rng();
429 let state = rng.gen::<State>();
430 let result = state.calculate_open_long(state.config.minimum_transaction_amount - 10);
431 assert!(result.is_err());
432 Ok(())
433 }
434
435 #[tokio::test]
437 async fn fuzz_error_open_long_max_txn_amount() -> Result<()> {
438 let max_base_delta = fixed!(1_000_000_000e18);
443
444 let mut rng = thread_rng();
445 for _ in 0..*FUZZ_RUNS {
446 let state = rng.gen::<State>();
447 let checkpoint_exposure = rng
448 .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
449 .raw()
450 .flip_sign_if(rng.gen());
451 let max_iterations = 7;
452 let max_trade = panic::catch_unwind(|| {
454 state.calculate_max_long(U256::MAX, checkpoint_exposure, Some(max_iterations))
455 });
456 match max_trade {
459 Ok(max_trade) => match max_trade {
460 Ok(max_trade) => {
461 let base_amount = max_trade + max_base_delta;
462 let bond_amount =
463 panic::catch_unwind(|| state.calculate_open_long(base_amount));
464 match bond_amount {
465 Ok(result) => match result {
466 Ok(_) => {
467 return Err(eyre!(
468 format!(
469 "calculate_open_long for {} base should have failed but succeeded.",
470 base_amount,
471 )
472 ));
473 }
474 Err(_) => continue, },
476 Err(_) => continue, }
478 }
479 Err(_) => continue, },
481 Err(_) => continue, }
483 }
484
485 Ok(())
486 }
487
488 #[tokio::test]
489 pub async fn fuzz_sol_calc_open_long() -> Result<()> {
490 let tolerance = fixed!(1e3);
491
492 let mut rng = {
496 let mut rng = thread_rng();
497 let seed = rng.gen();
498 ChaCha8Rng::seed_from_u64(seed)
499 };
500
501 let chain = TestChain::new().await?;
503 let mut alice = chain.alice().await?;
504 let mut bob = chain.bob().await?;
505 let mut celine = chain.celine().await?;
506
507 for _ in 0..*FUZZ_RUNS {
508 let id = chain.snapshot().await?;
510
511 initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
513
514 let mut state = alice.get_state().await?;
516 let min_txn_amount = state.minimum_transaction_amount();
517 let max_long = bob.calculate_max_long(None).await?;
518 let base_amount = rng.gen_range(min_txn_amount..=max_long);
519
520 bob.fund(base_amount + base_amount * fixed!(0.001e18))
522 .await?;
523 match bob
524 .hyperdrive()
525 .open_long(
526 base_amount.into(),
527 fixed!(0).into(),
528 fixed!(0).into(),
529 Options {
530 destination: bob.address(),
531 as_base: true,
532 extra_data: [].into(),
533 },
534 )
535 .call()
536 .await
537 {
538 Ok((_, sol_bonds)) => {
539 let new_vault_share_price = alice.get_state().await?.vault_share_price();
541 state.info.vault_share_price = new_vault_share_price.into();
542 let rust_bonds = state.calculate_open_long(base_amount);
543
544 let rust_bonds_unwrapped = rust_bonds.unwrap();
546 let error = if rust_bonds_unwrapped >= sol_bonds.into() {
547 rust_bonds_unwrapped - FixedPoint::from(sol_bonds)
548 } else {
549 FixedPoint::from(sol_bonds) - rust_bonds_unwrapped
550 };
551 assert!(
552 error <= tolerance,
553 "error {} exceeds tolerance of {}",
554 error,
555 tolerance
556 );
557 }
558 Err(sol_err) => {
559 let new_vault_share_price = alice.get_state().await?.vault_share_price();
561 state.info.vault_share_price = new_vault_share_price.into();
562 let rust_bonds = state.calculate_open_long(base_amount);
563 assert!(
564 rust_bonds.is_err(),
565 "sol_err={:#?}, but rust_bonds={:#?} did not error",
566 sol_err,
567 rust_bonds
568 );
569 }
570 }
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
579 Ok(())
580 }
581
582 #[tokio::test]
587 async fn fuzz_open_long_derivative() -> Result<()> {
588 let mut rng = thread_rng();
589 let empirical_derivative_epsilon = fixed!(1e12);
592 let test_comparison_epsilon = fixed!(10e18);
594
595 for _ in 0..*FAST_FUZZ_RUNS {
596 let state = rng.gen::<State>();
597 let amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18));
598
599 let f_x = match panic::catch_unwind(|| state.calculate_open_long(amount)) {
601 Ok(result) => match result {
602 Ok(result) => result,
603 Err(_) => continue, },
605 Err(_) => continue, };
607
608 let f_x_plus_delta = match panic::catch_unwind(|| {
609 state.calculate_open_long(amount + empirical_derivative_epsilon)
610 }) {
611 Ok(result) => match result {
612 Ok(result) => result,
613 Err(_) => continue,
614 },
615 Err(_) => continue,
617 };
618 assert!(f_x_plus_delta > f_x);
620
621 let empirical_derivative = (f_x_plus_delta - f_x) / empirical_derivative_epsilon;
622 let open_long_derivative = state.calculate_open_long_derivative(amount)?;
623 let derivative_diff = if open_long_derivative >= empirical_derivative {
624 open_long_derivative - empirical_derivative
625 } else {
626 empirical_derivative - open_long_derivative
627 };
628 assert!(
629 derivative_diff < test_comparison_epsilon,
630 "expected (derivative_diff={}) < (test_comparison_epsilon={}), \
631 calculated_derivative={}, emperical_derivative={}",
632 derivative_diff,
633 test_comparison_epsilon,
634 open_long_derivative,
635 empirical_derivative
636 );
637 }
638
639 Ok(())
640 }
641}