Skip to main content

tycho_simulation/evm/protocol/rocketpool/
state.rs

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_mul_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; // 1e18
25const VARIABLE_DEPOSIT_AMOUNT: u128 = 31_000_000_000_000_000_000; // 31 ETH
26
27// Queue capacity for wrap-around calculation (from RocketMinipoolQueue contract)
28// capacity = 2^255 (max uint256 / 2)
29fn queue_capacity() -> U256 {
30    U256::from(1) << 255
31}
32
33#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
34pub struct RocketpoolState {
35    pub reth_supply: U256,
36    pub total_eth: U256,
37    /// ETH available in the deposit pool contract
38    pub deposit_contract_balance: U256,
39    /// ETH available in the rETH contract
40    pub reth_contract_liquidity: U256,
41    /// Deposit fee as %, scaled by DEPOSIT_FEE_BASE, such as 5000000000000000 represents 0.5% fee.
42    pub deposit_fee: U256,
43    pub deposits_enabled: bool,
44    pub min_deposit_amount: U256,
45    pub max_deposit_pool_size: U256,
46    /// Whether assigning deposits is enabled (allows using minipool queue capacity)
47    pub deposit_assigning_enabled: bool,
48    /// Maximum number of minipool assignments per deposit
49    pub deposit_assign_maximum: U256,
50    /// The base number of minipools to try to assign per deposit
51    pub deposit_assign_socialised_maximum: U256,
52    /// Minipool queue indices for variable deposits
53    pub queue_variable_start: U256,
54    pub queue_variable_end: U256,
55}
56
57impl RocketpoolState {
58    #[allow(clippy::too_many_arguments)]
59    pub fn new(
60        reth_supply: U256,
61        total_eth: U256,
62        deposit_contract_balance: U256,
63        reth_contract_liquidity: U256,
64        deposit_fee: U256,
65        deposits_enabled: bool,
66        min_deposit_amount: U256,
67        max_deposit_pool_size: U256,
68        deposit_assigning_enabled: bool,
69        deposit_assign_maximum: U256,
70        deposit_assign_socialised_maximum: U256,
71        queue_variable_start: U256,
72        queue_variable_end: U256,
73    ) -> Self {
74        Self {
75            reth_supply,
76            total_eth,
77            deposit_contract_balance,
78            reth_contract_liquidity,
79            deposit_fee,
80            deposits_enabled,
81            min_deposit_amount,
82            max_deposit_pool_size,
83            deposit_assigning_enabled,
84            deposit_assign_maximum,
85            deposit_assign_socialised_maximum,
86            queue_variable_start,
87            queue_variable_end,
88        }
89    }
90
91    /// Calculates rETH amount out for a given ETH deposit amount.
92    fn get_reth_value(&self, eth_amount: U256) -> Result<U256, SimulationError> {
93        // fee = ethIn * deposit_fee / DEPOSIT_FEE_BASE
94        let fee = mul_div(eth_amount, self.deposit_fee, U256::from(DEPOSIT_FEE_BASE))?;
95        let net_eth = safe_sub_u256(eth_amount, fee)?;
96
97        // rethOut = netEth * rethSupply / totalEth
98        mul_div(net_eth, self.reth_supply, self.total_eth)
99    }
100
101    /// Calculates ETH amount out for a given rETH burn amount.
102    fn get_eth_value(&self, reth_amount: U256) -> Result<U256, SimulationError> {
103        // ethOut = rethIn * totalEth / rethSupply
104        mul_div(reth_amount, self.total_eth, self.reth_supply)
105    }
106
107    fn is_depositing_eth(token_in: &Bytes) -> bool {
108        token_in.as_ref() == ETH_ADDRESS
109    }
110
111    fn assert_deposits_enabled(&self) -> Result<(), SimulationError> {
112        if !self.deposits_enabled {
113            Err(SimulationError::RecoverableError(
114                "Deposits are currently disabled in Rocketpool".to_string(),
115            ))
116        } else {
117            Ok(())
118        }
119    }
120
121    /// Calculates the length of a queue given its start and end indices.
122    /// Handles wrap-around when end < start.
123    fn get_queue_length(start: U256, end: U256) -> U256 {
124        if end < start {
125            // Wrap-around case: end = end + capacity
126            end + queue_capacity() - start
127        } else {
128            end - start
129        }
130    }
131
132    /// Calculates the total effective capacity of the minipool queues.
133    ///
134    /// The full formula from RocketMinipoolQueue.getEffectiveCapacity():
135    /// full_queue_length * FULL_DEPOSIT_USER_AMOUNT
136    /// + half_queue_length * HALF_DEPOSIT_USER_AMOUNT
137    /// + variable_queue_length * VARIABLE_DEPOSIT_AMOUNT
138    ///
139    /// However, since Rocketpool v1.2, the full and half queues are empty and can no longer be
140    /// expanded, which means only the variable queue contributes to effective capacity.
141    /// If this assumption ever changes, there is a check on the indexer side that will fail loudly.
142    fn get_effective_capacity(&self) -> Result<U256, SimulationError> {
143        let variable_length =
144            Self::get_queue_length(self.queue_variable_start, self.queue_variable_end);
145
146        let variable_capacity =
147            safe_mul_u256(variable_length, U256::from(VARIABLE_DEPOSIT_AMOUNT))?;
148
149        Ok(variable_capacity)
150    }
151
152    /// Returns the maximum deposit capacity considering both the base pool size
153    /// and the minipool queue effective capacity (if deposit_assigning_enabled).
154    fn get_max_deposit_capacity(&self) -> Result<U256, SimulationError> {
155        if self.deposit_assigning_enabled {
156            let effective_capacity = self.get_effective_capacity()?;
157            safe_add_u256(self.max_deposit_pool_size, effective_capacity)
158        } else {
159            Ok(self.max_deposit_pool_size)
160        }
161    }
162
163    /// Returns the excess balance available for withdrawals from the deposit pool.
164    ///
165    /// Formula from RocketDepositPool.getExcessBalance():
166    /// if minipoolCapacity >= balance: return 0
167    /// else: return balance - minipoolCapacity
168    fn get_deposit_pool_excess_balance(&self) -> Result<U256, SimulationError> {
169        let minipool_capacity = self.get_effective_capacity()?;
170        if minipool_capacity >= self.deposit_contract_balance {
171            Ok(U256::ZERO)
172        } else {
173            safe_sub_u256(self.deposit_contract_balance, minipool_capacity)
174        }
175    }
176
177    /// Returns total available liquidity for withdrawals.
178    /// This is the sum of reth_contract_liquidity and the deposit pool excess balance.
179    fn get_total_available_for_withdrawal(&self) -> Result<U256, SimulationError> {
180        let deposit_pool_excess = self.get_deposit_pool_excess_balance()?;
181        safe_add_u256(self.reth_contract_liquidity, deposit_pool_excess)
182    }
183
184    /// Calculates the number of minipools to dequeue and the resulting ETH to assign given a
185    /// deposit. Returns (minipools_dequeued, eth_assigned) or panics for legacy queue.
186    ///
187    /// This method assumes deposit has already been added to deposit_contract_balance.
188    ///
189    /// Logic from _assignDepositsNew:
190    /// - scalingCount = deposit_amount / variableDepositAmount
191    /// - totalEthCount = new_balance / variableDepositAmount
192    /// - assignments = socialisedMax + scalingCount
193    /// - assignments = min(assignments, totalEthCount, maxAssignments, variable_queue_length)
194    /// - eth_assigned = assignments * variableDepositAmount
195    fn calculate_assign_deposits(
196        &self,
197        deposit_amount: U256,
198    ) -> Result<(U256, U256), SimulationError> {
199        if !self.deposit_assigning_enabled {
200            return Ok((U256::ZERO, U256::ZERO));
201        }
202
203        // The assignment logic in Rocketpool has both legacy (full/half) and variable queue
204        // handling. However since the V1.2 upgrade minipools can no longer be added to legacy
205        // queues, and since at the time of the upgrade legacy queues were already empty, we can
206        // assume that the legacy deposit assignment logic will never be called.
207        // If this assumption changes in the future, the indexer side has checks to fail loudly.
208
209        let variable_deposit = U256::from(VARIABLE_DEPOSIT_AMOUNT);
210
211        // Calculate assignments
212        let scaling_count = deposit_amount / variable_deposit;
213        let desired_assignments = self.deposit_assign_socialised_maximum + scaling_count;
214
215        let eth_cap_assignments = self.deposit_contract_balance / variable_deposit;
216        let settings_cap_assignments = self.deposit_assign_maximum;
217        let queue_cap_assignments =
218            Self::get_queue_length(self.queue_variable_start, self.queue_variable_end);
219
220        // Capped by available balance, max assignment setting and available queue capacity
221        let assignments = desired_assignments
222            .min(eth_cap_assignments)
223            .min(settings_cap_assignments)
224            .min(queue_cap_assignments);
225
226        let eth_assigned = safe_mul_u256(assignments, variable_deposit)?;
227
228        Ok((assignments, eth_assigned))
229    }
230}
231
232#[typetag::serde]
233impl ProtocolSim for RocketpoolState {
234    fn fee(&self) -> f64 {
235        unimplemented!("Rocketpool has asymmetric fees; use spot_price or get_amount_out instead")
236    }
237
238    fn spot_price(&self, _base: &Token, quote: &Token) -> Result<f64, SimulationError> {
239        // As we are computing the amount of quote needed to buy 1 base, we check the quote token.
240        let is_depositing_eth = RocketpoolState::is_depositing_eth(&quote.address);
241
242        // As the protocol has no slippage, we can use a fixed amount for spot price calculation.
243        // We compute how much base we get for 1 quote, then invert to get quote needed for 1 base.
244        let amount = U256::from(1e18);
245
246        let base_per_quote = if is_depositing_eth {
247            // base=rETH, quote=ETH: compute rETH for given ETH
248            self.assert_deposits_enabled()?;
249            self.get_reth_value(amount)?
250        } else {
251            // base=ETH, quote=rETH: compute ETH for given rETH
252            self.get_eth_value(amount)?
253        };
254
255        let base_per_quote = u256_to_f64(base_per_quote)? / 1e18;
256
257        // Invert to get how much quote needed to buy 1 base
258        Ok(1.0 / base_per_quote)
259    }
260
261    #[allow(clippy::collapsible_else_if)]
262    fn get_amount_out(
263        &self,
264        amount_in: BigUint,
265        token_in: &Token,
266        _token_out: &Token,
267    ) -> Result<GetAmountOutResult, SimulationError> {
268        let amount_in = biguint_to_u256(&amount_in);
269        let is_depositing_eth = RocketpoolState::is_depositing_eth(&token_in.address);
270
271        let amount_out = if is_depositing_eth {
272            self.assert_deposits_enabled()?;
273
274            if amount_in < self.min_deposit_amount {
275                return Err(SimulationError::InvalidInput(
276                    format!(
277                        "Deposit amount {} is less than the minimum deposit of {}",
278                        amount_in, self.min_deposit_amount
279                    ),
280                    None,
281                ));
282            }
283
284            let capacity_needed = safe_add_u256(self.deposit_contract_balance, amount_in)?;
285            let max_capacity = self.get_max_deposit_capacity()?;
286            if capacity_needed > max_capacity {
287                return Err(SimulationError::InvalidInput(
288                    format!(
289                        "Deposit would exceed maximum pool size (capacity needed: {}, max: {})",
290                        capacity_needed, max_capacity
291                    ),
292                    None,
293                ));
294            }
295
296            self.get_reth_value(amount_in)?
297        } else {
298            let eth_out = self.get_eth_value(amount_in)?;
299
300            let total_available = self.get_total_available_for_withdrawal()?;
301            if eth_out > total_available {
302                return Err(SimulationError::RecoverableError(format!(
303                    "Withdrawal {} exceeds available liquidity {}",
304                    eth_out, total_available
305                )));
306            }
307
308            eth_out
309        };
310
311        let mut new_state = self.clone();
312        // Note: total_eth and reth_supply are not updated as they are managed by an oracle.
313        if is_depositing_eth {
314            new_state.deposit_contract_balance =
315                safe_add_u256(new_state.deposit_contract_balance, amount_in)?;
316
317            // Process assign deposits - dequeue minipools and withdraw ETH from vault
318            let (assignments, eth_assigned) = new_state.calculate_assign_deposits(amount_in)?;
319            if assignments > U256::ZERO {
320                new_state.deposit_contract_balance =
321                    safe_sub_u256(new_state.deposit_contract_balance, eth_assigned)?;
322                new_state.queue_variable_start =
323                    safe_add_u256(new_state.queue_variable_start, assignments)?;
324            }
325        } else {
326            if amount_out <= new_state.reth_contract_liquidity {
327                // If there is sufficient liquidity in rETH contract, withdraw directly
328                new_state.reth_contract_liquidity =
329                    safe_sub_u256(new_state.reth_contract_liquidity, amount_out)?;
330            } else {
331                // Otherwise, use liquidity from the deposit pool contract
332                let needed_from_deposit_pool =
333                    safe_sub_u256(amount_out, new_state.reth_contract_liquidity)?;
334                new_state.deposit_contract_balance =
335                    safe_sub_u256(new_state.deposit_contract_balance, needed_from_deposit_pool)?;
336                new_state.reth_contract_liquidity = U256::ZERO;
337            }
338        };
339
340        // The ETH withdrawal gas estimation assumes a best case scenario when there is sufficient
341        // liquidity in the rETH contract. Note that there has never been a situation during a
342        // withdrawal when this was not the case, hence the simplified gas estimation.
343        let gas_used = if is_depositing_eth { 209_000u32 } else { 134_000u32 };
344
345        Ok(GetAmountOutResult::new(
346            u256_to_biguint(amount_out),
347            BigUint::from(gas_used),
348            Box::new(new_state),
349        ))
350    }
351
352    fn get_limits(
353        &self,
354        sell_token: Bytes,
355        _buy_token: Bytes,
356    ) -> Result<(BigUint, BigUint), SimulationError> {
357        let is_depositing_eth = Self::is_depositing_eth(&sell_token);
358
359        if is_depositing_eth {
360            // ETH -> rETH: max sell = maxDepositPoolSize +
361            // + effectiveCapacity (if assignDepositsEnabled) - deposit_contract_balance
362            let max_capacity = self.get_max_deposit_capacity()?;
363            let max_eth_sell = safe_sub_u256(max_capacity, self.deposit_contract_balance)?;
364            let max_reth_buy = self.get_reth_value(max_eth_sell)?;
365            Ok((u256_to_biguint(max_eth_sell), u256_to_biguint(max_reth_buy)))
366        } else {
367            // rETH -> ETH: max buy = total available for withdrawal
368            let max_eth_buy = self.get_total_available_for_withdrawal()?;
369            let max_reth_sell = mul_div(max_eth_buy, self.reth_supply, self.total_eth)?;
370            Ok((u256_to_biguint(max_reth_sell), u256_to_biguint(max_eth_buy)))
371        }
372    }
373
374    fn delta_transition(
375        &mut self,
376        delta: ProtocolStateDelta,
377        _tokens: &HashMap<Bytes, Token>,
378        _balances: &Balances,
379    ) -> Result<(), TransitionError<String>> {
380        self.total_eth = delta
381            .updated_attributes
382            .get("total_eth")
383            .map_or(self.total_eth, U256::from_bytes);
384        self.reth_supply = delta
385            .updated_attributes
386            .get("reth_supply")
387            .map_or(self.reth_supply, U256::from_bytes);
388
389        self.deposit_contract_balance = delta
390            .updated_attributes
391            .get("deposit_contract_balance")
392            .map_or(self.deposit_contract_balance, U256::from_bytes);
393        self.reth_contract_liquidity = delta
394            .updated_attributes
395            .get("reth_contract_liquidity")
396            .map_or(self.reth_contract_liquidity, U256::from_bytes);
397
398        self.deposits_enabled = delta
399            .updated_attributes
400            .get("deposits_enabled")
401            .map_or(self.deposits_enabled, |val| !U256::from_bytes(val).is_zero());
402        self.deposit_assigning_enabled = delta
403            .updated_attributes
404            .get("deposit_assigning_enabled")
405            .map_or(self.deposit_assigning_enabled, |val| !U256::from_bytes(val).is_zero());
406        self.deposit_fee = delta
407            .updated_attributes
408            .get("deposit_fee")
409            .map_or(self.deposit_fee, U256::from_bytes);
410        self.min_deposit_amount = delta
411            .updated_attributes
412            .get("min_deposit_amount")
413            .map_or(self.min_deposit_amount, U256::from_bytes);
414        self.max_deposit_pool_size = delta
415            .updated_attributes
416            .get("max_deposit_pool_size")
417            .map_or(self.max_deposit_pool_size, U256::from_bytes);
418        self.deposit_assign_maximum = delta
419            .updated_attributes
420            .get("deposit_assign_maximum")
421            .map_or(self.deposit_assign_maximum, U256::from_bytes);
422        self.deposit_assign_socialised_maximum = delta
423            .updated_attributes
424            .get("deposit_assign_socialised_maximum")
425            .map_or(self.deposit_assign_socialised_maximum, U256::from_bytes);
426
427        self.queue_variable_start = delta
428            .updated_attributes
429            .get("queue_variable_start")
430            .map_or(self.queue_variable_start, U256::from_bytes);
431        self.queue_variable_end = delta
432            .updated_attributes
433            .get("queue_variable_end")
434            .map_or(self.queue_variable_end, U256::from_bytes);
435
436        Ok(())
437    }
438
439    // TODO - consider using a trait default implementation
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 num_traits::ToPrimitive;
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    use crate::evm::protocol::utils::add_fee_markup;
490
491    /// Helper function to create a RocketpoolState with easy-to-compute defaults for testing
492    /// Mutate fields for specific tests.
493    /// - Exchange rate: 1 rETH = 2 ETH (100 rETH backed by 200 ETH)
494    /// - Deposit fee: 40%
495    /// - Deposit contract balance: 50 ETH
496    /// - rETH contract liquidity: 0 ETH
497    /// - Max pool size: 1000 ETH
498    /// - Assign deposits enabled: false
499    fn create_state() -> RocketpoolState {
500        RocketpoolState::new(
501            U256::from(100e18),                     // reth_supply: 100 rETH
502            U256::from(200e18),                     // total_eth: 200 ETH (1 rETH = 2 ETH)
503            U256::from(50e18),                      // deposit_contract_balance: 50 ETH
504            U256::ZERO,                             // reth_contract_liquidity: 0 ETH
505            U256::from(400_000_000_000_000_000u64), // deposit_fee: 40% (0.4e18)
506            true,                                   // deposits_enabled
507            U256::ZERO,                             // min_deposit_amount
508            U256::from(1000e18),                    // max_deposit_pool_size: 1000 ETH
509            false,                                  // deposit_assigning_enabled
510            U256::ZERO,                             // deposit_assign_maximum
511            U256::ZERO,                             // deposit_assign_socialised_maximum
512            U256::ZERO,                             // queue_variable_start
513            U256::ZERO,                             // queue_variable_end
514        )
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    // ============ Queue Length Tests ============
534
535    #[test]
536    fn test_queue_length_normal() {
537        let length = RocketpoolState::get_queue_length(U256::from(10), U256::from(15));
538        assert_eq!(length, U256::from(5));
539    }
540
541    #[test]
542    fn test_queue_length_empty() {
543        let length = RocketpoolState::get_queue_length(U256::from(10), U256::from(10));
544        assert_eq!(length, U256::ZERO);
545    }
546
547    #[test]
548    fn test_queue_length_wrap_around() {
549        let capacity = queue_capacity();
550        let start = capacity - U256::from(5);
551        let end = U256::from(3);
552        let length = RocketpoolState::get_queue_length(start, end);
553        // 3 + 2^255 - (2^255 - 5) = 8
554        assert_eq!(length, U256::from(8));
555    }
556
557    // ============ Effective Capacity Tests ============
558
559    #[test]
560    fn test_effective_capacity_empty() {
561        let state = create_state();
562        assert_eq!(state.get_effective_capacity().unwrap(), U256::ZERO);
563    }
564
565    #[test]
566    fn test_effective_capacity_variable_queue() {
567        let mut state = create_state();
568        state.queue_variable_end = U256::from(2); // 2 * 31 ETH = 62 ETH
569        assert_eq!(state.get_effective_capacity().unwrap(), U256::from(62e18));
570    }
571
572    // ============ Max Deposit Capacity Tests ============
573
574    #[test]
575    fn test_max_capacity_assign_disabled() {
576        let state = create_state();
577        let max = state
578            .get_max_deposit_capacity()
579            .unwrap();
580        assert_eq!(max, U256::from(1000e18));
581    }
582
583    #[test]
584    fn test_max_capacity_assign_enabled_empty_queue() {
585        let mut state = create_state();
586        state.deposit_assigning_enabled = true;
587        let max = state
588            .get_max_deposit_capacity()
589            .unwrap();
590        assert_eq!(max, U256::from(1000e18));
591    }
592
593    #[test]
594    fn test_max_capacity_assign_enabled_with_queue() {
595        let mut state = create_state();
596        state.deposit_assigning_enabled = true;
597        state.queue_variable_end = U256::from(10); // 310 ETH extra
598        let max = state
599            .get_max_deposit_capacity()
600            .unwrap();
601        // 1000 + 310 = 1310 ETH
602        assert_eq!(max, U256::from(1310e18));
603    }
604
605    // ============ Delta Transition Tests ============
606
607    #[test]
608    fn test_delta_transition_basic() {
609        let mut state = create_state();
610
611        let attributes: HashMap<String, Bytes> = [
612            ("total_eth", U256::from(300u64)),
613            ("reth_supply", U256::from(150u64)),
614            ("deposit_contract_balance", U256::from(100u64)),
615            ("reth_contract_liquidity", U256::from(20u64)),
616        ]
617        .into_iter()
618        .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
619        .collect();
620
621        let delta = ProtocolStateDelta {
622            component_id: "Rocketpool".to_owned(),
623            updated_attributes: attributes,
624            deleted_attributes: HashSet::new(),
625        };
626
627        state
628            .delta_transition(delta, &HashMap::new(), &Balances::default())
629            .unwrap();
630
631        assert_eq!(state.total_eth, U256::from(300u64));
632        assert_eq!(state.reth_supply, U256::from(150u64));
633        assert_eq!(state.deposit_contract_balance, U256::from(100u64));
634        assert_eq!(state.reth_contract_liquidity, U256::from(20u64));
635    }
636
637    #[test]
638    fn test_delta_transition_queue_fields() {
639        let mut state = create_state();
640
641        let attributes: HashMap<String, Bytes> = [
642            ("deposit_assigning_enabled", U256::from(1u64)),
643            ("queue_variable_end", U256::from(5u64)),
644        ]
645        .into_iter()
646        .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
647        .collect();
648
649        let delta = ProtocolStateDelta {
650            component_id: "Rocketpool".to_owned(),
651            updated_attributes: attributes,
652            deleted_attributes: HashSet::new(),
653        };
654
655        state
656            .delta_transition(delta, &HashMap::new(), &Balances::default())
657            .unwrap();
658
659        assert!(state.deposit_assigning_enabled);
660        assert_eq!(state.queue_variable_end, U256::from(5u64));
661    }
662
663    // ============ Spot Price Tests ============
664
665    #[test]
666    fn test_spot_price_deposit() {
667        let state = create_state();
668        // spot_price(ETH, rETH) = how much rETH (quote) needed to buy 1 ETH (base)
669        // Compute ETH per rETH: get_eth_value(1 rETH) = 200/100 = 2.0
670        // Invert: 1 / 2.0 = 0.5 rETH per ETH
671        let price = state
672            .spot_price(&eth_token(), &reth_token())
673            .unwrap();
674        assert_ulps_eq!(price, 0.5);
675    }
676
677    #[test]
678    fn test_spot_price_withdraw() {
679        let state = create_state();
680        // spot_price(rETH, ETH) = how much ETH (quote) needed to buy 1 rETH (base)
681        // Compute rETH per ETH: get_reth_value(1 ETH) = (1 - 0.4 fee) * 100/200 = 0.3
682        // Invert: 1 / 0.3 = 3.333...
683        let price = state
684            .spot_price(&reth_token(), &eth_token())
685            .unwrap();
686        assert_ulps_eq!(price, 1.0 / 0.3);
687    }
688
689    /// Creates RocketpoolState from real on-chain data at block 23929406.
690    fn create_state_at_block_23929406() -> RocketpoolState {
691        RocketpoolState::new(
692            U256::from_str_radix("4df2cf698437b72b8937", 16).unwrap(), // reth_supply
693            U256::from_str_radix("59c8a9cb90db4a5aa85e", 16).unwrap(), // total_eth
694            U256::from_str_radix("11e245d1725f73941", 16).unwrap(),    // deposit_contract_balance
695            U256::from_str_radix("b6e43509", 16).unwrap(),             // reth_contract_liquidity
696            U256::from_str_radix("1c6bf52634000", 16).unwrap(),        // deposit_fee (0.05%)
697            true,                                                      // deposits_enabled
698            U256::from_str_radix("2386f26fc10000", 16).unwrap(),       // min_deposit_amount
699            U256::from_str_radix("3cfc82e37e9a7400000", 16).unwrap(),  // max_deposit_pool_size
700            true,                                                      // deposit_assigning_enabled
701            U256::from_str_radix("5a", 16).unwrap(),                   // deposit_assign_maximum
702            U256::from_str_radix("2", 16).unwrap(), // deposit_assign_socialised_maximum
703            U256::from_str_radix("6d45", 16).unwrap(), // queue_variable_start
704            U256::from_str_radix("6de3", 16).unwrap(), // queue_variable_end
705        )
706    }
707
708    /// Test spot price against real getEthValue(1e18) result at block 23929406
709    /// 0.868179358382478 rETH -> 1 ETH (no fees on withdrawal)
710    #[test]
711    fn test_live_spot_price_reth_to_buy_eth_23929406() {
712        let state = create_state_at_block_23929406();
713
714        let price = state
715            .spot_price(&eth_token(), &reth_token())
716            .unwrap();
717
718        let expected = 0.868179358382478;
719        assert_ulps_eq!(price, expected, max_ulps = 10);
720    }
721
722    /// Test spot price against real getRethValue(1e18) result at block 23929406:
723    /// 1.151835724202335 ETH -> 1 rETH (without accounting for deposit fee)
724    #[test]
725    fn test_live_spot_price_eth_to_buy_reth_23929406() {
726        let state = create_state_at_block_23929406();
727
728        let price = state
729            .spot_price(&reth_token(), &eth_token())
730            .unwrap();
731
732        let expected_without_fee = 1.151835724202335;
733        let fee = state.deposit_fee.to_f64().unwrap() / DEPOSIT_FEE_BASE as f64;
734        let expected = add_fee_markup(expected_without_fee, fee);
735
736        assert_ulps_eq!(price, expected, max_ulps = 10);
737    }
738
739    #[test]
740    fn test_fee_panics() {
741        let state = create_state();
742        let result = std::panic::catch_unwind(|| state.fee());
743        assert!(result.is_err());
744    }
745
746    // ============ Get Limits Tests ============
747
748    #[test]
749    fn test_limits_deposit() {
750        let state = create_state();
751
752        let (max_sell, max_buy) = state
753            .get_limits(eth_token().address, reth_token().address)
754            .unwrap();
755
756        // max_sell = 1000 - 50 = 950 ETH
757        assert_eq!(max_sell, BigUint::from(950_000_000_000_000_000_000u128));
758        // max_buy = 950 * 0.6 * 100/200 = 285 rETH
759        assert_eq!(max_buy, BigUint::from(285_000_000_000_000_000_000u128));
760    }
761
762    #[test]
763    fn test_limits_withdrawal() {
764        let state = create_state();
765
766        let (max_sell, max_buy) = state
767            .get_limits(reth_token().address, eth_token().address)
768            .unwrap();
769
770        // max_buy = liquidity = 50 ETH
771        assert_eq!(max_buy, BigUint::from(50_000_000_000_000_000_000u128));
772        // max_sell = 50 * 100/200 = 25 rETH
773        assert_eq!(max_sell, BigUint::from(25_000_000_000_000_000_000u128));
774    }
775
776    #[test]
777    fn test_limits_with_extended_capacity() {
778        let mut state = create_state();
779        state.max_deposit_pool_size = U256::from(100e18);
780        state.deposit_assigning_enabled = true;
781        state.queue_variable_end = U256::from(2); // 62 ETH extra
782
783        let (max_sell, _) = state
784            .get_limits(eth_token().address, reth_token().address)
785            .unwrap();
786
787        // max_capacity = 100 + 62 = 162 ETH
788        // max_sell = 162 - 50 = 112 ETH
789        assert_eq!(max_sell, BigUint::from(112_000_000_000_000_000_000u128));
790    }
791
792    // ============ Get Amount Out - Happy Path Tests ============
793
794    #[test]
795    fn test_deposit_eth() {
796        let state = create_state();
797
798        // Deposit 10 ETH: fee=4, net=6 → 6*100/200 = 3 rETH
799        let res = state
800            .get_amount_out(
801                BigUint::from(10_000_000_000_000_000_000u128),
802                &eth_token(),
803                &reth_token(),
804            )
805            .unwrap();
806
807        assert_eq!(res.amount, BigUint::from(3_000_000_000_000_000_000u128));
808
809        let new_state = res
810            .new_state
811            .as_any()
812            .downcast_ref::<RocketpoolState>()
813            .unwrap();
814        // liquidity: 50 + 10 = 60
815        assert_eq!(new_state.deposit_contract_balance, U256::from(60e18));
816        // total_eth and reth_supply unchanged (managed by oracle)
817        assert_eq!(new_state.total_eth, U256::from(200e18));
818        assert_eq!(new_state.reth_supply, U256::from(100e18));
819    }
820
821    #[test]
822    fn test_deposit_within_extended_capacity() {
823        let mut state = create_state();
824        state.deposit_contract_balance = U256::from(990e18);
825        state.max_deposit_pool_size = U256::from(1000e18);
826        state.deposit_assigning_enabled = true;
827        state.queue_variable_end = U256::from(1); // 31 ETH extra
828
829        // Deposit 20 ETH: 990 + 20 = 1010 > 1000 base, but <= 1031 extended
830        // fee = 20 * 0.4 = 8, net = 12 → 12*100/200 = 6 rETH
831        let res = state
832            .get_amount_out(
833                BigUint::from(20_000_000_000_000_000_000u128),
834                &eth_token(),
835                &reth_token(),
836            )
837            .unwrap();
838
839        assert_eq!(res.amount, BigUint::from(6_000_000_000_000_000_000u128));
840
841        let new_state = res
842            .new_state
843            .as_any()
844            .downcast_ref::<RocketpoolState>()
845            .unwrap();
846        // liquidity: 990 + 20 = 1010
847        assert_eq!(new_state.deposit_contract_balance, U256::from(1010e18));
848        // total_eth and reth_supply unchanged (managed by oracle)
849        assert_eq!(new_state.total_eth, U256::from(200e18));
850        assert_eq!(new_state.reth_supply, U256::from(100e18));
851    }
852
853    #[test]
854    fn test_withdraw_reth() {
855        let state = create_state();
856
857        // Withdraw 10 rETH: 10*200/100 = 20 ETH
858        let res = state
859            .get_amount_out(
860                BigUint::from(10_000_000_000_000_000_000u128),
861                &reth_token(),
862                &eth_token(),
863            )
864            .unwrap();
865
866        assert_eq!(res.amount, BigUint::from(20_000_000_000_000_000_000u128));
867
868        let new_state = res
869            .new_state
870            .as_any()
871            .downcast_ref::<RocketpoolState>()
872            .unwrap();
873        // liquidity: 50 - 20 = 30
874        assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
875        // total_eth and reth_supply unchanged (managed by oracle)
876        assert_eq!(new_state.total_eth, U256::from(200e18));
877        assert_eq!(new_state.reth_supply, U256::from(100e18));
878    }
879
880    // ============ Get Amount Out - Error Cases Tests ============
881
882    #[test]
883    fn test_deposit_disabled() {
884        let mut state = create_state();
885        state.deposits_enabled = false;
886
887        let res = state.get_amount_out(BigUint::from(10u64), &eth_token(), &reth_token());
888        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
889    }
890
891    #[test]
892    fn test_deposit_below_minimum() {
893        let mut state = create_state();
894        state.min_deposit_amount = U256::from(100u64);
895
896        let res = state.get_amount_out(BigUint::from(50u64), &eth_token(), &reth_token());
897        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
898    }
899
900    #[test]
901    fn test_deposit_exceeds_max_pool() {
902        let mut state = create_state();
903        state.max_deposit_pool_size = U256::from(60e18); // Only 10 ETH room
904
905        let res = state.get_amount_out(
906            BigUint::from(20_000_000_000_000_000_000u128),
907            &eth_token(),
908            &reth_token(),
909        );
910        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
911    }
912
913    #[test]
914    fn test_deposit_queue_ignored_when_disabled() {
915        let mut state = create_state();
916        state.deposit_contract_balance = U256::from(990e18);
917        state.max_deposit_pool_size = U256::from(1000e18);
918        state.deposit_assigning_enabled = false;
919        state.queue_variable_end = U256::from(10); // Would add 310 ETH if enabled
920
921        // Deposit 20 ETH: 990 + 20 = 1010 > 1000 (queue ignored)
922        let res = state.get_amount_out(
923            BigUint::from(20_000_000_000_000_000_000u128),
924            &eth_token(),
925            &reth_token(),
926        );
927        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
928    }
929
930    #[test]
931    fn test_deposit_exceeds_extended_capacity() {
932        let mut state = create_state();
933        state.deposit_contract_balance = U256::from(990e18);
934        state.max_deposit_pool_size = U256::from(1000e18);
935        state.deposit_assigning_enabled = true;
936        state.queue_variable_end = U256::from(1); // 31 ETH extra, max = 1031
937
938        // Deposit 50 ETH: 990 + 50 = 1040 > 1031
939        let res = state.get_amount_out(
940            BigUint::from(50_000_000_000_000_000_000u128),
941            &eth_token(),
942            &reth_token(),
943        );
944        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
945    }
946
947    #[test]
948    fn test_withdrawal_insufficient_liquidity() {
949        let state = create_state(); // 50 ETH liquidity, withdraw needs 20 ETH per 10 rETH
950
951        // Try to withdraw 30 rETH = 60 ETH > 50 liquidity
952        let res = state.get_amount_out(
953            BigUint::from(30_000_000_000_000_000_000u128),
954            &reth_token(),
955            &eth_token(),
956        );
957        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
958    }
959
960    #[test]
961    fn test_withdrawal_limited_by_minipool_queue() {
962        let mut state = create_state();
963        state.deposit_contract_balance = U256::from(100e18);
964        // Queue has 2 variable minipools = 62 ETH capacity
965        state.queue_variable_end = U256::from(2);
966        // excess_balance = 100 - 62 = 38 ETH
967
968        // Try to withdraw 20 rETH = 40 ETH > 38 excess balance (but < 100 liquidity)
969        let res = state.get_amount_out(
970            BigUint::from(20_000_000_000_000_000_000u128),
971            &reth_token(),
972            &eth_token(),
973        );
974        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
975
976        // Withdraw 15 rETH = 30 ETH <= 38 excess balance should work
977        let res = state
978            .get_amount_out(
979                BigUint::from(15_000_000_000_000_000_000u128),
980                &reth_token(),
981                &eth_token(),
982            )
983            .unwrap();
984        assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
985    }
986
987    #[test]
988    fn test_withdrawal_zero_excess_balance() {
989        let mut state = create_state();
990        state.deposit_contract_balance = U256::from(62e18);
991        // Queue has 2 variable minipools = 62 ETH capacity
992        state.queue_variable_end = U256::from(2);
993        // excess_balance = 62 - 62 = 0 ETH
994
995        // Any withdrawal should fail
996        let res = state.get_amount_out(
997            BigUint::from(1_000_000_000_000_000_000u128),
998            &reth_token(),
999            &eth_token(),
1000        );
1001        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
1002    }
1003
1004    #[test]
1005    fn test_withdrawal_uses_both_pools() {
1006        let mut state = create_state();
1007        state.reth_contract_liquidity = U256::from(10e18);
1008        state.deposit_contract_balance = U256::from(50e18);
1009        // No queue, so full 50 ETH is excess balance
1010        // Total available: 10 + 50 = 60 ETH
1011
1012        // Withdraw 15 rETH = 30 ETH (more than reth_contract_liquidity of 10 ETH)
1013        let res = state
1014            .get_amount_out(
1015                BigUint::from(15_000_000_000_000_000_000u128),
1016                &reth_token(),
1017                &eth_token(),
1018            )
1019            .unwrap();
1020
1021        assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
1022
1023        let new_state = res
1024            .new_state
1025            .as_any()
1026            .downcast_ref::<RocketpoolState>()
1027            .unwrap();
1028
1029        // reth_contract_liquidity drained to 0
1030        assert_eq!(new_state.reth_contract_liquidity, U256::ZERO);
1031        // deposit_contract_balance: 50 - (30 - 10) = 30 ETH
1032        assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
1033    }
1034
1035    #[test]
1036    fn test_limits_withdrawal_with_queue() {
1037        let mut state = create_state();
1038        state.deposit_contract_balance = U256::from(100e18);
1039        // Queue has 2 variable minipools = 62 ETH capacity
1040        state.queue_variable_end = U256::from(2);
1041
1042        let (max_sell, max_buy) = state
1043            .get_limits(reth_token().address, eth_token().address)
1044            .unwrap();
1045
1046        // max_buy = excess_balance = 100 - 62 = 38 ETH
1047        assert_eq!(max_buy, BigUint::from(38_000_000_000_000_000_000u128));
1048        // max_sell = 38 * 100/200 = 19 rETH
1049        assert_eq!(max_sell, BigUint::from(19_000_000_000_000_000_000u128));
1050    }
1051
1052    // ============ Assign Deposits Tests ============
1053
1054    #[test]
1055    fn test_assign_deposits_dequeues_minipools() {
1056        let mut state = create_state();
1057        state.deposit_contract_balance = U256::from(100e18);
1058        state.deposit_assigning_enabled = true;
1059        state.deposit_assign_maximum = U256::from(10u64);
1060        state.deposit_assign_socialised_maximum = U256::from(2u64);
1061        state.queue_variable_end = U256::from(5); // 5 minipools in queue
1062
1063        // Deposit 62 ETH (2 * 31 ETH)
1064        // scalingCount = 62 / 31 = 2
1065        // totalEthCount = (100 + 62) / 31 = 5
1066        // assignments = socialised(2) + scaling(2) = 4
1067        // capped at min(4, 5, 10, 5) = 4
1068        // eth_assigned = 4 * 31 = 124 ETH
1069        // new_liquidity = 100 + 62 - 124 = 38 ETH
1070        let res = state
1071            .get_amount_out(
1072                BigUint::from(62_000_000_000_000_000_000u128),
1073                &eth_token(),
1074                &reth_token(),
1075            )
1076            .unwrap();
1077
1078        let new_state = res
1079            .new_state
1080            .as_any()
1081            .downcast_ref::<RocketpoolState>()
1082            .unwrap();
1083
1084        assert_eq!(new_state.deposit_contract_balance, U256::from(38e18));
1085        assert_eq!(new_state.queue_variable_start, U256::from(4u64));
1086    }
1087
1088    #[test]
1089    fn test_assign_deposits_capped_by_queue_length() {
1090        let mut state = create_state();
1091        state.deposit_contract_balance = U256::from(100e18);
1092        state.deposit_assigning_enabled = true;
1093        state.deposit_assign_maximum = U256::from(10u64);
1094        state.deposit_assign_socialised_maximum = U256::from(5u64);
1095        state.queue_variable_end = U256::from(2); // Only 2 minipools in queue
1096
1097        // Deposit 62 ETH
1098        // assignments = 5 + 2 = 7, but capped at queue length of 2
1099        // eth_assigned = 2 * 31 = 62 ETH
1100        // new_liquidity = 100 + 62 - 62 = 100 ETH
1101        let res = state
1102            .get_amount_out(
1103                BigUint::from(62_000_000_000_000_000_000u128),
1104                &eth_token(),
1105                &reth_token(),
1106            )
1107            .unwrap();
1108
1109        let new_state = res
1110            .new_state
1111            .as_any()
1112            .downcast_ref::<RocketpoolState>()
1113            .unwrap();
1114
1115        assert_eq!(new_state.deposit_contract_balance, U256::from(100e18));
1116        assert_eq!(new_state.queue_variable_start, U256::from(2u64));
1117    }
1118
1119    #[test]
1120    fn test_assign_deposits_capped_by_max_assignments() {
1121        let mut state = create_state();
1122        state.deposit_contract_balance = U256::from(100e18);
1123        state.deposit_assigning_enabled = true;
1124        state.deposit_assign_maximum = U256::from(1u64); // Only 1 assignment allowed
1125        state.deposit_assign_socialised_maximum = U256::from(5u64);
1126        state.queue_variable_end = U256::from(10);
1127
1128        // Deposit 62 ETH
1129        // assignments = 5 + 2 = 7, but capped at max of 1
1130        // eth_assigned = 1 * 31 = 31 ETH
1131        // new_liquidity = 100 + 62 - 31 = 131 ETH
1132        let res = state
1133            .get_amount_out(
1134                BigUint::from(62_000_000_000_000_000_000u128),
1135                &eth_token(),
1136                &reth_token(),
1137            )
1138            .unwrap();
1139
1140        let new_state = res
1141            .new_state
1142            .as_any()
1143            .downcast_ref::<RocketpoolState>()
1144            .unwrap();
1145
1146        assert_eq!(new_state.deposit_contract_balance, U256::from(131e18));
1147        assert_eq!(new_state.queue_variable_start, U256::from(1u64));
1148    }
1149
1150    #[test]
1151    fn test_assign_deposits_capped_by_total_eth() {
1152        let mut state = create_state();
1153        state.deposit_contract_balance = U256::from(10e18); // Low liquidity
1154        state.deposit_assigning_enabled = true;
1155        state.deposit_assign_maximum = U256::from(10u64);
1156        state.deposit_assign_socialised_maximum = U256::from(5u64);
1157        state.queue_variable_end = U256::from(10);
1158
1159        // Deposit 31 ETH
1160        // totalEthCount = (10 + 31) / 31 = 1
1161        // assignments = 5 + 1 = 6, but capped at totalEthCount of 1
1162        // eth_assigned = 1 * 31 = 31 ETH
1163        // new_liquidity = 10 + 31 - 31 = 10 ETH
1164        let res = state
1165            .get_amount_out(
1166                BigUint::from(31_000_000_000_000_000_000u128),
1167                &eth_token(),
1168                &reth_token(),
1169            )
1170            .unwrap();
1171
1172        let new_state = res
1173            .new_state
1174            .as_any()
1175            .downcast_ref::<RocketpoolState>()
1176            .unwrap();
1177
1178        assert_eq!(new_state.deposit_contract_balance, U256::from(10e18));
1179        assert_eq!(new_state.queue_variable_start, U256::from(1u64));
1180    }
1181
1182    #[test]
1183    fn test_assign_deposits_empty_queue_no_change() {
1184        let mut state = create_state();
1185        state.deposit_assigning_enabled = true;
1186        state.deposit_assign_maximum = U256::from(10u64);
1187        state.deposit_assign_socialised_maximum = U256::from(2u64);
1188        // queue_variable_end = 0, so queue is empty
1189
1190        // Deposit 10 ETH - no minipools to dequeue
1191        let res = state
1192            .get_amount_out(
1193                BigUint::from(10_000_000_000_000_000_000u128),
1194                &eth_token(),
1195                &reth_token(),
1196            )
1197            .unwrap();
1198
1199        let new_state = res
1200            .new_state
1201            .as_any()
1202            .downcast_ref::<RocketpoolState>()
1203            .unwrap();
1204
1205        // liquidity: 50 + 10 = 60 (no withdrawal)
1206        assert_eq!(new_state.deposit_contract_balance, U256::from(60e18));
1207        assert_eq!(new_state.queue_variable_start, U256::ZERO);
1208    }
1209
1210    // ============ Live Transaction Tests ============
1211
1212    /// Test against real transaction deposit on Rocketpool
1213    /// 0x6213b6c235c52d2132711c18a1c66934832722fd71c098e843bc792ecdbd11b3 where user deposited
1214    /// exactly 4.5 ETH and received 3.905847020555141679 rETH 1 minipool was assigned (31 ETH
1215    /// withdrawn from pool)
1216    #[test]
1217    fn test_live_deposit_tx_6213b6c2() {
1218        let state = RocketpoolState::new(
1219            U256::from_str_radix("4ec08ba071647b927594", 16).unwrap(), // reth_supply
1220            U256::from_str_radix("5aafbb189fbbc1704662", 16).unwrap(), // total_eth
1221            U256::from_str_radix("17a651238b0dbf892", 16).unwrap(),    // deposit_contract_balance
1222            U256::from(781003199),                                     // reth_contract_liquidity
1223            U256::from_str_radix("1c6bf52634000", 16).unwrap(),        // deposit_fee (0.05%)
1224            true,                                                      // deposits_enabled
1225            U256::from_str_radix("2386f26fc10000", 16).unwrap(),       // min_deposit_amount
1226            U256::from_str_radix("3cfc82e37e9a7400000", 16).unwrap(),  // max_deposit_pool_size
1227            true,                                                      // deposit_assigning_enabled
1228            U256::from_str_radix("5a", 16).unwrap(),                   // deposit_assign_maximum
1229            U256::from_str_radix("2", 16).unwrap(), // deposit_assign_socialised_maximum
1230            U256::from_str_radix("6d43", 16).unwrap(), // queue_variable_start
1231            U256::from_str_radix("6dde", 16).unwrap(), // queue_variable_end
1232        );
1233
1234        // User deposited exactly 4.5 ETH
1235        let deposit_amount = BigUint::from(4_500_000_000_000_000_000u128);
1236
1237        let res = state
1238            .get_amount_out(deposit_amount, &eth_token(), &reth_token())
1239            .unwrap();
1240
1241        println!("calculated rETH out: {}", res.amount);
1242        let expected_reth_out = BigUint::from(3_905_847_020_555_141_679u128);
1243        assert_eq!(res.amount, expected_reth_out);
1244
1245        let new_state = res
1246            .new_state
1247            .as_any()
1248            .downcast_ref::<RocketpoolState>()
1249            .unwrap();
1250
1251        // Expected final balance to reduce by 31 ETH (1 minipool assigned)
1252        let expected_balance = U256::from_str_radix("0aa2289fdd01f892", 16).unwrap();
1253        assert_eq!(new_state.deposit_contract_balance, expected_balance);
1254
1255        // Expected queue_variable_start to advance by 1 (1 minipool assigned)
1256        let expected_queue_start = U256::from_str_radix("6d44", 16).unwrap();
1257        assert_eq!(new_state.queue_variable_start, expected_queue_start);
1258
1259        // Other state variables unchanged
1260        assert_eq!(new_state.total_eth, state.total_eth);
1261        assert_eq!(new_state.reth_supply, state.reth_supply);
1262        assert_eq!(new_state.deposit_fee, state.deposit_fee);
1263        assert_eq!(new_state.deposits_enabled, state.deposits_enabled);
1264        assert_eq!(new_state.min_deposit_amount, state.min_deposit_amount);
1265        assert_eq!(new_state.max_deposit_pool_size, state.max_deposit_pool_size);
1266        assert_eq!(new_state.deposit_assigning_enabled, state.deposit_assigning_enabled);
1267        assert_eq!(new_state.deposit_assign_maximum, state.deposit_assign_maximum);
1268        assert_eq!(
1269            new_state.deposit_assign_socialised_maximum,
1270            state.deposit_assign_socialised_maximum
1271        );
1272        assert_eq!(new_state.queue_variable_end, state.queue_variable_end);
1273    }
1274
1275    /// Test against real withdrawal (burn) transaction on
1276    /// Rocketpool0xf0f615f5dcf40d6ba1168da654a9ea8a0e855e489a34f4ffc3c7d2ad165f0bd6 where user
1277    /// burned 20.873689741238146923 rETH and received 24.000828571949999998 ETH
1278    #[test]
1279    fn test_live_withdraw_tx_block_23736567() {
1280        let state = RocketpoolState::new(
1281            U256::from_str_radix("516052628fbe875ffff0", 16).unwrap(), // reth_supply
1282            U256::from_str_radix("5d9143622860d8bdacea", 16).unwrap(), // total_eth
1283            U256::from_str_radix("1686dc9300da8004d", 16).unwrap(),    // deposit_contract_balance
1284            U256::from_str_radix("14d141273efab8a43", 16).unwrap(),    // reth_contract_liquidity
1285            U256::from_str_radix("1c6bf52634000", 16).unwrap(),        // deposit_fee (0.05%)
1286            true,                                                      // deposits_enabled
1287            U256::from_str_radix("2386f26fc10000", 16).unwrap(),       // min_deposit_amount
1288            U256::from_str_radix("3cfc82e37e9a7400000", 16).unwrap(),  // max_deposit_pool_size
1289            true,                                                      // deposit_assigning_enabled
1290            U256::from_str_radix("5a", 16).unwrap(),                   // deposit_assign_maximum
1291            U256::from_str_radix("2", 16).unwrap(), // deposit_assign_socialised_maximum
1292            U256::from_str_radix("6d34", 16).unwrap(), // queue_variable_start
1293            U256::from_str_radix("6dd0", 16).unwrap(), // queue_variable_end
1294        );
1295
1296        // User burned exactly 20873689741238146923 rETH
1297        let burn_amount = BigUint::from(20_873_689_741_238_146_923u128);
1298
1299        let res = state
1300            .get_amount_out(burn_amount, &reth_token(), &eth_token())
1301            .unwrap();
1302
1303        // Expected ETH out: 24000828571949999998 wei
1304        let expected_eth_out = BigUint::from(24_000_828_571_949_999_998u128);
1305        assert_eq!(res.amount, expected_eth_out);
1306
1307        let new_state = res
1308            .new_state
1309            .as_any()
1310            .downcast_ref::<RocketpoolState>()
1311            .unwrap();
1312
1313        // Verify liquidity was updated correctly
1314        let expected_liquidity = U256::from_str_radix("74d8b62c5", 16).unwrap();
1315        assert_eq!(new_state.reth_contract_liquidity, expected_liquidity);
1316
1317        // Other state variables unchanged
1318        assert_eq!(new_state.total_eth, state.total_eth);
1319        assert_eq!(new_state.reth_supply, state.reth_supply);
1320        assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1321        assert_eq!(new_state.deposit_fee, state.deposit_fee);
1322        assert_eq!(new_state.deposits_enabled, state.deposits_enabled);
1323        assert_eq!(new_state.min_deposit_amount, state.min_deposit_amount);
1324        assert_eq!(new_state.max_deposit_pool_size, state.max_deposit_pool_size);
1325        assert_eq!(new_state.deposit_assigning_enabled, state.deposit_assigning_enabled);
1326        assert_eq!(new_state.deposit_assign_maximum, state.deposit_assign_maximum);
1327        assert_eq!(
1328            new_state.deposit_assign_socialised_maximum,
1329            state.deposit_assign_socialised_maximum
1330        );
1331        assert_eq!(new_state.queue_variable_start, state.queue_variable_start);
1332        assert_eq!(new_state.queue_variable_end, state.queue_variable_end);
1333    }
1334}