Skip to content

Commit

Permalink
moves short principal & adds tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dpaiton committed Apr 17, 2024
1 parent ceb9f5b commit 3872077
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 61 deletions.
96 changes: 38 additions & 58 deletions crates/hyperdrive-math/src/short/max.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ethers::types::I256;
use eyre::Result;
use fixed_point::FixedPoint;
use fixed_point_macros::fixed;

Expand Down Expand Up @@ -143,8 +144,14 @@ 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);
let derivative = match self.short_deposit_derivative(
max_bond_amount,
open_vault_share_price,
open_vault_share_price.max(self.vault_share_price()),
) {
Ok(derivative) => derivative,
Err(err) => panic!("Error {}", err),
};
if deposit < target_budget {
max_bond_amount += (target_budget - deposit) / derivative;
} else if deposit > target_budget {
Expand Down Expand Up @@ -401,7 +408,7 @@ impl State {
/// Using this, calculating $D'(x)$ is straightforward:
///
/// $$
/// D'(x) = \tfrac{c}{c_0} - (c \cdot P'(x) - \phi_{curve} \cdot (1 - p)) + \phi_{flat}
/// D'(x) = \tfrac{c1}{c0} + \phi_{flat} - c \cdot P'(x) - P(x)
/// $$
///
/// $$
Expand All @@ -410,19 +417,33 @@ impl State {
fn short_deposit_derivative(
&self,
bond_amount: FixedPoint,
spot_price: FixedPoint,
open_vault_share_price: FixedPoint,
) -> FixedPoint {
// 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()))
* self
.theta(bond_amount)
.pow(self.time_stretch() / (fixed!(1e18) - self.time_stretch()));
(self.vault_share_price() / open_vault_share_price)
+ self.flat_fee()
+ self.curve_fee() * (fixed!(1e18) - spot_price)
- payment_factor
close_vault_share_price: FixedPoint,
) -> Result<FixedPoint> {
println!("close_vault_share_price {:#?}", close_vault_share_price);
println!("open_vault_share_price {:#?}", open_vault_share_price);
println!(
"close / open {:#?}",
close_vault_share_price / open_vault_share_price
);
println!("flat_fee {:#?}", self.flat_fee());
println!(
"close / open - flat_fee {:#?}",
close_vault_share_price / open_vault_share_price - self.flat_fee()
);
println!(
"vault_share_price * short_principal_derivative {:#?}",
self.vault_share_price() * self.short_principal_derivative(bond_amount)
);
println!(
"top - vault_share_price * short_principal_derivative {:#?}",
close_vault_share_price / open_vault_share_price
- self.vault_share_price() * self.short_principal_derivative(bond_amount)
- self.flat_fee()
);
Ok(close_vault_share_price / open_vault_share_price
- self.flat_fee()
- self.vault_share_price() * self.short_principal_derivative(bond_amount))
}

/// Calculates the pool's solvency after opening a short.
Expand Down Expand Up @@ -514,54 +535,13 @@ impl State {
None
}
}

/// Calculates the derivative of the short principal $P(x)$ w.r.t. the amount of
/// bonds that are shorted $x$.
///
/// The derivative is calculated as:
///
/// $$
/// P'(x) = \tfrac{1}{c} \cdot (y + x)^{-t_s} \cdot \left(
/// \tfrac{\mu}{c} \cdot (k - (y + x)^{1 - t_s})
/// \right)^{\tfrac{t_s}{1 - t_s}}
/// $$
fn short_principal_derivative(&self, bond_amount: FixedPoint) -> FixedPoint {
let lhs = fixed!(1e18)
/ (self
.vault_share_price()
.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())))
.pow(
self.time_stretch()
.div_up(fixed!(1e18) - self.time_stretch()),
);
lhs * rhs
}

/// A helper function used in calculating the short deposit.
///
/// This calculates the inner component of the `short_principal` calculation,
/// which makes the `short_principal` and `short_deposit_derivative` calculations
/// easier. $\theta(x)$ is defined as:
///
/// $$
/// \theta(x) = \tfrac{\mu}{c} \cdot (k - (y + x)^{1 - t_s})
/// $$
fn theta(&self, bond_amount: FixedPoint) -> FixedPoint {
(self.initial_vault_share_price() / self.vault_share_price())
* (self.k_down()
- (self.bond_reserves() + bond_amount).pow(fixed!(1e18) - self.time_stretch()))
}
}

