Skip to content

Commit

Permalink
add short proceeds test
Browse files Browse the repository at this point in the history
  • Loading branch information
dpaiton committed May 29, 2024
1 parent e9901a3 commit f046c80
Showing 1 changed file with 151 additions and 78 deletions.
229 changes: 151 additions & 78 deletions crates/hyperdrive-math/src/short/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ impl State {

let share_reserves_delta = self.calculate_short_principal(bond_amount)?;
// 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.
// 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 {
return Err(eyre!("InsufficientLiquidity: Negative Interest",));
}
Expand All @@ -77,7 +77,8 @@ impl State {
// don't benefit from negative interest that accrued during the current
// checkpoint.
let curve_fee = self.open_short_curve_fee(bond_amount)?;
// Use the position duraiton as the current time and maturity time to simulate closing at maturity.
// Use the position duraiton as the current time and maturity time to
// simulate closing at maturity.
let flat_fee = self.close_short_flat_fee(
bond_amount,
self.position_duration().into(),
Expand All @@ -86,8 +87,8 @@ impl State {

if share_reserves_delta < curve_fee {
return Err(eyre!(format!(
"The transaction curve fee = {} from curve fee coefficient = {}
is too high, it must be less than share reserves delta = {}",
"The transaction curve fee = {}, computed with coefficient = {},
is too high. It must be less than share reserves delta = {}",
curve_fee,
self.curve_fee(),
share_reserves_delta
Expand Down Expand Up @@ -452,6 +453,9 @@ mod tests {
use crate::test_utils::agent::HyperdriveMathAgent;

/// Executes random trades throughout a Hyperdrive term.
///
/// This fn initializes a Hyperdrive pool and does random trades
/// to force the pool into a random state.
async fn preamble(
rng: &mut ChaCha8Rng,
alice: &mut Agent<ChainClient<LocalWallet>, ChaCha8Rng>,
Expand All @@ -476,17 +480,15 @@ mod tests {
let mut time_remaining = alice.get_config().position_duration;
while time_remaining > uint256!(0) {
// Bob opens a long.
let discount = rng.gen_range(fixed!(0.1e18)..=fixed!(0.5e18));
let long_amount =
rng.gen_range(fixed!(1e12)..=bob.calculate_max_long(None).await? * discount);
let min_txn =
FixedPoint::from(alice.get_state().await?.config.minimum_transaction_amount);
let max_long = fixed!(1e26); // 100M max
let long_amount = rng.gen_range(min_txn..=max_long);
bob.open_long(long_amount, None, None).await?;

// Celine opens a short.
let discount = rng.gen_range(fixed!(0.1e18)..=fixed!(0.5e18));
let min_short =
FixedPoint::from(alice.get_state().await?.config.minimum_transaction_amount);
let max_short = celine.calculate_max_short(None).await? * discount;
let short_amount = rng.gen_range(min_short..=max_short);
let max_short = fixed!(1e26); // 100M max
let short_amount = rng.gen_range(min_txn..=max_short);
celine.open_short(short_amount, None, None).await?;

// Advance the time and mint all of the intermediate checkpoints.
Expand All @@ -511,6 +513,73 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn test_calculate_short_proceeds_up() -> Result<()> {
// TODO: make an array of values & run the tests in a loop.

let chain = TestChain::new().await?;
let mut rng = thread_rng();
let mut alice = chain.alice().await?;
let mut bob = chain.bob().await?;

// Snapshot the chain.
// let id = chain.snapshot().await?;

alice
.fund(rng.gen_range(fixed!(1_000e18)..=fixed!(500_000_000e18)))
.await?;
bob.fund(rng.gen_range(fixed!(1_000e18)..=fixed!(500_000_000e18)))
.await?;

let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
alice.initialize(fixed_rate, alice.base(), None).await?;
let state = alice.get_state().await?;

let bond_amount = fixed!(1.05e18);
let share_amount = fixed!(1e18);
let open_vault_share_price = fixed!(1e18);
let close_vault_share_price = fixed!(1e18);
let vault_share_price = fixed!(1e18);
let flat_fee = fixed!(0);

let actual = panic::catch_unwind(|| {
state.calculate_short_proceeds_up(
bond_amount,
share_amount,
open_vault_share_price,
close_vault_share_price,
vault_share_price,
flat_fee,
)
});

match chain
.mock_hyperdrive_math()
.calculate_short_proceeds_up(
bond_amount.into(),
share_amount.into(),
open_vault_share_price.into(),
close_vault_share_price.into(),
vault_share_price.into(),
flat_fee.into(),
)
.call()
.await
{
Ok(expected_proceeds) => {
assert_eq!(actual.unwrap(), expected_proceeds.into())
}
Err(_) => assert!(actual.is_err()),
}

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

Ok(())
}

#[tokio::test]
async fn test_calculate_pool_deltas_after_open_short() -> Result<()> {
let chain = TestChain::new().await?;
Expand Down Expand Up @@ -1053,76 +1122,80 @@ mod tests {
// Snapshot the chain.
let id = chain.snapshot().await?;

// Run the preamble.
// Run the preamble, but only test on valid states.
let fixed_rate = fixed!(0.05e18);
preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await?;

// Get state and trade details.
let state = alice.get_state().await?;
let Checkpoint {
vault_share_price: open_vault_share_price,
weighted_spot_price: _,
last_weighted_spot_price_update_time: _,
} = alice
.get_checkpoint(state.to_checkpoint(alice.now().await?))
.await?;
let slippage_tolerance = fixed!(0.001e18);
let max_short = bob.calculate_max_short(Some(slippage_tolerance)).await?;
// Minimum transaction amount is without units; it's a global min across all transaction types.
let min_short = FixedPoint::from(state.config.minimum_transaction_amount);
let short_amount;
if max_short < min_short {
continue;
} else if max_short == min_short {
short_amount = min_short;
} else {
short_amount = rng.gen_range(
FixedPoint::from(state.config.minimum_transaction_amount)..=max_short,
);
}
// Compare the open short call output against calculate_open_short.
let actual_base_amount =
state.calculate_open_short(short_amount, open_vault_share_price.into());

match bob
.hyperdrive()
.open_short(
short_amount.into(),
FixedPoint::from(U256::MAX).into(),
fixed!(0).into(),
Options {
destination: bob.address(),
as_base: true,
extra_data: [].into(),
},
)
.call()
.await
{
Ok((_, expected_base_amount)) => {
let actual = actual_base_amount.unwrap();
let error = if actual >= expected_base_amount.into() {
actual - FixedPoint::from(expected_base_amount)
} else {
FixedPoint::from(expected_base_amount) - actual
};
assert!(
error <= tolerance,
"error {} exceeds tolerance of {}",
error,
tolerance
if let Ok(_) = preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await {
// Get state and trade details.
let state = alice.get_state().await?;

// Get the open vault share price.
let Checkpoint {
vault_share_price: open_vault_share_price,
weighted_spot_price: _,
last_weighted_spot_price_update_time: _,
} = alice
.get_checkpoint(state.to_checkpoint(alice.now().await?))
.await?;

// We could use calculate_max_short here, but we don't want to conflate what we are testing.
// So instead we will use a big number and only run the test if it is valid.
let max_short = fixed!(1e26); // 100M max bonds

// Minimum transaction amount is without units; it's a global min across all transaction types.
let min_txn = FixedPoint::from(state.config.minimum_transaction_amount);
let bond_amount;
if max_short < min_txn {
continue;
} else if max_short == min_txn {
bond_amount = min_txn;
} else {
bond_amount = rng.gen_range(
FixedPoint::from(state.config.minimum_transaction_amount)..=max_short,
);
}
Err(_) => assert!(actual_base_amount.is_err()),
}
// Compare the open short call output against calculate_open_short.
let actual_base_amount =
state.calculate_open_short(bond_amount, open_vault_share_price.into());

match bob
.hyperdrive()
.open_short(
bond_amount.into(),
FixedPoint::from(U256::MAX).into(),
fixed!(0).into(),
Options {
destination: bob.address(),
as_base: true,
extra_data: [].into(),
},
)
.call()
.await
{
Ok((_, expected_base_amount)) => {
let actual = actual_base_amount.unwrap();
let error = if actual >= expected_base_amount.into() {
actual - FixedPoint::from(expected_base_amount)
} else {
FixedPoint::from(expected_base_amount) - actual
};
assert!(
error <= tolerance,
"error {} exceeds tolerance of {}",
error,
tolerance
);
}
Err(_) => assert!(actual_base_amount.is_err()),
}

// 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?;
// 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(())
}
}

0 comments on commit f046c80

Please sign in to comment.