hyperdrive_math/
utils.rs

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    // Calculate the benchmark time stretch. This time stretch is tuned for
11    // a position duration of 1 year.
12    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    // We know that the following simultaneous equations hold:
17    //
18    // (1 + apr) * A ** timeStretch = 1
19    //
20    // and
21    //
22    // (1 + apr * (positionDuration / 365 days)) * A ** targetTimeStretch = 1
23    //
24    // where A is the reserve ratio. We can solve these equations for the
25    // target time stretch as follows:
26    //
27    // targetTimeStretch = (
28    //     ln(1 + apr * (positionDuration / 365 days)) /
29    //     ln(1 + apr)
30    // ) * timeStretch
31    //
32    // NOTE: Round down so that the output is an underestimate.
33    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
40/// Calculates the share reserves after zeta adjustment, aka the effective share
41/// reserves: `$z_e = z - zeta$`.
42pub 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
53/// Calculates the bond reserves assuming that the pool has a given
54/// effective share reserves and fixed rate APR.
55///
56/// NOTE: This function should not be used for computing reserve levels when
57/// initializing a pool. Instead use
58/// (calculate_initial_reserves)[State::calculate_initial_reserves].
59///
60/// ```math
61/// \begin{aligned}
62/// r &= \tfrac{(1 / p) - 1}{t} \\
63/// &= \frac{1 - p}{p \cdot t}
64/// ```
65///
66/// ```math
67/// p = \left( \tfrac{u * z}{y} \right)^{t}
68/// ```
69///
70/// Returns the bond reserves that make the pool have a specified APR.
71pub 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    // NOTE: Round down to underestimate the initial bond reserves.
79    //
80    // Normalize the time to maturity to fractions of a year since the provided
81    // rate is an APR.
82    let t = position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365));
83
84    // NOTE: Round down to underestimate the initial bond reserves.
85    //
86    // inner = (1 + apr * t) ** (1 / t_s)
87    let mut inner = fixed!(1e18) + target_rate.mul_down(t);
88    if inner >= fixed!(1e18) {
89        // Rounding down the exponent results in a smaller result.
90        inner = inner.pow(fixed!(1e18) / time_stretch)?;
91    } else {
92        // Rounding up the exponent results in a smaller result.
93        inner = inner.pow(fixed!(1e18).div_up(time_stretch))?;
94    }
95
96    // NOTE: Round down to underestimate the initial bond reserves.
97    //
98    // mu * (z - zeta) * (1 + apr * t) ** (1 / tau)
99    Ok(initial_vault_share_price
100        .mul_down(effective_share_reserves)
101        .mul_down(inner))
102}
103
104/// Calculate the rate assuming a given price is constant for some annualized duration.
105///
106/// We calculate the rate for a fixed length of time as:
107///
108/// ```math
109/// r = \frac{(1 - p)}{p \cdot t}
110/// ```
111///
112/// where $p$ is the price and $t$ is the length of time that this price is
113/// assumed to be constant, in units of years. For example, if the price is
114/// constant for 6 months, then `$t=0.5$`.
115/// In our case, `$t = \text{position_duration} / (60*60*24*365)$`.
116pub 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
125/// Calculate the holding period return (HPR) given a non-compounding, annualized rate (APR).
126///
127/// Since the rate is non-compounding, we calculate the hpr as:
128///
129/// ```math
130/// \text{hpr} = \text{apr} \cdot t
131/// ```
132///
133/// where `$t$` is the holding period, in units of years. For example, if the
134/// holding period is 6 months, then `$t=0.5$`.
135pub 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
143/// Calculate the holding period return (HPR) given a compounding, annualized rate (APY).
144///
145/// Since the rate is compounding, we calculate the hpr as:
146///
147/// ```math
148/// \text{hpr} = (1 +  \text{apy})^{t} - 1
149/// ```
150///
151/// where `$t$` is the holding period, in units of years. For example, if the
152/// holding period is 6 months, then `$t=0.5$`.
153pub 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        // Fuzz the rust and solidity implementations against each other.
181        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            // Get the current state of the mock contract
186            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            // Gen the random state.
212            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            // Get the min rate.
220            // We need to catch panics because of overflows.
221            let max_long = match state.calculate_max_long(U256::MAX, checkpoint_exposure, None) {
222                Ok(max_long) => max_long,
223                Err(_) => continue, // Max threw an Err. Don't finish this fuzz iteration.
224            };
225            let min_rate = state.calculate_spot_rate_after_long(max_long, None)?;
226
227            // Get the max rate.
228            // We need to catch panics because of overflows.
229            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, // Max threw an Err; don't finish this fuzz iteration.
241                },
242                Err(_) => continue, // Max threw a panic; don't finish this fuzz iteration.
243            };
244            let max_rate = state.calculate_spot_rate_after_short(max_short, None)?;
245
246            // Get a random target rate that is allowable.
247            let target_rate = rng.gen_range(min_rate..=max_rate);
248
249            // Calculate the new bond reserves.
250            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            // Make a new state with the updated reserves & check the spot rate.
259            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}