1use ethers::types::U256;
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, FixedPoint};
4
5use crate::{State, YieldSpace};
6
7impl State {
8 fn calculate_close_short_flat<F: Into<FixedPoint<U256>>>(
9 &self,
10 bond_amount: F,
11 maturity_time: U256,
12 current_time: U256,
13 ) -> FixedPoint<U256> {
14 let bond_amount = bond_amount.into();
16 let normalized_time_remaining =
17 self.calculate_normalized_time_remaining(maturity_time, current_time);
18 bond_amount.mul_div_up(
19 fixed!(1e18) - normalized_time_remaining,
20 self.vault_share_price(),
21 )
22 }
23
24 fn calculate_close_short_curve<F: Into<FixedPoint<U256>>>(
25 &self,
26 bond_amount: F,
27 maturity_time: U256,
28 current_time: U256,
29 ) -> Result<FixedPoint<U256>> {
30 let bond_amount = bond_amount.into();
31 let normalized_time_remaining =
32 self.calculate_normalized_time_remaining(maturity_time, current_time);
33 if normalized_time_remaining > fixed!(0) {
34 let curve_bonds_in = bond_amount.mul_up(normalized_time_remaining);
38 Ok(self.calculate_shares_in_given_bonds_out_up(curve_bonds_in)?)
39 } else {
40 Ok(fixed!(0))
41 }
42 }
43
44 fn calculate_close_short_flat_plus_curve<F: Into<FixedPoint<U256>>>(
45 &self,
46 bond_amount: F,
47 maturity_time: U256,
48 current_time: U256,
49 ) -> Result<FixedPoint<U256>> {
50 let bond_amount = bond_amount.into();
51 let flat = self.calculate_close_short_flat(bond_amount, maturity_time, current_time);
53 let curve = self.calculate_close_short_curve(bond_amount, maturity_time, current_time)?;
55
56 Ok(flat + curve)
57 }
58
59 pub fn calculate_short_proceeds_up(
85 &self,
86 bond_amount: FixedPoint<U256>,
87 share_amount: FixedPoint<U256>,
88 open_vault_share_price: FixedPoint<U256>,
89 close_vault_share_price: FixedPoint<U256>,
90 ) -> FixedPoint<U256> {
91 let mut total_value = bond_amount
100 .mul_div_up(close_vault_share_price, open_vault_share_price)
101 .div_up(self.vault_share_price());
102
103 total_value += bond_amount.mul_div_up(self.flat_fee(), self.vault_share_price());
108
109 if total_value > share_amount {
114 total_value - share_amount
116 } else {
117 fixed!(0)
118 }
119 }
120
121 fn calculate_short_proceeds_down(
147 &self,
148 bond_amount: FixedPoint<U256>,
149 share_amount: FixedPoint<U256>,
150 open_vault_share_price: FixedPoint<U256>,
151 close_vault_share_price: FixedPoint<U256>,
152 ) -> FixedPoint<U256> {
153 let mut total_value = bond_amount
162 .mul_div_down(close_vault_share_price, open_vault_share_price)
163 .div_down(self.vault_share_price());
164
165 total_value += bond_amount.mul_div_down(self.flat_fee(), self.vault_share_price());
170
171 if total_value > share_amount {
176 total_value - share_amount
178 } else {
179 fixed!(0)
180 }
181 }
182
183 fn calculate_close_short_max_spot_price(&self) -> Result<FixedPoint<U256>> {
194 Ok(fixed!(1e18)
195 - self
196 .curve_fee()
197 .mul_up(fixed!(1e18) - self.calculate_spot_price()?))
198 }
199
200 pub fn calculate_close_short<F: Into<FixedPoint<U256>>>(
202 &self,
203 bond_amount: F,
204 open_vault_share_price: F,
205 close_vault_share_price: F,
206 maturity_time: U256,
207 current_time: U256,
208 ) -> Result<FixedPoint<U256>> {
209 let bond_amount = bond_amount.into();
210 let open_vault_share_price = open_vault_share_price.into();
211 let close_vault_share_price = close_vault_share_price.into();
212
213 if bond_amount < self.config.minimum_transaction_amount.into() {
214 return Err(eyre!("MinimumTransactionAmount: Input amount too low"));
215 }
216
217 let share_curve_delta =
220 self.calculate_close_short_curve(bond_amount, maturity_time, current_time)?;
221 let bond_reserves_delta = bond_amount
222 .mul_up(self.calculate_normalized_time_remaining(maturity_time, current_time));
223 let short_curve_spot_price = {
224 let mut state: State = self.clone();
225 state.info.bond_reserves -= bond_reserves_delta.into();
226 state.info.share_reserves += share_curve_delta.into();
227 state.calculate_spot_price()?
228 };
229 let max_spot_price = self.calculate_close_short_max_spot_price()?;
230 if short_curve_spot_price > max_spot_price {
231 return Err(eyre!("InsufficientLiquidity: Negative Interest"));
232 }
233
234 let curve_fee = self.close_short_curve_fee(bond_amount, maturity_time, current_time)?;
236 let share_curve_delta_with_fees = share_curve_delta + curve_fee
237 - self.close_short_governance_fee(
238 bond_amount,
239 maturity_time,
240 current_time,
241 Some(curve_fee),
242 )?;
243 let share_curve_delta_with_fees_spot_price = {
244 let mut state: State = self.clone();
245 state.info.bond_reserves -= bond_reserves_delta.into();
246 state.info.share_reserves += share_curve_delta_with_fees.into();
247 state.calculate_spot_price()?
248 };
249 if share_curve_delta_with_fees_spot_price > fixed!(1e18) {
250 return Err(eyre!("InsufficientLiquidity: Negative Interest"));
251 }
252
253 let share_reserves_delta =
258 self.calculate_close_short_flat_plus_curve(bond_amount, maturity_time, current_time)?;
259 let share_reserves_delta_with_fees = share_reserves_delta
261 + self.close_short_curve_fee(bond_amount, maturity_time, current_time)?
262 + self.close_short_flat_fee(bond_amount, maturity_time, current_time);
263
264 Ok(self.calculate_short_proceeds_down(
266 bond_amount,
267 share_reserves_delta_with_fees,
268 open_vault_share_price,
269 close_vault_share_price,
270 ))
271 }
272
273 pub fn calculate_market_value_short<F: Into<FixedPoint<U256>>>(
285 &self,
286 bond_amount: F,
287 open_vault_share_price: F,
288 close_vault_share_price: F,
289 maturity_time: U256,
290 current_time: U256,
291 ) -> Result<FixedPoint<U256>> {
292 let bond_amount = bond_amount.into();
293 let open_vault_share_price = open_vault_share_price.into();
294 let close_vault_share_price = close_vault_share_price.into();
295
296 let spot_price = self.calculate_spot_price()?;
297 if spot_price > fixed!(1e18) {
298 return Err(eyre!("Negative fixed interest!"));
299 }
300
301 let time_remaining = self.calculate_normalized_time_remaining(maturity_time, current_time);
303
304 let flat = self.calculate_close_short_flat(bond_amount, maturity_time, current_time);
306
307 let curve = bond_amount
309 .mul_up(spot_price)
310 .mul_up(time_remaining)
311 .div_up(self.vault_share_price());
312 let flat_fees_paid = self.close_short_flat_fee(bond_amount, maturity_time, current_time);
313 let curve_fees_paid =
314 self.close_short_curve_fee(bond_amount, maturity_time, current_time)?;
315
316 let share_reserves_delta = flat + curve;
318 let share_reserves_delta_with_fees =
319 share_reserves_delta + flat_fees_paid + curve_fees_paid;
320
321 Ok(self.calculate_short_proceeds_down(
324 bond_amount,
325 share_reserves_delta_with_fees,
326 open_vault_share_price,
327 close_vault_share_price,
328 ))
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use std::panic;
335
336 use ethers::types::I256;
337 use fixedpointmath::int256;
338 use hyperdrive_test_utils::{chain::TestChain, constants::FAST_FUZZ_RUNS};
339 use rand::{thread_rng, Rng};
340
341 use super::*;
342
343 #[tokio::test]
344 async fn fuzz_calculate_close_short_after_maturity() -> Result<()> {
345 let mut rng = thread_rng();
348 for _ in 0..*FAST_FUZZ_RUNS {
349 let state = rng.gen::<State>();
350 let in_ = rng.gen_range(fixed!(0)..=state.effective_share_reserves()?);
351 let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
352 let maturity_time = state.position_duration();
356 let just_after_maturity = maturity_time + state.checkpoint_duration();
358 let base_earned_just_after_maturity = state.calculate_close_short(
359 in_,
360 open_vault_share_price,
361 state.vault_share_price(),
362 maturity_time.into(),
363 just_after_maturity.into(),
364 )? * state.vault_share_price();
365 let well_after_maturity = just_after_maturity + fixed!(1e10);
367 let base_earned_well_after_maturity = state.calculate_close_short(
368 in_,
369 open_vault_share_price,
370 state.vault_share_price(),
371 maturity_time.into(),
372 well_after_maturity.into(),
373 )? * state.vault_share_price();
374 assert!(
376 base_earned_well_after_maturity == base_earned_just_after_maturity,
377 "Trader should not have earned any more after maturity:
378 earned_well_after_maturity={:?} != earned_just_after_maturity={:?}",
379 base_earned_well_after_maturity,
380 base_earned_just_after_maturity
381 );
382 }
383 Ok(())
384 }
385
386 #[tokio::test]
387 async fn fuzz_sol_calculate_short_proceeds_up() -> Result<()> {
388 let chain = TestChain::new().await?;
389
390 let mut rng = thread_rng();
392 for _ in 0..*FAST_FUZZ_RUNS {
393 let state = rng.gen::<State>();
394 let bond_amount = rng.gen_range(fixed!(0)..=state.bond_reserves());
395 let share_amount = rng.gen_range(fixed!(0)..=bond_amount);
396 let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
397 let actual = panic::catch_unwind(|| {
398 state.calculate_short_proceeds_up(
399 bond_amount,
400 share_amount,
401 open_vault_share_price,
402 state.vault_share_price(),
403 )
404 });
405 match chain
406 .mock_hyperdrive_math()
407 .calculate_short_proceeds_up(
408 bond_amount.into(),
409 share_amount.into(),
410 open_vault_share_price.into(),
411 state.vault_share_price().into(),
412 state.vault_share_price().into(),
413 state.flat_fee().into(),
414 )
415 .call()
416 .await
417 {
418 Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected)),
419 Err(_) => assert!(actual.is_err()),
420 }
421 }
422
423 Ok(())
424 }
425
426 #[tokio::test]
427 async fn fuzz_sol_calculate_short_proceeds_down() -> Result<()> {
428 let chain = TestChain::new().await?;
429
430 let mut rng = thread_rng();
432 for _ in 0..*FAST_FUZZ_RUNS {
433 let state = rng.gen::<State>();
434 let bond_amount = rng.gen_range(fixed!(0)..=state.bond_reserves());
435 let share_amount = rng.gen_range(fixed!(0)..=bond_amount);
436 let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
437 let actual = panic::catch_unwind(|| {
438 state.calculate_short_proceeds_down(
439 bond_amount,
440 share_amount,
441 open_vault_share_price,
442 state.vault_share_price(),
443 )
444 });
445 match chain
446 .mock_hyperdrive_math()
447 .calculate_short_proceeds_down(
448 bond_amount.into(),
449 share_amount.into(),
450 open_vault_share_price.into(),
451 state.vault_share_price().into(),
452 state.vault_share_price().into(),
453 state.flat_fee().into(),
454 )
455 .call()
456 .await
457 {
458 Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected)),
459 Err(_) => assert!(actual.is_err()),
460 }
461 }
462
463 Ok(())
464 }
465
466 #[tokio::test]
467 async fn fuzz_sol_calculate_close_short_flat_plus_curve() -> Result<()> {
468 let chain = TestChain::new().await?;
469
470 let mut rng = thread_rng();
472 for _ in 0..*FAST_FUZZ_RUNS {
473 let state = rng.gen::<State>();
474 let in_ = rng.gen_range(fixed!(0)..=state.bond_reserves());
475 let maturity_time = state.position_duration();
476 let current_time = rng.gen_range(fixed!(0)..=maturity_time);
477 let actual = panic::catch_unwind(|| {
478 state.calculate_close_short_flat_plus_curve(
479 in_,
480 maturity_time.into(),
481 current_time.into(),
482 )
483 });
484
485 let normalized_time_remaining = state
486 .calculate_normalized_time_remaining(maturity_time.into(), current_time.into());
487 match chain
488 .mock_hyperdrive_math()
489 .calculate_close_short(
490 state.effective_share_reserves()?.into(),
491 state.bond_reserves().into(),
492 in_.into(),
493 normalized_time_remaining.into(),
494 state.t().into(),
495 state.c().into(),
496 state.mu().into(),
497 )
498 .call()
499 .await
500 {
501 Ok(expected) => assert_eq!(actual.unwrap().unwrap(), FixedPoint::from(expected.2)),
502 Err(_) => assert!(actual.is_err() || actual.unwrap().is_err()),
503 }
504 }
505
506 Ok(())
507 }
508
509 #[tokio::test]
511 async fn test_close_short_min_txn_amount() -> Result<()> {
512 let mut rng = thread_rng();
513 let state = rng.gen::<State>();
514 let result = state.calculate_close_short(
515 (state.config.minimum_transaction_amount - 10).into(),
516 state.calculate_spot_price()?,
517 state.vault_share_price(),
518 0.into(),
519 0.into(),
520 );
521 assert!(result.is_err());
522 Ok(())
523 }
524
525 #[tokio::test]
531 async fn test_calculate_market_value_short() -> Result<()> {
532 let tolerance = int256!(1e12); let mut rng = thread_rng();
536 for _ in 0..*FAST_FUZZ_RUNS {
537 let mut scaled_tolerance = tolerance;
538
539 let state = rng.gen::<State>();
540 let bond_amount = state.minimum_transaction_amount();
541 let open_vault_share_price = rng.gen_range(fixed!(0.5e18)..=fixed!(2.5e18));
542 let maturity_time = U256::try_from(state.position_duration())?;
543 let current_time = rng.gen_range(fixed!(0)..=FixedPoint::from(maturity_time));
544
545 let reserves_ratio = state.effective_share_reserves()? / state.bond_reserves();
549 if reserves_ratio < fixed!(1e12) {
550 scaled_tolerance *= int256!(100);
551 } else if reserves_ratio < fixed!(1e14) {
552 scaled_tolerance *= int256!(10);
553 }
554
555 let hyperdrive_valuation = state.calculate_close_short(
556 bond_amount,
557 open_vault_share_price,
558 state.vault_share_price(),
559 maturity_time.into(),
560 current_time.into(),
561 )?;
562
563 let spot_valuation = state.calculate_market_value_short(
564 bond_amount,
565 open_vault_share_price,
566 state.vault_share_price(),
567 maturity_time.into(),
568 current_time.into(),
569 )?;
570
571 let error = if spot_valuation >= hyperdrive_valuation {
572 I256::try_from(spot_valuation - hyperdrive_valuation)?
573 } else {
574 I256::try_from(hyperdrive_valuation - spot_valuation)?
575 };
576
577 assert!(
578 error < scaled_tolerance,
579 "error {:?} exceeds tolerance of {}",
580 error,
581 scaled_tolerance
582 );
583 }
584
585 Ok(())
586 }
587}