hyperdrive_math/long/
open.rs

1use ethers::types::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 long amount that will be opened for a given base amount.
9    ///
10    /// The long amount `$y(x)$` that a trader will receive is given by:
11    ///
12    /// ```math
13    /// y(x) = y_{*}(x) - c(x)
14    /// ```
15    ///
16    /// Where `$y_{*}(x)$` is the amount of long that would be opened if there was
17    /// no curve fee and `$c(x)$` is the
18    /// [curve fee](State::open_long_curve_fee). `$y_{*}(x)$` is given by:
19    ///
20    /// ```math
21    /// y_{*}(x) = y - \left(
22    ///                k - \tfrac{c}{\mu} \cdot \left(
23    ///                    \mu \cdot \left( z + \tfrac{x}{c}
24    ///                \right) \right)^{1 - t_s}
25    ///            \right)^{\tfrac{1}{1 - t_s}}
26    /// ```
27    pub fn calculate_open_long<F: Into<FixedPoint<U256>>>(
28        &self,
29        base_amount: F,
30    ) -> Result<FixedPoint<U256>> {
31        let base_amount = base_amount.into();
32
33        if base_amount < self.minimum_transaction_amount() {
34            return Err(eyre!("MinimumTransactionAmount: Input amount too low",));
35        }
36
37        let bond_amount =
38            self.calculate_bonds_out_given_shares_in_down(base_amount / self.vault_share_price())?;
39
40        // Throw an error if opening the long would result in negative interest.
41        let ending_spot_price =
42            self.calculate_spot_price_after_long(base_amount, bond_amount.into())?;
43        let max_spot_price = self.calculate_max_spot_price()?;
44        if ending_spot_price > max_spot_price {
45            return Err(eyre!("InsufficientLiquidity: Negative Interest",));
46        }
47
48        Ok(bond_amount - self.open_long_curve_fee(base_amount)?)
49    }
50
51    /// Calculates the derivative of
52    /// [calculate open long](State::calculate_open_long) with respect to the
53    /// base amount.
54    ///
55    /// We calculate the derivative of the long amount `$y(x)$` as:
56    ///
57    /// ```math
58    /// y'(x) = y_{*}'(x) - c'(x)
59    /// ```
60    ///
61    /// Where `$y_{*}'(x)$` is the derivative of `$y_{*}(x)$` and `$c^{\prime}(x)$`
62    /// is the derivative of `$c(x)$`, the [long curve fee](State::open_long_curve_fee).
63    /// `$y_{*}^{\prime}(x)$` is given by:
64    ///
65    /// ```math
66    /// y_{*}'(x) = \left( \mu \cdot (z + \tfrac{x}{c}) \right)^{-t_s}
67    ///             \left(
68    ///                 k - \tfrac{c}{\mu} \cdot
69    ///                 \left(
70    ///                     \mu \cdot (z + \tfrac{x}{c}
71    ///                 \right)^{1 - t_s}
72    ///             \right)^{\tfrac{t_s}{1 - t_s}}
73    /// ```
74    ///
75    /// and `$c^{\prime}(x)$` is given by:
76    ///
77    /// ```math
78    /// c^{\prime}(x) = \phi_{c} \cdot \left( \tfrac{1}{p} - 1 \right)
79    /// ```
80    pub(super) fn calculate_open_long_derivative(
81        &self,
82        base_amount: FixedPoint<U256>,
83    ) -> Result<FixedPoint<U256>> {
84        let share_amount = base_amount / self.vault_share_price();
85        let inner =
86            self.initial_vault_share_price() * (self.effective_share_reserves()? + share_amount);
87        let mut derivative = fixed!(1e18) / (inner).pow(self.time_stretch())?;
88
89        // It's possible that k is slightly larger than the rhs in the inner
90        // calculation. If this happens, we are close to the root, and we short
91        // circuit.
92        let k = self.k_down()?;
93        let rhs = self.vault_share_price().mul_div_down(
94            inner.pow(self.time_stretch())?,
95            self.initial_vault_share_price(),
96        );
97        if k < rhs {
98            return Err(eyre!("Open long derivative is undefined."));
99        }
100        derivative *= (k - rhs).pow(
101            self.time_stretch()
102                .div_up(fixed!(1e18) - self.time_stretch()),
103        )?;
104
105        // Finish computing the derivative.
106        derivative -=
107            self.curve_fee() * ((fixed!(1e18) / self.calculate_spot_price()?) - fixed!(1e18));
108
109        Ok(derivative)
110    }
111
112    /// Calculate an updated pool state after opening a long.
113    ///
114    /// For a given base delta and bond delta, the base delta is converted to
115    /// shares and the reserves are updated such that
116    /// `state.bond_reserves -= bond_delta` and
117    /// `state.share_reserves += base_delta / vault_share_price`.
118    pub fn calculate_pool_state_after_open_long(
119        &self,
120        base_amount: FixedPoint<U256>,
121        maybe_bond_delta: Option<FixedPoint<U256>>,
122    ) -> Result<Self> {
123        let (share_delta, bond_delta) =
124            self.calculate_pool_deltas_after_open_long(base_amount, maybe_bond_delta)?;
125        let mut state = self.clone();
126        state.info.bond_reserves -= bond_delta.into();
127        state.info.share_reserves += share_delta.into();
128        Ok(state)
129    }
130
131    /// Calculate the share and bond deltas to be applied to the pool after opening a long.
132    pub fn calculate_pool_deltas_after_open_long(
133        &self,
134        base_amount: FixedPoint<U256>,
135        maybe_bond_delta: Option<FixedPoint<U256>>,
136    ) -> Result<(FixedPoint<U256>, FixedPoint<U256>)> {
137        let bond_delta = match maybe_bond_delta {
138            Some(delta) => delta,
139            None => self.calculate_open_long(base_amount)?,
140        };
141        let total_gov_curve_fee_shares = self
142            .open_long_governance_fee(base_amount, None)?
143            .div_down(self.vault_share_price());
144        let share_delta =
145            base_amount.div_down(self.vault_share_price()) - total_gov_curve_fee_shares;
146        Ok((share_delta, bond_delta))
147    }
148
149    /// Calculates the spot price after opening a Hyperdrive long.
150    /// If a bond_amount is not provided, then one is estimated using `calculate_open_long`.
151    pub fn calculate_spot_price_after_long(
152        &self,
153        base_amount: FixedPoint<U256>,
154        maybe_bond_pool_delta: Option<FixedPoint<U256>>,
155    ) -> Result<FixedPoint<U256>> {
156        let state =
157            self.calculate_pool_state_after_open_long(base_amount, maybe_bond_pool_delta)?;
158        state.calculate_spot_price()
159    }
160
161    /// Calculate the spot rate after a long has been opened.
162    /// If a bond_amount is not provided, then one is estimated using
163    /// [calculate_open_long](State::calculate_open_long).
164    ///
165    /// We calculate the rate for a fixed length of time as:
166    ///
167    /// ```math
168    /// r(\Delta y) = \frac{1 - p(\Delta y)}{p(\Delta y) t}
169    /// ```
170    ///
171    /// where `$p(x)$` is the spot price after a long for `delta_base` `$= x$`
172    /// and `$t$` is the normalized position druation.
173    ///
174    /// In this case, we use the resulting spot price after a hypothetical long
175    /// for `base_amount` is opened.
176    pub fn calculate_spot_rate_after_long(
177        &self,
178        base_amount: FixedPoint<U256>,
179        maybe_bond_amount: Option<FixedPoint<U256>>,
180    ) -> Result<FixedPoint<U256>> {
181        Ok(calculate_rate_given_fixed_price(
182            self.calculate_spot_price_after_long(base_amount, maybe_bond_amount)?,
183            self.position_duration(),
184        ))
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use std::panic;
191
192    use ethers::types::{I256, U256};
193    use fixedpointmath::{fixed, fixed_u256, FixedPointValue};
194    use hyperdrive_test_utils::{
195        chain::TestChain,
196        constants::{FAST_FUZZ_RUNS, FUZZ_RUNS, SLOW_FUZZ_RUNS},
197    };
198    use hyperdrive_wrappers::wrappers::ihyperdrive::Options;
199    use rand::{thread_rng, Rng, SeedableRng};
200    use rand_chacha::ChaCha8Rng;
201
202    use super::*;
203    use crate::test_utils::{
204        agent::HyperdriveMathAgent, preamble::initialize_pool_with_random_state,
205    };
206
207    #[tokio::test]
208    async fn fuzz_calculate_pool_state_after_open_long() -> Result<()> {
209        // TODO: We should not need a tolerance.
210        let share_adjustment_test_tolerance = fixed_u256!(0);
211        let bond_reserves_test_tolerance = fixed!(1e10);
212        let share_reserves_test_tolerance = fixed!(1e10);
213        // Initialize a test chain and agents.
214        let chain = TestChain::new().await?;
215        let mut alice = chain.alice().await?;
216        let mut bob = chain.bob().await?;
217        let mut celine = chain.celine().await?;
218        // Set up a random number generator. We use ChaCha8Rng with a randomly
219        // generated seed, which makes it easy to reproduce test failures given
220        // the seed.
221        let mut rng = {
222            let mut rng = thread_rng();
223            let seed = rng.gen();
224            ChaCha8Rng::seed_from_u64(seed)
225        };
226        for _ in 0..*SLOW_FUZZ_RUNS {
227            // Snapshot the chain & run the preamble.
228            let id = chain.snapshot().await?;
229            initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
230            // Reset the variable rate to zero; get the state.
231            alice.advance_time(fixed!(0), fixed!(0)).await?;
232            let original_state = alice.get_state().await?;
233            // Get a random long amount.
234            let checkpoint_exposure = alice
235                .get_checkpoint_exposure(original_state.to_checkpoint(alice.now().await?))
236                .await?;
237            let max_long_amount =
238                original_state.calculate_max_long(U256::MAX, checkpoint_exposure, None)?;
239            let base_amount =
240                rng.gen_range(original_state.minimum_transaction_amount()..=max_long_amount);
241            // Mock the trade using Rust.
242            let rust_state =
243                original_state.calculate_pool_state_after_open_long(base_amount, None)?;
244            // Execute the trade on the contracts.
245            bob.fund(base_amount * fixed!(1.5e18)).await?;
246            bob.open_long(base_amount, None, None).await?;
247            let sol_state = alice.get_state().await?;
248            // Check that the results are the same.
249            let rust_share_adjustment = rust_state.share_adjustment();
250            let sol_share_adjustment = sol_state.share_adjustment();
251            let share_adjustment_error = if rust_share_adjustment < sol_share_adjustment {
252                FixedPoint::try_from(sol_share_adjustment - rust_share_adjustment)?
253            } else {
254                FixedPoint::try_from(rust_share_adjustment - sol_share_adjustment)?
255            };
256            assert!(
257                share_adjustment_error <= share_adjustment_test_tolerance,
258                "expected abs(rust_share_adjustment={}-sol_share_adjustment={})={} <= test_tolerance={}",
259                rust_share_adjustment, sol_share_adjustment, share_adjustment_error, share_adjustment_test_tolerance
260            );
261            let rust_bond_reserves = rust_state.bond_reserves();
262            let sol_bond_reserves = sol_state.bond_reserves();
263            let bond_reserves_error = if rust_bond_reserves < sol_bond_reserves {
264                sol_bond_reserves - rust_bond_reserves
265            } else {
266                rust_bond_reserves - sol_bond_reserves
267            };
268            assert!(
269                bond_reserves_error <= bond_reserves_test_tolerance,
270                "expected abs(rust_bond_reserves={}-sol_bond_reserves={})={} <= test_tolerance={}",
271                rust_bond_reserves,
272                sol_bond_reserves,
273                bond_reserves_error,
274                bond_reserves_test_tolerance
275            );
276            let rust_share_reserves = rust_state.share_reserves();
277            let sol_share_reserves = sol_state.share_reserves();
278            let share_reserves_error = if rust_share_reserves < sol_share_reserves {
279                sol_share_reserves - rust_share_reserves
280            } else {
281                rust_share_reserves - sol_share_reserves
282            };
283            assert!(
284                share_reserves_error <= share_reserves_test_tolerance,
285                "expected abs(rust_share_reserves={}-sol_share_reserves={})={} <= test_tolerance={}",
286                rust_share_reserves,
287                sol_share_reserves,
288                share_reserves_error,
289                share_reserves_test_tolerance
290            );
291            // Revert to the snapshot and reset the agent's wallets.
292            chain.revert(id).await?;
293            alice.reset(Default::default()).await?;
294            bob.reset(Default::default()).await?;
295            celine.reset(Default::default()).await?;
296        }
297        Ok(())
298    }
299
300    #[tokio::test]
301    async fn fuzz_calculate_spot_price_after_long() -> Result<()> {
302        // Spawn a test chain and create two agents -- Alice and Bob. Alice
303        // is funded with a large amount of capital so that she can initialize
304        // the pool. Bob is funded with a small amount of capital so that we
305        // can test opening a long and verify that the ending spot price is what
306        // we expect.
307        let mut rng = thread_rng();
308        let chain = TestChain::new().await?;
309        let mut alice = chain.alice().await?;
310        let mut bob = chain.bob().await?;
311
312        for _ in 0..*FUZZ_RUNS {
313            // Snapshot the chain.
314            let id = chain.snapshot().await?;
315
316            // Fund Alice and Bob.
317            let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
318            let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
319            let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
320            alice.fund(contribution).await?;
321            bob.fund(budget).await?;
322
323            // Alice initializes the pool.
324            alice.initialize(fixed_rate, contribution, None).await?;
325
326            // Attempt to predict the spot price after opening a long.
327            let base_paid = rng.gen_range(fixed!(0.1e18)..=bob.calculate_max_long(None).await?);
328            let expected_spot_price = bob
329                .get_state()
330                .await?
331                .calculate_spot_price_after_long(base_paid, None)?;
332
333            // Open the long.
334            bob.open_long(base_paid, None, None).await?;
335
336            // Verify that the predicted spot price is equal to the ending spot
337            // price. These won't be exactly equal because the vault share price
338            // increases between the prediction and opening the long.
339            let actual_spot_price = bob.get_state().await?.calculate_spot_price()?;
340            let delta = if actual_spot_price > expected_spot_price {
341                actual_spot_price - expected_spot_price
342            } else {
343                expected_spot_price - actual_spot_price
344            };
345            let tolerance = fixed!(1e9);
346            assert!(
347                delta < tolerance,
348                "expected: delta = {} < {} = tolerance",
349                delta,
350                tolerance
351            );
352
353            // Revert to the snapshot and reset the agent's wallets.
354            chain.revert(id).await?;
355            alice.reset(Default::default()).await?;
356            bob.reset(Default::default()).await?;
357        }
358        Ok(())
359    }
360
361    #[tokio::test]
362    async fn fuzz_calculate_spot_rate_after_long() -> Result<()> {
363        // Spawn a test chain and create two agents -- Alice and Bob. Alice
364        // is funded with a large amount of capital so that she can initialize
365        // the pool. Bob is funded with a small amount of capital so that we
366        // can test opening a long and verify that the ending spot rate is what
367        // we expect.
368        let mut rng = thread_rng();
369        let chain = TestChain::new().await?;
370        let mut alice = chain.alice().await?;
371        let mut bob = chain.bob().await?;
372
373        for _ in 0..*FUZZ_RUNS {
374            // Snapshot the chain.
375            let id = chain.snapshot().await?;
376
377            // Fund Alice and Bob.
378            let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
379            let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
380            let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
381            alice.fund(contribution).await?;
382            bob.fund(budget).await?;
383
384            // Alice initializes the pool.
385            alice.initialize(fixed_rate, contribution, None).await?;
386
387            // Attempt to predict the spot price after opening a long.
388            let base_paid = rng.gen_range(
389                alice.get_state().await?.minimum_transaction_amount()
390                    ..=bob.calculate_max_long(None).await?,
391            );
392            let expected_spot_rate = bob
393                .get_state()
394                .await?
395                .calculate_spot_rate_after_long(base_paid, None)?;
396
397            // Open the long.
398            bob.open_long(base_paid, None, None).await?;
399
400            // Verify that the predicted spot rate is equal to the ending spot
401            // rate. These won't be exactly equal because the vault share price
402            // increases between the prediction and opening the long.
403            let actual_spot_rate = bob.get_state().await?.calculate_spot_rate()?;
404            let delta = if actual_spot_rate > expected_spot_rate {
405                actual_spot_rate - expected_spot_rate
406            } else {
407                expected_spot_rate - actual_spot_rate
408            };
409            let tolerance = fixed!(1e9);
410            assert!(
411                delta < tolerance,
412                "expected: delta = {} < {} = tolerance",
413                delta,
414                tolerance
415            );
416
417            // Revert to the snapshot and reset the agent's wallets.
418            chain.revert(id).await?;
419            alice.reset(Default::default()).await?;
420            bob.reset(Default::default()).await?;
421        }
422        Ok(())
423    }
424
425    // Tests open long with an amount smaller than the minimum.
426    #[tokio::test]
427    async fn test_error_open_long_min_txn_amount() -> Result<()> {
428        let mut rng = thread_rng();
429        let state = rng.gen::<State>();
430        let result = state.calculate_open_long(state.config.minimum_transaction_amount - 10);
431        assert!(result.is_err());
432        Ok(())
433    }
434
435    // Tests open long with an amount larger than the maximum.
436    #[tokio::test]
437    async fn fuzz_error_open_long_max_txn_amount() -> Result<()> {
438        // This amount gets added to the max trade to cause a failure.
439        // TODO: You should be able to add a small amount (e.g. 1e18) to max to fail.
440        // calc_open_long or calc_max_long must be incorrect for the additional
441        // amount to have to be so large.
442        let max_base_delta = fixed!(1_000_000_000e18);
443
444        let mut rng = thread_rng();
445        for _ in 0..*FUZZ_RUNS {
446            let state = rng.gen::<State>();
447            let checkpoint_exposure = rng
448                .gen_range(fixed!(0)..=FixedPoint::<I256>::MAX)
449                .raw()
450                .flip_sign_if(rng.gen());
451            let max_iterations = 7;
452            // We need to catch panics because of FixedPoint<U256> overflows & underflows.
453            let max_trade = panic::catch_unwind(|| {
454                state.calculate_max_long(U256::MAX, checkpoint_exposure, Some(max_iterations))
455            });
456            // Since we're fuzzing it's possible that the max can fail.
457            // We're only going to use it in this test if it succeeded.
458            match max_trade {
459                Ok(max_trade) => match max_trade {
460                    Ok(max_trade) => {
461                        let base_amount = max_trade + max_base_delta;
462                        let bond_amount =
463                            panic::catch_unwind(|| state.calculate_open_long(base_amount));
464                        match bond_amount {
465                            Ok(result) => match result {
466                                Ok(_) => {
467                                    return Err(eyre!(
468                                        format!(
469                                            "calculate_open_long for {} base should have failed but succeeded.",
470                                            base_amount,
471                                        )
472                                    ));
473                                }
474                                Err(_) => continue, // Open threw an Err.
475                            },
476                            Err(_) => continue, // Open threw a panic, likely due to FixedPoint<U256> under/over flow.
477                        }
478                    }
479                    Err(_) => continue, // Max threw an Err.
480                },
481                Err(_) => continue, // Max thew an panic, likely due to FixedPoint<U256> under/over flow.
482            }
483        }
484
485        Ok(())
486    }
487
488    #[tokio::test]
489    pub async fn fuzz_sol_calc_open_long() -> Result<()> {
490        let tolerance = fixed!(1e3);
491
492        // Set up a random number generator. We use ChaCha8Rng with a randomly
493        // generated seed, which makes it easy to reproduce test failures given
494        // the seed.
495        let mut rng = {
496            let mut rng = thread_rng();
497            let seed = rng.gen();
498            ChaCha8Rng::seed_from_u64(seed)
499        };
500
501        // Initialize the test chain.
502        let chain = TestChain::new().await?;
503        let mut alice = chain.alice().await?;
504        let mut bob = chain.bob().await?;
505        let mut celine = chain.celine().await?;
506
507        for _ in 0..*FUZZ_RUNS {
508            // Snapshot the chain.
509            let id = chain.snapshot().await?;
510
511            // Run the preamble.
512            initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
513
514            // Get state and trade details.
515            let mut state = alice.get_state().await?;
516            let min_txn_amount = state.minimum_transaction_amount();
517            let max_long = bob.calculate_max_long(None).await?;
518            let base_amount = rng.gen_range(min_txn_amount..=max_long);
519
520            // Fund a little extra to allow for of slippage.
521            bob.fund(base_amount + base_amount * fixed!(0.001e18))
522                .await?;
523            match bob
524                .hyperdrive()
525                .open_long(
526                    base_amount.into(),
527                    fixed!(0).into(),
528                    fixed!(0).into(),
529                    Options {
530                        destination: bob.address(),
531                        as_base: true,
532                        extra_data: [].into(),
533                    },
534                )
535                .call()
536                .await
537            {
538                Ok((_, sol_bonds)) => {
539                    // Anvil ticks the block before applying solidity fn; update state with new price.
540                    let new_vault_share_price = alice.get_state().await?.vault_share_price();
541                    state.info.vault_share_price = new_vault_share_price.into();
542                    let rust_bonds = state.calculate_open_long(base_amount);
543
544                    // Compare the Rust open long call output against calculate_open_long.
545                    let rust_bonds_unwrapped = rust_bonds.unwrap();
546                    let error = if rust_bonds_unwrapped >= sol_bonds.into() {
547                        rust_bonds_unwrapped - FixedPoint::from(sol_bonds)
548                    } else {
549                        FixedPoint::from(sol_bonds) - rust_bonds_unwrapped
550                    };
551                    assert!(
552                        error <= tolerance,
553                        "error {} exceeds tolerance of {}",
554                        error,
555                        tolerance
556                    );
557                }
558                Err(sol_err) => {
559                    // Anvil ticks the block before applying solidity fn; update state with new price.
560                    let new_vault_share_price = alice.get_state().await?.vault_share_price();
561                    state.info.vault_share_price = new_vault_share_price.into();
562                    let rust_bonds = state.calculate_open_long(base_amount);
563                    assert!(
564                        rust_bonds.is_err(),
565                        "sol_err={:#?}, but rust_bonds={:#?} did not error",
566                        sol_err,
567                        rust_bonds
568                    );
569                }
570            }
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
579        Ok(())
580    }
581
582    /// This test empirically tests the derivative returned by
583    /// `calculate_open_long_derivative` by calling `calculate_open_long` at two
584    /// points and comparing the empirical result with the output of
585    /// `calculate_open_long_derivative`.
586    #[tokio::test]
587    async fn fuzz_open_long_derivative() -> Result<()> {
588        let mut rng = thread_rng();
589        // We use a relatively large epsilon here due to the underlying fixed point pow
590        // function not being monotonically increasing.
591        let empirical_derivative_epsilon = fixed!(1e12);
592        // TODO pretty big comparison epsilon here
593        let test_comparison_epsilon = fixed!(10e18);
594
595        for _ in 0..*FAST_FUZZ_RUNS {
596            let state = rng.gen::<State>();
597            let amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18));
598
599            // We need to catch panics here because FixedPoint<U256> panics on overflow or underflow.
600            let f_x = match panic::catch_unwind(|| state.calculate_open_long(amount)) {
601                Ok(result) => match result {
602                    Ok(result) => result,
603                    Err(_) => continue, // Err; the amount results in the pool being insolvent.
604                },
605                Err(_) => continue, // panic; likely in FixedPoint<U256>
606            };
607
608            let f_x_plus_delta = match panic::catch_unwind(|| {
609                state.calculate_open_long(amount + empirical_derivative_epsilon)
610            }) {
611                Ok(result) => match result {
612                    Ok(result) => result,
613                    Err(_) => continue,
614                },
615                // If the amount results in the pool being insolvent, skip this iteration.
616                Err(_) => continue,
617            };
618            // Sanity check.
619            assert!(f_x_plus_delta > f_x);
620
621            let empirical_derivative = (f_x_plus_delta - f_x) / empirical_derivative_epsilon;
622            let open_long_derivative = state.calculate_open_long_derivative(amount)?;
623            let derivative_diff = if open_long_derivative >= empirical_derivative {
624                open_long_derivative - empirical_derivative
625            } else {
626                empirical_derivative - open_long_derivative
627            };
628            assert!(
629                derivative_diff < test_comparison_epsilon,
630                "expected (derivative_diff={}) < (test_comparison_epsilon={}), \
631                calculated_derivative={}, emperical_derivative={}",
632                derivative_diff,
633                test_comparison_epsilon,
634                open_long_derivative,
635                empirical_derivative
636            );
637        }
638
639        Ok(())
640    }
641}