Skip to content

Commit

Permalink
Fix calculate_open_long negative interest check, fix `get_spot_pric…
Browse files Browse the repository at this point in the history
…e_after_short` (#919)

* Use `spot_price_after_long` in `calculate_open_long`

* Include fees in get_spot_price_after_short

* Fix argument order

* Fix function call after rebase

* Delete moved test

* Fix rust-analyzer error

* Consolidate spot_price_after_open_short fns, add test

* Remove redundant import
  • Loading branch information
ryangoree authored Apr 3, 2024
1 parent bc92d73 commit 99e5eeb
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 91 deletions.
8 changes: 2 additions & 6 deletions crates/hyperdrive-math/src/long/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,8 @@ impl State {
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 = {
let mut state: State = self.clone();
state.info.bond_reserves -= long_amount.into();
state.info.share_reserves += (base_amount / self.vault_share_price()).into();
state.calculate_spot_price()
};
let ending_spot_price =
self.calculate_spot_price_after_long(base_amount, long_amount.into());
let max_spot_price = self.calculate_max_spot_price();
if ending_spot_price > max_spot_price {
// TODO would be nice to return a `Result` here instead of a panic.
Expand Down
149 changes: 77 additions & 72 deletions crates/hyperdrive-math/src/short/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,24 @@ impl State {
)
}

/// Calculates the spot price after opening the short on the YieldSpace curve and
/// before calculating the fees.
pub fn calculate_spot_price_after_short(&self, bond_amount: FixedPoint) -> FixedPoint {
let shares_amount = self.calculate_shares_out_given_bonds_in_down(bond_amount);
self.spot_price_after_short(shares_amount * self.vault_share_price(), bond_amount)
}

fn spot_price_after_short(
/// Calculates the spot price after opening a Hyperdrive short.
pub fn calculate_spot_price_after_short(
&self,
base_amount: FixedPoint,
bond_amount: FixedPoint,
base_amount: Option<FixedPoint>,
) -> FixedPoint {
let shares_amount = match base_amount {
Some(base_amount) => base_amount / self.vault_share_price(),
None => {
let spot_price = self.calculate_spot_price();
self.calculate_shares_out_given_bonds_in_down(bond_amount)
- self.open_short_curve_fee(bond_amount, spot_price)
+ self.open_short_governance_fee(bond_amount, spot_price)
}
};
let mut state: State = self.clone();
state.info.bond_reserves += bond_amount.into();
state.info.share_reserves -= (base_amount / state.vault_share_price()).into();
state.info.share_reserves -= shares_amount.into();
state.calculate_spot_price()
}

Expand All @@ -85,75 +88,77 @@ impl State {

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

use ethers::types::{I256, U256};
use hyperdrive_wrappers::wrappers::mock_hyperdrive_math::MaxTradeParams;
use fixed_point_macros::fixed;
use rand::{thread_rng, Rng};
use test_utils::{chain::TestChainWithMocks, constants::FAST_FUZZ_RUNS};
use test_utils::{
agent::Agent,
chain::{Chain, TestChain},
constants::FUZZ_RUNS,
};

use super::*;

/// This test differentially fuzzes the `calculate_max_short` function against the
/// Solidity analogue `calculateMaxShort`. `calculateMaxShort` doesn't take
/// a trader's budget into account, so it only provides a subset of
/// `calculate_max_short`'s functionality. With this in mind, we provide
/// `calculate_max_short` with a budget of `U256::MAX` to ensure that the two
/// functions are equivalent.
#[tokio::test]
async fn fuzz_calculate_max_short_no_budget() -> Result<()> {
let chain = TestChainWithMocks::new(1).await?;

// Fuzz the rust and solidity implementations against each other.
async fn fuzz_calculate_spot_price_after_short() -> Result<()> {
// Spawn a test chain and create two agents -- Alice and Bob. Alice is
// funded with a large amount of capital so that she can initialize the
// pool. Bob is funded with a small amount of capital so that we can
// test opening a short and verify that the ending spot price is what we
// expect.
let mut rng = thread_rng();
for _ in 0..*FAST_FUZZ_RUNS {
let state = rng.gen::<State>();
let checkpoint_exposure = {
let value = rng.gen_range(fixed!(0)..=fixed!(10_000_000e18));
if rng.gen() {
-I256::try_from(value).unwrap()
} else {
I256::try_from(value).unwrap()
}
let chain = TestChain::new(2).await?;
let (alice, bob) = (chain.accounts()[0].clone(), chain.accounts()[1].clone());
let mut alice =
Agent::new(chain.client(alice).await?, chain.addresses().clone(), None).await?;
let mut bob = Agent::new(chain.client(bob).await?, chain.addresses(), None).await?;

for _ in 0..*FUZZ_RUNS {
// Snapshot the chain.
let id = chain.snapshot().await?;

// Fund Alice and Bob.
let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
alice.fund(contribution).await?;
bob.fund(budget).await?;

// Alice initializes the pool.
alice.initialize(fixed_rate, contribution, None).await?;

// Attempt to predict the spot price after opening a short.
let short_amount =
rng.gen_range(fixed!(0.01e18)..=bob.calculate_max_short(None).await?);
let current_state = bob.get_state().await?;
let expected_spot_price =
current_state.calculate_spot_price_after_short(short_amount, None);

// Open the short.
bob.open_short(short_amount, None, None).await?;

// 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 delta = if actual_spot_price > expected_spot_price {
actual_spot_price - expected_spot_price
} else {
expected_spot_price - actual_spot_price
};
let max_iterations = 7;
let actual = panic::catch_unwind(|| {
state.calculate_max_short(
U256::MAX,
fixed!(0),
checkpoint_exposure,
None,
Some(max_iterations),
)
});
match chain
.mock_hyperdrive_math()
.calculate_max_short(
MaxTradeParams {
share_reserves: state.info.share_reserves,
bond_reserves: state.info.bond_reserves,
longs_outstanding: state.info.longs_outstanding,
long_exposure: state.info.long_exposure,
share_adjustment: state.info.share_adjustment,
time_stretch: state.config.time_stretch,
vault_share_price: state.info.vault_share_price,
initial_vault_share_price: state.config.initial_vault_share_price,
minimum_share_reserves: state.config.minimum_share_reserves,
curve_fee: state.config.fees.curve,
flat_fee: state.config.fees.flat,
governance_lp_fee: state.config.fees.governance_lp,
},
checkpoint_exposure,
max_iterations.into(),
)
.call()
.await
{
Ok(expected) => {
assert_eq!(actual.unwrap(), FixedPoint::from(expected));
}
Err(_) => assert!(actual.is_err()),
}
// TODO: Why can't this pass with a tolerance of 1e9?
let tolerance = fixed!(1e10);

assert!(
delta < tolerance,
"expected: delta = {} < {} = tolerance",
delta,
tolerance
);

// Revert to the snapshot and reset the agent's wallets.
chain.revert(id).await?;
alice.reset(Default::default());
bob.reset(Default::default());
}

Ok(())
Expand Down
18 changes: 5 additions & 13 deletions crates/test-utils/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -947,10 +947,7 @@ impl Agent<ChainClient, ChaCha8Rng> {
let Checkpoint {
vault_share_price: open_vault_share_price,
..
} = self
.hyperdrive
.get_checkpoint(state.to_checkpoint(self.now().await?))
.await?;
} = self.get_checkpoint(self.latest_checkpoint().await?).await?;
state.calculate_open_short(
short_amount,
state.calculate_spot_price(),
Expand Down Expand Up @@ -983,18 +980,13 @@ impl Agent<ChainClient, ChaCha8Rng> {
let budget =
self.wallet.base * (fixed!(1e18) - maybe_slippage_tolerance.unwrap_or(fixed!(0.01e18)));

let state = self.get_state().await?;
let latest_checkpoint = self.latest_checkpoint().await?;
let Checkpoint {
vault_share_price: open_vault_share_price,
..
} = self
.hyperdrive
.get_checkpoint(state.to_checkpoint(self.now().await?))
.await?;
let checkpoint_exposure = self
.hyperdrive
.get_checkpoint_exposure(state.to_checkpoint(self.now().await?))
.await?;
} = self.get_checkpoint(latest_checkpoint).await?;
let checkpoint_exposure = self.get_checkpoint_exposure(latest_checkpoint).await?;
let state = self.get_state().await?;

// We linearly interpolate between the current spot price and the minimum
// price that the pool can support. This is a conservative estimate of
Expand Down

0 comments on commit 99e5eeb

Please sign in to comment.