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