hyperdrive_math/long/max.rs
1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, int256, FixedPoint};
4
5use crate::{State, YieldSpace};
6
7impl State {
8 /// Calculates the pool's max spot price.
9 ///
10 /// Hyperdrive has assertions to ensure that traders don't purchase bonds at
11 /// negative interest rates. The maximum spot price that longs can push the
12 /// market to is given by:
13 ///
14 /// ```math
15 /// p_{\text{max}} = \frac{1 - \phi_f}{1 + \phi_c \cdot
16 /// \left( p_0^{-1} - 1 \right) \cdot \left( \phi_f - 1 \right)}
17 /// ```
18 pub fn calculate_max_spot_price(&self) -> Result<FixedPoint<U256>> {
19 Ok((fixed!(1e18) - self.flat_fee())
20 / (fixed!(1e18)
21 + self
22 .curve_fee()
23 .mul_up(fixed!(1e18).div_up(self.calculate_spot_price()?) - fixed!(1e18)))
24 .mul_up(fixed!(1e18) - self.flat_fee()))
25 }
26
27 /// Calculates the max long that can be opened given a budget.
28 ///
29 /// We start by calculating the long that brings the pool's spot price to 1.
30 /// If we are solvent at this point, then we're done. Otherwise, we approach
31 /// the max long iteratively using Newton's method.
32 pub fn calculate_max_long<F: Into<FixedPoint<U256>>, I: Into<I256>>(
33 &self,
34 budget: F,
35 checkpoint_exposure: I,
36 maybe_max_iterations: Option<usize>,
37 ) -> Result<FixedPoint<U256>> {
38 let budget = budget.into();
39 let checkpoint_exposure = checkpoint_exposure.into();
40
41 // Check the spot price after opening a minimum long is less than the
42 // max spot price
43 let spot_price_after_min_long =
44 self.calculate_spot_price_after_long(self.minimum_transaction_amount(), None)?;
45 if spot_price_after_min_long > self.calculate_max_spot_price()? {
46 return Ok(fixed!(0));
47 }
48
49 // Calculate the maximum long that brings the spot price to 1.
50 // If the pool is solvent after opening this long, then we're done.
51 let (absolute_max_base_amount, absolute_max_bond_amount) = self.absolute_max_long()?;
52 if self
53 .solvency_after_long(
54 absolute_max_base_amount,
55 absolute_max_bond_amount,
56 checkpoint_exposure,
57 )
58 .is_ok()
59 && absolute_max_base_amount >= self.minimum_transaction_amount()
60 {
61 return Ok(absolute_max_base_amount.min(budget));
62 }
63
64 // Use Newton's method to iteratively approach a solution. We use pool's
65 // solvency `$S(x)$` as our objective function, which will converge to the
66 // amount of base that needs to be paid to open the maximum long. The
67 // derivative of `$S(x)$` is negative (since solvency decreases as more
68 // longs are opened). The fixed point library doesn't support negative
69 // numbers, so we use the negation of the derivative to side-step the
70 // issue.
71 //
72 // Given the current guess of `$x_n$`, Newton's method gives us an updated
73 // guess of `$x_{n+1}$`:
74 //
75 // ```math
76 // x_{n+1} = x_n - \tfrac{S(x_n)}{S'(x_n)} = x_n + \tfrac{S(x_n)}{-S'(x_n)}
77 // ```
78 //
79 // The guess that we make is very important in determining how quickly
80 // we converge to the solution.
81 let mut max_base_amount =
82 self.max_long_guess(absolute_max_base_amount, checkpoint_exposure)?;
83
84 // possible_max_base_amount might be less than minimum transaction amount.
85 // we clamp here if so
86 if max_base_amount < self.minimum_transaction_amount() {
87 max_base_amount = self.minimum_transaction_amount();
88 }
89 let mut solvency = match self.solvency_after_long(
90 max_base_amount,
91 self.calculate_open_long(max_base_amount)?,
92 checkpoint_exposure,
93 ) {
94 Ok(solvency) => solvency,
95 Err(err) => {
96 return Err(eyre!(
97 "Initial guess in `calculate_max_long` is insolvent with error:\n{:#?}",
98 err
99 ))
100 }
101 };
102 for _ in 0..maybe_max_iterations.unwrap_or(7) {
103 // If the max base amount is equal to or exceeds the absolute max,
104 // we've gone too far and the calculation deviated from reality at
105 // some point.
106 if max_base_amount >= absolute_max_base_amount {
107 return Err(eyre!(
108 "Reached absolute max bond amount in `calculate_max_long`."
109 ));
110 }
111
112 // If the max base amount exceeds the budget, we know that the
113 // entire budget can be consumed without running into solvency
114 // constraints.
115 if max_base_amount >= budget {
116 return Ok(budget);
117 }
118
119 // TODO: It may be better to gracefully handle crossing over the
120 // root by extending the fixed point math library to handle negative
121 // numbers or even just using an if-statement to handle the negative
122 // numbers.
123 //
124 // Proceed to the next step of Newton's method. Once we have a
125 // candidate solution, we check to see if the pool is solvent after
126 // a long is opened with the candidate amount. If the pool isn't
127 // solvent, then we're done.
128 let derivative = match self.solvency_after_long_derivative_negation(max_base_amount) {
129 Ok(d) => d,
130 Err(_) => break,
131 };
132
133 let mut possible_max_base_amount = max_base_amount + solvency / derivative;
134
135 // possible_max_base_amount might be less than minimum transaction amount.
136 // we clamp here if so
137 if possible_max_base_amount < self.minimum_transaction_amount() {
138 possible_max_base_amount = self.minimum_transaction_amount();
139 }
140
141 solvency = match self.solvency_after_long(
142 possible_max_base_amount,
143 self.calculate_open_long(possible_max_base_amount)?,
144 checkpoint_exposure,
145 ) {
146 Ok(solvency) => solvency,
147 Err(_) => break,
148 };
149 max_base_amount = possible_max_base_amount;
150 }
151
152 // If the max base amount is less than the minimum transaction amount, we return 0 as the max long.
153 if max_base_amount <= self.minimum_transaction_amount() {
154 return Ok(fixed!(0));
155 }
156
157 // Ensure that the final result is less than the absolute max and clamp
158 // to the budget.
159 if max_base_amount >= absolute_max_base_amount {
160 return Err(eyre!(
161 "Reached absolute max bond amount in `calculate_max_long`."
162 ));
163 }
164
165 Ok(max_base_amount.min(budget))
166 }
167
168 /// Calculates the largest long that can be opened without buying bonds at a
169 /// negative interest rate. This calculation does not take Hyperdrive's
170 /// solvency constraints into account and shouldn't be used directly.
171 fn absolute_max_long(&self) -> Result<(FixedPoint<U256>, FixedPoint<U256>)> {
172 // We are targeting the pool's max spot price of:
173 //
174 // p_max = (1 - flatFee) / (1 + curveFee * (1 / p_0 - 1) * (1 - flatFee))
175 //
176 // We can derive a formula for the target bond reserves y_t in
177 // terms of the target share reserves z_t as follows:
178 //
179 // p_max = ((mu * z_t) / y_t) ** t_s
180 //
181 // =>
182 //
183 // y_t = (mu * z_t) * ((1 + curveFee * (1 / p_0 - 1) * (1 - flatFee)) / (1 - flatFee)) ** (1 / t_s)
184 //
185 // Our equation for price is the inverse of that used by YieldSpace, which must be considered when
186 // deriving the invariant from the price equation.
187 // With this in mind, we can use this formula to solve our YieldSpace invariant for z_t:
188 //
189 // k = (c / mu) * (mu * z_t) ** (1 - t_s) +
190 // (
191 // (mu * z_t) * ((1 + curveFee * (1 / p_0 - 1) * (1 - flatFee)) / (1 - flatFee)) ** (1 / t_s)
192 // ) ** (1 - t_s)
193 //
194 // =>
195 //
196 // z_t = (1 / mu) * (
197 // k / (
198 // (c / mu) +
199 // ((1 + curveFee * (1 / p_0 - 1) * (1 - flatFee)) / (1 - flatFee)) ** ((1 - t_s) / t_s))
200 // )
201 // ) ** (1 / (1 - t_s))
202 let inner = self
203 .k_down()?
204 .div_down(
205 self.vault_share_price()
206 .div_up(self.initial_vault_share_price())
207 + ((fixed!(1e18)
208 + self
209 .curve_fee()
210 .mul_up(
211 fixed!(1e18).div_up(self.calculate_spot_price()?) - fixed!(1e18),
212 )
213 .mul_up(fixed!(1e18) - self.flat_fee()))
214 .div_up(fixed!(1e18) - self.flat_fee()))
215 .pow((fixed!(1e18) - self.time_stretch()).div_down(self.time_stretch()))?,
216 )
217 .pow(fixed!(1e18).div_down(fixed!(1e18) - self.time_stretch()))?;
218 let target_share_reserves = inner.div_down(self.initial_vault_share_price());
219
220 // Now that we have the target share reserves, we can calculate the
221 // target bond reserves using the formula:
222 //
223 // y_t = (mu * z_t) * ((1 + curveFee * (1 / p_0 - 1) * (1 - flatFee)) / (1 - flatFee)) ** (1 / t_s)
224 //
225 // `inner` as defined above is `mu * z_t` so we calculate y_t as
226 //
227 // y_t = inner * ((1 + curveFee * (1 / p_0 - 1) * (1 - flatFee)) / (1 - flatFee)) ** (1 / t_s)
228 let fee_adjustment = self.curve_fee()
229 * (fixed!(1e18) / self.calculate_spot_price()? - fixed!(1e18))
230 * (fixed!(1e18) - self.flat_fee());
231 let target_bond_reserves = ((fixed!(1e18) + fee_adjustment)
232 / (fixed!(1e18) - self.flat_fee()))
233 .pow(fixed!(1e18).div_up(self.time_stretch()))?
234 * inner;
235
236 // Catch if the target share reserves are smaller than the effective share reserves.
237 let effective_share_reserves = self.effective_share_reserves()?;
238 if target_share_reserves < effective_share_reserves {
239 return Err(eyre!(
240 "target share reserves less than effective share reserves"
241 ));
242 }
243
244 // The absolute max base amount is given by:
245 // absolute_max_base_amount = (z_t - z_e) * c
246 let absolute_max_base_amount =
247 (target_share_reserves - effective_share_reserves) * self.vault_share_price();
248
249 // The absolute max bond amount is given by:
250 // absolute_max_bond_amount = (y - y_t) - Phi_c(absolute_max_base_amount)
251 let absolute_max_bond_amount = (self.bond_reserves() - target_bond_reserves)
252 - self.open_long_curve_fee(absolute_max_base_amount)?;
253
254 Ok((absolute_max_base_amount, absolute_max_bond_amount))
255 }
256
257 /// Calculates an initial guess of the max long that can be opened. This is a
258 /// reasonable estimate that is guaranteed to be less than the true max
259 /// long. We use this to get a reasonable starting point for Newton's
260 /// method.
261 fn max_long_guess(
262 &self,
263 absolute_max_base_amount: FixedPoint<U256>,
264 checkpoint_exposure: I256,
265 ) -> Result<FixedPoint<U256>> {
266 // Calculate an initial estimate of the max long by using the spot price as
267 // our conservative price.
268 let spot_price = self.calculate_spot_price()?;
269 let guess = self.max_long_estimate(spot_price, spot_price, checkpoint_exposure)?;
270
271 // We know that the spot price is 1 when the absolute max base amount is
272 // used to open a long. We also know that our spot price isn't a great
273 // estimate (conservative or otherwise) of the realized price that the
274 // max long will pay, so we calculate a better estimate of the realized
275 // price by interpolating between the spot price and 1 depending on how
276 // large the estimate is.
277 let t = (guess / absolute_max_base_amount)
278 .pow(fixed!(1e18).div_up(fixed!(1e18) - self.time_stretch()))?
279 * fixed!(0.8e18);
280 let estimate_price = spot_price * (fixed!(1e18) - t) + fixed!(1e18) * t;
281
282 // Recalculate our intial guess using the bootstrapped conservative
283 // estimate of the realized price.
284 self.max_long_estimate(estimate_price, spot_price, checkpoint_exposure)
285 }
286
287 /// Estimates the max long based on the pool's current solvency and a
288 /// conservative price estimate, `$p_r$`.
289 ///
290 /// We can use our estimate price `$p_r$` to approximate `$y(x)$` as
291 /// `$y(x) \approx p_r^{-1} \cdot x - c(x)$`. Plugging this into our
292 /// solvency function $s(x)$, we can calculate the share reserves and
293 /// exposure after opening a long with `$x$` base as:
294 ///
295 /// ```math
296 /// \begin{aligned}
297 /// z(x) &= z_0 + \tfrac{x - g(x)}{c} \\
298 /// e(x) &= e_0 + min(\text{exposure}_{c}, 0) + 2 \cdot y(x) - x + g(x) \\
299 /// &= e_0 + min(\text{exposure}_{c}, 0) + 2 \cdot p_r^{-1} \cdot x -
300 /// 2 \cdot c(x) - x + g(x)
301 /// \end{aligned}
302 /// ```
303 ///
304 /// We debit and negative checkpoint exposure from $e_0$ since the
305 /// global exposure doesn't take into account the negative exposure
306 /// from non-netted shorts in the checkpoint. These forumulas allow us
307 /// to calculate the approximate ending solvency of:
308 ///
309 /// ```math
310 /// s(x) \approx z(x) - \tfrac{e(x) - min(exposure_{c}, 0)}{c} - z_{min}
311 /// ```
312 ///
313 /// If we let the initial solvency be given by `$s_0$`, we can solve for
314 /// `$x$` as:
315 ///
316 /// ```math
317 /// x = \frac{c}{2} \cdot \frac{s_0 + min(exposure_{c}, 0)}{
318 /// p_r^{-1} +
319 /// \phi_{g} \cdot \phi_{c} \cdot \left( 1 - p \right) -
320 /// 1 -
321 /// \phi_{c} \cdot \left( p^{-1} - 1 \right)
322 /// }
323 /// ```
324 fn max_long_estimate(
325 &self,
326 estimate_price: FixedPoint<U256>,
327 spot_price: FixedPoint<U256>,
328 checkpoint_exposure: I256,
329 ) -> Result<FixedPoint<U256>> {
330 let checkpoint_exposure = FixedPoint::try_from(-checkpoint_exposure.min(int256!(0)))?;
331 let mut estimate =
332 self.calculate_solvency()? + checkpoint_exposure / self.vault_share_price();
333 estimate = estimate.mul_div_down(self.vault_share_price(), fixed!(2e18));
334 estimate /= fixed!(1e18) / estimate_price
335 + self.governance_lp_fee() * self.curve_fee() * (fixed!(1e18) - spot_price)
336 - fixed!(1e18)
337 - self.curve_fee() * (fixed!(1e18) / spot_price - fixed!(1e18));
338 Ok(estimate)
339 }
340
341 /// Calculates the solvency of the pool `$S(x)$` after a long is opened with
342 /// a base amount `$x$`.
343 ///
344 /// Since longs can net out with shorts in this checkpoint, we decrease
345 /// the global exposure variable by any negative exposure we have
346 /// in the checkpoint. The pool's solvency is calculated as:
347 ///
348 /// ```math
349 /// s = z - \tfrac{exposure + min(\text{exposure}_{c}, 0)}{c} - z_{min}
350 /// ```
351 ///
352 /// When a long is opened, the share reserves `$z$` increase by:
353 ///
354 /// ```math
355 /// \Delta z = \tfrac{x - g(x)}{c}
356 /// ```
357 ///
358 /// Opening the long increases the non-netted longs by the bond amount. From
359 /// this, the change in the exposure is given by:
360 ///
361 /// ```math
362 /// \Delta exposure = y(x)
363 /// ```
364 ///
365 /// From this, we can calculate `$S(x)$` as:
366 ///
367 /// ```math
368 /// S(x) = \left( z + \Delta z \right) - \left(
369 /// \tfrac{exposure + min(exposure_{checkpoint}, 0) + \Delta exposure}{c}
370 /// \right) - z_{min}
371 /// ```
372 ///
373 /// It's possible that the pool is insolvent after opening a long. In this
374 /// case, we return `None` since the fixed point library can't represent
375 /// negative numbers.
376 pub(super) fn solvency_after_long(
377 &self,
378 base_amount: FixedPoint<U256>,
379 bond_amount: FixedPoint<U256>,
380 checkpoint_exposure: I256,
381 ) -> Result<FixedPoint<U256>> {
382 let governance_fee_shares =
383 self.open_long_governance_fee(base_amount, None)? / self.vault_share_price();
384 let share_amount = base_amount / self.vault_share_price();
385 if self.share_reserves() + share_amount < governance_fee_shares {
386 return Err(eyre!(
387 "expected new_share_amount={:#?} >= governance_fee_shares={:#?}",
388 self.share_reserves() + share_amount,
389 governance_fee_shares
390 ));
391 }
392 let share_reserves = (self.share_reserves() + share_amount) - governance_fee_shares;
393 let exposure = self.long_exposure() + bond_amount;
394 let checkpoint_exposure = FixedPoint::try_from(-checkpoint_exposure.min(int256!(0)))?;
395 if share_reserves + checkpoint_exposure / self.vault_share_price()
396 >= exposure / self.vault_share_price() + self.minimum_share_reserves()
397 {
398 Ok(
399 share_reserves + checkpoint_exposure / self.vault_share_price()
400 - exposure / self.vault_share_price()
401 - self.minimum_share_reserves(),
402 )
403 } else {
404 Err(eyre!("Long would result in an insolvent pool."))
405 }
406 }
407
408 /// Calculates the negation of the derivative of the pool's solvency with respect
409 /// to the base amount that the long pays.
410 ///
411 /// The derivative of the pool's solvency `$S(x)$` with respect to the base
412 /// amount that the long pays is given by:
413 ///
414 /// ```math
415 /// S'(x) = \tfrac{1}{c} \cdot \left( 1 - y'(x) - \phi_{g} \cdot p \cdot c'(x) \right) \\
416 /// = \tfrac{1}{c} \cdot \left(
417 /// 1 - y'(x) - \phi_{g} \cdot \phi_{c} \cdot \left( 1 - p \right)
418 /// \right)
419 /// ```
420 ///
421 /// This derivative is negative since solvency decreases as more longs are
422 /// opened. We use the negation of the derivative to stay in the positive
423 /// domain, which allows us to use the fixed point library.
424 pub(super) fn solvency_after_long_derivative_negation(
425 &self,
426 base_amount: FixedPoint<U256>,
427 ) -> Result<FixedPoint<U256>> {
428 let derivative = self.calculate_open_long_derivative(base_amount)?;
429 let spot_price = self.calculate_spot_price()?;
430 Ok(
431 (derivative
432 + self.governance_lp_fee() * self.curve_fee() * (fixed!(1e18) - spot_price)
433 - fixed!(1e18))
434 .div_down(self.vault_share_price()),
435 )
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use std::panic;
442
443 use ethers::types::U256;
444 use fixedpointmath::{uint256, FixedPointValue};
445 use hyperdrive_test_utils::{
446 chain::TestChain,
447 constants::{FAST_FUZZ_RUNS, FUZZ_RUNS},
448 };
449 use hyperdrive_wrappers::wrappers::mock_hyperdrive_math::MaxTradeParams;
450 use rand::{thread_rng, Rng};
451
452 use super::*;
453 use crate::{calculate_effective_share_reserves, test_utils::agent::HyperdriveMathAgent};
454
455 /// This test differentially fuzzes the `absolute_max_long` function against
456 /// the Solidity analogue `calculateAbsoluteMaxLong`.
457 #[tokio::test]
458 async fn fuzz_sol_absolute_max_long() -> Result<()> {
459 let chain = TestChain::new().await?;
460
461 // Fuzz the rust and solidity implementations against each other.
462 let mut rng = thread_rng();
463 for _ in 0..*FAST_FUZZ_RUNS {
464 let state = rng.gen::<State>();
465 let rust_absolute_max_long = panic::catch_unwind(|| state.absolute_max_long());
466 match chain
467 .mock_hyperdrive_math()
468 .calculate_absolute_max_long(
469 MaxTradeParams {
470 share_reserves: state.info.share_reserves,
471 bond_reserves: state.info.bond_reserves,
472 longs_outstanding: state.info.longs_outstanding,
473 long_exposure: state.info.long_exposure,
474 share_adjustment: state.info.share_adjustment,
475 time_stretch: state.config.time_stretch,
476 vault_share_price: state.info.vault_share_price,
477 initial_vault_share_price: state.config.initial_vault_share_price,
478 minimum_share_reserves: state.config.minimum_share_reserves,
479 curve_fee: state.config.fees.curve,
480 flat_fee: state.config.fees.flat,
481 governance_lp_fee: state.config.fees.governance_lp,
482 },
483 calculate_effective_share_reserves(
484 state.info.share_reserves.into(),
485 state.info.share_adjustment,
486 )?
487 .into(),
488 state.calculate_spot_price()?.into(),
489 )
490 .call()
491 .await
492 {
493 Ok((sol_base_amount, sol_bond_amount)) => {
494 let (rust_base_amount, rust_bond_amount) =
495 rust_absolute_max_long.unwrap().unwrap();
496 assert_eq!(rust_base_amount, FixedPoint::from(sol_base_amount));
497 assert_eq!(rust_bond_amount, FixedPoint::from(sol_bond_amount));
498 }
499 Err(_) => assert!(
500 rust_absolute_max_long.is_err() || rust_absolute_max_long.unwrap().is_err()
501 ),
502 }
503 }
504
505 Ok(())
506 }
507
508 /// This test differentially fuzzes the `calculate_max_long` function against the
509 /// Solidity analogue `calculateMaxLong`. `calculateMaxLong` doesn't take
510 /// a trader's budget into account, so it only provides a subset of
511 /// `calculate_max_long`'s functionality. With this in mind, we provide
512 /// `calculate_max_long` with a budget of `U256::MAX` to ensure that the two
513 /// functions are equivalent.
514 #[tokio::test]
515 async fn fuzz_sol_calculate_max_long() -> Result<()> {
516 let chain = TestChain::new().await?;
517
518 // Fuzz the rust and solidity implementations against each other.
519 let mut rng = thread_rng();
520 for _ in 0..*FAST_FUZZ_RUNS {
521 // Snapshot the chain.
522 let id = chain.snapshot().await?;
523
524 // Gen a random state.
525 let state = rng.gen::<State>();
526
527 // Generate a random checkpoint exposure.
528 let checkpoint_exposure = rng.gen_range(0..=i128::MAX).flip_sign_if(rng.gen()).into();
529
530 // Check Solidity against Rust.
531 let max_iterations = 8usize;
532 // We need to catch panics because of overflows.
533 let actual = panic::catch_unwind(|| {
534 state.calculate_max_long(
535 U256::MAX,
536 checkpoint_exposure,
537 Some(max_iterations.into()),
538 )
539 });
540 match chain
541 .mock_hyperdrive_math()
542 .calculate_max_long(
543 MaxTradeParams {
544 share_reserves: state.info.share_reserves,
545 bond_reserves: state.info.bond_reserves,
546 longs_outstanding: state.info.longs_outstanding,
547 long_exposure: state.info.long_exposure,
548 share_adjustment: state.info.share_adjustment,
549 time_stretch: state.config.time_stretch,
550 vault_share_price: state.info.vault_share_price,
551 initial_vault_share_price: state.config.initial_vault_share_price,
552 minimum_share_reserves: state.config.minimum_share_reserves,
553 curve_fee: state.config.fees.curve,
554 flat_fee: state.config.fees.flat,
555 governance_lp_fee: state.config.fees.governance_lp,
556 },
557 checkpoint_exposure,
558 max_iterations.into(),
559 )
560 .call()
561 .await
562 {
563 Ok((expected_base_amount, ..)) => {
564 assert_eq!(
565 actual.unwrap().unwrap(),
566 FixedPoint::from(expected_base_amount)
567 );
568 }
569 Err(_) => assert!(actual.is_err() || actual.unwrap().is_err()),
570 }
571
572 // Reset chain snapshot.
573 chain.revert(id).await?;
574 }
575
576 Ok(())
577 }
578
579 #[tokio::test]
580 async fn test_calculate_max_long() -> Result<()> {
581 // Spawn a test chain and create two agents -- Alice and Bob. Alice
582 // is funded with a large amount of capital so that she can initialize
583 // the pool. Bob is funded with a small amount of capital so that we
584 // can test `calculate_max_long` when budget is the primary constraint.
585 let mut rng = thread_rng();
586 let chain = TestChain::new().await?;
587 let mut alice = chain.alice().await?;
588 let mut bob = chain.bob().await?;
589
590 let config = bob.get_config().clone();
591
592 for _ in 0..*FUZZ_RUNS {
593 // Snapshot the chain.
594 let id = chain.snapshot().await?;
595
596 // Fund Alice and Bob.
597 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
598 let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
599 let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
600 alice.fund(contribution).await?;
601 bob.fund(budget).await?;
602
603 // Alice initializes the pool.
604 alice.initialize(fixed_rate, contribution, None).await?;
605
606 // Some of the checkpoint passes and variable interest accrues.
607 alice
608 .checkpoint(alice.latest_checkpoint().await?, uint256!(0), None)
609 .await?;
610 let rate = rng.gen_range(fixed!(0)..=fixed!(0.5e18));
611 alice
612 .advance_time(
613 rate,
614 FixedPoint::from(config.checkpoint_duration) * fixed!(0.5e18),
615 )
616 .await?;
617
618 // Bob opens a max long.
619 let max_spot_price = bob.get_state().await?.calculate_max_spot_price()?;
620 let max_long = bob.calculate_max_long(None).await?;
621 let spot_price_after_long = bob
622 .get_state()
623 .await?
624 .calculate_spot_price_after_long(max_long, None)?;
625 bob.open_long(max_long, None, None).await?;
626
627 // One of three things should be true after opening the long:
628 //
629 // 1. The pool's spot price reached the max spot price prior to
630 // considering fees.
631 // 2. The pool's solvency is close to zero.
632 // 3. Bob's budget is consumed.
633 let is_max_price =
634 max_spot_price - spot_price_after_long.min(max_spot_price) < fixed!(1e15);
635 let is_solvency_consumed = {
636 let state = alice.get_state().await?;
637 let error_tolerance = fixed!(1_000e18).mul_div_down(fixed_rate, fixed!(0.1e18));
638 state.calculate_solvency()? < error_tolerance
639 };
640 let is_budget_consumed = {
641 let error_tolerance = fixed!(1e18);
642 bob.base() < error_tolerance
643 };
644 assert!(
645 is_max_price || is_solvency_consumed || is_budget_consumed,
646 "Invalid max long."
647 );
648
649 // Revert to the snapshot and reset the agent's wallets.
650 chain.revert(id).await?;
651 alice.reset(Default::default()).await?;
652 bob.reset(Default::default()).await?;
653 }
654
655 Ok(())
656 }
657}