tycho_simulation/evm/protocol/rocketpool/
state.rs

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