From 3e15cabffddaec42a854f57ea5a13bc9aec28673 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Dec 2024 11:45:14 -0800 Subject: [PATCH 1/6] update short functions --- crates/hyperdrive-math/src/short/max.rs | 108 ++++++++++++------------ 1 file changed, 52 insertions(+), 56 deletions(-) diff --git a/crates/hyperdrive-math/src/short/max.rs b/crates/hyperdrive-math/src/short/max.rs index 82ca2e70..4a6ce8c4 100644 --- a/crates/hyperdrive-math/src/short/max.rs +++ b/crates/hyperdrive-math/src/short/max.rs @@ -1,6 +1,6 @@ use ethers::types::{I256, U256}; use eyre::{eyre, Result}; -use fixedpointmath::{fixed, FixedPoint}; +use fixedpointmath::{fixed, fixed_i256, FixedPoint}; use crate::{calculate_effective_share_reserves, State, YieldSpace}; @@ -39,47 +39,45 @@ impl State { .pow(self.time_stretch()) } - /// Calculate the minimum share reserves allowed by the pool given the - /// current exposure and share adjustment. - pub fn calculate_min_share_reserves( - &self, - checkpoint_exposure: I256, - ) -> Result> { - // We have the twin constraints that `$z \geq z_{min} + exposure$` and - // `$z - \zeta \geq z_{min}$`. Combining these together, we calculate - // the share reserves after a max short as - // `$z_{optimal} = z_{min} + max(0, \zeta) + exposure$`. - let exposure_shares = self.long_minus_checkpoint_exposure_shares(checkpoint_exposure)?; + /// Calculate the minimum pool share reserves assuming a short trade is + /// next, given the current exposure and share adjustment. + /// + /// We have the twin constraints that `$z \geq z_{min} + exposure$` and + /// `$z - \zeta \geq z_{min}$`. Combining these together, we calculate + /// the share reserves after a max short as + /// `$z_{optimal} = z_{min} + max(0, \zeta) + exposure$`. + /// + /// This function is useful when working with shorts because it converts + /// a piece-wise linear calculation into a linear one by computing the net + /// exposure. + pub fn calculate_min_share_reserves_given_exposure(&self) -> Result> { + // We want the current total exposure, with no netting from shorts. + let exposure_shares = self.exposure_after_short_shares(fixed!(0))?; let min_share_reserves = self.minimum_share_reserves() + FixedPoint::try_from(self.share_adjustment().max(I256::zero()))? + exposure_shares; Ok(min_share_reserves) } - /// Calculate the long exposure minus the checkpoint exposure, in shares. + /// Calculate the current exposure minus new short exposure, in shares. /// /// The long exposure will account for any executed trades. Any new short - /// net previous longs by subtracting from the long exposure. This increases - /// solvency until the checkpoint exposure goes negative. Past that point, - /// shorts will impact solvency by decreasing share reserves. - /// - /// This function is useful when working with shorts because it converts - /// a piece-wise linear calculation into a linear one by computing the net - /// exposure. - fn long_minus_checkpoint_exposure_shares( + /// nets previous longs by subtracting bonds from the long exposure. + /// This increases solvency until the checkpoint exposure goes negative. + /// Past that point, shorts will impact solvency by decreasing share + /// reserves. + fn exposure_after_short_shares( &self, - checkpoint_exposure: I256, + bond_amount: FixedPoint, ) -> Result> { let exposure_shares = { - let checkpoint_exposure = FixedPoint::try_from(checkpoint_exposure.max(I256::zero()))?; - if self.long_exposure() < checkpoint_exposure { - return Err(eyre!( - "expected long_exposure={:#?} >= checkpoint_exposure={:#?}.", - self.long_exposure(), - checkpoint_exposure - )); - } else { - (self.long_exposure() - checkpoint_exposure).div_up(self.vault_share_price()) + // The short will net out all long exposure. + if self.long_exposure() < bond_amount { + fixed!(0) + } + // The short will leave some exposure. + else { + (self.long_exposure() - bond_amount).div_up(self.vault_share_price()) } }; Ok(exposure_shares) @@ -724,26 +722,9 @@ impl State { /// short: /// /// ```math - /// e(\Delta y) = e_0 - min(\Delta y + D(\Delta y), max(e_{c}, 0)) - /// ``` - /// - /// We simplify our `$e(\Delta y)$` formula by noting that the max short is - /// only constrained by solvency when - /// `$\Delta y + D(\Delta y) > max(e_{c}, 0)$` since - /// `$\Delta y + D(\Delta y)$` grows faster than - /// `$P(\Delta y) - \tfrac{\phi_{c}}{c} \cdot \left( 1 - p \right) \cdot \Delta y$`. - /// With this in mind, - /// `$min(\Delta y + D(\Delta y), max(e_{c}, 0)) = max(e_{c}, 0)$` whenever - /// solvency is actually a constraint, so we can write: - /// - /// ```math - /// e(\Delta y) = e_0 - max(e_{c}, 0) + /// e(\Delta y) = e_0 - \Delta y /// ``` - fn solvency_after_short( - &self, - bond_amount: FixedPoint, - checkpoint_exposure: I256, - ) -> Result> { + fn solvency_after_short(&self, bond_amount: FixedPoint) -> Result> { let pool_share_delta = self.calculate_pool_share_delta_after_open_short(bond_amount)?; // If the share reserves would underflow when the short is opened, // then we revert with an insufficient liquidity error. @@ -754,7 +735,7 @@ impl State { pool_share_delta )); } - // Need to check that z - zeta >= z_min + // Need to check that z - zeta >= z_min. let new_share_reserves = self.share_reserves() - pool_share_delta; let new_effective_share_reserves = calculate_effective_share_reserves(new_share_reserves, self.share_adjustment())?; @@ -763,7 +744,7 @@ impl State { new_effective_share_reserves, self.minimum_share_reserves())); } // Check global exposure. This also ensures z >= z_min. - let exposure_shares = self.long_minus_checkpoint_exposure_shares(checkpoint_exposure)?; + let exposure_shares = self.exposure_after_short_shares(bond_amount)?; if new_share_reserves >= exposure_shares + self.minimum_share_reserves() { Ok(new_share_reserves - exposure_shares - self.minimum_share_reserves()) } else { @@ -811,7 +792,17 @@ impl State { /// /// ```math /// s'(\Delta y) = 0 - \left( P'(\Delta y) - \frac{(c'(\Delta y) - /// - g'(\Delta y))}{c} \right) + /// - g'(\Delta y))}{c} \right) - \frac{e'(\Delta y)}{c} + /// ``` + /// + /// where `$e'(\Delta y)$` is piecewise linear: + /// + /// ```math + /// e'(\Delta y) = + /// \begin{cases} + /// -\frac{1}{c} & \text{if} e_0 <\Delta y \\ + /// 0 & \text{otherwise} + /// \end{cases} /// ``` fn solvency_after_short_derivative( &self, @@ -825,15 +816,20 @@ impl State { * (fixed!(1e18) - self.governance_lp_fee()) / self.vault_share_price()) .change_type::()?; - Ok(-(lhs - rhs)) + let exposure_prime = if self.long_exposure() < bond_amount { + fixed_i256!(0) + } else { + -(fixed!(1e18).div_up(self.vault_share_price())).change_type::()? + }; + Ok(-(lhs - rhs) - exposure_prime) } } #[cfg(test)] mod tests { - use std::{panic, path::absolute}; + use std::panic; - use ethers::types::{I256, U128, U256}; + use ethers::types::U256; use fixedpointmath::{fixed, fixed_u256, uint256, FixedPoint}; use hyperdrive_test_utils::{ chain::TestChain, From ed830b52a281e355c74c4acc09116ab0303af68c Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Dec 2024 11:59:35 -0800 Subject: [PATCH 2/6] propagate changes --- .../python/hyperdrivepy/hyperdrive_state.py | 4 - .../src/hyperdrive_state_methods/short/max.rs | 8 - bindings/hyperdrivepy/tests/wrapper_test.py | 6 - crates/hyperdrive-math/src/short/max.rs | 234 +++++------------- crates/hyperdrive-math/src/short/open.rs | 102 +++----- .../hyperdrive-math/src/test_utils/agent.rs | 2 - .../src/test_utils/integration_tests.rs | 12 +- .../src/test_utils/preamble.rs | 28 +-- crates/hyperdrive-math/src/utils.rs | 8 +- 9 files changed, 101 insertions(+), 303 deletions(-) diff --git a/bindings/hyperdrivepy/python/hyperdrivepy/hyperdrive_state.py b/bindings/hyperdrivepy/python/hyperdrivepy/hyperdrive_state.py index feed0971..f1514517 100644 --- a/bindings/hyperdrivepy/python/hyperdrivepy/hyperdrive_state.py +++ b/bindings/hyperdrivepy/python/hyperdrivepy/hyperdrive_state.py @@ -557,7 +557,6 @@ def calculate_max_short( pool_info: types.PoolInfoType, budget: str, open_vault_share_price: str, - checkpoint_exposure: str, maybe_conservative_price: str | None, maybe_max_iterations: int | None, ) -> str: @@ -575,8 +574,6 @@ def calculate_max_short( The account budget in base for making a short. open_vault_share_price: str (FixedPoint) The share price of underlying vault. - checkpoint_exposure: str (FixedPoint) - The net exposure for the given checkpoint. maybe_conservative_price: str (FixedPoint), optional A lower bound on the realized price that the short will pay. maybe_max_iterations: int, optional @@ -590,7 +587,6 @@ def calculate_max_short( return _get_interface(pool_config, pool_info).calculate_max_short( budget, open_vault_share_price, - checkpoint_exposure, maybe_conservative_price, maybe_max_iterations, ) diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs index eeda4f75..4aaf76b2 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs @@ -10,7 +10,6 @@ impl HyperdriveState { &self, budget: &str, open_vault_share_price: &str, - checkpoint_exposure: &str, maybe_conservative_price: Option<&str>, maybe_max_iterations: Option, ) -> PyResult { @@ -27,12 +26,6 @@ impl HyperdriveState { open_vault_share_price, err )) })?); - let checkpoint_exposure_i = I256::from_dec_str(checkpoint_exposure).map_err(|err| { - PyErr::new::(format!( - "Failed to convert checkpoint_exposure string {} to I256: {}", - checkpoint_exposure, err - )) - })?; let maybe_conservative_price_fp = if let Some(conservative_price) = maybe_conservative_price { Some(FixedPoint::from( @@ -51,7 +44,6 @@ impl HyperdriveState { .calculate_max_short( budget_fp, open_vault_share_price_fp, - checkpoint_exposure_i, maybe_conservative_price_fp, maybe_max_iterations, ) diff --git a/bindings/hyperdrivepy/tests/wrapper_test.py b/bindings/hyperdrivepy/tests/wrapper_test.py index f2436e07..0766ea4a 100644 --- a/bindings/hyperdrivepy/tests/wrapper_test.py +++ b/bindings/hyperdrivepy/tests/wrapper_test.py @@ -329,7 +329,6 @@ def test_max_short(): # test using the state directly budget = str(int(10 * 10**18)) # 10k base open_vault_share_price = str(int(1 * 10**18)) # 1 base - checkpoint_exposure = str(0) conservative_price = None max_iterations = 20 max_short = hyperdrivepy.calculate_max_short( @@ -337,7 +336,6 @@ def test_max_short(): POOL_INFO, budget, open_vault_share_price, - checkpoint_exposure, conservative_price, max_iterations, ) @@ -347,7 +345,6 @@ def test_max_short(): def test_max_short_fail_conversion(): """Test calculate_max_short.""" open_vault_share_price = str(int(1 * 10**18)) # 1 base - checkpoint_exposure = str(0) conservative_price = None max_iterations = 20 # bad string inputs @@ -358,7 +355,6 @@ def test_max_short_fail_conversion(): POOL_INFO, budget, open_vault_share_price, - checkpoint_exposure, conservative_price, max_iterations, ) @@ -369,7 +365,6 @@ def test_max_short_fail_conversion(): POOL_INFO, budget, open_vault_share_price, - checkpoint_exposure, conservative_price, max_iterations, ) @@ -381,7 +376,6 @@ def test_max_short_fail_conversion(): POOL_INFO, budget, open_vault_share_price, - checkpoint_exposure, conservative_price, max_iterations, ) diff --git a/crates/hyperdrive-math/src/short/max.rs b/crates/hyperdrive-math/src/short/max.rs index 4a6ce8c4..be3b4348 100644 --- a/crates/hyperdrive-math/src/short/max.rs +++ b/crates/hyperdrive-math/src/short/max.rs @@ -223,25 +223,19 @@ impl State { /// on the realized price that the short will pay. This is used to help the /// algorithm converge faster in real world situations. If this is `None`, /// then we'll use the theoretical worst case realized price. - pub fn calculate_max_short< - F1: Into>, - F2: Into>, - I: Into, - >( + pub fn calculate_max_short>, F2: Into>>( &self, budget: F1, open_vault_share_price: F2, - checkpoint_exposure: I, maybe_conservative_price: Option>, // TODO: Is there a nice way of abstracting the inner type? maybe_max_iterations: Option, ) -> Result> { let budget = budget.into(); let open_vault_share_price = open_vault_share_price.into(); - let checkpoint_exposure = checkpoint_exposure.into(); // Sanity check that we can open any shorts at all. if self - .solvency_after_short(self.minimum_transaction_amount(), checkpoint_exposure) + .solvency_after_short(self.minimum_transaction_amount()) .is_err() { return Err(eyre!("No solvent short is possible.")); @@ -288,7 +282,7 @@ impl State { // The initial guess should be guaranteed correct, and we should only // get better from there. let absolute_max_bond_amount = - self.calculate_absolute_max_short(checkpoint_exposure, None, maybe_max_iterations)?; + self.calculate_absolute_max_short(None, maybe_max_iterations)?; // The max bond amount might be below the pool's minimum. If so, no // short can be opened. if absolute_max_bond_amount < self.minimum_transaction_amount() { @@ -311,11 +305,10 @@ impl State { maybe_conservative_price, ) .max(self.minimum_transaction_amount()); - let mut best_valid_max_bond_amount = - match self.solvency_after_short(max_bond_amount, checkpoint_exposure) { - Ok(_) => max_bond_amount, - Err(_) => self.minimum_transaction_amount(), - }; + let mut best_valid_max_bond_amount = match self.solvency_after_short(max_bond_amount) { + Ok(_) => max_bond_amount, + Err(_) => self.minimum_transaction_amount(), + }; // Use Newton's method to iteratively approach a solution. We use the // short deposit in base minus the budget as our objective function, @@ -531,7 +524,6 @@ impl State { /// insolvency the pool should be. pub fn calculate_absolute_max_short( &self, - checkpoint_exposure: I256, maybe_bond_tolerance: Option>, maybe_max_iterations: Option, ) -> Result> { @@ -539,15 +531,15 @@ impl State { let max_iterations = maybe_max_iterations.unwrap_or(500); // Use a conservative guess to get us close and speed up the process. let target_pool_shares = calculate_effective_share_reserves( - self.calculate_min_share_reserves(checkpoint_exposure)?, + self.calculate_min_share_reserves_given_exposure()?, self.share_adjustment(), )? .change_type::()?; - let mut last_good_bond_amount = self.absolute_max_short_guess(checkpoint_exposure)?; + let mut last_good_bond_amount = self.absolute_max_short_guess()?; for _ in 0..max_iterations { // Return if we are within tolerance bonds of insolvency. - let solvency_within_tolerance = self - .solvency_after_short(last_good_bond_amount + bond_tolerance, checkpoint_exposure); + let solvency_within_tolerance = + self.solvency_after_short(last_good_bond_amount + bond_tolerance); if solvency_within_tolerance.is_err() { return Ok(last_good_bond_amount); } @@ -581,36 +573,32 @@ impl State { }; // Calculate the current solvency & iterate w/ a good bond amount. - last_good_bond_amount = - match self.solvency_after_short(new_bond_amount, checkpoint_exposure) { - Ok(solvency) => new_bond_amount, - // New bond amount is too large. Use a binary search to find a - // new starting point. - Err(_) => { - // Start from halfway between the new bond amount and the - // last good bond amount. - let mut return_amount = new_bond_amount - - ((new_bond_amount - last_good_bond_amount) / fixed!(2e18)); - let mut num_tries = 0; - loop { - if self - .solvency_after_short(return_amount, checkpoint_exposure) - .is_ok() - { - break return_amount; - } - // Return amount is still too large, try again. - // U256 ensures return_amount > last_good_bond_amount - return_amount -= (return_amount - last_good_bond_amount) / fixed!(2e18); - num_tries += 1; - // Avoid running forever by returning a guaranteed good - // bond amount. - if num_tries >= 100_000 { - break last_good_bond_amount * fixed!(0.99e18); - } + last_good_bond_amount = match self.solvency_after_short(new_bond_amount) { + Ok(_) => new_bond_amount, + // New bond amount is too large. Use a binary search to find a + // new starting point. + Err(_) => { + // Start from halfway between the new bond amount and the + // last good bond amount. + let mut return_amount = new_bond_amount + - ((new_bond_amount - last_good_bond_amount) / fixed!(2e18)); + let mut num_tries = 0; + loop { + if self.solvency_after_short(return_amount).is_ok() { + break return_amount; + } + // Return amount is still too large, try again. + // U256 ensures return_amount > last_good_bond_amount + return_amount -= (return_amount - last_good_bond_amount) / fixed!(2e18); + num_tries += 1; + // Avoid running forever by returning a guaranteed good + // bond amount. + if num_tries >= 100_000 { + break last_good_bond_amount * fixed!(0.99e18); } } - }; + } + }; } // We did not find a solution within tolerance in the provided number of // iterations. @@ -658,11 +646,11 @@ impl State { /// While this should always provide a conservative estimate, we include /// an iterative loop that checks solvency and refines the result as a /// precautionary measure. - fn absolute_max_short_guess(&self, checkpoint_exposure: I256) -> Result> { + fn absolute_max_short_guess(&self) -> Result> { // We cannot directly solve for a valid delta y that produces the // minimum effective share reserves, so instead we use a linear // approximation of the YieldSpace component. - let min_share_reserves = self.calculate_min_share_reserves(checkpoint_exposure)?; + let min_share_reserves = self.calculate_min_share_reserves_given_exposure()?; if self.effective_share_reserves()? < min_share_reserves { return Err(eyre!( "Current effective pool share reserves = {:#?} are below the minimum = {:#?}.", @@ -686,7 +674,7 @@ impl State { ); // Iteratively adjust to ensure solvency. loop { - match self.solvency_after_short(conservative_bond_delta, checkpoint_exposure) { + match self.solvency_after_short(conservative_bond_delta) { Ok(_) => break, Err(_) => { conservative_bond_delta /= fixed!(2e18); @@ -855,15 +843,6 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - }; - // Min trade amount should be at least 1,000x the derivative epsilon let bond_amount = rng.gen_range(fixed!(1e18)..=fixed!(10_000_000e18)); @@ -932,21 +911,10 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - }; - // Min trade amount should be at least 1,000x the derivative epsilon let bond_amount = rng.gen_range(fixed!(1e18)..=fixed!(10_000_000e18)); - let f_x = match panic::catch_unwind(|| { - state.solvency_after_short(bond_amount, checkpoint_exposure) - }) { + let f_x = match panic::catch_unwind(|| state.solvency_after_short(bond_amount)) { Ok(result) => match result { Ok(result) => result, Err(_) => continue, // The amount resulted in the pool being insolvent. @@ -954,10 +922,7 @@ mod tests { Err(_) => continue, // Overflow or underflow error from FixedPoint. }; let f_x_plus_delta = match panic::catch_unwind(|| { - state.solvency_after_short( - bond_amount + empirical_derivative_epsilon, - checkpoint_exposure, - ) + state.solvency_after_short(bond_amount + empirical_derivative_epsilon) }) { Ok(result) => match result { Ok(result) => result, @@ -1011,24 +976,12 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FUZZ_RUNS { let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - }; // TODO: Absolute max doesn't always work. Sometimes the random // state causes an overflow when calculating absolute max. // Unlikely: fix the state generator so that the random state always has a valid max. // Likely: fix absolute max short such that the output is guaranteed to be solvent. match panic::catch_unwind(|| { - state.calculate_absolute_max_short( - checkpoint_exposure, - Some(test_tolerance), - Some(max_iterations), - ) + state.calculate_absolute_max_short(Some(test_tolerance), Some(max_iterations)) }) { Ok(max_bond_no_panic) => { match max_bond_no_panic { @@ -1048,13 +1001,12 @@ mod tests { }, Err(_) => continue, } - let max_short_bonds = - match get_max_short(state.clone(), checkpoint_exposure, None) { - Ok(max_short_trade) => { - max_short_trade.min(absolute_max_bond_amount) - } - Err(_) => continue, - }; + let max_short_bonds = match get_max_short(state.clone(), None) { + Ok(max_short_trade) => { + max_short_trade.min(absolute_max_bond_amount) + } + Err(_) => continue, + }; let max_short_base = state .calculate_open_short(max_short_bonds, open_vault_share_price)?; let target_base_amount = @@ -1104,34 +1056,22 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { // Compute a random state and checkpoint exposure. let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - } - .min(I256::try_from(state.long_exposure())?); - // Check that a short is possible. if state .effective_share_reserves()? .min(state.share_reserves()) - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { continue; } - match state - .solvency_after_short(state.minimum_transaction_amount(), checkpoint_exposure) - { + match state.solvency_after_short(state.minimum_transaction_amount()) { Ok(_) => (), Err(_) => continue, } // Compute the guess, check that it is solvent. - let max_short_guess = state.absolute_max_short_guess(checkpoint_exposure)?; - let solvency = state.solvency_after_short(max_short_guess, checkpoint_exposure)?; + let max_short_guess = state.absolute_max_short_guess()?; + let solvency = state.solvency_after_short(max_short_guess)?; // Check that the remaining available shares in the pool are below a // solvency tolerance. @@ -1157,47 +1097,30 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FUZZ_RUNS { let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - } - .min(I256::try_from(state.long_exposure())?); - // Make sure a short is possible. if state .effective_share_reserves()? .min(state.share_reserves()) - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { continue; } - match state - .solvency_after_short(state.minimum_transaction_amount(), checkpoint_exposure) - { + match state.solvency_after_short(state.minimum_transaction_amount()) { Ok(_) => (), Err(_) => continue, } // Get the max short. - let absolute_max_short = state.calculate_absolute_max_short( - checkpoint_exposure, - Some(bonds_tolerance), - Some(max_iterations), - )?; + let absolute_max_short = + state.calculate_absolute_max_short(Some(bonds_tolerance), Some(max_iterations))?; // The short should be valid. assert!(absolute_max_short >= state.minimum_transaction_amount()); - assert!(state - .solvency_after_short(absolute_max_short, checkpoint_exposure) - .is_ok()); + assert!(state.solvency_after_short(absolute_max_short).is_ok()); // Adding tolerance more bonds should be insolvent. assert!(state - .solvency_after_short(absolute_max_short + bonds_tolerance, checkpoint_exposure) + .solvency_after_short(absolute_max_short + bonds_tolerance) .is_err()); } Ok(()) @@ -1253,21 +1176,16 @@ mod tests { } = alice .get_checkpoint(state.to_checkpoint(alice.now().await?)) .await?; - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; - // Check that a short is possible. if state.effective_share_reserves()? - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { chain.revert(id).await?; alice.reset(Default::default()).await?; // Don't need to reset bob because he hasn't done anything. continue; } - let global_max_short_bonds = - state.calculate_absolute_max_short(checkpoint_exposure, None, None)?; + let global_max_short_bonds = state.calculate_absolute_max_short(None, None)?; // Bob should always be budget constrained when trying to open the short. let global_max_base_required = state @@ -1376,7 +1294,7 @@ mod tests { // TODO: We will remove this check in the future; this a failure case in rust that is not // checked in solidity. if state.effective_share_reserves()? - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { chain.revert(id).await?; alice.reset(Default::default()).await?; @@ -1387,11 +1305,7 @@ mod tests { // Solidity reports everything is good, so we run the Rust fns. let rust_max_bonds = panic::catch_unwind(|| { - state.calculate_absolute_max_short( - checkpoint_exposure, - None, - Some(max_iterations), - ) + state.calculate_absolute_max_short(None, Some(max_iterations)) }); // Compare the max bond amounts. @@ -1499,17 +1413,9 @@ mod tests { state.info.vault_share_price = vault_share_price.into(); // Get the current checkpoint exposure. - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; - // Solidity reports everything is good, so we run the Rust fns. let rust_max_bonds = panic::catch_unwind(|| { - state.calculate_absolute_max_short( - checkpoint_exposure, - None, - Some(max_iterations), - ) + state.calculate_absolute_max_short(None, Some(max_iterations)) }); assert!(rust_max_bonds.is_err() || rust_max_bonds.unwrap().is_err()); @@ -1539,20 +1445,10 @@ mod tests { } for _ in 0..*FUZZ_RUNS { let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - }; // Vary the baseline scale factor by adjusting the initial bond amount. let bond_amount = rng.gen_range(fixed!(1e10)..=fixed!(100_000_000e18)); // Compute f_x at the baseline bond_amount. - let f_x = match panic::catch_unwind(|| { - state.solvency_after_short(bond_amount, checkpoint_exposure) - }) { + let f_x = match panic::catch_unwind(|| state.solvency_after_short(bond_amount)) { Ok(result) => match result { Ok(result) => result, Err(_) => continue, // Start value is not solvent; skip the test. @@ -1562,9 +1458,7 @@ mod tests { // Compute f_x for each increment. let mut f_x_values = Vec::new(); for &inc in &increments { - match panic::catch_unwind(|| { - state.solvency_after_short(bond_amount + inc, checkpoint_exposure) - }) { + match panic::catch_unwind(|| state.solvency_after_short(bond_amount + inc)) { Ok(result) => match result { Ok(val) => f_x_values.push((inc, val)), Err(_) => { diff --git a/crates/hyperdrive-math/src/short/open.rs b/crates/hyperdrive-math/src/short/open.rs index e6c263e7..f47a1a86 100644 --- a/crates/hyperdrive-math/src/short/open.rs +++ b/crates/hyperdrive-math/src/short/open.rs @@ -492,8 +492,8 @@ impl State { mod tests { use std::panic; - use ethers::types::{U128, U256}; - use fixedpointmath::{fixed, fixed_u256, int256, FixedPointValue}; + use ethers::types::U256; + use fixedpointmath::{fixed, fixed_u256, int256}; use hyperdrive_test_utils::{ chain::TestChain, constants::{FAST_FUZZ_RUNS, FUZZ_RUNS, SLOW_FUZZ_RUNS}, @@ -534,13 +534,9 @@ mod tests { // Reset the variable rate to zero; get the state. alice.advance_time(fixed!(0), fixed!(0)).await?; let original_state = alice.get_state().await?; - // Get a random short amount. - let checkpoint_exposure = alice - .get_checkpoint_exposure(original_state.to_checkpoint(alice.now().await?)) - .await?; // Check that a short is possible. if original_state.effective_share_reserves()? - < original_state.calculate_min_share_reserves(checkpoint_exposure)? + < original_state.calculate_min_share_reserves_given_exposure()? { chain.revert(id).await?; alice.reset(Default::default()).await?; @@ -548,11 +544,10 @@ mod tests { celine.reset(Default::default()).await?; continue; } - + // Get a random short amount. let max_short_amount = original_state.calculate_max_short( U256::MAX, original_state.vault_share_price(), - checkpoint_exposure, None, None, )?; @@ -623,24 +618,15 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed_u256!(0)..=fixed!(10_000_000e18)); - if rng.gen() { - -I256::try_from(value).unwrap() - } else { - I256::try_from(value).unwrap() - } - }; // We need to catch panics because of overflows. - let max_bond_amount = match panic::catch_unwind(|| { - state.calculate_absolute_max_short(checkpoint_exposure, None, None) - }) { - Ok(max_bond_amount) => match max_bond_amount { - Ok(max_bond_amount) => max_bond_amount, - Err(_) => continue, // Max threw an Err. - }, - Err(_) => continue, // Max threw a panic. - }; + let max_bond_amount = + match panic::catch_unwind(|| state.calculate_absolute_max_short(None, None)) { + Ok(max_bond_amount) => match max_bond_amount { + Ok(max_bond_amount) => max_bond_amount, + Err(_) => continue, // Max threw an Err. + }, + Err(_) => continue, // Max threw a panic. + }; if max_bond_amount < state.minimum_transaction_amount() + fixed!(1) { continue; } @@ -875,11 +861,8 @@ mod tests { let mut state = alice.get_state().await?; // Check that a short is possible. - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; if state.effective_share_reserves()? - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { chain.revert(id).await?; alice.reset(Default::default()).await?; @@ -938,29 +921,20 @@ mod tests { // fails because we want to test the default behavior when the state // allows all actions. let state = rng.gen::(); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - }; // We need to catch panics because of overflows. - let max_bond_amount = match panic::catch_unwind(|| { - state.calculate_absolute_max_short(checkpoint_exposure, None, Some(10)) - }) { - Ok(max_bond_amount) => match max_bond_amount { - Ok(max_bond_amount) => { - if max_bond_amount <= state.minimum_transaction_amount() { - continue; + let max_bond_amount = + match panic::catch_unwind(|| state.calculate_absolute_max_short(None, Some(10))) { + Ok(max_bond_amount) => match max_bond_amount { + Ok(max_bond_amount) => { + if max_bond_amount <= state.minimum_transaction_amount() { + continue; + } + max_bond_amount } - max_bond_amount - } - Err(_) => continue, // Err; max short insolvent - }, - Err(_) => continue, // panic; likely in FixedPoint - }; + Err(_) => continue, // Err; max short insolvent + }, + Err(_) => continue, // panic; likely in FixedPoint + }; // Using the default behavior let bond_amount = rng.gen_range(state.minimum_transaction_amount()..=max_bond_amount); let price_with_default = state.calculate_spot_price_after_short(bond_amount, None)?; @@ -1016,11 +990,8 @@ mod tests { // Check that a short is possible. let state = alice.get_state().await?; - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; if state.effective_share_reserves()? - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { chain.revert(id).await?; alice.reset(Default::default()).await?; @@ -1112,15 +1083,11 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let checkpoint_exposure = rng - .gen_range(fixed!(0)..=FixedPoint::::MAX) - .raw() - .flip_sign_if(rng.gen()); let max_iterations = 7; let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price()); // We need to catch panics because of FixedPoint overflows & underflows. let max_trade = panic::catch_unwind(|| { - state.calculate_absolute_max_short(checkpoint_exposure, None, Some(max_iterations)) + state.calculate_absolute_max_short(None, Some(max_iterations)) }); // Since we're fuzzing it's possible that the max can fail. // We're only going to use it in this test if it succeeded. @@ -1186,11 +1153,8 @@ mod tests { let min_txn_amount = state.minimum_transaction_amount(); // Check that a short is possible. - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; if state.effective_share_reserves()? - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { chain.revert(id).await?; alice.reset(Default::default()).await?; @@ -1305,15 +1269,7 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); let open_vault_share_price = rng.gen_range(fixed!(1e5)..=state.vault_share_price()); - let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX))); - if rng.gen() { - -I256::try_from(value)? - } else { - I256::try_from(value)? - } - }; - match get_max_short(state.clone(), checkpoint_exposure, None) { + match get_max_short(state.clone(), None) { Ok(max_short_bonds) => { let bond_amount = rng.gen_range(state.minimum_transaction_amount()..=max_short_bonds); diff --git a/crates/hyperdrive-math/src/test_utils/agent.rs b/crates/hyperdrive-math/src/test_utils/agent.rs index 85b31eb2..c7eadcba 100644 --- a/crates/hyperdrive-math/src/test_utils/agent.rs +++ b/crates/hyperdrive-math/src/test_utils/agent.rs @@ -179,7 +179,6 @@ impl HyperdriveMathAgent for Agent, ChaCha8Rng> { vault_share_price: open_vault_share_price, .. } = self.get_checkpoint(latest_checkpoint).await?; - let checkpoint_exposure = self.get_checkpoint_exposure(latest_checkpoint).await?; let state = self.get_state().await?; // We linearly interpolate between the current spot price and the minimum @@ -204,7 +203,6 @@ impl HyperdriveMathAgent for Agent, ChaCha8Rng> { state.calculate_max_short( budget, open_vault_share_price, - checkpoint_exposure, Some(conservative_price), None, ) diff --git a/crates/hyperdrive-math/src/test_utils/integration_tests.rs b/crates/hyperdrive-math/src/test_utils/integration_tests.rs index d067194e..5118b208 100644 --- a/crates/hyperdrive-math/src/test_utils/integration_tests.rs +++ b/crates/hyperdrive-math/src/test_utils/integration_tests.rs @@ -51,16 +51,8 @@ mod tests { } = alice .get_checkpoint(state.to_checkpoint(alice.now().await?)) .await?; - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; - let global_max_short = state.calculate_max_short( - U256::MAX, - open_vault_share_price, - checkpoint_exposure, - None, - None, - )?; + let global_max_short = + state.calculate_max_short(U256::MAX, open_vault_share_price, None, None)?; let budget = bob.base(); let slippage_tolerance = fixed!(0.001e18); let max_short = bob.calculate_max_short(Some(slippage_tolerance)).await?; diff --git a/crates/hyperdrive-math/src/test_utils/preamble.rs b/crates/hyperdrive-math/src/test_utils/preamble.rs index b06b670a..f6d9635a 100644 --- a/crates/hyperdrive-math/src/test_utils/preamble.rs +++ b/crates/hyperdrive-math/src/test_utils/preamble.rs @@ -47,11 +47,8 @@ pub async fn initialize_pool_with_random_state( // Celine opens a short. state = alice.get_state().await?; - // Get the current checkpoint exposure. - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; - let max_short = get_max_short(state, checkpoint_exposure, None)?; + // Open a short. + let max_short = get_max_short(state, None)?; let short_amount = rng.gen_range(min_txn..=max_short); celine.fund(short_amount + fixed!(10e18)).await?; // Fund a little extra for slippage. celine.open_short(short_amount, None, None).await?; @@ -131,11 +128,7 @@ fn get_max_long(state: State, maybe_max_num_tries: Option) -> Result, -) -> Result> { +pub fn get_max_short(state: State, maybe_max_num_tries: Option) -> Result> { let max_num_tries = maybe_max_num_tries.unwrap_or(10); // We linearly interpolate between the current spot price and the minimum @@ -159,7 +152,6 @@ pub fn get_max_short( state.calculate_max_short( U256::from(U128::MAX), state.vault_share_price(), - checkpoint_exposure, Some(conservative_price), Some(5), ) @@ -263,29 +255,21 @@ mod tests { let seed = rng.gen(); ChaCha8Rng::seed_from_u64(seed) }; - // Initialize the test chain & agents. let chain = TestChain::new().await?; let mut alice = chain.alice().await?; let mut bob = chain.bob().await?; let mut celine = chain.celine().await?; - for _ in 0..*FUZZ_RUNS { // Snapshot the chain. let id = chain.snapshot().await?; - // Run the preamble. initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?; - - // Get state and trade details. + // Get state. let state = alice.get_state().await?; - let checkpoint_exposure = alice - .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) - .await?; - // Check that a short is possible. if state.effective_share_reserves()? - < state.calculate_min_share_reserves(checkpoint_exposure)? + < state.calculate_min_share_reserves_given_exposure()? { chain.revert(id).await?; alice.reset(Default::default()).await?; @@ -293,13 +277,11 @@ mod tests { celine.reset(Default::default()).await?; continue; } - // Open the max short. let max_short = bob.calculate_max_short(None).await?; assert!(max_short >= state.minimum_transaction_amount()); bob.fund(max_short + fixed!(10e18)).await?; bob.open_short(max_short, None, None).await?; - // Reset the chain & agents. chain.revert(id).await?; alice.reset(Default::default()).await?; diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index 28d39c5e..7bf78e68 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -227,13 +227,7 @@ mod tests { // Get the max rate. // We need to catch panics because of overflows. let max_short = match panic::catch_unwind(|| { - state.calculate_max_short( - U256::MAX, - open_vault_share_price, - checkpoint_exposure, - None, - None, - ) + state.calculate_max_short(U256::MAX, open_vault_share_price, None, None) }) { Ok(max_short) => match max_short { Ok(max_short) => max_short, From f73a7e9ed12efe1ed63e7733d3cd19c9eccfb6a9 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Dec 2024 12:01:00 -0800 Subject: [PATCH 3/6] docstring explaining why checkpoint exposure stays for longs --- crates/hyperdrive-math/src/long/max.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/hyperdrive-math/src/long/max.rs b/crates/hyperdrive-math/src/long/max.rs index ec4ca1c9..d37d84a4 100644 --- a/crates/hyperdrive-math/src/long/max.rs +++ b/crates/hyperdrive-math/src/long/max.rs @@ -392,6 +392,8 @@ impl State { } let share_reserves = (self.share_reserves() + share_amount) - governance_fee_shares; let exposure = self.long_exposure() + bond_amount; + // Netting allows us to remove any negative checkpoint exposure from the + // long exposure. let checkpoint_exposure = FixedPoint::try_from(-checkpoint_exposure.min(int256!(0)))?; if share_reserves + checkpoint_exposure / self.vault_share_price() >= exposure / self.vault_share_price() + self.minimum_share_reserves() From d2b434fa9f18baea4980bba7ff1cdebb5df17d11 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Dec 2024 12:03:30 -0800 Subject: [PATCH 4/6] bump version --- Cargo.lock | 14 +++++++------- Cargo.toml | 8 ++++---- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c8add9a..73821f9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -1103,7 +1103,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fixedpointmath" -version = "0.20.0" +version = "0.20.1" dependencies = [ "ethers", "eyre", @@ -1509,7 +1509,7 @@ dependencies = [ [[package]] name = "hyperdrive-math" -version = "0.20.0" +version = "0.20.1" dependencies = [ "ethers", "eyre", @@ -1525,7 +1525,7 @@ dependencies = [ [[package]] name = "hyperdrive-test-utils" -version = "0.20.0" +version = "0.20.1" dependencies = [ "async-trait", "dotenvy", @@ -1547,7 +1547,7 @@ dependencies = [ [[package]] name = "hyperdrive-wrappers" -version = "0.20.0" +version = "0.20.1" dependencies = [ "dotenv", "ethers", @@ -1565,7 +1565,7 @@ dependencies = [ [[package]] name = "hyperdrivepy" -version = "0.20.0" +version = "0.20.1" dependencies = [ "ethers", "eyre", @@ -3281,7 +3281,7 @@ dependencies = [ [[package]] name = "test-utils" -version = "0.20.0" +version = "0.20.1" dependencies = [ "async-trait", "ethers", diff --git a/Cargo.toml b/Cargo.toml index b2c728ab..cd4e3b1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ [workspace.package] name = "hyperdrive-rs" -version = "0.20.0" +version = "0.20.1" authors = [ "Alex Towle ", "Dylan Paiton ", @@ -29,9 +29,9 @@ repository = "https://github.com/delvtech/hyperdrive-rs" description = "API for simulating Hyperdrive smart contract transactions." [workspace.dependencies] -fixedpointmath = { version = "0.20.0", path="crates/fixedpointmath" } -hyperdrive-wrappers = { version = "0.20.0", path="crates/hyperdrive-wrappers" } -hyperdrive-math = { version = "0.20.0", path="crates/hyperdrive-math" } +fixedpointmath = { version = "0.20.1", path="crates/fixedpointmath" } +hyperdrive-wrappers = { version = "0.20.1", path="crates/hyperdrive-wrappers" } +hyperdrive-math = { version = "0.20.1", path="crates/hyperdrive-math" } [workspace.lints.clippy] comparison_chain = "allow" diff --git a/pyproject.toml b/pyproject.toml index ad86ae58..f27998c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hyperdrivepy" -version = "0.20.0" +version = "0.20.1" authors = [ { name = "Dylan Paiton", email = "dylan@delv.tech" }, { name = "Matthew Brown", email = "matt@delv.tech" }, diff --git a/setup.py b/setup.py index 20e91d87..1b697a85 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="hyperdrivepy", - version="0.20.0", + version="0.20.1", packages=["hyperdrivepy"], package_dir={"": "bindings/hyperdrivepy/python"}, rust_extensions=[ From ea954c9d14398e74a8cc0cfcf1a814abfa4a5f4b Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Dec 2024 12:50:53 -0800 Subject: [PATCH 5/6] fix tests --- crates/hyperdrive-math/src/short/open.rs | 2 +- crates/hyperdrive-math/src/utils.rs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/hyperdrive-math/src/short/open.rs b/crates/hyperdrive-math/src/short/open.rs index f47a1a86..5778669e 100644 --- a/crates/hyperdrive-math/src/short/open.rs +++ b/crates/hyperdrive-math/src/short/open.rs @@ -923,7 +923,7 @@ mod tests { let state = rng.gen::(); // We need to catch panics because of overflows. let max_bond_amount = - match panic::catch_unwind(|| state.calculate_absolute_max_short(None, Some(10))) { + match panic::catch_unwind(|| state.calculate_absolute_max_short(None, None)) { Ok(max_bond_amount) => match max_bond_amount { Ok(max_bond_amount) => { if max_bond_amount <= state.minimum_transaction_amount() { diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index 7bf78e68..11076829 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -206,6 +206,7 @@ mod tests { #[tokio::test] async fn fuzz_calculate_bonds_given_effective_shares_and_rate() -> Result<()> { + let test_tolerance = fixed!(1e8); let mut rng = thread_rng(); for _ in 0..*FUZZ_RUNS { // Gen the random state. @@ -218,8 +219,13 @@ mod tests { // Get the min rate. // We need to catch panics because of overflows. - let max_long = match state.calculate_max_long(U256::MAX, checkpoint_exposure, None) { - Ok(max_long) => max_long, + let max_long = match panic::catch_unwind(|| { + state.calculate_max_long(U256::MAX, checkpoint_exposure, None) + }) { + Ok(max_long) => match max_long { + Ok(max_long) => max_long, + Err(_) => continue, // Max threw an Err. Don't finish this fuzz iteration. + }, Err(_) => continue, // Max threw an Err. Don't finish this fuzz iteration. }; let min_rate = state.calculate_spot_rate_after_long(max_long, None)?; @@ -253,14 +259,13 @@ mod tests { let mut new_state: State = state.clone(); new_state.info.bond_reserves = bond_reserves.into(); let new_spot_rate = new_state.calculate_spot_rate()?; - let tolerance = fixed!(1e8); assert!( - target_rate.abs_diff(new_spot_rate) < tolerance, + target_rate.abs_diff(new_spot_rate) < test_tolerance, r#" target rate: {target_rate} new spot rate: {new_spot_rate} diff: {diff} - tolerance: {tolerance} + tolerance: {test_tolerance} "#, diff = target_rate.abs_diff(new_spot_rate), ); From 8c870334b0c1b66f2f67872e2562a27e3ac3de85 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Dec 2024 14:07:18 -0800 Subject: [PATCH 6/6] test and comment cleanup --- crates/hyperdrive-math/src/long/max.rs | 6 ++++++ crates/hyperdrive-math/src/short/max.rs | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/hyperdrive-math/src/long/max.rs b/crates/hyperdrive-math/src/long/max.rs index d37d84a4..812a5bf5 100644 --- a/crates/hyperdrive-math/src/long/max.rs +++ b/crates/hyperdrive-math/src/long/max.rs @@ -621,6 +621,12 @@ mod tests { // Bob opens a max long. let max_spot_price = bob.get_state().await?.calculate_max_spot_price()?; let max_long = bob.calculate_max_long(None).await?; + let state = alice.get_state().await?; + // Ensure max long is valid. + if state.calculate_open_long(max_long).is_err() { + continue; + } + // Get the andicipated spot price & open the log. let spot_price_after_long = bob .get_state() .await? diff --git a/crates/hyperdrive-math/src/short/max.rs b/crates/hyperdrive-math/src/short/max.rs index be3b4348..1b4318fb 100644 --- a/crates/hyperdrive-math/src/short/max.rs +++ b/crates/hyperdrive-math/src/short/max.rs @@ -753,8 +753,8 @@ impl State { /// \right)^{\frac{1}{1 - t_s}} /// - \frac{\phi_c \cdot (1 - p) \cdot (1 - \phi_g) \cdot \Delta y}{c} /// ``` - /// where the second term is P_{\text{lp}}(\Delta y) and the remaining terms - /// are from fees. Therefore, the derivative is + /// where the second term is `$P_{\text{lp}}(\Delta y)$` and the remaining + /// terms are from fees. Therefore, the derivative is /// /// ```math /// \frac{\partial \Delta z}{\partial \Delta y} = - P_{\text{lp}}'(\Delta y)