hyperdrive_math/lp/math.rs
1use std::cmp::{max, min, Ordering};
2
3use ethers::types::{I256, U256};
4use eyre::{eyre, Result};
5use fixedpointmath::{fixed, int256, FixedPoint};
6
7use crate::{calculate_effective_share_reserves, State, YieldSpace};
8
9pub static SHARE_PROCEEDS_MAX_ITERATIONS: u64 = 4;
10pub static SHARE_PROCEEDS_SHORT_CIRCUIT_TOLERANCE: u128 = 1_000_000_000; // 1e9
11pub static SHARE_PROCEEDS_TOLERANCE: u128 = 100_000_000_000_000; // 1e14
12
13impl State {
14 /// Calculates the initial reserves. We solve for the initial reserves
15 /// by solving the following equations simultaneously:
16 ///
17 /// (1) `$c \cdot z = c \cdot z_e + p_{\text{target}} \cdot y$`
18 ///
19 /// (2) `$p_{\text{target}} = \left(\tfrac{\mu \cdot z_e}{y}\right)^{t_s}$`
20 ///
21 /// where `$p_{\text{target}}$` is the target spot price implied by the
22 /// target spot rate.
23 pub fn calculate_initial_reserves(
24 &self,
25 share_amount: FixedPoint<U256>,
26 target_apr: FixedPoint<U256>,
27 ) -> Result<(FixedPoint<U256>, I256, FixedPoint<U256>)> {
28 // NOTE: Round down to underestimate the initial bond reserves.
29 //
30 // Normalize the time to maturity to fractions of a year since the provided
31 // rate is an APR.
32 let t = self
33 .position_duration()
34 .div_down(U256::from(60 * 60 * 24 * 365).into());
35
36 // NOTE: Round up to underestimate the initial bond reserves.
37 //
38 // Calculate the target price implied by the target rate.
39 let one = fixed!(1e18);
40 let target_price = one.div_up(one + target_apr.mul_down(t));
41
42 // The share reserves is just the share amount since we are initializing
43 // the pool.
44 let share_reserves = share_amount;
45
46 // NOTE: Round down to underestimate the initial bond reserves.
47 //
48 // Calculate the initial bond reserves. This is given by:
49 //
50 // y = (mu * c * z) / (c * p_target ** (1 / t_s) + mu * p_target)
51 let bond_reserves = self.initial_vault_share_price().mul_div_down(
52 self.vault_share_price().mul_down(share_reserves),
53 self.vault_share_price()
54 .mul_down(target_price.pow(one.div_down(self.time_stretch()))?)
55 + self.initial_vault_share_price().mul_up(target_price),
56 );
57
58 // NOTE: Round down to underestimate the initial share adjustment.
59 //
60 // Calculate the initial share adjustment. This is given by:
61 //
62 // zeta = (p_target * y) / c
63 let share_adjustment =
64 I256::try_from(bond_reserves.mul_div_down(target_price, self.vault_share_price()))?;
65
66 Ok((share_reserves, share_adjustment, bond_reserves))
67 }
68
69 /// Calculates the resulting share_reserves, share_adjustment, and
70 /// bond_reserves when updating liquidity with a `share_reserves_delta`.
71 pub fn calculate_update_liquidity(
72 &self,
73 share_reserves: FixedPoint<U256>,
74 share_adjustment: I256,
75 bond_reserves: FixedPoint<U256>,
76 minimum_share_reserves: FixedPoint<U256>,
77 share_reserves_delta: I256,
78 ) -> Result<(FixedPoint<U256>, I256, FixedPoint<U256>)> {
79 // If the share reserves delta is zero, we can return early since no
80 // action is needed.
81 if share_reserves_delta == I256::zero() {
82 return Ok((share_reserves, share_adjustment, bond_reserves));
83 }
84
85 // Update the share reserves by applying the share reserves delta. We
86 // ensure that our minimum share reserves invariant is still maintained.
87 let new_share_reserves = I256::try_from(share_reserves)? + share_reserves_delta;
88 if new_share_reserves < I256::try_from(minimum_share_reserves).unwrap() {
89 return Err(eyre!(
90 "update would result in share reserves below minimum."
91 ));
92 }
93 let new_share_reserves = FixedPoint::try_from(new_share_reserves)?;
94
95 // Update the share adjustment by holding the ratio of share reserves
96 // to share adjustment proportional. In general, our pricing model cannot
97 // support negative values for the z coordinate, so this is important as
98 // it ensures that if z - zeta starts as a positive value, it ends as a
99 // positive value. With this in mind, we update the share adjustment as:
100 //
101 // zeta_old / z_old = zeta_new / z_new
102 // =>
103 // zeta_new = zeta_old * (z_new / z_old)
104 let new_share_adjustment = if share_adjustment >= I256::zero() {
105 let share_adjustment_fp = FixedPoint::try_from(share_adjustment)?;
106 I256::try_from(new_share_reserves.mul_div_down(share_adjustment_fp, share_reserves))?
107 } else {
108 let share_adjustment_fp = FixedPoint::try_from(-share_adjustment)?;
109 -I256::try_from(new_share_reserves.mul_div_up(share_adjustment_fp, share_reserves))?
110 };
111
112 // NOTE: Rounding down to avoid introducing dust into the computation.
113 //
114 // The liquidity update should hold the spot price invariant. The spot
115 // price of base in terms of bonds is given by:
116 //
117 // p = (mu * (z - zeta) / y) ** tau
118 //
119 // This formula implies that holding the ratio of share reserves to bond
120 // reserves constant will hold the spot price constant. This allows us
121 // to calculate the updated bond reserves as:
122 //
123 // (z_old - zeta_old) / y_old = (z_new - zeta_new) / y_new
124 // =>
125 // y_new = (z_new - zeta_new) * (y_old / (z_old - zeta_old))
126 let old_effective_share_reserves =
127 calculate_effective_share_reserves(self.share_reserves(), self.share_adjustment())?;
128 let new_effective_share_reserves =
129 calculate_effective_share_reserves(new_share_reserves, new_share_adjustment)?;
130 let new_bond_reserves =
131 bond_reserves.mul_div_down(new_effective_share_reserves, old_effective_share_reserves);
132
133 Ok((new_share_reserves, new_share_adjustment, new_bond_reserves))
134 }
135
136 /// Calculates the present value in shares of LP's capital in the pool.
137 pub fn calculate_present_value(
138 &self,
139 current_block_timestamp: U256,
140 ) -> Result<FixedPoint<U256>> {
141 // Calculate the average time remaining for the longs and shorts.
142
143 // To keep precision of long and short average maturity time (from contract call)
144 // we scale the block timestamp and position duration by 1e18 to calculate
145 // the normalized time remaining.
146 let long_average_time_remaining = self.calculate_scaled_normalized_time_remaining(
147 self.long_average_maturity_time(),
148 current_block_timestamp,
149 );
150 let short_average_time_remaining = self.calculate_scaled_normalized_time_remaining(
151 self.short_average_maturity_time(),
152 current_block_timestamp,
153 );
154 let net_curve_trade = self
155 .calculate_net_curve_trade(long_average_time_remaining, short_average_time_remaining)?;
156 let net_flat_trade = self
157 .calculate_net_flat_trade(long_average_time_remaining, short_average_time_remaining)?;
158
159 let present_value: I256 =
160 I256::try_from(self.share_reserves())? + net_curve_trade + net_flat_trade
161 - I256::try_from(self.minimum_share_reserves())?;
162
163 if present_value < int256!(0) {
164 return Err(eyre!("Negative present value!"));
165 }
166
167 Ok(present_value.try_into()?)
168 }
169
170 pub fn calculate_net_curve_trade(
171 &self,
172 long_average_time_remaining: FixedPoint<U256>,
173 short_average_time_remaining: FixedPoint<U256>,
174 ) -> Result<I256> {
175 // NOTE: To underestimate the impact of closing the net curve position,
176 // we round up the long side of the net curve position (since this
177 // results in a larger value removed from the share reserves) and round
178 // down the short side of the net curve position (since this results in
179 // a smaller value added to the share reserves).
180 //
181 // The net curve position is the net of the longs and shorts that are
182 // currently tradeable on the curve. Given the amount of outstanding
183 // longs `y_l` and shorts `y_s` as well as the average time remaining
184 // of outstanding longs `t_l` and shorts `t_s`, we can
185 // compute the net curve position as:
186 //
187 // netCurveTrade = y_l * t_l - y_s * t_s.
188 let net_curve_position =
189 I256::try_from(self.longs_outstanding().mul_up(long_average_time_remaining))?
190 - I256::try_from(
191 self.shorts_outstanding()
192 .mul_down(short_average_time_remaining),
193 )?;
194 match self.effective_share_reserves() {
195 Ok(_) => {}
196 // NOTE: Return 0 to indicate that the net curve trade couldn't be
197 // computed.
198 Err(_) => return Ok(I256::zero()),
199 }
200
201 // If the net curve position is positive, then the pool is net long.
202 // Closing the net curve position results in the longs being paid out
203 // from the share reserves, so we negate the result.
204 match net_curve_position.cmp(&int256!(0)) {
205 Ordering::Greater => {
206 let net_curve_position: FixedPoint<U256> =
207 FixedPoint::try_from(net_curve_position)?;
208 let max_curve_trade =
209 match self.calculate_max_sell_bonds_in(self.minimum_share_reserves()) {
210 Ok(max_curve_trade) => max_curve_trade,
211 Err(_) => {
212 // NOTE: Return 0 to indicate that the net curve trade couldn't
213 // be computed.
214 return Ok(I256::zero());
215 }
216 };
217
218 if max_curve_trade >= net_curve_position {
219 match self.calculate_shares_out_given_bonds_in_down(net_curve_position) {
220 Ok(net_curve_trade) => Ok(-I256::try_from(net_curve_trade)?),
221 Err(err) => {
222 // If the net curve position is smaller than the
223 // minimum transaction amount and the trade fails,
224 // we mark it to 0. This prevents liveness problems
225 // when the net curve position is very small.
226 if net_curve_position < self.minimum_transaction_amount() {
227 Ok(I256::zero())
228 } else {
229 Err(err)
230 }
231 }
232 }
233 } else {
234 // If the share adjustment is greater than or equal to zero,
235 // then the effective share reserves are less than or equal to
236 // the share reserves. In this case, the maximum amount of
237 // shares that can be removed from the share reserves is
238 // `effectiveShareReserves - minimumShareReserves`.
239 if self.share_adjustment() >= I256::from(0) {
240 Ok(-I256::try_from(
241 self.effective_share_reserves()? - self.minimum_share_reserves(),
242 )?)
243
244 // Otherwise, the effective share reserves are greater than the
245 // share reserves. In this case, the maximum amount of shares
246 // that can be removed from the share reserves is
247 // `shareReserves - minimumShareReserves`.
248 } else {
249 Ok(-I256::try_from(
250 self.share_reserves() - self.minimum_share_reserves(),
251 )?)
252 }
253 }
254 }
255 Ordering::Less => {
256 let net_curve_position: FixedPoint<U256> =
257 FixedPoint::try_from(-net_curve_position)?;
258 let max_curve_trade = match self.calculate_max_buy_bonds_out() {
259 Ok(max_curve_trade) => max_curve_trade,
260 Err(_) => {
261 // NOTE: Return 0 to indicate that the net curve trade couldn't
262 // be computed.
263 return Ok(I256::zero());
264 }
265 };
266 if max_curve_trade >= net_curve_position {
267 match self.calculate_shares_in_given_bonds_out_up(net_curve_position) {
268 Ok(net_curve_trade) => Ok(I256::try_from(net_curve_trade)?),
269 Err(err) => {
270 // If the net curve position is smaller than the
271 // minimum transaction amount and the trade fails,
272 // we mark it to 0. This prevents liveness problems
273 // when the net curve position is very small.
274 if net_curve_position < self.minimum_transaction_amount() {
275 Ok(I256::zero())
276 } else {
277 Err(err)
278 }
279 }
280 }
281 } else {
282 let max_share_payment = match self.calculate_max_buy_shares_in() {
283 Ok(max_share_payment) => max_share_payment,
284 Err(_) => {
285 // NOTE: Return 0 to indicate that the net curve trade couldn't
286 // be computed.
287 return Ok(I256::zero());
288 }
289 };
290
291 // NOTE: We round the difference down to underestimate the
292 // impact of closing the net curve position.
293 Ok(I256::try_from(
294 max_share_payment
295 + (net_curve_position - max_curve_trade)
296 .div_down(self.vault_share_price()),
297 )?)
298 }
299 }
300 Ordering::Equal => Ok(int256!(0)),
301 }
302 }
303
304 /// Calculates the result of closing the net flat position.
305 pub fn calculate_net_flat_trade(
306 &self,
307 long_average_time_remaining: FixedPoint<U256>,
308 short_average_time_remaining: FixedPoint<U256>,
309 ) -> Result<I256> {
310 if self.vault_share_price() == fixed!(0) {
311 return Err(eyre!("Vault share price is zero."));
312 }
313 if short_average_time_remaining > fixed!(1e18) || long_average_time_remaining > fixed!(1e18)
314 {
315 return Err(eyre!("Average time remaining is greater than 1e18."));
316 }
317 // NOTE: In order to underestimate the impact of closing all of the
318 // flat trades, we round the impact of closing the shorts down and round
319 // the impact of closing the longs up.
320 //
321 // Compute the net of the longs and shorts that will be traded flat and
322 // apply this net to the reserves.
323 let net_flat_trade = I256::try_from(self.shorts_outstanding().mul_div_down(
324 fixed!(1e18) - short_average_time_remaining,
325 self.vault_share_price(),
326 ))? - I256::try_from(self.longs_outstanding().mul_div_up(
327 fixed!(1e18) - long_average_time_remaining,
328 self.vault_share_price(),
329 ))?;
330
331 Ok(net_flat_trade)
332 }
333
334 /// Calculates the amount of withdrawal shares that can be redeemed and
335 /// the share proceeds the withdrawal pool should receive given the
336 /// pool's current idle liquidity. We use the following algorithm to
337 /// ensure that the withdrawal pool receives the correct amount of
338 /// shares to (1) preserve the LP share price and (2) pay out as much
339 /// of the idle liquidity as possible to the withdrawal pool:
340 ///
341 /// 1. If `$y_s \cdot t_s <= y_l \cdot t_l$` or
342 /// `$y_{\text{max\_out}}(I) >= y_s \cdot t_s - y_l \cdot t_l$` ,
343 /// set `$dz_{\text{max}} = I$` and proceed to step (3).
344 /// Otherwise, proceed to step (2).
345 /// 2. Solve
346 /// `$y_{\text{max\_out}}(dz_{\text{max}}) = y_s \cdot t_s - y_l \cdot t_l$`
347 /// for `$dz_{\text{max}}$` using Newton's method.
348 /// 3. Set `$dw = (1 - \tfrac{PV(dz_{\text{max}})}{PV(0)}) \cdot l$`.
349 /// If `$dw <= w$`, then proceed to step (5). Otherwise, set `$dw = w$`
350 /// and continue to step (4).
351 /// 4. Solve `$\tfrac{PV(0)}{l} = \tfrac{PV(dz)}{(l - dw)}$` for `$dz$`
352 /// using Newton's method if `$y_l \cdot t_l ~= y_s \cdot t_s$` or
353 /// directly otherwise.
354 /// 5. Return `$dw$` and `$dz$`.
355 ///
356 /// Returns `(withdrawal_shares_redeemed, share_proceeds, success)`
357 pub fn calculate_distribute_excess_idle(
358 &self,
359 current_block_timestamp: U256,
360 active_lp_total_supply: FixedPoint<U256>,
361 withdrawal_shares_total_supply: FixedPoint<U256>, // withdraw shares - ready to withdraw
362 max_iterations: u64,
363 ) -> Result<(FixedPoint<U256>, FixedPoint<U256>)> {
364 // Steps 1 and 2: Calculate the maximum amount the share reserves can be
365 // debited. If the effective share reserves or the maximum share
366 // reserves delta can't be calculated or if the maximum share reserves
367 // delta is zero, idle can't be distributed.
368 let success = match self.effective_share_reserves() {
369 Ok(_) => true,
370 // The error is safe from the calculation, panics are not.
371 Err(_) => false,
372 };
373 if success == false {
374 return Ok((fixed!(0), fixed!(0)));
375 }
376 let (max_share_reserves_delta, success) =
377 self.calculate_max_share_reserves_delta_safe(current_block_timestamp)?;
378 if success == false || max_share_reserves_delta == fixed!(0) {
379 return Ok((fixed!(0), fixed!(0)));
380 }
381
382 // Step 3: Calculate the amount of withdrawal shares that can be
383 // redeemed given the maximum share reserves delta. Otherwise, we
384 // proceed to calculating the amount of shares that should be paid out
385 // to redeem all of the withdrawal shares.
386 let withdrawal_shares_redeemed = {
387 let withdrawal_shares_redeemed = self
388 .calculate_distribute_excess_idle_withdrawal_shares_redeemed(
389 current_block_timestamp,
390 max_share_reserves_delta,
391 active_lp_total_supply,
392 withdrawal_shares_total_supply,
393 )?;
394
395 // Step 3: If none of the withdrawal shares could be redeemed, then
396 // we're done and we pay out nothing.
397 if withdrawal_shares_redeemed == fixed!(0) {
398 return Ok((fixed!(0), fixed!(0)));
399 }
400 // Step 3: Otherwise if this amount is less than or equal to the amount
401 // of withdrawal shares outstanding, then we're done and we pay out the
402 // full maximum share reserves delta.
403 else if withdrawal_shares_redeemed <= withdrawal_shares_total_supply {
404 return Ok((withdrawal_shares_redeemed, max_share_reserves_delta));
405 }
406 // Step 3: Otherwise, all of the withdrawal shares are redeemed, and we
407 // need to calculate the amount of shares the withdrawal pool should
408 // receive.
409 else {
410 withdrawal_shares_total_supply
411 }
412 };
413
414 // Step 4: Solve for the share proceeds that hold the LP share price
415 // invariant after all of the withdrawal shares are redeemed. If the
416 // calculation returns a share proceeds of zero, we can't pay out
417 // anything.
418 let share_proceeds = self.calculate_distribute_excess_idle_share_proceeds(
419 current_block_timestamp,
420 active_lp_total_supply,
421 withdrawal_shares_total_supply,
422 max_share_reserves_delta,
423 max_iterations,
424 )?;
425 if share_proceeds == fixed!(0) {
426 return Ok((fixed!(0), fixed!(0)));
427 }
428
429 // Step 4: If the share proceeds are greater than or equal to the
430 // maximum share reserves delta that was previously calculated, then
431 // we can't distribute excess idle since we ruled out the possibility
432 // of paying out the full maximum share reserves delta in step 3.
433 if share_proceeds >= max_share_reserves_delta {
434 return Ok((fixed!(0), fixed!(0)));
435 }
436
437 // Step 5: Return the amount of withdrawal shares redeemed and the
438 // share proceeds.
439 Ok((withdrawal_shares_redeemed, share_proceeds))
440 }
441
442 /// Calculates the amount of withdrawal shares that can be redeemed
443 /// given an amount of shares to remove from the share reserves.
444 /// Assuming that dz is the amount of shares to remove from the
445 /// reserves and dl is the amount of LP shares to be burned, we can
446 /// derive the calculation as follows:
447 ///
448 /// PV(0) / l = PV(dz) / (l - dl)
449 /// =>
450 /// dl = l - l * (PV(dz) / PV(0))
451 ///
452 /// We round this calculation up to err on the side of slightly too
453 /// many withdrawal shares being redeemed.
454 fn calculate_distribute_excess_idle_withdrawal_shares_redeemed(
455 &self,
456 current_block_timestamp: U256,
457 share_reserves_delta: FixedPoint<U256>,
458 active_lp_total_supply: FixedPoint<U256>,
459 withdrawal_shares_total_supply: FixedPoint<U256>,
460 ) -> Result<FixedPoint<U256>> {
461 // Calculate the present value after debiting the share reserves delta.
462 let updated_state =
463 match self.get_state_after_liquidity_update(-I256::try_from(share_reserves_delta)?) {
464 Ok(state) => state,
465 // NOTE: Return zero to indicate that the withdrawal shares redeemed
466 // couldn't be calculated.
467 Err(_) => return Ok(fixed!(0)),
468 };
469 let starting_present_value = match self.calculate_present_value(current_block_timestamp) {
470 Ok(present_value) => present_value,
471 // NOTE: Return zero to indicate that the withdrawal shares redeemed
472 // couldn't be calculated.
473 // Errors are safe from this calculation, panics are not.
474 Err(_) => return Ok(fixed!(0)),
475 };
476 let ending_present_value =
477 match updated_state.calculate_present_value(current_block_timestamp) {
478 Ok(present_value) => present_value,
479 // NOTE: Return zero to indicate that the withdrawal shares redeemed
480 // couldn't be calculated.
481 // Errors are safe from this calculation, panics are not.
482 Err(_) => return Ok(fixed!(0)),
483 };
484
485 // If the ending present value is greater than or equal to the starting
486 // present value, we short-circuit to avoid distributing excess idle.
487 // This edge-case can occur when the share reserves is very close to the
488 // minimum share reserves with a large value of k.
489 if ending_present_value >= starting_present_value {
490 return Ok(fixed!(0));
491 }
492
493 // NOTE: This subtraction is safe since the ending present value is less
494 // than the starting present value and the rhs is rounded down.
495 //
496 // Calculate the amount of withdrawal shares that can be redeemed.
497 let lp_total_supply = active_lp_total_supply + withdrawal_shares_total_supply;
498 Ok(lp_total_supply
499 - lp_total_supply.mul_div_down(ending_present_value, starting_present_value))
500 }
501
502 /// Calculates the share proceeds to distribute to the withdrawal pool
503 /// assuming that all of the outstanding withdrawal shares will be
504 /// redeemed. The share proceeds are calculated such that the LP share
505 /// price is conserved. When we need to round, we round down to err on
506 /// the side of slightly too few shares being paid out.
507 fn calculate_distribute_excess_idle_share_proceeds(
508 &self,
509 current_block_timestamp: U256,
510 active_lp_total_supply: FixedPoint<U256>, // just the number of lp tokens
511 withdrawal_shares_total_supply: FixedPoint<U256>,
512 max_share_reserves_delta: FixedPoint<U256>,
513 max_iterations: u64,
514 ) -> Result<FixedPoint<U256>> {
515 // Calculate the LP total supply.
516 let lp_total_supply = active_lp_total_supply + withdrawal_shares_total_supply;
517
518 // NOTE: Round the initial guess down to avoid overshooting.
519 //
520 // We make an initial guess for Newton's method by assuming that the
521 // ratio of the share reserves delta to the withdrawal shares
522 // outstanding is equal to the LP share price. In reality, the
523 // withdrawal pool should receive more than this, but it's a good
524 // starting point. The calculation is:
525 //
526 // x_0 = w * (PV(0) / l)
527 let starting_present_value = self.calculate_present_value(current_block_timestamp)?;
528 let mut share_proceeds =
529 withdrawal_shares_total_supply.mul_div_down(starting_present_value, lp_total_supply);
530
531 // If the pool is net neutral, the initial guess is equal to the final
532 // result.
533 let net_curve_trade =
534 self.calculate_net_curve_trade_from_timestamp(current_block_timestamp)?;
535 if net_curve_trade == int256!(0) {
536 return Ok(share_proceeds);
537 }
538
539 // Proceed with Newton's method. The objective function, `F(x)`, is
540 // given by:
541 //
542 // F(x) = PV(x) * l - PV(0) * (l - w)
543 //
544 // Newton's method will terminate as soon as the current iteration is
545 // within the minimum tolerance or the maximum number of iterations has
546 // been reached.
547 let mut smallest_delta = I256::zero();
548 let mut closest_share_proceeds = fixed!(0);
549 let mut closest_present_value = fixed!(0);
550 for _ in 0..max(max_iterations, SHARE_PROCEEDS_MAX_ITERATIONS) {
551 // Clamp the share proceeds to the max share reserves delta since
552 // values above this threshold are always invalid.
553 share_proceeds = min(share_proceeds, max_share_reserves_delta);
554
555 // Simulate applying the share proceeds to the reserves.
556 //
557 // NOTE: We are calling this with 'self' so that original values are
558 // used with the updated value of share_proceeds
559 let updated_state =
560 match self.get_state_after_liquidity_update(-I256::try_from(share_proceeds)?) {
561 Ok(state) => state,
562 // NOTE: If the updated reserves can't be calculated, we can't
563 // continue the calculation. Return 0 to indicate that the share
564 // proceeds couldn't be calculated.
565 // Errors are safe from this calculation, panics are not.
566 Err(_) => return Ok(fixed!(0)),
567 };
568
569 // Recalculate the present value.
570 let present_value = match updated_state.calculate_present_value(current_block_timestamp)
571 {
572 Ok(present_value) => present_value,
573 // NOTE: If the present value can't be calculated, we can't
574 // continue the calculation. Return 0 to indicate that the share
575 // proceeds couldn't be calculated.
576 // Errors are safe from this calculation, panics are not.
577 Err(_) => return Ok(fixed!(0)),
578 };
579
580 // Short-circuit if we are within the minimum tolerance.
581 if self.should_short_circuit_distribute_excess_idle_share_proceeds(
582 active_lp_total_supply,
583 starting_present_value,
584 lp_total_supply,
585 present_value,
586 )? {
587 return Ok(share_proceeds);
588 }
589
590 // If the pool is net long, we can solve for the next iteration of
591 // Newton's method directly when the net curve trade is greater than
592 // or equal to the max bond amount.
593 if net_curve_trade > I256::zero() {
594 // Calculate the max bond amount. If the calculation fails, we
595 // return a failure flag.
596 let max_bond_amount = match updated_state
597 .calculate_max_sell_bonds_in(self.minimum_share_reserves())
598 {
599 Ok(max_bond_amount) => max_bond_amount,
600 // NOTE: If the max bond amount couldn't be calculated, we
601 // can't continue the calculation. Return 0 to indicate that
602 // the share proceeds couldn't be calculated.
603 // Errors are safe from this calculation, panics are not.
604 Err(_) => return Ok(fixed!(0)),
605 };
606
607 // If the net curve trade is greater than or equal to the max
608 // bond amount, we can solve directly for the share proceeds.
609 let net_curve_trade = FixedPoint::from(U256::try_from(net_curve_trade)?);
610 if net_curve_trade >= max_bond_amount {
611 // Solve the objective function directly assuming that it is
612 // linear with respect to the share proceeds.
613
614 let (share_proceeds, success) = updated_state
615 .calculate_distribute_excess_idle_share_proceeds_net_long_edge_case_safe(
616 current_block_timestamp,
617 self.share_adjustment(),
618 self.share_reserves(),
619 starting_present_value,
620 active_lp_total_supply,
621 withdrawal_shares_total_supply,
622 )?;
623 if success == false {
624 // NOTE: Return 0 to indicate that the share proceeds
625 // couldn't be calculated.
626 return Ok(share_proceeds);
627 }
628
629 // Simulate applying the share proceeds to the reserves and
630 // recalculate the max bond amount.
631 //
632 // NOTE: We are calling this with 'self' so that original
633 // values are used with the updated value of share_proceeds.
634 let updated_state = match self
635 .get_state_after_liquidity_update(-I256::try_from(share_proceeds)?)
636 {
637 Ok(state) => state,
638 // NOTE: Return 0 to indicate that the share proceeds
639 // couldn't be calculated.
640 // Errors are safe from this calculation, panics are not.
641 Err(_) => return Ok(fixed!(0)),
642 };
643 let max_bond_amount = match updated_state
644 .calculate_max_sell_bonds_in(self.minimum_share_reserves())
645 {
646 Ok(max_bond_amount) => max_bond_amount,
647 // NOTE: Return 0 to indicate that the share proceeds
648 // couldn't be calculated.
649 // Errors are safe from this calculation, panics are not.
650 Err(_) => return Ok(fixed!(0)),
651 };
652
653 // If the max bond amount is less than or equal to the net
654 // curve trade, then Newton's method has terminated since
655 // proceeding to the next step would result in reaching the
656 // same point.
657 if max_bond_amount <= net_curve_trade {
658 return Ok(share_proceeds);
659 }
660 // Otherwise, we continue to the next iteration of Newton's
661 // method.
662 else {
663 continue;
664 }
665 }
666 }
667
668 // We calculate the derivative of F(x) using the derivative of
669 // `calculateSharesOutGivenBondsIn` when the pool is net long or
670 // the derivative of `calculateSharesInGivenBondsOut`. when the pool
671 // is net short.
672 let derivative = match updated_state
673 .calculate_shares_delta_given_bonds_delta_derivative(
674 net_curve_trade,
675 self.share_reserves(),
676 self.bond_reserves(),
677 self.effective_share_reserves()?,
678 self.share_adjustment(),
679 ) {
680 Ok(derivative) => derivative,
681 // NOTE: Return 0 to indicate that the share proceeds
682 // couldn't be calculated.
683 // Errors are safe from this calculation, panics are not.
684 Err(_) => return Ok(fixed!(0)),
685 };
686 if derivative >= fixed!(1e18) {
687 return Ok(fixed!(0));
688 }
689 let derivative = fixed!(1e18) - derivative;
690
691 // NOTE: Round the delta down to avoid overshooting.
692 //
693 // Calculate the objective function's value. If the value's magnitude
694 // is smaller than the previous smallest value, then we update the
695 // value and record the share proceeds. We'll ultimately return the
696 // share proceeds that resulted in the smallest value.
697 let delta = I256::try_from(present_value.mul_down(lp_total_supply))?
698 - I256::try_from(starting_present_value.mul_up(active_lp_total_supply))?;
699 if smallest_delta == I256::from(0) || delta.abs() < smallest_delta.abs() {
700 smallest_delta = delta;
701 closest_share_proceeds = share_proceeds;
702 closest_present_value = present_value;
703 }
704
705 // We calculate the updated share proceeds `x_n+1` by proceeding
706 // with Newton's method. This is given by:
707 //
708 // x_n+1 = x_n - F(x_n) / F'(x_n)
709 if delta > I256::zero() {
710 // NOTE: Round the quotient down to avoid overshooting.
711 share_proceeds = share_proceeds
712 + FixedPoint::try_from(delta)?
713 .div_down(derivative)
714 .div_down(lp_total_supply);
715 } else if delta < I256::zero() {
716 let delta = FixedPoint::try_from(-delta)?
717 .div_down(derivative)
718 .div_down(lp_total_supply);
719 if delta < share_proceeds {
720 share_proceeds = share_proceeds - delta;
721 } else {
722 // NOTE: Returning 0 to indicate that the share proceeds
723 // couldn't be calculated.
724 return Ok(fixed!(0));
725 }
726 } else {
727 break;
728 }
729 }
730
731 // Get the updated present value after applying the share proceeds.
732 let updated_state =
733 match self.get_state_after_liquidity_update(-I256::try_from(share_proceeds)?) {
734 Ok(state) => state,
735 // NOTE: Return 0 to indicate that the share proceeds couldn't
736 // be calculated.
737 // Errors are safe from this calculation, panics are not.
738 Err(_) => return Ok(fixed!(0)),
739 };
740 let present_value = updated_state.calculate_present_value(current_block_timestamp)?;
741
742 // Check to see if the current share proceeds is closer to the optimal
743 // value than the previous closest value. We'll choose whichever of the
744 // share proceeds that is closer to the optimal value.
745 let last_delta = I256::try_from(present_value.mul_down(lp_total_supply))?
746 - I256::try_from(starting_present_value.mul_up(active_lp_total_supply))?;
747 if last_delta.abs() < smallest_delta.abs() {
748 closest_share_proceeds = share_proceeds;
749 closest_present_value = present_value;
750 }
751
752 // Verify that the LP share price was conserved within a reasonable
753 // tolerance.
754 if
755 // NOTE: Round down to make the check stricter.
756 closest_present_value.div_down(active_lp_total_supply)
757 < starting_present_value.mul_div_up(
758 fixed!(1e18) - FixedPoint::from(SHARE_PROCEEDS_TOLERANCE),
759 lp_total_supply,
760 ) ||
761 // NOTE: Round up to make the check stricter.
762 closest_present_value.div_up(active_lp_total_supply)
763 > starting_present_value.mul_div_down(
764 fixed!(1e18) + FixedPoint::from(SHARE_PROCEEDS_TOLERANCE),
765 lp_total_supply,
766 )
767 {
768 return Err(eyre!("LP share price was not conserved within tolerance."));
769 }
770
771 Ok(closest_share_proceeds)
772 }
773
774 /// One of the edge cases that occurs when using Newton's method for
775 /// the share proceeds while distributing excess idle is when the net
776 /// curve trade is larger than the max bond amount. In this case, the
777 /// the present value simplifies to the following:
778 ///
779 /// PV(dz) = (z - dz) + net_c(dz) + net_f - z_min
780 /// = (z - dz) - z_max_out(dz) + net_f - z_min
781 ///
782 /// There are two cases to evaluate:
783 ///
784 /// (1) zeta > 0:
785 ///
786 /// z_max_out(dz) = ((z - dz) / z) * (z - zeta) - z_min
787 ///
788 /// =>
789 ///
790 /// PV(dz) = zeta * ((z - dz) / z) + net_f
791 ///
792 /// (2) zeta <= 0:
793 ///
794 /// z_max_out(dz) = (z - dz) - z_min
795 ///
796 /// =>
797 ///
798 /// PV(dz) = net_f
799 ///
800 /// Since the present value is constant with respect to the share
801 /// proceeds in case 2, Newton's method has achieved a stationary point
802 /// and can't proceed. On the other hand, the present value is linear
803 /// with respect to the share proceeds, and we can solve for the next
804 /// step of Newton's method directly as follows:
805 ///
806 /// PV(0) / l = PV(dz) / (l - w)
807 ///
808 /// =>
809 ///
810 /// dz = z - ((PV(0) / l) * (l - w) - net_f) / (zeta / z)
811 ///
812 /// We round the share proceeds down to err on the side of the
813 /// withdrawal pool receiving slightly less shares.
814 fn calculate_distribute_excess_idle_share_proceeds_net_long_edge_case_safe(
815 &self,
816 current_block_timestamp: U256,
817 original_share_adjustment: I256,
818 original_share_reserves: FixedPoint<U256>,
819 starting_present_value: FixedPoint<U256>,
820 active_lp_total_supply: FixedPoint<U256>,
821 withdrawal_shares_total_supply: FixedPoint<U256>,
822 ) -> Result<(FixedPoint<U256>, bool)> {
823 // If the original share adjustment is zero or negative, we cannot
824 // calculate the share proceeds. This should never happen, but for
825 // safety we return a failure flag and break the loop at this point.
826 if original_share_adjustment <= I256::zero() {
827 return Ok((fixed!(0), false));
828 }
829 let original_share_adjustment: U256 = original_share_adjustment.abs().try_into().unwrap();
830
831 // Calculate the net flat trade.
832 let net_flat_trade =
833 self.calculate_net_flat_trade_from_timestamp(current_block_timestamp)?;
834
835 // Avoid panic: make sure we don't divide by zero before calculating the rhs.
836 if active_lp_total_supply + withdrawal_shares_total_supply == fixed!(0) {
837 return Err(eyre!(
838 "active_lp_total_supply + withdrawal_shares_total_supply is zero"
839 ));
840 }
841
842 // NOTE: Round up since this is the rhs of the final subtraction.
843 //
844 // rhs = (PV(0) / l) * (l - w) - net_f
845 let rhs = {
846 let rhs: FixedPoint<U256> = starting_present_value.mul_div_up(
847 active_lp_total_supply,
848 active_lp_total_supply + withdrawal_shares_total_supply,
849 );
850 if net_flat_trade >= I256::zero() {
851 if net_flat_trade < I256::try_from(rhs)? {
852 rhs - net_flat_trade.try_into()?
853 } else {
854 // NOTE: Return a failure flag if computing the rhs would
855 // underflow.
856 return Ok((fixed!(0), false));
857 }
858 } else {
859 rhs + (-net_flat_trade).try_into()?
860 }
861 };
862
863 // NOTE: Round up since this is the rhs of the final subtraction.
864 //
865 // rhs = ((PV(0) / l) * (l - w) - net_f) / (zeta / z)
866 let rhs =
867 original_share_reserves.mul_div_up(rhs, FixedPoint::from(original_share_adjustment));
868
869 // share proceeds = z - rhs
870 if original_share_reserves < rhs {
871 return Ok((fixed!(0), false));
872 }
873
874 Ok((original_share_reserves - rhs, true))
875 }
876
877 /// Checks to see if we should short-circuit the iterative calculation of
878 /// the share proceeds when distributing excess idle liquidity. This
879 /// verifies that the ending LP share price is greater than or equal to the
880 /// starting LP share price and less than or equal to the starting LP share
881 /// price plus the minimum tolerance.
882 fn should_short_circuit_distribute_excess_idle_share_proceeds(
883 &self,
884 active_lp_total_supply: FixedPoint<U256>, // just the total number of lp shares
885 starting_present_value: FixedPoint<U256>,
886 lp_total_supply: FixedPoint<U256>, // total lp shares and withdrawal shares - w.s. ready to withraw
887 present_value: FixedPoint<U256>,
888 ) -> Result<bool> {
889 Ok(
890 // Ensure that new LP share price is greater than or equal to the
891 // previous LP share price:
892 //
893 // PV_1 / l_1 >= PV_0 / l_0
894 //
895 // NOTE: Round the LHS down to make the check stricter.
896 present_value.div_down(active_lp_total_supply) >=
897 starting_present_value.div_up(lp_total_supply)
898 // Ensure that new LP share price is less than or equal to the
899 // previous LP share price plus the minimum tolerance:
900 //
901 // PV_1 / l_1 <= (PV_0 / l_0) * (1 + tolerance)
902 //
903 // NOTE: Round the LHS up to make the check stricter.
904 && present_value.div_up(active_lp_total_supply) <=
905 (fixed!(1e18) + FixedPoint::from(SHARE_PROCEEDS_SHORT_CIRCUIT_TOLERANCE)).mul_div_down(
906 starting_present_value,
907 lp_total_supply),
908 )
909 }
910
911 /// Calculates the upper bound on the share proceeds of distributing
912 /// excess idle. When the pool is net long or net neutral, the upper
913 /// bound is the amount of idle liquidity. When the pool is net short,
914 /// the upper bound is the share reserves delta that results in the
915 /// maximum amount of bonds that can be purchased being equal to the
916 /// net short position.
917 fn calculate_max_share_reserves_delta_safe(
918 &self,
919 current_block_timestamp: U256,
920 ) -> Result<(FixedPoint<U256>, bool)> {
921 let net_curve_trade =
922 self.calculate_net_curve_trade_from_timestamp(current_block_timestamp)?;
923 let idle = self.calculate_idle_share_reserves();
924 // If the net curve position is zero or net long, then the maximum
925 // share reserves delta is equal to the pool's idle.
926 if net_curve_trade >= I256::from(0) {
927 return Ok((idle, true));
928 }
929 let net_curve_trade = FixedPoint::try_from(-net_curve_trade)?;
930
931 // Calculate the max bond amount. if the calculation fails, we return a
932 // failure flag. if the calculation succeeds but the max bond amount
933 // is zero, then we return a failure flag since we can't divide by zero.
934 let max_bond_amount = match self.calculate_max_buy_bonds_out() {
935 Ok(result) => result,
936 // Errors are safe from the calculation, panics are not.
937 Err(_) => fixed!(0),
938 };
939 if max_bond_amount == fixed!(0) {
940 return Ok((fixed!(0), false));
941 }
942
943 // We can solve for the maximum share reserves delta in one shot using
944 // the fact that the maximum amount of bonds that can be purchased is
945 // linear with respect to the scaling factor applied to the reserves.
946 // In other words, if s > 0 is a factor scaling the reserves, we have
947 // the following relationship:
948 //
949 // y_out^max(s * z, s * y, s * zeta) = s * y_out^max(z, y, zeta)
950 //
951 // We solve for the maximum share reserves delta by finding the scaling
952 // factor that results in the maximum amount of bonds that can be
953 // purchased being equal to the net curve trade. We can derive this
954 // maximum using the linearity property mentioned above as follows:
955 //
956 // y_out^max(s * z, s * y, s * zeta) - netCurveTrade = 0
957 // =>
958 // s * y_out^max(z, y, zeta) - netCurveTrade = 0
959 // =>
960 // s = netCurveTrade / y_out^max(z, y, zeta)
961 let max_scaling_factor = net_curve_trade.div_up(max_bond_amount);
962
963 // Using the maximum scaling factor, we can calculate the maximum share
964 // reserves delta as:
965 //
966 // maxShareReservesDelta = z * (1 - s)
967 let max_share_reserves_delta = if max_scaling_factor <= fixed!(1e18) {
968 (fixed!(1e18) - max_scaling_factor).mul_down(self.share_reserves())
969 } else {
970 // NOTE: If the max scaling factor is greater than one, the
971 // calculation fails and we return a failure flag.
972 return Ok((fixed!(0), false));
973 };
974
975 // If the maximum share reserves delta is greater than the idle, then
976 // the maximum share reserves delta is equal to the idle.
977 if max_share_reserves_delta > idle {
978 return Ok((idle, true));
979 }
980 return Ok((max_share_reserves_delta, true));
981 }
982
983 /// TODO: https://github.com/delvtech/hyperdrive/issues/965
984 ///
985 /// Note that the state is the present state of the pool and original values
986 /// passed in as parameters. Present sate variables are not expressly
987 /// paased in because so that downstream function like kUp() can still be
988 /// used.
989 ///
990 /// Given a signed bond amount, this function calculates the negation
991 /// of the derivative of `calculateSharesOutGivenBondsIn` when the
992 /// bond amount is positive or the derivative of
993 /// `calculateSharesInGivenBondsOut` when the bond amount is negative.
994 /// In both cases, the calculation is given by:
995 ///
996 /// derivative = (1 - zeta / z) * (
997 /// 1 - (1 / c) * (
998 /// c * (mu * z_e(x)) ** -t_s +
999 /// (y / z_e) * y(x) ** -t_s -
1000 /// (y / z_e) * (y(x) + dy) ** -t_s
1001 /// ) * (
1002 /// (mu / c) * (k(x) - (y(x) + dy) ** (1 - t_s))
1003 /// ) ** (t_s / (1 - t_s))
1004 /// )
1005 ///
1006 /// This quantity is used in Newton's method to search for the optimal
1007 /// share proceeds. When the pool is net long, We can express the
1008 /// derivative of the objective function F(x) by the derivative
1009 /// -z_out'(x) that this function returns:
1010 ///
1011 /// -F'(x) = l * -PV'(x)
1012 /// = l * (1 - net_c'(x))
1013 /// = l * (1 + z_out'(x))
1014 /// = l * (1 - derivative)
1015 ///
1016 /// When the pool is net short, we can express the derivative of the
1017 /// objective function F(x) by the derivative z_in'(x) that this
1018 /// function returns:
1019 ///
1020 /// -F'(x) = l * -PV'(x)
1021 /// = l * (1 - net_c'(x))
1022 /// = l * (1 - z_in'(x))
1023 /// = l * (1 - derivative)
1024 ///
1025 /// With these calculations in mind, this function rounds its result
1026 /// down so that F'(x) is overestimated. Since F'(x) is in the
1027 /// denominator of Newton's method, overestimating F'(x) helps to avoid
1028 /// overshooting the optimal solution.
1029 fn calculate_shares_delta_given_bonds_delta_derivative(
1030 &self,
1031 bond_amount: I256,
1032 original_share_reserves: FixedPoint<U256>,
1033 original_bond_reserves: FixedPoint<U256>,
1034 original_effective_share_reserves: FixedPoint<U256>,
1035 original_share_adjustment: I256,
1036 ) -> Result<FixedPoint<U256>> {
1037 // Calculate the bond reserves after the bond amount is applied.
1038 let bond_reserves_after = if bond_amount >= I256::zero() {
1039 self.bond_reserves() + bond_amount.try_into()?
1040 } else {
1041 let bond_amount = FixedPoint::from(U256::try_from(-bond_amount)?);
1042 if bond_amount < self.bond_reserves() {
1043 self.bond_reserves() - bond_amount
1044 } else {
1045 return Err(eyre!("Calculating the bond reserves underflows"));
1046 }
1047 };
1048
1049 // NOTE: Round up since this is on the rhs of the final subtraction.
1050 //
1051 // derivative = c * (mu * z_e(x)) ** -t_s +
1052 // (y / z_e) * (y(x)) ** -t_s -
1053 // (y / z_e) * (y(x) + dy) ** -t_s
1054 let effective_share_reserves = self.effective_share_reserves()?;
1055 // NOTE: The exponent is positive and base is flipped to handle the negative value.
1056 let derivative = self.vault_share_price().div_up(
1057 self.initial_vault_share_price()
1058 .mul_down(effective_share_reserves)
1059 .pow(self.time_stretch())?,
1060 ) + original_bond_reserves.div_up(
1061 original_effective_share_reserves
1062 .mul_down(self.bond_reserves().pow(self.time_stretch())?),
1063 );
1064
1065 // NOTE: Rounding this down rounds the subtraction up.
1066 let rhs = original_bond_reserves.div_down(
1067 original_effective_share_reserves.mul_up(bond_reserves_after.pow(self.time_stretch())?),
1068 );
1069 if derivative < rhs {
1070 return Err(eyre!("Derivative is less than right hand side"));
1071 }
1072 let derivative = derivative - rhs;
1073
1074 // NOTE: Round up since this is on the rhs of the final subtraction.
1075 //
1076 // inner = (
1077 // (mu / c) * (k(x) - (y(x) + dy) ** (1 - t_s))
1078 // ) ** (t_s / (1 - t_s))
1079 let k = self.k_up()?;
1080 let inner = bond_reserves_after.pow(fixed!(1e18) - self.time_stretch())?;
1081 if k < inner {
1082 return Err(eyre!("k is less than inner"));
1083 }
1084 let inner = k - inner;
1085 let inner = inner.mul_div_up(self.initial_vault_share_price(), self.vault_share_price());
1086 let inner = if inner >= fixed!(1e18) {
1087 // NOTE: Round the exponent up since this rounds the result up.
1088 inner.pow(
1089 self.time_stretch()
1090 .div_up(fixed!(1e18) - self.time_stretch()),
1091 )?
1092 } else {
1093 // NOTE: Round the exponent down since this rounds the result up.
1094 inner.pow(
1095 self.time_stretch()
1096 .div_down(fixed!(1e18) - self.time_stretch()),
1097 )?
1098 };
1099 let derivative = derivative.mul_div_up(inner, self.vault_share_price());
1100 let derivative = if fixed!(1e18) > derivative {
1101 fixed!(1e18) - derivative
1102 } else {
1103 // NOTE: Small rounding errors can result in the derivative being
1104 // slightly (on the order of a few wei) greater than 1. In this case,
1105 // we return 0 since we should proceed with Newton's method.
1106 return Ok(fixed!(0));
1107 };
1108 // NOTE: Round down to round the final result down.
1109 //
1110 // derivative = derivative * (1 - (zeta / z))
1111 let derivative = if original_share_adjustment >= I256::zero() {
1112 let right_hand_side =
1113 FixedPoint::try_from(original_share_adjustment)?.div_up(original_share_reserves);
1114 if right_hand_side > fixed!(1e18) {
1115 return Err(eyre!("Right hand side is greater than 1e18"));
1116 }
1117 let right_hand_side = fixed!(1e18) - right_hand_side;
1118 derivative.mul_down(right_hand_side)
1119 } else {
1120 derivative.mul_down(
1121 fixed!(1e18)
1122 + FixedPoint::try_from(-original_share_adjustment)?
1123 .div_down(original_share_reserves),
1124 )
1125 };
1126
1127 Ok(derivative)
1128 }
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133 use fixedpointmath::{fixed_i256, uint256};
1134 use hyperdrive_test_utils::{
1135 chain::TestChain,
1136 constants::{FAST_FUZZ_RUNS, FUZZ_RUNS},
1137 };
1138 use hyperdrive_wrappers::wrappers::mock_lp_math::{
1139 DistributeExcessIdleParams, PresentValueParams,
1140 };
1141 use rand::{thread_rng, Rng};
1142
1143 use super::*;
1144
1145 #[tokio::test]
1146 async fn fuzz_calculate_initial_reserves() -> Result<()> {
1147 let chain = TestChain::new().await?;
1148
1149 // Fuzz the rust and solidity implementations against each other.
1150 let mut rng = thread_rng();
1151 for _ in 0..*FAST_FUZZ_RUNS {
1152 let state = rng.gen::<State>();
1153 let initial_contribution = rng.gen_range(fixed!(0)..=state.bond_reserves());
1154 let initial_rate = rng.gen_range(fixed!(0)..=fixed!(1));
1155 let (actual_share_reserves, actual_share_adjustment, actual_bond_reserves) =
1156 state.calculate_initial_reserves(initial_contribution, initial_rate)?;
1157 match chain
1158 .mock_lp_math()
1159 .calculate_initial_reserves(
1160 initial_contribution.into(),
1161 state.vault_share_price().into(),
1162 state.initial_vault_share_price().into(),
1163 initial_rate.into(),
1164 state.position_duration().into(),
1165 state.time_stretch().into(),
1166 )
1167 .call()
1168 .await
1169 {
1170 Ok(expected) => {
1171 assert_eq!(actual_share_reserves, expected.0.into());
1172 assert_eq!(actual_share_adjustment, expected.1);
1173 assert_eq!(actual_bond_reserves, expected.2.into());
1174 }
1175 Err(_) => {}
1176 }
1177 }
1178
1179 Ok(())
1180 }
1181
1182 #[tokio::test]
1183 async fn fuzz_calculate_present_value() -> Result<()> {
1184 let chain = TestChain::new().await?;
1185
1186 // Fuzz the rust and solidity implementations against each other.
1187 let mut rng = thread_rng();
1188 for _ in 0..*FAST_FUZZ_RUNS {
1189 let state = rng.gen::<State>();
1190 let current_block_timestamp = rng.gen_range(fixed!(1)..=fixed!(1e4));
1191 let actual = state.calculate_present_value(current_block_timestamp.into());
1192 match chain
1193 .mock_lp_math()
1194 .calculate_present_value(PresentValueParams {
1195 share_reserves: state.info.share_reserves,
1196 bond_reserves: state.info.bond_reserves,
1197 longs_outstanding: state.info.longs_outstanding,
1198 share_adjustment: state.info.share_adjustment,
1199 time_stretch: state.config.time_stretch,
1200 vault_share_price: state.info.vault_share_price,
1201 initial_vault_share_price: state.config.initial_vault_share_price,
1202 minimum_share_reserves: state.config.minimum_share_reserves,
1203 minimum_transaction_amount: state.config.minimum_transaction_amount,
1204 long_average_time_remaining: state
1205 .calculate_scaled_normalized_time_remaining(
1206 state.long_average_maturity_time(),
1207 current_block_timestamp.into(),
1208 )
1209 .into(),
1210 short_average_time_remaining: state
1211 .calculate_scaled_normalized_time_remaining(
1212 state.short_average_maturity_time(),
1213 current_block_timestamp.into(),
1214 )
1215 .into(),
1216 shorts_outstanding: state.shorts_outstanding().into(),
1217 })
1218 .call()
1219 .await
1220 {
1221 Ok(expected) => {
1222 assert_eq!(actual.unwrap(), FixedPoint::from(expected));
1223 }
1224 Err(_) => assert!(actual.is_err()),
1225 }
1226 }
1227
1228 Ok(())
1229 }
1230
1231 #[tokio::test]
1232 async fn fuzz_calculate_net_curve_trade_safe() -> Result<()> {
1233 let chain = TestChain::new().await?;
1234
1235 // Fuzz the rust and solidity implementations against each other.
1236 let mut rng = thread_rng();
1237 for _ in 0..*FAST_FUZZ_RUNS {
1238 let state = rng.gen::<State>();
1239 let current_block_timestamp = rng.gen_range(fixed!(1)..=fixed!(1e4));
1240 let long_average_time_remaining = state.calculate_normalized_time_remaining(
1241 state.long_average_maturity_time().into(),
1242 current_block_timestamp.into(),
1243 );
1244 let short_average_time_remaining = state.calculate_normalized_time_remaining(
1245 state.short_average_maturity_time().into(),
1246 current_block_timestamp.into(),
1247 );
1248 let actual = state.calculate_net_curve_trade(
1249 long_average_time_remaining,
1250 short_average_time_remaining,
1251 );
1252 match chain
1253 .mock_lp_math()
1254 .calculate_net_curve_trade(PresentValueParams {
1255 share_reserves: state.info.share_reserves,
1256 bond_reserves: state.info.bond_reserves,
1257 longs_outstanding: state.info.longs_outstanding,
1258 share_adjustment: state.info.share_adjustment,
1259 time_stretch: state.config.time_stretch,
1260 vault_share_price: state.info.vault_share_price,
1261 initial_vault_share_price: state.config.initial_vault_share_price,
1262 minimum_share_reserves: state.config.minimum_share_reserves,
1263 minimum_transaction_amount: state.config.minimum_transaction_amount,
1264 long_average_time_remaining: long_average_time_remaining.into(),
1265 short_average_time_remaining: short_average_time_remaining.into(),
1266 shorts_outstanding: state.shorts_outstanding().into(),
1267 })
1268 .call()
1269 .await
1270 {
1271 Ok(expected) => {
1272 assert_eq!(actual.unwrap(), expected);
1273 }
1274 Err(_) => {
1275 assert!(actual.is_err());
1276 }
1277 }
1278 }
1279
1280 Ok(())
1281 }
1282
1283 #[tokio::test]
1284 async fn fuzz_calculate_net_flat_trade() -> Result<()> {
1285 let chain = TestChain::new().await?;
1286
1287 // Fuzz the rust and solidity implementations against each other.
1288 let mut rng = thread_rng();
1289 for _ in 0..*FAST_FUZZ_RUNS {
1290 let state = rng.gen::<State>();
1291
1292 let current_block_timestamp = rng.gen_range(fixed!(1)..=fixed!(1e4));
1293
1294 let long_average_time_remaining = state.calculate_normalized_time_remaining(
1295 state.long_average_maturity_time().into(),
1296 current_block_timestamp.into(),
1297 );
1298 let short_average_time_remaining = state.calculate_normalized_time_remaining(
1299 state.short_average_maturity_time().into(),
1300 current_block_timestamp.into(),
1301 );
1302 let actual = state.calculate_net_flat_trade(
1303 long_average_time_remaining,
1304 short_average_time_remaining,
1305 );
1306 match chain
1307 .mock_lp_math()
1308 .calculate_net_flat_trade(PresentValueParams {
1309 share_reserves: state.info.share_reserves,
1310 bond_reserves: state.info.bond_reserves,
1311 longs_outstanding: state.info.longs_outstanding,
1312 share_adjustment: state.info.share_adjustment,
1313 time_stretch: state.config.time_stretch,
1314 vault_share_price: state.info.vault_share_price,
1315 initial_vault_share_price: state.config.initial_vault_share_price,
1316 minimum_share_reserves: state.config.minimum_share_reserves,
1317 minimum_transaction_amount: state.config.minimum_transaction_amount,
1318 long_average_time_remaining: long_average_time_remaining.into(),
1319 short_average_time_remaining: short_average_time_remaining.into(),
1320 shorts_outstanding: state.shorts_outstanding().into(),
1321 })
1322 .call()
1323 .await
1324 {
1325 Ok(expected) => {
1326 assert_eq!(actual.unwrap(), expected);
1327 }
1328 Err(_) => assert!(actual.is_err()),
1329 }
1330 }
1331
1332 Ok(())
1333 }
1334
1335 #[tokio::test]
1336 async fn fuzz_calculate_distribute_excess_idle() -> Result<()> {
1337 let chain = TestChain::new().await?;
1338 let alice = chain.alice().await?;
1339 let mock = chain.mock_lp_math();
1340
1341 // Fuzz the rust and solidity implementations against each other.
1342 let mut rng = thread_rng();
1343
1344 for _ in 0..*FUZZ_RUNS {
1345 // Generate random states.
1346 let mut present_state = rng.gen::<State>();
1347
1348 // Make sure maturity times are in the future.
1349 let current_block_timestamp = alice.now().await?;
1350 present_state.info.long_average_maturity_time += current_block_timestamp;
1351 present_state.info.short_average_maturity_time += current_block_timestamp;
1352
1353 // active_lp_total_supply and _withdrawal_shares_total_supply are just the token supplies.
1354 // Neither are supplied in the PoolInfo so we need to make them here. lp_total_supply is defined as:
1355 // lp_total_supply = active_lp_total_supply + withdrawal_shares_total_supply - withdrawal_shares_ready_to_withdraw
1356 // active_lp_total_supply = lp_total_supply - withdrawal_shares_total_supply + withdrawal_shares_ready_to_withdraw
1357 // We clip withdrawal_shares_total_supply to ensure active_lp_total_supply doesn't underflow.
1358 let active_lp_total_supply = present_state.lp_total_supply()
1359 + present_state.withdrawal_shares_ready_to_withdraw();
1360 let withdrawal_shares_total_supply = rng.gen_range(fixed!(0)..=active_lp_total_supply);
1361 let active_lp_total_supply = active_lp_total_supply - withdrawal_shares_total_supply;
1362 // This errors out a lot so we need to catch that here.
1363 let starting_present_value =
1364 match present_state.calculate_present_value(U256::from(current_block_timestamp)) {
1365 Ok(result) => result,
1366 Err(_) => continue,
1367 };
1368
1369 // Calculate the result from the Rust implementation.
1370 let actual = present_state.calculate_distribute_excess_idle(
1371 current_block_timestamp,
1372 active_lp_total_supply,
1373 withdrawal_shares_total_supply,
1374 SHARE_PROCEEDS_MAX_ITERATIONS,
1375 );
1376
1377 // To keep precision of long and short average maturity time (from contract call)
1378 // we scale the block timestamp and position duration by 1e18 to calculate
1379 // the normalized time remaining.
1380 let long_average_time_remaining = present_state
1381 .calculate_scaled_normalized_time_remaining(
1382 present_state.long_average_maturity_time(),
1383 current_block_timestamp,
1384 );
1385 let short_average_time_remaining = present_state
1386 .calculate_scaled_normalized_time_remaining(
1387 present_state.short_average_maturity_time(),
1388 current_block_timestamp,
1389 );
1390 let net_curve_trade = present_state.calculate_net_curve_trade(
1391 long_average_time_remaining,
1392 short_average_time_remaining,
1393 )?;
1394
1395 let params = DistributeExcessIdleParams {
1396 present_value_params: PresentValueParams {
1397 share_reserves: present_state.info.share_reserves,
1398 bond_reserves: present_state.info.bond_reserves,
1399 longs_outstanding: present_state.info.longs_outstanding,
1400 share_adjustment: present_state.info.share_adjustment,
1401 time_stretch: present_state.config.time_stretch,
1402 vault_share_price: present_state.info.vault_share_price,
1403 initial_vault_share_price: present_state.config.initial_vault_share_price,
1404 minimum_share_reserves: present_state.config.minimum_share_reserves,
1405 minimum_transaction_amount: present_state.config.minimum_transaction_amount,
1406 long_average_time_remaining: long_average_time_remaining.into(),
1407 short_average_time_remaining: short_average_time_remaining.into(),
1408 shorts_outstanding: present_state.shorts_outstanding().into(),
1409 },
1410 starting_present_value: starting_present_value.into(),
1411 active_lp_total_supply: active_lp_total_supply.into(),
1412 withdrawal_shares_total_supply: withdrawal_shares_total_supply.into(),
1413 idle: present_state.calculate_idle_share_reserves().into(),
1414 net_curve_trade: net_curve_trade,
1415 original_share_reserves: present_state.share_reserves().into(),
1416 original_share_adjustment: present_state.share_adjustment(),
1417 original_bond_reserves: present_state.bond_reserves().into(),
1418 };
1419
1420 // Make the solidity call and compare to the Rust implementation.
1421 match mock
1422 .calculate_distribute_excess_idle(params, SHARE_PROCEEDS_MAX_ITERATIONS.into())
1423 .call()
1424 .await
1425 {
1426 Ok(expected) => {
1427 let (sol_withdrawal_shares_redeemed, sol_share_proceeds) = expected;
1428 let (rust_withdrawal_shares_redeemed, rust_share_proceeds) = actual?;
1429 assert_eq!(
1430 sol_withdrawal_shares_redeemed,
1431 U256::from(rust_withdrawal_shares_redeemed)
1432 );
1433 assert_eq!(sol_share_proceeds, U256::from(rust_share_proceeds));
1434 }
1435 Err(_) => {
1436 assert!(actual.is_err())
1437 }
1438 }
1439 }
1440 Ok(())
1441 }
1442
1443 #[tokio::test]
1444 async fn fuzz_calculate_distribute_excess_idle_withdrawal_shares_redeemed() -> Result<()> {
1445 let chain = TestChain::new().await?;
1446 let alice = chain.alice().await?;
1447 let mock = chain.mock_lp_math();
1448
1449 // Fuzz the rust and solidity implementations against each other.
1450 let mut rng = thread_rng();
1451
1452 for _ in 0..*FAST_FUZZ_RUNS {
1453 // Generate random states.
1454 let mut present_state = rng.gen::<State>();
1455
1456 // Make sure maturity times are in the future.
1457 let current_block_timestamp = alice.now().await?;
1458 present_state.info.long_average_maturity_time += current_block_timestamp;
1459 present_state.info.short_average_maturity_time += current_block_timestamp;
1460
1461 // active_lp_total_supply and _withdrawal_shares_total_supply are just the token supplies.
1462 // Neither are supplied in the PoolInfo so we need to make them here. lp_total_supply is defined as:
1463 // lp_total_supply = active_lp_total_supply + withdrawal_shares_total_supply - withdrawal_shares_ready_to_withdraw
1464 // active_lp_total_supply = lp_total_supply - withdrawal_shares_total_supply + withdrawal_shares_ready_to_withdraw
1465 // We clip withdrawal_shares_total_supply to ensure active_lp_total_supply doesn't underflow.
1466 let active_lp_total_supply = present_state.lp_total_supply()
1467 + present_state.withdrawal_shares_ready_to_withdraw();
1468 let withdrawal_shares_total_supply = rng.gen_range(fixed!(0)..=active_lp_total_supply);
1469 let active_lp_total_supply = active_lp_total_supply - withdrawal_shares_total_supply;
1470 // This errors out a lot so we need to catch that here.
1471 let starting_present_value =
1472 match present_state.calculate_present_value(U256::from(current_block_timestamp)) {
1473 Ok(result) => result,
1474 Err(_) => continue,
1475 };
1476 let (share_reserves_delta, _) =
1477 present_state.calculate_max_share_reserves_delta_safe(current_block_timestamp)?;
1478
1479 // Calculate the result from the Rust implementation.
1480 let actual = present_state.calculate_distribute_excess_idle_withdrawal_shares_redeemed(
1481 current_block_timestamp,
1482 share_reserves_delta,
1483 active_lp_total_supply,
1484 withdrawal_shares_total_supply,
1485 );
1486
1487 // To keep precision of long and short average maturity time (from contract call)
1488 // we scale the block timestamp and position duration by 1e18 to calculate
1489 // the normalized time remaining.
1490 let long_average_time_remaining = present_state
1491 .calculate_scaled_normalized_time_remaining(
1492 present_state.long_average_maturity_time(),
1493 current_block_timestamp,
1494 );
1495 let short_average_time_remaining = present_state
1496 .calculate_scaled_normalized_time_remaining(
1497 present_state.short_average_maturity_time(),
1498 current_block_timestamp,
1499 );
1500 let net_curve_trade = present_state.calculate_net_curve_trade(
1501 long_average_time_remaining,
1502 short_average_time_remaining,
1503 )?;
1504
1505 let params = DistributeExcessIdleParams {
1506 present_value_params: PresentValueParams {
1507 share_reserves: present_state.info.share_reserves,
1508 bond_reserves: present_state.info.bond_reserves,
1509 longs_outstanding: present_state.info.longs_outstanding,
1510 share_adjustment: present_state.info.share_adjustment,
1511 time_stretch: present_state.config.time_stretch,
1512 vault_share_price: present_state.info.vault_share_price,
1513 initial_vault_share_price: present_state.config.initial_vault_share_price,
1514 minimum_share_reserves: present_state.config.minimum_share_reserves,
1515 minimum_transaction_amount: present_state.config.minimum_transaction_amount,
1516 long_average_time_remaining: long_average_time_remaining.into(),
1517 short_average_time_remaining: short_average_time_remaining.into(),
1518 shorts_outstanding: present_state.shorts_outstanding().into(),
1519 },
1520 starting_present_value: starting_present_value.into(),
1521 active_lp_total_supply: active_lp_total_supply.into(),
1522 withdrawal_shares_total_supply: withdrawal_shares_total_supply.into(),
1523 idle: present_state.calculate_idle_share_reserves().into(),
1524 net_curve_trade: net_curve_trade,
1525 original_share_reserves: present_state.share_reserves().into(),
1526 original_share_adjustment: present_state.share_adjustment(),
1527 original_bond_reserves: present_state.bond_reserves().into(),
1528 };
1529
1530 // Make the solidity call and compare to the Rust implementation.
1531 match mock
1532 .calculate_distribute_excess_idle_withdrawal_shares_redeemed(
1533 params,
1534 share_reserves_delta.into(),
1535 )
1536 .call()
1537 .await
1538 {
1539 Ok(expected) => {
1540 let sol_withdrawal_shares_redeemed = expected;
1541 let rust_withdrawal_shares_redeemed = U256::from(actual?);
1542 assert_eq!(
1543 sol_withdrawal_shares_redeemed,
1544 rust_withdrawal_shares_redeemed,
1545 );
1546 }
1547 Err(_) => {
1548 assert!(actual.is_err())
1549 }
1550 }
1551 }
1552 Ok(())
1553 }
1554
1555 #[tokio::test]
1556 async fn fuzz_calculate_distribute_excess_idle_share_proceeds_net_long_edge_case() -> Result<()>
1557 {
1558 let chain = TestChain::new().await?;
1559 let alice = chain.alice().await?;
1560 let mock = chain.mock_lp_math();
1561
1562 // Fuzz the rust and solidity implementations against each other.
1563 let mut rng = thread_rng();
1564
1565 for _ in 0..*FAST_FUZZ_RUNS {
1566 // Generate random states.
1567 let original_state = rng.gen::<State>();
1568 let mut present_state = rng.gen::<State>();
1569
1570 // Generate random variables.
1571 let net_curve_trade = rng.gen_range(fixed_i256!(0)..=fixed!(1e24)).raw(); // 1 million
1572
1573 // active_lp_total_supply and _withdrawal_shares_total_supply are just the token supplies.
1574 // Neither are supplied in the PoolInfo so we need to make them here. lp_total_supply is defined as:
1575 // lp_total_supply = active_lp_total_supply + withdrawal_shares_total_supply - withdrawal_shares_ready_to_withdraw
1576 // active_lp_total_supply = lp_total_supply - withdrawal_shares_total_supply + withdrawal_shares_ready_to_withdraw
1577 // We clip withdrawal_shares_total_supply to ensure active_lp_total_supply doesn't underflow.
1578 let active_lp_total_supply = present_state.lp_total_supply()
1579 + present_state.withdrawal_shares_ready_to_withdraw();
1580 let withdrawal_shares_total_supply = rng.gen_range(fixed!(0)..=active_lp_total_supply);
1581 let active_lp_total_supply = active_lp_total_supply - withdrawal_shares_total_supply;
1582
1583 // Make sure maturity times are in the future.
1584 let current_block_timestamp = alice.now().await?;
1585 present_state.info.long_average_maturity_time += current_block_timestamp;
1586 present_state.info.short_average_maturity_time += current_block_timestamp;
1587
1588 // This errors out a lot so we need to catch that here.
1589 let starting_present_value =
1590 match original_state.calculate_present_value(U256::from(current_block_timestamp)) {
1591 Ok(result) => result,
1592 Err(_) => continue,
1593 };
1594
1595 // Calculate idle capital.
1596 let idle = present_state.calculate_idle_share_reserves();
1597
1598 // Calculate the result from the Rust implementation.
1599 let actual = present_state
1600 .calculate_distribute_excess_idle_share_proceeds_net_long_edge_case_safe(
1601 current_block_timestamp,
1602 original_state.share_adjustment(),
1603 original_state.share_reserves(),
1604 starting_present_value,
1605 active_lp_total_supply,
1606 withdrawal_shares_total_supply,
1607 );
1608
1609 let long_average_time_remaining = present_state
1610 .calculate_scaled_normalized_time_remaining(
1611 present_state.long_average_maturity_time(),
1612 current_block_timestamp,
1613 );
1614 let short_average_time_remaining = present_state
1615 .calculate_scaled_normalized_time_remaining(
1616 present_state.short_average_maturity_time(),
1617 current_block_timestamp,
1618 );
1619 // Gather the parameters for the solidity call. There are a lot
1620 // that aren't actually used, but the solidity call needs them.
1621 let params = DistributeExcessIdleParams {
1622 present_value_params: PresentValueParams {
1623 share_reserves: present_state.info.share_reserves,
1624 bond_reserves: present_state.info.bond_reserves,
1625 longs_outstanding: present_state.info.longs_outstanding,
1626 share_adjustment: present_state.info.share_adjustment,
1627 time_stretch: present_state.config.time_stretch,
1628 vault_share_price: present_state.info.vault_share_price,
1629 initial_vault_share_price: present_state.config.initial_vault_share_price,
1630 minimum_share_reserves: present_state.config.minimum_share_reserves,
1631 minimum_transaction_amount: present_state.config.minimum_transaction_amount,
1632 long_average_time_remaining: long_average_time_remaining.into(),
1633 short_average_time_remaining: short_average_time_remaining.into(),
1634 shorts_outstanding: present_state.shorts_outstanding().into(),
1635 },
1636 starting_present_value: starting_present_value.into(),
1637 active_lp_total_supply: active_lp_total_supply.into(),
1638 withdrawal_shares_total_supply: withdrawal_shares_total_supply.into(),
1639 idle: idle.into(),
1640 net_curve_trade: net_curve_trade,
1641 original_share_reserves: original_state.share_reserves().into(),
1642 original_share_adjustment: original_state.share_adjustment(),
1643 original_bond_reserves: original_state.bond_reserves().into(),
1644 };
1645
1646 // Make the solidity call and compare to the Rust implementation.
1647 match mock
1648 .calculate_distribute_excess_idle_share_proceeds_net_long_edge_case_safe(params)
1649 .call()
1650 .await
1651 {
1652 Ok(expected) => {
1653 let (expected_result, expected_success) = expected;
1654 let (actual_result, actual_success) = actual?;
1655 assert_eq!(actual_result, FixedPoint::from(expected_result));
1656 assert_eq!(actual_success, expected_success);
1657 }
1658 Err(_) => {
1659 assert!(actual.is_err())
1660 }
1661 }
1662 }
1663 Ok(())
1664 }
1665
1666 #[tokio::test]
1667 async fn fuzz_should_short_circuit_distribute_excess_idle_share_proceeds() -> Result<()> {
1668 let chain = TestChain::new().await?;
1669 let mock = chain.mock_lp_math();
1670
1671 // Fuzz the rust and solidity implementations against each other.
1672 let mut rng = thread_rng();
1673
1674 for _ in 0..*FAST_FUZZ_RUNS {
1675 // Generate random states.
1676 let present_state = rng.gen::<State>();
1677
1678 let starting_present_value = rng.gen_range(fixed!(0)..=fixed!(1e24));
1679 let present_value = rng.gen_range(fixed!(0)..=fixed!(1e24));
1680 let active_lp_total_supply = rng.gen_range(fixed!(0)..=fixed!(1e24));
1681 let lp_total_supply = rng.gen_range(fixed!(0)..=fixed!(1e24));
1682
1683 // Calculate the result from the Rust implementation.
1684 let actual = present_state.should_short_circuit_distribute_excess_idle_share_proceeds(
1685 active_lp_total_supply,
1686 starting_present_value,
1687 lp_total_supply,
1688 present_value,
1689 );
1690
1691 // Gather the parameters for the solidity call.
1692 let mut params = DistributeExcessIdleParams::default();
1693 params.active_lp_total_supply = active_lp_total_supply.into();
1694 params.starting_present_value = starting_present_value.into();
1695
1696 // Make the solidity call and compare to the Rust implementation.
1697 match mock
1698 .should_short_circuit_distribute_excess_idle_share_proceeds(
1699 params,
1700 lp_total_supply.into(),
1701 present_value.into(),
1702 )
1703 .call()
1704 .await
1705 {
1706 Ok(expected) => {
1707 if expected == true {
1708 assert!(actual? == true);
1709 } else {
1710 assert!(actual? == false);
1711 }
1712 }
1713 Err(_) => {
1714 assert!(actual.is_err())
1715 }
1716 }
1717 }
1718 Ok(())
1719 }
1720
1721 #[tokio::test]
1722 async fn fuzz_calculate_shares_delta_given_bonds_delta_derivative() -> Result<()> {
1723 let chain = TestChain::new().await?;
1724 let mock = chain.mock_lp_math();
1725
1726 // Fuzz the rust and solidity implementations against each other.
1727 let mut rng = thread_rng();
1728
1729 for _ in 0..*FAST_FUZZ_RUNS {
1730 // Generate random states.
1731 let original_state = rng.gen::<State>();
1732 let present_state = rng.gen::<State>();
1733
1734 // Get the bond amount, which in this case is equal to the net_curve_trade.
1735 let bond_amount = rng.gen_range(fixed_i256!(0)..=fixed!(1e24)).raw(); // 1 million
1736
1737 // Maturity time goes from 0 to position duration, so we'll just set
1738 // this to zero to make the math simpler.
1739 let current_block_timestamp = fixed!(0);
1740
1741 // Calcuulate the result from the Rust implementation.
1742 let actual = present_state.calculate_shares_delta_given_bonds_delta_derivative(
1743 bond_amount,
1744 original_state.share_reserves(),
1745 original_state.bond_reserves(),
1746 original_state.effective_share_reserves()?,
1747 original_state.share_adjustment(),
1748 );
1749
1750 // This errors out a lot so we need to catch that here.
1751 let starting_present_value_result =
1752 original_state.calculate_present_value(U256::from(current_block_timestamp));
1753 if starting_present_value_result.is_err() {
1754 continue;
1755 }
1756 let starting_present_value = starting_present_value_result?;
1757 let idle = present_state.calculate_idle_share_reserves();
1758
1759 // Gather the parameters for the solidity call. There are a lot
1760 // that aren't actually used, but the solidity call needs them.
1761 let params = DistributeExcessIdleParams {
1762 present_value_params: PresentValueParams {
1763 share_reserves: present_state.info.share_reserves,
1764 bond_reserves: present_state.info.bond_reserves,
1765 longs_outstanding: present_state.info.longs_outstanding,
1766 share_adjustment: present_state.info.share_adjustment,
1767 time_stretch: present_state.config.time_stretch,
1768 vault_share_price: present_state.info.vault_share_price,
1769 initial_vault_share_price: present_state.config.initial_vault_share_price,
1770 minimum_share_reserves: present_state.config.minimum_share_reserves,
1771 minimum_transaction_amount: present_state.config.minimum_transaction_amount,
1772 long_average_time_remaining: present_state
1773 .calculate_normalized_time_remaining(
1774 present_state.long_average_maturity_time().into(),
1775 current_block_timestamp.into(),
1776 )
1777 .into(),
1778 short_average_time_remaining: present_state
1779 .calculate_normalized_time_remaining(
1780 present_state.short_average_maturity_time().into(),
1781 current_block_timestamp.into(),
1782 )
1783 .into(),
1784 shorts_outstanding: present_state.shorts_outstanding().into(),
1785 },
1786 starting_present_value: starting_present_value.into(),
1787 active_lp_total_supply: original_state.lp_total_supply().into(),
1788 withdrawal_shares_total_supply: uint256!(0),
1789 idle: idle.into(),
1790 net_curve_trade: bond_amount,
1791 original_share_reserves: original_state.share_reserves().into(),
1792 original_share_adjustment: original_state.share_adjustment(),
1793 original_bond_reserves: original_state.bond_reserves().into(),
1794 };
1795
1796 // Make the solidity call and compare to the Rust implementation.
1797 match mock
1798 .calculate_shares_delta_given_bonds_delta_derivative_safe(
1799 params,
1800 U256::from(original_state.effective_share_reserves()?),
1801 bond_amount,
1802 )
1803 .call()
1804 .await
1805 {
1806 Ok(expected) => {
1807 let (result, success) = expected;
1808 if !success && result == uint256!(0) {
1809 assert!(actual.is_err());
1810 } else if success && result == uint256!(0) {
1811 assert_eq!(actual?, fixed!(0));
1812 } else {
1813 assert_eq!(actual?, FixedPoint::from(expected.0));
1814 }
1815 }
1816 Err(_) => {
1817 assert!(actual.is_err())
1818 }
1819 }
1820 }
1821 Ok(())
1822 }
1823}