hyperdrive_math/long/
targeted.rs

1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, FixedPoint};
4
5use crate::{State, YieldSpace};
6
7impl State {
8    /// Gets a target long that can be opened given a budget to achieve a
9    /// desired fixed rate.
10    ///
11    /// If the long amount to reach the target is greater than the budget,
12    /// the budget is returned.
13    /// If the long amount to reach the target is invalid (i.e. it would produce
14    /// an insolvent pool), then an error is thrown, and the user is advised to
15    /// use [calculate_max_long](State::calculate_max_long).
16    pub fn calculate_targeted_long_with_budget<
17        F1: Into<FixedPoint<U256>>,
18        F2: Into<FixedPoint<U256>>,
19        F3: Into<FixedPoint<U256>>,
20        I: Into<I256>,
21    >(
22        &self,
23        budget: F1,
24        target_rate: F2,
25        checkpoint_exposure: I,
26        maybe_max_iterations: Option<usize>,
27        maybe_allowable_error: Option<F3>,
28    ) -> Result<FixedPoint<U256>> {
29        let budget = budget.into();
30        match self.calculate_targeted_long(
31            target_rate,
32            checkpoint_exposure,
33            maybe_max_iterations,
34            maybe_allowable_error,
35        ) {
36            Ok(long_amount) => Ok(long_amount.min(budget)),
37            Err(error) => Err(error),
38        }
39    }
40
41    /// Gets a target long that can be opened to achieve a desired fixed rate.
42    fn calculate_targeted_long<
43        F1: Into<FixedPoint<U256>>,
44        F2: Into<FixedPoint<U256>>,
45        I: Into<I256>,
46    >(
47        &self,
48        target_rate: F1,
49        checkpoint_exposure: I,
50        maybe_max_iterations: Option<usize>,
51        maybe_allowable_error: Option<F2>,
52    ) -> Result<FixedPoint<U256>> {
53        // Check input args.
54        let target_rate = target_rate.into();
55        let checkpoint_exposure = checkpoint_exposure.into();
56        let allowable_error = match maybe_allowable_error {
57            Some(allowable_error) => allowable_error.into(),
58            None => fixed!(1e15),
59        };
60        let current_rate = self.calculate_spot_rate()?;
61        if target_rate >= current_rate {
62            return Err(eyre!(
63                "target_rate = {} argument must be less than the current_rate = {} for a targeted long.",
64                target_rate, current_rate,
65            ));
66        }
67
68        // Estimate the long that achieves a target rate.
69        let (target_pool_share_reserves, target_pool_bond_reserves) =
70            self.reserves_given_rate_ignoring_exposure(target_rate)?;
71        let (mut target_user_base_delta, target_user_bond_delta) = self
72            .long_trade_needed_given_reserves(
73                target_pool_share_reserves,
74                target_pool_bond_reserves,
75            )?;
76        // Determine what rate was achieved.
77        let resulting_rate = self
78            .calculate_spot_rate_after_long(target_user_base_delta, Some(target_user_bond_delta))?;
79
80        // The estimated long will usually underestimate because the realized price
81        // should always be greater than the spot price.
82        //
83        // However, if we overshot the zero-crossing (due to errors arising from FixedPoint<U256> arithmetic),
84        // then either return or reduce the starting base amount and start on Newton's method.
85        if target_rate > resulting_rate {
86            let rate_error = target_rate - resulting_rate;
87
88            // If we were still close enough and solvent, return.
89            if self
90                .solvency_after_long(
91                    target_user_base_delta,
92                    target_user_bond_delta,
93                    checkpoint_exposure,
94                )
95                .is_ok()
96                && rate_error < allowable_error
97            {
98                return Ok(target_user_base_delta);
99            }
100            // Else, cut the initial guess down by an order of magnitude and go to Newton's method.
101            else {
102                target_user_base_delta /= fixed!(10e18);
103            }
104        }
105        // Else check if we are close enough to return.
106        else {
107            // If solvent & within the allowable error, stop here.
108            let rate_error = resulting_rate - target_rate;
109            if self
110                .solvency_after_long(
111                    target_user_base_delta,
112                    target_user_bond_delta,
113                    checkpoint_exposure,
114                )
115                .is_ok()
116                && rate_error < allowable_error
117            {
118                return Ok(target_user_base_delta);
119            }
120        }
121
122        // Iterate to find a solution.
123        // We can use the initial guess as a starting point since we know it is less than the target.
124        let mut possible_target_base_delta = target_user_base_delta;
125
126        // Iteratively find a solution
127        for _ in 0..maybe_max_iterations.unwrap_or(7) {
128            let possible_target_bond_delta =
129                self.calculate_open_long(possible_target_base_delta)?;
130            let resulting_rate = self.calculate_spot_rate_after_long(
131                possible_target_base_delta,
132                Some(possible_target_bond_delta),
133            )?;
134
135            // We assume that the loss is positive only because Newton's
136            // method will always underestimate.
137            if target_rate > resulting_rate {
138                return Err(eyre!(
139                    "We overshot the zero-crossing during Newton's method.",
140                ));
141            }
142            // We choose the difference between the rates as the loss because it
143            // is convex given the above check, differentiable almost everywhere,
144            // and has a simple derivative.
145            let loss = resulting_rate - target_rate;
146
147            // If solvent & within error, then return the value.
148            if self
149                .solvency_after_long(
150                    possible_target_base_delta,
151                    possible_target_bond_delta,
152                    checkpoint_exposure,
153                )
154                .is_ok()
155                && loss < allowable_error
156            {
157                return Ok(possible_target_base_delta);
158            }
159            // Otherwise perform another iteration.
160            else {
161                // The derivative of the loss is $l'(x) = r'(x)$.
162                // We return $-l'(x)$ because $r'(x)$ is negative, which
163                // can't be represented with FixedPoint<U256>.
164                let negative_loss_derivative = self.rate_after_long_derivative_negation(
165                    possible_target_base_delta,
166                    possible_target_bond_delta,
167                )?;
168
169                // Adding the negative loss derivative instead of subtracting the loss derivative
170                // ∆x_{n+1} = ∆x_{n} - l / l'
171                //          = ∆x_{n} + l / (-l')
172                possible_target_base_delta += loss / negative_loss_derivative;
173            }
174        }
175
176        // Final solvency check.
177        if self
178            .solvency_after_long(
179                possible_target_base_delta,
180                self.calculate_open_long(possible_target_base_delta)?,
181                checkpoint_exposure,
182            )
183            .is_err()
184        {
185            return Err(eyre!("Guess in `calculate_targeted_long` is insolvent."));
186        }
187
188        // Final accuracy check.
189        let possible_target_bond_delta = self.calculate_open_long(possible_target_base_delta)?;
190        let resulting_rate = self.calculate_spot_rate_after_long(
191            possible_target_base_delta,
192            Some(possible_target_bond_delta),
193        )?;
194        if target_rate > resulting_rate {
195            return Err(eyre!(
196                "We overshot the zero-crossing after Newton's method.",
197            ));
198        }
199        let loss = resulting_rate - target_rate;
200        if loss >= allowable_error {
201            return Err(eyre!(
202                "Unable to find an acceptable loss with max iterations. Final loss = {}.",
203                loss
204            ));
205        }
206
207        Ok(possible_target_base_delta)
208    }
209
210    /// The derivative of the equation for calculating the rate after a long.
211    ///
212    /// For some `$r = \frac{(1 - p(x))}{(p(x) \cdot t)}$`, where $p(x)$
213    /// is the spot price after a long of `delta_base``$= x$` was opened and
214    /// `$t$` is the annualized position duration, the rate derivative is:
215    ///
216    /// ```math
217    /// r'(x) = \frac{(-p'(x) \cdot p(x) t
218    /// - (1 - p(x)) (p'(x) \cdot t))}{(p(x) \cdot t)^2} //
219    /// r'(x) = \frac{-p'(x)}{t \cdot p(x)^2}
220    /// ```
221    ///
222    /// We return `$-r'(x)$` because negative numbers cannot be represented by FixedPoint<U256>.
223    fn rate_after_long_derivative_negation(
224        &self,
225        base_amount: FixedPoint<U256>,
226        bond_amount: FixedPoint<U256>,
227    ) -> Result<FixedPoint<U256>> {
228        let price = self.calculate_spot_price_after_long(base_amount, Some(bond_amount))?;
229        let price_derivative = self.price_after_long_derivative(base_amount, bond_amount)?;
230        // The actual equation we want to represent is:
231        // r' = -p' / (t p^2)
232        // We can do a trick to return a positive-only version and
233        // indicate that it should be negative in the fn name.
234        // We use price * price instead of price.pow(fixed!(2e18)) to avoid error introduced by pow.
235        Ok(price_derivative / (self.annualized_position_duration() * price * price))
236    }
237
238    /// The derivative of the price after a long.
239    ///
240    /// The price after a long that moves shares by $\Delta z$ and bonds by
241    /// `$\Delta y$` is equal to:
242    ///
243    /// ```math
244    /// p(\Delta z) = \left( \frac{\mu \cdot
245    ///     (z_{0} + \Delta z - (\zeta_{0} + \Delta \zeta))}
246    ///     {y - \Delta y} \right)^{t_{s}}
247    /// ```
248    ///
249    /// where `$t_s$` is the time stretch constant and `$z_{e,0}$` is the
250    /// initial effective share reserves, and `$\zeta$` is the zeta adjustment.
251    /// The zeta adjustment is constant when opening a long, i.e.
252    /// `$\Delta \zeta = 0$`, so we drop the subscript. Equivalently, for some
253    /// amount of `delta_base` `$= \Delta x$` provided to open a long,
254    /// we can write:
255    ///
256    /// ```math
257    /// p(\Delta x) = \left(
258    ///     \frac{\mu (z_{0} + \frac{1}{c}
259    ///     \cdot \left( \Delta x - \Phi_{g,ol}(\Delta x) \right) - \zeta)}
260    ///     {y_0 - y(\Delta x)}
261    /// \right)^{t_{s}}
262    /// ```
263    ///
264    /// where `$\Phi_{g,ol}(\Delta x)$` is the
265    /// [open_long_governance_fee](State::open_long_governance_fee),
266    /// `$y(\Delta x)$` is the [bond_amount](State::calculate_open_long),
267    ///
268    /// To compute the derivative, we first define some auxiliary variables:
269    ///
270    /// ```math
271    /// a(\Delta x) &= \mu (z_{0} + \frac{\Delta x}{c} - \frac{\Phi_{g,ol}(\Delta x)}{c} - \zeta) \\
272    ///     &= \mu \left( z_{e,0} + \frac{\Delta x}{c} - \frac{\Phi_{g,ol}(\Delta x)}{c} \right) \\
273    /// b(\Delta x) &= y_0 - y(\Delta x) \\
274    /// v(\Delta x) &= \frac{a(\Delta x)}{b(\Delta x)}
275    /// ```
276    ///
277    /// and thus `$p(\Delta x) = v(\Delta x)^{t_{s}}$`.
278    /// Given these, we can write out intermediate derivatives:
279    ///
280    /// ```math
281    /// \begin{aligned}
282    /// a'(\Delta x) &= \frac{\mu}{c} (1 - \Phi_{g,ol}'(\Delta x)) \\
283    /// b'(\Delta x) &= -y'(\Delta x) \\
284    /// v'(\Delta x) &= \frac{b(\Delta x) \cdot a'(\Delta x) - a(\Delta x) \cdotb'(\Delta x)}{b(\Delta x)^2}
285    /// \end{aligned}
286    /// ```
287    ///
288    /// And finally, the price after long derivative is:
289    ///
290    /// ```math
291    /// p'(\Delta x) = v'(\Delta x) \cdot t_{s} \cdot v(\Delta x)^{(t_{s} - 1)}
292    /// ```
293    ///
294    fn price_after_long_derivative(
295        &self,
296        base_amount: FixedPoint<U256>,
297        bond_amount: FixedPoint<U256>,
298    ) -> Result<FixedPoint<U256>> {
299        // g'(x) = \phi_g \phi_c (1 - p_0)
300        let gov_fee_derivative = self.governance_lp_fee()
301            * self.curve_fee()
302            * (fixed!(1e18) - self.calculate_spot_price()?);
303
304        // a(x) = mu * (z_{e,0} + 1/c (x - g(x))
305        let inner_numerator = self.mu()
306            * (self.ze()?
307                + (base_amount - self.open_long_governance_fee(base_amount, None)?)
308                    .div_down(self.vault_share_price()));
309
310        // a'(x) = (mu / c) (1 - g'(x))
311        let inner_numerator_derivative = self
312            .mu()
313            .mul_div_down(fixed!(1e18) - gov_fee_derivative, self.vault_share_price());
314        //(self.mu() / self.vault_share_price()) * (fixed!(1e18) - gov_fee_derivative);
315
316        // b(x) = y_0 - y(x)
317        let inner_denominator = self.bond_reserves() - bond_amount;
318
319        // b'(x) = -y'(x)
320        // -b'(x) = y'(x)
321        let long_amount_derivative = self.calculate_open_long_derivative(base_amount)?;
322
323        // v(x) = a(x) / b(x)
324        // v'(x) = ( b(x) * a'(x) - a(x) * b'(x) ) / b(x)^2
325        //       = ( b(x) * a'(x) + a(x) * -b'(x) ) / b(x)^2
326        // Note that we are adding the negative b'(x) to avoid negative fixedpoint numbers
327        let inner_derivative = (inner_denominator * inner_numerator_derivative
328            + inner_numerator * long_amount_derivative)
329            / (inner_denominator * inner_denominator);
330
331        // p'(x) = v'(x) * t_s * v(x)^(t_s - 1)
332        // p'(x) = v'(x) * t_s * v(x)^(-1)^(1 - t_s)
333        // v(x) is flipped to (denominator / numerator) to avoid a negative exponent
334        Ok(inner_derivative
335            * self.time_stretch()
336            * (inner_denominator / inner_numerator).pow(fixed!(1e18) - self.time_stretch())?)
337    }
338
339    /// Calculate the base & bond trade amount for an open long trade that moves the
340    /// current state to the given desired ending reserve levels.
341    ///
342    /// Given a target ending pool share reserves, `$z_1$`, and bond reserves,
343    /// `$y_1$`, the trade deltas to achieve that state would be:
344    ///
345    /// From the pool's perspective:
346    /// ```math
347    /// z_1 = z_0 + \Delta z \\
348    /// \Delta z = z_1 - z_0
349    /// ```
350    ///
351    /// From the trader's perspective, for a provided `base_amount`
352    /// `$= \Delta x$`, the pool share reserves update, `$\Delta z$`, would be:
353    /// ```math
354    /// \Delta z = \frac{1}{c} (\Delta x - \Phi_{g,ol}(\Delta x))
355    /// ```
356    ///
357    /// Solving for the change in base:
358    /// ```math
359    /// \begin{aligned}
360    /// c \cdot \Delta z
361    ///   &= \Delta x - \Phi_{g,ol}(\Delta x) \\
362    /// c \cdot \Delta z
363    ///   &= \Delta x - (1 - p) \cdot \phi_c \cdot \phi_g \cdot \Delta x \\
364    /// c \cdot \Delta z
365    ///   &= \Delta x \cdot (1 - (1 - p) \cdot \phi_c \cdot \phi_g) \\
366    /// &\therefore \\
367    /// \Delta x
368    ///   &= \frac{c \cdot \Delta z}{(1 - (1 - p) \cdot \phi_c \cdot \phi_g)}
369    /// \end{aligned}
370    /// ```
371    ///
372    /// These should be equal, therefore:
373    /// ```math
374    /// \Delta x = \frac{c \cdot \Delta z}{(1 - (1 - p) \cdot \phi_c \cdot \phi_g)} \\
375    /// \Delta x = \frac{c \cdot (z_1 - z_0)}{(1 - (1 - p) \cdot \phi_c \cdot \phi_g)} \\
376    /// ```
377    /// where `$c$` is the vault share price, `$p$` is the original spot price,
378    /// and `$\Phi_{g,ol}(\Delta x)$` is the
379    /// (open long governance fee)[State::open_long_governance_fee].
380    ///
381    /// The change in bonds, $\Delta y$ is equal for the trader and the pool and
382    /// can be determined with (`calculate_open_long`)[State::calculate_open_long].
383    fn long_trade_needed_given_reserves(
384        &self,
385        ending_share_reserves: FixedPoint<U256>,
386        ending_bond_reserves: FixedPoint<U256>,
387    ) -> Result<(FixedPoint<U256>, FixedPoint<U256>)> {
388        if self.bond_reserves() < ending_bond_reserves {
389            return Err(eyre!(
390                "expected bond_reserves={} >= ending_bond_reserves={}",
391                self.bond_reserves(),
392                ending_bond_reserves,
393            ));
394        }
395        if ending_share_reserves < self.share_reserves() {
396            return Err(eyre!(
397                "expected ending_share_reserves={} >= share_reserves={}",
398                ending_share_reserves,
399                self.share_reserves(),
400            ));
401        }
402        let share_delta = ending_share_reserves - self.share_reserves();
403        let fees = fixed!(1e18)
404            - (fixed!(1e18) - self.calculate_spot_price()?)
405                * self.curve_fee()
406                * self.governance_lp_fee();
407        let base_delta = self.vault_share_price().mul_div_down(share_delta, fees);
408        let bond_delta = self.calculate_open_long(base_delta)?;
409        Ok((base_delta, bond_delta))
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use std::panic;
416
417    use ethers::types::U256;
418    use fixedpointmath::{uint256, FixedPointValue};
419    use hyperdrive_test_utils::{chain::TestChain, constants::FUZZ_RUNS};
420    use rand::{thread_rng, Rng};
421
422    use super::*;
423    use crate::test_utils::agent::HyperdriveMathAgent;
424
425    #[tokio::test]
426    async fn fuzz_long_trade_needed_given_reserves() -> Result<()> {
427        let base_reserve_test_tolerance = fixed!(1e10);
428        let bond_reserve_test_tolerance = fixed!(1e10);
429        let mut rng = thread_rng();
430        for _ in 0..*FUZZ_RUNS {
431            let state = rng.gen::<State>();
432            // Get a random long trade amount.
433            let checkpoint_exposure = rng
434                .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
435                .raw()
436                .flip_sign_if(rng.gen());
437            let max_long_trade = match panic::catch_unwind(|| {
438                state.calculate_max_long(U256::MAX, checkpoint_exposure, None)
439            }) {
440                Ok(max_trade) => match max_trade {
441                    Ok(max_trade) => max_trade,
442                    Err(_) => continue, // Max threw an Err
443                },
444                Err(_) => continue, // Max threw a panic
445            };
446            let long_base_amount =
447                rng.gen_range(state.minimum_transaction_amount()..=max_long_trade);
448            // Do the long to see the bond delta (same amount for the user & pool in this case).
449            let long_bond_amount = state.calculate_open_long(long_base_amount)?;
450            // Get the reserve levels after the state was updated from the open long.
451            let updated_state = state
452                .calculate_pool_state_after_open_long(long_base_amount, Some(long_bond_amount))?;
453            let (final_share_reserves, final_bond_reserves) = (
454                FixedPoint::from(updated_state.info.share_reserves),
455                FixedPoint::from(updated_state.info.bond_reserves),
456            );
457            // Estimate the trade amounts from the final reserve levels.
458            let (estimated_base_amount, estimated_bond_amount) = state
459                .long_trade_needed_given_reserves(final_share_reserves, final_bond_reserves)?;
460            // Make sure the estimates match the realized transaction amounts.
461            let base_error = if estimated_base_amount > long_base_amount {
462                estimated_base_amount - long_base_amount
463            } else {
464                long_base_amount - estimated_base_amount
465            };
466            assert!(
467                base_error <= base_reserve_test_tolerance,
468                "expected abs(estimated_base_amount={}-long_base_amount={})={} <= test_tolerance={}",
469                estimated_base_amount,
470                long_base_amount,
471                base_error,
472                base_reserve_test_tolerance,
473            );
474            let bond_error = if estimated_bond_amount > long_bond_amount {
475                estimated_bond_amount - long_bond_amount
476            } else {
477                long_bond_amount - estimated_bond_amount
478            };
479            assert!(
480                bond_error <= bond_reserve_test_tolerance,
481                "expected abs(estimated_bond_amount={}-long_bond_amount={})={} <= test_tolerance={}",
482                estimated_bond_amount,
483                long_bond_amount,
484                bond_error,
485                bond_reserve_test_tolerance,
486            );
487        }
488        Ok(())
489    }
490
491    #[tokio::test]
492    async fn test_calculate_targeted_long_with_budget() -> Result<()> {
493        // Spawn a test chain and create two agents -- Alice and Bob.
494        // Alice is funded with a large amount of capital so that she can initialize
495        // the pool. Bob is funded with a random amount of capital so that we
496        // can test `calculate_targeted_long` when budget is the primary constraint
497        // and when it is not.
498        let allowable_solvency_error = fixed!(1e5);
499        let allowable_budget_error = fixed!(1e5);
500        let allowable_rate_error = fixed!(1e11);
501        let num_newton_iters = 7;
502
503        // Initialize a test chain and agents.
504        let chain = TestChain::new().await?;
505        let mut alice = chain.alice().await?;
506        let mut bob = chain.bob().await?;
507        let config = bob.get_config().clone();
508
509        // Fuzz test
510        let mut rng = thread_rng();
511        for _ in 0..*FUZZ_RUNS {
512            // Snapshot the chain.
513            let id = chain.snapshot().await?;
514
515            // Alice initializes the pool.
516            // Large budget for initializing the pool.
517            let contribution = fixed!(1_000_000e18);
518            alice.fund(contribution).await?;
519            let initial_fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
520            alice
521                .initialize(initial_fixed_rate, contribution, None)
522                .await?;
523
524            // Small lower bound on Bob's budget for resource-constrained targeted longs.
525            let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
526            // Half the time we will open a long & let it mature.
527            if rng.gen_range(0..=1) == 0 {
528                // Open a long.
529                let max_long =
530                    bob.get_state()
531                        .await?
532                        .calculate_max_long(U256::MAX, I256::from(0), None)?;
533                let long_amount =
534                    (max_long / fixed!(100e18)).max(config.minimum_transaction_amount.into());
535                bob.fund(long_amount + budget).await?;
536                bob.open_long(long_amount, None, None).await?;
537                // Advance time to just after maturity.
538                let variable_rate = rng.gen_range(fixed!(0)..=fixed!(0.5e18));
539                let time_amount = FixedPoint::from(config.position_duration) * fixed!(1.05e18); // 1.05 * position_duraiton
540                alice.advance_time(variable_rate, time_amount).await?;
541                // Checkpoint to auto-close the position.
542                alice
543                    .checkpoint(alice.latest_checkpoint().await?, uint256!(0), None)
544                    .await?;
545            }
546            // Else we will just fund a random budget amount and do the targeted long.
547            else {
548                bob.fund(budget).await?;
549            }
550
551            // Some of the checkpoint passes and variable interest accrues.
552            alice
553                .checkpoint(alice.latest_checkpoint().await?, uint256!(0), None)
554                .await?;
555            let variable_rate = rng.gen_range(fixed!(0)..=fixed!(0.5e18));
556            alice
557                .advance_time(
558                    variable_rate,
559                    FixedPoint::from(config.checkpoint_duration) * fixed!(0.5e18),
560                )
561                .await?;
562
563            // Get a targeted long amount.
564            let target_rate = bob.get_state().await?.calculate_spot_rate()?
565                / rng.gen_range(fixed!(1.0001e18)..=fixed!(10e18));
566            let targeted_long_result = bob
567                .calculate_targeted_long(
568                    target_rate,
569                    Some(num_newton_iters),
570                    Some(allowable_rate_error),
571                )
572                .await;
573
574            // Bob opens a targeted long.
575            let current_state = bob.get_state().await?;
576            let max_spot_price_before_long = current_state.calculate_max_spot_price()?;
577            match targeted_long_result {
578                // If the code ran without error, open the long
579                Ok(targeted_long) => {
580                    bob.open_long(targeted_long, None, None).await?;
581                }
582
583                // Else parse the error for a to improve error messaging.
584                Err(e) => {
585                    // If the fn failed it's possible that the target rate would be insolvent.
586                    if e.to_string()
587                        .contains("Unable to find an acceptable loss with max iterations")
588                    {
589                        let max_long = bob.calculate_max_long(None).await?;
590                        let rate_after_max_long =
591                            current_state.calculate_spot_rate_after_long(max_long, None)?;
592                        // If the rate after the max long is at or below the target, then we could have hit it.
593                        if rate_after_max_long <= target_rate {
594                            return Err(eyre!(
595                                "ERROR {}\nA long that hits the target rate exists but was not found.",
596                                e
597                            ));
598                        }
599                        // Otherwise the target would have resulted in insolvency and wasn't possible.
600                        else {
601                            return Err(eyre!(
602                                "ERROR {}\nThe target rate would result in insolvency.",
603                                e
604                            ));
605                        }
606                    }
607                    // If the error is not the one we're looking for, return it, causing the test to fail.
608                    else {
609                        return Err(e);
610                    }
611                }
612            }
613
614            // Three things should be true after opening the long:
615            //
616            // 1. The pool's spot price is under the max spot price prior to
617            //    considering fees
618            // 2. The pool's solvency is above zero.
619            // 3. IF Bob's budget is not consumed; then new rate is close to the target rate
620
621            // Check that our resulting price is under the max
622            let current_state = alice.get_state().await?;
623            let spot_price_after_long = current_state.calculate_spot_price()?;
624            assert!(
625                max_spot_price_before_long > spot_price_after_long,
626                "Resulting price is greater than the max."
627            );
628
629            // Check solvency
630            let is_solvent = { current_state.calculate_solvency()? > allowable_solvency_error };
631            assert!(is_solvent, "Resulting pool state is not solvent.");
632
633            let new_rate = current_state.calculate_spot_rate()?;
634            // If the budget was NOT consumed, then we assume the target was hit.
635            if bob.base() > allowable_budget_error {
636                // Actual price might result in long overshooting the target.
637                let abs_error = if target_rate > new_rate {
638                    target_rate - new_rate
639                } else {
640                    new_rate - target_rate
641                };
642                assert!(
643                    abs_error <= allowable_rate_error,
644                    "target_rate was {}, realized rate is {}. abs_error={} was not <= {}.",
645                    target_rate,
646                    new_rate,
647                    abs_error,
648                    allowable_rate_error
649                );
650            }
651            // Else, we should have undershot,
652            // or by some coincidence the budget was the perfect amount
653            // and we hit the rate exactly.
654            else {
655                assert!(
656                    new_rate >= target_rate,
657                    "The new_rate={} should be >= target_rate={} when budget constrained.",
658                    new_rate,
659                    target_rate
660                );
661            }
662
663            // Revert to the snapshot and reset the agent's wallets.
664            chain.revert(id).await?;
665            alice.reset(Default::default()).await?;
666            bob.reset(Default::default()).await?;
667        }
668
669        Ok(())
670    }
671}