1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::FixedPoint;
4
5use crate::State;
6
7impl State {
8 pub fn calculate_add_liquidity(
10 &self,
11 current_block_timestamp: U256,
12 contribution: FixedPoint<U256>,
13 min_lp_share_price: FixedPoint<U256>,
14 min_apr: FixedPoint<U256>,
15 max_apr: FixedPoint<U256>,
16 as_base: bool,
17 ) -> Result<FixedPoint<U256>> {
18 let apr = self.calculate_spot_rate()?;
20 if apr < min_apr || apr > max_apr {
21 return Err(eyre!("InvalidApr: Apr is outside the slippage guard."));
22 }
23
24 let lp_total_supply = self.lp_total_supply();
26
27 let starting_present_value = self.calculate_present_value(current_block_timestamp)?;
29
30 let new_state = self.calculate_pool_state_after_add_liquidity(contribution, as_base)?;
32 let ending_present_value = new_state.calculate_present_value(current_block_timestamp)?;
33
34 if ending_present_value < starting_present_value {
36 return Err(eyre!("DecreasedPresentValueWhenAddingLiquidity: Present value decreased after adding liquidity."));
37 }
38
39 let lp_shares = (ending_present_value - starting_present_value)
41 .mul_div_down(lp_total_supply, starting_present_value);
42
43 if lp_shares < self.minimum_transaction_amount() {
45 return Err(eyre!(
46 "MinimumTransactionAmount: Not enough lp shares minted."
47 ));
48 }
49
50 if contribution.div_down(lp_shares) < min_lp_share_price {
52 return Err(eyre!("OutputLimit: Not enough lp shares minted."));
53 }
54
55 Ok(lp_shares)
56 }
57
58 pub fn calculate_pool_state_after_add_liquidity(
59 &self,
60 contribution: FixedPoint<U256>,
61 as_base: bool,
62 ) -> Result<State> {
63 if contribution < self.minimum_transaction_amount() {
66 return Err(eyre!(
67 "MinimumTransactionAmount: Contribution is smaller than the minimum transaction."
68 ));
69 }
70
71 let share_contribution = {
72 if as_base {
73 I256::try_from(contribution.div_down(self.vault_share_price()))?
74 } else {
75 I256::try_from(contribution)?
76 }
77 };
78 Ok(self.get_state_after_liquidity_update(share_contribution)?)
79 }
80
81 pub fn calculate_pool_deltas_after_add_liquidity(
82 &self,
83 contribution: FixedPoint<U256>,
84 as_base: bool,
85 ) -> Result<(FixedPoint<U256>, I256, FixedPoint<U256>)> {
86 let share_contribution = match as_base {
87 true => contribution / self.vault_share_price(),
88 false => contribution,
89 };
90 let (share_reserves, share_adjustment, bond_reserves) = self.calculate_update_liquidity(
91 self.share_reserves(),
92 self.share_adjustment(),
93 self.bond_reserves(),
94 self.minimum_share_reserves(),
95 I256::from(0),
96 )?;
97 let (new_share_reserves, new_share_adjustment, new_bond_reserves) = self
98 .calculate_update_liquidity(
99 self.share_reserves(),
100 self.share_adjustment(),
101 self.bond_reserves(),
102 self.minimum_share_reserves(),
103 I256::try_from(share_contribution)?,
104 )?;
105 Ok((
106 new_share_reserves - share_reserves,
107 new_share_adjustment - share_adjustment,
108 new_bond_reserves - bond_reserves,
109 ))
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use std::panic;
116
117 use fixedpointmath::{fixed, int256, uint256};
118 use hyperdrive_test_utils::{
119 chain::TestChain,
120 constants::{FAST_FUZZ_RUNS, FUZZ_RUNS},
121 };
122 use rand::{thread_rng, Rng};
123
124 use super::*;
125 use crate::test_utils::agent::HyperdriveMathAgent;
126
127 #[tokio::test]
128 async fn fuzz_calculate_add_liquidity_unhappy_with_random_state() -> Result<()> {
129 let mut rng = thread_rng();
131
132 for _ in 0..*FAST_FUZZ_RUNS {
133 let state = rng.gen::<State>();
134 let contribution = rng.gen_range(fixed!(0)..=state.bond_reserves());
135 let current_block_timestamp = U256::from(rng.gen_range(0..=60 * 60 * 24 * 365));
136 let min_lp_share_price = rng.gen_range(fixed!(0)..=fixed!(10e18));
137 let min_apr = rng.gen_range(fixed!(0)..fixed!(1e18));
138 let max_apr = rng.gen_range(fixed!(5e17)..fixed!(1e18));
139
140 match panic::catch_unwind(|| {
143 state.calculate_add_liquidity(
144 current_block_timestamp,
145 contribution,
146 min_lp_share_price,
147 min_apr,
148 max_apr,
149 true,
150 )
151 }) {
152 Ok(lp_shares) => match lp_shares {
153 Ok(lp_shares) => assert!(lp_shares >= min_lp_share_price),
154 Err(err) => {
155 let message = err.to_string();
156
157 if message == "MinimumTransactionAmount: Contribution is smaller than the minimum transaction." {
158 assert!(contribution < state.minimum_transaction_amount());
159 }
160
161 else if message == "InvalidApr: Apr is outside the slippage guard." {
162 let apr = state.calculate_spot_rate()?;
163 assert!(apr < min_apr || apr > max_apr);
164 }
165
166 else if message == "DecreasedPresentValueWhenAddingLiquidity: Present value decreased after adding liquidity." {
167 let share_contribution =
168 I256::try_from(contribution / state.vault_share_price()).unwrap();
169 let new_state = state.get_state_after_liquidity_update(share_contribution)?;
170 let starting_present_value = state.calculate_present_value(current_block_timestamp)?;
171 let ending_present_value = new_state.calculate_present_value(current_block_timestamp)?;
172 assert!(ending_present_value < starting_present_value);
173 }
174
175 else if message == "MinimumTransactionAmount: Not enough lp shares minted." {
176 let share_contribution =
177 I256::try_from(contribution / state.vault_share_price()).unwrap();
178 let new_state = state.get_state_after_liquidity_update(share_contribution)?;
179 let starting_present_value = state.calculate_present_value(current_block_timestamp)?;
180 let ending_present_value = new_state.calculate_present_value(current_block_timestamp)?;
181 let lp_shares = (ending_present_value - starting_present_value)
182 .mul_div_down(state.lp_total_supply(), starting_present_value);
183 assert!(lp_shares < state.minimum_transaction_amount());
184 }
185
186 else if message == "OutputLimit: Not enough lp shares minted." {
187 let share_contribution =
188 I256::try_from(contribution / state.vault_share_price()).unwrap();
189 let new_state = state.get_state_after_liquidity_update(share_contribution)?;
190 let starting_present_value = state.calculate_present_value(current_block_timestamp)?;
191 let ending_present_value = new_state.calculate_present_value(current_block_timestamp)?;
192 let lp_shares = (ending_present_value - starting_present_value)
193 .mul_div_down(state.lp_total_supply(), starting_present_value);
194 assert!(contribution.div_down(lp_shares) < min_lp_share_price);
195 }
196 }
197 },
198 Err(_) => continue, }
200 }
201
202 Ok(())
203 }
204 #[tokio::test]
205 async fn fuzz_calculate_add_liquidity() -> Result<()> {
206 let mut rng = thread_rng();
208 let chain = TestChain::new().await?;
209 let mut alice = chain.alice().await?;
210 let mut bob = chain.bob().await?;
211 let config = bob.get_config().clone();
212
213 for _ in 0..*FUZZ_RUNS {
215 let id = chain.snapshot().await?;
217
218 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
220 let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
221 let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
222 alice.fund(contribution).await?;
223 bob.fund(budget).await?;
224
225 alice.initialize(fixed_rate, contribution, None).await?;
227
228 alice
230 .checkpoint(alice.latest_checkpoint().await?, uint256!(0), None)
231 .await?;
232 let rate = rng.gen_range(fixed!(0)..=fixed!(0.5e18));
233 alice
234 .advance_time(
235 rate,
236 FixedPoint::from(config.checkpoint_duration) * fixed!(0.5e18),
237 )
238 .await?;
239
240 let hd_state = bob.get_state().await?;
242 let state = State {
243 config: hd_state.config.clone(),
244 info: hd_state.info.clone(),
245 };
246
247 bob.add_liquidity(budget, None).await?;
249 let lp_shares_mock = bob.lp_shares();
250
251 let lp_shares = state
253 .calculate_add_liquidity(
254 bob.now().await?,
255 budget,
256 fixed!(0),
257 fixed!(0),
258 FixedPoint::from(U256::MAX),
259 true,
260 )
261 .unwrap();
262
263 assert!(lp_shares >= lp_shares_mock, "Should over estimate.");
265 assert!(
267 fixed!(1e18) - lp_shares_mock / lp_shares < fixed!(1e11),
268 "Difference should be less than 0.0000001."
269 );
270
271 chain.revert(id).await?;
273 alice.reset(Default::default()).await?;
274 bob.reset(Default::default()).await?;
275 }
276
277 Ok(())
278 }
279
280 #[tokio::test]
281 async fn fuzz_calculate_pool_state_after_add_liquidity() -> Result<()> {
282 let mut rng = thread_rng();
284 let chain = TestChain::new().await?;
285 let mut alice = chain.alice().await?;
286 let mut bob = chain.bob().await?;
287 let config = bob.get_config().clone();
288
289 for _ in 0..*FUZZ_RUNS {
291 let id = chain.snapshot().await?;
293
294 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
296 let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
297 let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
298 alice.fund(contribution).await?;
299 bob.fund(budget).await?;
300
301 alice.initialize(fixed_rate, contribution, None).await?;
303
304 alice
306 .checkpoint(alice.latest_checkpoint().await?, uint256!(0), None)
307 .await?;
308 let rate = fixed!(0);
309 alice
310 .advance_time(
311 rate,
312 FixedPoint::from(config.checkpoint_duration).mul_down(fixed!(0.5e18)),
313 )
314 .await?;
315
316 let state = State {
318 config: bob.get_state().await?.config.clone(),
319 info: bob.get_state().await?.info.clone(),
320 };
321
322 bob.add_liquidity(budget, None).await?;
324
325 let expected_state = State {
327 config: bob.get_state().await?.config.clone(),
328 info: bob.get_state().await?.info.clone(),
329 };
330
331 let actual_state = state
333 .calculate_pool_state_after_add_liquidity(budget, true)
334 .unwrap();
335
336 let share_reserves_equal = expected_state.share_reserves()
338 <= actual_state.share_reserves() + fixed!(1e9)
339 && expected_state.share_reserves() >= actual_state.share_reserves() - fixed!(1e9);
340 assert!(share_reserves_equal, "Share reserves should be equal.");
341
342 let bond_reserves_equal = expected_state.bond_reserves()
343 <= actual_state.bond_reserves() + fixed!(1e10)
344 && expected_state.bond_reserves() >= actual_state.bond_reserves() - fixed!(1e10);
345 assert!(bond_reserves_equal, "Bond reserves should be equal.");
346
347 let share_adjustment_equal = expected_state.share_adjustment()
348 <= actual_state.share_adjustment() + int256!(1e10)
349 && expected_state.share_adjustment()
350 >= actual_state.share_adjustment() - int256!(1e10);
351 assert!(share_adjustment_equal, "Share adjustment should be equal.");
352
353 chain.revert(id).await?;
355 alice.reset(Default::default()).await?;
356 bob.reset(Default::default()).await?;
357 }
358
359 Ok(())
360 }
361}