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
459#[cfg(test)]
460mod tests {
461    use std::{
462        collections::{HashMap, HashSet},
463        str::FromStr,
464    };
465
466    use approx::assert_ulps_eq;
467    use num_bigint::BigUint;
468    use num_traits::ToPrimitive;
469    use tycho_common::{
470        dto::ProtocolStateDelta,
471        hex_bytes::Bytes,
472        models::{token::Token, Chain},
473        simulation::{
474            errors::SimulationError,
475            protocol_sim::{Balances, ProtocolSim},
476        },
477    };
478
479    use super::*;
480    use crate::evm::protocol::utils::add_fee_markup;
481
482    /// Helper function to create a RocketpoolState with easy-to-compute defaults for testing
483    /// Mutate fields for specific tests.
484    /// - Exchange rate: 1 rETH = 2 ETH (100 rETH backed by 200 ETH)
485    /// - Deposit fee: 40%
486    /// - Deposit contract balance: 50 ETH
487    /// - rETH contract liquidity: 0 ETH
488    /// - Max pool size: 1000 ETH
489    /// - Assign deposits enabled: false
490    fn create_state() -> RocketpoolState {
491        RocketpoolState::new(
492            U256::from(100e18),                     // reth_supply: 100 rETH
493            U256::from(200e18),                     // total_eth: 200 ETH (1 rETH = 2 ETH)
494            U256::from(50e18),                      // deposit_contract_balance: 50 ETH
495            U256::ZERO,                             // reth_contract_liquidity: 0 ETH
496            U256::from(400_000_000_000_000_000u64), // deposit_fee: 40% (0.4e18)
497            true,                                   // deposits_enabled
498            U256::ZERO,                             // min_deposit_amount
499            U256::from(1000e18),                    // max_deposit_pool_size: 1000 ETH
500            false,                                  // deposit_assigning_enabled
501            U256::ZERO,                             // deposit_assign_maximum
502            U256::ZERO,                             // deposit_assign_socialised_maximum
503            U256::ZERO,                             // queue_variable_start
504            U256::ZERO,                             // queue_variable_end
505        )
506    }
507
508    fn eth_token() -> Token {
509        Token::new(&Bytes::from(ETH_ADDRESS), "ETH", 18, 0, &[Some(100_000)], Chain::Ethereum, 100)
510    }
511
512    fn reth_token() -> Token {
513        Token::new(
514            &Bytes::from_str("0xae78736Cd615f374D3085123A210448E74Fc6393").unwrap(),
515            "rETH",
516            18,
517            0,
518            &[Some(100_000)],
519            Chain::Ethereum,
520            100,
521        )
522    }
523
524    // ============ Queue Length Tests ============
525
526    #[test]
527    fn test_queue_length_normal() {
528        let length = RocketpoolState::get_queue_length(U256::from(10), U256::from(15));
529        assert_eq!(length, U256::from(5));
530    }
531
532    #[test]
533    fn test_queue_length_empty() {
534        let length = RocketpoolState::get_queue_length(U256::from(10), U256::from(10));
535        assert_eq!(length, U256::ZERO);
536    }
537
538    #[test]
539    fn test_queue_length_wrap_around() {
540        let capacity = queue_capacity();
541        let start = capacity - U256::from(5);
542        let end = U256::from(3);
543        let length = RocketpoolState::get_queue_length(start, end);
544        // 3 + 2^255 - (2^255 - 5) = 8
545        assert_eq!(length, U256::from(8));
546    }
547
548    // ============ Effective Capacity Tests ============
549
550    #[test]
551    fn test_effective_capacity_empty() {
552        let state = create_state();
553        assert_eq!(state.get_effective_capacity().unwrap(), U256::ZERO);
554    }
555
556    #[test]
557    fn test_effective_capacity_variable_queue() {
558        let mut state = create_state();
559        state.queue_variable_end = U256::from(2); // 2 * 31 ETH = 62 ETH
560        assert_eq!(state.get_effective_capacity().unwrap(), U256::from(62e18));
561    }
562
563    // ============ Max Deposit Capacity Tests ============
564
565    #[test]
566    fn test_max_capacity_assign_disabled() {
567        let state = create_state();
568        let max = state
569            .get_max_deposit_capacity()
570            .unwrap();
571        assert_eq!(max, U256::from(1000e18));
572    }
573
574    #[test]
575    fn test_max_capacity_assign_enabled_empty_queue() {
576        let mut state = create_state();
577        state.deposit_assigning_enabled = true;
578        let max = state
579            .get_max_deposit_capacity()
580            .unwrap();
581        assert_eq!(max, U256::from(1000e18));
582    }
583
584    #[test]
585    fn test_max_capacity_assign_enabled_with_queue() {
586        let mut state = create_state();
587        state.deposit_assigning_enabled = true;
588        state.queue_variable_end = U256::from(10); // 310 ETH extra
589        let max = state
590            .get_max_deposit_capacity()
591            .unwrap();
592        // 1000 + 310 = 1310 ETH
593        assert_eq!(max, U256::from(1310e18));
594    }
595
596    // ============ Delta Transition Tests ============
597
598    #[test]
599    fn test_delta_transition_basic() {
600        let mut state = create_state();
601
602        let attributes: HashMap<String, Bytes> = [
603            ("total_eth", U256::from(300u64)),
604            ("reth_supply", U256::from(150u64)),
605            ("deposit_contract_balance", U256::from(100u64)),
606            ("reth_contract_liquidity", U256::from(20u64)),
607        ]
608        .into_iter()
609        .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
610        .collect();
611
612        let delta = ProtocolStateDelta {
613            component_id: "Rocketpool".to_owned(),
614            updated_attributes: attributes,
615            deleted_attributes: HashSet::new(),
616        };
617
618        state
619            .delta_transition(delta, &HashMap::new(), &Balances::default())
620            .unwrap();
621
622        assert_eq!(state.total_eth, U256::from(300u64));
623        assert_eq!(state.reth_supply, U256::from(150u64));
624        assert_eq!(state.deposit_contract_balance, U256::from(100u64));
625        assert_eq!(state.reth_contract_liquidity, U256::from(20u64));
626    }
627
628    #[test]
629    fn test_delta_transition_queue_fields() {
630        let mut state = create_state();
631
632        let attributes: HashMap<String, Bytes> = [
633            ("deposit_assigning_enabled", U256::from(1u64)),
634            ("queue_variable_end", U256::from(5u64)),
635        ]
636        .into_iter()
637        .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
638        .collect();
639
640        let delta = ProtocolStateDelta {
641            component_id: "Rocketpool".to_owned(),
642            updated_attributes: attributes,
643            deleted_attributes: HashSet::new(),
644        };
645
646        state
647            .delta_transition(delta, &HashMap::new(), &Balances::default())
648            .unwrap();
649
650        assert!(state.deposit_assigning_enabled);
651        assert_eq!(state.queue_variable_end, U256::from(5u64));
652    }
653
654    // ============ Spot Price Tests ============
655
656    #[test]
657    fn test_spot_price_deposit() {
658        let state = create_state();
659        // spot_price(ETH, rETH) = how much rETH (quote) needed to buy 1 ETH (base)
660        // Compute ETH per rETH: get_eth_value(1 rETH) = 200/100 = 2.0
661        // Invert: 1 / 2.0 = 0.5 rETH per ETH
662        let price = state
663            .spot_price(&eth_token(), &reth_token())
664            .unwrap();
665        assert_ulps_eq!(price, 0.5);
666    }
667
668    #[test]
669    fn test_spot_price_withdraw() {
670        let state = create_state();
671        // spot_price(rETH, ETH) = how much ETH (quote) needed to buy 1 rETH (base)
672        // Compute rETH per ETH: get_reth_value(1 ETH) = (1 - 0.4 fee) * 100/200 = 0.3
673        // Invert: 1 / 0.3 = 3.333...
674        let price = state
675            .spot_price(&reth_token(), &eth_token())
676            .unwrap();
677        assert_ulps_eq!(price, 1.0 / 0.3);
678    }
679
680    /// Creates RocketpoolState from real on-chain data at block 23929406.
681    fn create_state_at_block_23929406() -> RocketpoolState {
682        RocketpoolState::new(
683            U256::from_str_radix("4df2cf698437b72b8937", 16).unwrap(), // reth_supply
684            U256::from_str_radix("59c8a9cb90db4a5aa85e", 16).unwrap(), // total_eth
685            U256::from_str_radix("11e245d1725f73941", 16).unwrap(),    // deposit_contract_balance
686            U256::from_str_radix("b6e43509", 16).unwrap(),             // reth_contract_liquidity
687            U256::from_str_radix("1c6bf52634000", 16).unwrap(),        // deposit_fee (0.05%)
688            true,                                                      // deposits_enabled
689            U256::from_str_radix("2386f26fc10000", 16).unwrap(),       // min_deposit_amount
690            U256::from_str_radix("3cfc82e37e9a7400000", 16).unwrap(),  // max_deposit_pool_size
691            true,                                                      // deposit_assigning_enabled
692            U256::from_str_radix("5a", 16).unwrap(),                   // deposit_assign_maximum
693            U256::from_str_radix("2", 16).unwrap(), // deposit_assign_socialised_maximum
694            U256::from_str_radix("6d45", 16).unwrap(), // queue_variable_start
695            U256::from_str_radix("6de3", 16).unwrap(), // queue_variable_end
696        )
697    }
698
699    /// Test spot price against real getEthValue(1e18) result at block 23929406
700    /// 0.868179358382478 rETH -> 1 ETH (no fees on withdrawal)
701    #[test]
702    fn test_live_spot_price_reth_to_buy_eth_23929406() {
703        let state = create_state_at_block_23929406();
704
705        let price = state
706            .spot_price(&eth_token(), &reth_token())
707            .unwrap();
708
709        let expected = 0.868179358382478;
710        assert_ulps_eq!(price, expected, max_ulps = 10);
711    }
712
713    /// Test spot price against real getRethValue(1e18) result at block 23929406:
714    /// 1.151835724202335 ETH -> 1 rETH (without accounting for deposit fee)
715    #[test]
716    fn test_live_spot_price_eth_to_buy_reth_23929406() {
717        let state = create_state_at_block_23929406();
718
719        let price = state
720            .spot_price(&reth_token(), &eth_token())
721            .unwrap();
722
723        let expected_without_fee = 1.151835724202335;
724        let fee = state.deposit_fee.to_f64().unwrap() / DEPOSIT_FEE_BASE as f64;
725        let expected = add_fee_markup(expected_without_fee, fee);
726
727        assert_ulps_eq!(price, expected, max_ulps = 10);
728    }
729
730    #[test]
731    fn test_fee_panics() {
732        let state = create_state();
733        let result = std::panic::catch_unwind(|| state.fee());
734        assert!(result.is_err());
735    }
736
737    // ============ Get Limits Tests ============
738
739    #[test]
740    fn test_limits_deposit() {
741        let state = create_state();
742
743        let (max_sell, max_buy) = state
744            .get_limits(eth_token().address, reth_token().address)
745            .unwrap();
746
747        // max_sell = 1000 - 50 = 950 ETH
748        assert_eq!(max_sell, BigUint::from(950_000_000_000_000_000_000u128));
749        // max_buy = 950 * 0.6 * 100/200 = 285 rETH
750        assert_eq!(max_buy, BigUint::from(285_000_000_000_000_000_000u128));
751    }
752
753    #[test]
754    fn test_limits_withdrawal() {
755        let state = create_state();
756
757        let (max_sell, max_buy) = state
758            .get_limits(reth_token().address, eth_token().address)
759            .unwrap();
760
761        // max_buy = liquidity = 50 ETH
762        assert_eq!(max_buy, BigUint::from(50_000_000_000_000_000_000u128));
763        // max_sell = 50 * 100/200 = 25 rETH
764        assert_eq!(max_sell, BigUint::from(25_000_000_000_000_000_000u128));
765    }
766
767    #[test]
768    fn test_limits_with_extended_capacity() {
769        let mut state = create_state();
770        state.max_deposit_pool_size = U256::from(100e18);
771        state.deposit_assigning_enabled = true;
772        state.queue_variable_end = U256::from(2); // 62 ETH extra
773
774        let (max_sell, _) = state
775            .get_limits(eth_token().address, reth_token().address)
776            .unwrap();
777
778        // max_capacity = 100 + 62 = 162 ETH
779        // max_sell = 162 - 50 = 112 ETH
780        assert_eq!(max_sell, BigUint::from(112_000_000_000_000_000_000u128));
781    }
782
783    // ============ Get Amount Out - Happy Path Tests ============
784
785    #[test]
786    fn test_deposit_eth() {
787        let state = create_state();
788
789        // Deposit 10 ETH: fee=4, net=6 → 6*100/200 = 3 rETH
790        let res = state
791            .get_amount_out(
792                BigUint::from(10_000_000_000_000_000_000u128),
793                &eth_token(),
794                &reth_token(),
795            )
796            .unwrap();
797
798        assert_eq!(res.amount, BigUint::from(3_000_000_000_000_000_000u128));
799
800        let new_state = res
801            .new_state
802            .as_any()
803            .downcast_ref::<RocketpoolState>()
804            .unwrap();
805        // liquidity: 50 + 10 = 60
806        assert_eq!(new_state.deposit_contract_balance, U256::from(60e18));
807        // total_eth and reth_supply unchanged (managed by oracle)
808        assert_eq!(new_state.total_eth, U256::from(200e18));
809        assert_eq!(new_state.reth_supply, U256::from(100e18));
810    }
811
812    #[test]
813    fn test_deposit_within_extended_capacity() {
814        let mut state = create_state();
815        state.deposit_contract_balance = U256::from(990e18);
816        state.max_deposit_pool_size = U256::from(1000e18);
817        state.deposit_assigning_enabled = true;
818        state.queue_variable_end = U256::from(1); // 31 ETH extra
819
820        // Deposit 20 ETH: 990 + 20 = 1010 > 1000 base, but <= 1031 extended
821        // fee = 20 * 0.4 = 8, net = 12 → 12*100/200 = 6 rETH
822        let res = state
823            .get_amount_out(
824                BigUint::from(20_000_000_000_000_000_000u128),
825                &eth_token(),
826                &reth_token(),
827            )
828            .unwrap();
829
830        assert_eq!(res.amount, BigUint::from(6_000_000_000_000_000_000u128));
831
832        let new_state = res
833            .new_state
834            .as_any()
835            .downcast_ref::<RocketpoolState>()
836            .unwrap();
837        // liquidity: 990 + 20 = 1010
838        assert_eq!(new_state.deposit_contract_balance, U256::from(1010e18));
839        // total_eth and reth_supply unchanged (managed by oracle)
840        assert_eq!(new_state.total_eth, U256::from(200e18));
841        assert_eq!(new_state.reth_supply, U256::from(100e18));
842    }
843
844    #[test]
845    fn test_withdraw_reth() {
846        let state = create_state();
847
848        // Withdraw 10 rETH: 10*200/100 = 20 ETH
849        let res = state
850            .get_amount_out(
851                BigUint::from(10_000_000_000_000_000_000u128),
852                &reth_token(),
853                &eth_token(),
854            )
855            .unwrap();
856
857        assert_eq!(res.amount, BigUint::from(20_000_000_000_000_000_000u128));
858
859        let new_state = res
860            .new_state
861            .as_any()
862            .downcast_ref::<RocketpoolState>()
863            .unwrap();
864        // liquidity: 50 - 20 = 30
865        assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
866        // total_eth and reth_supply unchanged (managed by oracle)
867        assert_eq!(new_state.total_eth, U256::from(200e18));
868        assert_eq!(new_state.reth_supply, U256::from(100e18));
869    }
870
871    // ============ Get Amount Out - Error Cases Tests ============
872
873    #[test]
874    fn test_deposit_disabled() {
875        let mut state = create_state();
876        state.deposits_enabled = false;
877
878        let res = state.get_amount_out(BigUint::from(10u64), &eth_token(), &reth_token());
879        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
880    }
881
882    #[test]
883    fn test_deposit_below_minimum() {
884        let mut state = create_state();
885        state.min_deposit_amount = U256::from(100u64);
886
887        let res = state.get_amount_out(BigUint::from(50u64), &eth_token(), &reth_token());
888        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
889    }
890
891    #[test]
892    fn test_deposit_exceeds_max_pool() {
893        let mut state = create_state();
894        state.max_deposit_pool_size = U256::from(60e18); // Only 10 ETH room
895
896        let res = state.get_amount_out(
897            BigUint::from(20_000_000_000_000_000_000u128),
898            &eth_token(),
899            &reth_token(),
900        );
901        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
902    }
903
904    #[test]
905    fn test_deposit_queue_ignored_when_disabled() {
906        let mut state = create_state();
907        state.deposit_contract_balance = U256::from(990e18);
908        state.max_deposit_pool_size = U256::from(1000e18);
909        state.deposit_assigning_enabled = false;
910        state.queue_variable_end = U256::from(10); // Would add 310 ETH if enabled
911
912        // Deposit 20 ETH: 990 + 20 = 1010 > 1000 (queue ignored)
913        let res = state.get_amount_out(
914            BigUint::from(20_000_000_000_000_000_000u128),
915            &eth_token(),
916            &reth_token(),
917        );
918        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
919    }
920
921    #[test]
922    fn test_deposit_exceeds_extended_capacity() {
923        let mut state = create_state();
924        state.deposit_contract_balance = U256::from(990e18);
925        state.max_deposit_pool_size = U256::from(1000e18);
926        state.deposit_assigning_enabled = true;
927        state.queue_variable_end = U256::from(1); // 31 ETH extra, max = 1031
928
929        // Deposit 50 ETH: 990 + 50 = 1040 > 1031
930        let res = state.get_amount_out(
931            BigUint::from(50_000_000_000_000_000_000u128),
932            &eth_token(),
933            &reth_token(),
934        );
935        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
936    }
937
938    #[test]
939    fn test_withdrawal_insufficient_liquidity() {
940        let state = create_state(); // 50 ETH liquidity, withdraw needs 20 ETH per 10 rETH
941
942        // Try to withdraw 30 rETH = 60 ETH > 50 liquidity
943        let res = state.get_amount_out(
944            BigUint::from(30_000_000_000_000_000_000u128),
945            &reth_token(),
946            &eth_token(),
947        );
948        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
949    }
950
951    #[test]
952    fn test_withdrawal_limited_by_minipool_queue() {
953        let mut state = create_state();
954        state.deposit_contract_balance = U256::from(100e18);
955        // Queue has 2 variable minipools = 62 ETH capacity
956        state.queue_variable_end = U256::from(2);
957        // excess_balance = 100 - 62 = 38 ETH
958
959        // Try to withdraw 20 rETH = 40 ETH > 38 excess balance (but < 100 liquidity)
960        let res = state.get_amount_out(
961            BigUint::from(20_000_000_000_000_000_000u128),
962            &reth_token(),
963            &eth_token(),
964        );
965        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
966
967        // Withdraw 15 rETH = 30 ETH <= 38 excess balance should work
968        let res = state
969            .get_amount_out(
970                BigUint::from(15_000_000_000_000_000_000u128),
971                &reth_token(),
972                &eth_token(),
973            )
974            .unwrap();
975        assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
976    }
977
978    #[test]
979    fn test_withdrawal_zero_excess_balance() {
980        let mut state = create_state();
981        state.deposit_contract_balance = U256::from(62e18);
982        // Queue has 2 variable minipools = 62 ETH capacity
983        state.queue_variable_end = U256::from(2);
984        // excess_balance = 62 - 62 = 0 ETH
985
986        // Any withdrawal should fail
987        let res = state.get_amount_out(
988            BigUint::from(1_000_000_000_000_000_000u128),
989            &reth_token(),
990            &eth_token(),
991        );
992        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
993    }
994
995    #[test]
996    fn test_withdrawal_uses_both_pools() {
997        let mut state = create_state();
998        state.reth_contract_liquidity = U256::from(10e18);
999        state.deposit_contract_balance = U256::from(50e18);
1000        // No queue, so full 50 ETH is excess balance
1001        // Total available: 10 + 50 = 60 ETH
1002
1003        // Withdraw 15 rETH = 30 ETH (more than reth_contract_liquidity of 10 ETH)
1004        let res = state
1005            .get_amount_out(
1006                BigUint::from(15_000_000_000_000_000_000u128),
1007                &reth_token(),
1008                &eth_token(),
1009            )
1010            .unwrap();
1011
1012        assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
1013
1014        let new_state = res
1015            .new_state
1016            .as_any()
1017            .downcast_ref::<RocketpoolState>()
1018            .unwrap();
1019
1020        // reth_contract_liquidity drained to 0
1021        assert_eq!(new_state.reth_contract_liquidity, U256::ZERO);
1022        // deposit_contract_balance: 50 - (30 - 10) = 30 ETH
1023        assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
1024    }
1025
1026    #[test]
1027    fn test_limits_withdrawal_with_queue() {
1028        let mut state = create_state();
1029        state.deposit_contract_balance = U256::from(100e18);
1030        // Queue has 2 variable minipools = 62 ETH capacity
1031        state.queue_variable_end = U256::from(2);
1032
1033        let (max_sell, max_buy) = state
1034            .get_limits(reth_token().address, eth_token().address)
1035            .unwrap();
1036
1037        // max_buy = excess_balance = 100 - 62 = 38 ETH
1038        assert_eq!(max_buy, BigUint::from(38_000_000_000_000_000_000u128));
1039        // max_sell = 38 * 100/200 = 19 rETH
1040        assert_eq!(max_sell, BigUint::from(19_000_000_000_000_000_000u128));
1041    }
1042
1043    // ============ Assign Deposits Tests ============
1044
1045    #[test]
1046    fn test_assign_deposits_dequeues_minipools() {
1047        let mut state = create_state();
1048        state.deposit_contract_balance = U256::from(100e18);
1049        state.deposit_assigning_enabled = true;
1050        state.deposit_assign_maximum = U256::from(10u64);
1051        state.deposit_assign_socialised_maximum = U256::from(2u64);
1052        state.queue_variable_end = U256::from(5); // 5 minipools in queue
1053
1054        // Deposit 62 ETH (2 * 31 ETH)
1055        // scalingCount = 62 / 31 = 2
1056        // totalEthCount = (100 + 62) / 31 = 5
1057        // assignments = socialised(2) + scaling(2) = 4
1058        // capped at min(4, 5, 10, 5) = 4
1059        // eth_assigned = 4 * 31 = 124 ETH
1060        // new_liquidity = 100 + 62 - 124 = 38 ETH
1061        let res = state
1062            .get_amount_out(
1063                BigUint::from(62_000_000_000_000_000_000u128),
1064                &eth_token(),
1065                &reth_token(),
1066            )
1067            .unwrap();
1068
1069        let new_state = res
1070            .new_state
1071            .as_any()
1072            .downcast_ref::<RocketpoolState>()
1073            .unwrap();
1074
1075        assert_eq!(new_state.deposit_contract_balance, U256::from(38e18));
1076        assert_eq!(new_state.queue_variable_start, U256::from(4u64));
1077    }
1078
1079    #[test]
1080    fn test_assign_deposits_capped_by_queue_length() {
1081        let mut state = create_state();
1082        state.deposit_contract_balance = U256::from(100e18);
1083        state.deposit_assigning_enabled = true;
1084        state.deposit_assign_maximum = U256::from(10u64);
1085        state.deposit_assign_socialised_maximum = U256::from(5u64);
1086        state.queue_variable_end = U256::from(2); // Only 2 minipools in queue
1087
1088        // Deposit 62 ETH
1089        // assignments = 5 + 2 = 7, but capped at queue length of 2
1090        // eth_assigned = 2 * 31 = 62 ETH
1091        // new_liquidity = 100 + 62 - 62 = 100 ETH
1092        let res = state
1093            .get_amount_out(
1094                BigUint::from(62_000_000_000_000_000_000u128),
1095                &eth_token(),
1096                &reth_token(),
1097            )
1098            .unwrap();
1099
1100        let new_state = res
1101            .new_state
1102            .as_any()
1103            .downcast_ref::<RocketpoolState>()
1104            .unwrap();
1105
1106        assert_eq!(new_state.deposit_contract_balance, U256::from(100e18));
1107        assert_eq!(new_state.queue_variable_start, U256::from(2u64));
1108    }
1109
1110    #[test]
1111    fn test_assign_deposits_capped_by_max_assignments() {
1112        let mut state = create_state();
1113        state.deposit_contract_balance = U256::from(100e18);
1114        state.deposit_assigning_enabled = true;
1115        state.deposit_assign_maximum = U256::from(1u64); // Only 1 assignment allowed
1116        state.deposit_assign_socialised_maximum = U256::from(5u64);
1117        state.queue_variable_end = U256::from(10);
1118
1119        // Deposit 62 ETH
1120        // assignments = 5 + 2 = 7, but capped at max of 1
1121        // eth_assigned = 1 * 31 = 31 ETH
1122        // new_liquidity = 100 + 62 - 31 = 131 ETH
1123        let res = state
1124            .get_amount_out(
1125                BigUint::from(62_000_000_000_000_000_000u128),
1126                &eth_token(),
1127                &reth_token(),
1128            )
1129            .unwrap();
1130
1131        let new_state = res
1132            .new_state
1133            .as_any()
1134            .downcast_ref::<RocketpoolState>()
1135            .unwrap();
1136
1137        assert_eq!(new_state.deposit_contract_balance, U256::from(131e18));
1138        assert_eq!(new_state.queue_variable_start, U256::from(1u64));
1139    }
1140
1141    #[test]
1142    fn test_assign_deposits_capped_by_total_eth() {
1143        let mut state = create_state();
1144        state.deposit_contract_balance = U256::from(10e18); // Low liquidity
1145        state.deposit_assigning_enabled = true;
1146        state.deposit_assign_maximum = U256::from(10u64);
1147        state.deposit_assign_socialised_maximum = U256::from(5u64);
1148        state.queue_variable_end = U256::from(10);
1149
1150        // Deposit 31 ETH
1151        // totalEthCount = (10 + 31) / 31 = 1
1152        // assignments = 5 + 1 = 6, but capped at totalEthCount of 1
1153        // eth_assigned = 1 * 31 = 31 ETH
1154        // new_liquidity = 10 + 31 - 31 = 10 ETH
1155        let res = state
1156            .get_amount_out(
1157                BigUint::from(31_000_000_000_000_000_000u128),
1158                &eth_token(),
1159                &reth_token(),
1160            )
1161            .unwrap();
1162
1163        let new_state = res
1164            .new_state
1165            .as_any()
1166            .downcast_ref::<RocketpoolState>()
1167            .unwrap();
1168
1169        assert_eq!(new_state.deposit_contract_balance, U256::from(10e18));
1170        assert_eq!(new_state.queue_variable_start, U256::from(1u64));
1171    }
1172
1173    #[test]
1174    fn test_assign_deposits_empty_queue_no_change() {
1175        let mut state = create_state();
1176        state.deposit_assigning_enabled = true;
1177        state.deposit_assign_maximum = U256::from(10u64);
1178        state.deposit_assign_socialised_maximum = U256::from(2u64);
1179        // queue_variable_end = 0, so queue is empty
1180
1181        // Deposit 10 ETH - no minipools to dequeue
1182        let res = state
1183            .get_amount_out(
1184                BigUint::from(10_000_000_000_000_000_000u128),
1185                &eth_token(),
1186                &reth_token(),
1187            )
1188            .unwrap();
1189
1190        let new_state = res
1191            .new_state
1192            .as_any()
1193            .downcast_ref::<RocketpoolState>()
1194            .unwrap();
1195
1196        // liquidity: 50 + 10 = 60 (no withdrawal)
1197        assert_eq!(new_state.deposit_contract_balance, U256::from(60e18));
1198        assert_eq!(new_state.queue_variable_start, U256::ZERO);
1199    }
1200
1201    // ============ Live Transaction Tests ============
1202
1203    /// Test against real transaction deposit on Rocketpool
1204    /// 0x6213b6c235c52d2132711c18a1c66934832722fd71c098e843bc792ecdbd11b3 where user deposited
1205    /// exactly 4.5 ETH and received 3.905847020555141679 rETH 1 minipool was assigned (31 ETH
1206    /// withdrawn from pool)
1207    #[test]
1208    fn test_live_deposit_tx_6213b6c2() {
1209        let state = RocketpoolState::new(
1210            U256::from_str_radix("4ec08ba071647b927594", 16).unwrap(), // reth_supply
1211            U256::from_str_radix("5aafbb189fbbc1704662", 16).unwrap(), // total_eth
1212            U256::from_str_radix("17a651238b0dbf892", 16).unwrap(),    // deposit_contract_balance
1213            U256::from(781003199),                                     // reth_contract_liquidity
1214            U256::from_str_radix("1c6bf52634000", 16).unwrap(),        // deposit_fee (0.05%)
1215            true,                                                      // deposits_enabled
1216            U256::from_str_radix("2386f26fc10000", 16).unwrap(),       // min_deposit_amount
1217            U256::from_str_radix("3cfc82e37e9a7400000", 16).unwrap(),  // max_deposit_pool_size
1218            true,                                                      // deposit_assigning_enabled
1219            U256::from_str_radix("5a", 16).unwrap(),                   // deposit_assign_maximum
1220            U256::from_str_radix("2", 16).unwrap(), // deposit_assign_socialised_maximum
1221            U256::from_str_radix("6d43", 16).unwrap(), // queue_variable_start
1222            U256::from_str_radix("6dde", 16).unwrap(), // queue_variable_end
1223        );
1224
1225        // User deposited exactly 4.5 ETH
1226        let deposit_amount = BigUint::from(4_500_000_000_000_000_000u128);
1227
1228        let res = state
1229            .get_amount_out(deposit_amount, &eth_token(), &reth_token())
1230            .unwrap();
1231
1232        println!("calculated rETH out: {}", res.amount);
1233        let expected_reth_out = BigUint::from(3_905_847_020_555_141_679u128);
1234        assert_eq!(res.amount, expected_reth_out);
1235
1236        let new_state = res
1237            .new_state
1238            .as_any()
1239            .downcast_ref::<RocketpoolState>()
1240            .unwrap();
1241
1242        // Expected final balance to reduce by 31 ETH (1 minipool assigned)
1243        let expected_balance = U256::from_str_radix("0aa2289fdd01f892", 16).unwrap();
1244        assert_eq!(new_state.deposit_contract_balance, expected_balance);
1245
1246        // Expected queue_variable_start to advance by 1 (1 minipool assigned)
1247        let expected_queue_start = U256::from_str_radix("6d44", 16).unwrap();
1248        assert_eq!(new_state.queue_variable_start, expected_queue_start);
1249
1250        // Other state variables unchanged
1251        assert_eq!(new_state.total_eth, state.total_eth);
1252        assert_eq!(new_state.reth_supply, state.reth_supply);
1253        assert_eq!(new_state.deposit_fee, state.deposit_fee);
1254        assert_eq!(new_state.deposits_enabled, state.deposits_enabled);
1255        assert_eq!(new_state.min_deposit_amount, state.min_deposit_amount);
1256        assert_eq!(new_state.max_deposit_pool_size, state.max_deposit_pool_size);
1257        assert_eq!(new_state.deposit_assigning_enabled, state.deposit_assigning_enabled);
1258        assert_eq!(new_state.deposit_assign_maximum, state.deposit_assign_maximum);
1259        assert_eq!(
1260            new_state.deposit_assign_socialised_maximum,
1261            state.deposit_assign_socialised_maximum
1262        );
1263        assert_eq!(new_state.queue_variable_end, state.queue_variable_end);
1264    }
1265
1266    /// Test against real withdrawal (burn) transaction on
1267    /// Rocketpool0xf0f615f5dcf40d6ba1168da654a9ea8a0e855e489a34f4ffc3c7d2ad165f0bd6 where user
1268    /// burned 20.873689741238146923 rETH and received 24.000828571949999998 ETH
1269    #[test]
1270    fn test_live_withdraw_tx_block_23736567() {
1271        let state = RocketpoolState::new(
1272            U256::from_str_radix("516052628fbe875ffff0", 16).unwrap(), // reth_supply
1273            U256::from_str_radix("5d9143622860d8bdacea", 16).unwrap(), // total_eth
1274            U256::from_str_radix("1686dc9300da8004d", 16).unwrap(),    // deposit_contract_balance
1275            U256::from_str_radix("14d141273efab8a43", 16).unwrap(),    // reth_contract_liquidity
1276            U256::from_str_radix("1c6bf52634000", 16).unwrap(),        // deposit_fee (0.05%)
1277            true,                                                      // deposits_enabled
1278            U256::from_str_radix("2386f26fc10000", 16).unwrap(),       // min_deposit_amount
1279            U256::from_str_radix("3cfc82e37e9a7400000", 16).unwrap(),  // max_deposit_pool_size
1280            true,                                                      // deposit_assigning_enabled
1281            U256::from_str_radix("5a", 16).unwrap(),                   // deposit_assign_maximum
1282            U256::from_str_radix("2", 16).unwrap(), // deposit_assign_socialised_maximum
1283            U256::from_str_radix("6d34", 16).unwrap(), // queue_variable_start
1284            U256::from_str_radix("6dd0", 16).unwrap(), // queue_variable_end
1285        );
1286
1287        // User burned exactly 20873689741238146923 rETH
1288        let burn_amount = BigUint::from(20_873_689_741_238_146_923u128);
1289
1290        let res = state
1291            .get_amount_out(burn_amount, &reth_token(), &eth_token())
1292            .unwrap();
1293
1294        // Expected ETH out: 24000828571949999998 wei
1295        let expected_eth_out = BigUint::from(24_000_828_571_949_999_998u128);
1296        assert_eq!(res.amount, expected_eth_out);
1297
1298        let new_state = res
1299            .new_state
1300            .as_any()
1301            .downcast_ref::<RocketpoolState>()
1302            .unwrap();
1303
1304        // Verify liquidity was updated correctly
1305        let expected_liquidity = U256::from_str_radix("74d8b62c5", 16).unwrap();
1306        assert_eq!(new_state.reth_contract_liquidity, expected_liquidity);
1307
1308        // Other state variables unchanged
1309        assert_eq!(new_state.total_eth, state.total_eth);
1310        assert_eq!(new_state.reth_supply, state.reth_supply);
1311        assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1312        assert_eq!(new_state.deposit_fee, state.deposit_fee);
1313        assert_eq!(new_state.deposits_enabled, state.deposits_enabled);
1314        assert_eq!(new_state.min_deposit_amount, state.min_deposit_amount);
1315        assert_eq!(new_state.max_deposit_pool_size, state.max_deposit_pool_size);
1316        assert_eq!(new_state.deposit_assigning_enabled, state.deposit_assigning_enabled);
1317        assert_eq!(new_state.deposit_assign_maximum, state.deposit_assign_maximum);
1318        assert_eq!(
1319            new_state.deposit_assign_socialised_maximum,
1320            state.deposit_assign_socialised_maximum
1321        );
1322        assert_eq!(new_state.queue_variable_start, state.queue_variable_start);
1323        assert_eq!(new_state.queue_variable_end, state.queue_variable_end);
1324    }
1325}