hyperdrive_math/long/
close.rs1use ethers::types::U256;
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, FixedPoint};
4
5use crate::{State, YieldSpace};
6
7impl State {
8 pub fn calculate_close_long<F: Into<FixedPoint<U256>>>(
10 &self,
11 bond_amount: F,
12 maturity_time: U256,
13 current_time: U256,
14 ) -> Result<FixedPoint<U256>> {
15 let bond_amount = bond_amount.into();
16
17 if bond_amount < self.config.minimum_transaction_amount.into() {
18 return Err(eyre!("MinimumTransactionAmount: Input amount too low"));
19 }
20
21 let long_proceeds =
23 self.calculate_close_long_flat_plus_curve(bond_amount, maturity_time, current_time)?;
24 let long_curve_fee = self.close_long_curve_fee(bond_amount, maturity_time, current_time)?;
25 let long_flat_fee = self.close_long_flat_fee(bond_amount, maturity_time, current_time);
26
27 if long_proceeds < (long_curve_fee + long_flat_fee) {
29 return Err(eyre!(
30 "Closing the long results in fees exceeding the long proceeds."
31 ));
32 }
33
34 Ok(long_proceeds - long_curve_fee - long_flat_fee)
36 }
37
38 fn calculate_close_long_flat_plus_curve<F: Into<FixedPoint<U256>>>(
40 &self,
41 bond_amount: F,
42 maturity_time: U256,
43 current_time: U256,
44 ) -> Result<FixedPoint<U256>> {
45 let bond_amount = bond_amount.into();
46 let normalized_time_remaining =
47 self.calculate_normalized_time_remaining(maturity_time, current_time);
48
49 let flat = bond_amount.mul_div_down(
51 fixed!(1e18) - normalized_time_remaining,
52 self.vault_share_price(),
53 );
54
55 let curve = if normalized_time_remaining > fixed!(0) {
57 let curve_bonds_in = bond_amount * normalized_time_remaining;
58 self.calculate_shares_out_given_bonds_in_down(curve_bonds_in)?
59 } else {
60 fixed!(0)
61 };
62
63 Ok(flat + curve)
64 }
65
66 pub fn calculate_market_value_long<F: Into<FixedPoint<U256>>>(
78 &self,
79 bond_amount: F,
80 maturity_time: U256,
81 current_time: U256,
82 ) -> Result<FixedPoint<U256>> {
83 let bond_amount = bond_amount.into();
84
85 let spot_price = self.calculate_spot_price()?;
86 if spot_price > fixed!(1e18) {
87 return Err(eyre!("Negative fixed interest!"));
88 }
89
90 let time_remaining = self.calculate_normalized_time_remaining(maturity_time, current_time);
92
93 let flat_value =
95 bond_amount.mul_div_down(fixed!(1e18) - time_remaining, self.vault_share_price());
96 let curve_bonds = bond_amount * time_remaining;
97 let curve_value = curve_bonds * spot_price / self.vault_share_price();
98
99 let trading_proceeds = flat_value + curve_value;
100 let flat_fees_paid = self.close_long_flat_fee(bond_amount, maturity_time, current_time);
101 let curve_fees_paid =
102 self.close_long_curve_fee(bond_amount, maturity_time, current_time)?;
103 let fees_paid = flat_fees_paid + curve_fees_paid;
104
105 if fees_paid > trading_proceeds {
106 Ok(fixed!(0))
107 } else {
108 Ok(trading_proceeds - flat_fees_paid - curve_fees_paid)
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use ethers::types::I256;
116 use fixedpointmath::int256;
117 use hyperdrive_test_utils::{chain::TestChain, constants::FAST_FUZZ_RUNS};
118 use rand::{thread_rng, Rng};
119
120 use super::*;
121
122 #[tokio::test]
123 async fn fuzz_calculate_close_long_after_maturity() -> Result<()> {
124 let mut rng = thread_rng();
127 for _ in 0..*FAST_FUZZ_RUNS {
128 let state = rng.gen::<State>();
129 let in_ = rng.gen_range(fixed!(0)..=state.effective_share_reserves()?);
130 let maturity_time = state.position_duration();
134 let just_after_maturity = maturity_time + state.checkpoint_duration();
136 let base_earned_just_after_maturity = state.calculate_close_long(
137 in_,
138 maturity_time.into(),
139 just_after_maturity.into(),
140 )? * state.vault_share_price();
141 let well_after_maturity = just_after_maturity + fixed!(1e10);
143 let base_earned_well_after_maturity = state.calculate_close_long(
144 in_,
145 maturity_time.into(),
146 well_after_maturity.into(),
147 )? * state.vault_share_price();
148 assert!(
150 base_earned_well_after_maturity == base_earned_just_after_maturity,
151 "Trader should not have earned any more after maturity:
152 earned_well_after_maturity={:?} != earned_just_after_maturity={:?}",
153 base_earned_well_after_maturity,
154 base_earned_just_after_maturity
155 );
156 }
157 Ok(())
158 }
159
160 #[tokio::test]
161 async fn fuzz_sol_calculate_close_long_flat_plus_curve() -> Result<()> {
162 let chain = TestChain::new().await?;
163
164 let mut rng = thread_rng();
166 for _ in 0..*FAST_FUZZ_RUNS {
167 let state = rng.gen::<State>();
168 let in_ = rng.gen_range(fixed!(0)..=state.effective_share_reserves()?);
169 let maturity_time = state.position_duration();
170 let current_time = rng.gen_range(fixed!(0)..=maturity_time);
171 let normalized_time_remaining = state
172 .calculate_normalized_time_remaining(maturity_time.into(), current_time.into());
173 let actual = state.calculate_close_long_flat_plus_curve(
174 in_,
175 maturity_time.into(),
176 current_time.into(),
177 );
178 match chain
179 .mock_hyperdrive_math()
180 .calculate_close_long(
181 state.effective_share_reserves()?.into(),
182 state.bond_reserves().into(),
183 in_.into(),
184 normalized_time_remaining.into(),
185 state.t().into(),
186 state.c().into(),
187 state.mu().into(),
188 )
189 .call()
190 .await
191 {
192 Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected.2)),
193 Err(_) => assert!(actual.is_err()),
194 }
195 }
196
197 Ok(())
198 }
199
200 #[tokio::test]
202 async fn test_close_long_min_txn_amount() -> Result<()> {
203 let mut rng = thread_rng();
204 let state = rng.gen::<State>();
205 let result = state.calculate_close_long(
206 state.config.minimum_transaction_amount - 10,
207 0.into(),
208 0.into(),
209 );
210 assert!(result.is_err());
211 Ok(())
212 }
213
214 #[tokio::test]
218 async fn test_calculate_market_value_long() -> Result<()> {
219 let tolerance = int256!(1e12); let mut rng = thread_rng();
223 for _ in 0..*FAST_FUZZ_RUNS {
224 let mut scaled_tolerance = tolerance;
225
226 let state = rng.gen::<State>();
227 let bond_amount = state.minimum_transaction_amount();
228 let maturity_time = U256::try_from(state.position_duration())?;
229 let current_time = rng.gen_range(fixed!(0)..=FixedPoint::from(maturity_time));
230
231 let spot_price = state.calculate_spot_price()?;
235 if state.curve_fee() * (fixed!(1e18) - spot_price) > spot_price {
236 continue;
237 }
238
239 let reserves_ratio = state.effective_share_reserves()? / state.bond_reserves();
243 if reserves_ratio < fixed!(1e12) {
244 scaled_tolerance *= int256!(100);
245 } else if reserves_ratio < fixed!(1e14) {
246 scaled_tolerance *= int256!(10);
247 }
248
249 let hyperdrive_valuation = state.calculate_close_long(
250 bond_amount,
251 maturity_time.into(),
252 current_time.into(),
253 )?;
254
255 let spot_valuation = state.calculate_market_value_long(
256 bond_amount,
257 maturity_time.into(),
258 current_time.into(),
259 )?;
260
261 let diff = spot_valuation
262 .abs_diff(hyperdrive_valuation)
263 .change_type::<I256>()?
264 .raw();
265
266 assert!(
267 diff < scaled_tolerance,
268 r#"
269 hyperdrive_valuation: {hyperdrive_valuation}
270 spot_valuation: {spot_valuation}
271 diff: {diff}
272 tolerance: {scaled_tolerance}
273"#,
274 );
275 }
276
277 Ok(())
278 }
279}