1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, FixedPoint};
4
5use super::math::SHARE_PROCEEDS_MAX_ITERATIONS;
6use crate::State;
7
8impl State {
9 pub fn calculate_remove_liquidity(
11 &self,
12 current_block_timestamp: U256,
13 active_lp_total_supply: FixedPoint<U256>,
14 withdrawal_shares_total_supply: FixedPoint<U256>,
15 lp_shares: FixedPoint<U256>,
16 total_vault_shares: FixedPoint<U256>,
17 total_vault_assets: FixedPoint<U256>,
18 min_output_per_share: FixedPoint<U256>,
19 minimum_transaction_amount: FixedPoint<U256>,
20 as_base: bool,
21 ) -> Result<(FixedPoint<U256>, FixedPoint<U256>, State)> {
22 if lp_shares < minimum_transaction_amount {
25 return Err(eyre!("Minimum transaction amount not met"));
26 }
27
28 let mut state = self.clone();
30 state.info.lp_total_supply -= lp_shares.into();
31 let active_lp_total_supply = active_lp_total_supply - lp_shares;
32
33 let withdrawal_shares_total_supply = withdrawal_shares_total_supply + lp_shares;
35
36 let (proceeds, withdrawal_shares_redeemed, updated_state) = state
38 .redeem_withddrawal_shares(
39 current_block_timestamp,
40 active_lp_total_supply,
41 withdrawal_shares_total_supply,
42 lp_shares,
43 total_vault_shares,
44 total_vault_assets,
45 min_output_per_share,
46 as_base,
47 )?;
48 let withdrawal_shares = lp_shares - withdrawal_shares_redeemed;
49
50 Ok((proceeds, withdrawal_shares, updated_state))
51 }
52
53 pub fn redeem_withddrawal_shares(
58 &self,
59 current_block_timestamp: U256,
60 active_lp_total_supply: FixedPoint<U256>,
61 withdrawal_shares_total_supply: FixedPoint<U256>,
62 withdrawal_shares: FixedPoint<U256>,
63 total_supply: FixedPoint<U256>,
64 total_assets: FixedPoint<U256>,
65 min_output_per_share: FixedPoint<U256>,
66 as_base: bool,
67 ) -> Result<(FixedPoint<U256>, FixedPoint<U256>, State)> {
68 let (_withdrawal_shares_redeemed, _share_proceeds, updated_state, _success) = self
74 .distribute_excess_idle(
75 current_block_timestamp,
76 active_lp_total_supply,
77 withdrawal_shares_total_supply,
78 SHARE_PROCEEDS_MAX_ITERATIONS,
79 )?;
80
81 let ready_to_withdraw = updated_state.withdrawal_shares_ready_to_withdraw();
82 let withdrawal_share_proceeds = updated_state.withdrawal_shares_proceeds();
83
84 let mut withdrawal_shares_redeemed = withdrawal_shares;
88 if withdrawal_shares_redeemed > ready_to_withdraw {
89 withdrawal_shares_redeemed = ready_to_withdraw;
90 }
91 if withdrawal_shares_redeemed == fixed!(0) {
92 return Ok((fixed!(0), fixed!(0), self.clone()));
93 }
94
95 let vault_share_price = updated_state.vault_share_price();
99 let share_proceeds =
100 withdrawal_shares_redeemed.mul_div_down(withdrawal_share_proceeds, ready_to_withdraw);
101
102 let mut updated_state = updated_state.clone();
104 updated_state.info.withdrawal_shares_ready_to_withdraw -= withdrawal_shares_redeemed.into();
105 updated_state.info.withdrawal_shares_proceeds -= share_proceeds.into();
106
107 let proceeds = updated_state.withdraw(
109 share_proceeds,
110 vault_share_price,
111 total_supply,
112 total_assets,
113 as_base,
114 )?;
115
116 if proceeds < min_output_per_share.mul_up(withdrawal_shares_redeemed) {
120 return Err(eyre!("Output limit not met"));
121 }
122
123 Ok((proceeds, withdrawal_shares_redeemed, updated_state))
124 }
125
126 fn distribute_excess_idle(
129 &self,
130 current_block_timestamp: U256,
131 active_lp_total_supply: FixedPoint<U256>,
132 withdrawal_shares_total_supply: FixedPoint<U256>,
133 max_iterations: u64,
134 ) -> Result<(FixedPoint<U256>, FixedPoint<U256>, State, bool)> {
135 let withdrawal_shares_total_supply =
136 withdrawal_shares_total_supply - self.withdrawal_shares_ready_to_withdraw();
137
138 if withdrawal_shares_total_supply == fixed!(0) {
141 return Ok((fixed!(0), fixed!(0), self.clone(), true));
142 }
143
144 let idle = self.calculate_idle_share_reserves();
146 if idle == fixed!(0) {
147 return Ok((fixed!(0), fixed!(0), self.clone(), true));
148 }
149
150 let (withdrawal_shares_redeemed, share_proceeds) = self.calculate_distribute_excess_idle(
153 current_block_timestamp,
154 active_lp_total_supply,
155 withdrawal_shares_total_supply,
156 max_iterations,
157 )?;
158
159 match self.calculate_update_liquidity(
161 self.share_reserves(),
162 self.share_adjustment(),
163 self.bond_reserves(),
164 self.minimum_share_reserves(),
165 -I256::try_from(share_proceeds)?,
166 ) {
167 Ok(_) => {}
168 Err(_) => return Ok((fixed!(0), fixed!(0), self.clone(), false)),
169 };
170
171 let mut updated_state =
173 self.get_state_after_liquidity_update(-I256::try_from(share_proceeds)?)?;
174 updated_state.info.withdrawal_shares_ready_to_withdraw += withdrawal_shares_redeemed.into();
175 updated_state.info.withdrawal_shares_proceeds += share_proceeds.into();
176
177 return Ok((
178 withdrawal_shares_redeemed,
179 share_proceeds,
180 updated_state,
181 true,
182 ));
183 }
184
185 fn withdraw(
186 &self,
187 shares: FixedPoint<U256>,
188 vault_share_price: FixedPoint<U256>,
189 total_shares: FixedPoint<U256>,
190 total_assets: FixedPoint<U256>,
191 as_base: bool,
192 ) -> Result<FixedPoint<U256>> {
193 let base_amount = shares.mul_down(vault_share_price);
195 let shares = self.convert_to_shares(base_amount, total_shares, total_assets)?;
196
197 if as_base {
198 let amount_withdrawn = self.convert_to_assets(shares, total_shares, total_assets)?;
199 return Ok(amount_withdrawn);
200 }
201 Ok(shares)
202 }
203
204 fn convert_to_shares(
205 &self,
206 base_amount: FixedPoint<U256>,
207 total_supply: FixedPoint<U256>,
208 total_assets: FixedPoint<U256>,
209 ) -> Result<FixedPoint<U256>> {
210 Ok(base_amount.mul_div_down(total_supply, total_assets))
211 }
212
213 fn convert_to_assets(
214 &self,
215 share_amount: FixedPoint<U256>,
216 total_supply: FixedPoint<U256>,
217 total_assets: FixedPoint<U256>,
218 ) -> Result<FixedPoint<U256>> {
219 Ok(share_amount.mul_div_down(total_assets, total_supply))
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use std::cmp::min;
226
227 use fixedpointmath::uint256;
228 use hyperdrive_test_utils::{chain::TestChain, constants::FUZZ_RUNS};
229 use hyperdrive_wrappers::wrappers::ihyperdrive::Options;
230 use rand::{thread_rng, Rng};
231
232 use super::*;
233 use crate::test_utils::agent::HyperdriveMathAgent;
234
235 #[tokio::test]
236 async fn fuzz_test_calculate_remove_liquidity() -> Result<()> {
237 let mut rng = thread_rng();
239 let chain = TestChain::new().await?;
240 let mut alice = chain.alice().await?;
241 let mut bob = chain.bob().await?;
242 let config = bob.get_config().clone();
243
244 for _ in 0..*FUZZ_RUNS {
245 let id = chain.snapshot().await?;
247
248 let fixed_rate = rng.gen_range(fixed!(0.01e18)..=fixed!(0.1e18));
250 let contribution = rng.gen_range(fixed!(10_000e18)..=fixed!(500_000_000e18));
251 let budget = rng.gen_range(fixed!(10e18)..=fixed!(500_000_000e18));
252 alice.fund(contribution).await?;
253 bob.fund(budget).await?;
254
255 alice.initialize(fixed_rate, contribution, None).await?;
257
258 alice
260 .checkpoint(alice.latest_checkpoint().await?, uint256!(0), None)
261 .await?;
262 let rate = rng.gen_range(fixed!(0)..=fixed!(0.5e18));
263 alice
264 .advance_time(
265 rate,
266 FixedPoint::from(config.checkpoint_duration) * fixed!(0.5e18),
267 )
268 .await?;
269
270 bob.add_liquidity(budget, None).await?;
272
273 let timestamp = alice.now().await?;
276 let total_supply: FixedPoint<U256> = bob.vault().total_supply().call().await?.into();
277 let total_assets: FixedPoint<U256> = bob
278 .vault()
279 .total_assets_with_timestamp(timestamp + uint256!(1))
280 .call()
281 .await?
282 .into();
283
284 let hd_state = bob.get_state().await?;
286 let mut state = State {
287 config: hd_state.config.clone(),
288 info: hd_state.info.clone(),
289 };
290
291 let lp_token_asset_id = U256::zero();
294 let active_lp_total_supply: FixedPoint<U256> = bob
295 .hyperdrive()
296 .total_supply(lp_token_asset_id)
297 .await?
298 .into();
299 let withdrawal_share_asset_id = U256::from(3) << 248;
300 let withdrawal_shares_total_supply: FixedPoint<U256> = bob
301 .hyperdrive()
302 .total_supply(withdrawal_share_asset_id)
303 .await?
304 .into();
305
306 let remove_budget = min(
309 rng.gen_range(fixed!(0)..=fixed!(1.01e18) * bob.wallet.lp_shares),
310 bob.wallet.lp_shares,
311 );
312 let remove_budget = min(
313 active_lp_total_supply - fixed!(2e18) * state.minimum_share_reserves(),
314 remove_budget,
315 );
316
317 let as_base = true;
319 let options = Options {
320 destination: bob.client().address(),
321 as_base,
322 extra_data: [].into(),
323 };
324 let tx_result = bob
325 .remove_liquidity(remove_budget, Some(options), None)
326 .await;
327 let sol_final_state = bob.get_state().await?;
328
329 let current_block_timestamp = bob.now().await?;
331 let vault_share_price = bob.get_state().await?.info.vault_share_price;
332 state.info.vault_share_price = vault_share_price;
333
334 let result = std::panic::catch_unwind(|| {
336 state
337 .calculate_remove_liquidity(
338 current_block_timestamp,
339 active_lp_total_supply,
340 withdrawal_shares_total_supply,
341 remove_budget,
342 total_supply,
343 total_assets,
344 fixed!(0),
345 fixed!(1),
346 as_base,
347 )
348 .unwrap()
349 });
350
351 match result {
352 Ok((rust_amount, rust_withdrawal_shares, rust_final_state)) => {
353 let (sol_amount, sol_withdrawal_shares) = tx_result?;
354 assert!(rust_amount == sol_amount.into());
356
357 assert!(rust_withdrawal_shares == sol_withdrawal_shares.into());
360
361 assert!(sol_final_state.bond_reserves() == rust_final_state.bond_reserves());
363 assert!(sol_final_state.share_reserves() == rust_final_state.share_reserves());
364 assert!(
365 sol_final_state.lp_total_supply() == rust_final_state.lp_total_supply()
366 );
367 assert!(
368 sol_final_state.share_adjustment() == rust_final_state.share_adjustment()
369 );
370 assert!(
371 sol_final_state.withdrawal_shares_ready_to_withdraw()
372 == rust_final_state.withdrawal_shares_ready_to_withdraw()
373 );
374 assert!(
375 sol_final_state.withdrawal_shares_proceeds()
376 == rust_final_state.withdrawal_shares_proceeds()
377 );
378 }
379 Err(err) => {
380 println!("err {:#?}", err);
381 println!("tx_result {:#?}", tx_result);
382 assert!(tx_result.is_err());
383 }
384 }
385
386 chain.revert(id).await?;
388 alice.reset(Default::default()).await?;
389 bob.reset(Default::default()).await?;
390 }
391
392 Ok(())
393 }
394}