use ethers::types::{I256, U256};
use eyre::{eyre, Result};
use fixedpointmath::{fixed, ln, uint256, FixedPoint};
pub fn calculate_time_stretch(
rate: FixedPoint<U256>,
position_duration: FixedPoint<U256>,
) -> Result<FixedPoint<U256>> {
let seconds_in_a_year = FixedPoint::from(U256::from(60 * 60 * 24 * 365));
let time_stretch = fixed!(5.24592e18)
/ (fixed!(0.04665e18) * FixedPoint::from(U256::from(rate) * uint256!(100)));
let time_stretch = fixed!(1e18) / time_stretch;
Ok((FixedPoint::try_from(ln(I256::try_from(
fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year),
)?)?)?
/ FixedPoint::try_from(ln(I256::try_from(fixed!(1e18) + rate)?)?)?)
* time_stretch)
}
pub fn calculate_effective_share_reserves(
share_reserves: FixedPoint<U256>,
share_adjustment: I256,
) -> Result<FixedPoint<U256>> {
let effective_share_reserves = I256::try_from(share_reserves)? - share_adjustment;
if effective_share_reserves < I256::from(0) {
return Err(eyre!("effective share reserves cannot be negative"));
}
effective_share_reserves.try_into()
}
pub fn calculate_bonds_given_effective_shares_and_rate(
effective_share_reserves: FixedPoint<U256>,
target_rate: FixedPoint<U256>,
initial_vault_share_price: FixedPoint<U256>,
position_duration: FixedPoint<U256>,
time_stretch: FixedPoint<U256>,
) -> Result<FixedPoint<U256>> {
let t = position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
let mut inner = fixed!(1e18) + target_rate.mul_down(t);
if inner >= fixed!(1e18) {
inner = inner.pow(fixed!(1e18) / time_stretch)?;
} else {
inner = inner.pow(fixed!(1e18).div_up(time_stretch))?;
}
Ok(initial_vault_share_price
.mul_down(effective_share_reserves)
.mul_down(inner))
}
pub fn calculate_rate_given_fixed_price(
price: FixedPoint<U256>,
position_duration: FixedPoint<U256>,
) -> FixedPoint<U256> {
let fixed_price_duration_in_years =
position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
(fixed!(1e18) - price) / (price * fixed_price_duration_in_years)
}
pub fn calculate_hpr_given_apr(apr: I256, position_duration: FixedPoint<U256>) -> Result<I256> {
let holding_period_in_years =
position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
let (sign, apr_abs) = apr.into_sign_and_abs();
let hpr = FixedPoint::from(apr_abs) * holding_period_in_years;
Ok(I256::checked_from_sign_and_abs(sign, hpr.into()).unwrap())
}
pub fn calculate_hpr_given_apy(apy: I256, position_duration: FixedPoint<U256>) -> Result<I256> {
let holding_period_in_years =
position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
let (sign, apy_abs) = apy.into_sign_and_abs();
let hpr =
(fixed!(1e18) + FixedPoint::from(apy_abs)).pow(holding_period_in_years)? - fixed!(1e18);
Ok(I256::checked_from_sign_and_abs(sign, hpr.into()).unwrap())
}
#[cfg(test)]
mod tests {
use std::panic;
use fixedpointmath::FixedPointValue;
use hyperdrive_test_utils::{
chain::TestChain,
constants::{FAST_FUZZ_RUNS, FUZZ_RUNS},
};
use rand::{thread_rng, Rng};
use super::*;
use crate::State;
#[tokio::test]
async fn fuzz_calculate_time_stretch() -> Result<()> {
let chain = TestChain::new().await?;
let seconds_in_ten_years = U256::from(10 * 60 * 60 * 24 * 365);
let seconds_in_a_day = U256::from(60 * 60 * 24);
let mut rng = thread_rng();
for _ in 0..*FAST_FUZZ_RUNS {
let position_duration = rng.gen_range(
FixedPoint::from(seconds_in_a_day)..=FixedPoint::from(seconds_in_ten_years),
);
let apr = rng.gen_range(fixed!(0.001e18)..=fixed!(10.0e18));
let actual_t = calculate_time_stretch(apr, position_duration);
match chain
.mock_hyperdrive_math()
.calculate_time_stretch(apr.into(), position_duration.into())
.call()
.await
{
Ok(expected_t) => {
assert_eq!(actual_t.unwrap(), FixedPoint::from(expected_t));
}
Err(_) => assert!(actual_t.is_err()),
}
}
Ok(())
}
#[tokio::test]
async fn fuzz_calculate_bonds_given_effective_shares_and_rate() -> Result<()> {
let mut rng = thread_rng();
for _ in 0..*FUZZ_RUNS {
let state = rng.gen::<State>();
let checkpoint_exposure = rng
.gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
.raw()
.flip_sign_if(rng.gen());
let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
let max_long = match state.calculate_max_long(U256::MAX, checkpoint_exposure, None) {
Ok(max_long) => max_long,
Err(_) => continue, };
let min_rate = state.calculate_spot_rate_after_long(max_long, None)?;
let max_short = match panic::catch_unwind(|| {
state.calculate_max_short(
U256::MAX,
open_vault_share_price,
checkpoint_exposure,
None,
None,
)
}) {
Ok(max_short) => match max_short {
Ok(max_short) => max_short,
Err(_) => continue, },
Err(_) => continue, };
let max_rate = state.calculate_spot_rate_after_short(max_short, None)?;
let target_rate = rng.gen_range(min_rate..=max_rate);
let bond_reserves = calculate_bonds_given_effective_shares_and_rate(
state.effective_share_reserves()?,
target_rate,
state.initial_vault_share_price(),
state.position_duration(),
state.time_stretch(),
)?;
let mut new_state: State = state.clone();
new_state.info.bond_reserves = bond_reserves.into();
let new_spot_rate = new_state.calculate_spot_rate()?;
let tolerance = fixed!(1e8);
assert!(
target_rate.abs_diff(new_spot_rate) < tolerance,
r#"
target rate: {target_rate}
new spot rate: {new_spot_rate}
diff: {diff}
tolerance: {tolerance}
"#,
diff = target_rate.abs_diff(new_spot_rate),
);
}
Ok(())
}
}