hyperdrive_math/short/
open.rs

1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, FixedPoint};
4
5use crate::{calculate_rate_given_fixed_price, State, YieldSpace};
6
7impl State {
8    /// Calculates the amount of base the trader will need to deposit for a
9    /// short of a given size.
10    ///
11    /// For some number of bonds being shorted, `$\Delta y$`, the short deposit,
12    /// `$D(\Delta y)$`, is made up of several components:
13    ///
14    /// - The short principal:
15    ///   `$P_{\text{lp}}(\Delta y)$`
16    /// - The curve fee:
17    ///   `$\Phi_{c,os}(\Delta y) = \phi_{c} \cdot ( 1 - p_{0} ) \cdot \Delta y$`
18    /// - The governance-curve fee:
19    ///   `$\Phi_{g,os}(\Delta y) = \phi_{g} \Phi_{c,os}(\Delta y)$`
20    /// - The flat fee:
21    ///   `$\Phi_{f,os}(\Delta y) = \tfrac{1}{c} ( \Delta y \cdot (1 - t) \cdot \phi_{f} )$`
22    /// - The total value in shares that underlies the bonds:
23    ///   `$\tfrac{c_1}{c_0 \cdot c} \Delta y$`
24    ///
25    /// The short principal is given by:
26    ///
27    /// ```math
28    /// P_{\text{lp}}(\Delta y) = z - \tfrac{1}{\mu} \cdot (
29    ///     \tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s})
30    /// )^{\tfrac{1}{1 - t_s}}
31    /// ```
32    ///
33    /// The adjusted value in shares that underlies the bonds is given by:
34    ///
35    /// ```math
36    /// P_\text{adj} = \left( \frac{c_1}{c_0} + \phi_f \right) \cdot \frac{\Delta y}{c}
37    /// ```
38    ///
39    /// And finally the short deposit in base is:
40    ///
41    /// ```math
42    /// D(\Delta y) =
43    /// \begin{cases}
44    ///     P_\text{adj} - P_{\text{lp}}(\Delta y) + \Phi_{c}(\Delta y),
45    ///       & \text{if } P_{\text{adj}} > P_{\text{lp}}(\Delta y) - \Phi_{c}(\Delta y) \\
46    ///     0, & \text{otherwise}
47    /// \end{cases}
48    /// ```
49    pub fn calculate_open_short(
50        &self,
51        bond_amount: FixedPoint<U256>,
52        open_vault_share_price: FixedPoint<U256>,
53    ) -> Result<FixedPoint<U256>> {
54        // Ensure that the bond amount is greater than or equal to the minimum
55        // transaction amount.
56        if bond_amount < self.minimum_transaction_amount() {
57            return Err(eyre!(
58                "MinimumTransactionAmount: Input amount too low. bond_amount = {:#?} must be >= {:#?}",
59                bond_amount,
60                self.minimum_transaction_amount()
61            ));
62        }
63
64        // If the open share price hasn't been set, we use the current share
65        // price, since this is what will be set as the checkpoint share price
66        // in this transaction.
67        let open_vault_share_price = if open_vault_share_price == fixed!(0) {
68            self.vault_share_price()
69        } else {
70            open_vault_share_price
71        };
72
73        // Calculate the effect that opening the short will have on the pool's
74        // share reserves.
75        let share_reserves_delta = self.calculate_short_principal(bond_amount)?;
76
77        // NOTE: Round up to make the check stricter.
78        //
79        // If the base proceeds of selling the bonds is greater than the bond
80        // amount, then the trade occurred in the negative interest domain.
81        // We revert in these pathological cases.
82        if share_reserves_delta.mul_up(self.vault_share_price()) > bond_amount {
83            return Err(eyre!(
84                "InsufficientLiquidity: Negative Interest.
85                expected bond_amount={} <= share_reserves_delta_in_shares={}",
86                bond_amount,
87                share_reserves_delta
88            ));
89        }
90
91        // NOTE: Round up to overestimate the base deposit.
92        //
93        // The trader will need to deposit capital to pay for the fixed rate,
94        // the fees, and any back-paid interest that will be received back upon
95        // closing the trade.
96        let curve_fee_shares = self
97            .open_short_curve_fee(bond_amount)?
98            .div_up(self.vault_share_price());
99        if share_reserves_delta < curve_fee_shares {
100            return Err(eyre!(format!(
101                "The transaction curve fee = {}, computed with coefficient = {},
102                is too high. It must be less than share reserves delta = {}",
103                curve_fee_shares,
104                self.curve_fee(),
105                share_reserves_delta
106            )));
107        }
108
109        // If negative interest has accrued during the current checkpoint, we
110        // set the close vault share price to equal the open vault share price.
111        // This ensures that shorts don't benefit from negative interest that
112        // accrued during the current checkpoint.
113        let close_vault_share_price = open_vault_share_price.max(self.vault_share_price());
114
115        // Now we can calculate adjusted proceeds account for the backdated
116        // vault price:
117        //
118        // ```math
119        // \text{base_proceeds} = (
120        //    \frac{c1 \cdot \Delta y}{c0 \cdot c}
121        //    + \frac{\Delta y \cdot \phi_f}{c} - \Delta z
122        // ) \cdot c
123        // ```
124        let base_proceeds = self
125            .calculate_short_proceeds_up(
126                bond_amount,
127                share_reserves_delta - curve_fee_shares,
128                open_vault_share_price,
129                close_vault_share_price,
130            )
131            .mul_up(self.vault_share_price());
132
133        Ok(base_proceeds)
134    }
135
136    /// Calculates the derivative of the short deposit function with respect to
137    /// the short amount.
138    ///
139    /// This derivative allows us to use Newton's method to approximate the
140    /// maximum short that a trader can open. The share adjustment derivative is
141    /// a constant:
142    ///
143    /// ```math
144    /// P^{\prime}_{\text{adj}}(\Delta y)
145    /// = \tfrac{c_{1}}{c_{0} \cdot c} + \tfrac{\phi_{f}}{c}
146    /// ```
147    ///
148    /// The curve fee dervative is given by:
149    ///
150    /// ```math
151    /// \Phi^{\prime}_{\text{c}}(\Delta y) = \phi_{c} \cdot (1 - p_0),
152    /// ```
153    ///
154    /// where `$p_0$` is the opening (or initial) spot price. Using these and the
155    /// short principal derivative, we can calculate the open short derivative:
156    ///
157    /// ```math
158    /// D^{\prime}(\Delta y) =
159    ///\begin{cases}
160    ///    c \cdot \left(
161    ///      P^{\prime}_{\text{adj}}(\Delta y)
162    ///      - P^{\prime}_{\text{lp}}(\Delta y)
163    ///      + \Phi^{\prime}_{c,os}(\Delta y)
164    ///    \right),
165    ///    & \text{if }
166    ///      P_{\text{adj}} > P_{\text{lp}}(\Delta y) - \Phi_{c,os}(\Delta y) \\
167    ///    0, & \text{otherwise}
168    ///\end{cases}
169    /// ```
170    pub fn calculate_open_short_derivative(
171        &self,
172        bond_amount: FixedPoint<U256>,
173        open_vault_share_price: FixedPoint<U256>,
174        maybe_initial_spot_price: Option<FixedPoint<U256>>,
175    ) -> Result<FixedPoint<U256>> {
176        // We're avoiding negative interest, so we will cap the close share
177        // price to be greater than or equal to the open share price. This
178        // will assume the max loss for the trader, some of which may be
179        // reimbursed upon closing.
180        let close_vault_share_price = open_vault_share_price.max(self.vault_share_price());
181
182        // Short circuit the derivative if the function returns 0.
183        if self.calculate_short_proceeds_up(
184            bond_amount,
185            self.calculate_short_principal(bond_amount)?
186                - self
187                    .open_short_curve_fee(bond_amount)?
188                    .div_up(self.vault_share_price()),
189            open_vault_share_price,
190            close_vault_share_price,
191        ) == fixed!(0)
192        {
193            return Ok(fixed!(0));
194        }
195
196        let spot_price = match maybe_initial_spot_price {
197            Some(spot_price) => spot_price,
198            None => self.calculate_spot_price()?,
199        };
200
201        // All of these are in base.
202        let share_adjustment_derivative =
203            close_vault_share_price.div_up(open_vault_share_price) + self.flat_fee();
204        let short_principal_derivative = self
205            .calculate_short_principal_derivative(bond_amount)?
206            .mul_up(self.vault_share_price());
207        let curve_fee_derivative = self.curve_fee().mul_up((fixed!(1e18) - spot_price));
208
209        // Multiply by the share price to return base.
210        Ok(share_adjustment_derivative - short_principal_derivative + curve_fee_derivative)
211    }
212
213    /// Calculates the amount of short principal that the LPs need to pay to
214    /// back a short before fees are taken into consideration,
215    /// `$P_\text{lp}(\Delta y)$`.
216    ///
217    /// Let the LP principal that backs $\Delta y$ shorts be given by
218    /// `$P_{\text{lp}}(\Delta y)$`. We can solve for this in terms of
219    /// `$\Delta y$` using the YieldSpace invariant:
220    ///
221    /// ```math
222    /// k = \tfrac{c}{\mu} \cdot (\mu \cdot (z - P(\Delta y)))^{1 - t_s} + (y + \Delta y)^{1 - t_s} \\
223    /// \implies \\
224    /// P_{\text{lp}}(\Delta y) = z - \tfrac{1}{\mu}
225    /// \cdot \left(
226    ///   \tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s})
227    /// \right)^{\tfrac{1}{1 - t_s}}
228    /// ```
229    pub fn calculate_short_principal(
230        &self,
231        bond_amount: FixedPoint<U256>,
232    ) -> Result<FixedPoint<U256>> {
233        self.calculate_shares_out_given_bonds_in_down(bond_amount)
234    }
235
236    /// Calculates the derivative of the short principal w.r.t. the amount of
237    /// bonds that are shorted.
238    ///
239    /// The derivative is:
240    ///
241    /// ```math
242    /// P^{\prime}_{\text{lp}}(\Delta y) = \tfrac{1}{c} \cdot (y + \Delta y)^{-t_s}
243    /// \cdot \left(
244    ///     \tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s})
245    /// \right)^{\tfrac{t_s}{1 - t_s}}
246    /// ```
247    pub fn calculate_short_principal_derivative(
248        &self,
249        bond_amount: FixedPoint<U256>,
250    ) -> Result<FixedPoint<U256>> {
251        // Avoid negative exponent by putting the term in the denominator.
252        let lhs = fixed!(1e18).div_up(
253            self.vault_share_price()
254                .mul_up((self.bond_reserves() + bond_amount).pow(self.time_stretch())?),
255        );
256        let rhs = (self
257            .initial_vault_share_price()
258            .div_up(self.vault_share_price())
259            .mul_up(
260                self.k_up()?
261                    - (self.bond_reserves() + bond_amount)
262                        .pow(fixed!(1e18) - self.time_stretch())?,
263            ))
264        .pow(
265            self.time_stretch()
266                .div_up(fixed!(1e18) - self.time_stretch()),
267        )?;
268        Ok(lhs.mul_up(rhs))
269    }
270
271    /// Calculate an updated pool state after opening a short.
272    ///
273    /// For a given bond amount and share amount,
274    /// the reserves are updated such that
275    /// `state.bond_reserves += bond_amount` and
276    /// `state.share_reserves -= share_amount`.
277    pub fn calculate_pool_state_after_open_short(
278        &self,
279        bond_amount: FixedPoint<U256>,
280        maybe_share_amount: Option<FixedPoint<U256>>,
281    ) -> Result<Self> {
282        let share_amount = match maybe_share_amount {
283            Some(share_amount) => share_amount,
284            None => self.calculate_pool_share_delta_after_open_short(bond_amount)?,
285        };
286        let mut state = self.clone();
287        state.info.bond_reserves += bond_amount.into();
288        state.info.share_reserves -= share_amount.into();
289        Ok(state)
290    }
291
292    /// Calculate the share delta to be applied to the pool after opening a short.
293    pub fn calculate_pool_share_delta_after_open_short(
294        &self,
295        bond_amount: FixedPoint<U256>,
296    ) -> Result<FixedPoint<U256>> {
297        let curve_fee_base = self.open_short_curve_fee(bond_amount)?;
298        let curve_fee_shares = curve_fee_base.div_up(self.vault_share_price());
299        let gov_curve_fee_shares = self
300            .open_short_governance_fee(bond_amount, Some(curve_fee_base))?
301            .div_up(self.vault_share_price());
302        let short_principal = self.calculate_short_principal(bond_amount)?;
303        if short_principal.mul_up(self.vault_share_price()) > bond_amount {
304            return Err(eyre!("InsufficientLiquidity: Negative Interest"));
305        }
306        if short_principal < (curve_fee_shares - gov_curve_fee_shares) {
307            return Err(eyre!(
308                "short_principal={:#?} is too low to account for fees={:#?}",
309                short_principal,
310                curve_fee_shares - gov_curve_fee_shares
311            ));
312        }
313        Ok(short_principal - (curve_fee_shares - gov_curve_fee_shares))
314    }
315
316    /// Calculates the spot price after opening a short.
317    /// Arguments are deltas that would be applied to the pool.
318    pub fn calculate_spot_price_after_short(
319        &self,
320        bond_amount: FixedPoint<U256>,
321        maybe_base_amount: Option<FixedPoint<U256>>,
322    ) -> Result<FixedPoint<U256>> {
323        let share_amount = match maybe_base_amount {
324            Some(base_amount) => base_amount / self.vault_share_price(),
325            None => self.calculate_pool_share_delta_after_open_short(bond_amount)?,
326        };
327        let updated_state =
328            self.calculate_pool_state_after_open_short(bond_amount, Some(share_amount))?;
329        updated_state.calculate_spot_price()
330    }
331
332    /// Calculate the spot rate after a short has been opened.
333    /// If a base_amount is not provided, then one is estimated
334    /// using [calculate_pool_share_delta_after_open_short](State::calculate_pool_share_delta_after_open_short).
335    ///
336    /// We calculate the rate for a fixed length of time as:
337    ///
338    /// ```math
339    /// r(\Delta y) = \frac{1 - p(\Delta y)}{p(\Delta y) \cdot t}
340    /// ```
341    ///
342    /// where `$p(\Delta y)$` is the spot price after a short for
343    /// `delta_bonds` `$= \Delta y$` and `$t$` is the normalized position
344    /// druation.
345    pub fn calculate_spot_rate_after_short(
346        &self,
347        bond_amount: FixedPoint<U256>,
348        maybe_base_amount: Option<FixedPoint<U256>>,
349    ) -> Result<FixedPoint<U256>> {
350        let price = self.calculate_spot_price_after_short(bond_amount, maybe_base_amount)?;
351        Ok(calculate_rate_given_fixed_price(
352            price,
353            self.position_duration(),
354        ))
355    }
356
357    /// Calculate the implied rate of opening a short at a given size. This rate
358    /// is calculated as an APY.
359    ///
360    /// Given the effective fixed rate the short will pay
361    /// `$r_{\text{effective}}$` and the variable rate the short will receive
362    /// `$r_{\text{variable}}$`, the short's implied APY,
363    /// `$r_{\text{implied}}$` will be:
364    ///
365    /// ```math
366    /// r_{\text{implied}} = \frac{r_{\text{variable}}
367    /// - r_{\text{effective}}}{r_{\text{effective}}}
368    /// ```
369    ///
370    /// We can short-cut this calculation using the amount of base the short
371    /// will pay and comparing this to the amount of base the short will receive
372    /// if the variable rate stays the same. The implied rate is just the ROI
373    /// if the variable rate stays the same.
374    ///
375    /// To do this, we must figure out the term-adjusted yield `$TPY$` according
376    /// to the position duration `$t$`. Since we start off from a compounded APY
377    /// and also output a compounded TPY, the compounding frequency `$f$` is
378    /// simplified away. Thus, the adjusted yield will be:
379    ///
380    /// ```math
381    /// \text{APR} = f \cdot (( 1 + \text{APY})^{\tfrac{1}{f}}  - 1)
382    /// ```
383    ///
384    /// Therefore,
385    ///
386    /// ```math
387    /// \begin{aligned}
388    /// TPY &= (1 + \frac{APR}{f})^{d \cdot f} \\
389    /// &= (1 + APY)^{d} - 1
390    /// \end{aligned}
391    /// ```
392    ///
393    /// We use the TPY to figure out the base proceeds, and calculate the rate
394    /// of return based on the short's opening cost. Since shorts must backpay
395    /// the variable interest accrued since the last checkpoint, we subtract
396    /// that from the opening cost, as they get it back upon closing the short.
397    pub fn calculate_implied_rate(
398        &self,
399        bond_amount: FixedPoint<U256>,
400        open_vault_share_price: FixedPoint<U256>,
401        variable_apy: FixedPoint<U256>,
402    ) -> Result<I256> {
403        let full_base_paid = self.calculate_open_short(bond_amount, open_vault_share_price)?;
404        let backpaid_interest = bond_amount
405            .mul_div_down(self.vault_share_price(), open_vault_share_price)
406            - bond_amount;
407        let base_paid = full_base_paid - backpaid_interest;
408        let tpy =
409            (fixed!(1e18) + variable_apy).pow(self.annualized_position_duration())? - fixed!(1e18);
410        let base_proceeds = bond_amount * tpy;
411        if base_proceeds > base_paid {
412            Ok(I256::try_from(
413                (base_proceeds - base_paid) / (base_paid * self.annualized_position_duration()),
414            )?)
415        } else {
416            Ok(-I256::try_from(
417                (base_paid - base_proceeds) / (base_paid * self.annualized_position_duration()),
418            )?)
419        }
420    }
421
422    /// Estimate the bonds that would be shorted given a base deposit amount.
423    ///
424    /// The LP principal in shares is bounded from below by
425    /// $S(min)$, which is the output of calculating the short principal on the
426    /// minimum transaction amount.
427    ///
428    /// Given this, we can compute the a conservative estimate of the bonds
429    /// shorted, $\Delta y$, given the trader's base deposit amount,
430    /// $c \cdot D(\Delta y)$:
431    ///
432    /// ```math
433    /// A = \frac{c_1}{c_0} + \phi_f + \phi_c \cdot (1 - p) \\
434    ///
435    /// D(\Delta y) = A \cdot \frac{\Delta y}{c} - S(\Delta y) \\
436    ///
437    /// S(\text{min\_tx}) \le S(\Delta y) \\
438    ///
439    /// \therefore \\
440    ///
441    /// \Delta y(D) &\ge \frac{c}{A} \cdot \left( D + S(\text{min\_tx}) \right)
442    ///
443    /// ```
444    ///
445    /// The resulting bond amount is guaranteed to cost a trader less than or
446    /// equal to the provided base deposit amount.
447    pub fn calculate_approximate_short_bonds_given_base_deposit(
448        &self,
449        base_deposit: FixedPoint<U256>,
450        open_vault_share_price: FixedPoint<U256>,
451    ) -> Result<FixedPoint<U256>> {
452        let close_vault_share_price = open_vault_share_price.max(self.vault_share_price());
453        let shares_deposit = base_deposit / self.vault_share_price();
454        let minimum_short_principal =
455            self.calculate_short_principal(self.minimum_transaction_amount())?;
456        let price_adjustment_with_fees = close_vault_share_price / open_vault_share_price
457            + self.flat_fee()
458            + self.curve_fee() * (fixed!(1e18) - self.calculate_spot_price()?);
459        let approximate_bond_amount = (self.vault_share_price() / price_adjustment_with_fees)
460            * (shares_deposit + minimum_short_principal);
461        Ok(approximate_bond_amount)
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use std::panic;
468
469    use ethers::types::{U128, U256};
470    use fixedpointmath::{fixed, fixed_u256, int256, FixedPointValue};
471    use hyperdrive_test_utils::{
472        chain::TestChain,
473        constants::{FAST_FUZZ_RUNS, FUZZ_RUNS, SLOW_FUZZ_RUNS},
474    };
475    use hyperdrive_wrappers::wrappers::ihyperdrive::{Checkpoint, Options};
476    use rand::{thread_rng, Rng, SeedableRng};
477    use rand_chacha::ChaCha8Rng;
478
479    use super::*;
480    use crate::test_utils::{
481        agent::HyperdriveMathAgent,
482        preamble::{get_max_short, initialize_pool_with_random_state},
483    };
484
485    #[tokio::test]
486    async fn fuzz_calculate_pool_state_after_open_short() -> Result<()> {
487        // TODO: There must be a rounding error; we should not need a tolerance.
488        let share_adjustment_test_tolerance = fixed_u256!(0);
489        let bond_reserves_test_tolerance = fixed!(0);
490        let share_reserves_test_tolerance = fixed!(1);
491        // Initialize a test chain and agents.
492        let chain = TestChain::new().await?;
493        let mut alice = chain.alice().await?;
494        let mut bob = chain.bob().await?;
495        let mut celine = chain.celine().await?;
496        // Set up a random number generator. We use ChaCha8Rng with a randomly
497        // generated seed, which makes it easy to reproduce test failures given
498        // the seed.
499        let mut rng = {
500            let mut rng = thread_rng();
501            let seed = rng.gen();
502            ChaCha8Rng::seed_from_u64(seed)
503        };
504        for _ in 0..*SLOW_FUZZ_RUNS {
505            // Snapshot the chain & run the preamble.
506            let id = chain.snapshot().await?;
507            initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
508            // Reset the variable rate to zero; get the state.
509            alice.advance_time(fixed!(0), fixed!(0)).await?;
510            let original_state = alice.get_state().await?;
511            // Get a random short amount.
512            let checkpoint_exposure = alice
513                .get_checkpoint_exposure(original_state.to_checkpoint(alice.now().await?))
514                .await?;
515            let max_short_amount = original_state.calculate_max_short(
516                U256::MAX,
517                original_state.vault_share_price(),
518                checkpoint_exposure,
519                None,
520                None,
521            )?;
522            let bond_amount =
523                rng.gen_range(original_state.minimum_transaction_amount()..=max_short_amount);
524            // Mock the trade using Rust.
525            let rust_state =
526                original_state.calculate_pool_state_after_open_short(bond_amount, None)?;
527            // Execute the trade on the contracts.
528            bob.fund(bond_amount * fixed!(1.5e18)).await?;
529            bob.open_short(bond_amount, None, None).await?;
530            let sol_state = alice.get_state().await?;
531            // Check that the results are the same.
532            let rust_share_adjustment = rust_state.share_adjustment();
533            let sol_share_adjustment = sol_state.share_adjustment();
534            let share_adjustment_error = if rust_share_adjustment < sol_share_adjustment {
535                FixedPoint::try_from(sol_share_adjustment - rust_share_adjustment)?
536            } else {
537                FixedPoint::try_from(rust_share_adjustment - sol_share_adjustment)?
538            };
539            assert!(
540                share_adjustment_error <= share_adjustment_test_tolerance,
541                "expected abs(rust_share_adjustment={}-sol_share_adjustment={}) <= test_tolerance={}",
542                rust_share_adjustment, sol_share_adjustment, share_adjustment_test_tolerance
543            );
544            let rust_bond_reserves = rust_state.bond_reserves();
545            let sol_bond_reserves = sol_state.bond_reserves();
546            let bond_reserves_error = if rust_bond_reserves < sol_bond_reserves {
547                sol_bond_reserves - rust_bond_reserves
548            } else {
549                rust_bond_reserves - sol_bond_reserves
550            };
551            assert!(
552                bond_reserves_error <= bond_reserves_test_tolerance,
553                "expected abs(rust_bond_reserves={}-sol_bond_reserves={}) <= test_tolerance={}",
554                rust_bond_reserves,
555                sol_bond_reserves,
556                bond_reserves_test_tolerance
557            );
558            let rust_share_reserves = rust_state.share_reserves();
559            let sol_share_reserves = sol_state.share_reserves();
560            let share_reserves_error = if rust_share_reserves < sol_share_reserves {
561                sol_share_reserves - rust_share_reserves
562            } else {
563                rust_share_reserves - sol_share_reserves
564            };
565            assert!(
566                share_reserves_error <= share_reserves_test_tolerance,
567                "expected abs(rust_share_reserves={}-sol_share_reserves={}) <= test_tolerance={}",
568                rust_share_reserves,
569                sol_share_reserves,
570                share_reserves_test_tolerance
571            );
572            // Revert to the snapshot and reset the agent's wallets.
573            chain.revert(id).await?;
574            alice.reset(Default::default()).await?;
575            bob.reset(Default::default()).await?;
576            celine.reset(Default::default()).await?;
577        }
578        Ok(())
579    }
580
581    #[tokio::test]
582    async fn test_sol_calculate_pool_share_delta_after_open_short() -> Result<()> {
583        let test_tolerance = fixed!(10);
584
585        let chain = TestChain::new().await?;
586        let mut rng = thread_rng();
587        for _ in 0..*FAST_FUZZ_RUNS {
588            let state = rng.gen::<State>();
589            let checkpoint_exposure = {
590                let value = rng.gen_range(fixed_u256!(0)..=fixed!(10_000_000e18));
591                if rng.gen() {
592                    -I256::try_from(value).unwrap()
593                } else {
594                    I256::try_from(value).unwrap()
595                }
596            };
597            // We need to catch panics because of overflows.
598            let max_bond_amount = match panic::catch_unwind(|| {
599                state.calculate_absolute_max_short(
600                    state.calculate_spot_price()?,
601                    checkpoint_exposure,
602                    None,
603                )
604            }) {
605                Ok(max_bond_amount) => match max_bond_amount {
606                    Ok(max_bond_amount) => max_bond_amount,
607                    Err(_) => continue, // Max threw an Err.
608                },
609                Err(_) => continue, // Max threw a panic.
610            };
611            if max_bond_amount < state.minimum_transaction_amount() + fixed!(1) {
612                continue;
613            }
614            let bond_amount = rng.gen_range(state.minimum_transaction_amount()..=max_bond_amount);
615            let rust_pool_delta = state.calculate_pool_share_delta_after_open_short(bond_amount);
616            let curve_fee_base = state.open_short_curve_fee(bond_amount)?;
617            let gov_fee_base =
618                state.open_short_governance_fee(bond_amount, Some(curve_fee_base))?;
619            let fees = curve_fee_base.div_up(state.vault_share_price())
620                - gov_fee_base.div_up(state.vault_share_price());
621            match chain
622                .mock_hyperdrive_math()
623                .calculate_open_short(
624                    state.effective_share_reserves()?.into(),
625                    state.bond_reserves().into(),
626                    bond_amount.into(),
627                    state.time_stretch().into(),
628                    state.vault_share_price().into(),
629                    state.initial_vault_share_price().into(),
630                )
631                .call()
632                .await
633            {
634                Ok(sol_pool_delta) => {
635                    let sol_pool_delta_with_fees = FixedPoint::from(sol_pool_delta) - fees;
636                    let rust_pool_delta_unwrapped = rust_pool_delta.unwrap();
637                    let result_equal = sol_pool_delta_with_fees
638                        <= rust_pool_delta_unwrapped + test_tolerance
639                        && sol_pool_delta_with_fees >= rust_pool_delta_unwrapped - test_tolerance;
640                    assert!(result_equal, "Should be equal.");
641                }
642                Err(_) => {
643                    assert!(rust_pool_delta.is_err())
644                }
645            };
646        }
647        Ok(())
648    }
649
650    #[tokio::test]
651    async fn test_sol_calculate_short_principal() -> Result<()> {
652        // This test is the same as the yield_space.rs `fuzz_calculate_max_buy_shares_in_safe`,
653        // but is worth having around in case we ever change how we compute short principal.
654        let chain = TestChain::new().await?;
655        let mut rng = thread_rng();
656        let state = rng.gen::<State>();
657        let bond_amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18));
658        let actual = state.calculate_short_principal(bond_amount);
659        match chain
660            .mock_yield_space_math()
661            .calculate_shares_out_given_bonds_in_down_safe(
662                state.effective_share_reserves()?.into(),
663                state.bond_reserves().into(),
664                bond_amount.into(),
665                (fixed!(1e18) - state.time_stretch()).into(),
666                state.vault_share_price().into(),
667                state.initial_vault_share_price().into(),
668            )
669            .call()
670            .await
671        {
672            Ok((expected, expected_status)) => {
673                assert_eq!(actual.is_ok(), expected_status);
674                assert_eq!(actual.unwrap_or(fixed!(0)), expected.into());
675            }
676            Err(_) => assert!(actual.is_err()),
677        }
678        Ok(())
679    }
680
681    /// This test empirically tests `calculate_short_principal_derivative` by calling
682    /// `calculate_short_principal` at two points and comparing the empirical result
683    /// with the output of `calculate_short_principal_derivative`.
684    #[tokio::test]
685    async fn fuzz_calculate_short_principal_derivative() -> Result<()> {
686        // We use a relatively large epsilon here due to the underlying fixed point pow
687        // function not being monotonically increasing.
688        let empirical_derivative_epsilon = fixed!(1e14);
689        let test_tolerance = fixed!(1e14);
690
691        let mut rng = thread_rng();
692        for _ in 0..*FAST_FUZZ_RUNS {
693            let state = rng.gen::<State>();
694
695            // Min trade amount should be at least 1,000x the derivative epsilon.
696            let bond_amount = rng.gen_range(fixed!(1e18)..=fixed!(10_000_000e18));
697
698            // Calculate the function output at the bond amount and a small perturbation away.
699            let f_x = match panic::catch_unwind(|| state.calculate_short_principal(bond_amount)) {
700                Ok(result) => match result {
701                    Ok(result) => result,
702                    Err(_) => continue, // The amount resulted in the pool being insolvent.
703                },
704                Err(_) => continue, // Overflow or underflow error from FixedPoint<U256>.
705            };
706            let f_x_plus_delta = match panic::catch_unwind(|| {
707                state.calculate_short_principal(bond_amount + empirical_derivative_epsilon)
708            }) {
709                Ok(result) => match result {
710                    Ok(result) => result,
711                    Err(_) => continue, // The amount resulted in the pool being insolvent.
712                },
713                Err(_) => continue, // Overflow or underflow error from FixedPoint<U256>.
714            };
715
716            // Sanity check
717            assert!(f_x_plus_delta > f_x);
718
719            // Compute the empirical and analytical derivatives.
720            let empirical_derivative = (f_x_plus_delta - f_x) / empirical_derivative_epsilon;
721            let short_principal_derivative =
722                state.calculate_short_principal_derivative(bond_amount)?;
723
724            // Ensure that the empirical and analytical derivatives match.
725            let derivative_diff = if short_principal_derivative >= empirical_derivative {
726                short_principal_derivative - empirical_derivative
727            } else {
728                empirical_derivative - short_principal_derivative
729            };
730            assert!(
731                derivative_diff < test_tolerance,
732                "expected abs(derivative_diff={}) < test_tolerance={};
733                calculated_derivative={}, emperical_derivative={}",
734                derivative_diff,
735                test_tolerance,
736                short_principal_derivative,
737                empirical_derivative
738            );
739        }
740
741        Ok(())
742    }
743
744    /// This test empirically tests `calculate_open_short_derivative` by calling
745    /// `calculate_open_short` at two points and comparing the empirical result
746    /// with the output of `calculate_open_short_derivative`.
747    #[tokio::test]
748    async fn fuzz_calculate_open_short_derivative() -> Result<()> {
749        // We use a relatively large epsilon here due to the underlying fixed point pow
750        // function not being monotonically increasing.
751        let empirical_derivative_epsilon = fixed!(1e14);
752        let test_tolerance = fixed!(1e14);
753
754        let mut rng = thread_rng();
755        for _ in 0..*FAST_FUZZ_RUNS {
756            let state = rng.gen::<State>();
757            // Min trade amount should be at least 1,000x the derivative epsilon.
758            let bond_amount = rng.gen_range(fixed!(1e18)..=fixed!(10_000_000e18));
759
760            // Calculate the function output at the bond amount and a small perturbation away.
761            let f_x = match panic::catch_unwind(|| {
762                state.calculate_open_short(bond_amount, state.vault_share_price())
763            }) {
764                Ok(result) => match result {
765                    Ok(result) => result,
766                    Err(_) => continue, // The amount results in the pool being insolvent.
767                },
768                Err(_) => continue, // Overflow or underflow error from FixedPoint<U256>.
769            };
770            let f_x_plus_delta = match panic::catch_unwind(|| {
771                state.calculate_open_short(
772                    bond_amount + empirical_derivative_epsilon,
773                    state.vault_share_price(),
774                )
775            }) {
776                Ok(result) => match result {
777                    Ok(result) => result,
778                    Err(_) => continue, // The amount results in the pool being insolvent.
779                },
780                Err(_) => continue, // Overflow or underflow error from FixedPoint<U256>.
781            };
782
783            // Sanity check
784            assert!(f_x_plus_delta > f_x);
785
786            // Compute the empirical and analytical derivatives.
787            // Setting open, close, and current vault share price to be equal assumes 0% variable yield.
788            let empirical_derivative = (f_x_plus_delta - f_x) / empirical_derivative_epsilon;
789            let short_deposit_derivative = state.calculate_open_short_derivative(
790                bond_amount,
791                state.vault_share_price(),
792                Some(state.calculate_spot_price()?),
793            )?;
794
795            // Ensure that the empirical and analytical derivatives match.
796            let derivative_diff = if short_deposit_derivative >= empirical_derivative {
797                short_deposit_derivative - empirical_derivative
798            } else {
799                empirical_derivative - short_deposit_derivative
800            };
801            assert!(
802                derivative_diff < test_tolerance,
803                "expected abs(derivative_diff={}) < test_tolerance={};
804                calculated_derivative={}, emperical_derivative={}",
805                derivative_diff,
806                test_tolerance,
807                short_deposit_derivative,
808                empirical_derivative
809            );
810        }
811
812        Ok(())
813    }
814
815    #[tokio::test]
816    async fn fuzz_sol_calculate_spot_price_after_short() -> Result<()> {
817        let test_tolerance = fixed!(1e3);
818
819        // Spawn a test chain and create two agents -- Alice and Bob. Alice is
820        // funded with a large amount of capital so that she can initialize the
821        // pool. Bob is funded with a small amount of capital so that we can
822        // test opening a short and verify that the ending spot price is what we
823        // expect.
824        let mut rng = thread_rng();
825        let chain = TestChain::new().await?;
826        let mut alice = chain.alice().await?;
827        let mut bob = chain.bob().await?;
828
829        for _ in 0..*FUZZ_RUNS {
830            // Snapshot the chain.
831            let id = chain.snapshot().await?;
832
833            // Fund Alice and Bob.
834            let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
835            let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
836            let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
837            alice.fund(contribution).await?;
838            bob.fund(budget).await?;
839
840            // Alice initializes the pool.
841            alice.initialize(fixed_rate, contribution, None).await?;
842
843            // Attempt to predict the spot price after opening a short.
844            let mut state = alice.get_state().await?;
845            let bond_amount = rng.gen_range(
846                state.minimum_transaction_amount()..=bob.calculate_max_short(None).await?,
847            );
848
849            // Open the short.
850            bob.open_short(bond_amount, None, None).await?;
851
852            // Calling any Solidity Hyperdrive transaction causes the
853            // mock yield source to accrue some interest. We want to use
854            // the state before the Solidity OpenShort, but with the
855            // vault share price after the block tick.
856            let new_state = alice.get_state().await?;
857            let new_vault_share_price = new_state.vault_share_price();
858            state.info.vault_share_price = new_vault_share_price.into();
859
860            // Verify that the predicted spot price is equal to the ending spot
861            // price.
862            let expected_spot_price = state.calculate_spot_price_after_short(bond_amount, None)?;
863            let actual_spot_price = new_state.calculate_spot_price()?;
864            let abs_spot_price_diff = if actual_spot_price >= expected_spot_price {
865                actual_spot_price - expected_spot_price
866            } else {
867                expected_spot_price - actual_spot_price
868            };
869            assert!(
870                abs_spot_price_diff <= test_tolerance,
871                "expected abs(spot_price_diff={}) <= test_tolerance={};
872                calculated_spot_price={}, actual_spot_price={}",
873                abs_spot_price_diff,
874                test_tolerance,
875                expected_spot_price,
876                actual_spot_price,
877            );
878            // Revert to the snapshot and reset the agent's wallets.
879            chain.revert(id).await?;
880            alice.reset(Default::default()).await?;
881            bob.reset(Default::default()).await?;
882        }
883
884        Ok(())
885    }
886
887    #[tokio::test]
888    async fn test_defaults_calculate_spot_price_after_short() -> Result<()> {
889        let mut rng = thread_rng();
890        let mut num_checks = 0;
891        // We don't need a lot of tests for this because each component is
892        // tested elsewhere.
893        for _ in 0..*SLOW_FUZZ_RUNS {
894            // We use a random state but we will ignore any case where a call
895            // fails because we want to test the default behavior when the state
896            // allows all actions.
897            let state = rng.gen::<State>();
898            let checkpoint_exposure = rng
899                .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
900                .raw()
901                .flip_sign_if(rng.gen());
902            // We need to catch panics because of overflows.
903            let max_bond_amount = match panic::catch_unwind(|| {
904                state.calculate_absolute_max_short(
905                    state.calculate_spot_price()?,
906                    checkpoint_exposure,
907                    Some(3),
908                )
909            }) {
910                Ok(max_bond_amount) => match max_bond_amount {
911                    Ok(max_bond_amount) => max_bond_amount,
912                    Err(_) => continue, // Err; max short insolvent
913                },
914                Err(_) => continue, // panic; likely in FixedPoint<U256>
915            };
916            if max_bond_amount == fixed!(0) {
917                continue;
918            }
919            // Using the default behavior
920            let bond_amount = rng.gen_range(state.minimum_transaction_amount()..=max_bond_amount);
921            let price_with_default = state.calculate_spot_price_after_short(bond_amount, None)?;
922
923            // Using a pre-calculated base amount
924            let base_amount = match state.calculate_pool_share_delta_after_open_short(bond_amount) {
925                Ok(share_amount) => Some(share_amount * state.vault_share_price()),
926                Err(_) => continue,
927            };
928            let price_with_base_amount =
929                state.calculate_spot_price_after_short(bond_amount, base_amount)?;
930
931            // Test equality
932            assert_eq!(
933                price_with_default, price_with_base_amount,
934                "`calculate_spot_price_after_short` is not handling default base_amount correctly."
935            );
936            num_checks += 1
937        }
938        // We want to make sure we didn't `continue` through all possible fuzz states
939        assert!(num_checks > 0);
940        Ok(())
941    }
942
943    #[tokio::test]
944    async fn fuzz_calculate_implied_rate() -> Result<()> {
945        let tolerance = int256!(1e12);
946
947        // Spawn a test chain with two agents.
948        let mut rng = thread_rng();
949        let chain = TestChain::new().await?;
950        let mut alice = chain.alice().await?;
951        let mut bob = chain.bob().await?;
952
953        for _ in 0..*FUZZ_RUNS {
954            // Snapshot the chain.
955            let id = chain.snapshot().await?;
956
957            // Fund Alice and Bob.
958            let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
959            let contribution = rng.gen_range(fixed!(100_000e18)..=fixed!(100_000_000e18));
960            let budget = fixed!(100_000_000e18);
961            alice.fund(contribution).await?;
962            bob.fund(budget).await?;
963
964            // Alice initializes the pool.
965            // TODO: We'd like to set a random position duration & checkpoint duration.
966            alice.initialize(fixed_rate, contribution, None).await?;
967
968            // Set a random variable rate.
969            let variable_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(1e18));
970            alice.advance_time(variable_rate, 12.into()).await?;
971
972            // Bob opens a short with a random bond amount. Before opening the
973            // short, we calculate the implied rate.
974            let bond_amount = rng.gen_range(
975                FixedPoint::from(bob.get_config().minimum_transaction_amount)
976                    ..=bob.calculate_max_short(None).await? * fixed!(0.9e18),
977            );
978            let implied_rate = bob.get_state().await?.calculate_implied_rate(
979                bond_amount,
980                bob.get_state().await?.vault_share_price(),
981                variable_rate,
982            )?;
983            let (maturity_time, base_paid) = bob.open_short(bond_amount, None, None).await?;
984
985            // The term passes and interest accrues.
986            chain
987                .increase_time(bob.get_config().position_duration.low_u128())
988                .await?;
989
990            // Bob closes his short.
991            let base_proceeds = bob.close_short(maturity_time, bond_amount, None).await?;
992            let annualized_position_duration =
993                bob.get_state().await?.annualized_position_duration();
994
995            // Ensure that the implied rate matches the realized rate from
996            // holding the short to maturity.
997            let realized_rate = if base_proceeds > base_paid {
998                I256::try_from(
999                    (base_proceeds - base_paid) / (base_paid * annualized_position_duration),
1000                )?
1001            } else {
1002                -I256::try_from(
1003                    (base_paid - base_proceeds) / (base_paid * annualized_position_duration),
1004                )?
1005            };
1006            let error = (implied_rate - realized_rate).abs();
1007            let scaled_tolerance = if implied_rate > int256!(1e18) {
1008                I256::from(tolerance * implied_rate)
1009            } else {
1010                tolerance
1011            };
1012            assert!(
1013                error < scaled_tolerance,
1014                "error {:?} exceeds tolerance of {} (scaled to {})",
1015                error,
1016                tolerance,
1017                scaled_tolerance
1018            );
1019
1020            // Revert to the snapshot and reset the agent's wallets.
1021            chain.revert(id).await?;
1022            alice.reset(Default::default()).await?;
1023            bob.reset(Default::default()).await?;
1024        }
1025
1026        Ok(())
1027    }
1028
1029    // Tests open short with an amount smaller than the minimum.
1030    #[tokio::test]
1031    async fn test_error_open_short_min_txn_amount() -> Result<()> {
1032        let min_bond_delta = fixed!(1);
1033
1034        let mut rng = thread_rng();
1035        let state = rng.gen::<State>();
1036        let result = state.calculate_open_short(
1037            state.minimum_transaction_amount() - min_bond_delta,
1038            state.vault_share_price(),
1039        );
1040        assert!(result.is_err());
1041        Ok(())
1042    }
1043
1044    // Tests open short with an amount larger than the maximum.
1045    #[tokio::test]
1046    async fn fuzz_error_open_short_max_txn_amount() -> Result<()> {
1047        // This amount gets added to the max trade to cause a failure.
1048        // TODO: You should be able to add a small amount (e.g. 1e18) to max to fail.
1049        // calc_open_short or calc_max_short must be incorrect for the additional
1050        // amount to have to be so large.
1051        let max_bond_delta = fixed!(1_000_000_000e18);
1052
1053        let mut rng = thread_rng();
1054        for _ in 0..*FAST_FUZZ_RUNS {
1055            let state = rng.gen::<State>();
1056            let checkpoint_exposure = rng
1057                .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
1058                .raw()
1059                .flip_sign_if(rng.gen());
1060            let max_iterations = 7;
1061            let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
1062            // We need to catch panics because of FixedPoint<U256> overflows & underflows.
1063            let max_trade = panic::catch_unwind(|| {
1064                state.calculate_absolute_max_short(
1065                    state.calculate_spot_price()?,
1066                    checkpoint_exposure,
1067                    Some(max_iterations),
1068                )
1069            });
1070            // Since we're fuzzing it's possible that the max can fail.
1071            // We're only going to use it in this test if it succeeded.
1072            match max_trade {
1073                Ok(max_trade) => match max_trade {
1074                    Ok(max_trade) => {
1075                        let bond_amount = max_trade + max_bond_delta;
1076                        let base_amount = panic::catch_unwind(|| {
1077                            state.calculate_open_short(bond_amount, open_vault_share_price)
1078                        });
1079                        match base_amount {
1080                            Ok(result) => match result {
1081                                Ok(base_amount) => {
1082                                    return Err(eyre!(format!(
1083                                        "calculate_open_short on bond_amount={:#?} > max_bond_amount={:#?} \
1084                                        returned base_amount={:#?}, but should have failed.",
1085                                        bond_amount,
1086                                        max_trade,
1087                                        base_amount,
1088                                    )));
1089                                }
1090                                Err(_) => continue, // Open threw an Err.
1091                            },
1092                            Err(_) => continue, // Open threw a panic, likely due to FixedPoint<U256> under/over flow.
1093                        }
1094                    }
1095                    Err(_) => continue, // Max threw an Err.
1096                },
1097                Err(_) => continue, // Max thew an panic, likely due to FixedPoint<U256> under/over flow.
1098            }
1099        }
1100
1101        Ok(())
1102    }
1103
1104    #[tokio::test]
1105    pub async fn fuzz_sol_calculate_open_short() -> Result<()> {
1106        // Set up a random number generator. We use ChaCha8Rng with a randomly
1107        // generated seed, which makes it easy to reproduce test failures given
1108        // the seed.
1109        let mut rng = {
1110            let mut rng = thread_rng();
1111            let seed = rng.gen();
1112            ChaCha8Rng::seed_from_u64(seed)
1113        };
1114
1115        // Initialize the test chain.
1116        let chain = TestChain::new().await?;
1117        let mut alice = chain.alice().await?;
1118        let mut bob = chain.bob().await?;
1119        let mut celine = chain.celine().await?;
1120
1121        for _ in 0..*FUZZ_RUNS {
1122            // Snapshot the chain.
1123            let id = chain.snapshot().await?;
1124
1125            // Run the preamble.
1126            initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
1127
1128            // Get state and trade details.
1129            let mut state = alice.get_state().await?;
1130            let min_txn_amount = state.minimum_transaction_amount();
1131            let max_short = celine.calculate_max_short(None).await?;
1132            let bond_amount = rng.gen_range(min_txn_amount..=max_short);
1133
1134            // The base required should always be less than the short amount.
1135            celine.fund(bond_amount).await?;
1136
1137            // Compare the open short call output against calculate_open_short.
1138            match celine
1139                .hyperdrive()
1140                .open_short(
1141                    bond_amount.into(),
1142                    FixedPoint::from(U256::MAX).into(),
1143                    fixed!(0).into(),
1144                    Options {
1145                        destination: celine.address(),
1146                        as_base: true,
1147                        extra_data: [].into(),
1148                    },
1149                )
1150                .call()
1151                .await
1152            {
1153                Ok((_, sol_base)) => {
1154                    // Calling any Solidity Hyperdrive transaction causes the
1155                    // mock yield source to accrue some interest. We want to use
1156                    // the state before the Solidity OpenShort, but with the
1157                    // vault share price after the block tick.
1158
1159                    // Get the current vault share price & update state.
1160                    let vault_share_price = alice.get_state().await?.vault_share_price();
1161                    state.info.vault_share_price = vault_share_price.into();
1162
1163                    // Get the open vault share price.
1164                    let Checkpoint {
1165                        weighted_spot_price: _,
1166                        last_weighted_spot_price_update_time: _,
1167                        vault_share_price: open_vault_share_price,
1168                    } = alice
1169                        .get_checkpoint(state.to_checkpoint(alice.now().await?))
1170                        .await?;
1171
1172                    // Run the Rust function.
1173                    let rust_base =
1174                        state.calculate_open_short(bond_amount, open_vault_share_price.into());
1175
1176                    // Compare the results.
1177                    let rust_base_unwrapped = rust_base.unwrap();
1178                    let sol_base_fp = FixedPoint::from(sol_base);
1179                    assert_eq!(
1180                        rust_base_unwrapped, sol_base_fp,
1181                        "expected rust_base={:#?} == sol_base={:#?}",
1182                        rust_base_unwrapped, sol_base_fp
1183                    );
1184                }
1185                Err(sol_err) => {
1186                    // Get the current vault share price & update state.
1187                    let vault_share_price = alice.get_state().await?.vault_share_price();
1188                    state.info.vault_share_price = vault_share_price.into();
1189
1190                    // Get the open vault share price.
1191                    let Checkpoint {
1192                        weighted_spot_price: _,
1193                        last_weighted_spot_price_update_time: _,
1194                        vault_share_price: open_vault_share_price,
1195                    } = alice
1196                        .get_checkpoint(state.to_checkpoint(alice.now().await?))
1197                        .await?;
1198
1199                    // Run the Rust function.
1200                    let rust_base =
1201                        state.calculate_open_short(bond_amount, open_vault_share_price.into());
1202
1203                    // Make sure Rust failed.
1204                    assert!(
1205                        rust_base.is_err(),
1206                        "sol_err={:#?}, but rust_base={:#?} did not error",
1207                        sol_err,
1208                        rust_base,
1209                    );
1210                }
1211            }
1212
1213            // Revert to the snapshot and reset the agent's wallets.
1214            chain.revert(id).await?;
1215            alice.reset(Default::default()).await?;
1216            bob.reset(Default::default()).await?;
1217            celine.reset(Default::default()).await?;
1218        }
1219
1220        Ok(())
1221    }
1222
1223    #[tokio::test]
1224    async fn fuzz_calculate_approximate_short_bonds_given_deposit() -> Result<()> {
1225        let mut rng = thread_rng();
1226        for _ in 0..*FAST_FUZZ_RUNS {
1227            let state = rng.gen::<State>();
1228            let open_vault_share_price = rng.gen_range(fixed!(1e5)..=state.vault_share_price());
1229            let checkpoint_exposure = {
1230                let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX)));
1231                if rng.gen() {
1232                    -I256::try_from(value)?
1233                } else {
1234                    I256::try_from(value)?
1235                }
1236            };
1237            match get_max_short(state.clone(), checkpoint_exposure, None) {
1238                Ok(max_short_bonds) => {
1239                    let bond_amount =
1240                        rng.gen_range(state.minimum_transaction_amount()..=max_short_bonds);
1241                    let base_amount =
1242                        state.calculate_open_short(bond_amount, open_vault_share_price)?;
1243                    // We approximately invert this flow, so we receive base and return
1244                    // bonds.
1245                    let approximate_bond_amount = state
1246                        .calculate_approximate_short_bonds_given_base_deposit(
1247                            base_amount,
1248                            open_vault_share_price,
1249                        )?;
1250                    // We want to make sure that the approximation is safe, so the
1251                    // approximate amount should be less than or equal to the target.
1252                    assert!(
1253                        approximate_bond_amount <= bond_amount,
1254                        "approximate_bond_amount={:#?} not <= bond_amount={:#?}",
1255                        approximate_bond_amount,
1256                        bond_amount
1257                    );
1258                    // Make sure we can open a short with this approximate bond
1259                    // amount, and again check that the resulting deposit is
1260                    // less than the target deposit.
1261                    match state.calculate_open_short(approximate_bond_amount, open_vault_share_price) {
1262                        Ok(approximate_base_amount) => {
1263                            assert!(
1264                                approximate_base_amount <= base_amount,
1265                                "approximate_base_amount={:#?} not <= base_amount={:#?}",
1266                                approximate_base_amount,
1267                                base_amount
1268                            );
1269                        }
1270                        Err(_) => assert!(
1271                            false,
1272                            "Failed to run calculate_open_short with the approximate bond amount = {:#?}.",
1273                            approximate_bond_amount
1274                        ),
1275                    };
1276                }
1277                Err(_) => continue,
1278            }
1279        }
1280        Ok(())
1281    }
1282}