1mod base;
2mod long;
3mod lp;
4mod short;
5#[cfg(test)]
6mod test_utils;
7mod utils;
8mod yield_space;
9
10use ethers::types::{Address, I256, U256};
11use eyre::{eyre, Result};
12use fixedpointmath::{fixed, fixed_i256, FixedPoint};
13use hyperdrive_wrappers::wrappers::ihyperdrive::{Fees, PoolConfig, PoolInfo};
14use rand::{
15 distributions::{Distribution, Standard},
16 Rng,
17};
18pub use utils::*;
19pub use yield_space::YieldSpace;
20
21#[derive(Clone, Debug)]
22pub struct State {
23 pub config: PoolConfig,
24 pub info: PoolInfo,
25}
26
27impl Distribution<State> for Standard {
28 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> State {
31 let one_day_in_seconds = 60 * 60 * 24;
32 let one_hour_in_seconds = 60 * 60;
33 let config = PoolConfig {
34 base_token: Address::zero(),
35 vault_shares_token: Address::zero(),
36 linker_factory: Address::zero(),
37 linker_code_hash: [0; 32],
38 initial_vault_share_price: rng.gen_range(fixed!(0.5e18)..=fixed!(2.5e18)).into(),
39 minimum_share_reserves: rng.gen_range(fixed!(0.1e18)..=fixed!(1e18)).into(),
40 minimum_transaction_amount: rng.gen_range(fixed!(0.1e18)..=fixed!(1e18)).into(),
41 circuit_breaker_delta: rng.gen_range(fixed!(0.01e18)..=fixed!(10e18)).into(),
42 position_duration: rng
43 .gen_range(
44 FixedPoint::from(91 * one_day_in_seconds)
45 ..=FixedPoint::from(365 * one_day_in_seconds),
46 )
47 .into(),
48 checkpoint_duration: rng
49 .gen_range(
50 FixedPoint::from(one_hour_in_seconds)..=FixedPoint::from(one_day_in_seconds),
51 )
52 .into(),
53 time_stretch: rng.gen_range(fixed!(0.005e18)..=fixed!(0.5e18)).into(),
54 governance: Address::zero(),
55 fee_collector: Address::zero(),
56 sweep_collector: Address::zero(),
57 checkpoint_rewarder: Address::zero(),
58 fees: Fees {
59 curve: rng.gen_range(fixed!(0.0001e18)..=fixed!(0.2e18)).into(),
60 flat: rng.gen_range(fixed!(0.0001e18)..=fixed!(0.2e18)).into(),
61 governance_lp: rng.gen_range(fixed!(0.0001e18)..=fixed!(0.2e18)).into(),
62 governance_zombie: rng.gen_range(fixed!(0.0001e18)..=fixed!(0.2e18)).into(),
63 },
64 };
65 let share_reserves = rng.gen_range(fixed!(1_000e18)..=fixed!(100_000_000e18));
66 let share_adjustment = {
67 if rng.gen() {
68 rng.gen_range(fixed_i256!(-100_000e18)..=fixed!(0)).raw()
69 } else {
70 I256::try_from(rng.gen_range(
74 fixed!(0)
75 ..=(share_reserves
76 - FixedPoint::from(config.minimum_share_reserves)
77 - fixed!(10e18)),
78 ))
79 .unwrap()
80 }
81 };
82 let effective_share_reserves =
83 calculate_effective_share_reserves(share_reserves, share_adjustment).unwrap();
84 let bond_reserves = rng.gen_range(
87 effective_share_reserves * FixedPoint::from(config.initial_vault_share_price)
88 ..=fixed!(1_000_000_000e18),
89 );
90 let info = PoolInfo {
92 share_reserves: share_reserves.into(),
93 zombie_base_proceeds: fixed!(0).into(),
94 zombie_share_reserves: fixed!(0).into(),
95 bond_reserves: bond_reserves.into(),
96 vault_share_price: rng.gen_range(fixed!(0.5e18)..=fixed!(2.5e18)).into(),
97 longs_outstanding: rng.gen_range(fixed!(0)..=fixed!(100_000e18)).into(),
98 shorts_outstanding: rng.gen_range(fixed!(0)..=fixed!(100_000e18)).into(),
99 long_exposure: rng.gen_range(fixed!(0)..=fixed!(100_000e18)).into(),
100 share_adjustment: share_adjustment.into(),
101 long_average_maturity_time: rng
104 .gen_range(fixed!(0)..=FixedPoint::from(365 * one_day_in_seconds) * fixed!(1e18))
105 .into(),
106 short_average_maturity_time: rng
107 .gen_range(fixed!(0)..=FixedPoint::from(365 * one_day_in_seconds) * fixed!(1e18))
108 .into(),
109 lp_total_supply: rng
110 .gen_range(fixed!(1_000e18)..=fixed!(100_000_000e18))
111 .into(),
112 lp_share_price: rng.gen_range(fixed!(0.01e18)..=fixed!(5e18)).into(),
114 withdrawal_shares_proceeds: rng.gen_range(fixed!(0)..=fixed!(100_000e18)).into(),
115 withdrawal_shares_ready_to_withdraw: rng
116 .gen_range(fixed!(1_000e18)..=fixed!(100_000_000e18))
117 .into(),
118 };
119 State { config, info }
120 }
121}
122
123impl State {
124 pub fn new(config: PoolConfig, info: PoolInfo) -> Self {
126 Self { config, info }
127 }
128
129 pub fn calculate_spot_price(&self) -> Result<FixedPoint<U256>> {
131 YieldSpace::calculate_spot_price(self)
132 }
133
134 pub fn calculate_spot_rate(&self) -> Result<FixedPoint<U256>> {
136 Ok(calculate_rate_given_fixed_price(
137 self.calculate_spot_price()?,
138 self.position_duration(),
139 ))
140 }
141
142 pub fn to_checkpoint(&self, time: U256) -> U256 {
144 time - time % self.config.checkpoint_duration
145 }
146
147 fn calculate_normalized_time_remaining(
149 &self,
150 maturity_time: U256,
151 current_time: U256,
152 ) -> FixedPoint<U256> {
153 let latest_checkpoint = self.to_checkpoint(current_time);
154 if maturity_time > latest_checkpoint {
155 FixedPoint::from(maturity_time - latest_checkpoint).div_down(self.position_duration())
157 } else {
158 fixed!(0)
159 }
160 }
161
162 fn reserves_given_rate_ignoring_exposure<F: Into<FixedPoint<U256>>>(
195 &self,
196 target_rate: F,
197 ) -> Result<(FixedPoint<U256>, FixedPoint<U256>)> {
198 let target_rate = target_rate.into();
199
200 let c_over_mu = self
202 .vault_share_price()
203 .div_up(self.initial_vault_share_price());
204 let scaled_rate = (target_rate.mul_up(self.annualized_position_duration()) + fixed!(1e18))
205 .pow(fixed!(1e18) / self.time_stretch())?;
206 let inner = (self.k_down()?
207 / (c_over_mu + scaled_rate.pow(fixed!(1e18) - self.time_stretch())?))
208 .pow(fixed!(1e18) / (fixed!(1e18) - self.time_stretch()))?;
209 let target_effective_share_reserves = inner / self.initial_vault_share_price();
210 let target_share_reserves_i256 =
211 I256::try_from(target_effective_share_reserves)? + self.share_adjustment();
212
213 let target_share_reserves = if target_share_reserves_i256 > I256::from(0) {
214 FixedPoint::try_from(target_share_reserves_i256)?
215 } else {
216 return Err(eyre!("Target rate would result in share reserves <= 0."));
217 };
218
219 let target_bond_reserves = inner * scaled_rate;
221
222 Ok((target_share_reserves, target_bond_reserves))
223 }
224
225 fn position_duration(&self) -> FixedPoint<U256> {
226 self.config.position_duration.into()
227 }
228
229 fn annualized_position_duration(&self) -> FixedPoint<U256> {
230 self.position_duration() / FixedPoint::from(U256::from(60 * 60 * 24 * 365))
231 }
232
233 fn checkpoint_duration(&self) -> FixedPoint<U256> {
234 self.config.checkpoint_duration.into()
235 }
236
237 fn time_stretch(&self) -> FixedPoint<U256> {
238 self.config.time_stretch.into()
239 }
240
241 fn initial_vault_share_price(&self) -> FixedPoint<U256> {
242 self.config.initial_vault_share_price.into()
243 }
244
245 fn minimum_share_reserves(&self) -> FixedPoint<U256> {
246 self.config.minimum_share_reserves.into()
247 }
248
249 fn minimum_transaction_amount(&self) -> FixedPoint<U256> {
250 self.config.minimum_transaction_amount.into()
251 }
252
253 fn curve_fee(&self) -> FixedPoint<U256> {
254 self.config.fees.curve.into()
255 }
256
257 fn flat_fee(&self) -> FixedPoint<U256> {
258 self.config.fees.flat.into()
259 }
260
261 fn governance_lp_fee(&self) -> FixedPoint<U256> {
262 self.config.fees.governance_lp.into()
263 }
264
265 pub fn vault_share_price(&self) -> FixedPoint<U256> {
266 self.info.vault_share_price.into()
267 }
268
269 fn share_reserves(&self) -> FixedPoint<U256> {
270 self.info.share_reserves.into()
271 }
272
273 fn effective_share_reserves(&self) -> Result<FixedPoint<U256>> {
274 calculate_effective_share_reserves(self.share_reserves(), self.share_adjustment())
275 }
276
277 fn bond_reserves(&self) -> FixedPoint<U256> {
278 self.info.bond_reserves.into()
279 }
280
281 fn longs_outstanding(&self) -> FixedPoint<U256> {
282 self.info.longs_outstanding.into()
283 }
284
285 fn long_average_maturity_time(&self) -> FixedPoint<U256> {
286 self.info.long_average_maturity_time.into()
287 }
288
289 fn shorts_outstanding(&self) -> FixedPoint<U256> {
290 self.info.shorts_outstanding.into()
291 }
292
293 fn short_average_maturity_time(&self) -> FixedPoint<U256> {
294 self.info.short_average_maturity_time.into()
295 }
296
297 fn long_exposure(&self) -> FixedPoint<U256> {
298 self.info.long_exposure.into()
299 }
300
301 fn share_adjustment(&self) -> I256 {
302 self.info.share_adjustment
303 }
304
305 fn lp_total_supply(&self) -> FixedPoint<U256> {
306 self.info.lp_total_supply.into()
307 }
308
309 fn withdrawal_shares_proceeds(&self) -> FixedPoint<U256> {
310 self.info.withdrawal_shares_proceeds.into()
311 }
312
313 fn withdrawal_shares_ready_to_withdraw(&self) -> FixedPoint<U256> {
314 self.info.withdrawal_shares_ready_to_withdraw.into()
315 }
316}
317
318impl YieldSpace for State {
319 fn z(&self) -> FixedPoint<U256> {
320 self.share_reserves()
321 }
322
323 fn zeta(&self) -> I256 {
324 self.share_adjustment()
325 }
326
327 fn y(&self) -> FixedPoint<U256> {
328 self.bond_reserves()
329 }
330
331 fn mu(&self) -> FixedPoint<U256> {
332 self.initial_vault_share_price()
333 }
334
335 fn c(&self) -> FixedPoint<U256> {
336 self.vault_share_price()
337 }
338
339 fn t(&self) -> FixedPoint<U256> {
340 self.time_stretch()
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use fixedpointmath::{fixed, uint256};
347 use hyperdrive_test_utils::constants::FAST_FUZZ_RUNS;
348 use rand::thread_rng;
349
350 use super::*;
351
352 #[tokio::test]
353 async fn fuzz_reserves_given_rate_ignoring_exposure() -> Result<()> {
354 let test_tolerance = fixed!(1e15);
355 let mut rng = thread_rng();
356 let mut counter = 0;
357 for _ in 0..*FAST_FUZZ_RUNS {
358 let mut state = rng.gen::<State>();
360 state.info.longs_outstanding = uint256!(0);
362 state.info.long_average_maturity_time = uint256!(0);
363 state.info.long_exposure = uint256!(0);
364 state.info.shorts_outstanding = uint256!(0);
365 state.info.short_average_maturity_time = uint256!(0);
366 if state.calculate_spot_price()? < state.calculate_min_spot_price()?
368 || state.calculate_spot_price()? > fixed!(1e18)
369 || state.calculate_solvency().is_err()
370 {
371 continue;
372 }
373 let min_rate = calculate_rate_given_fixed_price(
375 state.calculate_max_spot_price()?,
376 state.position_duration(),
377 );
378 let max_rate = calculate_rate_given_fixed_price(
379 state.calculate_min_spot_price()?,
380 state.position_duration(),
381 );
382 let target_rate = rng.gen_range(min_rate..=max_rate);
383 let (target_share_reserves, target_bond_reserves) =
388 match state.reserves_given_rate_ignoring_exposure(target_rate) {
389 Ok(result) => result,
390 Err(err) => {
391 if err
392 .to_string()
393 .contains("Target rate would result in share reserves <= 0.")
394 {
395 continue;
396 } else {
397 return Err(err);
398 }
399 }
400 };
401 let mut new_state = state.clone();
403 new_state.info.share_reserves = target_share_reserves.into();
404 new_state.info.bond_reserves = target_bond_reserves.into();
405 if new_state.calculate_solvency().is_err()
406 || new_state.calculate_spot_price()? > fixed!(1e18)
407 {
408 continue;
409 }
410 let realized_rate = new_state.calculate_spot_rate()?;
412 let error = if realized_rate > target_rate {
413 realized_rate - target_rate
414 } else {
415 target_rate - realized_rate
416 };
417 assert!(
418 error <= test_tolerance,
419 "expected error={} <= tolerance={}",
420 error,
421 test_tolerance
422 );
423 counter += 1;
424 }
425 assert!(counter >= 5_000); Ok(())
427 }
428
429 #[tokio::test]
430 async fn test_calculate_normalized_time_remaining() -> Result<()> {
431 let mut rng = thread_rng();
433 let mut state = rng.gen::<State>();
434
435 state.config.position_duration = fixed!(28209717).into();
438 state.config.checkpoint_duration = fixed!(43394).into();
439 let expected_time_remaining = fixed!(3544877816392);
440
441 let maturity_time = U256::from(100);
442 let current_time = U256::from(90);
443 let time_remaining = state.calculate_normalized_time_remaining(maturity_time, current_time);
444
445 assert_eq!(expected_time_remaining, time_remaining);
446 Ok(())
447 }
448}