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}