Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve calculate_max_long and calculate_max_short differential testing #985

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion crates/fixed-point-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, LitFloat, LitInt, Result,
parse_macro_input, Expr, LitFloat, LitInt, Result,
};

struct Number {
Expand Down Expand Up @@ -133,3 +133,20 @@ pub fn fixed(input: TokenStream) -> TokenStream {
let result: [u8; 32] = result.to_u256().into();
quote!(FixedPoint::from([ #(#result),* ])).into()
}

#[proc_macro]
pub fn result(input: TokenStream) -> TokenStream {
let expr = parse_macro_input!(input as Expr);
quote!(
std::panic::catch_unwind(|| #expr).or_else(|panic_info| {
let panic_message = if let Some(s) = panic_info.downcast_ref::<String>() {
s.as_str()
} else if let Some(&s) = panic_info.downcast_ref::<&str>() {
s
} else {
"Operation failed with unknown panic"
};
Err(eyre::eyre!("Operation failed: {}", panic_message))
}))
.into()
}
22 changes: 22 additions & 0 deletions crates/fixed-point/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,28 @@ impl FixedPoint {
}

pub fn div_down(self, other: FixedPoint) -> FixedPoint {
let scale = uint256!(1e18);
let mut adjustment: u64 = 1;

// Dynamically adjust the scale to prevent overflow.
while let (_, true) = self.0.overflowing_mul(scale / adjustment) {
adjustment *= 10;
}

// let adjusted_value = self.mul_div_down(FixedPoint(scale / adjustment), other);
// FixedPoint(adjusted_value.0 * adjustment)

if adjustment > 1 {
let adjusted_value = self.mul_div_down(FixedPoint(scale / adjustment), other);
let less_precise_value = FixedPoint(adjusted_value.0 * adjustment);

println!(
"FixedPointMath: div_down precision adjustment needed to prevent overflow for {} / {}",
self, other
);
println!("\tLess precise value: {}", less_precise_value);
}

self.mul_div_down(fixed!(1e18), other)
}

Expand Down
4 changes: 4 additions & 0 deletions crates/hyperdrive-math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ impl State {
fixed!(0)
}
}
}

// Suppress dead code warnings for the config and info getters.
#[allow(dead_code)]
impl State {
/// Config ///

fn position_duration(&self) -> FixedPoint {
Expand Down
85 changes: 52 additions & 33 deletions crates/hyperdrive-math/src/long/max.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::panic;

use ethers::types::I256;
use eyre::{eyre, Result};
use fixed_point::FixedPoint;
use fixed_point_macros::{fixed, int256};
use fixed_point_macros::{fixed, int256, result};

use crate::{State, YieldSpace};

Expand Down Expand Up @@ -44,13 +47,16 @@ impl State {
budget: F,
checkpoint_exposure: I,
maybe_max_iterations: Option<usize>,
) -> FixedPoint {
) -> Result<FixedPoint> {
let budget = budget.into();
let checkpoint_exposure = checkpoint_exposure.into();

// Calculate the maximum long that brings the spot price to 1. If the pool is
// solvent after opening this long, then we're done.
let (absolute_max_base_amount, absolute_max_bond_amount) = self.absolute_max_long();
let (absolute_max_base_amount, absolute_max_bond_amount) = match self.absolute_max_long() {
Ok(v) => v,
Err(e) => return Err(e),
};
if self
.solvency_after_long(
absolute_max_base_amount,
Expand All @@ -59,7 +65,7 @@ impl State {
)
.is_some()
{
return absolute_max_base_amount.min(budget);
return Ok(absolute_max_base_amount.min(budget));
}

// Use Newton's method to iteratively approach a solution. We use pool's
Expand Down Expand Up @@ -102,7 +108,7 @@ impl State {
// entire budget can be consumed without running into solvency
// constraints.
if max_base_amount >= budget {
return budget;
return Ok(budget);
}

// TODO: It may be better to gracefully handle crossing over the
Expand Down Expand Up @@ -138,16 +144,16 @@ impl State {
panic!("Reached absolute max bond amount in `calculate_max_long`.");
}
if max_base_amount >= budget {
return budget;
return Ok(budget);
}

max_base_amount
Ok(max_base_amount)
}

/// Calculates the largest long that can be opened without buying bonds at a
/// negative interest rate. This calculation does not take Hyperdrive's
/// solvency constraints into account and shouldn't be used directly.
fn absolute_max_long(&self) -> (FixedPoint, FixedPoint) {
fn absolute_max_long(&self) -> Result<(FixedPoint, FixedPoint)> {
// We are targeting the pool's max spot price of:
//
// p_max = (1 - flatFee) / (1 + curveFee * (1 / p_0 - 1) * (1 - flatFee))
Expand Down Expand Up @@ -208,19 +214,26 @@ impl State {
/ (fixed!(1e18) - self.flat_fee()))
.pow(fixed!(1e18).div_up(self.time_stretch()));

let effective_share_reserves = self.effective_share_reserves();
if effective_share_reserves > target_share_reserves {
return Err(eyre!(
"Effective share reserves exceed target share reserves."
));
}

// The absolute max base amount is given by:
//
// absoluteMaxBaseAmount = c * (z_t - z)
let absolute_max_base_amount =
(target_share_reserves - self.effective_share_reserves()) * self.vault_share_price();
(target_share_reserves - effective_share_reserves) * self.vault_share_price();

// The absolute max bond amount is given by:
//
// absoluteMaxBondAmount = (y - y_t) - c(x)
let absolute_max_bond_amount = (self.bond_reserves() - target_bond_reserves)
- self.open_long_curve_fees(absolute_max_base_amount);

(absolute_max_base_amount, absolute_max_bond_amount)
Ok((absolute_max_base_amount, absolute_max_bond_amount))
}

/// Calculates an initial guess of the max long that can be opened. This is a
Expand Down Expand Up @@ -248,7 +261,7 @@ impl State {
* fixed!(0.8e18);
let estimate_price = spot_price * (fixed!(1e18) - t) + fixed!(1e18) * t;

// Recalculate our intial guess using the bootstrapped conservative
// Recalculate our initial guess using the bootstrapped conservative
// estimate of the realized price.
self.max_long_estimate(estimate_price, spot_price, checkpoint_exposure)
}
Expand All @@ -270,7 +283,7 @@ impl State {
///
/// We debit and negative checkpoint exposure from $e_0$ since the
/// global exposure doesn't take into account the negative exposure
/// from non-netted shorts in the checkpoint. These forumulas allow us
/// from non-netted shorts in the checkpoint. These formulas allow us
/// to calculate the approximate ending solvency of:
///
/// $$
Expand Down Expand Up @@ -346,22 +359,25 @@ impl State {
bond_amount: FixedPoint,
checkpoint_exposure: I256,
) -> Option<FixedPoint> {
let governance_fee = self.open_long_governance_fee(base_amount);
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)));
if share_reserves + checkpoint_exposure / self.vault_share_price()
>= exposure / self.vault_share_price() + self.minimum_share_reserves()
{
Some(
share_reserves + checkpoint_exposure / self.vault_share_price()
- exposure / self.vault_share_price()
- self.minimum_share_reserves(),
)
} else {
None
}
result!({
let governance_fee = self.open_long_governance_fee(base_amount);
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)));
if share_reserves + checkpoint_exposure / self.vault_share_price()
>= exposure / self.vault_share_price() + self.minimum_share_reserves()
{
Some(
share_reserves + checkpoint_exposure / self.vault_share_price()
- exposure / self.vault_share_price()
- self.minimum_share_reserves(),
)
} else {
None
}
})
.unwrap_or(None)
}

/// Calculates the negation of the derivative of the pool's solvency with respect
Expand Down Expand Up @@ -480,7 +496,7 @@ mod tests {
let mut rng = thread_rng();
for _ in 0..*FAST_FUZZ_RUNS {
let state = rng.gen::<State>();
let actual = panic::catch_unwind(|| state.absolute_max_long());
let actual = state.absolute_max_long();
match chain
.mock_hyperdrive_math()
.calculate_absolute_max_long(
Expand Down Expand Up @@ -509,7 +525,12 @@ mod tests {
.await
{
Ok((expected_base_amount, expected_bond_amount)) => {
let (actual_base_amount, actual_bond_amount) = actual.unwrap();
let (actual_base_amount, actual_bond_amount) = actual.unwrap_or_else(|err| {
panic!(
"Expected ({}, {}), but got an error: {}",
expected_base_amount, expected_bond_amount, err
)
});
assert_eq!(actual_base_amount, FixedPoint::from(expected_base_amount));
assert_eq!(actual_bond_amount, FixedPoint::from(expected_bond_amount));
}
Expand Down Expand Up @@ -543,9 +564,7 @@ mod tests {
I256::try_from(value).unwrap()
}
};
let actual = panic::catch_unwind(|| {
state.calculate_max_long(U256::MAX, checkpoint_exposure, None)
});
let actual = state.calculate_max_long(U256::MAX, checkpoint_exposure, None);
match chain
.mock_hyperdrive_math()
.calculate_max_long(
Expand Down
2 changes: 1 addition & 1 deletion crates/hyperdrive-math/src/long/targeted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ mod tests {
let max_long =
bob.get_state()
.await?
.calculate_max_long(U256::MAX, I256::from(0), None);
.calculate_max_long(U256::MAX, I256::from(0), None).unwrap();
let long_amount =
(max_long / fixed!(100e18)).max(config.minimum_transaction_amount.into());
bob.fund(long_amount + budget).await?;
Expand Down
2 changes: 1 addition & 1 deletion crates/test-utils/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,7 @@ impl Agent<ChainClient<LocalWallet>, ChaCha8Rng> {
.hyperdrive
.get_checkpoint_exposure(state.to_checkpoint(self.now().await?))
.await?;
Ok(state.calculate_max_long(self.wallet.base, checkpoint_exposure, maybe_max_iterations))
state.calculate_max_long(self.wallet.base, checkpoint_exposure, maybe_max_iterations)
}

/// Gets the long that moves the fixed rate to a target value.
Expand Down
Loading