Skip to content

Commit

Permalink
fix open long delta calculations and add tests (#178)
Browse files Browse the repository at this point in the history
# Resolved Issues
working towards #171 and
#21

# Description
- The code for calculating pool deltas & state after opening a long did
not correctly account for the gov fee's impact on the share reserves. I
fixed this and added a test to demonstrate the failure
- I added a similar test on the short side, which almost passes with
equality (seems there is a rounding error)
- I renamed some functions, cleaned up some comments, fixed some typos
  • Loading branch information
dpaiton authored Jul 22, 2024
1 parent 1c15b58 commit 2d56ace
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 50 deletions.
6 changes: 3 additions & 3 deletions bindings/hyperdrivepy/python/hyperdrivepy/hyperdrive_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def calculate_pool_deltas_after_open_long(
pool_config: types.PoolConfigType,
pool_info: types.PoolInfoType,
base_amount: str,
) -> str:
) -> tuple[str, str]:
"""Calculate the bond deltas to be applied to the pool after opening a long.
Arguments
Expand All @@ -237,8 +237,8 @@ def calculate_pool_deltas_after_open_long(
Returns
-------
str (FixedPoint)
The amount of bonds to remove from the pool reserves.
(str (FixedPoint), str (FixedPoint))
The (shares, bonds) deltas to apply to the pool state.
"""
return _get_interface(pool_config, pool_info).calculate_pool_deltas_after_open_long(base_amount)

Expand Down
14 changes: 9 additions & 5 deletions bindings/hyperdrivepy/src/hyperdrive_state_methods/long/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,28 @@ impl HyperdriveState {
Ok(result)
}

