Skip to content

Commit

Permalink
Rust cleanup; add tests; simplify fn signatures (#990)
Browse files Browse the repository at this point in the history
* remove spot price arg

* fix comment

* rename variables for consistency

* pub agent wallet and hyperdrive

* rename test to reflect fuzzing

* random open vault share price

* adds max open short test

* adds tests and moves short principal derivative
  • Loading branch information
dpaiton authored Apr 19, 2024
1 parent 1d85c7b commit d14c4ef
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 106 deletions.
3 changes: 1 addition & 2 deletions contracts/src/internal/HyperdriveShort.sol
Original file line number Diff line number Diff line change
Expand Up @@ -471,8 +471,7 @@ abstract contract HyperdriveShort is IHyperdriveEvents, HyperdriveLP {
Errors.throwInsufficientLiquidityError();
}

// Calculate the fees charged to the user (curveFee) and the portion
// of those fees that are paid to governance (governanceCurveFee).
// Calculate the current spot price.
uint256 curveFee;
uint256 governanceCurveFee;
uint256 spotPrice = HyperdriveMath.calculateSpotPrice(
Expand Down
8 changes: 4 additions & 4 deletions crates/hyperdrive-math/src/long/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ impl State {
pub fn calculate_spot_price_after_long(
&self,
base_amount: FixedPoint,
bond_amount: Option<FixedPoint>,
maybe_bond_amount: Option<FixedPoint>,
) -> Result<FixedPoint> {
let bond_amount = match bond_amount {
let bond_amount = match maybe_bond_amount {
Some(bond_amount) => bond_amount,
None => self.calculate_open_long(base_amount)?,
};
Expand Down Expand Up @@ -81,10 +81,10 @@ impl State {
pub fn calculate_spot_rate_after_long(
&self,
base_amount: FixedPoint,
bond_amount: Option<FixedPoint>,
maybe_bond_amount: Option<FixedPoint>,
) -> Result<FixedPoint> {
Ok(calculate_rate_given_fixed_price(
self.calculate_spot_price_after_long(base_amount, bond_amount)?,
self.calculate_spot_price_after_long(base_amount, maybe_bond_amount)?,
self.position_duration(),
))
}
Expand Down
8 changes: 4 additions & 4 deletions crates/hyperdrive-math/src/short/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ impl State {
/// Calculates the curve fee paid by the trader when they open a short.
pub fn open_short_curve_fee(
&self,
short_amount: FixedPoint,
bond_amount: FixedPoint,
spot_price: FixedPoint,
) -> FixedPoint {
self.curve_fee() * (fixed!(1e18) - spot_price) * short_amount
self.curve_fee() * (fixed!(1e18) - spot_price) * bond_amount
}

/// Calculates the governance fee paid by the trader when they open a short.
pub fn open_short_governance_fee(
&self,
short_amount: FixedPoint,
bond_amount: FixedPoint,
spot_price: FixedPoint,
) -> FixedPoint {
self.governance_lp_fee() * self.open_short_curve_fee(short_amount, spot_price)
self.governance_lp_fee() * self.open_short_curve_fee(bond_amount, spot_price)
}

/// Calculates the curve fee paid by shorts for a given bond amount.
Expand Down
85 changes: 24 additions & 61 deletions crates/hyperdrive-math/src/short/max.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl State {
self.absolute_max_short(spot_price, checkpoint_exposure, maybe_max_iterations);
let absolute_max_bond_amount = max_bond_amount;
let absolute_max_deposit =
match self.calculate_open_short(max_bond_amount, spot_price, open_vault_share_price) {
match self.calculate_open_short(max_bond_amount, open_vault_share_price) {
Ok(d) => d,
Err(_) => return max_bond_amount,
};
Expand Down Expand Up @@ -125,11 +125,7 @@ impl State {
// where budget is less than this bias.
let target_budget = budget - self.minimum_transaction_amount();
for _ in 0..maybe_max_iterations.unwrap_or(7) {
let deposit = match self.calculate_open_short(
max_bond_amount,
spot_price,
open_vault_share_price,
) {
let deposit = match self.calculate_open_short(max_bond_amount, open_vault_share_price) {
Ok(d) => d,
Err(_) => {
// The pool is insolvent for the guess at this point.
Expand Down Expand Up @@ -166,11 +162,7 @@ impl State {
// Verify that the max short satisfies the budget.
if budget
< self
.calculate_open_short(
best_valid_max_bond_amount,
spot_price,
open_vault_share_price,
)
.calculate_open_short(best_valid_max_bond_amount, open_vault_share_price)
.unwrap()
{
panic!("max short exceeded budget");
Expand Down Expand Up @@ -224,9 +216,7 @@ impl State {
+ self.flat_fee()
+ self.curve_fee() * (fixed!(1e18) - spot_price)
- conservative_price);
if let Ok(deposit) =
self.calculate_open_short(guess, spot_price, open_vault_share_price)
{
if let Ok(deposit) = self.calculate_open_short(guess, open_vault_share_price) {
if budget >= deposit {
return guess;
}
Expand All @@ -243,11 +233,10 @@ impl State {
// subtract these components from the budget to get a better estimate of
// the max bond amount. If subtracting these components results in a
// negative number, we just 0 as our initial guess.
let worst_case_deposit =
match self.calculate_open_short(budget, spot_price, open_vault_share_price) {
Ok(d) => d,
Err(_) => return fixed!(0),
};
let worst_case_deposit = match self.calculate_open_short(budget, open_vault_share_price) {
Ok(d) => d,
Err(_) => return fixed!(0),
};
if budget >= worst_case_deposit {
budget - worst_case_deposit
} else {
Expand Down Expand Up @@ -420,15 +409,15 @@ impl State {
/// $$
fn short_deposit_derivative(
&self,
short_amount: FixedPoint,
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() + short_amount).pow(self.time_stretch()))
/ (self.bond_reserves() + bond_amount).pow(self.time_stretch()))
* self
.theta(short_amount)
.theta(bond_amount)
.pow(self.time_stretch() / (fixed!(1e18) - self.time_stretch()));
(self.vault_share_price() / open_vault_share_price)
+ self.flat_fee()
Expand Down Expand Up @@ -469,19 +458,19 @@ impl State {
/// $$
fn solvency_after_short(
&self,
short_amount: FixedPoint,
bond_amount: FixedPoint,
spot_price: FixedPoint,
checkpoint_exposure: I256,
) -> Option<FixedPoint> {
let principal = if let Ok(p) = self.short_principal(short_amount) {
let principal = if let Ok(p) = self.calculate_short_principal(bond_amount) {
p
} else {
return None;
};
let share_reserves = self.share_reserves()
- (principal
- (self.open_short_curve_fee(short_amount, spot_price)
- self.open_short_governance_fee(short_amount, spot_price))
- (self.open_short_curve_fee(bond_amount, spot_price)
- self.open_short_governance_fee(bond_amount, spot_price))
/ self.vault_share_price());
let exposure = {
let checkpoint_exposure: FixedPoint = checkpoint_exposure.max(I256::zero()).into();
Expand Down Expand Up @@ -511,10 +500,10 @@ impl State {
/// doesn't support negative values.
fn solvency_after_short_derivative(
&self,
short_amount: FixedPoint,
bond_amount: FixedPoint,
spot_price: FixedPoint,
) -> Option<FixedPoint> {
let lhs = self.short_principal_derivative(short_amount);
let lhs = self.calculate_short_principal_derivative(bond_amount);
let rhs = self.curve_fee()
* (fixed!(1e18) - spot_price)
* (fixed!(1e18) - self.governance_lp_fee())
Expand All @@ -526,31 +515,6 @@ impl State {
}
}

/// 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, short_amount: FixedPoint) -> FixedPoint {
let lhs = fixed!(1e18)
/ (self
.vault_share_price()
.mul_up((self.bond_reserves() + short_amount).pow(self.time_stretch())));
let rhs = ((self.initial_vault_share_price() / self.vault_share_price())
* (self.k_down()
- (self.bond_reserves() + short_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,
Expand All @@ -560,10 +524,10 @@ impl State {
/// $$
/// \theta(x) = \tfrac{\mu}{c} \cdot (k - (y + x)^{1 - t_s})
/// $$
fn theta(&self, short_amount: FixedPoint) -> FixedPoint {
fn theta(&self, bond_amount: FixedPoint) -> FixedPoint {
(self.initial_vault_share_price() / self.vault_share_price())
* (self.k_down()
- (self.bond_reserves() + short_amount).pow(fixed!(1e18) - self.time_stretch()))
- (self.bond_reserves() + bond_amount).pow(fixed!(1e18) - self.time_stretch()))
}
}

Expand Down Expand Up @@ -609,10 +573,11 @@ mod tests {
}
};
let max_iterations = 7;
let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
let actual = panic::catch_unwind(|| {
state.calculate_max_short(
U256::MAX,
fixed!(0),
open_vault_share_price,
checkpoint_exposure,
None,
Some(max_iterations),
Expand Down Expand Up @@ -656,7 +621,7 @@ mod tests {
/// result with the output of `short_deposit_derivative`.
#[traced_test]
#[tokio::test]
async fn test_short_deposit_derivative() -> Result<()> {
async fn fuzz_short_deposit_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.
Expand All @@ -671,7 +636,6 @@ mod tests {
let p1_result = std::panic::catch_unwind(|| {
state.calculate_open_short(
amount - empirical_derivative_epsilon,
state.calculate_spot_price(),
state.vault_share_price(),
)
});
Expand All @@ -689,7 +653,6 @@ mod tests {
let p2_result = std::panic::catch_unwind(|| {
state.calculate_open_short(
amount + empirical_derivative_epsilon,
state.calculate_spot_price(),
state.vault_share_price(),
)
});
Expand Down Expand Up @@ -734,7 +697,7 @@ mod tests {
/// Tests that the absolute max short can be executed on chain.
#[traced_test]
#[tokio::test]
async fn test_calculate_absolute_max_short_execute() -> Result<()> {
async fn fuzz_calculate_absolute_max_short_execute() -> 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 plenty of capital to ensure we can execute
Expand Down Expand Up @@ -807,7 +770,7 @@ mod tests {

#[traced_test]
#[tokio::test]
async fn test_calculate_max_short() -> Result<()> {
async fn fuzz_calculate_max_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
Expand Down
Loading

0 comments on commit d14c4ef

Please sign in to comment.