1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::U256;
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use tycho_common::{
7 dto::ProtocolStateDelta,
8 models::token::Token,
9 simulation::{
10 errors::{SimulationError, TransitionError},
11 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
12 },
13 Bytes,
14};
15use tycho_ethereum::BytesCodec;
16
17use crate::evm::protocol::{
18 rocketpool::ETH_ADDRESS,
19 safe_math::{safe_add_u256, safe_sub_u256},
20 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
21 utils::solidity_math::mul_div,
22};
23
24const DEPOSIT_FEE_BASE: u128 = 1_000_000_000_000_000_000; const FULL_DEPOSIT_VALUE: u128 = 32_000_000_000_000_000_000u128;
28
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct RocketpoolState {
31 pub reth_supply: U256,
32 pub total_eth: U256,
33 pub deposit_contract_balance: U256,
35 pub reth_contract_liquidity: U256,
37 pub deposit_fee: U256,
40 pub deposits_enabled: bool,
41 pub min_deposit_amount: U256,
42 pub max_deposit_pool_size: U256,
43 pub deposit_assigning_enabled: bool,
45 pub deposit_assign_maximum: U256,
47 pub deposit_assign_socialised_maximum: U256,
49 pub megapool_queue_requested_total: U256,
51 pub target_reth_collateral_rate: U256,
54}
55
56impl RocketpoolState {
57 #[allow(clippy::too_many_arguments)]
58 pub fn new(
59 reth_supply: U256,
60 total_eth: U256,
61 deposit_contract_balance: U256,
62 reth_contract_liquidity: U256,
63 deposit_fee: U256,
64 deposits_enabled: bool,
65 min_deposit_amount: U256,
66 max_deposit_pool_size: U256,
67 deposit_assigning_enabled: bool,
68 deposit_assign_maximum: U256,
69 deposit_assign_socialised_maximum: U256,
70 megapool_queue_requested_total: U256,
71 target_reth_collateral_rate: U256,
72 ) -> Self {
73 Self {
74 reth_supply,
75 total_eth,
76 deposit_contract_balance,
77 reth_contract_liquidity,
78 deposit_fee,
79 deposits_enabled,
80 min_deposit_amount,
81 max_deposit_pool_size,
82 deposit_assigning_enabled,
83 deposit_assign_maximum,
84 deposit_assign_socialised_maximum,
85 megapool_queue_requested_total,
86 target_reth_collateral_rate,
87 }
88 }
89
90 fn get_reth_value(&self, eth_amount: U256) -> Result<U256, SimulationError> {
92 let fee = mul_div(eth_amount, self.deposit_fee, U256::from(DEPOSIT_FEE_BASE))?;
94 let net_eth = safe_sub_u256(eth_amount, fee)?;
95 mul_div(net_eth, self.reth_supply, self.total_eth)
97 }
98
99 fn get_eth_value(&self, reth_amount: U256) -> Result<U256, SimulationError> {
101 mul_div(reth_amount, self.total_eth, self.reth_supply)
103 }
104
105 fn is_depositing_eth(token_in: &Bytes) -> bool {
106 token_in.as_ref() == ETH_ADDRESS
107 }
108
109 fn assert_deposits_enabled(&self) -> Result<(), SimulationError> {
110 if !self.deposits_enabled {
111 Err(SimulationError::RecoverableError(
112 "Deposits are currently disabled in Rocketpool".to_string(),
113 ))
114 } else {
115 Ok(())
116 }
117 }
118
119 fn get_max_deposit_capacity(&self) -> Result<U256, SimulationError> {
126 if self.deposit_assigning_enabled {
127 safe_add_u256(self.max_deposit_pool_size, self.megapool_queue_requested_total)
128 } else {
129 Ok(self.max_deposit_pool_size)
130 }
131 }
132
133 fn get_deposit_pool_excess_balance(&self) -> Result<U256, SimulationError> {
138 if self.megapool_queue_requested_total >= self.deposit_contract_balance {
139 Ok(U256::ZERO)
140 } else {
141 safe_sub_u256(self.deposit_contract_balance, self.megapool_queue_requested_total)
142 }
143 }
144
145 fn get_total_available_for_withdrawal(&self) -> Result<U256, SimulationError> {
148 let deposit_pool_excess = self.get_deposit_pool_excess_balance()?;
149 safe_add_u256(self.reth_contract_liquidity, deposit_pool_excess)
150 }
151
152 fn compute_deposit_routing(
157 &self,
158 deposit_amount: U256,
159 ) -> Result<(U256, U256), SimulationError> {
160 let target_collateral = mul_div(
162 self.total_eth,
163 self.target_reth_collateral_rate,
164 U256::from(DEPOSIT_FEE_BASE),
165 )?;
166 let shortfall = target_collateral.saturating_sub(self.reth_contract_liquidity);
168 let to_reth = deposit_amount.min(shortfall);
169 let to_vault = deposit_amount - to_reth;
170 Ok((to_reth, to_vault))
171 }
172
173 fn calculate_assign_deposits(&self, deposit_amount: U256) -> U256 {
183 if !self.deposit_assigning_enabled ||
184 self.megapool_queue_requested_total
185 .is_zero()
186 {
187 return U256::ZERO;
188 }
189
190 let full_deposit_value = U256::from(FULL_DEPOSIT_VALUE);
191
192 let scaling_count = deposit_amount / full_deposit_value;
194 let count_cap = (self.deposit_assign_socialised_maximum + scaling_count)
195 .min(self.deposit_assign_maximum);
196
197 let vault_cap = self.deposit_contract_balance / full_deposit_value;
199
200 let queue_entries = self.megapool_queue_requested_total / full_deposit_value;
202
203 let entries = count_cap
205 .min(vault_cap)
206 .min(queue_entries);
207
208 entries * full_deposit_value
209 }
210}
211
212#[typetag::serde]
213impl ProtocolSim for RocketpoolState {
214 fn fee(&self) -> f64 {
215 unimplemented!("Rocketpool has asymmetric fees; use spot_price or get_amount_out instead")
216 }
217
218 fn spot_price(&self, _base: &Token, quote: &Token) -> Result<f64, SimulationError> {
219 let is_depositing_eth = RocketpoolState::is_depositing_eth("e.address);
221
222 let amount = U256::from(1e18);
225
226 let base_per_quote = if is_depositing_eth {
227 self.assert_deposits_enabled()?;
229 self.get_reth_value(amount)?
230 } else {
231 self.get_eth_value(amount)?
233 };
234
235 let base_per_quote = u256_to_f64(base_per_quote)? / 1e18;
236 Ok(1.0 / base_per_quote)
238 }
239
240 fn get_amount_out(
241 &self,
242 amount_in: BigUint,
243 token_in: &Token,
244 _token_out: &Token,
245 ) -> Result<GetAmountOutResult, SimulationError> {
246 let amount_in = biguint_to_u256(&amount_in);
247 let is_depositing_eth = RocketpoolState::is_depositing_eth(&token_in.address);
248
249 let amount_out = if is_depositing_eth {
250 self.assert_deposits_enabled()?;
251
252 if amount_in < self.min_deposit_amount {
253 return Err(SimulationError::InvalidInput(
254 format!(
255 "Deposit amount {} is less than the minimum deposit of {}",
256 amount_in, self.min_deposit_amount
257 ),
258 None,
259 ));
260 }
261
262 let capacity_needed = safe_add_u256(self.deposit_contract_balance, amount_in)?;
263 let max_capacity = self.get_max_deposit_capacity()?;
264 if capacity_needed > max_capacity {
265 return Err(SimulationError::InvalidInput(
266 format!(
267 "Deposit would exceed maximum pool size (capacity needed: {}, max: {})",
268 capacity_needed, max_capacity
269 ),
270 None,
271 ));
272 }
273
274 self.get_reth_value(amount_in)?
275 } else {
276 let eth_out = self.get_eth_value(amount_in)?;
277
278 let total_available = self.get_total_available_for_withdrawal()?;
279 if eth_out > total_available {
280 return Err(SimulationError::RecoverableError(format!(
281 "Withdrawal {} exceeds available liquidity {}",
282 eth_out, total_available
283 )));
284 }
285
286 eth_out
287 };
288
289 let mut new_state = self.clone();
290 if is_depositing_eth {
292 let (to_reth, to_vault) = new_state.compute_deposit_routing(amount_in)?;
294 new_state.reth_contract_liquidity =
295 safe_add_u256(new_state.reth_contract_liquidity, to_reth)?;
296 new_state.deposit_contract_balance =
297 safe_add_u256(new_state.deposit_contract_balance, to_vault)?;
298
299 let eth_assigned = new_state.calculate_assign_deposits(amount_in);
302 if eth_assigned > U256::ZERO {
303 new_state.deposit_contract_balance =
304 safe_sub_u256(new_state.deposit_contract_balance, eth_assigned)?;
305 new_state.megapool_queue_requested_total =
306 safe_sub_u256(new_state.megapool_queue_requested_total, eth_assigned)?;
307 }
308 } else {
309 let needed_from_deposit_pool =
311 amount_out.saturating_sub(new_state.reth_contract_liquidity);
312 new_state.reth_contract_liquidity = new_state
313 .reth_contract_liquidity
314 .saturating_sub(amount_out);
315 new_state.deposit_contract_balance =
316 safe_sub_u256(new_state.deposit_contract_balance, needed_from_deposit_pool)?;
317 }
318
319 let gas_used = if is_depositing_eth { 209_000u32 } else { 134_000u32 };
323
324 Ok(GetAmountOutResult::new(
325 u256_to_biguint(amount_out),
326 BigUint::from(gas_used),
327 Box::new(new_state),
328 ))
329 }
330
331 fn get_limits(
332 &self,
333 sell_token: Bytes,
334 _buy_token: Bytes,
335 ) -> Result<(BigUint, BigUint), SimulationError> {
336 let is_depositing_eth = Self::is_depositing_eth(&sell_token);
337
338 if is_depositing_eth {
339 let max_capacity = self.get_max_deposit_capacity()?;
341 let max_eth_sell = safe_sub_u256(max_capacity, self.deposit_contract_balance)?;
342 let max_reth_buy = self.get_reth_value(max_eth_sell)?;
343 Ok((u256_to_biguint(max_eth_sell), u256_to_biguint(max_reth_buy)))
344 } else {
345 let max_eth_buy = self.get_total_available_for_withdrawal()?;
347 let max_reth_sell = mul_div(max_eth_buy, self.reth_supply, self.total_eth)?;
348 Ok((u256_to_biguint(max_reth_sell), u256_to_biguint(max_eth_buy)))
349 }
350 }
351
352 fn delta_transition(
353 &mut self,
354 delta: ProtocolStateDelta,
355 _tokens: &HashMap<Bytes, Token>,
356 _balances: &Balances,
357 ) -> Result<(), TransitionError> {
358 self.total_eth = delta
359 .updated_attributes
360 .get("total_eth")
361 .map_or(self.total_eth, U256::from_bytes);
362 self.reth_supply = delta
363 .updated_attributes
364 .get("reth_supply")
365 .map_or(self.reth_supply, U256::from_bytes);
366 self.deposit_contract_balance = delta
367 .updated_attributes
368 .get("deposit_contract_balance")
369 .map_or(self.deposit_contract_balance, U256::from_bytes);
370 self.reth_contract_liquidity = delta
371 .updated_attributes
372 .get("reth_contract_liquidity")
373 .map_or(self.reth_contract_liquidity, U256::from_bytes);
374 self.deposits_enabled = delta
375 .updated_attributes
376 .get("deposits_enabled")
377 .map_or(self.deposits_enabled, |val| !U256::from_bytes(val).is_zero());
378 self.deposit_assigning_enabled = delta
379 .updated_attributes
380 .get("deposit_assigning_enabled")
381 .map_or(self.deposit_assigning_enabled, |val| !U256::from_bytes(val).is_zero());
382 self.deposit_fee = delta
383 .updated_attributes
384 .get("deposit_fee")
385 .map_or(self.deposit_fee, U256::from_bytes);
386 self.min_deposit_amount = delta
387 .updated_attributes
388 .get("min_deposit_amount")
389 .map_or(self.min_deposit_amount, U256::from_bytes);
390 self.max_deposit_pool_size = delta
391 .updated_attributes
392 .get("max_deposit_pool_size")
393 .map_or(self.max_deposit_pool_size, U256::from_bytes);
394 self.deposit_assign_maximum = delta
395 .updated_attributes
396 .get("deposit_assign_maximum")
397 .map_or(self.deposit_assign_maximum, U256::from_bytes);
398 self.deposit_assign_socialised_maximum = delta
399 .updated_attributes
400 .get("deposit_assign_socialised_maximum")
401 .map_or(self.deposit_assign_socialised_maximum, U256::from_bytes);
402 self.megapool_queue_requested_total = delta
403 .updated_attributes
404 .get("megapool_queue_requested_total")
405 .map_or(self.megapool_queue_requested_total, U256::from_bytes);
406 self.target_reth_collateral_rate = delta
407 .updated_attributes
408 .get("target_reth_collateral_rate")
409 .map_or(self.target_reth_collateral_rate, U256::from_bytes);
410
411 Ok(())
412 }
413
414 fn clone_box(&self) -> Box<dyn ProtocolSim> {
415 Box::new(self.clone())
416 }
417
418 fn as_any(&self) -> &dyn Any {
419 self
420 }
421
422 fn as_any_mut(&mut self) -> &mut dyn Any {
423 self
424 }
425
426 fn eq(&self, other: &dyn ProtocolSim) -> bool {
427 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
428 self == other_state
429 } else {
430 false
431 }
432 }
433
434 fn query_pool_swap(
435 &self,
436 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
437 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
438 crate::evm::query_pool_swap::query_pool_swap(self, params)
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use std::{
445 collections::{HashMap, HashSet},
446 str::FromStr,
447 };
448
449 use approx::assert_ulps_eq;
450 use num_bigint::BigUint;
451 use rstest::rstest;
452 use tycho_common::{
453 dto::ProtocolStateDelta,
454 hex_bytes::Bytes,
455 models::{token::Token, Chain},
456 simulation::{
457 errors::SimulationError,
458 protocol_sim::{Balances, ProtocolSim},
459 },
460 };
461
462 use super::*;
463
464 fn create_state() -> RocketpoolState {
473 RocketpoolState::new(
474 U256::from(100e18), U256::from(200e18), U256::from(50e18), U256::ZERO, U256::from(400_000_000_000_000_000u64), true, U256::ZERO, U256::from(1000e18), false, U256::ZERO, U256::ZERO, U256::ZERO, U256::from(10_000_000_000_000_000u64), )
489 }
490
491 fn eth_token() -> Token {
492 Token::new(&Bytes::from(ETH_ADDRESS), "ETH", 18, 0, &[Some(100_000)], Chain::Ethereum, 100)
493 }
494
495 fn reth_token() -> Token {
496 Token::new(
497 &Bytes::from_str("0xae78736Cd615f374D3085123A210448E74Fc6393").unwrap(),
498 "rETH",
499 18,
500 0,
501 &[Some(100_000)],
502 Chain::Ethereum,
503 100,
504 )
505 }
506
507 #[test]
510 fn test_max_capacity_assign_disabled() {
511 let state = create_state();
512 assert_eq!(
513 state
514 .get_max_deposit_capacity()
515 .unwrap(),
516 U256::from(1000e18)
517 );
518 }
519
520 #[test]
521 fn test_max_capacity_assign_enabled_empty_queue() {
522 let mut state = create_state();
523 state.deposit_assigning_enabled = true;
524 assert_eq!(
525 state
526 .get_max_deposit_capacity()
527 .unwrap(),
528 U256::from(1000e18)
529 );
530 }
531
532 #[test]
533 fn test_max_capacity_assign_enabled_with_queue() {
534 let mut state = create_state();
535 state.deposit_assigning_enabled = true;
536 state.megapool_queue_requested_total = U256::from(500e18);
537 assert_eq!(
539 state
540 .get_max_deposit_capacity()
541 .unwrap(),
542 U256::from(1500e18)
543 );
544 }
545
546 #[rstest]
549 #[case::all_to_reth(
550 U256::ZERO, U256::from(10_000_000_000_000_000u64), 1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128, 0u128, )]
556 #[case::split(
558 U256::ZERO,
559 U256::from(10_000_000_000_000_000u64),
560 10_000_000_000_000_000_000u128, 2_000_000_000_000_000_000u128, 8_000_000_000_000_000_000u128, )]
564 #[case::all_to_vault(
566 U256::from(10_000_000_000_000_000_000u128), U256::from(10_000_000_000_000_000u64),
568 5_000_000_000_000_000_000u128, 0u128,
570 5_000_000_000_000_000_000u128,
571 )]
572 #[case::zero_collateral_rate(
574 U256::ZERO,
575 U256::ZERO, 5_000_000_000_000_000_000u128,
577 0u128,
578 5_000_000_000_000_000_000u128,
579 )]
580 fn test_deposit_routing(
581 #[case] reth_contract_liquidity: U256,
582 #[case] target_reth_collateral_rate: U256,
583 #[case] deposit: u128,
584 #[case] expected_to_reth: u128,
585 #[case] expected_to_vault: u128,
586 ) {
587 let mut state = create_state();
588 state.reth_contract_liquidity = reth_contract_liquidity;
589 state.target_reth_collateral_rate = target_reth_collateral_rate;
590
591 let (to_reth, to_vault) = state
592 .compute_deposit_routing(U256::from(deposit))
593 .unwrap();
594
595 assert_eq!(to_reth, U256::from(expected_to_reth));
596 assert_eq!(to_vault, U256::from(expected_to_vault));
597 }
598
599 #[test]
602 fn test_delta_transition_basic() {
603 let mut state = create_state();
604
605 let attributes: HashMap<String, Bytes> = [
606 ("total_eth", U256::from(300u64)),
607 ("reth_supply", U256::from(150u64)),
608 ("deposit_contract_balance", U256::from(100u64)),
609 ("reth_contract_liquidity", U256::from(20u64)),
610 ]
611 .into_iter()
612 .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
613 .collect();
614
615 let delta = ProtocolStateDelta {
616 component_id: "Rocketpool".to_owned(),
617 updated_attributes: attributes,
618 deleted_attributes: HashSet::new(),
619 };
620
621 state
622 .delta_transition(delta, &HashMap::new(), &Balances::default())
623 .unwrap();
624
625 assert_eq!(state.total_eth, U256::from(300u64));
626 assert_eq!(state.reth_supply, U256::from(150u64));
627 assert_eq!(state.deposit_contract_balance, U256::from(100u64));
628 assert_eq!(state.reth_contract_liquidity, U256::from(20u64));
629 }
630
631 #[test]
632 fn test_delta_transition_megapool_fields() {
633 let mut state = create_state();
634
635 let attributes: HashMap<String, Bytes> = [
636 ("megapool_queue_requested_total", U256::from(1000u64)),
637 ("target_reth_collateral_rate", U256::from(20_000_000_000_000_000u64)),
638 ]
639 .into_iter()
640 .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
641 .collect();
642
643 let delta = ProtocolStateDelta {
644 component_id: "Rocketpool".to_owned(),
645 updated_attributes: attributes,
646 deleted_attributes: HashSet::new(),
647 };
648
649 state
650 .delta_transition(delta, &HashMap::new(), &Balances::default())
651 .unwrap();
652
653 assert_eq!(state.megapool_queue_requested_total, U256::from(1000u64));
654 assert_eq!(state.target_reth_collateral_rate, U256::from(20_000_000_000_000_000u64));
655 }
656
657 #[test]
660 fn test_spot_price_deposit() {
661 let state = create_state();
662 let price = state
663 .spot_price(ð_token(), &reth_token())
664 .unwrap();
665 assert_ulps_eq!(price, 0.5);
666 }
667
668 #[test]
669 fn test_spot_price_withdraw() {
670 let state = create_state();
671 let price = state
672 .spot_price(&reth_token(), ð_token())
673 .unwrap();
674 assert_ulps_eq!(price, 1.0 / 0.3);
675 }
676
677 #[test]
681 fn test_live_spot_price_withdrawal() {
682 let state = create_state_at_block_24480104();
683 let price = state
684 .spot_price(ð_token(), &reth_token())
685 .unwrap();
686
687 let on_chain_eth_value = 1_157_737_589_816_937_166f64;
689 let expected = 1e18 / on_chain_eth_value;
690 assert_ulps_eq!(price, expected, max_ulps = 10);
691 }
692
693 #[test]
697 fn test_live_spot_price_deposit() {
698 use crate::evm::protocol::utils::add_fee_markup;
699
700 let state = create_state_at_block_24480104();
701 let price = state
702 .spot_price(&reth_token(), ð_token())
703 .unwrap();
704
705 let on_chain_reth_value = 863_753_590_447_141_981f64;
708 let rate_without_fee = 1e18 / on_chain_reth_value;
709 let fee = 500_000_000_000_000f64 / DEPOSIT_FEE_BASE as f64; let expected = add_fee_markup(rate_without_fee, fee);
711 assert_ulps_eq!(price, expected, max_ulps = 10);
712 }
713
714 #[test]
715 fn test_fee_panics() {
716 let state = create_state();
717 let result = std::panic::catch_unwind(|| state.fee());
718 assert!(result.is_err());
719 }
720
721 #[test]
724 fn test_limits_deposit() {
725 let state = create_state();
726 let (max_sell, max_buy) = state
727 .get_limits(eth_token().address, reth_token().address)
728 .unwrap();
729 assert_eq!(max_sell, BigUint::from(950_000_000_000_000_000_000u128));
731 assert_eq!(max_buy, BigUint::from(285_000_000_000_000_000_000u128));
733 }
734
735 #[test]
736 fn test_limits_withdrawal() {
737 let state = create_state();
738 let (max_sell, max_buy) = state
739 .get_limits(reth_token().address, eth_token().address)
740 .unwrap();
741 assert_eq!(max_buy, BigUint::from(50_000_000_000_000_000_000u128));
743 assert_eq!(max_sell, BigUint::from(25_000_000_000_000_000_000u128));
745 }
746
747 #[test]
748 fn test_limits_with_megapool_queue() {
749 let mut state = create_state();
750 state.max_deposit_pool_size = U256::from(100e18);
751 state.deposit_assigning_enabled = true;
752 state.megapool_queue_requested_total = U256::from(62e18);
753
754 let (max_sell, _) = state
755 .get_limits(eth_token().address, reth_token().address)
756 .unwrap();
757 assert_eq!(max_sell, BigUint::from(112_000_000_000_000_000_000u128));
759 }
760
761 #[test]
764 fn test_limits_deposit_boundary_accepted() {
765 let mut state = create_state();
766 state.deposit_assigning_enabled = true;
767 state.megapool_queue_requested_total = U256::from(64e18);
768
769 let (max_sell, _) = state
770 .get_limits(eth_token().address, reth_token().address)
771 .unwrap();
772
773 let res = state.get_amount_out(max_sell.clone(), ð_token(), &reth_token());
775 assert!(res.is_ok(), "max_sell should be accepted");
776
777 let over = max_sell + BigUint::from(1u64);
779 let res = state.get_amount_out(over, ð_token(), &reth_token());
780 assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
781 }
782
783 #[test]
784 fn test_limits_withdrawal_boundary_accepted() {
785 let mut state = create_state();
786 state.reth_contract_liquidity = U256::from(30e18);
787
788 let (max_sell, _) = state
789 .get_limits(reth_token().address, eth_token().address)
790 .unwrap();
791
792 let res = state.get_amount_out(max_sell.clone(), &reth_token(), ð_token());
794 assert!(res.is_ok(), "max_sell should be accepted");
795
796 let over = max_sell + BigUint::from(1u64);
798 let res = state.get_amount_out(over, &reth_token(), ð_token());
799 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
800 }
801
802 #[test]
805 fn test_deposit_eth() {
806 let state = create_state();
807 let res = state
809 .get_amount_out(
810 BigUint::from(10_000_000_000_000_000_000u128),
811 ð_token(),
812 &reth_token(),
813 )
814 .unwrap();
815
816 assert_eq!(res.amount, BigUint::from(3_000_000_000_000_000_000u128));
817
818 let new_state = res
822 .new_state
823 .as_any()
824 .downcast_ref::<RocketpoolState>()
825 .unwrap();
826 assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
827 assert_eq!(new_state.reth_contract_liquidity, U256::from(2e18));
828 }
829
830 #[test]
831 fn test_withdraw_reth() {
832 let state = create_state();
833 let res = state
835 .get_amount_out(
836 BigUint::from(10_000_000_000_000_000_000u128),
837 &reth_token(),
838 ð_token(),
839 )
840 .unwrap();
841
842 assert_eq!(res.amount, BigUint::from(20_000_000_000_000_000_000u128));
843
844 let new_state = res
845 .new_state
846 .as_any()
847 .downcast_ref::<RocketpoolState>()
848 .unwrap();
849 assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
850 }
851
852 #[test]
855 fn test_deposit_disabled() {
856 let mut state = create_state();
857 state.deposits_enabled = false;
858 let res = state.get_amount_out(BigUint::from(10u64), ð_token(), &reth_token());
859 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
860 }
861
862 #[test]
863 fn test_deposit_below_minimum() {
864 let mut state = create_state();
865 state.min_deposit_amount = U256::from(100u64);
866 let res = state.get_amount_out(BigUint::from(50u64), ð_token(), &reth_token());
867 assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
868 }
869
870 #[test]
871 fn test_deposit_exceeds_max_pool() {
872 let mut state = create_state();
873 state.max_deposit_pool_size = U256::from(60e18);
874 let res = state.get_amount_out(
875 BigUint::from(20_000_000_000_000_000_000u128),
876 ð_token(),
877 &reth_token(),
878 );
879 assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
880 }
881
882 #[test]
883 fn test_withdrawal_insufficient_liquidity() {
884 let state = create_state();
885 let res = state.get_amount_out(
886 BigUint::from(30_000_000_000_000_000_000u128),
887 &reth_token(),
888 ð_token(),
889 );
890 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
891 }
892
893 #[test]
894 fn test_withdrawal_limited_by_queue() {
895 let mut state = create_state();
896 state.deposit_contract_balance = U256::from(100e18);
897 state.megapool_queue_requested_total = U256::from(62e18);
898
899 let res = state.get_amount_out(
901 BigUint::from(20_000_000_000_000_000_000u128),
902 &reth_token(),
903 ð_token(),
904 );
905 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
906
907 let res = state
909 .get_amount_out(
910 BigUint::from(15_000_000_000_000_000_000u128),
911 &reth_token(),
912 ð_token(),
913 )
914 .unwrap();
915 assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
916 }
917
918 #[test]
919 fn test_withdrawal_uses_both_pools() {
920 let mut state = create_state();
921 state.reth_contract_liquidity = U256::from(10e18);
922 state.deposit_contract_balance = U256::from(50e18);
923
924 let res = state
926 .get_amount_out(
927 BigUint::from(15_000_000_000_000_000_000u128),
928 &reth_token(),
929 ð_token(),
930 )
931 .unwrap();
932
933 assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
934
935 let new_state = res
936 .new_state
937 .as_any()
938 .downcast_ref::<RocketpoolState>()
939 .unwrap();
940 assert_eq!(new_state.reth_contract_liquidity, U256::ZERO);
941 assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
942 }
943
944 #[test]
947 fn test_assign_deposits_with_queue() {
948 let mut state = create_state();
949 state.deposit_contract_balance = U256::from(200e18);
950 state.reth_contract_liquidity = U256::from(10e18); state.deposit_assigning_enabled = true;
952 state.deposit_assign_maximum = U256::from(90u64);
953 state.megapool_queue_requested_total = U256::from(128e18); let res = state
957 .get_amount_out(
958 BigUint::from(100_000_000_000_000_000_000u128),
959 ð_token(),
960 &reth_token(),
961 )
962 .unwrap();
963
964 let new_state = res
965 .new_state
966 .as_any()
967 .downcast_ref::<RocketpoolState>()
968 .unwrap();
969 assert_eq!(new_state.deposit_contract_balance, U256::from(204e18));
979 assert_eq!(new_state.megapool_queue_requested_total, U256::from(32e18));
980 }
981
982 #[test]
983 fn test_assign_deposits_empty_queue() {
984 let mut state = create_state();
985 state.deposit_assigning_enabled = true;
986
987 let res = state
988 .get_amount_out(
989 BigUint::from(10_000_000_000_000_000_000u128),
990 ð_token(),
991 &reth_token(),
992 )
993 .unwrap();
994
995 let new_state = res
996 .new_state
997 .as_any()
998 .downcast_ref::<RocketpoolState>()
999 .unwrap();
1000 assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
1002 }
1003
1004 #[test]
1005 fn test_assign_deposits_disabled() {
1006 let mut state = create_state();
1007 state.deposit_assigning_enabled = false;
1008 state.megapool_queue_requested_total = U256::from(100e18);
1009
1010 let res = state
1011 .get_amount_out(
1012 BigUint::from(10_000_000_000_000_000_000u128),
1013 ð_token(),
1014 &reth_token(),
1015 )
1016 .unwrap();
1017
1018 let new_state = res
1019 .new_state
1020 .as_any()
1021 .downcast_ref::<RocketpoolState>()
1022 .unwrap();
1023 assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
1025 }
1026
1027 #[test]
1036 fn test_deposit_split_routing_with_assignment() {
1037 let mut state = create_state();
1038 state.reth_contract_liquidity = U256::ZERO;
1039 state.deposit_contract_balance = U256::from(50e18);
1040 state.deposit_assigning_enabled = true;
1041 state.deposit_assign_maximum = U256::from(90u64);
1042 state.megapool_queue_requested_total = U256::from(96e18); state.target_reth_collateral_rate = U256::from(100_000_000_000_000_000u64); let res = state
1047 .get_amount_out(
1048 BigUint::from(100_000_000_000_000_000_000u128),
1049 ð_token(),
1050 &reth_token(),
1051 )
1052 .unwrap();
1053
1054 let new_state = res
1055 .new_state
1056 .as_any()
1057 .downcast_ref::<RocketpoolState>()
1058 .unwrap();
1059 assert_eq!(new_state.reth_contract_liquidity, U256::from(20e18));
1060 assert_eq!(new_state.deposit_contract_balance, U256::from(34e18));
1061 assert_eq!(new_state.megapool_queue_requested_total, U256::ZERO);
1062 }
1063
1064 fn create_assign_state(
1069 deposit_contract_balance: U256,
1070 megapool_queue_requested_total: U256,
1071 deposit_assign_maximum: U256,
1072 deposit_assign_socialised_maximum: U256,
1073 ) -> RocketpoolState {
1074 let mut state = create_state();
1075 state.deposit_assigning_enabled = true;
1076 state.deposit_contract_balance = deposit_contract_balance;
1077 state.megapool_queue_requested_total = megapool_queue_requested_total;
1078 state.deposit_assign_maximum = deposit_assign_maximum;
1079 state.deposit_assign_socialised_maximum = deposit_assign_socialised_maximum;
1080 state.reth_contract_liquidity = U256::from(10_000_000_000_000_000_000u128);
1082 state
1083 }
1084
1085 #[rstest]
1086 #[case::count_cap_limits(
1089 64_000_000_000_000_000_000u128, 300_000_000_000_000_000_000u128, 128_000_000_000_000_000_000u128, 90u64, 0u64, 64_000_000_000_000_000_000u128, )]
1096 #[case::vault_cap_limits(
1099 200_000_000_000_000_000_000u128,
1100 64_000_000_000_000_000_000u128,
1101 192_000_000_000_000_000_000u128,
1102 90u64,
1103 0u64,
1104 64_000_000_000_000_000_000u128, )]
1106 #[case::queue_depth_limits(
1109 200_000_000_000_000_000_000u128,
1110 300_000_000_000_000_000_000u128,
1111 64_000_000_000_000_000_000u128,
1112 90u64,
1113 0u64,
1114 64_000_000_000_000_000_000u128, )]
1116 #[case::max_cap_limits(
1119 200_000_000_000_000_000_000u128,
1120 300_000_000_000_000_000_000u128,
1121 192_000_000_000_000_000_000u128,
1122 3u64,
1123 0u64,
1124 96_000_000_000_000_000_000u128, )]
1126 #[case::socialised_max(
1129 10_000_000_000_000_000_000u128,
1130 200_000_000_000_000_000_000u128,
1131 128_000_000_000_000_000_000u128,
1132 90u64,
1133 2u64,
1134 64_000_000_000_000_000_000u128, )]
1136 #[case::small_deposit_no_assignment(
1138 10_000_000_000_000_000_000u128,
1139 200_000_000_000_000_000_000u128,
1140 128_000_000_000_000_000_000u128,
1141 90u64,
1142 0u64,
1143 0u128
1144 )]
1145 #[case::vault_below_32(
1148 100_000_000_000_000_000_000u128,
1149 20_000_000_000_000_000_000u128,
1150 128_000_000_000_000_000_000u128,
1151 90u64,
1152 0u64,
1153 0u128
1154 )]
1155 fn test_assign_constraint(
1156 #[case] deposit: u128,
1157 #[case] vault: u128,
1158 #[case] queue: u128,
1159 #[case] max: u64,
1160 #[case] socialised: u64,
1161 #[case] expected_assigned: u128,
1162 ) {
1163 let state = create_assign_state(
1164 U256::from(vault),
1165 U256::from(queue),
1166 U256::from(max),
1167 U256::from(socialised),
1168 );
1169 let assigned = state.calculate_assign_deposits(U256::from(deposit));
1170 assert_eq!(assigned, U256::from(expected_assigned));
1171 }
1172
1173 fn create_state_at_block_24480104() -> RocketpoolState {
1178 RocketpoolState::new(
1179 U256::from_str_radix("489a96a246a2e92bbbd1", 16).unwrap(), U256::from_str_radix("540e645ee4119f4d8b9e", 16).unwrap(), U256::from_str_radix("8dcfa9d0071987bb", 16).unwrap(), U256::from_str_radix("c28d2e1d64f99ea24", 16).unwrap(), U256::from_str_radix("1c6bf52634000", 16).unwrap(), true, U256::from_str_radix("2386f26fc10000", 16).unwrap(), U256::from_str_radix("4f68ca6d8cd91c6000000", 16).unwrap(), true, U256::from(90u64), U256::ZERO, U256::from_str_radix("4a60532ad51bf000000", 16).unwrap(), U256::from(10_000_000_000_000_000u64), )
1193 }
1194
1195 #[test]
1204 fn test_live_deposit_post_saturn() {
1205 let state = create_state_at_block_24480104();
1206
1207 let deposit_amount = BigUint::from(85_000_000_000_000_000_000u128);
1208 let res = state
1209 .get_amount_out(deposit_amount, ð_token(), &reth_token())
1210 .unwrap();
1211
1212 let expected_reth_out = BigUint::from(73_382_345_660_413_064_855u128);
1214 assert_eq!(res.amount, expected_reth_out);
1215
1216 let new_state = res
1218 .new_state
1219 .as_any()
1220 .downcast_ref::<RocketpoolState>()
1221 .unwrap();
1222 assert_eq!(new_state.total_eth, state.total_eth);
1223 assert_eq!(new_state.reth_supply, state.reth_supply);
1224 assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1226 assert_eq!(
1228 new_state.reth_contract_liquidity,
1229 safe_add_u256(
1230 state.reth_contract_liquidity,
1231 U256::from(85_000_000_000_000_000_000u128)
1232 )
1233 .unwrap()
1234 );
1235 assert_eq!(new_state.megapool_queue_requested_total, state.megapool_queue_requested_total);
1237 }
1238
1239 #[test]
1251 fn test_live_burn_post_saturn() {
1252 let state = create_state_at_block_24480104();
1253
1254 let burn_amount = BigUint::from(2_515_686_112_138_065_226u128);
1255 let res = state
1256 .get_amount_out(burn_amount, &reth_token(), ð_token())
1257 .unwrap();
1258
1259 let expected_eth_out = BigUint::from(2_912_504_376_202_664_754u128);
1261 assert_eq!(res.amount, expected_eth_out);
1262
1263 let new_state = res
1265 .new_state
1266 .as_any()
1267 .downcast_ref::<RocketpoolState>()
1268 .unwrap();
1269 assert_eq!(new_state.total_eth, state.total_eth);
1270 assert_eq!(new_state.reth_supply, state.reth_supply);
1271 assert_eq!(
1273 new_state.reth_contract_liquidity,
1274 safe_sub_u256(state.reth_contract_liquidity, U256::from(2_912_504_376_202_664_754u128))
1275 .unwrap()
1276 );
1277 assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1278 }
1279}