From 2561faef406bc28138fa3436d0e76231e927872c Mon Sep 17 00:00:00 2001 From: Dylan Paiton Date: Sun, 26 May 2024 17:26:37 -0700 Subject: [PATCH] Replace panic with Result (#113) # Resolved Issues https://github.com/delvtech/hyperdrive-rs/issues/99 https://github.com/delvtech/hyperdrive-rs/issues/64 https://github.com/delvtech/hyperdrive-rs/issues/65 https://github.com/delvtech/hyperdrive-rs/issues/66 # Description Our use of both panic and result results in a worse developer experience, and should be changed to using Result everywhere possible. This PR removes almost all `panic` uses in the core libs; where functions once would panic but return a type `T` they now return an error with the type`Result`. I also removed `unwrap()` statements, which convert `Result` to `panic`, in most places around the repo. Exceptions were made for core (anything with sugar like `+`, `-`, `*`) FixedPoint arithmetic (thus keeping any U256 panics), certain test-related cases, and macros. I also improved some error messaging (in bindings mostly, but also elsewhere), added comments, & added a lint ignore. I ran the fuzz testing with 10k `FUZZ_RUNS` and 50k `FAST_FUZZ_RUNS` twice without error locally. --- Cargo.toml | 3 + bindings/hyperdrivepy/src/hyperdrive_state.rs | 23 ++- .../src/hyperdrive_state_methods.rs | 28 +-- .../hyperdrive_state_methods/long/close.rs | 30 +++- .../src/hyperdrive_state_methods/long/max.rs | 18 +- .../src/hyperdrive_state_methods/long/open.rs | 61 ++++--- .../hyperdrive_state_methods/long/targeted.rs | 32 ++-- .../src/hyperdrive_state_methods/lp/add.rs | 48 +++--- .../src/hyperdrive_state_methods/lp/math.rs | 14 +- .../hyperdrive_state_methods/short/close.rs | 58 ++++--- .../src/hyperdrive_state_methods/short/max.rs | 51 +++--- .../hyperdrive_state_methods/short/open.rs | 56 ++++-- .../hyperdrive_state_methods/yield_space.rs | 60 +++++-- bindings/hyperdrivepy/src/hyperdrive_utils.rs | 115 ++++++++----- bindings/hyperdrivepy/src/utils.rs | 12 +- bindings/hyperdrivepy/tests/wrapper_test.py | 12 +- crates/fixed-point/src/lib.rs | 62 ++++--- crates/fixed-point/src/macros.rs | 2 +- crates/hyperdrive-math/src/lib.rs | 23 +-- crates/hyperdrive-math/src/long/close.rs | 51 +++--- crates/hyperdrive-math/src/long/fees.rs | 27 +-- crates/hyperdrive-math/src/long/max.rs | 144 ++++++++-------- crates/hyperdrive-math/src/long/open.rs | 14 +- crates/hyperdrive-math/src/long/targeted.rs | 52 +++--- crates/hyperdrive-math/src/lp/add.rs | 46 +++-- crates/hyperdrive-math/src/lp/math.rs | 72 ++++---- crates/hyperdrive-math/src/short/close.rs | 74 ++++---- crates/hyperdrive-math/src/short/fees.rs | 31 ++-- crates/hyperdrive-math/src/short/max.rs | 160 ++++++++--------- crates/hyperdrive-math/src/short/open.rs | 161 +++++++++--------- .../hyperdrive-math/src/test_utils/agent.rs | 19 +-- .../src/test_utils/integration_tests.rs | 4 +- crates/hyperdrive-math/src/utils.rs | 69 ++++---- crates/hyperdrive-math/src/yield_space.rs | 150 ++++++++-------- .../hyperdrive-test-utils/src/chain/deploy.rs | 27 +-- 35 files changed, 1006 insertions(+), 803 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b9575062..0f12bc80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,6 @@ authors = [ "Ryan Goree ", "Sheng Lundquist ", ] + +[workspace.lints.clippy] +comparison_chain = "allow" diff --git a/bindings/hyperdrivepy/src/hyperdrive_state.rs b/bindings/hyperdrivepy/src/hyperdrive_state.rs index 06122386..b512af75 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state.rs @@ -1,5 +1,5 @@ use hyperdrive_math::State; -use pyo3::prelude::*; +use pyo3::{exceptions::PyAssertionError, prelude::*}; use crate::{PyPoolConfig, PyPoolInfo}; @@ -13,22 +13,27 @@ impl HyperdriveState { HyperdriveState { state } } - pub(crate) fn new_from_pool(pool_config: &PyAny, pool_info: &PyAny) -> Self { + pub(crate) fn new_from_pool(pool_config: &PyAny, pool_info: &PyAny) -> PyResult { let rust_pool_config = match PyPoolConfig::extract(pool_config) { Ok(py_pool_config) => py_pool_config.pool_config, Err(err) => { - panic!("Error extracting pool config: {:?}", err); + return Err(PyErr::new::(format!( + "Error extracting pool config {:?}: {}", + pool_config, err + ))); } }; let rust_pool_info = match PyPoolInfo::extract(pool_info) { Ok(py_pool_info) => py_pool_info.pool_info, Err(err) => { - // Handle the error, e.g., printing an error message or panicking - panic!("Error extracting pool info: {:?}", err); + return Err(PyErr::new::(format!( + "Error extracting pool info {:?}: {}", + pool_info, err + ))); } }; let state = State::new(rust_pool_config, rust_pool_info); - HyperdriveState::new(state) + Ok(HyperdriveState::new(state)) } } @@ -38,8 +43,10 @@ impl From for HyperdriveState { } } -impl From<(&PyAny, &PyAny)> for HyperdriveState { - fn from(args: (&PyAny, &PyAny)) -> Self { +impl TryFrom<(&PyAny, &PyAny)> for HyperdriveState { + type Error = PyErr; + + fn try_from(args: (&PyAny, &PyAny)) -> PyResult { HyperdriveState::new_from_pool(args.0, args.1) } } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods.rs index 62457be9..1fc0efeb 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods.rs @@ -4,25 +4,25 @@ mod short; mod yield_space; use ethers::core::types::U256; -use hyperdrive_math::State; use pyo3::{exceptions::PyValueError, prelude::*}; +pub use crate::utils::*; use crate::HyperdriveState; -pub use crate::{utils::*, PyPoolConfig, PyPoolInfo}; #[pymethods] impl HyperdriveState { #[new] pub fn __init__(pool_config: &PyAny, pool_info: &PyAny) -> PyResult { - let rust_pool_config = PyPoolConfig::extract(pool_config)?.pool_config; - let rust_pool_info = PyPoolInfo::extract(pool_info)?.pool_info; - let state = State::new(rust_pool_config, rust_pool_info); - Ok(HyperdriveState::new(state)) + HyperdriveState::new_from_pool(pool_config, pool_info) } pub fn to_checkpoint(&self, time: &str) -> PyResult { - let time_int = U256::from_dec_str(time) - .map_err(|_| PyErr::new::("Failed to convert time string to U256"))?; + let time_int = U256::from_dec_str(time).map_err(|err| { + PyErr::new::(format!( + "Failed to convert time string {} to U256: {}", + time, err + )) + })?; let result_int = self.state.to_checkpoint(time_int); let result = result_int.to_string(); Ok(result) @@ -35,19 +35,25 @@ impl HyperdriveState { } pub fn calculate_spot_price(&self) -> PyResult { - let result_fp = self.state.calculate_spot_price(); + let result_fp = self.state.calculate_spot_price().map_err(|err| { + PyErr::new::(format!("calculate_spot_price: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } pub fn calculate_spot_rate(&self) -> PyResult { - let result_fp = self.state.calculate_spot_rate(); + let result_fp = self.state.calculate_spot_rate().map_err(|err| { + PyErr::new::(format!("calculate_spot_rate: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } pub fn calculate_max_spot_price(&self) -> PyResult { - let result_fp = self.state.calculate_max_spot_price(); + let result_fp = self.state.calculate_max_spot_price().map_err(|err| { + PyErr::new::(format!("calculate_max_spot_price: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/close.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/close.rs index cfa0c9b7..764816cd 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/close.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/close.rs @@ -12,19 +12,31 @@ impl HyperdriveState { maturity_time: &str, current_time: &str, ) -> PyResult { - let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|_| { - PyErr::new::("Failed to convert bond_amount string to U256") + let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert bond_amount string {} to U256: {}", + bond_amount, err + )) })?); - let maturity_time = U256::from_dec_str(maturity_time).map_err(|_| { - PyErr::new::("Failed to convert maturity_time string to U256") + let maturity_time = U256::from_dec_str(maturity_time).map_err(|err| { + PyErr::new::(format!( + "Failed to convert maturity_time string {} to U256: {}", + maturity_time, err + )) })?; - let current_time = U256::from_dec_str(current_time).map_err(|_| { - PyErr::new::("Failed to convert current_time string to U256") + let current_time = U256::from_dec_str(current_time).map_err(|err| { + PyErr::new::(format!( + "Failed to convert current_time string {} to U256: {}", + current_time, err + )) })?; - let result_fp = - self.state - .calculate_close_long(bond_amount_fp, maturity_time, current_time); + let result_fp = self + .state + .calculate_close_long(bond_amount_fp, maturity_time, current_time) + .map_err(|err| { + PyErr::new::(format!("calculate_close_long: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/max.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/max.rs index 5b523372..c733b9f0 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/max.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/max.rs @@ -12,18 +12,22 @@ impl HyperdriveState { checkpoint_exposure: &str, maybe_max_iterations: Option, ) -> PyResult { - let budget_fp = FixedPoint::from(U256::from_dec_str(budget).map_err(|_| { - PyErr::new::("Failed to convert budget string to U256") + let budget_fp = FixedPoint::from(U256::from_dec_str(budget).map_err(|err| { + PyErr::new::(format!( + "Failed to convert budget string {} to U256: {}", + budget, err + )) })?); - let checkpoint_exposure_i = I256::from_dec_str(checkpoint_exposure).map_err(|_| { - PyErr::new::("Failed to convert checkpoint_exposure string to I256") + 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 result_fp = self .state .calculate_max_long(budget_fp, checkpoint_exposure_i, maybe_max_iterations) - .map_err(|e| { - PyErr::new::(format!("Failed to calculate max long: {}", e)) - })?; + .map_err(|err| PyErr::new::(format!("calculate_max_long: {}", err)))?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/open.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/open.rs index 9b5d0549..ee792558 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/open.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/open.rs @@ -7,24 +7,35 @@ use crate::HyperdriveState; #[pymethods] impl HyperdriveState { pub fn calculate_open_long(&self, base_amount: &str) -> PyResult { - let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|_| { - PyErr::new::("Failed to convert base_amount string to U256") + let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert base_amount string {} to U256: {}", + base_amount, err + )) })?); - let result_fp = self.state.calculate_open_long(base_amount_fp).unwrap(); + let result_fp = self + .state + .calculate_open_long(base_amount_fp) + .map_err(|err| { + PyErr::new::(format!("calculate_open_long: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } pub fn calculate_pool_deltas_after_open_long(&self, base_amount: &str) -> PyResult { - let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|_| { - PyErr::new::("Failed to convert base_amount string to U256") + let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert base_amount string {} to U256: {}", + base_amount, err + )) })?); let result_fp = self .state .calculate_pool_deltas_after_open_long(base_amount_fp) .map_err(|err| { PyErr::new::(format!( - "calculate_pool_deltas_after_open_long returned the error: {:?}", + "calculate_pool_deltas_after_open_long: {:?}", err )) })?; @@ -37,15 +48,19 @@ impl HyperdriveState { base_amount: &str, maybe_bond_amount: Option<&str>, ) -> PyResult { - let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|_| { - PyErr::new::("Failed to convert base_amount string to U256") + let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert base_amount string {} to U256: {}", + base_amount, err + )) })?); let maybe_bond_amount_fp = if let Some(bond_amount) = maybe_bond_amount { Some(FixedPoint::from(U256::from_dec_str(bond_amount).map_err( - |_| { - PyErr::new::( - "Failed to convert maybe_bond_amount string to U256", - ) + |err| { + PyErr::new::(format!( + "Failed to convert maybe_bond_amount string {} to U256: {}", + bond_amount, err + )) }, )?)) } else { @@ -54,7 +69,9 @@ impl HyperdriveState { let result_fp = self .state .calculate_spot_price_after_long(base_amount_fp, maybe_bond_amount_fp) - .unwrap(); + .map_err(|err| { + PyErr::new::(format!("calculate_spot_price_after_long: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } @@ -64,15 +81,19 @@ impl HyperdriveState { base_amount: &str, maybe_bond_amount: Option<&str>, ) -> PyResult { - let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|_| { - PyErr::new::("Failed to convert base_amount string to U256") + let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert base_amount string {} to U256: {}", + base_amount, err + )) })?); let maybe_bond_amount_fp = if let Some(bond_amount) = maybe_bond_amount { Some(FixedPoint::from(U256::from_dec_str(bond_amount).map_err( - |_| { - PyErr::new::( - "Failed to convert maybe_bond_amount string to U256", - ) + |err| { + PyErr::new::(format!( + "Failed to convert maybe_bond_amount string {} to U256: {}", + bond_amount, err + )) }, )?)) } else { @@ -81,7 +102,7 @@ impl HyperdriveState { let result_fp = self .state .calculate_spot_rate_after_long(base_amount_fp, maybe_bond_amount_fp) - .unwrap(); + .map_err(|err| PyErr::new::(format!("{}", err)))?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/targeted.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/targeted.rs index 556ba1e5..95c07bdd 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/targeted.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/long/targeted.rs @@ -14,21 +14,31 @@ impl HyperdriveState { maybe_max_iterations: Option, maybe_allowable_error: Option<&str>, ) -> PyResult { - let budget_fp = FixedPoint::from(U256::from_dec_str(budget).map_err(|_| { - PyErr::new::("Failed to convert budget string to U256") + let budget_fp = FixedPoint::from(U256::from_dec_str(budget).map_err(|err| { + PyErr::new::(format!( + "Failed to convert budget string {} to U256: {}", + budget, err + )) })?); - let target_rate_fp = FixedPoint::from(U256::from_dec_str(target_rate).map_err(|_| { - PyErr::new::("Failed to convert target_rate string to U256") + let target_rate_fp = FixedPoint::from(U256::from_dec_str(target_rate).map_err(|err| { + PyErr::new::(format!( + "Failed to convert target_rate string {} to U256: {}", + target_rate, err + )) })?); - let checkpoint_exposure_i = I256::from_dec_str(checkpoint_exposure).map_err(|_| { - PyErr::new::("Failed to convert checkpoint_exposure string to I256") + 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_allowable_error_fp = if let Some(allowable_error) = maybe_allowable_error { Some(FixedPoint::from( - U256::from_dec_str(allowable_error).map_err(|_| { - PyErr::new::( - "Failed to convert maybe_allowable_error string to U256", - ) + U256::from_dec_str(allowable_error).map_err(|err| { + PyErr::new::(format!( + "Failed to convert maybe_allowable_error string {} to U256: {}", + allowable_error, err + )) })?, )) } else { @@ -45,7 +55,7 @@ impl HyperdriveState { ) .map_err(|err| { PyErr::new::(format!( - "Calculate_targeted_long_with_budget returned the error: {:?}", + "calculate_targeted_long_with_budget: {:?}", err )) })?; diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/add.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/add.rs index df38d141..0d77692f 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/add.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/add.rs @@ -18,40 +18,40 @@ impl HyperdriveState { let current_block_timestamp = U256::from_dec_str(current_block_timestamp).map_err(|err| { PyErr::new::(format!( - "Failed to convert current_block_timestamp string to U256: {}", - err + "Failed to convert current_block_timestamp string {} to U256: {}", + current_block_timestamp, err )) })?; let contribution_fp = FixedPoint::from(U256::from_dec_str(contribution).map_err(|err| { PyErr::new::(format!( - "Failed to convert contribution string to U256: {}", - err + "Failed to convert contribution string {} to U256: {}", + contribution, err )) })?); let min_lp_share_price_fp = FixedPoint::from(U256::from_dec_str(min_lp_share_price).map_err(|err| { PyErr::new::(format!( - "Failed to convert min_lp_share_price string to U256: {}", - err + "Failed to convert min_lp_share_price string {} to U256: {}", + min_lp_share_price, err )) })?); let min_apr_fp = FixedPoint::from(U256::from_dec_str(min_apr).map_err(|err| { PyErr::new::(format!( - "Failed to convert min_apr string to U256: {}", - err + "Failed to convert min_apr string {} to U256: {}", + min_apr, err )) })?); let max_apr_fp = FixedPoint::from(U256::from_dec_str(max_apr).map_err(|err| { PyErr::new::(format!( - "Failed to convert max_apr string to U256: {}", - err + "Failed to convert max_apr string {} to U256: {}", + max_apr, err )) })?); let as_base = as_base.parse::().map_err(|err| { PyErr::new::(format!( - "Failed to convert as_base string to bool: {}", - err + "Failed to convert as_base string {} to bool: {}", + as_base, err )) })?; let result_fp = self @@ -65,10 +65,7 @@ impl HyperdriveState { as_base, ) .map_err(|err| { - PyErr::new::(format!( - "Failed to run calculate_add_liquidity: {}", - err - )) + PyErr::new::(format!("calculate_add_liquidity: {}", err)) })?; let result = U256::from(result_fp).to_string(); Ok(result) @@ -79,18 +76,25 @@ impl HyperdriveState { contribution: &str, as_base: &str, ) -> PyResult<(String, String, String)> { - let contribution_fp = FixedPoint::from(U256::from_dec_str(contribution).map_err(|_| { - PyErr::new::("Failed to convert contribution string to U256") - })?); - let as_base = as_base.parse::().map_err(|_| { - PyErr::new::("Failed to convert as_base string to bool") + let contribution_fp = + FixedPoint::from(U256::from_dec_str(contribution).map_err(|err| { + PyErr::new::(format!( + "Failed to convert contribution string {} to U256: {}", + contribution, err + )) + })?); + let as_base = as_base.parse::().map_err(|err| { + PyErr::new::(format!( + "Failed to convert as_base string {} to bool: {}", + as_base, err + )) })?; let (result_fp1, result_i256, result_fp2) = self .state .calculate_pool_deltas_after_add_liquidity(contribution_fp, as_base) .map_err(|err| { PyErr::new::(format!( - "calculate_pool_deltas_after_add_liquidity returned the error: {:?}", + "calculate_pool_deltas_after_add_liquidity: {:?}", err )) })?; diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/math.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/math.rs index 7ad16f20..57410e7c 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/math.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/lp/math.rs @@ -7,17 +7,21 @@ use crate::HyperdriveState; impl HyperdriveState { pub fn calculate_present_value(&self, current_block_timestamp: &str) -> PyResult { let current_block_timestamp_int = - U256::from_dec_str(current_block_timestamp).map_err(|_| { - PyErr::new::( - "Failed to convert current_block_timestamp string to U256", - ) + U256::from_dec_str(current_block_timestamp).map_err(|err| { + PyErr::new::(format!( + "Failed to convert current_block_timestamp string {} to U256: {}", + current_block_timestamp, err + )) })?; match self .state .calculate_present_value(current_block_timestamp_int) { Ok(result) => Ok(U256::from(result).to_string()), - Err(err) => Err(PyErr::new::(format!("{:?}", err))), + Err(err) => Err(PyErr::new::(format!( + "calculate_present_value: {:?}", + err + ))), } } } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/close.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/close.rs index 2a4873e9..100a7d47 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/close.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/close.rs @@ -14,34 +14,50 @@ impl HyperdriveState { maturity_time: &str, current_time: &str, ) -> PyResult { - let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|_| { - PyErr::new::("Failed to convert bond_amount string to U256") + let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert bond_amount string {} to U256: {}", + bond_amount, err + )) })?); let open_vault_share_price_fp = - FixedPoint::from(U256::from_dec_str(open_vault_share_price).map_err(|_| { - PyErr::new::( - "Failed to convert open_vault_share_price string to U256", - ) + FixedPoint::from(U256::from_dec_str(open_vault_share_price).map_err(|err| { + PyErr::new::(format!( + "Failed to convert open_vault_share_price string {} to U256: {}", + open_vault_share_price, err + )) })?); let close_vault_share_price_fp = - FixedPoint::from(U256::from_dec_str(close_vault_share_price).map_err(|_| { - PyErr::new::( - "Failed to convert close_vault_share_price string to U256", - ) + FixedPoint::from(U256::from_dec_str(close_vault_share_price).map_err(|err| { + PyErr::new::(format!( + "Failed to convert close_vault_share_price string {} to U256: {}", + close_vault_share_price, err + )) })?); - let maturity_time = U256::from_dec_str(maturity_time).map_err(|_| { - PyErr::new::("Failed to convert maturity_time string to U256") + let maturity_time = U256::from_dec_str(maturity_time).map_err(|err| { + PyErr::new::(format!( + "Failed to convert maturity_time string {} to U256: {}", + maturity_time, err + )) })?; - let current_time = U256::from_dec_str(current_time).map_err(|_| { - PyErr::new::("Failed to convert current_time string to U256") + let current_time = U256::from_dec_str(current_time).map_err(|err| { + PyErr::new::(format!( + "Failed to convert current_time string {} to U256: {}", + current_time, err + )) })?; - let result_fp = self.state.calculate_close_short( - bond_amount_fp, - open_vault_share_price_fp, - close_vault_share_price_fp, - maturity_time, - current_time, - ); + let result_fp = self + .state + .calculate_close_short( + bond_amount_fp, + open_vault_share_price_fp, + close_vault_share_price_fp, + maturity_time, + current_time, + ) + .map_err(|err| { + PyErr::new::(format!("calculate_close_short: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs index 654bb3ba..829672ad 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/max.rs @@ -14,37 +14,50 @@ impl HyperdriveState { maybe_conservative_price: Option<&str>, maybe_max_iterations: Option, ) -> PyResult { - let budget_fp = FixedPoint::from(U256::from_dec_str(budget).map_err(|_| { - PyErr::new::("Failed to convert budget string to U256") + let budget_fp = FixedPoint::from(U256::from_dec_str(budget).map_err(|err| { + PyErr::new::(format!( + "Failed to convert budget string {} to U256: {}", + budget, err + )) })?); let open_vault_share_price_fp = - FixedPoint::from(U256::from_dec_str(open_vault_share_price).map_err(|_| { - PyErr::new::( - "Failed to convert open_vault_share_price string to U256", - ) + FixedPoint::from(U256::from_dec_str(open_vault_share_price).map_err(|err| { + PyErr::new::(format!( + "Failed to convert open_vault_share_price string {} to U256: {}", + open_vault_share_price, err + )) })?); - let checkpoint_exposure_i = I256::from_dec_str(checkpoint_exposure).map_err(|_| { - PyErr::new::("Failed to convert checkpoint_exposure string to I256") + 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( - U256::from_dec_str(conservative_price).map_err(|_| { - PyErr::new::( - "Failed to convert maybe_conservative_price string to U256", - ) + U256::from_dec_str(conservative_price).map_err(|err| { + PyErr::new::(format!( + "Failed to convert maybe_conservative_price string {} to U256: {}", + conservative_price, err + )) })?, )) } else { None }; - let result_fp = self.state.calculate_max_short( - budget_fp, - open_vault_share_price_fp, - checkpoint_exposure_i, - maybe_conservative_price_fp, - maybe_max_iterations, - ); + let result_fp = self + .state + .calculate_max_short( + budget_fp, + open_vault_share_price_fp, + checkpoint_exposure_i, + maybe_conservative_price_fp, + maybe_max_iterations, + ) + .map_err(|err| { + PyErr::new::(format!("calculate_max_short: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/open.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/open.rs index 8df5b34b..6b0c1422 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/open.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/short/open.rs @@ -11,31 +11,47 @@ impl HyperdriveState { bond_amount: &str, open_vault_share_price: &str, ) -> PyResult { - let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|_| { - PyErr::new::("Failed to convert bond_amount string to U256") + let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert bond_amount string {} to U256: {}", + bond_amount, err + )) })?); let open_vault_share_price_fp = - FixedPoint::from(U256::from_dec_str(open_vault_share_price).map_err(|_| { - PyErr::new::( - "Failed to convert open_vault_share_price string to U256", - ) + FixedPoint::from(U256::from_dec_str(open_vault_share_price).map_err(|err| { + PyErr::new::(format!( + "Failed to convert open_vault_share_price string {} to U256: {}", + open_vault_share_price, err + )) })?); let result_fp = self .state .calculate_open_short(bond_amount_fp, open_vault_share_price_fp) - .unwrap(); + .map_err(|err| { + PyErr::new::(format!("calculate_open_short: {}", err)) + })?; + let result = U256::from(result_fp).to_string(); Ok(result) } pub fn calculate_pool_deltas_after_open_short(&self, bond_amount: &str) -> PyResult { - let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|_| { - PyErr::new::("Failed to convert bond_amount string to U256") + let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert bond_amount string {} to U256: {}", + bond_amount, err + )) })?); let result_fp = self .state .calculate_pool_deltas_after_open_short(bond_amount_fp) - .unwrap(); + .map_err(|err| { + PyErr::new::(format!( + "calculate_pool_deltas_after_open_short: {}", + err + )) + })?; + let result = U256::from(result_fp).to_string(); Ok(result) } @@ -45,15 +61,19 @@ impl HyperdriveState { bond_amount: &str, maybe_base_amount: Option<&str>, ) -> PyResult { - let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|_| { - PyErr::new::("Failed to convert bond_amount string to U256") + let bond_amount_fp = FixedPoint::from(U256::from_dec_str(bond_amount).map_err(|err| { + PyErr::new::(format!( + "Failed to convert bond_amount string {} to U256: {}", + bond_amount, err + )) })?); let maybe_base_amount_fp = if let Some(base_amount) = maybe_base_amount { Some(FixedPoint::from(U256::from_dec_str(base_amount).map_err( - |_| { - PyErr::new::( - "Failed to convert maybe_base_amount string to U256", - ) + |err| { + PyErr::new::(format!( + "Failed to convert base_amount string {} to U256: {}", + base_amount, err + )) }, )?)) } else { @@ -62,8 +82,8 @@ impl HyperdriveState { let result_fp = self .state .calculate_spot_price_after_short(bond_amount_fp, maybe_base_amount_fp) - .map_err(|_| { - PyErr::new::("Failed to calculate spot price after short.") + .map_err(|err| { + PyErr::new::(format!("calculate_spot_price_after_short: {}", err)) })?; Ok(U256::from(result_fp).to_string()) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_state_methods/yield_space.rs b/bindings/hyperdrivepy/src/hyperdrive_state_methods/yield_space.rs index 6cf5ef25..a362f8a5 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_state_methods/yield_space.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_state_methods/yield_space.rs @@ -8,47 +8,81 @@ use crate::HyperdriveState; #[pymethods] impl HyperdriveState { pub fn calculate_bonds_out_given_shares_in_down(&self, amount_in: &str) -> PyResult { - let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|_| { - PyErr::new::("Failed to convert amount_in string to U256") + let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|err| { + PyErr::new::(format!( + "Failed to convert amount_in string {} to U256: {}", + amount_in, err + )) })?); let result_fp = self .state - .calculate_bonds_out_given_shares_in_down(amount_in_fp); + .calculate_bonds_out_given_shares_in_down(amount_in_fp) + .map_err(|err| { + PyErr::new::(format!( + "calculate_bonds_out_given_shares_in_down: {}", + err + )) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } pub fn calculate_shares_in_given_bonds_out_up(&self, amount_in: &str) -> PyResult { - let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|_| { - PyErr::new::("Failed to convert amount_in string to U256") + let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|err| { + PyErr::new::(format!( + "Failed to convert amount_in string {} to U256: {}", + amount_in, err + )) })?); - // We unwrap the error here to throw panic error if this fails let result_fp = self .state .calculate_shares_in_given_bonds_out_up_safe(amount_in_fp) - .unwrap(); + .map_err(|err| { + PyErr::new::(format!( + "Failed to execute calculate_shares_in_given_bonds_out_up_safe: {}", + err + )) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } pub fn calculate_shares_in_given_bonds_out_down(&self, amount_in: &str) -> PyResult { - let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|_| { - PyErr::new::("Failed to convert amount_in string to U256") + let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|err| { + PyErr::new::(format!( + "Failed to convert amount_in string {} to U256: {}", + amount_in, err + )) })?); let result_fp = self .state - .calculate_shares_in_given_bonds_out_down(amount_in_fp); + .calculate_shares_in_given_bonds_out_down(amount_in_fp) + .map_err(|err| { + PyErr::new::(format!( + "calculate_shares_in_given_bonds_out_down: {}", + err + )) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } pub fn calculate_shares_out_given_bonds_in_down(&self, amount_in: &str) -> PyResult { - let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|_| { - PyErr::new::("Failed to convert amount_in string to U256") + let amount_in_fp = FixedPoint::from(U256::from_dec_str(amount_in).map_err(|err| { + PyErr::new::(format!( + "Failed to convert amount_in string {} to U256: {}", + amount_in, err + )) })?); let result_fp = self .state - .calculate_shares_out_given_bonds_in_down(amount_in_fp); + .calculate_shares_out_given_bonds_in_down(amount_in_fp) + .map_err(|err| { + PyErr::new::(format!( + "calculate_shares_out_given_bonds_in_down: {}", + err + )) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/hyperdrive_utils.rs b/bindings/hyperdrivepy/src/hyperdrive_utils.rs index eaab2c6e..c575d3c4 100644 --- a/bindings/hyperdrivepy/src/hyperdrive_utils.rs +++ b/bindings/hyperdrivepy/src/hyperdrive_utils.rs @@ -17,26 +17,38 @@ pub fn calculate_bonds_given_effective_shares_and_rate( time_stretch: &str, ) -> PyResult { let effective_share_reserves_fp = - FixedPoint::from(U256::from_dec_str(effective_share_reserves).map_err(|_| { - PyErr::new::( - "Failed to convert effective_share_reserves string to U256", - ) + FixedPoint::from(U256::from_dec_str(effective_share_reserves).map_err(|err| { + PyErr::new::(format!( + "Failed to convert effective_share_reserves string {} to U256: {}", + effective_share_reserves, err + )) })?); - let target_rate_fp = FixedPoint::from(U256::from_dec_str(target_rate).map_err(|_| { - PyErr::new::("Failed to convert target_rate string to U256") + let target_rate_fp = FixedPoint::from(U256::from_dec_str(target_rate).map_err(|err| { + PyErr::new::(format!( + "Failed to convert target_rate string {} to U256: {}", + target_rate, err + )) })?); - let initial_vault_share_price_fp = - FixedPoint::from(U256::from_dec_str(initial_vault_share_price).map_err(|_| { - PyErr::new::( - "Failed to convert initial_vault_share_price string to U256", - ) - })?); + let initial_vault_share_price_fp = FixedPoint::from( + U256::from_dec_str(initial_vault_share_price).map_err(|err| { + PyErr::new::(format!( + "Failed to convert initial_vault_share_price string {} to U256: {}", + initial_vault_share_price, err + )) + })?, + ); let position_duration_fp = - FixedPoint::from(U256::from_dec_str(position_duration).map_err(|_| { - PyErr::new::("Failed to convert position_duration string to U256") + FixedPoint::from(U256::from_dec_str(position_duration).map_err(|err| { + PyErr::new::(format!( + "Failed to convert position_duration string {} to U256: {}", + position_duration, err + )) })?); - let time_stretch_fp = FixedPoint::from(U256::from_dec_str(time_stretch).map_err(|_| { - PyErr::new::("Failed to convert time_stretch string to U256") + let time_stretch_fp = FixedPoint::from(U256::from_dec_str(time_stretch).map_err(|err| { + PyErr::new::(format!( + "Failed to convert time_stretch string {} to U256: {}", + time_stretch, err + )) })?); let result_fp = rs_calculate_bonds_given_effective_shares_and_rate( effective_share_reserves_fp, @@ -44,7 +56,13 @@ pub fn calculate_bonds_given_effective_shares_and_rate( initial_vault_share_price_fp, position_duration_fp, time_stretch_fp, - ); + ) + .map_err(|err| { + PyErr::new::(format!( + "calculate_bonds_given_effective_shares_and_rate: {}", + err + )) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } @@ -54,26 +72,41 @@ pub fn calculate_effective_share_reserves( share_reserves: &str, share_adjustment: &str, ) -> PyResult { - let share_reserves_fp = FixedPoint::from(U256::from_dec_str(share_reserves).map_err(|_| { - PyErr::new::("Failed to convert share_reserves string to U256") - })?); - let share_adjustment_i = I256::from_dec_str(share_adjustment).map_err(|_| { - PyErr::new::("Failed to convert share_adjustment string to I256") + let share_reserves_fp = + FixedPoint::from(U256::from_dec_str(share_reserves).map_err(|err| { + PyErr::new::(format!( + "Failed to convert share_reserves string {} to U256: {}", + share_reserves, err + )) + })?); + let share_adjustment_i = I256::from_dec_str(share_adjustment).map_err(|err| { + PyErr::new::(format!( + "Failed to convert share_adjustment string {} to I256: {}", + share_adjustment, err + )) })?; - let result_fp = rs_calculate_effective_share_reserves(share_reserves_fp, share_adjustment_i); + let result_fp = rs_calculate_effective_share_reserves(share_reserves_fp, share_adjustment_i) + .map_err(|err| { + PyErr::new::(format!("calculate_effective_share_reserves: {}", err)) + })?; let result = U256::from(result_fp).to_string(); Ok(result) } #[pyfunction] pub fn calculate_rate_given_fixed_price(price: &str, position_duration: &str) -> PyResult { - let price_fp = - FixedPoint::from(U256::from_dec_str(price).map_err(|_| { - PyErr::new::("Failed to convert price string to U256") - })?); + let price_fp = FixedPoint::from(U256::from_dec_str(price).map_err(|err| { + PyErr::new::(format!( + "Failed to convert price string {} to U256: {}", + price, err + )) + })?); let position_duration_fp = - FixedPoint::from(U256::from_dec_str(position_duration).map_err(|_| { - PyErr::new::("Failed to convert position_duration string to U256") + FixedPoint::from(U256::from_dec_str(position_duration).map_err(|err| { + PyErr::new::(format!( + "Failed to convert position_duration string {} to U256: {}", + position_duration, err + )) })?); let result_fp = rs_calculate_rate_given_fixed_price(price_fp, position_duration_fp); let result = U256::from(result_fp).to_string(); @@ -82,15 +115,21 @@ pub fn calculate_rate_given_fixed_price(price: &str, position_duration: &str) -> #[pyfunction] pub fn calculate_time_stretch(rate: &str, position_duration: &str) -> PyResult { - let rate_fp = FixedPoint::from( - U256::from_dec_str(rate) - .map_err(|_| PyErr::new::("Failed to convert rate string to U256"))?, - ); - let position_duration_fp = FixedPoint::from( - U256::from_dec_str(position_duration) - .map_err(|_| PyErr::new::("Failed to convert rate string to U256"))?, - ); - let result_fp = rs_calculate_time_stretch(rate_fp, position_duration_fp); + let rate_fp = FixedPoint::from(U256::from_dec_str(rate).map_err(|err| { + PyErr::new::(format!( + "Failed to convert rate string {} to U256: {}", + rate, err + )) + })?); + let position_duration_fp = + FixedPoint::from(U256::from_dec_str(position_duration).map_err(|err| { + PyErr::new::(format!( + "Failed to convert position_duration string {} to U256: {}", + position_duration, err + )) + })?); + let result_fp = rs_calculate_time_stretch(rate_fp, position_duration_fp) + .map_err(|err| PyErr::new::(format!("calculate_time_stretch: {}", err)))?; let result = U256::from(result_fp).to_string(); Ok(result) } diff --git a/bindings/hyperdrivepy/src/utils.rs b/bindings/hyperdrivepy/src/utils.rs index c04be95b..a2e7b3b4 100644 --- a/bindings/hyperdrivepy/src/utils.rs +++ b/bindings/hyperdrivepy/src/utils.rs @@ -6,29 +6,29 @@ use pyo3::{exceptions::PyValueError, prelude::*}; pub fn extract_u256_from_attr(ob: &PyAny, attr: &str) -> PyResult { let value_str: String = ob.getattr(attr)?.extract()?; U256::from_dec_str(&value_str) - .map_err(|e| PyErr::new::(format!("Invalid U256 for {}: {}", attr, e))) + .map_err(|err| PyErr::new::(format!("Invalid U256 for {}: {}", attr, err))) } // Helper function to extract I256 values from Python object attributes pub fn extract_i256_from_attr(ob: &PyAny, attr: &str) -> PyResult { let value_str: String = ob.getattr(attr)?.extract()?; I256::from_dec_str(&value_str) - .map_err(|e| PyErr::new::(format!("Invalid I256 for {}: {}", attr, e))) + .map_err(|err| PyErr::new::(format!("Invalid I256 for {}: {}", attr, err))) } // Helper function to extract Ethereum Address values from Python object attributes pub fn extract_address_from_attr(ob: &PyAny, attr: &str) -> PyResult
{ let address_str: String = ob.getattr(attr)?.extract()?; - address_str.parse::
().map_err(|e| { - PyErr::new::(format!("Invalid Ethereum address for {}: {}", attr, e)) + address_str.parse::
().map_err(|err| { + PyErr::new::(format!("Invalid Ethereum address for {}: {}", attr, err)) }) } // Helper function to extract bytes32 values from Python object attributes pub fn extract_bytes32_from_attr(ob: &PyAny, attr: &str) -> PyResult<[u8; 32]> { let bytes32_str: String = ob.getattr(attr)?.extract()?; - let bytes32_h256: H256 = bytes32_str.parse::().map_err(|e| { - PyErr::new::(format!("Invalid bytes32 for {}: {}", attr, e)) + let bytes32_h256: H256 = bytes32_str.parse::().map_err(|err| { + PyErr::new::(format!("Invalid bytes32 for {}: {}", attr, err)) })?; Ok(bytes32_h256.into()) } diff --git a/bindings/hyperdrivepy/tests/wrapper_test.py b/bindings/hyperdrivepy/tests/wrapper_test.py index 03d5b9ef..705195f9 100644 --- a/bindings/hyperdrivepy/tests/wrapper_test.py +++ b/bindings/hyperdrivepy/tests/wrapper_test.py @@ -281,15 +281,15 @@ def test_max_long_fail_conversion(): # bad string inputs budget = "asdf" checkpoint_exposure = "100" - with pytest.raises(ValueError, match="Failed to convert budget string to U256"): + with pytest.raises(ValueError): hyperdrivepy.calculate_max_long(POOL_CONFIG, POOL_INFO, budget, checkpoint_exposure, max_iterations) budget = "1.23" checkpoint_exposure = "100" - with pytest.raises(ValueError, match="Failed to convert budget string to U256"): + with pytest.raises(ValueError): hyperdrivepy.calculate_max_long(POOL_CONFIG, POOL_INFO, budget, checkpoint_exposure, max_iterations) budget = "1000000000000000000" # 1 base checkpoint_exposure = "asdf" - with pytest.raises(ValueError, match="Failed to convert checkpoint_exposure string to I256"): + with pytest.raises(ValueError): hyperdrivepy.calculate_max_long(POOL_CONFIG, POOL_INFO, budget, checkpoint_exposure, max_iterations) @@ -321,7 +321,7 @@ def test_max_short_fail_conversion(): max_iterations = 20 # bad string inputs budget = "asdf" - with pytest.raises(ValueError, match="Failed to convert budget string to U256"): + with pytest.raises(ValueError): hyperdrivepy.calculate_max_short( POOL_CONFIG, POOL_INFO, @@ -332,7 +332,7 @@ def test_max_short_fail_conversion(): max_iterations, ) budget = "1.23" - with pytest.raises(ValueError, match="Failed to convert budget string to U256"): + with pytest.raises(ValueError): hyperdrivepy.calculate_max_short( POOL_CONFIG, POOL_INFO, @@ -344,7 +344,7 @@ def test_max_short_fail_conversion(): ) budget = "10000000000000000000000" # 10k base open_vault_share_price = "asdf" - with pytest.raises(ValueError, match="Failed to convert open_vault_share_price string to U256"): + with pytest.raises(ValueError): hyperdrivepy.calculate_max_short( POOL_CONFIG, POOL_INFO, diff --git a/crates/fixed-point/src/lib.rs b/crates/fixed-point/src/lib.rs index ef784268..bab1a607 100644 --- a/crates/fixed-point/src/lib.rs +++ b/crates/fixed-point/src/lib.rs @@ -4,7 +4,7 @@ use std::{ }; use ethers::types::{Sign, I256, U256}; -use eyre::{eyre, Error, Result}; +use eyre::{eyre, ErrReport, Error, Result}; use rand::{ distributions::{ uniform::{SampleBorrow, SampleUniform, UniformSampler}, @@ -44,10 +44,17 @@ impl fmt::Display for FixedPoint { /// Conversions /// -impl From for FixedPoint { - fn from(i: I256) -> FixedPoint { - assert!(i >= int256!(0), "FixedPoint cannot be negative"); - i.into_raw().into() +impl TryFrom for FixedPoint { + type Error = ErrReport; + + fn try_from(i: I256) -> Result { + if i < int256!(0) { + return Err(eyre!( + "failed to convert {} into FixedPoint; intput must be positive", + i + )); + } + Ok(i.into_raw().into()) } } @@ -86,7 +93,7 @@ impl TryFrom for I256 { fn try_from(f: FixedPoint) -> Result { I256::checked_from_sign_and_abs(Sign::Positive, f.0) - .ok_or(eyre!("fixed-point: failed to convert {} to I256", f)) + .ok_or(eyre!("failed to convert {} to I256", f)) } } @@ -179,43 +186,43 @@ impl FixedPoint { self.mul_div_up(fixed!(1e18), other) } - pub fn pow(self, y: FixedPoint) -> FixedPoint { + pub fn pow(self, y: FixedPoint) -> Result { // If the exponent is 0, return 1. if y == fixed!(0) { - return fixed!(1e18); + return Ok(fixed!(1e18)); } // If the base is 0, return 0. if self == fixed!(0) { - return fixed!(0); + return Ok(fixed!(0)); } // Using properties of logarithms we calculate x^y: // -> ln(x^y) = y * ln(x) // -> e^(y * ln(x)) = x^y - let y_int256 = I256::try_from(y).unwrap(); + let y_int256 = I256::try_from(y)?; // Compute y*ln(x) // Any overflow for x will be caught in _ln() in the initial bounds check - let lnx: I256 = Self::ln(I256::from_raw(self.0)); + let lnx: I256 = Self::ln(I256::from_raw(self.0))?; let mut ylnx: I256 = y_int256.wrapping_mul(lnx); ylnx = ylnx.wrapping_div(int256!(1e18)); // Calculate exp(y * ln(x)) to get x^y - Self::exp(ylnx).into() + Self::exp(ylnx)?.try_into() } - fn exp(mut x: I256) -> I256 { + fn exp(mut x: I256) -> Result { // When the result is < 0.5 we return zero. This happens when // x <= floor(log(0.5e18) * 1e18) ~ -42e18 if x <= int256!(-42139678854452767551) { - return I256::zero(); + return Ok(I256::zero()); } // When the result is > (2**255 - 1) / 1e18 we can not represent it as an // int. This happens when x >= floor(log((2**255 - 1) / 1e18) * 1e18) ~ 135. if x >= int256!(135305999368893231589) { - panic!("invalid exponent"); + return Err(eyre!("invalid exponent")); } // x is now in the range (-42, 136) * 1e18. Convert to (-42, 136) * 2**96 @@ -291,12 +298,12 @@ impl FixedPoint { .shr(int256!(195).wrapping_sub(k).low_usize()), ); - r + Ok(r) } - pub fn ln(mut x: I256) -> I256 { + pub fn ln(mut x: I256) -> Result { if x <= I256::zero() { - panic!("ln of negative number or zero"); + return Err(eyre!("ln of negative number or zero")); } // We want to convert x from 10**18 fixed point to 2**96 fixed point. @@ -384,7 +391,7 @@ impl FixedPoint { // base conversion: mul 2**18 / 2**192 r = r.asr(174); - r + Ok(r) } fn to_scaled_string(self, decimals: usize) -> String { @@ -481,7 +488,6 @@ mod tests { use std::panic; use ethers::signers::Signer; - use eyre::Result; use hyperdrive_wrappers::wrappers::mock_fixed_point_math::MockFixedPointMath; use rand::thread_rng; use test_utils::{chain::Chain, constants::DEPLOYER}; @@ -713,7 +719,7 @@ mod tests { for _ in 0..10_000 { let x: FixedPoint = rng.gen_range(fixed!(0)..=fixed!(1e18)); let y: FixedPoint = rng.gen_range(fixed!(0)..=fixed!(1e18)); - let actual = panic::catch_unwind(|| x.pow(y)); + let actual = x.pow(y); match mock_fixed_point_math.pow(x.into(), y.into()).call().await { Ok(expected) => { assert_eq!(actual.unwrap(), FixedPoint::from(expected)); @@ -737,7 +743,7 @@ mod tests { for _ in 0..10_000 { let x: FixedPoint = rng.gen(); let y: FixedPoint = rng.gen(); - let actual = panic::catch_unwind(|| x.pow(y)); + let actual = x.pow(y); match mock_fixed_point_math.pow(x.into(), y.into()).call().await { Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected)), Err(_) => assert!(actual.is_err()), @@ -758,7 +764,7 @@ mod tests { let mut rng = thread_rng(); for _ in 0..10_000 { let x: I256 = I256::try_from(rng.gen_range(fixed!(0)..=fixed!(1e18))).unwrap(); - let actual = panic::catch_unwind(|| FixedPoint::ln(x)); + let actual = FixedPoint::ln(x); match mock_fixed_point_math.ln(x).call().await { Ok(expected) => assert_eq!(actual.unwrap(), expected), Err(_) => assert!(actual.is_err()), @@ -779,8 +785,8 @@ mod tests { let mut rng = thread_rng(); for _ in 0..10_000 { let x: I256 = - I256::try_from(rng.gen_range(fixed!(0)..FixedPoint::from(I256::MAX))).unwrap(); - let actual = panic::catch_unwind(|| FixedPoint::exp(x)); + I256::try_from(rng.gen_range(fixed!(0)..FixedPoint::try_from(I256::MAX)?)).unwrap(); + let actual = FixedPoint::exp(x); match mock_fixed_point_math.exp(x).call().await { Ok(expected) => assert_eq!(actual.unwrap(), expected), Err(_) => assert!(actual.is_err()), @@ -801,7 +807,7 @@ mod tests { let mut rng = thread_rng(); for _ in 0..10_000 { let x: I256 = I256::try_from(rng.gen_range(fixed!(0)..=fixed!(1e18))).unwrap(); - let actual = panic::catch_unwind(|| FixedPoint::ln(x)); + let actual = FixedPoint::ln(x); match mock_fixed_point_math.ln(x).call().await { Ok(expected) => assert_eq!(actual.unwrap(), expected), Err(_) => assert!(actual.is_err()), @@ -822,8 +828,8 @@ mod tests { let mut rng = thread_rng(); for _ in 0..10_000 { let x: I256 = - I256::try_from(rng.gen_range(fixed!(0)..FixedPoint::from(I256::MAX))).unwrap(); - let actual = panic::catch_unwind(|| FixedPoint::ln(x)); + I256::try_from(rng.gen_range(fixed!(0)..FixedPoint::try_from(I256::MAX)?)).unwrap(); + let actual = FixedPoint::ln(x); match mock_fixed_point_math.ln(x).call().await { Ok(expected) => assert_eq!(actual.unwrap(), expected), Err(_) => assert!(actual.is_err()), diff --git a/crates/fixed-point/src/macros.rs b/crates/fixed-point/src/macros.rs index 4144e5e4..81ad76c3 100644 --- a/crates/fixed-point/src/macros.rs +++ b/crates/fixed-point/src/macros.rs @@ -110,7 +110,7 @@ macro_rules! fixed { mod tests { use ethers::types::{I256, U256}; - use crate::{fixed, int256, uint256, FixedPoint}; + use crate::FixedPoint; #[test] fn test_int256() { diff --git a/crates/hyperdrive-math/src/lib.rs b/crates/hyperdrive-math/src/lib.rs index 5c59767e..52ebf06f 100644 --- a/crates/hyperdrive-math/src/lib.rs +++ b/crates/hyperdrive-math/src/lib.rs @@ -121,13 +121,16 @@ impl State { } /// Calculates the pool's spot price. - pub fn calculate_spot_price(&self) -> FixedPoint { + pub fn calculate_spot_price(&self) -> Result { YieldSpace::calculate_spot_price(self) } /// Calculate the pool's current spot (aka "fixed") rate. - pub fn calculate_spot_rate(&self) -> FixedPoint { - calculate_rate_given_fixed_price(self.calculate_spot_price(), self.position_duration()) + pub fn calculate_spot_rate(&self) -> Result { + Ok(calculate_rate_given_fixed_price( + self.calculate_spot_price()?, + self.position_duration(), + )) } /// Converts a timestamp to the checkpoint timestamp that it corresponds to. @@ -183,7 +186,7 @@ impl State { fn reserves_given_rate_ignoring_exposure>( &self, target_rate: F, - ) -> (FixedPoint, FixedPoint) { + ) -> Result<(FixedPoint, FixedPoint)> { let target_rate = target_rate.into(); // First get the target share reserves @@ -191,16 +194,16 @@ impl State { .vault_share_price() .div_up(self.initial_vault_share_price()); let scaled_rate = (target_rate.mul_up(self.annualized_position_duration()) + fixed!(1e18)) - .pow(fixed!(1e18) / self.time_stretch()); - let inner = (self.k_down() - / (c_over_mu + scaled_rate.pow(fixed!(1e18) - self.time_stretch()))) - .pow(fixed!(1e18) / (fixed!(1e18) - self.time_stretch())); + .pow(fixed!(1e18) / self.time_stretch())?; + let inner = (self.k_down()? + / (c_over_mu + scaled_rate.pow(fixed!(1e18) - self.time_stretch())?)) + .pow(fixed!(1e18) / (fixed!(1e18) - self.time_stretch()))?; let target_share_reserves = inner / self.initial_vault_share_price(); // Then get the target bond reserves. let target_bond_reserves = inner * scaled_rate; - (target_share_reserves, target_bond_reserves) + Ok((target_share_reserves, target_bond_reserves)) } /// Config /// @@ -255,7 +258,7 @@ impl State { self.info.share_reserves.into() } - fn effective_share_reserves(&self) -> FixedPoint { + fn effective_share_reserves(&self) -> Result { calculate_effective_share_reserves(self.share_reserves(), self.share_adjustment()) } diff --git a/crates/hyperdrive-math/src/long/close.rs b/crates/hyperdrive-math/src/long/close.rs index e07ceecd..c125c8d6 100644 --- a/crates/hyperdrive-math/src/long/close.rs +++ b/crates/hyperdrive-math/src/long/close.rs @@ -1,4 +1,5 @@ use ethers::types::U256; +use eyre::{eyre, Result}; use fixed_point::{fixed, FixedPoint}; use crate::{State, YieldSpace}; @@ -10,18 +11,19 @@ impl State { bond_amount: F, maturity_time: U256, current_time: U256, - ) -> FixedPoint { + ) -> Result { let bond_amount = bond_amount.into(); if bond_amount < self.config.minimum_transaction_amount.into() { - // TODO would be nice to return a `Result` here instead of a panic. - panic!("MinimumTransactionAmount: Input amount too low"); + return Err(eyre!("MinimumTransactionAmount: Input amount too low")); } // Subtract the fees from the trade - self.calculate_close_long_flat_plus_curve(bond_amount, maturity_time, current_time) - - self.close_long_curve_fee(bond_amount, maturity_time, current_time) - - self.close_long_flat_fee(bond_amount, maturity_time, current_time) + Ok( + self.calculate_close_long_flat_plus_curve(bond_amount, maturity_time, current_time)? + - self.close_long_curve_fee(bond_amount, maturity_time, current_time)? + - self.close_long_flat_fee(bond_amount, maturity_time, current_time), + ) } /// Calculate the amount of shares returned when selling bonds without considering fees. @@ -30,7 +32,7 @@ impl State { bond_amount: F, maturity_time: U256, current_time: U256, - ) -> FixedPoint { + ) -> Result { let bond_amount = bond_amount.into(); let normalized_time_remaining = self.calculate_normalized_time_remaining(maturity_time, current_time); @@ -44,20 +46,17 @@ impl State { // Calculate the curve part of the trade let curve = if normalized_time_remaining > fixed!(0) { let curve_bonds_in = bond_amount * normalized_time_remaining; - self.calculate_shares_out_given_bonds_in_down(curve_bonds_in) + self.calculate_shares_out_given_bonds_in_down(curve_bonds_in)? } else { fixed!(0) }; - flat + curve + Ok(flat + curve) } } #[cfg(test)] mod tests { - use std::panic; - - use eyre::Result; use hyperdrive_test_utils::{chain::TestChain, constants::FAST_FUZZ_RUNS}; use rand::{thread_rng, Rng}; @@ -71,22 +70,20 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let in_ = rng.gen_range(fixed!(0)..=state.effective_share_reserves()); + let in_ = rng.gen_range(fixed!(0)..=state.effective_share_reserves()?); let maturity_time = state.checkpoint_duration(); let current_time = rng.gen_range(fixed!(0)..=maturity_time); let normalized_time_remaining = state .calculate_normalized_time_remaining(maturity_time.into(), current_time.into()); - let actual = panic::catch_unwind(|| { - state.calculate_close_long_flat_plus_curve( - in_, - maturity_time.into(), - current_time.into(), - ) - }); + let actual = state.calculate_close_long_flat_plus_curve( + in_, + maturity_time.into(), + current_time.into(), + ); match chain .mock_hyperdrive_math() .calculate_close_long( - state.effective_share_reserves().into(), + state.effective_share_reserves()?.into(), state.bond_reserves().into(), in_.into(), normalized_time_remaining.into(), @@ -110,13 +107,11 @@ mod tests { async fn test_close_long_min_txn_amount() -> Result<()> { let mut rng = thread_rng(); let state = rng.gen::(); - let result = std::panic::catch_unwind(|| { - state.calculate_close_long( - state.config.minimum_transaction_amount - 10, - 0.into(), - 0.into(), - ) - }); + let result = state.calculate_close_long( + state.config.minimum_transaction_amount - 10, + 0.into(), + 0.into(), + ); assert!(result.is_err()); Ok(()) } diff --git a/crates/hyperdrive-math/src/long/fees.rs b/crates/hyperdrive-math/src/long/fees.rs index 88a080f0..a2ebcc77 100644 --- a/crates/hyperdrive-math/src/long/fees.rs +++ b/crates/hyperdrive-math/src/long/fees.rs @@ -1,4 +1,5 @@ use ethers::types::U256; +use eyre::Result; use fixed_point::{fixed, FixedPoint}; use crate::State; @@ -12,11 +13,12 @@ impl State { /// $$ /// \Phi_{c,ol}(\Delta x) = \phi_c \cdot \left( \tfrac{1}{p} - 1 \right) \cdot \Delta x /// $$ - pub fn open_long_curve_fee(&self, base_amount: FixedPoint) -> FixedPoint { + pub fn open_long_curve_fee(&self, base_amount: FixedPoint) -> Result { // NOTE: Round up to overestimate the curve fee. - self.curve_fee() - .mul_up(fixed!(1e18).div_up(self.calculate_spot_price()) - fixed!(1e18)) - .mul_up(base_amount) + Ok(self + .curve_fee() + .mul_up(fixed!(1e18).div_up(self.calculate_spot_price()?) - fixed!(1e18)) + .mul_up(base_amount)) } /// Calculates the governance fee paid when opening longs with a given base amount. @@ -31,15 +33,15 @@ impl State { &self, base_amount: FixedPoint, maybe_curve_fee: Option, - ) -> FixedPoint { + ) -> Result { let curve_fee = match maybe_curve_fee { Some(maybe_curve_fee) => maybe_curve_fee, - None => self.open_long_curve_fee(base_amount), + None => self.open_long_curve_fee(base_amount)?, }; // NOTE: Round down to underestimate the governance curve fee. - curve_fee + Ok(curve_fee .mul_down(self.governance_lp_fee()) - .mul_down(self.calculate_spot_price()) + .mul_down(self.calculate_spot_price()?)) } /// Calculates the curve fee paid when closing longs for a given bond amount. @@ -57,14 +59,15 @@ impl State { bond_amount: FixedPoint, maturity_time: U256, current_time: U256, - ) -> FixedPoint { + ) -> Result { let normalized_time_remaining = self.calculate_normalized_time_remaining(maturity_time, current_time); // NOTE: Round up to overestimate the curve fee. - self.curve_fee() - .mul_up(fixed!(1e18) - self.calculate_spot_price()) + Ok(self + .curve_fee() + .mul_up(fixed!(1e18) - self.calculate_spot_price()?) .mul_up(bond_amount) - .mul_div_up(normalized_time_remaining, self.vault_share_price()) + .mul_div_up(normalized_time_remaining, self.vault_share_price())) } /// Calculates the flat fee paid when closing longs for a given bond amount. diff --git a/crates/hyperdrive-math/src/long/max.rs b/crates/hyperdrive-math/src/long/max.rs index 9a78ba14..c67fec74 100644 --- a/crates/hyperdrive-math/src/long/max.rs +++ b/crates/hyperdrive-math/src/long/max.rs @@ -14,13 +14,13 @@ impl State { /// $$ /// p_max = \frac{1 - \phi_f}{1 + \phi_c * \left( p_0^{-1} - 1 \right) * \left( \phi_f - 1 \right)} /// $$ - pub fn calculate_max_spot_price(&self) -> FixedPoint { - (fixed!(1e18) - self.flat_fee()) + pub fn calculate_max_spot_price(&self) -> Result { + Ok((fixed!(1e18) - self.flat_fee()) / (fixed!(1e18) + self .curve_fee() - .mul_up(fixed!(1e18).div_up(self.calculate_spot_price()) - fixed!(1e18))) - .mul_up(fixed!(1e18) - self.flat_fee()) + .mul_up(fixed!(1e18).div_up(self.calculate_spot_price()?) - fixed!(1e18))) + .mul_up(fixed!(1e18) - self.flat_fee())) } /// Calculates the pool's solvency. @@ -52,7 +52,7 @@ impl State { // max spot price let spot_price_after_min_long = self.calculate_spot_price_after_long(self.minimum_transaction_amount(), None)?; - if spot_price_after_min_long > self.calculate_max_spot_price() { + if spot_price_after_min_long > self.calculate_max_spot_price()? { return Ok(fixed!(0)); } @@ -64,7 +64,7 @@ impl State { absolute_max_base_amount, absolute_max_bond_amount, checkpoint_exposure, - ) + )? .is_some() { return Ok(absolute_max_base_amount.min(budget)); @@ -88,7 +88,7 @@ impl State { // The guess that we make is very important in determining how quickly // we converge to the solution. let mut max_base_amount = - self.max_long_guess(absolute_max_base_amount, checkpoint_exposure); + self.max_long_guess(absolute_max_base_amount, checkpoint_exposure)?; // possible_max_base_amount might be less than minimum transaction amount. // we clamp here if so @@ -99,7 +99,7 @@ impl State { max_base_amount, self.calculate_open_long(max_base_amount)?, checkpoint_exposure, - ) { + )? { Some(solvency) => solvency, None => return Err(eyre!("Initial guess in `calculate_max_long` is insolvent.")), }; @@ -129,7 +129,7 @@ impl State { // candidate solution, we check to see if the pool is solvent after // a long is opened with the candidate amount. If the pool isn't // solvent, then we're done. - let derivative = match self.solvency_after_long_derivative(max_base_amount) { + let derivative = match self.solvency_after_long_derivative(max_base_amount)? { Some(derivative) => derivative, None => break, }; @@ -146,7 +146,7 @@ impl State { possible_max_base_amount, self.calculate_open_long(possible_max_base_amount)?, checkpoint_exposure, - ) { + )? { Some(s) => s, None => break, }; @@ -204,19 +204,21 @@ impl State { // ) // ) ** (1 / (1 - t_s)) let inner = self - .k_down() + .k_down()? .div_down( self.vault_share_price() .div_up(self.initial_vault_share_price()) + ((fixed!(1e18) + self .curve_fee() - .mul_up(fixed!(1e18).div_up(self.calculate_spot_price()) - fixed!(1e18)) + .mul_up( + fixed!(1e18).div_up(self.calculate_spot_price()?) - fixed!(1e18), + ) .mul_up(fixed!(1e18) - self.flat_fee())) .div_up(fixed!(1e18) - self.flat_fee())) - .pow((fixed!(1e18) - self.time_stretch()).div_down(self.time_stretch())), + .pow((fixed!(1e18) - self.time_stretch()).div_down(self.time_stretch()))?, ) - .pow(fixed!(1e18).div_down(fixed!(1e18) - self.time_stretch())); + .pow(fixed!(1e18).div_down(fixed!(1e18) - self.time_stretch()))?; let target_share_reserves = inner.div_down(self.initial_vault_share_price()); // Now that we have the target share reserves, we can calculate the @@ -228,15 +230,15 @@ impl State { // // y_t = inner * ((1 + curveFee * (1 / p_0 - 1) * (1 - flatFee)) / (1 - flatFee)) ** (1 / t_s) let fee_adjustment = self.curve_fee() - * (fixed!(1e18) / self.calculate_spot_price() - fixed!(1e18)) + * (fixed!(1e18) / self.calculate_spot_price()? - fixed!(1e18)) * (fixed!(1e18) - self.flat_fee()); let target_bond_reserves = ((fixed!(1e18) + fee_adjustment) / (fixed!(1e18) - self.flat_fee())) - .pow(fixed!(1e18).div_up(self.time_stretch())) + .pow(fixed!(1e18).div_up(self.time_stretch()))? * inner; // Catch if the target share reserves are smaller than the effective share reserves. - let effective_share_reserves = self.effective_share_reserves(); + let effective_share_reserves = self.effective_share_reserves()?; if target_share_reserves < effective_share_reserves { return Err(eyre!( "target share reserves less than effective share reserves" @@ -251,7 +253,7 @@ impl State { // The absolute max bond amount is given by: // absolute_max_bond_amount = (y - y_t) - Phi_c(absolute_max_base_amount) let absolute_max_bond_amount = (self.bond_reserves() - target_bond_reserves) - - self.open_long_curve_fee(absolute_max_base_amount); + - self.open_long_curve_fee(absolute_max_base_amount)?; Ok((absolute_max_base_amount, absolute_max_bond_amount)) } @@ -264,11 +266,11 @@ impl State { &self, absolute_max_base_amount: FixedPoint, checkpoint_exposure: I256, - ) -> FixedPoint { + ) -> Result { // Calculate an initial estimate of the max long by using the spot price as // our conservative price. - let spot_price = self.calculate_spot_price(); - let guess = self.max_long_estimate(spot_price, spot_price, checkpoint_exposure); + let spot_price = self.calculate_spot_price()?; + let guess = self.max_long_estimate(spot_price, spot_price, checkpoint_exposure)?; // We know that the spot price is 1 when the absolute max base amount is // used to open a long. We also know that our spot price isn't a great @@ -277,7 +279,7 @@ impl State { // price by interpolating between the spot price and 1 depending on how // large the estimate is. let t = (guess / absolute_max_base_amount) - .pow(fixed!(1e18).div_up(fixed!(1e18) - self.time_stretch())) + .pow(fixed!(1e18).div_up(fixed!(1e18) - self.time_stretch()))? * fixed!(0.8e18); let estimate_price = spot_price * (fixed!(1e18) - t) + fixed!(1e18) * t; @@ -326,8 +328,8 @@ impl State { estimate_price: FixedPoint, spot_price: FixedPoint, checkpoint_exposure: I256, - ) -> FixedPoint { - let checkpoint_exposure = FixedPoint::from(-checkpoint_exposure.min(int256!(0))); + ) -> Result { + let checkpoint_exposure = FixedPoint::try_from(-checkpoint_exposure.min(int256!(0)))?; let mut estimate = self.calculate_solvency() + checkpoint_exposure / self.vault_share_price(); estimate = estimate.mul_div_down(self.vault_share_price(), fixed!(2e18)); @@ -335,7 +337,7 @@ impl State { + self.governance_lp_fee() * self.curve_fee() * (fixed!(1e18) - spot_price) - fixed!(1e18) - self.curve_fee() * (fixed!(1e18) / spot_price - fixed!(1e18)); - estimate + Ok(estimate) } /// Calculates the solvency of the pool $S(x)$ after a long is opened with a base @@ -378,22 +380,22 @@ impl State { base_amount: FixedPoint, bond_amount: FixedPoint, checkpoint_exposure: I256, - ) -> Option { - let governance_fee = self.open_long_governance_fee(base_amount, None); + ) -> Result> { + let governance_fee = self.open_long_governance_fee(base_amount, None)?; let share_reserves = self.share_reserves() + base_amount / self.vault_share_price() - governance_fee / self.vault_share_price(); let exposure = self.long_exposure() + bond_amount; - let checkpoint_exposure = FixedPoint::from(-checkpoint_exposure.min(int256!(0))); + 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() { - Some( + Ok(Some( share_reserves + checkpoint_exposure / self.vault_share_price() - exposure / self.vault_share_price() - self.minimum_share_reserves(), - ) + )) } else { - None + Ok(None) } } @@ -416,16 +418,14 @@ impl State { pub(super) fn solvency_after_long_derivative( &self, base_amount: FixedPoint, - ) -> Option { - let maybe_derivative = self.long_amount_derivative(base_amount); - maybe_derivative.map(|derivative| { - (derivative - + self.governance_lp_fee() - * self.curve_fee() - * (fixed!(1e18) - self.calculate_spot_price()) + ) -> Result> { + let maybe_derivative = self.long_amount_derivative(base_amount)?; + let spot_price = self.calculate_spot_price()?; + Ok(maybe_derivative.map(|derivative| { + (derivative + self.governance_lp_fee() * self.curve_fee() * (fixed!(1e18) - spot_price) - fixed!(1e18)) .mul_div_down(fixed!(1e18), self.vault_share_price()) - }) + })) } /// Calculates the derivative of [long_amount](long_amount) with respect to the @@ -455,33 +455,36 @@ impl State { /// $$ /// c'(x) = \phi_{c} \cdot \left( \tfrac{1}{p} - 1 \right) /// $$ - pub(super) fn long_amount_derivative(&self, base_amount: FixedPoint) -> Option { + pub(super) fn long_amount_derivative( + &self, + base_amount: FixedPoint, + ) -> Result> { let share_amount = base_amount / self.vault_share_price(); let inner = - self.initial_vault_share_price() * (self.effective_share_reserves() + share_amount); - let mut derivative = fixed!(1e18) / (inner).pow(self.time_stretch()); + self.initial_vault_share_price() * (self.effective_share_reserves()? + share_amount); + let mut derivative = fixed!(1e18) / (inner).pow(self.time_stretch())?; // It's possible that k is slightly larger than the rhs in the inner // calculation. If this happens, we are close to the root, and we short // circuit. - let k = self.k_down(); + let k = self.k_down()?; let rhs = self.vault_share_price().mul_div_down( - inner.pow(self.time_stretch()), + inner.pow(self.time_stretch())?, self.initial_vault_share_price(), ); if k < rhs { - return None; + return Ok(None); } derivative *= (k - rhs).pow( self.time_stretch() .div_up(fixed!(1e18) - self.time_stretch()), - ); + )?; // Finish computing the derivative. derivative -= - self.curve_fee() * ((fixed!(1e18) / self.calculate_spot_price()) - fixed!(1e18)); + self.curve_fee() * ((fixed!(1e18) / self.calculate_spot_price()?) - fixed!(1e18)); - Some(derivative) + Ok(Some(derivative)) } } @@ -490,7 +493,6 @@ mod tests { use std::panic; use ethers::types::U256; - use eyre::Result; use fixed_point::uint256; use hyperdrive_test_utils::{ chain::TestChain, @@ -533,9 +535,9 @@ mod tests { calculate_effective_share_reserves( state.info.share_reserves.into(), state.info.share_adjustment, - ) + )? .into(), - state.calculate_spot_price().into(), + state.calculate_spot_price()?.into(), ) .call() .await @@ -573,7 +575,7 @@ mod tests { // Generate a random checkpoint exposure. let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(I256::MAX)); + let value = rng.gen_range(fixed!(0)..=FixedPoint::try_from(I256::MAX)?); let sign = rng.gen::(); if sign { -I256::try_from(value).unwrap() @@ -584,7 +586,7 @@ mod tests { // Check Solidity against Rust. let max_iterations = 8usize; - // Need to catch panics because of FixedPoint. + // We need to catch panics because of overflows. let actual = panic::catch_unwind(|| { state.calculate_max_long( U256::MAX, @@ -647,37 +649,33 @@ mod tests { let state = rng.gen::(); let amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18)); - let p1_result = std::panic::catch_unwind(|| { + // We need to catch panics here because FixedPoint panics on overflow or underflow. + let p1 = match panic::catch_unwind(|| { state.calculate_open_long(amount - empirical_derivative_epsilon) - }); - let p1; - let p2; - match p1_result { + }) { Ok(p) => match p { - Ok(p) => p1 = p, - Err(_) => continue, + Ok(p) => p, + Err(_) => continue, // Err; the amount results in the pool being insolvent. }, - // If the amount results in the pool being insolvent, skip this iteration - Err(_) => continue, - } + Err(_) => continue, // panic; likely in FixedPoint + }; - let p2_result = std::panic::catch_unwind(|| { + let p2 = match panic::catch_unwind(|| { state.calculate_open_long(amount + empirical_derivative_epsilon) - }); - match p2_result { + }) { Ok(p) => match p { - Ok(p) => p2 = p, + Ok(p) => p, Err(_) => continue, }, - // If the amount results in the pool being insolvent, skip this iteration + // If the amount results in the pool being insolvent, skip this iteration. Err(_) => continue, - } - // Sanity check + }; + // Sanity check. assert!(p2 > p1); let empirical_derivative = (p2 - p1) / (fixed!(2e18) * empirical_derivative_epsilon); - let open_long_derivative = state.long_amount_derivative(amount); - open_long_derivative.map(|derivative| { + let maybe_open_long_derivative = state.long_amount_derivative(amount)?; + maybe_open_long_derivative.map(|derivative| { let derivative_diff; if derivative >= empirical_derivative { derivative_diff = derivative - empirical_derivative; @@ -739,7 +737,7 @@ mod tests { .await?; // Bob opens a max long. - let max_spot_price = bob.get_state().await?.calculate_max_spot_price(); + let max_spot_price = bob.get_state().await?.calculate_max_spot_price()?; let max_long = bob.calculate_max_long(None).await?; let spot_price_after_long = bob .get_state() diff --git a/crates/hyperdrive-math/src/long/open.rs b/crates/hyperdrive-math/src/long/open.rs index 2a35fca6..d48a6c0c 100644 --- a/crates/hyperdrive-math/src/long/open.rs +++ b/crates/hyperdrive-math/src/long/open.rs @@ -31,17 +31,17 @@ impl State { } let bond_amount = - self.calculate_bonds_out_given_shares_in_down(base_amount / self.vault_share_price()); + self.calculate_bonds_out_given_shares_in_down(base_amount / self.vault_share_price())?; // Throw an error if opening the long would result in negative interest. let ending_spot_price = self.calculate_spot_price_after_long(base_amount, bond_amount.into())?; - let max_spot_price = self.calculate_max_spot_price(); + let max_spot_price = self.calculate_max_spot_price()?; if ending_spot_price > max_spot_price { return Err(eyre!("InsufficientLiquidity: Negative Interest",)); } - Ok(bond_amount - self.open_long_curve_fee(base_amount)) + Ok(bond_amount - self.open_long_curve_fee(base_amount)?) } /// Calculate an updated pool state after opening a long. @@ -88,9 +88,9 @@ impl State { let mut state: State = self.clone(); state.info.bond_reserves -= bond_amount.into(); state.info.share_reserves += (base_amount / state.vault_share_price() - - self.open_long_governance_fee(base_amount, None) / state.vault_share_price()) + - self.open_long_governance_fee(base_amount, None)? / state.vault_share_price()) .into(); - Ok(state.calculate_spot_price()) + state.calculate_spot_price() } /// Calculate the spot rate after a long has been opened. @@ -233,7 +233,7 @@ mod tests { // Verify that the predicted spot price is equal to the ending spot // price. These won't be exactly equal because the vault share price // increases between the prediction and opening the long. - let actual_spot_price = bob.get_state().await?.calculate_spot_price(); + let actual_spot_price = bob.get_state().await?.calculate_spot_price()?; let delta = if actual_spot_price > expected_spot_price { actual_spot_price - expected_spot_price } else { @@ -295,7 +295,7 @@ mod tests { // Verify that the predicted spot rate is equal to the ending spot // rate. These won't be exactly equal because the vault share price // increases between the prediction and opening the long. - let actual_spot_rate = bob.get_state().await?.calculate_spot_rate(); + let actual_spot_rate = bob.get_state().await?.calculate_spot_rate()?; let delta = if actual_spot_rate > expected_spot_rate { actual_spot_rate - expected_spot_rate } else { diff --git a/crates/hyperdrive-math/src/long/targeted.rs b/crates/hyperdrive-math/src/long/targeted.rs index 748df71c..b814be2d 100644 --- a/crates/hyperdrive-math/src/long/targeted.rs +++ b/crates/hyperdrive-math/src/long/targeted.rs @@ -51,7 +51,7 @@ impl State { }; // Check input args. - let current_rate = self.calculate_spot_rate(); + let current_rate = self.calculate_spot_rate()?; if target_rate > current_rate { return Err(eyre!( "target_rate = {} argument must be less than the current_rate = {} for a targeted long.", @@ -61,9 +61,9 @@ impl State { // Estimate the long that achieves a target rate. let (target_share_reserves, target_bond_reserves) = - self.reserves_given_rate_ignoring_exposure(target_rate); + self.reserves_given_rate_ignoring_exposure(target_rate)?; let (mut target_base_delta, target_bond_delta) = - self.long_trade_deltas_from_reserves(target_share_reserves, target_bond_reserves); + self.long_trade_deltas_from_reserves(target_share_reserves, target_bond_reserves)?; // Determine what rate was achieved. let resulting_rate = @@ -79,7 +79,7 @@ impl State { // If we were still close enough and solvent, return. if self - .solvency_after_long(target_base_delta, target_bond_delta, checkpoint_exposure) + .solvency_after_long(target_base_delta, target_bond_delta, checkpoint_exposure)? .is_some() && rate_error < allowable_error { @@ -95,7 +95,7 @@ impl State { // If solvent & within the allowable error, stop here. let rate_error = resulting_rate - target_rate; if self - .solvency_after_long(target_base_delta, target_bond_delta, checkpoint_exposure) + .solvency_after_long(target_base_delta, target_bond_delta, checkpoint_exposure)? .is_some() && rate_error < allowable_error { @@ -109,9 +109,8 @@ impl State { // Iteratively find a solution for _ in 0..maybe_max_iterations.unwrap_or(7) { - let possible_target_bond_delta = self - .calculate_open_long(possible_target_base_delta) - .unwrap(); + let possible_target_bond_delta = + self.calculate_open_long(possible_target_base_delta)?; let resulting_rate = self.calculate_spot_rate_after_long( possible_target_base_delta, Some(possible_target_bond_delta), @@ -135,7 +134,7 @@ impl State { possible_target_base_delta, possible_target_bond_delta, checkpoint_exposure, - ) + )? .is_some() && loss < allowable_error { @@ -162,19 +161,16 @@ impl State { if self .solvency_after_long( possible_target_base_delta, - self.calculate_open_long(possible_target_base_delta) - .unwrap(), + self.calculate_open_long(possible_target_base_delta)?, checkpoint_exposure, - ) + )? .is_none() { return Err(eyre!("Guess in `calculate_targeted_long` is insolvent.")); } // Final accuracy check. - let possible_target_bond_delta = self - .calculate_open_long(possible_target_base_delta) - .unwrap(); + let possible_target_bond_delta = self.calculate_open_long(possible_target_base_delta)?; let resulting_rate = self.calculate_spot_rate_after_long( possible_target_base_delta, Some(possible_target_bond_delta), @@ -282,12 +278,12 @@ impl State { // g'(x) = \phi_g \phi_c (1 - p_0) let gov_fee_derivative = self.governance_lp_fee() * self.curve_fee() - * (fixed!(1e18) - self.calculate_spot_price()); + * (fixed!(1e18) - self.calculate_spot_price()?); // a(x) = mu * (z_{e,0} + 1/c (x - g(x)) let inner_numerator = self.mu() - * (self.ze() - + (base_amount - self.open_long_governance_fee(base_amount, None)) + * (self.ze()? + + (base_amount - self.open_long_governance_fee(base_amount, None)?) .div_down(self.vault_share_price())); // a'(x) = (mu / c) (1 - g'(x)) @@ -301,7 +297,7 @@ impl State { // b'(x) = -y'(x) // -b'(x) = y'(x) - let long_amount_derivative = match self.long_amount_derivative(base_amount) { + let long_amount_derivative = match self.long_amount_derivative(base_amount)? { Some(derivative) => derivative, None => return Err(eyre!("long_amount_derivative failure.")), }; @@ -319,7 +315,7 @@ impl State { // v(x) is flipped to (denominator / numerator) to avoid a negative exponent Ok(inner_derivative * self.time_stretch() - * (inner_denominator / inner_numerator).pow(fixed!(1e18) - self.time_stretch())) + * (inner_denominator / inner_numerator).pow(fixed!(1e18) - self.time_stretch())?) } /// Calculate the base & bond deltas for a long trade that moves the current @@ -339,12 +335,12 @@ impl State { &self, ending_share_reserves: FixedPoint, ending_bond_reserves: FixedPoint, - ) -> (FixedPoint, FixedPoint) { + ) -> Result<(FixedPoint, FixedPoint)> { let base_delta = - (ending_share_reserves - self.effective_share_reserves()) * self.vault_share_price(); + (ending_share_reserves - self.effective_share_reserves()?) * self.vault_share_price(); let bond_delta = - (self.bond_reserves() - ending_bond_reserves) - self.open_long_curve_fee(base_delta); - (base_delta, bond_delta) + (self.bond_reserves() - ending_bond_reserves) - self.open_long_curve_fee(base_delta)?; + Ok((base_delta, bond_delta)) } } @@ -435,7 +431,7 @@ mod tests { // Get a targeted long amount. // TODO: explore tighter bounds on this. - let target_rate = bob.get_state().await?.calculate_spot_rate() + let target_rate = bob.get_state().await?.calculate_spot_rate()? / rng.gen_range(fixed!(1.0001e18)..=fixed!(10e18)); // let target_rate = initial_fixed_rate / fixed!(2e18); let targeted_long_result = bob @@ -448,7 +444,7 @@ mod tests { // Bob opens a targeted long. let current_state = bob.get_state().await?; - let max_spot_price_before_long = current_state.calculate_max_spot_price(); + let max_spot_price_before_long = current_state.calculate_max_spot_price()?; match targeted_long_result { // If the code ran without error, open the long Ok(targeted_long) => { @@ -495,7 +491,7 @@ mod tests { // Check that our resulting price is under the max let current_state = bob.get_state().await?; - let spot_price_after_long = current_state.calculate_spot_price(); + let spot_price_after_long = current_state.calculate_spot_price()?; assert!( max_spot_price_before_long > spot_price_after_long, "Resulting price is greater than the max." @@ -505,7 +501,7 @@ mod tests { let is_solvent = { current_state.calculate_solvency() > allowable_solvency_error }; assert!(is_solvent, "Resulting pool state is not solvent."); - let new_rate = current_state.calculate_spot_rate(); + let new_rate = current_state.calculate_spot_rate()?; // If the budget was NOT consumed, then we assume the target was hit. if bob.base() > allowable_budget_error { // Actual price might result in long overshooting the target. diff --git a/crates/hyperdrive-math/src/lp/add.rs b/crates/hyperdrive-math/src/lp/add.rs index ddce9d04..8e6dd977 100644 --- a/crates/hyperdrive-math/src/lp/add.rs +++ b/crates/hyperdrive-math/src/lp/add.rs @@ -16,7 +16,7 @@ impl State { as_base: bool, ) -> Result { // Enforce the slippage guard. - let apr = self.calculate_spot_rate(); + let apr = self.calculate_spot_rate()?; if apr < min_apr || apr > max_apr { return Err(eyre!("InvalidApr: Apr is outside the slippage guard.")); } @@ -70,12 +70,12 @@ impl State { let share_contribution = { if as_base { - I256::try_from(contribution.div_down(self.vault_share_price())).unwrap() + I256::try_from(contribution.div_down(self.vault_share_price()))? } else { - I256::try_from(contribution).unwrap() + I256::try_from(contribution)? } }; - Ok(self.get_state_after_liquidity_update(share_contribution)) + self.get_state_after_liquidity_update(share_contribution) } pub fn calculate_pool_deltas_after_add_liquidity( @@ -110,7 +110,7 @@ impl State { } /// Gets the resulting state when updating liquidity. - pub fn get_state_after_liquidity_update(&self, share_reserves_delta: I256) -> State { + pub fn get_state_after_liquidity_update(&self, share_reserves_delta: I256) -> Result { let share_reserves = self.share_reserves(); let share_adjustment = self.share_adjustment(); let bond_reserves = self.bond_reserves(); @@ -124,24 +124,23 @@ impl State { bond_reserves, minimum_share_reserves, share_reserves_delta, - ) - .unwrap(); + )?; // Update and return the new state. let mut new_info = self.info.clone(); new_info.share_reserves = U256::from(updated_share_reserves); new_info.share_adjustment = updated_share_adjustment; new_info.bond_reserves = U256::from(updated_bond_reserves); - State { + Ok(State { config: self.config.clone(), info: new_info, - } + }) } } #[cfg(test)] mod tests { - use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::panic; use fixed_point::{fixed, int256, uint256}; use hyperdrive_test_utils::{ @@ -167,7 +166,8 @@ mod tests { let max_apr = rng.gen_range(fixed!(5e17)..fixed!(1e18)); // Calculate lp_shares from the rust function. - let result = catch_unwind(AssertUnwindSafe(|| { + // Testing mostly unhappy paths here since random state will mostly fail. + match panic::catch_unwind(|| { state.calculate_add_liquidity( current_block_timestamp, contribution, @@ -176,30 +176,25 @@ mod tests { max_apr, true, ) - })); - - // Testing mostly unhappy paths here since random state will mostly fail. - match result { - Ok(result) => match result { - Ok(lp_shares) => { - assert!(lp_shares >= min_lp_share_price); - } + }) { + Ok(lp_shares) => match lp_shares { + Ok(lp_shares) => assert!(lp_shares >= min_lp_share_price), Err(err) => { let message = err.to_string(); if message == "MinimumTransactionAmount: Contribution is smaller than the minimum transaction." { - assert!(contribution < state.minimum_transaction_amount()); + assert!(contribution < state.minimum_transaction_amount()); } else if message == "InvalidApr: Apr is outside the slippage guard." { - let apr = state.calculate_spot_rate(); + let apr = state.calculate_spot_rate()?; assert!(apr < min_apr || apr > max_apr); } else if message == "DecreasedPresentValueWhenAddingLiquidity: Present value decreased after adding liquidity." { let share_contribution = I256::try_from(contribution / state.vault_share_price()).unwrap(); - let new_state = state.get_state_after_liquidity_update(share_contribution); + let new_state = state.get_state_after_liquidity_update(share_contribution)?; let starting_present_value = state.calculate_present_value(current_block_timestamp)?; let ending_present_value = new_state.calculate_present_value(current_block_timestamp)?; assert!(ending_present_value < starting_present_value); @@ -208,7 +203,7 @@ mod tests { else if message == "MinimumTransactionAmount: Not enough lp shares minted." { let share_contribution = I256::try_from(contribution / state.vault_share_price()).unwrap(); - let new_state = state.get_state_after_liquidity_update(share_contribution); + let new_state = state.get_state_after_liquidity_update(share_contribution)?; let starting_present_value = state.calculate_present_value(current_block_timestamp)?; let ending_present_value = new_state.calculate_present_value(current_block_timestamp)?; let lp_shares = (ending_present_value - starting_present_value) @@ -219,7 +214,7 @@ mod tests { else if message == "OutputLimit: Not enough lp shares minted." { let share_contribution = I256::try_from(contribution / state.vault_share_price()).unwrap(); - let new_state = state.get_state_after_liquidity_update(share_contribution); + let new_state = state.get_state_after_liquidity_update(share_contribution)?; let starting_present_value = state.calculate_present_value(current_block_timestamp)?; let ending_present_value = new_state.calculate_present_value(current_block_timestamp)?; let lp_shares = (ending_present_value - starting_present_value) @@ -228,8 +223,7 @@ mod tests { } } }, - // ignore inner panics - Err(_) => {} + Err(_) => continue, // FixedPoint underflow or overflow. } } diff --git a/crates/hyperdrive-math/src/lp/math.rs b/crates/hyperdrive-math/src/lp/math.rs index d1dce7f9..05426ce7 100644 --- a/crates/hyperdrive-math/src/lp/math.rs +++ b/crates/hyperdrive-math/src/lp/math.rs @@ -47,7 +47,7 @@ impl State { let bond_reserves = self.initial_vault_share_price().mul_div_down( self.vault_share_price().mul_down(share_reserves), self.vault_share_price() - .mul_down(target_price.pow(one.div_down(self.time_stretch()))) + .mul_down(target_price.pow(one.div_down(self.time_stretch()))?) + self.initial_vault_share_price().mul_up(target_price), ); @@ -57,8 +57,7 @@ impl State { // // zeta = (p_target * y) / c let share_adjustment = - I256::try_from(bond_reserves.mul_div_down(target_price, self.vault_share_price())) - .unwrap(); + I256::try_from(bond_reserves.mul_div_down(target_price, self.vault_share_price()))?; Ok((share_reserves, share_adjustment, bond_reserves)) } @@ -79,35 +78,35 @@ impl State { // Get the updated share reserves. let new_share_reserves = if share_reserves_delta > I256::zero() { - I256::try_from(share_reserves).unwrap() + share_reserves_delta + I256::try_from(share_reserves)? + share_reserves_delta } else { - I256::try_from(share_reserves).unwrap() - share_reserves_delta + I256::try_from(share_reserves)? - share_reserves_delta }; // Ensure the minimum share reserve level. - if new_share_reserves < I256::try_from(minimum_share_reserves).unwrap() { + if new_share_reserves < I256::try_from(minimum_share_reserves)? { return Err(eyre!( "Update would result in share reserves below minimum." )); } // Convert to Fixedpoint to allow the math below. - let new_share_reserves = FixedPoint::from(new_share_reserves); + let new_share_reserves = FixedPoint::try_from(new_share_reserves)?; // Get the updated share adjustment. let new_share_adjustment = if share_adjustment >= I256::zero() { - let share_adjustment_fp = FixedPoint::from(share_adjustment); + let share_adjustment_fp = FixedPoint::try_from(share_adjustment)?; I256::try_from(new_share_reserves.mul_div_down(share_adjustment_fp, share_reserves))? } else { - let share_adjustment_fp = FixedPoint::from(-share_adjustment); + let share_adjustment_fp = FixedPoint::try_from(-share_adjustment)?; -I256::try_from(new_share_reserves.mul_div_up(share_adjustment_fp, share_reserves))? }; // Get the updated bond reserves. let old_effective_share_reserves = - calculate_effective_share_reserves(self.share_reserves(), self.share_adjustment()); + calculate_effective_share_reserves(self.share_reserves(), self.share_adjustment())?; let new_effective_share_reserves = - calculate_effective_share_reserves(new_share_reserves, new_share_adjustment); + calculate_effective_share_reserves(new_share_reserves, new_share_adjustment)?; let new_bond_reserves = bond_reserves.mul_div_down(new_effective_share_reserves, old_effective_share_reserves); @@ -135,14 +134,14 @@ impl State { .calculate_net_flat_trade(long_average_time_remaining, short_average_time_remaining)?; let present_value: I256 = - I256::try_from(self.share_reserves()).unwrap() + net_curve_trade + net_flat_trade - - I256::try_from(self.minimum_share_reserves()).unwrap(); + I256::try_from(self.share_reserves())? + net_curve_trade + net_flat_trade + - I256::try_from(self.minimum_share_reserves())?; if present_value < int256!(0) { return Err(eyre!("Negative present value!")); } - Ok(present_value.into()) + present_value.try_into() } pub fn calculate_net_curve_trade( @@ -164,22 +163,20 @@ impl State { // // netCurveTrade = y_l * t_l - y_s * t_s. let net_curve_position: I256 = - I256::try_from(self.longs_outstanding().mul_up(long_average_time_remaining)).unwrap() + I256::try_from(self.longs_outstanding().mul_up(long_average_time_remaining))? - I256::try_from( self.shorts_outstanding() .mul_down(short_average_time_remaining), - ) - .unwrap(); + )?; // If the net curve position is positive, then the pool is net long. // Closing the net curve position results in the longs being paid out // from the share reserves, so we negate the result. match net_curve_position.cmp(&int256!(0)) { Ordering::Greater => { - let net_curve_position: FixedPoint = FixedPoint::from(net_curve_position); - let max_curve_trade = self - .calculate_max_sell_bonds_in_safe(self.minimum_share_reserves()) - .unwrap(); + let net_curve_position: FixedPoint = FixedPoint::try_from(net_curve_position)?; + let max_curve_trade = + self.calculate_max_sell_bonds_in_safe(self.minimum_share_reserves())?; if max_curve_trade >= net_curve_position { match self.calculate_shares_out_given_bonds_in_down_safe(net_curve_position) { Ok(net_curve_trade) => Ok(-I256::try_from(net_curve_trade)?), @@ -203,7 +200,7 @@ impl State { // `effectiveShareReserves - minimumShareReserves`. if self.share_adjustment() >= I256::from(0) { Ok(-I256::try_from( - self.effective_share_reserves() - self.minimum_share_reserves(), + self.effective_share_reserves()? - self.minimum_share_reserves(), )?) // Otherwise, the effective share reserves are greater than the @@ -218,7 +215,7 @@ impl State { } } Ordering::Less => { - let net_curve_position: FixedPoint = FixedPoint::from(-net_curve_position); + let net_curve_position: FixedPoint = FixedPoint::try_from(-net_curve_position)?; let max_curve_trade = self.calculate_max_buy_bonds_out_safe()?; if max_curve_trade >= net_curve_position { match self.calculate_shares_in_given_bonds_out_up_safe(net_curve_position) { @@ -337,7 +334,7 @@ impl State { ) -> Result { // Calculate the bond reserves after the bond amount is applied. let bond_reserves_after = if bond_amount >= I256::zero() { - self.bond_reserves() + bond_amount.into() + self.bond_reserves() + bond_amount.try_into()? } else { let bond_amount = FixedPoint::from(U256::try_from(-bond_amount)?); if bond_amount < self.bond_reserves() { @@ -357,15 +354,15 @@ impl State { let derivative = self.vault_share_price().div_up( self.initial_vault_share_price() .mul_down(effective_share_reserves) - .pow(self.time_stretch()), + .pow(self.time_stretch())?, ) + original_bond_reserves.div_up( original_effective_share_reserves - .mul_down(self.bond_reserves().pow(self.time_stretch())), + .mul_down(self.bond_reserves().pow(self.time_stretch())?), ); // NOTE: Rounding this down rounds the subtraction up. let rhs = original_bond_reserves.div_down( - original_effective_share_reserves.mul_up(bond_reserves_after.pow(self.time_stretch())), + original_effective_share_reserves.mul_up(bond_reserves_after.pow(self.time_stretch())?), ); if derivative < rhs { return Err(eyre!("Derivative is less than right hand side")); @@ -377,8 +374,8 @@ impl State { // inner = ( // (mu / c) * (k(x) - (y(x) + dy) ** (1 - t_s)) // ) ** (t_s / (1 - t_s)) - let k = self.k_up(); - let inner = bond_reserves_after.pow(fixed!(1e18) - self.time_stretch()); + let k = self.k_up()?; + let inner = bond_reserves_after.pow(fixed!(1e18) - self.time_stretch())?; if k < inner { return Err(eyre!("k is less than inner")); } @@ -389,13 +386,13 @@ impl State { inner.pow( self.time_stretch() .div_up(fixed!(1e18) - self.time_stretch()), - ) + )? } else { // NOTE: Round the exponent down since this rounds the result up. inner.pow( self.time_stretch() .div_down(fixed!(1e18) - self.time_stretch()), - ) + )? }; let derivative = derivative.mul_div_up(inner, self.vault_share_price()); let derivative = if fixed!(1e18) > derivative { @@ -411,7 +408,7 @@ impl State { // derivative = derivative * (1 - (zeta / z)) let derivative = if original_share_adjustment >= I256::zero() { let right_hand_side = - FixedPoint::from(original_share_adjustment).div_up(original_share_reserves); + FixedPoint::try_from(original_share_adjustment)?.div_up(original_share_reserves); if right_hand_side > fixed!(1e18) { return Err(eyre!("Right hand side is greater than 1e18")); } @@ -420,7 +417,7 @@ impl State { } else { derivative.mul_down( fixed!(1e18) - + FixedPoint::from(-original_share_adjustment) + + FixedPoint::try_from(-original_share_adjustment)? .div_down(original_share_reserves), ) }; @@ -450,9 +447,8 @@ mod tests { let state = rng.gen::(); let initial_contribution = rng.gen_range(fixed!(0)..=state.bond_reserves()); let initial_rate = rng.gen_range(fixed!(0)..=fixed!(1)); - let (actual_share_reserves, actual_share_adjustment, actual_bond_reserves) = state - .calculate_initial_reserves(initial_contribution, initial_rate) - .unwrap(); + let (actual_share_reserves, actual_share_adjustment, actual_bond_reserves) = + state.calculate_initial_reserves(initial_contribution, initial_rate)?; match chain .mock_lp_math() .calculate_initial_reserves( @@ -654,7 +650,7 @@ mod tests { bond_amount, original_state.share_reserves(), original_state.bond_reserves(), - original_state.effective_share_reserves(), + original_state.effective_share_reserves()?, original_state.share_adjustment(), ); @@ -708,7 +704,7 @@ mod tests { match mock .calculate_shares_delta_given_bonds_delta_derivative_safe( params, - U256::from(original_state.effective_share_reserves()), + U256::from(original_state.effective_share_reserves()?), bond_amount, ) .call() diff --git a/crates/hyperdrive-math/src/short/close.rs b/crates/hyperdrive-math/src/short/close.rs index 119aeea9..a95b16bf 100644 --- a/crates/hyperdrive-math/src/short/close.rs +++ b/crates/hyperdrive-math/src/short/close.rs @@ -1,4 +1,5 @@ use ethers::types::U256; +use eyre::{eyre, Result}; use fixed_point::{fixed, FixedPoint}; use crate::{State, YieldSpace}; @@ -25,7 +26,7 @@ impl State { bond_amount: F, maturity_time: U256, current_time: U256, - ) -> FixedPoint { + ) -> Result { let bond_amount = bond_amount.into(); let normalized_time_remaining = self.calculate_normalized_time_remaining(maturity_time, current_time); @@ -34,10 +35,9 @@ impl State { // payment. // let curve_bonds_in = bond_amount.mul_up(normalized_time_remaining); - self.calculate_shares_in_given_bonds_out_up_safe(curve_bonds_in) - .unwrap() + Ok(self.calculate_shares_in_given_bonds_out_up_safe(curve_bonds_in)?) } else { - fixed!(0) + Ok(fixed!(0)) } } @@ -46,14 +46,14 @@ impl State { bond_amount: F, maturity_time: U256, current_time: U256, - ) -> FixedPoint { + ) -> Result { let bond_amount = bond_amount.into(); // Calculate the flat part of the trade let flat = self.calculate_close_short_flat(bond_amount, maturity_time, current_time); // Calculate the curve part of the trade - let curve = self.calculate_close_short_curve(bond_amount, maturity_time, current_time); + let curve = self.calculate_close_short_curve(bond_amount, maturity_time, current_time)?; - flat + curve + Ok(flat + curve) } // Calculates the proceeds in shares of closing a short position. @@ -93,11 +93,11 @@ impl State { /// $$ /// p_max = 1 - \phi_c * (1 - p_0) /// $$ - fn calculate_close_short_max_spot_price(&self) -> FixedPoint { - fixed!(1e18) + fn calculate_close_short_max_spot_price(&self) -> Result { + Ok(fixed!(1e18) - self .curve_fee() - .mul_up(fixed!(1e18) - self.calculate_spot_price()) + .mul_up(fixed!(1e18) - self.calculate_spot_price()?)) } /// Calculates the amount of shares the trader will receive after fees for closing a short @@ -108,52 +108,49 @@ impl State { close_vault_share_price: F, maturity_time: U256, current_time: U256, - ) -> FixedPoint { + ) -> Result { let bond_amount = bond_amount.into(); let open_vault_share_price = open_vault_share_price.into(); let close_vault_share_price = close_vault_share_price.into(); if bond_amount < self.config.minimum_transaction_amount.into() { - // TODO would be nice to return a `Result` here instead of a panic. - panic!("MinimumTransactionAmount: Input amount too low"); + return Err(eyre!("MinimumTransactionAmount: Input amount too low")); } // Ensure that the trader didn't purchase bonds at a negative interest // rate after accounting for fees. let share_curve_delta = - self.calculate_close_short_curve(bond_amount, maturity_time, current_time); + self.calculate_close_short_curve(bond_amount, maturity_time, current_time)?; let bond_reserves_delta = bond_amount .mul_up(self.calculate_normalized_time_remaining(maturity_time, current_time)); let short_curve_spot_price = { let mut state: State = self.clone(); state.info.bond_reserves -= bond_reserves_delta.into(); state.info.share_reserves += share_curve_delta.into(); - state.calculate_spot_price() + state.calculate_spot_price()? }; - let max_spot_price = self.calculate_close_short_max_spot_price(); + let max_spot_price = self.calculate_close_short_max_spot_price()?; if short_curve_spot_price > max_spot_price { - // TODO would be nice to return a `Result` here instead of a panic. - panic!("InsufficientLiquidity: Negative Interest"); + return Err(eyre!("InsufficientLiquidity: Negative Interest")); } // Ensure ending spot price is less than one - let curve_fee = self.close_short_curve_fee(bond_amount, maturity_time, current_time); + let curve_fee = self.close_short_curve_fee(bond_amount, maturity_time, current_time)?; let share_curve_delta_with_fees = share_curve_delta + curve_fee - self.close_short_governance_fee( bond_amount, maturity_time, current_time, Some(curve_fee), - ); + )?; let share_curve_delta_with_fees_spot_price = { let mut state: State = self.clone(); state.info.bond_reserves -= bond_reserves_delta.into(); state.info.share_reserves += share_curve_delta_with_fees.into(); - state.calculate_spot_price() + state.calculate_spot_price()? }; if share_curve_delta_with_fees_spot_price > fixed!(1e18) { - // TODO would be nice to return a `Result` here instead of a panic. - panic!("InsufficientLiquidity: Negative Interest"); + return Err(eyre!("InsufficientLiquidity: Negative Interest")); } // Now calculate short proceeds @@ -161,21 +158,21 @@ impl State { // rework to avoid recalculating the curve and bond reserves // https://github.com/delvtech/hyperdrive/issues/943 let share_reserves_delta = - self.calculate_close_short_flat_plus_curve(bond_amount, maturity_time, current_time); + self.calculate_close_short_flat_plus_curve(bond_amount, maturity_time, current_time)?; // Calculate flat + curve and subtract the fees from the trade. let share_reserves_delta_with_fees = share_reserves_delta - + self.close_short_curve_fee(bond_amount, maturity_time, current_time) + + self.close_short_curve_fee(bond_amount, maturity_time, current_time)? + self.close_short_flat_fee(bond_amount, maturity_time, current_time); // Calculate the share proceeds owed to the short. - self.calculate_short_proceeds( + Ok(self.calculate_short_proceeds( bond_amount, share_reserves_delta_with_fees, open_vault_share_price, close_vault_share_price, self.vault_share_price(), self.flat_fee(), - ) + )) } } @@ -183,7 +180,6 @@ impl State { mod tests { use std::panic; - use eyre::Result; use hyperdrive_test_utils::{chain::TestChain, constants::FAST_FUZZ_RUNS}; use rand::{thread_rng, Rng}; @@ -255,7 +251,7 @@ mod tests { match chain .mock_hyperdrive_math() .calculate_close_short( - state.effective_share_reserves().into(), + state.effective_share_reserves()?.into(), state.bond_reserves().into(), in_.into(), normalized_time_remaining.into(), @@ -266,8 +262,8 @@ mod tests { .call() .await { - Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected.2)), - Err(_) => assert!(actual.is_err()), + Ok(expected) => assert_eq!(actual.unwrap().unwrap(), FixedPoint::from(expected.2)), + Err(_) => assert!(actual.is_err() || actual.unwrap().is_err()), } } @@ -279,15 +275,13 @@ mod tests { async fn test_close_short_min_txn_amount() -> Result<()> { let mut rng = thread_rng(); let state = rng.gen::(); - let result = std::panic::catch_unwind(|| { - state.calculate_close_short( - (state.config.minimum_transaction_amount - 10).into(), - state.calculate_spot_price(), - state.vault_share_price(), - 0.into(), - 0.into(), - ) - }); + let result = state.calculate_close_short( + (state.config.minimum_transaction_amount - 10).into(), + state.calculate_spot_price()?, + state.vault_share_price(), + 0.into(), + 0.into(), + ); assert!(result.is_err()); Ok(()) } diff --git a/crates/hyperdrive-math/src/short/fees.rs b/crates/hyperdrive-math/src/short/fees.rs index 16e3d87f..cb849a86 100644 --- a/crates/hyperdrive-math/src/short/fees.rs +++ b/crates/hyperdrive-math/src/short/fees.rs @@ -1,4 +1,5 @@ use ethers::types::U256; +use eyre::Result; use fixed_point::{fixed, FixedPoint}; use crate::State; @@ -13,11 +14,12 @@ impl State { /// \Phi_{c,os}(\Delta y) = \phi_c \cdot (1 - p) \cdot \Delta y /// $$ /// - pub fn open_short_curve_fee(&self, bond_amount: FixedPoint) -> FixedPoint { + pub fn open_short_curve_fee(&self, bond_amount: FixedPoint) -> Result { // NOTE: Round up to overestimate the curve fee. - self.curve_fee() - .mul_up(fixed!(1e18) - self.calculate_spot_price()) - .mul_up(bond_amount) + Ok(self + .curve_fee() + .mul_up(fixed!(1e18) - self.calculate_spot_price()?) + .mul_up(bond_amount)) } /// Calculates the governance fee paid when opening shorts with a given bond amount. @@ -32,13 +34,13 @@ impl State { &self, bond_amount: FixedPoint, maybe_curve_fee: Option, - ) -> FixedPoint { + ) -> Result { let curve_fee = match maybe_curve_fee { Some(maybe_curve_fee) => maybe_curve_fee, - None => self.open_short_curve_fee(bond_amount), + None => self.open_short_curve_fee(bond_amount)?, }; // NOTE: Round down to underestimate the governance fee. - curve_fee.mul_down(self.governance_lp_fee()) + Ok(curve_fee.mul_down(self.governance_lp_fee())) } /// Calculates the curve fee paid when opening shorts with a given bond amount. @@ -56,14 +58,15 @@ impl State { bond_amount: FixedPoint, maturity_time: U256, current_time: U256, - ) -> FixedPoint { + ) -> Result { let normalized_time_remaining = self.calculate_normalized_time_remaining(maturity_time, current_time); // NOTE: Round up to overestimate the curve fee. - self.curve_fee() - .mul_up(fixed!(1e18) - self.calculate_spot_price()) + Ok(self + .curve_fee() + .mul_up(fixed!(1e18) - self.calculate_spot_price()?) .mul_up(bond_amount) - .mul_div_up(normalized_time_remaining, self.vault_share_price()) + .mul_div_up(normalized_time_remaining, self.vault_share_price())) } /// Calculate the governance fee paid when closing shorts with a given bond amount. @@ -82,13 +85,13 @@ impl State { maturity_time: U256, current_time: U256, maybe_curve_fee: Option, - ) -> FixedPoint { + ) -> Result { let curve_fee = match maybe_curve_fee { Some(maybe_curve_fee) => maybe_curve_fee, - None => self.close_short_curve_fee(bond_amount, maturity_time, current_time), + None => self.close_short_curve_fee(bond_amount, maturity_time, current_time)?, }; // NOTE: Round down to underestimate the governance fee. - curve_fee.mul_down(self.governance_lp_fee()) + Ok(curve_fee.mul_down(self.governance_lp_fee())) } /// Calculate the flat fee paid when closing shorts with a given bond amount. diff --git a/crates/hyperdrive-math/src/short/max.rs b/crates/hyperdrive-math/src/short/max.rs index e94d1520..d83a0984 100644 --- a/crates/hyperdrive-math/src/short/max.rs +++ b/crates/hyperdrive-math/src/short/max.rs @@ -1,6 +1,5 @@ -use std::cmp::Ordering; - use ethers::types::I256; +use eyre::{eyre, Result}; use fixed_point::{fixed, FixedPoint}; use crate::{calculate_effective_share_reserves, State, YieldSpace}; @@ -26,12 +25,13 @@ impl State { /// $$ /// p = \left( \tfrac{\mu \cdot z_{min}}{y_{max}} \right)^{t_s} /// $$ - pub fn calculate_min_price(&self) -> FixedPoint { - let y_max = (self.k_up() + pub fn calculate_min_price(&self) -> Result { + let y_max = (self.k_up()? - (self.vault_share_price() / self.initial_vault_share_price()) * (self.initial_vault_share_price() * self.minimum_share_reserves()) - .pow(fixed!(1e18) - self.time_stretch())) - .pow(fixed!(1e18).div_up(fixed!(1e18) - self.time_stretch())); + .pow(fixed!(1e18) - self.time_stretch())?) + .pow(fixed!(1e18).div_up(fixed!(1e18) - self.time_stretch()))?; + ((self.initial_vault_share_price() * self.minimum_share_reserves()) / y_max) .pow(self.time_stretch()) } @@ -56,20 +56,20 @@ impl State { checkpoint_exposure: I, maybe_conservative_price: Option, // TODO: Is there a nice way of abstracting the inner type? maybe_max_iterations: Option, - ) -> FixedPoint { + ) -> Result { let budget = budget.into(); let open_vault_share_price = open_vault_share_price.into(); let checkpoint_exposure = checkpoint_exposure.into(); // If the budget is zero, then we return early. if budget == fixed!(0) { - return fixed!(0); + return Ok(fixed!(0)); } // Calculate the spot price and the open share price. If the open share price // is zero, then we'll use the current share price since the checkpoint // hasn't been minted yet. - let spot_price = self.calculate_spot_price(); + let spot_price = self.calculate_spot_price()?; let open_vault_share_price = if open_vault_share_price != fixed!(0) { open_vault_share_price } else { @@ -80,15 +80,15 @@ impl State { // can be opened. If the short satisfies the budget, this is the max // short amount. let mut max_bond_amount = - self.absolute_max_short(spot_price, checkpoint_exposure, maybe_max_iterations); + self.absolute_max_short(spot_price, checkpoint_exposure, maybe_max_iterations)?; let absolute_max_bond_amount = max_bond_amount; let absolute_max_deposit = match self.calculate_open_short(max_bond_amount, open_vault_share_price) { Ok(d) => d, - Err(_) => return max_bond_amount, + Err(_) => return Ok(max_bond_amount), }; if absolute_max_deposit <= budget { - return max_bond_amount; + return Ok(max_bond_amount); } // Use Newton's method to iteratively approach a solution. We use the @@ -145,36 +145,35 @@ impl State { // Iteratively update max_bond_amount via newton's method. let derivative = - self.short_deposit_derivative(max_bond_amount, spot_price, open_vault_share_price); - match deposit.cmp(&target_budget) { - Ordering::Less => max_bond_amount += (target_budget - deposit) / derivative, - Ordering::Greater => max_bond_amount -= (deposit - target_budget) / derivative, - Ordering::Equal => { - // If we find the exact solution, we set the result and stop - best_valid_max_bond_amount = max_bond_amount; - break; - } + self.short_deposit_derivative(max_bond_amount, spot_price, open_vault_share_price)?; + if deposit < target_budget { + max_bond_amount += (target_budget - deposit) / derivative + } else if deposit > target_budget { + max_bond_amount -= (deposit - target_budget) / derivative + } else { + // If we find the exact solution, we set the result and stop + best_valid_max_bond_amount = max_bond_amount; + break; } + // TODO this always iterates for max_iterations unless // it makes the pool insolvent. Likely want to check an // epsilon to early break } // Verify that the max short satisfies the budget. - if budget - < self - .calculate_open_short(best_valid_max_bond_amount, open_vault_share_price) - .unwrap() - { - panic!("max short exceeded budget"); + if budget < self.calculate_open_short(best_valid_max_bond_amount, open_vault_share_price)? { + return Err(eyre!("max short exceeded budget")); } // Ensure that the max bond amount is within the absolute max bond amount. if best_valid_max_bond_amount > absolute_max_bond_amount { - panic!("max short bond amount exceeded absolute max bond amount"); + return Err(eyre!( + "max short bond amount exceeded absolute max bond amount" + )); } - best_valid_max_bond_amount + Ok(best_valid_max_bond_amount) } /// Calculates an initial guess for the max short calculation. @@ -252,7 +251,7 @@ impl State { spot_price: FixedPoint, checkpoint_exposure: I256, maybe_max_iterations: Option, - ) -> FixedPoint { + ) -> Result { // We start by calculating the maximum short that can be opened on the // YieldSpace curve. let absolute_max_bond_amount = { @@ -260,7 +259,7 @@ impl State { // $z - \zeta \geq z_{min}$. Combining these together, we calculate // the optimal share reserves as $z_{optimal} = z_{min} + max(0, \zeta)$. let optimal_share_reserves = self.minimum_share_reserves() - + FixedPoint::from(self.share_adjustment().max(I256::zero())); + + FixedPoint::try_from(self.share_adjustment().max(I256::zero()))?; // We calculate the optimal bond reserves by solving for the bond // reserves that is implied by the optimal share reserves. We can do @@ -269,9 +268,11 @@ impl State { // k = (c / mu) * (mu * (z' - zeta)) ** (1 - t_s) + y' ** (1 - t_s) // => // y' = (k - (c / mu) * (mu * (z' - zeta)) ** (1 - t_s)) ** (1 / (1 - t_s)) - let optimal_effective_share_reserves = - calculate_effective_share_reserves(optimal_share_reserves, self.share_adjustment()); - let optimal_bond_reserves = self.k_down() + let optimal_effective_share_reserves = calculate_effective_share_reserves( + optimal_share_reserves, + self.share_adjustment(), + )?; + let optimal_bond_reserves = self.k_down()? - self .vault_share_price() .div_up(self.initial_vault_share_price()) @@ -279,23 +280,24 @@ impl State { (self .initial_vault_share_price() .mul_up(optimal_effective_share_reserves)) - .pow(fixed!(1e18) - self.time_stretch()), + .pow(fixed!(1e18) - self.time_stretch())?, ); let optimal_bond_reserves = if optimal_bond_reserves >= fixed!(1e18) { // Rounding the exponent down results in a smaller outcome. - optimal_bond_reserves.pow(fixed!(1e18) / (fixed!(1e18) - self.time_stretch())) + optimal_bond_reserves.pow(fixed!(1e18) / (fixed!(1e18) - self.time_stretch()))? } else { // Rounding the exponent up results in a smaller outcome. - optimal_bond_reserves.pow(fixed!(1e18).div_up(fixed!(1e18) - self.time_stretch())) + optimal_bond_reserves + .pow(fixed!(1e18).div_up(fixed!(1e18) - self.time_stretch()))? }; optimal_bond_reserves - self.bond_reserves() }; if self - .solvency_after_short(absolute_max_bond_amount, checkpoint_exposure) + .solvency_after_short(absolute_max_bond_amount, checkpoint_exposure)? .is_some() { - return absolute_max_bond_amount; + return Ok(absolute_max_bond_amount); } // Use Newton's method to iteratively approach a solution. We use pool's @@ -315,12 +317,11 @@ impl State { // // The guess that we make is very important in determining how quickly // we converge to the solution. - let mut max_bond_amount = self.absolute_max_short_guess(spot_price, checkpoint_exposure); - let mut maybe_solvency = self.solvency_after_short(max_bond_amount, checkpoint_exposure); - if maybe_solvency.is_none() { - panic!("Initial guess in `absolute_max_short` is insolvent."); - } - let mut solvency = maybe_solvency.unwrap(); + let mut max_bond_amount = self.absolute_max_short_guess(spot_price, checkpoint_exposure)?; + let mut solvency = match self.solvency_after_short(max_bond_amount, checkpoint_exposure)? { + Some(solvency) => solvency, + None => return Err(eyre!("Initial guess in `absolute_max_short` is insolvent.")), + }; for _ in 0..maybe_max_iterations.unwrap_or(7) { // TODO: It may be better to gracefully handle crossing over the // root by extending the fixed point math library to handle negative @@ -331,7 +332,7 @@ impl State { // is larger than the absolute max, we've gone too far and something // has gone wrong. let maybe_derivative = - self.solvency_after_short_derivative(max_bond_amount, spot_price); + self.solvency_after_short_derivative(max_bond_amount, spot_price)?; if maybe_derivative.is_none() { break; } @@ -342,17 +343,17 @@ impl State { // If the candidate is insolvent, we've gone too far and can stop // iterating. Otherwise, we update our guess and continue. - maybe_solvency = - self.solvency_after_short(possible_max_bond_amount, checkpoint_exposure); - if let Some(s) = maybe_solvency { - solvency = s; - max_bond_amount = possible_max_bond_amount; - } else { - break; - } + solvency = + match self.solvency_after_short(possible_max_bond_amount, checkpoint_exposure)? { + Some(solvency) => { + max_bond_amount = possible_max_bond_amount; + solvency + } + None => break, + }; } - max_bond_amount + Ok(max_bond_amount) } /// Calculates an initial guess for the absolute max short. This is a conservative @@ -382,13 +383,15 @@ impl State { &self, spot_price: FixedPoint, checkpoint_exposure: I256, - ) -> FixedPoint { + ) -> Result { let estimate_price = spot_price; let checkpoint_exposure = - FixedPoint::from(checkpoint_exposure.max(I256::zero())) / self.vault_share_price(); - (self.vault_share_price() * (self.calculate_solvency() + checkpoint_exposure)) - / (estimate_price - self.curve_fee() * (fixed!(1e18) - spot_price) - + self.governance_lp_fee() * self.curve_fee() * (fixed!(1e18) - spot_price)) + FixedPoint::try_from(checkpoint_exposure.max(I256::zero()))? / self.vault_share_price(); + Ok( + (self.vault_share_price() * (self.calculate_solvency() + checkpoint_exposure)) + / (estimate_price - self.curve_fee() * (fixed!(1e18) - spot_price) + + self.governance_lp_fee() * self.curve_fee() * (fixed!(1e18) - spot_price)), + ) } /// Calculates the pool's solvency after opening a short. @@ -426,26 +429,29 @@ impl State { &self, bond_amount: FixedPoint, checkpoint_exposure: I256, - ) -> Option { + ) -> Result> { let principal = if let Ok(p) = self.calculate_short_principal(bond_amount) { p } else { - return None; + return Ok(None); }; - let curve_fee_base = self.open_short_curve_fee(bond_amount); + let curve_fee_base = self.open_short_curve_fee(bond_amount)?; let share_reserves = self.share_reserves() - (principal - (curve_fee_base - - self.open_short_governance_fee(bond_amount, Some(curve_fee_base))) + - self.open_short_governance_fee(bond_amount, Some(curve_fee_base))?) / self.vault_share_price()); let exposure = { - let checkpoint_exposure: FixedPoint = checkpoint_exposure.max(I256::zero()).into(); + let checkpoint_exposure: FixedPoint = + checkpoint_exposure.max(I256::zero()).try_into()?; (self.long_exposure() - checkpoint_exposure) / self.vault_share_price() }; if share_reserves >= exposure + self.minimum_share_reserves() { - Some(share_reserves - exposure - self.minimum_share_reserves()) + Ok(Some( + share_reserves - exposure - self.minimum_share_reserves(), + )) } else { - None + Ok(None) } } @@ -468,16 +474,16 @@ impl State { &self, bond_amount: FixedPoint, spot_price: FixedPoint, - ) -> Option { - let lhs = self.calculate_short_principal_derivative(bond_amount); + ) -> Result> { + let lhs = self.calculate_short_principal_derivative(bond_amount)?; let rhs = self.curve_fee() * (fixed!(1e18) - spot_price) * (fixed!(1e18) - self.governance_lp_fee()) / self.vault_share_price(); if lhs >= rhs { - Some(lhs - rhs) + Ok(Some(lhs - rhs)) } else { - None + Ok(None) } } } @@ -487,7 +493,6 @@ mod tests { use std::panic; use ethers::types::U256; - use eyre::Result; use fixed_point::uint256; use hyperdrive_test_utils::{ chain::TestChain, @@ -516,7 +521,7 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=fixed!(10_000_000e18)); + let value = rng.gen_range(fixed!(0)..=FixedPoint::try_from(I256::MAX)?); if rng.gen() { -I256::try_from(value).unwrap() } else { @@ -525,6 +530,7 @@ mod tests { }; 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 overflows. let actual = panic::catch_unwind(|| { state.calculate_max_short( U256::MAX, @@ -564,12 +570,12 @@ mod tests { // exact matchces. Related issue: // https://github.com/delvtech/hyperdrive-rs/issues/45 assert_eq!( - U256::from(actual.unwrap()) / uint256!(1e11), + U256::from(actual.unwrap().unwrap()) / uint256!(1e11), expected / uint256!(1e11) ); } Err(_) => { - assert!(actual.is_err()); + assert!(actual.is_err() || actual.unwrap().is_err()); } }; } @@ -637,7 +643,7 @@ mod tests { checkpoint_exposure, None, None, - ); + )?; // It's known that global max short is in units of bonds, // but we fund bob with this amount regardless, since the amount required // for deposit << the global max short number of bonds. @@ -711,7 +717,7 @@ mod tests { checkpoint_exposure, None, None, - ); + )?; // Bob opens a max short position. We allow for a very small amount // of slippage to account for interest accrual between the time the diff --git a/crates/hyperdrive-math/src/short/open.rs b/crates/hyperdrive-math/src/short/open.rs index 03d3f27f..865b51cd 100644 --- a/crates/hyperdrive-math/src/short/open.rs +++ b/crates/hyperdrive-math/src/short/open.rs @@ -48,7 +48,7 @@ impl State { } // NOTE: The order of additions and subtractions is important to avoid underflows. - let spot_price = self.calculate_spot_price(); + let spot_price = self.calculate_spot_price()?; Ok( bond_amount.mul_div_down(self.vault_share_price(), open_vault_share_price) + self.flat_fee() * bond_amount @@ -75,7 +75,7 @@ impl State { bond_amount: FixedPoint, spot_price: FixedPoint, open_vault_share_price: FixedPoint, - ) -> FixedPoint { + ) -> Result { // Theta calculates the inner component of the `short_principal` calculation, // which makes the `short_principal` and `short_deposit_derivative` calculations // easier. $\theta(\Delta y)$ is defined as: @@ -84,16 +84,16 @@ impl State { // \theta(\Delta y) = \tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s}) // $$ let theta = (self.initial_vault_share_price() / self.vault_share_price()) - * (self.k_down() - - (self.bond_reserves() + bond_amount).pow(fixed!(1e18) - self.time_stretch())); + * (self.k_down()? + - (self.bond_reserves() + bond_amount).pow(fixed!(1e18) - self.time_stretch())?); // NOTE: The order of additions and subtractions is important to avoid underflows. let payment_factor = (fixed!(1e18) - / (self.bond_reserves() + bond_amount).pow(self.time_stretch())) - * theta.pow(self.time_stretch() / (fixed!(1e18) - self.time_stretch())); - (self.vault_share_price() / open_vault_share_price) + / (self.bond_reserves() + bond_amount).pow(self.time_stretch())?) + * theta.pow(self.time_stretch() / (fixed!(1e18) - self.time_stretch()))?; + Ok((self.vault_share_price() / open_vault_share_price) + self.flat_fee() + self.curve_fee() * (fixed!(1e18) - spot_price) - - payment_factor + - payment_factor) } /// Calculate an updated pool state after opening a short. @@ -122,10 +122,10 @@ impl State { &self, bond_amount: FixedPoint, ) -> Result { - let curve_fee_base = self.open_short_curve_fee(bond_amount); + let curve_fee_base = self.open_short_curve_fee(bond_amount)?; let curve_fee = curve_fee_base.div_up(self.vault_share_price()); let gov_curve_fee = self - .open_short_governance_fee(bond_amount, Some(curve_fee_base)) + .open_short_governance_fee(bond_amount, Some(curve_fee_base))? .div_up(self.vault_share_price()); let short_principal = self.calculate_shares_out_given_bonds_in_down_safe(bond_amount)?; if short_principal.mul_up(self.vault_share_price()) > bond_amount { @@ -149,7 +149,7 @@ impl State { }; let updated_state = self.calculate_pool_state_after_open_short(bond_amount, Some(shares_amount))?; - Ok(updated_state.calculate_spot_price()) + updated_state.calculate_spot_price() } /// Calculate the spot rate after a short has been opened. @@ -218,7 +218,7 @@ impl State { ) -> Result { let base_paid = self.calculate_open_short(bond_amount, open_vault_share_price)?; let tpy = - (fixed!(1e18) + variable_apy).pow(self.annualized_position_duration()) - fixed!(1e18); + (fixed!(1e18) + variable_apy).pow(self.annualized_position_duration())? - fixed!(1e18); let base_proceeds = bond_amount * tpy; if base_proceeds > base_paid { Ok(I256::try_from( @@ -256,19 +256,22 @@ impl State { /// \tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s}) /// \right)^{\tfrac{t_s}{1 - t_s}} /// $$ - pub fn calculate_short_principal_derivative(&self, bond_amount: FixedPoint) -> FixedPoint { + pub fn calculate_short_principal_derivative( + &self, + bond_amount: FixedPoint, + ) -> Result { let lhs = fixed!(1e18) / (self .vault_share_price() - .mul_up((self.bond_reserves() + bond_amount).pow(self.time_stretch()))); + .mul_up((self.bond_reserves() + bond_amount).pow(self.time_stretch())?)); let rhs = ((self.initial_vault_share_price() / self.vault_share_price()) - * (self.k_down() - - (self.bond_reserves() + bond_amount).pow(fixed!(1e18) - self.time_stretch()))) + * (self.k_down()? + - (self.bond_reserves() + bond_amount).pow(fixed!(1e18) - self.time_stretch())?)) .pow( self.time_stretch() .div_up(fixed!(1e18) - self.time_stretch()), - ); - lhs * rhs + )?; + Ok(lhs * rhs) } } @@ -368,6 +371,7 @@ mod tests { } }; let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price()); + // We need to catch panics because of overflows. let max_bond_amount = match panic::catch_unwind(|| { state.calculate_max_short( U256::MAX, @@ -377,23 +381,25 @@ mod tests { None, ) }) { - Ok(max_bond_amount) => max_bond_amount, - Err(_) => continue, + 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 == fixed!(0) { continue; } let bond_amount = rng.gen_range(state.minimum_transaction_amount()..=max_bond_amount); let actual = state.calculate_pool_deltas_after_open_short(bond_amount); - let curve_fee_base = state.open_short_curve_fee(bond_amount); + let curve_fee_base = state.open_short_curve_fee(bond_amount)?; + let gov_fee = state.open_short_governance_fee(bond_amount, Some(curve_fee_base))?; let fees = curve_fee_base.div_up(state.vault_share_price()) - - state - .open_short_governance_fee(bond_amount, Some(curve_fee_base)) - .div_up(state.vault_share_price()); + - gov_fee.div_up(state.vault_share_price()); match chain .mock_hyperdrive_math() .calculate_open_short( - state.effective_share_reserves().into(), + state.effective_share_reserves()?.into(), state.bond_reserves().into(), bond_amount.into(), state.time_stretch().into(), @@ -410,7 +416,9 @@ mod tests { && expected_with_fees >= actual_with_fees - fixed!(10); assert!(result_equal, "Should be equal."); } - Err(_) => assert!(actual.is_err()), + Err(_) => { + assert!(actual.is_err()) + } }; } Ok(()) @@ -428,7 +436,7 @@ mod tests { match chain .mock_yield_space_math() .calculate_shares_out_given_bonds_in_down_safe( - state.effective_share_reserves().into(), + state.effective_share_reserves()?.into(), state.bond_reserves().into(), bond_amount.into(), (fixed!(1e18) - state.time_stretch()).into(), @@ -463,16 +471,13 @@ mod tests { let state = rng.gen::(); let amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18)); - let p1_result = state.calculate_short_principal(amount - empirical_derivative_epsilon); - - let p1 = match p1_result { + let p1 = match state.calculate_short_principal(amount - empirical_derivative_epsilon) { // If the amount results in the pool being insolvent, skip this iteration Ok(p) => p, Err(_) => continue, }; - let p2_result = state.calculate_short_principal(amount + empirical_derivative_epsilon); - let p2 = match p2_result { + let p2 = match state.calculate_short_principal(amount + empirical_derivative_epsilon) { // If the amount results in the pool being insolvent, skip this iteration Ok(p) => p, Err(_) => continue, @@ -481,7 +486,7 @@ mod tests { assert!(p2 > p1); let empirical_derivative = (p2 - p1) / (fixed!(2e18) * empirical_derivative_epsilon); - let short_principal_derivative = state.calculate_short_principal_derivative(amount); + let short_principal_derivative = state.calculate_short_principal_derivative(amount)?; let derivative_diff; if short_principal_derivative >= empirical_derivative { @@ -519,37 +524,32 @@ mod tests { let state = rng.gen::(); let amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18)); - let p1_result = panic::catch_unwind(|| { + let p1 = match panic::catch_unwind(|| { state.calculate_open_short( amount - empirical_derivative_epsilon, state.vault_share_price(), ) - }); - let p1; - let p2; - match p1_result { - // If the amount results in the pool being insolvent, skip this iteration - Ok(p_panics) => match p_panics { - Ok(p) => p1 = p, - Err(_) => continue, + }) { + Ok(p) => match p { + Ok(p) => p, + Err(_) => continue, // The amount results in the pool being insolvent. }, - Err(_) => continue, - } + Err(_) => continue, // Overflow or underflow error from FixedPoint. + }; - let p2_result = panic::catch_unwind(|| { + let p2 = match panic::catch_unwind(|| { state.calculate_open_short( amount + empirical_derivative_epsilon, state.vault_share_price(), ) - }); - match p2_result { + }) { // If the amount results in the pool being insolvent, skip this iteration - Ok(p_panics) => match p_panics { - Ok(p) => p2 = p, - Err(_) => continue, + Ok(p) => match p { + Ok(p) => p, + Err(_) => continue, // The amount results in the pool being insolvent. }, - Err(_) => continue, - } + Err(_) => continue, // Overflow or underflow error from FixedPoint. + }; // Sanity check assert!(p2 > p1); @@ -560,9 +560,9 @@ mod tests { // Setting open, close, and current vault share price to be equal assumes 0% variable yield. let short_deposit_derivative = state.short_deposit_derivative( amount, - state.calculate_spot_price(), + state.calculate_spot_price()?, state.vault_share_price(), - ); + )?; let derivative_diff; if short_deposit_derivative >= empirical_derivative { @@ -625,7 +625,7 @@ mod tests { // Verify that the predicted spot price is equal to the ending spot // price. These won't be exactly equal because the vault share price // increases between the prediction and opening the short. - let actual_spot_price = bob.get_state().await?.calculate_spot_price(); + let actual_spot_price = bob.get_state().await?.calculate_spot_price()?; let error = if actual_spot_price > expected_spot_price { actual_spot_price - expected_spot_price } else { @@ -658,7 +658,7 @@ mod tests { // allows all actions. let state = rng.gen::(); let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=fixed!(10_000_000e18)); + let value = rng.gen_range(fixed!(0)..=FixedPoint::try_from(I256::MAX)?); if rng.gen() { -I256::try_from(value).unwrap() } else { @@ -666,6 +666,7 @@ mod tests { } }; let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price()); + // We need to catch panics because of overflows. let max_bond_amount = match panic::catch_unwind(|| { state.calculate_max_short( U256::MAX, @@ -675,8 +676,11 @@ mod tests { None, ) }) { - Ok(max_bond_amount) => max_bond_amount, - Err(_) => continue, + Ok(max_bond_amount) => match max_bond_amount { + Ok(max_bond_amount) => max_bond_amount, + Err(_) => continue, // Err; max short insolvent + }, + Err(_) => continue, // panic; likely in FixedPoint }; if max_bond_amount == fixed!(0) { continue; @@ -726,6 +730,10 @@ mod tests { alice.fund(contribution).await?; bob.fund(budget).await?; + // Alice initializes the pool. + // TODO: We'd like to set a random position duration & checkpoint duration. + alice.initialize(fixed_rate, contribution, None).await?; + // Set a random variable rate. let variable_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(1e18)); let vault = MockERC4626::new( @@ -734,10 +742,6 @@ mod tests { ); vault.set_rate(variable_rate.into()).send().await?; - // Alice initializes the pool. - // TODO: We'd like to set a random position duration & checkpoint duration. - alice.initialize(fixed_rate, contribution, None).await?; - // Bob opens a short with a random bond amount. Before opening the // short, we calculate the implied rate. let bond_amount = rng.gen_range( @@ -824,6 +828,7 @@ mod tests { }; 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 overflows. let max_trade = panic::catch_unwind(|| { state.calculate_max_short( U256::MAX, @@ -834,24 +839,28 @@ mod tests { ) }); // Since we're fuzzing it's possible that the max can fail. - // This failure can be an error or a panic. // We're only going to use it in this test if it succeeded. match max_trade { - Ok(max_trade) => { - // TODO: You should be able to add a small amount (e.g. 1e18) to max to fail. - // calc_open_short must be incorrect for the additional amount to have to be so large. - let result = state.calculate_open_short( - max_trade + fixed!(100_000_000e18), - state.vault_share_price(), - ); - match result { - Ok(_) => { - panic!("calculate_open_short should have failed but succeeded.") + Ok(max_trade) => match max_trade { + Ok(max_trade) => { + // TODO: Test that you should be able to add a small amount (e.g. 1e18) to max to fail. + // calc_open_short must be incorrect for the additional amount to have to be so large. + let result = state.calculate_open_short( + max_trade + fixed!(100_000_000e18), + state.vault_share_price(), + ); + match result { + Ok(_) => { + return Err(eyre!( + "calculate_open_short should have failed but succeeded." + )); + } + Err(_) => continue, // Max was fine; open resulted in an Err. } - Err(_) => continue, // Max was fine; open resulted in an Error. } - } - Err(_) => continue, // Max thew a panic (likely due to FixedPoint under/over flow. + Err(_) => continue, // Max threw an Err. + }, + Err(_) => continue, // Max thew an panic, likely due to FixedPoint under/over flow. } } diff --git a/crates/hyperdrive-math/src/test_utils/agent.rs b/crates/hyperdrive-math/src/test_utils/agent.rs index 20c0b390..7b6e8be0 100644 --- a/crates/hyperdrive-math/src/test_utils/agent.rs +++ b/crates/hyperdrive-math/src/test_utils/agent.rs @@ -83,10 +83,9 @@ pub trait HyperdriveMathAgent { impl HyperdriveMathAgent for Agent, ChaCha8Rng> { /// Gets the current state of the pool. async fn get_state(&self) -> Result { - Ok(State::new( - self.get_config().clone(), - self.hyperdrive().get_pool_info().await?, - )) + let pool_config = self.get_config().clone(); + let pool_info = self.hyperdrive().get_pool_info().await?; + Ok(State::new(pool_config, pool_info)) } /// Gets the latest checkpoint. @@ -95,7 +94,7 @@ impl HyperdriveMathAgent for Agent, ChaCha8Rng> { } /// Calculates the spot price. async fn calculate_spot_price(&self) -> Result { - Ok(self.get_state().await?.calculate_spot_price()) + self.get_state().await?.calculate_spot_price() } /// Calculates the long amount that will be opened for a given base amount. @@ -181,24 +180,24 @@ impl HyperdriveMathAgent for Agent, ChaCha8Rng> { // weighted average of the spot price and the minimum possible // spot price the pool can quote. We choose the weights so that this // is an underestimate of the worst case realized price. - let spot_price = state.calculate_spot_price(); - let min_price = state.calculate_min_price(); + let spot_price = state.calculate_spot_price()?; + let min_price = state.calculate_min_price()?; // Calculate the linear interpolation. let base_reserves = FixedPoint::from(state.info.vault_share_price) * (FixedPoint::from(state.info.share_reserves)); let weight = (min(self.wallet.base, base_reserves) / base_reserves) - .pow(fixed!(1e18) - FixedPoint::from(self.get_config().time_stretch)); + .pow(fixed!(1e18) - FixedPoint::from(self.get_config().time_stretch))?; spot_price * (fixed!(1e18) - weight) + min_price * weight }; - Ok(state.calculate_max_short( + state.calculate_max_short( budget, open_vault_share_price, checkpoint_exposure, Some(conservative_price), None, - )) + ) } #[instrument(skip(self))] diff --git a/crates/hyperdrive-math/src/test_utils/integration_tests.rs b/crates/hyperdrive-math/src/test_utils/integration_tests.rs index b202ec01..dca19848 100644 --- a/crates/hyperdrive-math/src/test_utils/integration_tests.rs +++ b/crates/hyperdrive-math/src/test_utils/integration_tests.rs @@ -121,7 +121,7 @@ mod tests { checkpoint_exposure, None, None, - ); + )?; let budget = bob.base(); let slippage_tolerance = fixed!(0.001e18); let max_short = bob.calculate_max_short(Some(slippage_tolerance)).await?; @@ -186,7 +186,7 @@ mod tests { // considering fees. // 2. The pool's solvency is close to zero. // 3. Bob's budget is consumed. - let max_spot_price = bob.get_state().await?.calculate_max_spot_price(); + let max_spot_price = bob.get_state().await?.calculate_max_spot_price()?; let max_long = bob.calculate_max_long(None).await?; let spot_price_after_long = bob .get_state() diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index b6098338..15df333e 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -2,7 +2,10 @@ use ethers::types::{I256, U256}; use eyre::{eyre, Result}; use fixed_point::{fixed, uint256, FixedPoint}; -pub fn calculate_time_stretch(rate: FixedPoint, position_duration: FixedPoint) -> FixedPoint { +pub fn calculate_time_stretch( + rate: FixedPoint, + position_duration: FixedPoint, +) -> Result { let seconds_in_a_year = FixedPoint::from(U256::from(60 * 60 * 24 * 365)); // Calculate the benchmark time stretch. This time stretch is tuned for // a position duration of 1 year. @@ -27,33 +30,33 @@ pub fn calculate_time_stretch(rate: FixedPoint, position_duration: FixedPoint) - // ) * timeStretch // // NOTE: Round down so that the output is an underestimate. - (FixedPoint::from(FixedPoint::ln( - I256::try_from(fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year)) - .unwrap(), - )) / FixedPoint::from(FixedPoint::ln(I256::try_from(fixed!(1e18) + rate).unwrap()))) - * time_stretch + Ok((FixedPoint::try_from(FixedPoint::ln(I256::try_from( + fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year), + )?)?)? + / FixedPoint::try_from(FixedPoint::ln(I256::try_from(fixed!(1e18) + rate)?)?)?) + * time_stretch) } pub fn calculate_effective_share_reserves( share_reserves: FixedPoint, share_adjustment: I256, -) -> FixedPoint { - let effective_share_reserves = I256::try_from(share_reserves).unwrap() - share_adjustment; +) -> Result { + let effective_share_reserves = I256::try_from(share_reserves)? - share_adjustment; if effective_share_reserves < I256::from(0) { - panic!("effective share reserves cannot be negative"); + return Err(eyre!("effective share reserves cannot be negative")); } - effective_share_reserves.into() + effective_share_reserves.try_into() } pub fn calculate_effective_share_reserves_safe( share_reserves: FixedPoint, share_adjustment: I256, ) -> Result { - let effective_share_reserves = I256::try_from(share_reserves).unwrap() - share_adjustment; + let effective_share_reserves = I256::try_from(share_reserves)? - share_adjustment; if effective_share_reserves < I256::from(0) { return Err(eyre!("effective share reserves cannot be negative")); } - Ok(effective_share_reserves.into()) + effective_share_reserves.try_into() } /// Calculates the bond reserves assuming that the pool has a given @@ -84,7 +87,7 @@ pub fn calculate_bonds_given_effective_shares_and_rate( initial_vault_share_price: FixedPoint, position_duration: FixedPoint, time_stretch: FixedPoint, -) -> FixedPoint { +) -> Result { // NOTE: Round down to underestimate the initial bond reserves. // // Normalize the time to maturity to fractions of a year since the provided @@ -97,18 +100,18 @@ pub fn calculate_bonds_given_effective_shares_and_rate( let mut inner = fixed!(1e18) + target_rate.mul_down(t); if inner >= fixed!(1e18) { // Rounding down the exponent results in a smaller result. - inner = inner.pow(fixed!(1e18) / time_stretch); + inner = inner.pow(fixed!(1e18) / time_stretch)?; } else { // Rounding up the exponent results in a smaller result. - inner = inner.pow(fixed!(1e18).div_up(time_stretch)); + inner = inner.pow(fixed!(1e18).div_up(time_stretch))?; } // NOTE: Round down to underestimate the initial bond reserves. // // mu * (z - zeta) * (1 + apr * t) ** (1 / tau) - initial_vault_share_price + Ok(initial_vault_share_price .mul_down(effective_share_reserves) - .mul_down(inner) + .mul_down(inner)) } /// Calculate the rate assuming a given price is constant for some annualized duration. @@ -158,10 +161,13 @@ pub fn calculate_hpr_given_apr(apr: FixedPoint, position_duration: FixedPoint) - /// /// where $t$ is the holding period, in units of years. For example, if the /// holding period is 6 months, then $t=0.5$. -pub fn calculate_hpr_given_apy(apy: FixedPoint, position_duration: FixedPoint) -> FixedPoint { +pub fn calculate_hpr_given_apy( + apy: FixedPoint, + position_duration: FixedPoint, +) -> Result { let holding_period_in_years = position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365)); - (fixed!(1e18) + apy).pow(holding_period_in_years) - fixed!(1e18) + Ok((fixed!(1e18) + apy).pow(holding_period_in_years)? - fixed!(1e18)) } #[cfg(test)] @@ -199,9 +205,9 @@ mod tests { .await { Ok(expected_t) => { - assert_eq!(actual_t, FixedPoint::from(expected_t)); + assert_eq!(actual_t.unwrap(), FixedPoint::from(expected_t)); } - Err(_) => panic!("Test failed."), + Err(_) => assert!(actual_t.is_err()), } } @@ -215,7 +221,7 @@ mod tests { // Gen the random state. let state = rng.gen::(); let checkpoint_exposure = { - let value = rng.gen_range(fixed!(0)..=FixedPoint::from(I256::MAX)); + let value = rng.gen_range(fixed!(0)..=FixedPoint::try_from(I256::MAX)?); let sign = rng.gen::(); if sign { -I256::try_from(value).unwrap() @@ -226,18 +232,20 @@ mod tests { let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price()); // Get the min rate. + // We need to catch panics because of overflows. 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, + Err(_) => continue, // Max threw an Err. Don't finish this fuzz iteration. }, - Err(_) => continue, // Don't finish this fuzz iteration. + Err(_) => continue, // Max threw a paanic. Don't finish this fuzz iteration. }; let min_rate = state.calculate_spot_rate_after_long(max_long, None)?; // 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, @@ -247,8 +255,11 @@ mod tests { None, ) }) { - Ok(max_short) => max_short, - Err(_) => continue, // Don't finish this fuzz iteration. + Ok(max_short) => match max_short { + Ok(max_short) => max_short, + Err(_) => continue, // Max threw an Err; don't finish this fuzz iteration. + }, + Err(_) => continue, // Max threw a panic; don't finish this fuzz iteration. }; let max_rate = state.calculate_spot_rate_after_short(max_short, None)?; @@ -257,17 +268,17 @@ mod tests { // Calculate the new bond reserves. let bond_reserves = calculate_bonds_given_effective_shares_and_rate( - state.effective_share_reserves(), + state.effective_share_reserves()?, target_rate, state.initial_vault_share_price(), state.position_duration(), state.time_stretch(), - ); + )?; // Make a new state with the updated reserves & check the spot rate. let mut new_state: State = state.clone(); new_state.info.bond_reserves = bond_reserves.into(); - assert_eq!(new_state.calculate_spot_rate(), target_rate) + assert_eq!(new_state.calculate_spot_rate()?, target_rate) } Ok(()) } diff --git a/crates/hyperdrive-math/src/yield_space.rs b/crates/hyperdrive-math/src/yield_space.rs index 54d231de..5d7c5d73 100644 --- a/crates/hyperdrive-math/src/yield_space.rs +++ b/crates/hyperdrive-math/src/yield_space.rs @@ -8,7 +8,7 @@ pub trait YieldSpace { /// Info /// /// The effective share reserves. - fn ze(&self) -> FixedPoint { + fn ze(&self) -> Result { calculate_effective_share_reserves(self.z(), self.zeta()) } @@ -32,23 +32,23 @@ pub trait YieldSpace { /// Core /// - fn calculate_spot_price(&self) -> FixedPoint { - ((self.mu() * self.ze()) / self.y()).pow(self.t()) + fn calculate_spot_price(&self) -> Result { + ((self.mu() * self.ze()?) / self.y()).pow(self.t()) } /// Calculates the amount of bonds a user will receive from the pool by /// providing a specified amount of shares. We underestimate the amount of /// bonds out to prevent sandwiches. - fn calculate_bonds_out_given_shares_in_down(&self, dz: FixedPoint) -> FixedPoint { + fn calculate_bonds_out_given_shares_in_down(&self, dz: FixedPoint) -> Result { // NOTE: We round k up to make the rhs of the equation larger. // // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) - let k = self.k_up(); + let k = self.k_up()?; // NOTE: We round z down to make the rhs of the equation larger. // // (µ * (ze + dz))^(1 - t) - let mut ze = (self.mu() * (self.ze() + dz)).pow(fixed!(1e18) - self.t()); + let mut ze = (self.mu() * (self.ze()? + dz)).pow(fixed!(1e18) - self.t())?; // (c / µ) * (µ * (ze + dz))^(1 - t) ze = self.c().mul_div_down(ze, self.mu()); @@ -58,14 +58,14 @@ pub trait YieldSpace { let mut y = k - ze; if y >= fixed!(1e18) { // Rounding up the exponent results in a larger result. - y = y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); + y = y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?; } else { // Rounding down the exponent results in a larger result. - y = y.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + y = y.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?; } // Δy = y - (k - (c / µ) * (µ * (z + dz))^(1 - t))^(1 / (1 - t))) - self.y() - y + Ok(self.y() - y) } /// Calculates the amount of shares a user must provide the pool to receive @@ -74,7 +74,7 @@ pub trait YieldSpace { // NOTE: We round k up to make the lhs of the equation larger. // // k = (c / µ) * (µ * z)^(1 - t) + y^(1 - t) - let k = self.k_up(); + let k = self.k_up()?; // (y - dy)^(1 - t) if self.y() < dy { @@ -84,7 +84,7 @@ pub trait YieldSpace { dy, )); } - let y = (self.y() - dy).pow(fixed!(1e18) - self.t()); + let y = (self.y() - dy).pow(fixed!(1e18) - self.t())?; // NOTE: We round _z up to make the lhs of the equation larger. // @@ -99,35 +99,35 @@ pub trait YieldSpace { let mut _z = (k - y).mul_div_up(self.mu(), self.c()); if _z >= fixed!(1e18) { // Rounding up the exponent results in a larger result. - _z = _z.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); + _z = _z.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?; } else { // Rounding down the exponent results in a larger result. - _z = _z.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + _z = _z.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?; } // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ _z = _z.div_up(self.mu()); // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze - if _z < self.ze() { + if _z < self.ze()? { return Err(eyre!( "calculate_shares_in_given_bonds_out_up_safe: _z = {} < {} = ze", _z, - self.ze(), + self.ze()?, )); } - Ok(_z - self.ze()) + Ok(_z - self.ze()?) } /// Calculates the amount of shares a user must provide the pool to receive /// a specified amount of bonds. We underestimate the amount of shares in. - fn calculate_shares_in_given_bonds_out_down(&self, dy: FixedPoint) -> FixedPoint { + fn calculate_shares_in_given_bonds_out_down(&self, dy: FixedPoint) -> Result { // NOTE: We round k down to make the lhs of the equation smaller. // // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) - let k = self.k_down(); + let k = self.k_down()?; // (y - dy)^(1 - t) - let y = (self.y() - dy).pow(fixed!(1e18) - self.t()); + let y = (self.y() - dy).pow(fixed!(1e18) - self.t())?; // NOTE: We round _ze down to make the lhs of the equation smaller. // @@ -135,38 +135,36 @@ pub trait YieldSpace { let mut ze = (k - y).mul_div_down(self.mu(), self.c()); if ze >= fixed!(1e18) { // Rounding down the exponent results in a smaller result. - ze = ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + ze = ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?; } else { // Rounding up the exponent results in a smaller result. - ze = ze.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); + ze = ze.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?; } // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ ze /= self.mu(); // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze - ze - self.ze() + Ok(ze - self.ze()?) } /// Calculates the amount of shares a user will receive from the pool by /// providing a specified amount of bonds. This function reverts if an /// integer overflow or underflow occurs. We underestimate the amount of /// shares out. - fn calculate_shares_out_given_bonds_in_down(&self, dy: FixedPoint) -> FixedPoint { + fn calculate_shares_out_given_bonds_in_down(&self, dy: FixedPoint) -> Result { self.calculate_shares_out_given_bonds_in_down_safe(dy) - .unwrap() } /// Calculates the amount of shares a user will receive from the pool by - /// providing a specified amount of bonds. This function returns a Result - /// instead of panicking. We underestimate the amount of shares out. + /// providing a specified amount of bonds. We underestimate the amount of shares out. fn calculate_shares_out_given_bonds_in_down_safe(&self, dy: FixedPoint) -> Result { // NOTE: We round k up to make the rhs of the equation larger. // // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) - let k = self.k_up(); + let k = self.k_up()?; // (y + dy)^(1 - t) - let y = (self.y() + dy).pow(fixed!(1e18) - self.t()); + let y = (self.y() + dy).pow(fixed!(1e18) - self.t())?; // If k is less than y, we return with a failure flag. if k < y { @@ -183,17 +181,17 @@ pub trait YieldSpace { let mut ze = (k - y).mul_div_up(self.mu(), self.c()); if ze >= fixed!(1e18) { // Rounding the exponent up results in a larger outcome. - ze = ze.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); + ze = ze.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?; } else { // Rounding the exponent down results in a larger outcome. - ze = ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + ze = ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?; } // ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ ze = ze.div_up(self.mu()); // Δz = ze - ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) / µ - if self.ze() > ze { - Ok(self.ze() - ze) + if self.ze()? > ze { + Ok(self.ze()? - ze) } else { Ok(fixed!(0)) } @@ -212,26 +210,26 @@ pub trait YieldSpace { // This gives us the maximum share reserves of: // // ze' = (1 / mu) * (k / ((c / mu) + 1)) ** (1 / (1 - tau)). - let k = self.k_down(); + let k = self.k_down()?; let mut optimal_ze = k.div_down(self.c().div_up(self.mu()) + fixed!(1e18)); if optimal_ze >= fixed!(1e18) { // Rounding the exponent up results in a larger outcome. - optimal_ze = optimal_ze.pow(fixed!(1e18).div_down(fixed!(1e18) - self.t())); + optimal_ze = optimal_ze.pow(fixed!(1e18).div_down(fixed!(1e18) - self.t()))?; } else { // Rounding the exponent down results in a larger outcome. - optimal_ze = optimal_ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + optimal_ze = optimal_ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?; } optimal_ze = optimal_ze.div_down(self.mu()); // The optimal trade size is given by dz = ze' - ze. If the calculation // underflows, we return a failure flag. - if optimal_ze >= self.ze() { - Ok(optimal_ze - self.ze()) + if optimal_ze >= self.ze()? { + Ok(optimal_ze - self.ze()?) } else { Err(eyre!( "calculate_max_buy_shares_in_safe: optimal_ze = {} < {} = ze", optimal_ze, - self.ze(), + self.ze()?, )) } } @@ -247,14 +245,14 @@ pub trait YieldSpace { // gives us the maximum bond reserves of // y' = (k / ((c / mu) + 1)) ** (1 / (1 - tau)) and the maximum share // reserves of ze' = y/mu. - let k = self.k_up(); + let k = self.k_up()?; let mut optimal_y = k.div_up(self.c() / self.mu() + fixed!(1e18)); if optimal_y >= fixed!(1e18) { // Rounding the exponent up results in a larger outcome. - optimal_y = optimal_y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); + optimal_y = optimal_y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?; } else { // Rounding the exponent down results in a larger outcome. - optimal_y = optimal_y.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + optimal_y = optimal_y.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?; } // The optimal trade size is given by dy = y - y'. If the calculation @@ -279,7 +277,7 @@ pub trait YieldSpace { // fall below the minimum share reserves. Otherwise, the minimum share // reserves is just zMin. if self.zeta() < I256::zero() { - z_min += FixedPoint::from(-self.zeta()); + z_min += FixedPoint::try_from(-self.zeta())?; } // We solve for the maximum sell using the constraint that the pool's @@ -288,17 +286,17 @@ pub trait YieldSpace { // k = (c / mu) * (mu * (zMin)) ** (1 - tau) + y' ** (1 - tau), and // gives us the maximum bond reserves of // y' = (k - (c / mu) * (mu * (zMin)) ** (1 - tau)) ** (1 / (1 - tau)). - let k = self.k_down(); + let k = self.k_down()?; let mut optimal_y = k - self.c().mul_div_up( - self.mu().mul_up(z_min).pow(fixed!(1e18) - self.t()), + self.mu().mul_up(z_min).pow(fixed!(1e18) - self.t())?, self.mu(), ); if optimal_y >= fixed!(1e18) { // Rounding the exponent down results in a smaller outcome. - optimal_y = optimal_y.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + optimal_y = optimal_y.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?; } else { // Rounding the exponent up results in a smaller outcome. - optimal_y = optimal_y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); + optimal_y = optimal_y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?; } // The optimal trade size is given by dy = y' - y. If this subtraction @@ -319,11 +317,11 @@ pub trait YieldSpace { /// k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) /// /// This variant of the calculation overestimates the result. - fn k_up(&self) -> FixedPoint { - self.c().mul_div_up( - (self.mu().mul_up(self.ze())).pow(fixed!(1e18) - self.t()), + fn k_up(&self) -> Result { + Ok(self.c().mul_div_up( + (self.mu().mul_up(self.ze()?)).pow(fixed!(1e18) - self.t())?, self.mu(), - ) + self.y().pow(fixed!(1e18) - self.t()) + ) + self.y().pow(fixed!(1e18) - self.t())?) } /// Calculates the YieldSpace invariant k. This invariant is given by: @@ -331,11 +329,11 @@ pub trait YieldSpace { /// k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) /// /// This variant of the calculation underestimates the result. - fn k_down(&self) -> FixedPoint { - self.c().mul_div_down( - (self.mu() * self.ze()).pow(fixed!(1e18) - self.t()), + fn k_down(&self) -> Result { + Ok(self.c().mul_div_down( + (self.mu() * self.ze()?).pow(fixed!(1e18) - self.t())?, self.mu(), - ) + self.y().pow(fixed!(1e18) - self.t()) + ) + self.y().pow(fixed!(1e18) - self.t())?) } } @@ -358,12 +356,13 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); let in_ = rng.gen::(); + // We need to catch panics because of overflows. let actual = panic::catch_unwind(|| state.calculate_bonds_out_given_shares_in_down(in_)); match chain .mock_yield_space_math() .calculate_bonds_out_given_shares_in_down( - state.ze().into(), + state.ze()?.into(), state.y().into(), in_.into(), (fixed!(1e18) - state.t()).into(), @@ -373,8 +372,8 @@ mod tests { .call() .await { - Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected)), - Err(_) => assert!(actual.is_err()), + Ok(expected) => assert_eq!(actual.unwrap().unwrap(), FixedPoint::from(expected)), + Err(_) => assert!(actual.is_err() || actual.unwrap().is_err()), } } @@ -394,7 +393,7 @@ mod tests { match chain .mock_yield_space_math() .calculate_shares_in_given_bonds_out_up( - state.ze().into(), + state.ze()?.into(), state.y().into(), in_.into(), (fixed!(1e18) - state.t()).into(), @@ -428,7 +427,7 @@ mod tests { match chain .mock_yield_space_math() .calculate_shares_in_given_bonds_out_down( - state.ze().into(), + state.ze()?.into(), state.y().into(), out.into(), (fixed!(1e18) - state.t()).into(), @@ -439,9 +438,9 @@ mod tests { .await { Ok(expected) => { - assert_eq!(actual.unwrap(), FixedPoint::from(expected)); + assert_eq!(actual.unwrap().unwrap(), FixedPoint::from(expected)); } - Err(_) => assert!(actual.is_err()), + Err(_) => assert!(actual.is_err() || actual.unwrap().is_err()), } } @@ -457,12 +456,11 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); let in_ = rng.gen::(); - let actual = - panic::catch_unwind(|| state.calculate_shares_out_given_bonds_in_down(in_)); + let actual = state.calculate_shares_out_given_bonds_in_down(in_); match chain .mock_yield_space_math() .calculate_shares_out_given_bonds_in_down( - state.ze().into(), + state.ze()?.into(), state.y().into(), in_.into(), (fixed!(1e18) - state.t()).into(), @@ -491,12 +489,11 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); let in_ = rng.gen::(); - let actual = - panic::catch_unwind(|| state.calculate_shares_out_given_bonds_in_down_safe(in_)); + let actual = state.calculate_shares_out_given_bonds_in_down_safe(in_); match chain .mock_yield_space_math() .calculate_shares_out_given_bonds_in_down_safe( - state.ze().into(), + state.ze()?.into(), state.y().into(), in_.into(), (fixed!(1e18) - state.t()).into(), @@ -507,7 +504,6 @@ mod tests { .await { Ok((expected_out, expected_status)) => { - let actual = actual.unwrap(); assert_eq!(actual.is_ok(), expected_status); assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out)); } @@ -526,11 +522,11 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let actual = panic::catch_unwind(|| state.calculate_max_buy_shares_in_safe()); + let actual = state.calculate_max_buy_shares_in_safe(); match chain .mock_yield_space_math() .calculate_max_buy_shares_in_safe( - state.ze().into(), + state.ze()?.into(), state.y().into(), (fixed!(1e18) - state.t()).into(), state.c().into(), @@ -540,7 +536,6 @@ mod tests { .await { Ok((expected_out, expected_status)) => { - let actual = actual.unwrap(); assert_eq!(actual.is_ok(), expected_status); assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out)); } @@ -559,11 +554,11 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let actual = panic::catch_unwind(|| state.calculate_max_buy_bonds_out_safe()); + let actual = state.calculate_max_buy_bonds_out_safe(); match chain .mock_yield_space_math() .calculate_max_buy_bonds_out_safe( - state.ze().into(), + state.ze()?.into(), state.y().into(), (fixed!(1e18) - state.t()).into(), state.c().into(), @@ -573,7 +568,6 @@ mod tests { .await { Ok((expected_out, expected_status)) => { - let actual = actual.unwrap(); assert_eq!(actual.is_ok(), expected_status); assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out)); } @@ -628,11 +622,11 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let actual = panic::catch_unwind(|| state.k_down()); + let actual = state.k_down(); match chain .mock_yield_space_math() .k_down( - state.ze().into(), + state.ze()?.into(), state.y().into(), (fixed!(1e18) - state.t()).into(), state.c().into(), @@ -657,11 +651,11 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let actual = panic::catch_unwind(|| state.k_up()); + let actual = state.k_up(); match chain .mock_yield_space_math() .k_up( - state.ze().into(), + state.ze()?.into(), state.y().into(), (fixed!(1e18) - state.t()).into(), state.c().into(), diff --git a/crates/hyperdrive-test-utils/src/chain/deploy.rs b/crates/hyperdrive-test-utils/src/chain/deploy.rs index 154aab11..831614a6 100644 --- a/crates/hyperdrive-test-utils/src/chain/deploy.rs +++ b/crates/hyperdrive-test-utils/src/chain/deploy.rs @@ -55,7 +55,10 @@ where } // This is defined in hyperdrive-math, but re-defined here to avoid cyclic dependencies. -pub fn calculate_time_stretch(rate: FixedPoint, position_duration: FixedPoint) -> FixedPoint { +pub fn calculate_time_stretch( + rate: FixedPoint, + position_duration: FixedPoint, +) -> Result { let seconds_in_a_year = FixedPoint::from(U256::from(60 * 60 * 24 * 365)); // Calculate the benchmark time stretch. This time stretch is tuned for // a position duration of 1 year. @@ -80,11 +83,11 @@ pub fn calculate_time_stretch(rate: FixedPoint, position_duration: FixedPoint) - // ) * timeStretch // // NOTE: Round down so that the output is an underestimate. - (FixedPoint::from(FixedPoint::ln( - I256::try_from(fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year)) - .unwrap(), - )) / FixedPoint::from(FixedPoint::ln(I256::try_from(fixed!(1e18) + rate).unwrap()))) - * time_stretch + Ok((FixedPoint::try_from(FixedPoint::ln(I256::try_from( + fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year), + )?)?)? + / FixedPoint::try_from(FixedPoint::ln(I256::try_from(fixed!(1e18) + rate)?)?)?) + * time_stretch) } /// A configuration for a test chain that specifies the factory parameters, @@ -333,7 +336,7 @@ impl TestnetDeploy for Chain { time_stretch: calculate_time_stretch( fixed!(0.05e18), U256::from(60 * 60 * 24 * 365).into(), - ) + )? .into(), // time stretch for 5% rate fee_collector: client.address(), sweep_collector: client.address(), @@ -931,7 +934,7 @@ impl TestnetDeploy for Chain { #[cfg(test)] mod tests { use hyperdrive_wrappers::wrappers::ihyperdrive::IHyperdrive; - use test_utils::{chain::Chain, constants::ALICE}; + use test_utils::constants::ALICE; use super::*; use crate::chain::TestChain; @@ -975,7 +978,7 @@ mod tests { test_chain_config .erc4626_hyperdrive_position_duration .into(), - ) + )? .into() ); assert_eq!(config.governance, test_chain_config.admin); @@ -1012,7 +1015,7 @@ mod tests { calculate_time_stretch( test_chain_config.steth_hyperdrive_time_stretch_apr.into(), test_chain_config.steth_hyperdrive_position_duration.into(), - ) + )? .into() ); assert_eq!(config.governance, test_chain_config.admin); @@ -1088,7 +1091,7 @@ mod tests { test_chain_config .erc4626_hyperdrive_position_duration .into(), - ) + )? .into() ); assert_eq!(config.governance, test_chain_config.admin); @@ -1125,7 +1128,7 @@ mod tests { calculate_time_stretch( test_chain_config.steth_hyperdrive_time_stretch_apr.into(), test_chain_config.steth_hyperdrive_position_duration.into(), - ) + )? .into() ); assert_eq!(config.governance, test_chain_config.admin);