#[cfg(test)]
mod tests {
use std::panic;

use ethers::types::U256;
use eyre::Result;
use fixed_point_macros::uint256;
use hyperdrive_wrappers::wrappers::{
ihyperdrive::Checkpoint, mock_hyperdrive_math::MaxTradeParams,
Expand Down Expand Up @@ -695,9 +675,9 @@ mod tests {
let empirical_derivative = (p2 - p1) / (fixed!(2e18) * empirical_derivative_epsilon);
let short_deposit_derivative = state.short_deposit_derivative(
amount,
state.calculate_spot_price(),
state.vault_share_price(),
);
state.vault_share_price(),
)?;

let derivative_diff;
if short_deposit_derivative >= empirical_derivative {
Expand Down
117 changes: 114 additions & 3 deletions crates/hyperdrive-math/src/short/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,17 @@ impl State {
open_vault_share_price = self.vault_share_price();
}

let share_reserves_delta = self.short_principal(bond_amount)?;
// $$
// P(\Delta y) = z - \tfrac{1}{\mu} \cdot (\tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s}))^{\tfrac{1}{1 - t_s}}
// $$
let short_principal = self.short_principal(bond_amount)?;

// NOTE: Round up to make the check stricter.
//
// If the base proceeds of selling the bonds is greater than the bond
// amount, then the trade occurred in the negative interest domain. We
// revert in these pathological cases.
if share_reserves_delta.mul_up(self.vault_share_price()) > bond_amount {
if short_principal.mul_up(self.vault_share_price()) > bond_amount {
return Err(eyre!("InsufficientLiquidity: Negative Interest",));
}

Expand Down Expand Up @@ -82,7 +85,7 @@ impl State {
let base_deposit = self
.calculate_short_proceeds_up(
bond_amount,
share_reserves_delta - curve_fee,
short_principal - curve_fee,
open_vault_share_price,
self.vault_share_price().max(open_vault_share_price),
self.vault_share_price(),
Expand Down Expand Up @@ -211,6 +214,23 @@ impl State {
pub fn short_principal(&self, bond_amount: FixedPoint) -> Result<FixedPoint> {
self.calculate_shares_out_given_bonds_in_down_safe(bond_amount)
}

pub fn short_principal_derivative(&self, bond_amount: FixedPoint) -> FixedPoint {
// Original
let new_reserves = self.bond_reserves() + bond_amount;
let lhs = fixed!(1e18)
/ (self
.vault_share_price()
.mul_up(new_reserves.pow(self.time_stretch())));
let mu_over_c = self.initial_vault_share_price() / self.vault_share_price();
let rhs = (mu_over_c
* (self.k_down() - new_reserves.pow(fixed!(1e18) - self.time_stretch())))
.pow(
self.time_stretch()
.div_up(fixed!(1e18) - self.time_stretch()),
);
lhs * rhs
}
}

#[cfg(test)]
Expand Down Expand Up @@ -291,6 +311,97 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn fuzz_short_principal() -> Result<()> {
// This test is the same as the yield_space.rs `fuzz_calculate_max_buy_shares_in_safe`,
// but is worth having around in case we ever change how we compute short principal.
let chain = TestChain::new().await?;
let mut rng = thread_rng();
// We use the smaller FUZZ_RUNS since this is a duplicate test.
for _ in 0..*FUZZ_RUNS {
let state = rng.gen::<State>();
let bond_amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18));
let actual = state.short_principal(bond_amount);
match chain
.mock_yield_space_math()
.calculate_shares_out_given_bonds_in_down_safe(
state.effective_share_reserves().into(),
state.bond_reserves().into(),
bond_amount.into(),
(fixed!(1e18) - state.time_stretch()).into(),
state.vault_share_price().into(),
state.initial_vault_share_price().into(),
)
.call()
.await
{
Ok((expected, expected_status)) => {
assert_eq!(actual.is_ok(), expected_status);
assert_eq!(actual.unwrap_or(fixed!(0)), expected.into());
}
Err(_) => assert!(actual.is_err()),
}
}
Ok(())
}

/// This test empirically tests `short_principal_derivative` by calling
/// `short_principal` at two points and comparing the empirical result
/// with the output of `short_principal_derivative`.
#[tokio::test]
async fn fuzz_short_principal_derivative() -> Result<()> {
let mut rng = thread_rng();
// We use a relatively large epsilon here due to the underlying fixed point pow
// function not being monotonically increasing.
let empirical_derivative_epsilon = fixed!(1e12);
// TODO pretty big comparison epsilon here
let test_comparison_epsilon = fixed!(1e15);

for _ in 0..*FAST_FUZZ_RUNS {
let state = rng.gen::<State>();
let amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18));

let p1_result = state.short_principal(amount - empirical_derivative_epsilon);
let p1;
let p2;
match p1_result {
// If the amount results in the pool being insolvent, skip this iteration
Ok(p) => p1 = p,
Err(_) => continue,
}

let p2_result = state.short_principal(amount + empirical_derivative_epsilon);
match p2_result {
// If the amount results in the pool being insolvent, skip this iteration
Ok(p) => p2 = p,
Err(_) => continue,
}
// Sanity check
assert!(p2 > p1);

let empirical_derivative = (p2 - p1) / (fixed!(2e18) * empirical_derivative_epsilon);
let short_deposit_derivative = state.short_principal_derivative(amount);

let derivative_diff;
if short_deposit_derivative >= empirical_derivative {
derivative_diff = short_deposit_derivative - empirical_derivative;
} else {
derivative_diff = empirical_derivative - short_deposit_derivative;
}
assert!(
derivative_diff < test_comparison_epsilon,
"expected (derivative_diff={}) < (test_comparison_epsilon={}), \
calculated_derivative={}, emperical_derivative={}",
derivative_diff,
test_comparison_epsilon,
short_deposit_derivative,
empirical_derivative
);
}

Ok(())
}

#[tokio::test]
async fn fuzz_calculate_spot_price_after_short() -> Result<()> {
// Spawn a test chain and create two agents -- Alice and Bob. Alice is
Expand Down

0 comments on commit 3872077

Please sign in to comment.