pub fn calculate_pool_deltas_after_open_long(&self, base_amount: &str) -> PyResult<String> {
pub fn calculate_pool_deltas_after_open_long(
&self,
base_amount: &str,
) -> PyResult<(String, String)> {
let base_amount_fp = FixedPoint::from(U256::from_dec_str(base_amount).map_err(|err| {
PyErr::new::<PyValueError, _>(format!(
"Failed to convert base_amount string {} to U256: {}",
base_amount, err
))
})?);
let result_fp = self
let (share_result_fp, bond_result_fp) = self
.state
.calculate_pool_deltas_after_open_long(base_amount_fp)
.calculate_pool_share_bond_deltas_after_open_long(base_amount_fp, None)
.map_err(|err| {
PyErr::new::<PyValueError, _>(format!(
"calculate_pool_deltas_after_open_long: {:?}",
err
))
})?;
let result = U256::from(result_fp).to_string();
Ok(result)
let share_result = U256::from(share_result_fp).to_string();
let bond_result = U256::from(bond_result_fp).to_string();
Ok((share_result, bond_result))
}

pub fn calculate_spot_price_after_long(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl HyperdriveState {
})?);
let result_fp = self
.state
.calculate_pool_deltas_after_open_short(bond_amount_fp)
.calculate_pool_share_deltas_after_open_short(bond_amount_fp)
.map_err(|err| {
PyErr::new::<PyValueError, _>(format!(
"calculate_pool_deltas_after_open_short: {}",
Expand Down
3 changes: 2 additions & 1 deletion crates/hyperdrive-math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ impl State {

/// Calculates the pool reserve levels to achieve a target interest rate.
/// This calculation does not take into account Hyperdrive's solvency
/// constraints or exposure and shouldn't be used directly.
/// constraints, share adjustments, or exposure and shouldn't be used
/// directly.
///
/// The price for a given fixed-rate is given by
/// `$p = \tfrac{1}{r \cdot t + 1}$`, where `$r$` is the fixed-rate and
Expand Down
14 changes: 8 additions & 6 deletions crates/hyperdrive-math/src/long/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ impl State {
.mul_up(base_amount))
}

/// Calculates the governance fee paid when opening longs with a given base amount.
/// Calculates the governance fee paid when opening longs with a given base
/// amount.
///
/// The open long governance fee, `$\Phi_{g,ol}(\Delta x)$`, is paid in base and
/// is given by:
/// The open long governance fee, `$\Phi_{g,ol}(\Delta x)$`, is paid in base
/// and is given by:
///
/// ```math
/// \Phi_{g,ol}(\Delta x) = \phi_g \cdot p \cdot \Phi_{c,ol}(\Delta x)
Expand All @@ -45,10 +46,11 @@ impl State {
.mul_down(self.calculate_spot_price()?))
}

/// Calculates the curve fee paid when closing longs for a given bond amount.
/// Calculates the curve fee paid when closing longs for a given bond
/// amount.
///
/// The the close long curve fee, `$\Phi_{c,cl}(\Delta y)$`, is paid in shares and
/// is given by:
/// The the close long curve fee, `$\Phi_{c,cl}(\Delta y)$`, is paid in
/// shares and is given by:
///
/// ```math
/// \Phi_{c,cl}(\Delta y) =
Expand Down
145 changes: 118 additions & 27 deletions crates/hyperdrive-math/src/long/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,50 +107,50 @@ impl State {

/// Calculate an updated pool state after opening a long.
///
/// For a given base amount and bond amount, base is converted to
/// For a given base delta and bond delta, the base delta is converted to
/// shares and the reserves are updated such that
/// `state.bond_reserves -= bond_amount` and
/// `state.share_reserves += base_amount / vault_share_price`.
/// `state.bond_reserves -= bond_delta` and
/// `state.share_reserves += base_delta / vault_share_price`.
pub fn calculate_pool_state_after_open_long(
&self,
base_amount: FixedPoint,
maybe_bond_amount: Option<FixedPoint>,
maybe_bond_deltas: Option<FixedPoint>,
) -> Result<Self> {
let bond_amount = match maybe_bond_amount {
Some(bond_amount) => bond_amount,
None => self.calculate_open_long(base_amount)?,
};
let (share_deltas, bond_deltas) =
self.calculate_pool_share_bond_deltas_after_open_long(base_amount, maybe_bond_deltas)?;
let mut state = self.clone();
state.info.bond_reserves -= bond_amount.into();
state.info.share_reserves += (base_amount / self.vault_share_price()).into();
state.info.bond_reserves -= bond_deltas.into();
state.info.share_reserves += share_deltas.into();
Ok(state)
}

/// Calculate the share deltas to be applied to the pool after opening a long.
pub fn calculate_pool_deltas_after_open_long(
pub fn calculate_pool_share_bond_deltas_after_open_long(
&self,
base_amount: FixedPoint,
) -> Result<FixedPoint> {
let bond_amount = self.calculate_open_long(base_amount)?;
Ok(bond_amount)
maybe_bond_delta: Option<FixedPoint>,
) -> Result<(FixedPoint, FixedPoint)> {
let bond_delta = match maybe_bond_delta {
Some(delta) => delta,
None => self.calculate_open_long(base_amount)?,
};
let total_gov_curve_fee_shares = self
.open_long_governance_fee(base_amount, None)?
.div_down(self.vault_share_price());
let share_delta =
base_amount.div_down(self.vault_share_price()) - total_gov_curve_fee_shares;
Ok((share_delta, bond_delta))
}

/// Calculates the spot price after opening a Hyperdrive long.
/// If a bond_amount is not provided, then one is estimated using `calculate_open_long`.
pub fn calculate_spot_price_after_long(
&self,
base_amount: FixedPoint,
maybe_bond_amount: Option<FixedPoint>,
maybe_bond_pool_delta: Option<FixedPoint>,
) -> Result<FixedPoint> {
let bond_amount = match maybe_bond_amount {
Some(bond_amount) => bond_amount,
None => self.calculate_open_long(base_amount)?,
};
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())
.into();
let state =
self.calculate_pool_state_after_open_long(base_amount, maybe_bond_pool_delta)?;
state.calculate_spot_price()
}

Expand Down Expand Up @@ -189,7 +189,7 @@ mod tests {
use fixedpointmath::fixed;
use hyperdrive_test_utils::{
chain::TestChain,
constants::{FAST_FUZZ_RUNS, FUZZ_RUNS},
constants::{FAST_FUZZ_RUNS, FUZZ_RUNS, SLOW_FUZZ_RUNS},
};
use hyperdrive_wrappers::wrappers::ihyperdrive::Options;
use rand::{thread_rng, Rng, SeedableRng};
Expand All @@ -200,6 +200,99 @@ mod tests {
agent::HyperdriveMathAgent, preamble::initialize_pool_with_random_state,
};

#[tokio::test]
async fn fuzz_calculate_pool_state_after_open_long() -> Result<()> {
// TODO: We should not need a tolerance.
let share_adjustment_test_tolerance = fixed!(0);
let bond_reserves_test_tolerance = fixed!(1e10);
let share_reserves_test_tolerance = fixed!(1e10);
// Initialize a test chain and agents.
let chain = TestChain::new().await?;
let mut alice = chain.alice().await?;
let mut bob = chain.bob().await?;
let mut celine = chain.celine().await?;
// Set up a random number generator. We use ChaCha8Rng with a randomly
// generated seed, which makes it easy to reproduce test failures given
// the seed.
let mut rng = {
let mut rng = thread_rng();
let seed = rng.gen();
ChaCha8Rng::seed_from_u64(seed)
};
for _ in 0..*SLOW_FUZZ_RUNS {
// Snapshot the chain & run the preamble.
let id = chain.snapshot().await?;
initialize_pool_with_random_state(&mut rng, &mut alice, &mut bob, &mut celine).await?;
// Reset the variable rate to zero; get the state.
alice.advance_time(fixed!(0), fixed!(0)).await?;
let original_state = alice.get_state().await?;
// Get a random long amount.
let checkpoint_exposure = alice
.get_checkpoint_exposure(original_state.to_checkpoint(alice.now().await?))
.await?;
let max_long_amount =
original_state.calculate_max_long(U256::MAX, checkpoint_exposure, None)?;
let base_amount =
rng.gen_range(original_state.minimum_transaction_amount()..=max_long_amount);
// Mock the trade using Rust.
let rust_state =
original_state.calculate_pool_state_after_open_long(base_amount, None)?;
// Execute the trade on the contracts.
bob.fund(base_amount * fixed!(1.5e18)).await?;
bob.open_long(base_amount, None, None).await?;
let sol_state = alice.get_state().await?;
// Check that the results are the same.
let rust_share_adjustment = rust_state.share_adjustment();
let sol_share_adjustment = sol_state.share_adjustment();
let share_adjustment_error = if rust_share_adjustment < sol_share_adjustment {
FixedPoint::try_from(sol_share_adjustment - rust_share_adjustment)?
} else {
FixedPoint::try_from(rust_share_adjustment - sol_share_adjustment)?
};
assert!(
share_adjustment_error <= share_adjustment_test_tolerance,
"expected abs(rust_share_adjustment={}-sol_share_adjustment={})={} <= test_tolerance={}",
rust_share_adjustment, sol_share_adjustment, share_adjustment_error, share_adjustment_test_tolerance
);
let rust_bond_reserves = rust_state.bond_reserves();
let sol_bond_reserves = sol_state.bond_reserves();
let bond_reserves_error = if rust_bond_reserves < sol_bond_reserves {
sol_bond_reserves - rust_bond_reserves
} else {
rust_bond_reserves - sol_bond_reserves
};
assert!(
bond_reserves_error <= bond_reserves_test_tolerance,
"expected abs(rust_bond_reserves={}-sol_bond_reserves={})={} <= test_tolerance={}",
rust_bond_reserves,
sol_bond_reserves,
bond_reserves_error,
bond_reserves_test_tolerance
);
let rust_share_reserves = rust_state.share_reserves();
let sol_share_reserves = sol_state.share_reserves();
let share_reserves_error = if rust_share_reserves < sol_share_reserves {
sol_share_reserves - rust_share_reserves
} else {
rust_share_reserves - sol_share_reserves
};
assert!(
share_reserves_error <= share_reserves_test_tolerance,
"expected abs(rust_share_reserves={}-sol_share_reserves={})={} <= test_tolerance={}",
rust_share_reserves,
sol_share_reserves,
share_reserves_error,
share_reserves_test_tolerance
);
// Revert to the snapshot and reset the agent's wallets.
chain.revert(id).await?;
alice.reset(Default::default()).await?;
bob.reset(Default::default()).await?;
celine.reset(Default::default()).await?;
}
Ok(())
}

#[tokio::test]
async fn fuzz_calculate_spot_price_after_long() -> Result<()> {
// Spawn a test chain and create two agents -- Alice and Bob. Alice
Expand Down Expand Up @@ -258,7 +351,6 @@ mod tests {
alice.reset(Default::default()).await?;
bob.reset(Default::default()).await?;
}

Ok(())
}

Expand Down Expand Up @@ -323,7 +415,6 @@ mod tests {
alice.reset(Default::default()).await?;
bob.reset(Default::default()).await?;
}

Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hyperdrive-math/src/short/max.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ impl State {
bond_amount: FixedPoint,
checkpoint_exposure: I256,
) -> Result<FixedPoint> {
let share_deltas = self.calculate_pool_deltas_after_open_short(bond_amount)?;
let share_deltas = self.calculate_pool_share_deltas_after_open_short(bond_amount)?;
if self.share_reserves() < share_deltas {
return Err(eyre!(
"expected share_reserves={:#?} >= share_deltas={:#?}",
Expand Down
Loading

0 comments on commit 2d56ace

Please sign in to comment.