1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, ln, uint256, FixedPoint};
4
5pub fn calculate_time_stretch(
6 rate: FixedPoint<U256>,
7 position_duration: FixedPoint<U256>,
8) -> Result<FixedPoint<U256>> {
9 let seconds_in_a_year = FixedPoint::from(U256::from(60 * 60 * 24 * 365));
10 let time_stretch = fixed!(5.24592e18)
13 / (fixed!(0.04665e18) * FixedPoint::from(U256::from(rate) * uint256!(100)));
14 let time_stretch = fixed!(1e18) / time_stretch;
15
16 Ok((FixedPoint::try_from(ln(I256::try_from(
34 fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year),
35 )?)?)?
36 / FixedPoint::try_from(ln(I256::try_from(fixed!(1e18) + rate)?)?)?)
37 * time_stretch)
38}
39
40pub fn calculate_effective_share_reserves(
43 share_reserves: FixedPoint<U256>,
44 share_adjustment: I256,
45) -> Result<FixedPoint<U256>> {
46 let effective_share_reserves = I256::try_from(share_reserves)? - share_adjustment;
47 if effective_share_reserves < I256::from(0) {
48 return Err(eyre!("effective share reserves cannot be negative"));
49 }
50 effective_share_reserves.try_into()
51}
52
53pub fn calculate_bonds_given_effective_shares_and_rate(
72 effective_share_reserves: FixedPoint<U256>,
73 target_rate: FixedPoint<U256>,
74 initial_vault_share_price: FixedPoint<U256>,
75 position_duration: FixedPoint<U256>,
76 time_stretch: FixedPoint<U256>,
77) -> Result<FixedPoint<U256>> {
78 let t = position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
83
84 let mut inner = fixed!(1e18) + target_rate.mul_down(t);
88 if inner >= fixed!(1e18) {
89 inner = inner.pow(fixed!(1e18) / time_stretch)?;
91 } else {
92 inner = inner.pow(fixed!(1e18).div_up(time_stretch))?;
94 }
95
96 Ok(initial_vault_share_price
100 .mul_down(effective_share_reserves)
101 .mul_down(inner))
102}
103
104pub fn calculate_rate_given_fixed_price(
117 price: FixedPoint<U256>,
118 position_duration: FixedPoint<U256>,
119) -> FixedPoint<U256> {
120 let fixed_price_duration_in_years =
121 position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
122 (fixed!(1e18) - price) / (price * fixed_price_duration_in_years)
123}
124
125pub fn calculate_hpr_given_apr(apr: I256, position_duration: FixedPoint<U256>) -> Result<I256> {
136 let holding_period_in_years =
137 position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
138 let (sign, apr_abs) = apr.into_sign_and_abs();
139 let hpr = FixedPoint::from(apr_abs) * holding_period_in_years;
140 Ok(I256::checked_from_sign_and_abs(sign, hpr.into()).unwrap())
141}
142
143pub fn calculate_hpr_given_apy(apy: I256, position_duration: FixedPoint<U256>) -> Result<I256> {
154 let holding_period_in_years =
155 position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
156 let (sign, apy_abs) = apy.into_sign_and_abs();
157 let hpr =
158 (fixed!(1e18) + FixedPoint::from(apy_abs)).pow(holding_period_in_years)? - fixed!(1e18);
159 Ok(I256::checked_from_sign_and_abs(sign, hpr.into()).unwrap())
160}
161
162#[cfg(test)]
163mod tests {
164 use std::panic;
165
166 use fixedpointmath::FixedPointValue;
167 use hyperdrive_test_utils::{
168 chain::TestChain,
169 constants::{FAST_FUZZ_RUNS, FUZZ_RUNS},
170 };
171 use rand::{thread_rng, Rng};
172
173 use super::*;
174 use crate::State;
175
176 #[tokio::test]
177 async fn fuzz_calculate_time_stretch() -> Result<()> {
178 let chain = TestChain::new().await?;
179
180 let seconds_in_ten_years = U256::from(10 * 60 * 60 * 24 * 365);
182 let seconds_in_a_day = U256::from(60 * 60 * 24);
183 let mut rng = thread_rng();
184 for _ in 0..*FAST_FUZZ_RUNS {
185 let position_duration = rng.gen_range(
187 FixedPoint::from(seconds_in_a_day)..=FixedPoint::from(seconds_in_ten_years),
188 );
189 let apr = rng.gen_range(fixed!(0.001e18)..=fixed!(10.0e18));
190 let actual_t = calculate_time_stretch(apr, position_duration);
191 match chain
192 .mock_hyperdrive_math()
193 .calculate_time_stretch(apr.into(), position_duration.into())
194 .call()
195 .await
196 {
197 Ok(expected_t) => {
198 assert_eq!(actual_t.unwrap(), FixedPoint::from(expected_t));
199 }
200 Err(_) => assert!(actual_t.is_err()),
201 }
202 }
203
204 Ok(())
205 }
206
207 #[tokio::test]
208 async fn fuzz_calculate_bonds_given_effective_shares_and_rate() -> Result<()> {
209 let mut rng = thread_rng();
210 for _ in 0..*FUZZ_RUNS {
211 let state = rng.gen::<State>();
213 let checkpoint_exposure = rng
214 .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
215 .raw()
216 .flip_sign_if(rng.gen());
217 let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
218
219 let max_long = match state.calculate_max_long(U256::MAX, checkpoint_exposure, None) {
222 Ok(max_long) => max_long,
223 Err(_) => continue, };
225 let min_rate = state.calculate_spot_rate_after_long(max_long, None)?;
226
227 let max_short = match panic::catch_unwind(|| {
230 state.calculate_max_short(
231 U256::MAX,
232 open_vault_share_price,
233 checkpoint_exposure,
234 None,
235 None,
236 )
237 }) {
238 Ok(max_short) => match max_short {
239 Ok(max_short) => max_short,
240 Err(_) => continue, },
242 Err(_) => continue, };
244 let max_rate = state.calculate_spot_rate_after_short(max_short, None)?;
245
246 let target_rate = rng.gen_range(min_rate..=max_rate);
248
249 let bond_reserves = calculate_bonds_given_effective_shares_and_rate(
251 state.effective_share_reserves()?,
252 target_rate,
253 state.initial_vault_share_price(),
254 state.position_duration(),
255 state.time_stretch(),
256 )?;
257
258 let mut new_state: State = state.clone();
260 new_state.info.bond_reserves = bond_reserves.into();
261 let new_spot_rate = new_state.calculate_spot_rate()?;
262 let tolerance = fixed!(1e8);
263 assert!(
264 target_rate.abs_diff(new_spot_rate) < tolerance,
265 r#"
266 target rate: {target_rate}
267 new spot rate: {new_spot_rate}
268 diff: {diff}
269 tolerance: {tolerance}
270"#,
271 diff = target_rate.abs_diff(new_spot_rate),
272 );
273 }
274 Ok(())
275 }
276}