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}