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
29const BASE_DEPOSIT_GAS: u64 = 209_000;
31const RETH_MINT_GAS: u64 = 50_000;
32const VAULT_DEPOSIT_OR_EXCESS_GAS: u64 = 20_000;
33const ASSIGN_DEPOSITS_BASE_GAS: u64 = 20_000;
34const GAS_PER_MEGAPOOL_ASSIGNMENT: u64 = 120_000;
36const BASE_WITHDRAWAL_GAS: u64 = 134_000;
37
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39pub struct RocketpoolState {
40 pub reth_supply: U256,
41 pub total_eth: U256,
42 pub deposit_contract_balance: U256,
44 pub reth_contract_liquidity: U256,
46 pub deposit_fee: U256,
49 pub deposits_enabled: bool,
50 pub min_deposit_amount: U256,
51 pub max_deposit_pool_size: U256,
52 pub deposit_assigning_enabled: bool,
54 pub deposit_assign_maximum: U256,
56 pub deposit_assign_socialised_maximum: U256,
58 pub megapool_queue_requested_total: U256,
60 pub target_reth_collateral_rate: U256,
63}
64
65impl RocketpoolState {
66 #[allow(clippy::too_many_arguments)]
67 pub fn new(
68 reth_supply: U256,
69 total_eth: U256,
70 deposit_contract_balance: U256,
71 reth_contract_liquidity: U256,
72 deposit_fee: U256,
73 deposits_enabled: bool,
74 min_deposit_amount: U256,
75 max_deposit_pool_size: U256,
76 deposit_assigning_enabled: bool,
77 deposit_assign_maximum: U256,
78 deposit_assign_socialised_maximum: U256,
79 megapool_queue_requested_total: U256,
80 target_reth_collateral_rate: U256,
81 ) -> Self {
82 Self {
83 reth_supply,
84 total_eth,
85 deposit_contract_balance,
86 reth_contract_liquidity,
87 deposit_fee,
88 deposits_enabled,
89 min_deposit_amount,
90 max_deposit_pool_size,
91 deposit_assigning_enabled,
92 deposit_assign_maximum,
93 deposit_assign_socialised_maximum,
94 megapool_queue_requested_total,
95 target_reth_collateral_rate,
96 }
97 }
98
99 fn get_reth_value(&self, eth_amount: U256) -> Result<U256, SimulationError> {
101 let fee = mul_div(eth_amount, self.deposit_fee, U256::from(DEPOSIT_FEE_BASE))?;
103 let net_eth = safe_sub_u256(eth_amount, fee)?;
104 mul_div(net_eth, self.reth_supply, self.total_eth)
106 }
107
108 fn get_eth_value(&self, reth_amount: U256) -> Result<U256, SimulationError> {
110 mul_div(reth_amount, self.total_eth, self.reth_supply)
112 }
113
114 fn is_depositing_eth(token_in: &Bytes) -> bool {
115 token_in.as_ref() == ETH_ADDRESS
116 }
117
118 fn assert_deposits_enabled(&self) -> Result<(), SimulationError> {
119 if !self.deposits_enabled {
120 Err(SimulationError::RecoverableError(
121 "Deposits are currently disabled in Rocketpool".to_string(),
122 ))
123 } else {
124 Ok(())
125 }
126 }
127
128 fn get_max_deposit_capacity(&self) -> Result<U256, SimulationError> {
135 if self.deposit_assigning_enabled {
136 safe_add_u256(self.max_deposit_pool_size, self.megapool_queue_requested_total)
137 } else {
138 Ok(self.max_deposit_pool_size)
139 }
140 }
141
142 fn get_deposit_pool_excess_balance(&self) -> Result<U256, SimulationError> {
147 if self.megapool_queue_requested_total >= self.deposit_contract_balance {
148 Ok(U256::ZERO)
149 } else {
150 safe_sub_u256(self.deposit_contract_balance, self.megapool_queue_requested_total)
151 }
152 }
153
154 fn get_total_available_for_withdrawal(&self) -> Result<U256, SimulationError> {
157 let deposit_pool_excess = self.get_deposit_pool_excess_balance()?;
158 safe_add_u256(self.reth_contract_liquidity, deposit_pool_excess)
159 }
160
161 fn compute_deposit_routing(
166 &self,
167 deposit_amount: U256,
168 ) -> Result<(U256, U256), SimulationError> {
169 let target_collateral = mul_div(
171 self.total_eth,
172 self.target_reth_collateral_rate,
173 U256::from(DEPOSIT_FEE_BASE),
174 )?;
175 let shortfall = target_collateral.saturating_sub(self.reth_contract_liquidity);
177 let to_reth = deposit_amount.min(shortfall);
178 let to_vault = deposit_amount - to_reth;
179 Ok((to_reth, to_vault))
180 }
181
182 fn calculate_assign_deposits(&self, deposit_amount: U256) -> (U256, u64) {
192 if !self.deposit_assigning_enabled ||
193 self.megapool_queue_requested_total
194 .is_zero()
195 {
196 return (U256::ZERO, 0);
197 }
198
199 let full_deposit_value = U256::from(FULL_DEPOSIT_VALUE);
200
201 let scaling_count = deposit_amount / full_deposit_value;
203 let count_cap = (self.deposit_assign_socialised_maximum + scaling_count)
204 .min(self.deposit_assign_maximum);
205
206 let vault_cap = self.deposit_contract_balance / full_deposit_value;
208
209 let queue_entries = self.megapool_queue_requested_total / full_deposit_value;
211
212 let entries = count_cap
214 .min(vault_cap)
215 .min(queue_entries);
216
217 (entries * full_deposit_value, entries.try_into().unwrap_or(u64::MAX))
218 }
219}
220
221#[typetag::serde]
222impl ProtocolSim for RocketpoolState {
223 fn fee(&self) -> f64 {
224 unimplemented!("Rocketpool has asymmetric fees; use spot_price or get_amount_out instead")
225 }
226
227 fn spot_price(&self, _base: &Token, quote: &Token) -> Result<f64, SimulationError> {
228 let is_depositing_eth = RocketpoolState::is_depositing_eth("e.address);
230
231 let amount = U256::from(1e18);
234
235 let base_per_quote = if is_depositing_eth {
236 self.assert_deposits_enabled()?;
238 self.get_reth_value(amount)?
239 } else {
240 self.get_eth_value(amount)?
242 };
243
244 let base_per_quote = u256_to_f64(base_per_quote)? / 1e18;
245 Ok(1.0 / base_per_quote)
247 }
248
249 fn get_amount_out(
250 &self,
251 amount_in: BigUint,
252 token_in: &Token,
253 _token_out: &Token,
254 ) -> Result<GetAmountOutResult, SimulationError> {
255 let amount_in = biguint_to_u256(&amount_in);
256 let is_depositing_eth = RocketpoolState::is_depositing_eth(&token_in.address);
257
258 let amount_out = if is_depositing_eth {
259 self.assert_deposits_enabled()?;
260
261 if amount_in < self.min_deposit_amount {
262 return Err(SimulationError::InvalidInput(
263 format!(
264 "Deposit amount {} is less than the minimum deposit of {}",
265 amount_in, self.min_deposit_amount
266 ),
267 None,
268 ));
269 }
270
271 let capacity_needed = safe_add_u256(self.deposit_contract_balance, amount_in)?;
272 let max_capacity = self.get_max_deposit_capacity()?;
273 if capacity_needed > max_capacity {
274 return Err(SimulationError::InvalidInput(
275 format!(
276 "Deposit would exceed maximum pool size (capacity needed: {}, max: {})",
277 capacity_needed, max_capacity
278 ),
279 None,
280 ));
281 }
282
283 self.get_reth_value(amount_in)?
284 } else {
285 let eth_out = self.get_eth_value(amount_in)?;
286
287 let total_available = self.get_total_available_for_withdrawal()?;
288 if eth_out > total_available {
289 return Err(SimulationError::RecoverableError(format!(
290 "Withdrawal {} exceeds available liquidity {}",
291 eth_out, total_available
292 )));
293 }
294
295 eth_out
296 };
297
298 let mut new_state = self.clone();
299 let mut gas_used = 0;
300
301 if is_depositing_eth {
303 gas_used += BASE_DEPOSIT_GAS;
304
305 let (to_reth, to_vault) = new_state.compute_deposit_routing(amount_in)?;
307 new_state.reth_contract_liquidity =
308 safe_add_u256(new_state.reth_contract_liquidity, to_reth)?;
309 new_state.deposit_contract_balance =
310 safe_add_u256(new_state.deposit_contract_balance, to_vault)?;
311 if to_reth > U256::ZERO {
312 gas_used += RETH_MINT_GAS;
313 }
314
315 if to_vault > U256::ZERO {
316 gas_used += VAULT_DEPOSIT_OR_EXCESS_GAS;
317 }
318
319 let (eth_assigned, entries) = new_state.calculate_assign_deposits(amount_in);
322
323 if entries > 0 {
324 gas_used += ASSIGN_DEPOSITS_BASE_GAS;
325 gas_used += entries * GAS_PER_MEGAPOOL_ASSIGNMENT;
326 }
327
328 if eth_assigned > U256::ZERO {
329 new_state.deposit_contract_balance =
330 safe_sub_u256(new_state.deposit_contract_balance, eth_assigned)?;
331 new_state.megapool_queue_requested_total =
332 safe_sub_u256(new_state.megapool_queue_requested_total, eth_assigned)?;
333 }
334 } else {
335 gas_used = BASE_WITHDRAWAL_GAS;
336 let needed_from_deposit_pool =
338 amount_out.saturating_sub(new_state.reth_contract_liquidity);
339 new_state.reth_contract_liquidity = new_state
340 .reth_contract_liquidity
341 .saturating_sub(amount_out);
342 new_state.deposit_contract_balance =
343 safe_sub_u256(new_state.deposit_contract_balance, needed_from_deposit_pool)?;
344 }
345
346 Ok(GetAmountOutResult::new(
351 u256_to_biguint(amount_out),
352 BigUint::from(gas_used),
353 Box::new(new_state),
354 ))
355 }
356
357 fn get_limits(
358 &self,
359 sell_token: Bytes,
360 _buy_token: Bytes,
361 ) -> Result<(BigUint, BigUint), SimulationError> {
362 let is_depositing_eth = Self::is_depositing_eth(&sell_token);
363
364 if is_depositing_eth {
365 let max_capacity = self.get_max_deposit_capacity()?;
367 let max_eth_sell = safe_sub_u256(max_capacity, self.deposit_contract_balance)?;
368 let max_reth_buy = self.get_reth_value(max_eth_sell)?;
369 Ok((u256_to_biguint(max_eth_sell), u256_to_biguint(max_reth_buy)))
370 } else {
371 let max_eth_buy = self.get_total_available_for_withdrawal()?;
373 let max_reth_sell = mul_div(max_eth_buy, self.reth_supply, self.total_eth)?;
374 Ok((u256_to_biguint(max_reth_sell), u256_to_biguint(max_eth_buy)))
375 }
376 }
377
378 fn delta_transition(
379 &mut self,
380 delta: ProtocolStateDelta,
381 _tokens: &HashMap<Bytes, Token>,
382 _balances: &Balances,
383 ) -> Result<(), TransitionError> {
384 self.total_eth = delta
385 .updated_attributes
386 .get("total_eth")
387 .map_or(self.total_eth, U256::from_bytes);
388 self.reth_supply = delta
389 .updated_attributes
390 .get("reth_supply")
391 .map_or(self.reth_supply, U256::from_bytes);
392 self.deposit_contract_balance = delta
393 .updated_attributes
394 .get("deposit_contract_balance")
395 .map_or(self.deposit_contract_balance, U256::from_bytes);
396 self.reth_contract_liquidity = delta
397 .updated_attributes
398 .get("reth_contract_liquidity")
399 .map_or(self.reth_contract_liquidity, U256::from_bytes);
400 self.deposits_enabled = delta
401 .updated_attributes
402 .get("deposits_enabled")
403 .map_or(self.deposits_enabled, |val| !U256::from_bytes(val).is_zero());
404 self.deposit_assigning_enabled = delta
405 .updated_attributes
406 .get("deposit_assigning_enabled")
407 .map_or(self.deposit_assigning_enabled, |val| !U256::from_bytes(val).is_zero());
408 self.deposit_fee = delta
409 .updated_attributes
410 .get("deposit_fee")
411 .map_or(self.deposit_fee, U256::from_bytes);
412 self.min_deposit_amount = delta
413 .updated_attributes
414 .get("min_deposit_amount")
415 .map_or(self.min_deposit_amount, U256::from_bytes);
416 self.max_deposit_pool_size = delta
417 .updated_attributes
418 .get("max_deposit_pool_size")
419 .map_or(self.max_deposit_pool_size, U256::from_bytes);
420 self.deposit_assign_maximum = delta
421 .updated_attributes
422 .get("deposit_assign_maximum")
423 .map_or(self.deposit_assign_maximum, U256::from_bytes);
424 self.deposit_assign_socialised_maximum = delta
425 .updated_attributes
426 .get("deposit_assign_socialised_maximum")
427 .map_or(self.deposit_assign_socialised_maximum, U256::from_bytes);
428 self.megapool_queue_requested_total = delta
429 .updated_attributes
430 .get("megapool_queue_requested_total")
431 .map_or(self.megapool_queue_requested_total, U256::from_bytes);
432 self.target_reth_collateral_rate = delta
433 .updated_attributes
434 .get("target_reth_collateral_rate")
435 .map_or(self.target_reth_collateral_rate, U256::from_bytes);
436
437 Ok(())
438 }
439
440 fn clone_box(&self) -> Box<dyn ProtocolSim> {
441 Box::new(self.clone())
442 }
443
444 fn as_any(&self) -> &dyn Any {
445 self
446 }
447
448 fn as_any_mut(&mut self) -> &mut dyn Any {
449 self
450 }
451
452 fn eq(&self, other: &dyn ProtocolSim) -> bool {
453 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
454 self == other_state
455 } else {
456 false
457 }
458 }
459
460 fn query_pool_swap(
461 &self,
462 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
463 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
464 crate::evm::query_pool_swap::query_pool_swap(self, params)
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use std::{
471 collections::{HashMap, HashSet},
472 str::FromStr,
473 };
474
475 use approx::assert_ulps_eq;
476 use num_bigint::BigUint;
477 use rstest::rstest;
478 use tycho_common::{
479 dto::ProtocolStateDelta,
480 hex_bytes::Bytes,
481 models::{token::Token, Chain},
482 simulation::{
483 errors::SimulationError,
484 protocol_sim::{Balances, ProtocolSim},
485 },
486 };
487
488 use super::*;
489
490 fn create_state() -> RocketpoolState {
499 RocketpoolState::new(
500 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), )
515 }
516
517 fn eth_token() -> Token {
518 Token::new(&Bytes::from(ETH_ADDRESS), "ETH", 18, 0, &[Some(100_000)], Chain::Ethereum, 100)
519 }
520
521 fn reth_token() -> Token {
522 Token::new(
523 &Bytes::from_str("0xae78736Cd615f374D3085123A210448E74Fc6393").unwrap(),
524 "rETH",
525 18,
526 0,
527 &[Some(100_000)],
528 Chain::Ethereum,
529 100,
530 )
531 }
532
533 #[test]
536 fn test_max_capacity_assign_disabled() {
537 let state = create_state();
538 assert_eq!(
539 state
540 .get_max_deposit_capacity()
541 .unwrap(),
542 U256::from(1000e18)
543 );
544 }
545
546 #[test]
547 fn test_max_capacity_assign_enabled_empty_queue() {
548 let mut state = create_state();
549 state.deposit_assigning_enabled = true;
550 assert_eq!(
551 state
552 .get_max_deposit_capacity()
553 .unwrap(),
554 U256::from(1000e18)
555 );
556 }
557
558 #[test]
559 fn test_max_capacity_assign_enabled_with_queue() {
560 let mut state = create_state();
561 state.deposit_assigning_enabled = true;
562 state.megapool_queue_requested_total = U256::from(500e18);
563 assert_eq!(
565 state
566 .get_max_deposit_capacity()
567 .unwrap(),
568 U256::from(1500e18)
569 );
570 }
571
572 #[rstest]
575 #[case::all_to_reth(
576 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, )]
582 #[case::split(
584 U256::ZERO,
585 U256::from(10_000_000_000_000_000u64),
586 10_000_000_000_000_000_000u128, 2_000_000_000_000_000_000u128, 8_000_000_000_000_000_000u128, )]
590 #[case::all_to_vault(
592 U256::from(10_000_000_000_000_000_000u128), U256::from(10_000_000_000_000_000u64),
594 5_000_000_000_000_000_000u128, 0u128,
596 5_000_000_000_000_000_000u128,
597 )]
598 #[case::zero_collateral_rate(
600 U256::ZERO,
601 U256::ZERO, 5_000_000_000_000_000_000u128,
603 0u128,
604 5_000_000_000_000_000_000u128,
605 )]
606 fn test_deposit_routing(
607 #[case] reth_contract_liquidity: U256,
608 #[case] target_reth_collateral_rate: U256,
609 #[case] deposit: u128,
610 #[case] expected_to_reth: u128,
611 #[case] expected_to_vault: u128,
612 ) {
613 let mut state = create_state();
614 state.reth_contract_liquidity = reth_contract_liquidity;
615 state.target_reth_collateral_rate = target_reth_collateral_rate;
616
617 let (to_reth, to_vault) = state
618 .compute_deposit_routing(U256::from(deposit))
619 .unwrap();
620
621 assert_eq!(to_reth, U256::from(expected_to_reth));
622 assert_eq!(to_vault, U256::from(expected_to_vault));
623 }
624
625 #[test]
628 fn test_delta_transition_basic() {
629 let mut state = create_state();
630
631 let attributes: HashMap<String, Bytes> = [
632 ("total_eth", U256::from(300u64)),
633 ("reth_supply", U256::from(150u64)),
634 ("deposit_contract_balance", U256::from(100u64)),
635 ("reth_contract_liquidity", U256::from(20u64)),
636 ]
637 .into_iter()
638 .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
639 .collect();
640
641 let delta = ProtocolStateDelta {
642 component_id: "Rocketpool".to_owned(),
643 updated_attributes: attributes,
644 deleted_attributes: HashSet::new(),
645 };
646
647 state
648 .delta_transition(delta, &HashMap::new(), &Balances::default())
649 .unwrap();
650
651 assert_eq!(state.total_eth, U256::from(300u64));
652 assert_eq!(state.reth_supply, U256::from(150u64));
653 assert_eq!(state.deposit_contract_balance, U256::from(100u64));
654 assert_eq!(state.reth_contract_liquidity, U256::from(20u64));
655 }
656
657 #[test]
658 fn test_delta_transition_megapool_fields() {
659 let mut state = create_state();
660
661 let attributes: HashMap<String, Bytes> = [
662 ("megapool_queue_requested_total", U256::from(1000u64)),
663 ("target_reth_collateral_rate", U256::from(20_000_000_000_000_000u64)),
664 ]
665 .into_iter()
666 .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
667 .collect();
668
669 let delta = ProtocolStateDelta {
670 component_id: "Rocketpool".to_owned(),
671 updated_attributes: attributes,
672 deleted_attributes: HashSet::new(),
673 };
674
675 state
676 .delta_transition(delta, &HashMap::new(), &Balances::default())
677 .unwrap();
678
679 assert_eq!(state.megapool_queue_requested_total, U256::from(1000u64));
680 assert_eq!(state.target_reth_collateral_rate, U256::from(20_000_000_000_000_000u64));
681 }
682
683 #[test]
686 fn test_spot_price_deposit() {
687 let state = create_state();
688 let price = state
689 .spot_price(ð_token(), &reth_token())
690 .unwrap();
691 assert_ulps_eq!(price, 0.5);
692 }
693
694 #[test]
695 fn test_spot_price_withdraw() {
696 let state = create_state();
697 let price = state
698 .spot_price(&reth_token(), ð_token())
699 .unwrap();
700 assert_ulps_eq!(price, 1.0 / 0.3);
701 }
702
703 #[test]
707 fn test_live_spot_price_withdrawal() {
708 let state = create_state_at_block_24480104();
709 let price = state
710 .spot_price(ð_token(), &reth_token())
711 .unwrap();
712
713 let on_chain_eth_value = 1_157_737_589_816_937_166f64;
715 let expected = 1e18 / on_chain_eth_value;
716 assert_ulps_eq!(price, expected, max_ulps = 10);
717 }
718
719 #[test]
723 fn test_live_spot_price_deposit() {
724 use crate::evm::protocol::utils::add_fee_markup;
725
726 let state = create_state_at_block_24480104();
727 let price = state
728 .spot_price(&reth_token(), ð_token())
729 .unwrap();
730
731 let on_chain_reth_value = 863_753_590_447_141_981f64;
734 let rate_without_fee = 1e18 / on_chain_reth_value;
735 let fee = 500_000_000_000_000f64 / DEPOSIT_FEE_BASE as f64; let expected = add_fee_markup(rate_without_fee, fee);
737 assert_ulps_eq!(price, expected, max_ulps = 10);
738 }
739
740 #[test]
741 fn test_fee_panics() {
742 let state = create_state();
743 let result = std::panic::catch_unwind(|| state.fee());
744 assert!(result.is_err());
745 }
746
747 #[test]
750 fn test_limits_deposit() {
751 let state = create_state();
752 let (max_sell, max_buy) = state
753 .get_limits(eth_token().address, reth_token().address)
754 .unwrap();
755 assert_eq!(max_sell, BigUint::from(950_000_000_000_000_000_000u128));
757 assert_eq!(max_buy, BigUint::from(285_000_000_000_000_000_000u128));
759 }
760
761 #[test]
762 fn test_limits_withdrawal() {
763 let state = create_state();
764 let (max_sell, max_buy) = state
765 .get_limits(reth_token().address, eth_token().address)
766 .unwrap();
767 assert_eq!(max_buy, BigUint::from(50_000_000_000_000_000_000u128));
769 assert_eq!(max_sell, BigUint::from(25_000_000_000_000_000_000u128));
771 }
772
773 #[test]
774 fn test_limits_with_megapool_queue() {
775 let mut state = create_state();
776 state.max_deposit_pool_size = U256::from(100e18);
777 state.deposit_assigning_enabled = true;
778 state.megapool_queue_requested_total = U256::from(62e18);
779
780 let (max_sell, _) = state
781 .get_limits(eth_token().address, reth_token().address)
782 .unwrap();
783 assert_eq!(max_sell, BigUint::from(112_000_000_000_000_000_000u128));
785 }
786
787 #[test]
790 fn test_limits_deposit_boundary_accepted() {
791 let mut state = create_state();
792 state.deposit_assigning_enabled = true;
793 state.megapool_queue_requested_total = U256::from(64e18);
794
795 let (max_sell, _) = state
796 .get_limits(eth_token().address, reth_token().address)
797 .unwrap();
798
799 let res = state.get_amount_out(max_sell.clone(), ð_token(), &reth_token());
801 assert!(res.is_ok(), "max_sell should be accepted");
802
803 let over = max_sell + BigUint::from(1u64);
805 let res = state.get_amount_out(over, ð_token(), &reth_token());
806 assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
807 }
808
809 #[test]
810 fn test_limits_withdrawal_boundary_accepted() {
811 let mut state = create_state();
812 state.reth_contract_liquidity = U256::from(30e18);
813
814 let (max_sell, _) = state
815 .get_limits(reth_token().address, eth_token().address)
816 .unwrap();
817
818 let res = state.get_amount_out(max_sell.clone(), &reth_token(), ð_token());
820 assert!(res.is_ok(), "max_sell should be accepted");
821
822 let over = max_sell + BigUint::from(1u64);
824 let res = state.get_amount_out(over, &reth_token(), ð_token());
825 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
826 }
827
828 #[test]
831 fn test_deposit_eth() {
832 let state = create_state();
833 let res = state
835 .get_amount_out(
836 BigUint::from(10_000_000_000_000_000_000u128),
837 ð_token(),
838 &reth_token(),
839 )
840 .unwrap();
841
842 assert_eq!(res.amount, BigUint::from(3_000_000_000_000_000_000u128));
843
844 let new_state = res
848 .new_state
849 .as_any()
850 .downcast_ref::<RocketpoolState>()
851 .unwrap();
852 assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
853 assert_eq!(new_state.reth_contract_liquidity, U256::from(2e18));
854 }
855
856 #[test]
857 fn test_withdraw_reth() {
858 let state = create_state();
859 let res = state
861 .get_amount_out(
862 BigUint::from(10_000_000_000_000_000_000u128),
863 &reth_token(),
864 ð_token(),
865 )
866 .unwrap();
867
868 assert_eq!(res.amount, BigUint::from(20_000_000_000_000_000_000u128));
869
870 let new_state = res
871 .new_state
872 .as_any()
873 .downcast_ref::<RocketpoolState>()
874 .unwrap();
875 assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
876 }
877
878 #[test]
881 fn test_deposit_disabled() {
882 let mut state = create_state();
883 state.deposits_enabled = false;
884 let res = state.get_amount_out(BigUint::from(10u64), ð_token(), &reth_token());
885 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
886 }
887
888 #[test]
889 fn test_deposit_below_minimum() {
890 let mut state = create_state();
891 state.min_deposit_amount = U256::from(100u64);
892 let res = state.get_amount_out(BigUint::from(50u64), ð_token(), &reth_token());
893 assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
894 }
895
896 #[test]
897 fn test_deposit_exceeds_max_pool() {
898 let mut state = create_state();
899 state.max_deposit_pool_size = U256::from(60e18);
900 let res = state.get_amount_out(
901 BigUint::from(20_000_000_000_000_000_000u128),
902 ð_token(),
903 &reth_token(),
904 );
905 assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
906 }
907
908 #[test]
909 fn test_withdrawal_insufficient_liquidity() {
910 let state = create_state();
911 let res = state.get_amount_out(
912 BigUint::from(30_000_000_000_000_000_000u128),
913 &reth_token(),
914 ð_token(),
915 );
916 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
917 }
918
919 #[test]
920 fn test_withdrawal_limited_by_queue() {
921 let mut state = create_state();
922 state.deposit_contract_balance = U256::from(100e18);
923 state.megapool_queue_requested_total = U256::from(62e18);
924
925 let res = state.get_amount_out(
927 BigUint::from(20_000_000_000_000_000_000u128),
928 &reth_token(),
929 ð_token(),
930 );
931 assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
932
933 let res = state
935 .get_amount_out(
936 BigUint::from(15_000_000_000_000_000_000u128),
937 &reth_token(),
938 ð_token(),
939 )
940 .unwrap();
941 assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
942 }
943
944 #[test]
945 fn test_withdrawal_uses_both_pools() {
946 let mut state = create_state();
947 state.reth_contract_liquidity = U256::from(10e18);
948 state.deposit_contract_balance = U256::from(50e18);
949
950 let res = state
952 .get_amount_out(
953 BigUint::from(15_000_000_000_000_000_000u128),
954 &reth_token(),
955 ð_token(),
956 )
957 .unwrap();
958
959 assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
960
961 let new_state = res
962 .new_state
963 .as_any()
964 .downcast_ref::<RocketpoolState>()
965 .unwrap();
966 assert_eq!(new_state.reth_contract_liquidity, U256::ZERO);
967 assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
968 }
969
970 #[test]
973 fn test_assign_deposits_with_queue() {
974 let mut state = create_state();
975 state.deposit_contract_balance = U256::from(200e18);
976 state.reth_contract_liquidity = U256::from(10e18); state.deposit_assigning_enabled = true;
978 state.deposit_assign_maximum = U256::from(90u64);
979 state.megapool_queue_requested_total = U256::from(128e18); let res = state
983 .get_amount_out(
984 BigUint::from(100_000_000_000_000_000_000u128),
985 ð_token(),
986 &reth_token(),
987 )
988 .unwrap();
989
990 let new_state = res
991 .new_state
992 .as_any()
993 .downcast_ref::<RocketpoolState>()
994 .unwrap();
995 assert_eq!(new_state.deposit_contract_balance, U256::from(204e18));
1005 assert_eq!(new_state.megapool_queue_requested_total, U256::from(32e18));
1006 }
1007
1008 #[test]
1009 fn test_assign_deposits_empty_queue() {
1010 let mut state = create_state();
1011 state.deposit_assigning_enabled = true;
1012
1013 let res = state
1014 .get_amount_out(
1015 BigUint::from(10_000_000_000_000_000_000u128),
1016 ð_token(),
1017 &reth_token(),
1018 )
1019 .unwrap();
1020
1021 let new_state = res
1022 .new_state
1023 .as_any()
1024 .downcast_ref::<RocketpoolState>()
1025 .unwrap();
1026 assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
1028 }
1029
1030 #[test]
1031 fn test_assign_deposits_disabled() {
1032 let mut state = create_state();
1033 state.deposit_assigning_enabled = false;
1034 state.megapool_queue_requested_total = U256::from(100e18);
1035
1036 let res = state
1037 .get_amount_out(
1038 BigUint::from(10_000_000_000_000_000_000u128),
1039 ð_token(),
1040 &reth_token(),
1041 )
1042 .unwrap();
1043
1044 let new_state = res
1045 .new_state
1046 .as_any()
1047 .downcast_ref::<RocketpoolState>()
1048 .unwrap();
1049 assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
1051 }
1052
1053 #[test]
1062 fn test_deposit_split_routing_with_assignment() {
1063 let mut state = create_state();
1064 state.reth_contract_liquidity = U256::ZERO;
1065 state.deposit_contract_balance = U256::from(50e18);
1066 state.deposit_assigning_enabled = true;
1067 state.deposit_assign_maximum = U256::from(90u64);
1068 state.megapool_queue_requested_total = U256::from(96e18); state.target_reth_collateral_rate = U256::from(100_000_000_000_000_000u64); let res = state
1073 .get_amount_out(
1074 BigUint::from(100_000_000_000_000_000_000u128),
1075 ð_token(),
1076 &reth_token(),
1077 )
1078 .unwrap();
1079
1080 let new_state = res
1081 .new_state
1082 .as_any()
1083 .downcast_ref::<RocketpoolState>()
1084 .unwrap();
1085 assert_eq!(new_state.reth_contract_liquidity, U256::from(20e18));
1086 assert_eq!(new_state.deposit_contract_balance, U256::from(34e18));
1087 assert_eq!(new_state.megapool_queue_requested_total, U256::ZERO);
1088 }
1089
1090 fn create_assign_state(
1095 deposit_contract_balance: U256,
1096 megapool_queue_requested_total: U256,
1097 deposit_assign_maximum: U256,
1098 deposit_assign_socialised_maximum: U256,
1099 ) -> RocketpoolState {
1100 let mut state = create_state();
1101 state.deposit_assigning_enabled = true;
1102 state.deposit_contract_balance = deposit_contract_balance;
1103 state.megapool_queue_requested_total = megapool_queue_requested_total;
1104 state.deposit_assign_maximum = deposit_assign_maximum;
1105 state.deposit_assign_socialised_maximum = deposit_assign_socialised_maximum;
1106 state.reth_contract_liquidity = U256::from(10_000_000_000_000_000_000u128);
1108 state
1109 }
1110
1111 #[rstest]
1112 #[case::count_cap_limits(
1115 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, )]
1122 #[case::vault_cap_limits(
1125 200_000_000_000_000_000_000u128,
1126 64_000_000_000_000_000_000u128,
1127 192_000_000_000_000_000_000u128,
1128 90u64,
1129 0u64,
1130 64_000_000_000_000_000_000u128, )]
1132 #[case::queue_depth_limits(
1135 200_000_000_000_000_000_000u128,
1136 300_000_000_000_000_000_000u128,
1137 64_000_000_000_000_000_000u128,
1138 90u64,
1139 0u64,
1140 64_000_000_000_000_000_000u128, )]
1142 #[case::max_cap_limits(
1145 200_000_000_000_000_000_000u128,
1146 300_000_000_000_000_000_000u128,
1147 192_000_000_000_000_000_000u128,
1148 3u64,
1149 0u64,
1150 96_000_000_000_000_000_000u128, )]
1152 #[case::socialised_max(
1155 10_000_000_000_000_000_000u128,
1156 200_000_000_000_000_000_000u128,
1157 128_000_000_000_000_000_000u128,
1158 90u64,
1159 2u64,
1160 64_000_000_000_000_000_000u128, )]
1162 #[case::small_deposit_no_assignment(
1164 10_000_000_000_000_000_000u128,
1165 200_000_000_000_000_000_000u128,
1166 128_000_000_000_000_000_000u128,
1167 90u64,
1168 0u64,
1169 0u128
1170 )]
1171 #[case::vault_below_32(
1174 100_000_000_000_000_000_000u128,
1175 20_000_000_000_000_000_000u128,
1176 128_000_000_000_000_000_000u128,
1177 90u64,
1178 0u64,
1179 0u128
1180 )]
1181 fn test_assign_constraint(
1182 #[case] deposit: u128,
1183 #[case] vault: u128,
1184 #[case] queue: u128,
1185 #[case] max: u64,
1186 #[case] socialised: u64,
1187 #[case] expected_assigned: u128,
1188 ) {
1189 let state = create_assign_state(
1190 U256::from(vault),
1191 U256::from(queue),
1192 U256::from(max),
1193 U256::from(socialised),
1194 );
1195 let (assigned, _gas_used) = state.calculate_assign_deposits(U256::from(deposit));
1196 assert_eq!(assigned, U256::from(expected_assigned));
1197 }
1198
1199 fn create_state_at_block_24480104() -> RocketpoolState {
1204 RocketpoolState::new(
1205 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), )
1219 }
1220
1221 #[test]
1230 fn test_live_deposit_post_saturn() {
1231 let state = create_state_at_block_24480104();
1232
1233 let deposit_amount = BigUint::from(85_000_000_000_000_000_000u128);
1234 let res = state
1235 .get_amount_out(deposit_amount, ð_token(), &reth_token())
1236 .unwrap();
1237
1238 let expected_reth_out = BigUint::from(73_382_345_660_413_064_855u128);
1240 assert_eq!(res.amount, expected_reth_out);
1241
1242 let new_state = res
1244 .new_state
1245 .as_any()
1246 .downcast_ref::<RocketpoolState>()
1247 .unwrap();
1248 assert_eq!(new_state.total_eth, state.total_eth);
1249 assert_eq!(new_state.reth_supply, state.reth_supply);
1250 assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1252 assert_eq!(
1254 new_state.reth_contract_liquidity,
1255 safe_add_u256(
1256 state.reth_contract_liquidity,
1257 U256::from(85_000_000_000_000_000_000u128)
1258 )
1259 .unwrap()
1260 );
1261 assert_eq!(new_state.megapool_queue_requested_total, state.megapool_queue_requested_total);
1263 }
1264
1265 #[test]
1277 fn test_live_burn_post_saturn() {
1278 let state = create_state_at_block_24480104();
1279
1280 let burn_amount = BigUint::from(2_515_686_112_138_065_226u128);
1281 let res = state
1282 .get_amount_out(burn_amount, &reth_token(), ð_token())
1283 .unwrap();
1284
1285 let expected_eth_out = BigUint::from(2_912_504_376_202_664_754u128);
1287 assert_eq!(res.amount, expected_eth_out);
1288
1289 let new_state = res
1291 .new_state
1292 .as_any()
1293 .downcast_ref::<RocketpoolState>()
1294 .unwrap();
1295 assert_eq!(new_state.total_eth, state.total_eth);
1296 assert_eq!(new_state.reth_supply, state.reth_supply);
1297 assert_eq!(
1299 new_state.reth_contract_liquidity,
1300 safe_sub_u256(state.reth_contract_liquidity, U256::from(2_912_504_376_202_664_754u128))
1301 .unwrap()
1302 );
1303 assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1304 }
1305}