Skip to main content

tycho_simulation/evm/protocol/rocketpool/
state.rs

1use std::{any::Any, collections::HashMap};
2
3use alloy::primitives::U256;
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use tycho_common::{
7    dto::ProtocolStateDelta,
8    models::token::Token,
9    simulation::{
10        errors::{SimulationError, TransitionError},
11        protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
12    },
13    Bytes,
14};
15use tycho_ethereum::BytesCodec;
16
17use crate::evm::protocol::{
18    rocketpool::ETH_ADDRESS,
19    safe_math::{safe_add_u256, safe_sub_u256},
20    u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
21    utils::solidity_math::mul_div,
22};
23
24const DEPOSIT_FEE_BASE: u128 = 1_000_000_000_000_000_000; // 1e18
25
26/// 32 ETH — the hardcoded amount every megapool queue entry requests.
27const FULL_DEPOSIT_VALUE: u128 = 32_000_000_000_000_000_000u128;
28
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct RocketpoolState {
31    pub reth_supply: U256,
32    pub total_eth: U256,
33    /// ETH available in the deposit pool contract
34    pub deposit_contract_balance: U256,
35    /// ETH available in the rETH contract
36    pub reth_contract_liquidity: U256,
37    /// Deposit fee as %, scaled by DEPOSIT_FEE_BASE, such as 500_000_000_000_000 represents 0.05%
38    /// fee.
39    pub deposit_fee: U256,
40    pub deposits_enabled: bool,
41    pub min_deposit_amount: U256,
42    pub max_deposit_pool_size: U256,
43    /// Whether assigning deposits is enabled (allows using queue capacity)
44    pub deposit_assigning_enabled: bool,
45    /// Maximum number of assignments per deposit
46    pub deposit_assign_maximum: U256,
47    /// The base number of assignments to try per deposit
48    pub deposit_assign_socialised_maximum: U256,
49    /// Total ETH requested across express + standard megapool queues
50    pub megapool_queue_requested_total: U256,
51    /// Target rETH collateral rate (scaled by 1e18, e.g. 0.01e18 = 1%).
52    /// On-chain: RocketDAOProtocolSettingsNetwork.getTargetRethCollateralRate()
53    pub target_reth_collateral_rate: 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        megapool_queue_requested_total: U256,
71        target_reth_collateral_rate: 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            megapool_queue_requested_total,
86            target_reth_collateral_rate,
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        // rethOut = netEth * rethSupply / totalEth
96        mul_div(net_eth, self.reth_supply, self.total_eth)
97    }
98
99    /// Calculates ETH amount out for a given rETH burn amount.
100    fn get_eth_value(&self, reth_amount: U256) -> Result<U256, SimulationError> {
101        // ethOut = rethIn * totalEth / rethSupply
102        mul_div(reth_amount, self.total_eth, self.reth_supply)
103    }
104
105    fn is_depositing_eth(token_in: &Bytes) -> bool {
106        token_in.as_ref() == ETH_ADDRESS
107    }
108
109    fn assert_deposits_enabled(&self) -> Result<(), SimulationError> {
110        if !self.deposits_enabled {
111            Err(SimulationError::RecoverableError(
112                "Deposits are currently disabled in Rocketpool".to_string(),
113            ))
114        } else {
115            Ok(())
116        }
117    }
118
119    /// Returns the maximum deposit capacity considering both the base pool size
120    /// and the megapool queue capacity (if deposit_assigning_enabled).
121    ///
122    /// Note: we only model megapool queues here. The legacy minipool queues were
123    /// fully drained before the Saturn (v1.4) upgrade activated, and deposits into
124    /// them are disabled, so their capacity is always zero.
125    fn get_max_deposit_capacity(&self) -> Result<U256, SimulationError> {
126        if self.deposit_assigning_enabled {
127            safe_add_u256(self.max_deposit_pool_size, self.megapool_queue_requested_total)
128        } else {
129            Ok(self.max_deposit_pool_size)
130        }
131    }
132
133    /// Returns the excess balance available for withdrawals from the deposit pool.
134    ///
135    /// ETH requested for queued megapool entries is reserved and cannot be withdrawn.
136    /// Only the surplus above queue requested is available.
137    fn get_deposit_pool_excess_balance(&self) -> Result<U256, SimulationError> {
138        if self.megapool_queue_requested_total >= self.deposit_contract_balance {
139            Ok(U256::ZERO)
140        } else {
141            safe_sub_u256(self.deposit_contract_balance, self.megapool_queue_requested_total)
142        }
143    }
144
145    /// Returns total available liquidity for withdrawals.
146    /// This is the sum of reth_contract_liquidity and the deposit pool excess balance.
147    fn get_total_available_for_withdrawal(&self) -> Result<U256, SimulationError> {
148        let deposit_pool_excess = self.get_deposit_pool_excess_balance()?;
149        safe_add_u256(self.reth_contract_liquidity, deposit_pool_excess)
150    }
151
152    /// Computes how `processDeposit()` routes deposited ETH between the rETH contract
153    /// (collateral buffer) and the deposit pool vault.
154    ///
155    /// Returns `(to_reth_contract, to_deposit_vault)`.
156    fn compute_deposit_routing(
157        &self,
158        deposit_amount: U256,
159    ) -> Result<(U256, U256), SimulationError> {
160        // target_collateral = total_eth * target_reth_collateral_rate / 1e18
161        let target_collateral = mul_div(
162            self.total_eth,
163            self.target_reth_collateral_rate,
164            U256::from(DEPOSIT_FEE_BASE),
165        )?;
166        // Fill the rETH contract up to its target, send the rest to the deposit vault.
167        let shortfall = target_collateral.saturating_sub(self.reth_contract_liquidity);
168        let to_reth = deposit_amount.min(shortfall);
169        let to_vault = deposit_amount - to_reth;
170        Ok((to_reth, to_vault))
171    }
172
173    /// Calculates ETH assigned from the deposit pool to megapool queue entries.
174    ///
175    /// Three constraints bound assignment:
176    ///   1. Count cap:    floor(deposit / 32 ETH) + socialisedMax, ≤ deposit_assign_maximum
177    ///   2. Vault cap:    floor(deposit_contract_balance / 32 ETH)
178    ///   3. Queue depth:  floor(megapool_queue_requested_total / 32 ETH)
179    ///
180    /// The minimum of these three gives the number of entries assigned.
181    /// Total ETH assigned = entries × 32 ETH.
182    fn calculate_assign_deposits(&self, deposit_amount: U256) -> U256 {
183        if !self.deposit_assigning_enabled ||
184            self.megapool_queue_requested_total
185                .is_zero()
186        {
187            return U256::ZERO;
188        }
189
190        let full_deposit_value = U256::from(FULL_DEPOSIT_VALUE);
191
192        // Constraint 1: count cap
193        let scaling_count = deposit_amount / full_deposit_value;
194        let count_cap = (self.deposit_assign_socialised_maximum + scaling_count)
195            .min(self.deposit_assign_maximum);
196
197        // Constraint 2: vault balance
198        let vault_cap = self.deposit_contract_balance / full_deposit_value;
199
200        // Constraint 3: queue depth
201        let queue_entries = self.megapool_queue_requested_total / full_deposit_value;
202
203        // Entries assigned = min of all three constraints
204        let entries = count_cap
205            .min(vault_cap)
206            .min(queue_entries);
207
208        entries * full_deposit_value
209    }
210}
211
212#[typetag::serde]
213impl ProtocolSim for RocketpoolState {
214    fn fee(&self) -> f64 {
215        unimplemented!("Rocketpool has asymmetric fees; use spot_price or get_amount_out instead")
216    }
217
218    fn spot_price(&self, _base: &Token, quote: &Token) -> Result<f64, SimulationError> {
219        // As we are computing the amount of quote needed to buy 1 base, we check the quote token.
220        let is_depositing_eth = RocketpoolState::is_depositing_eth(&quote.address);
221
222        // As the protocol has no slippage, we can use a fixed amount for spot price calculation.
223        // We compute how much base we get for 1 quote, then invert to get quote needed for 1 base.
224        let amount = U256::from(1e18);
225
226        let base_per_quote = if is_depositing_eth {
227            // base=rETH, quote=ETH: compute rETH for given ETH
228            self.assert_deposits_enabled()?;
229            self.get_reth_value(amount)?
230        } else {
231            // base=ETH, quote=rETH: compute ETH for given rETH
232            self.get_eth_value(amount)?
233        };
234
235        let base_per_quote = u256_to_f64(base_per_quote)? / 1e18;
236        // Invert to get how much quote needed to buy 1 base
237        Ok(1.0 / base_per_quote)
238    }
239
240    fn get_amount_out(
241        &self,
242        amount_in: BigUint,
243        token_in: &Token,
244        _token_out: &Token,
245    ) -> Result<GetAmountOutResult, SimulationError> {
246        let amount_in = biguint_to_u256(&amount_in);
247        let is_depositing_eth = RocketpoolState::is_depositing_eth(&token_in.address);
248
249        let amount_out = if is_depositing_eth {
250            self.assert_deposits_enabled()?;
251
252            if amount_in < self.min_deposit_amount {
253                return Err(SimulationError::InvalidInput(
254                    format!(
255                        "Deposit amount {} is less than the minimum deposit of {}",
256                        amount_in, self.min_deposit_amount
257                    ),
258                    None,
259                ));
260            }
261
262            let capacity_needed = safe_add_u256(self.deposit_contract_balance, amount_in)?;
263            let max_capacity = self.get_max_deposit_capacity()?;
264            if capacity_needed > max_capacity {
265                return Err(SimulationError::InvalidInput(
266                    format!(
267                        "Deposit would exceed maximum pool size (capacity needed: {}, max: {})",
268                        capacity_needed, max_capacity
269                    ),
270                    None,
271                ));
272            }
273
274            self.get_reth_value(amount_in)?
275        } else {
276            let eth_out = self.get_eth_value(amount_in)?;
277
278            let total_available = self.get_total_available_for_withdrawal()?;
279            if eth_out > total_available {
280                return Err(SimulationError::RecoverableError(format!(
281                    "Withdrawal {} exceeds available liquidity {}",
282                    eth_out, total_available
283                )));
284            }
285
286            eth_out
287        };
288
289        let mut new_state = self.clone();
290        // Note: total_eth and reth_supply are not updated as they are managed by an oracle.
291        if is_depositing_eth {
292            // route ETH between rETH collateral buffer and vault.
293            let (to_reth, to_vault) = new_state.compute_deposit_routing(amount_in)?;
294            new_state.reth_contract_liquidity =
295                safe_add_u256(new_state.reth_contract_liquidity, to_reth)?;
296            new_state.deposit_contract_balance =
297                safe_add_u256(new_state.deposit_contract_balance, to_vault)?;
298
299            // Assign deposits: dequeue megapool entries and send ETH from vault to validators.
300            // Both the vault balance and the queue requested are reduced by the same amount.
301            let eth_assigned = new_state.calculate_assign_deposits(amount_in);
302            if eth_assigned > U256::ZERO {
303                new_state.deposit_contract_balance =
304                    safe_sub_u256(new_state.deposit_contract_balance, eth_assigned)?;
305                new_state.megapool_queue_requested_total =
306                    safe_sub_u256(new_state.megapool_queue_requested_total, eth_assigned)?;
307            }
308        } else {
309            // Withdraw from rETH contract first, spill remainder into deposit pool.
310            let needed_from_deposit_pool =
311                amount_out.saturating_sub(new_state.reth_contract_liquidity);
312            new_state.reth_contract_liquidity = new_state
313                .reth_contract_liquidity
314                .saturating_sub(amount_out);
315            new_state.deposit_contract_balance =
316                safe_sub_u256(new_state.deposit_contract_balance, needed_from_deposit_pool)?;
317        }
318
319        // The ETH withdrawal gas estimation assumes a best case scenario when there is sufficient
320        // liquidity in the rETH contract. Note that there has never been a situation during a
321        // withdrawal when this was not the case, hence the simplified gas estimation.
322        let gas_used = if is_depositing_eth { 209_000u32 } else { 134_000u32 };
323
324        Ok(GetAmountOutResult::new(
325            u256_to_biguint(amount_out),
326            BigUint::from(gas_used),
327            Box::new(new_state),
328        ))
329    }
330
331    fn get_limits(
332        &self,
333        sell_token: Bytes,
334        _buy_token: Bytes,
335    ) -> Result<(BigUint, BigUint), SimulationError> {
336        let is_depositing_eth = Self::is_depositing_eth(&sell_token);
337
338        if is_depositing_eth {
339            // ETH -> rETH: max sell = max_deposit_capacity - deposit_contract_balance
340            let max_capacity = self.get_max_deposit_capacity()?;
341            let max_eth_sell = safe_sub_u256(max_capacity, self.deposit_contract_balance)?;
342            let max_reth_buy = self.get_reth_value(max_eth_sell)?;
343            Ok((u256_to_biguint(max_eth_sell), u256_to_biguint(max_reth_buy)))
344        } else {
345            // rETH -> ETH: max buy = total available for withdrawal
346            let max_eth_buy = self.get_total_available_for_withdrawal()?;
347            let max_reth_sell = mul_div(max_eth_buy, self.reth_supply, self.total_eth)?;
348            Ok((u256_to_biguint(max_reth_sell), u256_to_biguint(max_eth_buy)))
349        }
350    }
351
352    fn delta_transition(
353        &mut self,
354        delta: ProtocolStateDelta,
355        _tokens: &HashMap<Bytes, Token>,
356        _balances: &Balances,
357    ) -> Result<(), TransitionError<String>> {
358        self.total_eth = delta
359            .updated_attributes
360            .get("total_eth")
361            .map_or(self.total_eth, U256::from_bytes);
362        self.reth_supply = delta
363            .updated_attributes
364            .get("reth_supply")
365            .map_or(self.reth_supply, U256::from_bytes);
366        self.deposit_contract_balance = delta
367            .updated_attributes
368            .get("deposit_contract_balance")
369            .map_or(self.deposit_contract_balance, U256::from_bytes);
370        self.reth_contract_liquidity = delta
371            .updated_attributes
372            .get("reth_contract_liquidity")
373            .map_or(self.reth_contract_liquidity, U256::from_bytes);
374        self.deposits_enabled = delta
375            .updated_attributes
376            .get("deposits_enabled")
377            .map_or(self.deposits_enabled, |val| !U256::from_bytes(val).is_zero());
378        self.deposit_assigning_enabled = delta
379            .updated_attributes
380            .get("deposit_assigning_enabled")
381            .map_or(self.deposit_assigning_enabled, |val| !U256::from_bytes(val).is_zero());
382        self.deposit_fee = delta
383            .updated_attributes
384            .get("deposit_fee")
385            .map_or(self.deposit_fee, U256::from_bytes);
386        self.min_deposit_amount = delta
387            .updated_attributes
388            .get("min_deposit_amount")
389            .map_or(self.min_deposit_amount, U256::from_bytes);
390        self.max_deposit_pool_size = delta
391            .updated_attributes
392            .get("max_deposit_pool_size")
393            .map_or(self.max_deposit_pool_size, U256::from_bytes);
394        self.deposit_assign_maximum = delta
395            .updated_attributes
396            .get("deposit_assign_maximum")
397            .map_or(self.deposit_assign_maximum, U256::from_bytes);
398        self.deposit_assign_socialised_maximum = delta
399            .updated_attributes
400            .get("deposit_assign_socialised_maximum")
401            .map_or(self.deposit_assign_socialised_maximum, U256::from_bytes);
402        self.megapool_queue_requested_total = delta
403            .updated_attributes
404            .get("megapool_queue_requested_total")
405            .map_or(self.megapool_queue_requested_total, U256::from_bytes);
406        self.target_reth_collateral_rate = delta
407            .updated_attributes
408            .get("target_reth_collateral_rate")
409            .map_or(self.target_reth_collateral_rate, U256::from_bytes);
410
411        Ok(())
412    }
413
414    fn clone_box(&self) -> Box<dyn ProtocolSim> {
415        Box::new(self.clone())
416    }
417
418    fn as_any(&self) -> &dyn Any {
419        self
420    }
421
422    fn as_any_mut(&mut self) -> &mut dyn Any {
423        self
424    }
425
426    fn eq(&self, other: &dyn ProtocolSim) -> bool {
427        if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
428            self == other_state
429        } else {
430            false
431        }
432    }
433
434    fn query_pool_swap(
435        &self,
436        params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
437    ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
438        crate::evm::query_pool_swap::query_pool_swap(self, params)
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use std::{
445        collections::{HashMap, HashSet},
446        str::FromStr,
447    };
448
449    use approx::assert_ulps_eq;
450    use num_bigint::BigUint;
451    use rstest::rstest;
452    use tycho_common::{
453        dto::ProtocolStateDelta,
454        hex_bytes::Bytes,
455        models::{token::Token, Chain},
456        simulation::{
457            errors::SimulationError,
458            protocol_sim::{Balances, ProtocolSim},
459        },
460    };
461
462    use super::*;
463
464    /// Helper function to create a RocketpoolState with easy-to-compute defaults for testing.
465    /// - Exchange rate: 1 rETH = 2 ETH (100 rETH backed by 200 ETH)
466    /// - Deposit fee: 40%
467    /// - Deposit contract balance: 50 ETH
468    /// - rETH contract liquidity: 0 ETH
469    /// - Max pool size: 1000 ETH
470    /// - Assign deposits enabled: false
471    /// - Target rETH collateral rate: 1% (0.01e18)
472    fn create_state() -> RocketpoolState {
473        RocketpoolState::new(
474            U256::from(100e18), // reth_supply: 100 rETH
475            U256::from(200e18), /* total_eth: 200 ETH (1 rETH =
476                                 * 2 ETH) */
477            U256::from(50e18), // deposit_contract_balance: 50 ETH
478            U256::ZERO,        // reth_contract_liquidity: 0 ETH
479            U256::from(400_000_000_000_000_000u64), // deposit_fee: 40% (0.4e18)
480            true,              // deposits_enabled
481            U256::ZERO,        // min_deposit_amount
482            U256::from(1000e18), // max_deposit_pool_size: 1000 ETH
483            false,             // deposit_assigning_enabled
484            U256::ZERO,        // deposit_assign_maximum
485            U256::ZERO,        // deposit_assign_socialised_maximum
486            U256::ZERO,        // megapool_queue_requested_total
487            U256::from(10_000_000_000_000_000u64), // target_reth_collateral_rate: 1%
488        )
489    }
490
491    fn eth_token() -> Token {
492        Token::new(&Bytes::from(ETH_ADDRESS), "ETH", 18, 0, &[Some(100_000)], Chain::Ethereum, 100)
493    }
494
495    fn reth_token() -> Token {
496        Token::new(
497            &Bytes::from_str("0xae78736Cd615f374D3085123A210448E74Fc6393").unwrap(),
498            "rETH",
499            18,
500            0,
501            &[Some(100_000)],
502            Chain::Ethereum,
503            100,
504        )
505    }
506
507    // ============ Max Deposit Capacity Tests ============
508
509    #[test]
510    fn test_max_capacity_assign_disabled() {
511        let state = create_state();
512        assert_eq!(
513            state
514                .get_max_deposit_capacity()
515                .unwrap(),
516            U256::from(1000e18)
517        );
518    }
519
520    #[test]
521    fn test_max_capacity_assign_enabled_empty_queue() {
522        let mut state = create_state();
523        state.deposit_assigning_enabled = true;
524        assert_eq!(
525            state
526                .get_max_deposit_capacity()
527                .unwrap(),
528            U256::from(1000e18)
529        );
530    }
531
532    #[test]
533    fn test_max_capacity_assign_enabled_with_queue() {
534        let mut state = create_state();
535        state.deposit_assigning_enabled = true;
536        state.megapool_queue_requested_total = U256::from(500e18);
537        // 1000 + 500 = 1500 ETH
538        assert_eq!(
539            state
540                .get_max_deposit_capacity()
541                .unwrap(),
542            U256::from(1500e18)
543        );
544    }
545
546    // ============ Deposit Routing Tests ============
547
548    #[rstest]
549    #[case::all_to_reth(
550        U256::ZERO,  // reth_contract_liquidity
551        U256::from(10_000_000_000_000_000u64),  // target_reth_collateral_rate: 1%
552        1_000_000_000_000_000_000u128,  // deposit: 1 ETH
553        1_000_000_000_000_000_000u128,  // expected to_reth
554        0u128,  // expected to_vault
555    )]
556    // shortfall (2 ETH) < deposit (10 ETH) → split
557    #[case::split(
558        U256::ZERO,
559        U256::from(10_000_000_000_000_000u64),
560        10_000_000_000_000_000_000u128,  // deposit: 10 ETH
561        2_000_000_000_000_000_000u128,   // to_reth: 2 ETH (shortfall)
562        8_000_000_000_000_000_000u128,   // to_vault: 8 ETH
563    )]
564    // no shortfall (liquidity 10 ETH > target 2 ETH) → all to vault
565    #[case::all_to_vault(
566        U256::from(10_000_000_000_000_000_000u128),  // liquidity: 10 ETH > 2 ETH target
567        U256::from(10_000_000_000_000_000u64),
568        5_000_000_000_000_000_000u128,  // deposit: 5 ETH
569        0u128,
570        5_000_000_000_000_000_000u128,
571    )]
572    // zero collateral rate → all to vault
573    #[case::zero_collateral_rate(
574        U256::ZERO,
575        U256::ZERO,  // target_reth_collateral_rate: 0%
576        5_000_000_000_000_000_000u128,
577        0u128,
578        5_000_000_000_000_000_000u128,
579    )]
580    fn test_deposit_routing(
581        #[case] reth_contract_liquidity: U256,
582        #[case] target_reth_collateral_rate: U256,
583        #[case] deposit: u128,
584        #[case] expected_to_reth: u128,
585        #[case] expected_to_vault: u128,
586    ) {
587        let mut state = create_state();
588        state.reth_contract_liquidity = reth_contract_liquidity;
589        state.target_reth_collateral_rate = target_reth_collateral_rate;
590
591        let (to_reth, to_vault) = state
592            .compute_deposit_routing(U256::from(deposit))
593            .unwrap();
594
595        assert_eq!(to_reth, U256::from(expected_to_reth));
596        assert_eq!(to_vault, U256::from(expected_to_vault));
597    }
598
599    // ============ Delta Transition Tests ============
600
601    #[test]
602    fn test_delta_transition_basic() {
603        let mut state = create_state();
604
605        let attributes: HashMap<String, Bytes> = [
606            ("total_eth", U256::from(300u64)),
607            ("reth_supply", U256::from(150u64)),
608            ("deposit_contract_balance", U256::from(100u64)),
609            ("reth_contract_liquidity", U256::from(20u64)),
610        ]
611        .into_iter()
612        .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
613        .collect();
614
615        let delta = ProtocolStateDelta {
616            component_id: "Rocketpool".to_owned(),
617            updated_attributes: attributes,
618            deleted_attributes: HashSet::new(),
619        };
620
621        state
622            .delta_transition(delta, &HashMap::new(), &Balances::default())
623            .unwrap();
624
625        assert_eq!(state.total_eth, U256::from(300u64));
626        assert_eq!(state.reth_supply, U256::from(150u64));
627        assert_eq!(state.deposit_contract_balance, U256::from(100u64));
628        assert_eq!(state.reth_contract_liquidity, U256::from(20u64));
629    }
630
631    #[test]
632    fn test_delta_transition_megapool_fields() {
633        let mut state = create_state();
634
635        let attributes: HashMap<String, Bytes> = [
636            ("megapool_queue_requested_total", U256::from(1000u64)),
637            ("target_reth_collateral_rate", U256::from(20_000_000_000_000_000u64)),
638        ]
639        .into_iter()
640        .map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
641        .collect();
642
643        let delta = ProtocolStateDelta {
644            component_id: "Rocketpool".to_owned(),
645            updated_attributes: attributes,
646            deleted_attributes: HashSet::new(),
647        };
648
649        state
650            .delta_transition(delta, &HashMap::new(), &Balances::default())
651            .unwrap();
652
653        assert_eq!(state.megapool_queue_requested_total, U256::from(1000u64));
654        assert_eq!(state.target_reth_collateral_rate, U256::from(20_000_000_000_000_000u64));
655    }
656
657    // ============ Spot Price Tests ============
658
659    #[test]
660    fn test_spot_price_deposit() {
661        let state = create_state();
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        let price = state
672            .spot_price(&reth_token(), &eth_token())
673            .unwrap();
674        assert_ulps_eq!(price, 1.0 / 0.3);
675    }
676
677    /// Validate withdrawal spot price against on-chain getEthValue(1e18) at block 24480104.
678    /// On-chain: getEthValue(1e18) = 1157737589816937166 → 1 rETH = 1.1577... ETH.
679    /// Our spot_price(ETH, rETH) should return 1/1.1577... = 0.8637... (rETH per ETH).
680    #[test]
681    fn test_live_spot_price_withdrawal() {
682        let state = create_state_at_block_24480104();
683        let price = state
684            .spot_price(&eth_token(), &reth_token())
685            .unwrap();
686
687        // Expected from on-chain getEthValue(1e18) = 1157737589816937166
688        let on_chain_eth_value = 1_157_737_589_816_937_166f64;
689        let expected = 1e18 / on_chain_eth_value;
690        assert_ulps_eq!(price, expected, max_ulps = 10);
691    }
692
693    /// Validate deposit spot price against on-chain getRethValue(1e18) at block 24480104.
694    /// On-chain: getRethValue(1e18) = 863753590447141981 (no deposit fee).
695    /// Our spot_price(rETH, ETH) applies the 0.05% deposit fee on top.
696    #[test]
697    fn test_live_spot_price_deposit() {
698        use crate::evm::protocol::utils::add_fee_markup;
699
700        let state = create_state_at_block_24480104();
701        let price = state
702            .spot_price(&reth_token(), &eth_token())
703            .unwrap();
704
705        // On-chain getRethValue(1e18) = 863753590447141981 (no fee)
706        // → exchange rate without fee = 1e18 / 863753590447141981
707        let on_chain_reth_value = 863_753_590_447_141_981f64;
708        let rate_without_fee = 1e18 / on_chain_reth_value;
709        let fee = 500_000_000_000_000f64 / DEPOSIT_FEE_BASE as f64; // 0.05%
710        let expected = add_fee_markup(rate_without_fee, fee);
711        assert_ulps_eq!(price, expected, max_ulps = 10);
712    }
713
714    #[test]
715    fn test_fee_panics() {
716        let state = create_state();
717        let result = std::panic::catch_unwind(|| state.fee());
718        assert!(result.is_err());
719    }
720
721    // ============ Get Limits Tests ============
722
723    #[test]
724    fn test_limits_deposit() {
725        let state = create_state();
726        let (max_sell, max_buy) = state
727            .get_limits(eth_token().address, reth_token().address)
728            .unwrap();
729        // max_sell = 1000 - 50 = 950 ETH
730        assert_eq!(max_sell, BigUint::from(950_000_000_000_000_000_000u128));
731        // max_buy = 950 * 0.6 * 100/200 = 285 rETH
732        assert_eq!(max_buy, BigUint::from(285_000_000_000_000_000_000u128));
733    }
734
735    #[test]
736    fn test_limits_withdrawal() {
737        let state = create_state();
738        let (max_sell, max_buy) = state
739            .get_limits(reth_token().address, eth_token().address)
740            .unwrap();
741        // max_buy = liquidity = 50 ETH
742        assert_eq!(max_buy, BigUint::from(50_000_000_000_000_000_000u128));
743        // max_sell = 50 * 100/200 = 25 rETH
744        assert_eq!(max_sell, BigUint::from(25_000_000_000_000_000_000u128));
745    }
746
747    #[test]
748    fn test_limits_with_megapool_queue() {
749        let mut state = create_state();
750        state.max_deposit_pool_size = U256::from(100e18);
751        state.deposit_assigning_enabled = true;
752        state.megapool_queue_requested_total = U256::from(62e18);
753
754        let (max_sell, _) = state
755            .get_limits(eth_token().address, reth_token().address)
756            .unwrap();
757        // max_capacity = 100 + 62 = 162 ETH; max_sell = 162 - 50 = 112 ETH
758        assert_eq!(max_sell, BigUint::from(112_000_000_000_000_000_000u128));
759    }
760
761    // ============ Limits Boundary Consistency Tests ============
762
763    #[test]
764    fn test_limits_deposit_boundary_accepted() {
765        let mut state = create_state();
766        state.deposit_assigning_enabled = true;
767        state.megapool_queue_requested_total = U256::from(64e18);
768
769        let (max_sell, _) = state
770            .get_limits(eth_token().address, reth_token().address)
771            .unwrap();
772
773        // Depositing exactly max_sell should succeed
774        let res = state.get_amount_out(max_sell.clone(), &eth_token(), &reth_token());
775        assert!(res.is_ok(), "max_sell should be accepted");
776
777        // Depositing max_sell + 1 wei should fail
778        let over = max_sell + BigUint::from(1u64);
779        let res = state.get_amount_out(over, &eth_token(), &reth_token());
780        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
781    }
782
783    #[test]
784    fn test_limits_withdrawal_boundary_accepted() {
785        let mut state = create_state();
786        state.reth_contract_liquidity = U256::from(30e18);
787
788        let (max_sell, _) = state
789            .get_limits(reth_token().address, eth_token().address)
790            .unwrap();
791
792        // Withdrawing exactly max_sell should succeed
793        let res = state.get_amount_out(max_sell.clone(), &reth_token(), &eth_token());
794        assert!(res.is_ok(), "max_sell should be accepted");
795
796        // Withdrawing max_sell + 1 wei should fail
797        let over = max_sell + BigUint::from(1u64);
798        let res = state.get_amount_out(over, &reth_token(), &eth_token());
799        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
800    }
801
802    // ============ Get Amount Out - Happy Path Tests ============
803
804    #[test]
805    fn test_deposit_eth() {
806        let state = create_state();
807        // Deposit 10 ETH: fee=4, net=6 → 6*100/200 = 3 rETH
808        let res = state
809            .get_amount_out(
810                BigUint::from(10_000_000_000_000_000_000u128),
811                &eth_token(),
812                &reth_token(),
813            )
814            .unwrap();
815
816        assert_eq!(res.amount, BigUint::from(3_000_000_000_000_000_000u128));
817
818        // Collateral routing: target = 200e18 * 1e16 / 1e18 = 2e18.
819        // Shortfall = 2e18 - 0 = 2e18. to_reth = min(10, 2) = 2, to_vault = 8.
820        // deposit_contract_balance = 50 + 8 = 58 ETH
821        let new_state = res
822            .new_state
823            .as_any()
824            .downcast_ref::<RocketpoolState>()
825            .unwrap();
826        assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
827        assert_eq!(new_state.reth_contract_liquidity, U256::from(2e18));
828    }
829
830    #[test]
831    fn test_withdraw_reth() {
832        let state = create_state();
833        // Withdraw 10 rETH: 10*200/100 = 20 ETH
834        let res = state
835            .get_amount_out(
836                BigUint::from(10_000_000_000_000_000_000u128),
837                &reth_token(),
838                &eth_token(),
839            )
840            .unwrap();
841
842        assert_eq!(res.amount, BigUint::from(20_000_000_000_000_000_000u128));
843
844        let new_state = res
845            .new_state
846            .as_any()
847            .downcast_ref::<RocketpoolState>()
848            .unwrap();
849        assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
850    }
851
852    // ============ Get Amount Out - Error Cases Tests ============
853
854    #[test]
855    fn test_deposit_disabled() {
856        let mut state = create_state();
857        state.deposits_enabled = false;
858        let res = state.get_amount_out(BigUint::from(10u64), &eth_token(), &reth_token());
859        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
860    }
861
862    #[test]
863    fn test_deposit_below_minimum() {
864        let mut state = create_state();
865        state.min_deposit_amount = U256::from(100u64);
866        let res = state.get_amount_out(BigUint::from(50u64), &eth_token(), &reth_token());
867        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
868    }
869
870    #[test]
871    fn test_deposit_exceeds_max_pool() {
872        let mut state = create_state();
873        state.max_deposit_pool_size = U256::from(60e18);
874        let res = state.get_amount_out(
875            BigUint::from(20_000_000_000_000_000_000u128),
876            &eth_token(),
877            &reth_token(),
878        );
879        assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
880    }
881
882    #[test]
883    fn test_withdrawal_insufficient_liquidity() {
884        let state = create_state();
885        let res = state.get_amount_out(
886            BigUint::from(30_000_000_000_000_000_000u128),
887            &reth_token(),
888            &eth_token(),
889        );
890        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
891    }
892
893    #[test]
894    fn test_withdrawal_limited_by_queue() {
895        let mut state = create_state();
896        state.deposit_contract_balance = U256::from(100e18);
897        state.megapool_queue_requested_total = U256::from(62e18);
898
899        // excess = 100 - 62 = 38 ETH; try withdraw 20 rETH = 40 ETH > 38
900        let res = state.get_amount_out(
901            BigUint::from(20_000_000_000_000_000_000u128),
902            &reth_token(),
903            &eth_token(),
904        );
905        assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
906
907        // Withdraw 15 rETH = 30 ETH <= 38 should work
908        let res = state
909            .get_amount_out(
910                BigUint::from(15_000_000_000_000_000_000u128),
911                &reth_token(),
912                &eth_token(),
913            )
914            .unwrap();
915        assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
916    }
917
918    #[test]
919    fn test_withdrawal_uses_both_pools() {
920        let mut state = create_state();
921        state.reth_contract_liquidity = U256::from(10e18);
922        state.deposit_contract_balance = U256::from(50e18);
923
924        // Withdraw 15 rETH = 30 ETH (more than reth_contract_liquidity of 10 ETH)
925        let res = state
926            .get_amount_out(
927                BigUint::from(15_000_000_000_000_000_000u128),
928                &reth_token(),
929                &eth_token(),
930            )
931            .unwrap();
932
933        assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
934
935        let new_state = res
936            .new_state
937            .as_any()
938            .downcast_ref::<RocketpoolState>()
939            .unwrap();
940        assert_eq!(new_state.reth_contract_liquidity, U256::ZERO);
941        assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
942    }
943
944    // ============ Assign Deposits Tests ============
945
946    #[test]
947    fn test_assign_deposits_with_queue() {
948        let mut state = create_state();
949        state.deposit_contract_balance = U256::from(200e18);
950        state.reth_contract_liquidity = U256::from(10e18); // above 1% target, no shortfall
951        state.deposit_assigning_enabled = true;
952        state.deposit_assign_maximum = U256::from(90u64);
953        state.megapool_queue_requested_total = U256::from(128e18); // 4 entries of 32 ETH
954
955        // Deposit 100 ETH
956        let res = state
957            .get_amount_out(
958                BigUint::from(100_000_000_000_000_000_000u128),
959                &eth_token(),
960                &reth_token(),
961            )
962            .unwrap();
963
964        let new_state = res
965            .new_state
966            .as_any()
967            .downcast_ref::<RocketpoolState>()
968            .unwrap();
969        // Collateral routing: target = 200e18 * 0.01 = 2e18.
970        // Shortfall = 2e18 - 10e18 = 0 (already above target). to_reth=0, to_vault=100.
971        // vault after routing = 200 + 100 = 300 ETH.
972        //
973        // Assignment: scaling_count = 100/32 = 3.
974        // count_cap = min(0 + 3, 90) = 3.
975        // vault_cap = 300/32 = 9. queue_entries = 128/32 = 4.
976        // entries = min(3, 9, 4) = 3. assigned = 3 * 32 = 96 ETH.
977        // vault after = 300 - 96 = 204. queue after = 128 - 96 = 32.
978        assert_eq!(new_state.deposit_contract_balance, U256::from(204e18));
979        assert_eq!(new_state.megapool_queue_requested_total, U256::from(32e18));
980    }
981
982    #[test]
983    fn test_assign_deposits_empty_queue() {
984        let mut state = create_state();
985        state.deposit_assigning_enabled = true;
986
987        let res = state
988            .get_amount_out(
989                BigUint::from(10_000_000_000_000_000_000u128),
990                &eth_token(),
991                &reth_token(),
992            )
993            .unwrap();
994
995        let new_state = res
996            .new_state
997            .as_any()
998            .downcast_ref::<RocketpoolState>()
999            .unwrap();
1000        // No queue, no assignment. Routing: to_reth=2, to_vault=8. Balance = 50+8 = 58.
1001        assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
1002    }
1003
1004    #[test]
1005    fn test_assign_deposits_disabled() {
1006        let mut state = create_state();
1007        state.deposit_assigning_enabled = false;
1008        state.megapool_queue_requested_total = U256::from(100e18);
1009
1010        let res = state
1011            .get_amount_out(
1012                BigUint::from(10_000_000_000_000_000_000u128),
1013                &eth_token(),
1014                &reth_token(),
1015            )
1016            .unwrap();
1017
1018        let new_state = res
1019            .new_state
1020            .as_any()
1021            .downcast_ref::<RocketpoolState>()
1022            .unwrap();
1023        // Assign disabled, no assignment. Routing: to_reth=2, to_vault=8. Balance = 50+8 = 58.
1024        assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
1025    }
1026
1027    /// Full flow: deposit splits between collateral buffer and vault, then assignment drains vault.
1028    /// Exercises the combined routing + assignment path that is unique to Saturn v1.4.
1029    ///
1030    /// Setup: shortfall = 20 ETH, deposit = 100 ETH, vault = 50 ETH, queue = 96 ETH (3 entries).
1031    /// Routing: to_reth = 20 (fills shortfall), to_vault = 80. Vault after = 50 + 80 = 130 ETH.
1032    /// Assignment: scaling_count = 100/32 = 3, count_cap = min(3, 90) = 3.
1033    ///   vault_cap = 130/32 = 4. queue = 96/32 = 3. entries = min(3, 4, 3) = 3. assigned = 96 ETH.
1034    /// Final: vault = 130 - 96 = 34, queue = 96 - 96 = 0, reth_liq = 0 + 20 = 20.
1035    #[test]
1036    fn test_deposit_split_routing_with_assignment() {
1037        let mut state = create_state();
1038        state.reth_contract_liquidity = U256::ZERO;
1039        state.deposit_contract_balance = U256::from(50e18);
1040        state.deposit_assigning_enabled = true;
1041        state.deposit_assign_maximum = U256::from(90u64);
1042        state.megapool_queue_requested_total = U256::from(96e18); // 3 entries
1043                                                                  // target = 200e18 * 10% / 1e18 = 20 ETH → shortfall = 20 - 0 = 20
1044        state.target_reth_collateral_rate = U256::from(100_000_000_000_000_000u64); // 10%
1045
1046        let res = state
1047            .get_amount_out(
1048                BigUint::from(100_000_000_000_000_000_000u128),
1049                &eth_token(),
1050                &reth_token(),
1051            )
1052            .unwrap();
1053
1054        let new_state = res
1055            .new_state
1056            .as_any()
1057            .downcast_ref::<RocketpoolState>()
1058            .unwrap();
1059        assert_eq!(new_state.reth_contract_liquidity, U256::from(20e18));
1060        assert_eq!(new_state.deposit_contract_balance, U256::from(34e18));
1061        assert_eq!(new_state.megapool_queue_requested_total, U256::ZERO);
1062    }
1063
1064    // ============ Assign Deposits — Constraint Unit Tests ============
1065
1066    /// Helper: create a state pre-configured for assignment tests.
1067    /// Deposits enabled, high max, collateral already met (no routing interference).
1068    fn create_assign_state(
1069        deposit_contract_balance: U256,
1070        megapool_queue_requested_total: U256,
1071        deposit_assign_maximum: U256,
1072        deposit_assign_socialised_maximum: U256,
1073    ) -> RocketpoolState {
1074        let mut state = create_state();
1075        state.deposit_assigning_enabled = true;
1076        state.deposit_contract_balance = deposit_contract_balance;
1077        state.megapool_queue_requested_total = megapool_queue_requested_total;
1078        state.deposit_assign_maximum = deposit_assign_maximum;
1079        state.deposit_assign_socialised_maximum = deposit_assign_socialised_maximum;
1080        // Put liquidity above target so routing sends everything to vault
1081        state.reth_contract_liquidity = U256::from(10_000_000_000_000_000_000u128);
1082        state
1083    }
1084
1085    #[rstest]
1086    // Count cap limits: deposit=64 ETH (2 entries), vault=300, queue=128 (4), max=90
1087    // count_cap = min(0 + 2, 90) = 2; vault_cap = 9; queue = 4 → 2 entries
1088    #[case::count_cap_limits(
1089        64_000_000_000_000_000_000u128,   // deposit
1090        300_000_000_000_000_000_000u128,  // vault
1091        128_000_000_000_000_000_000u128,  // queue
1092        90u64,                            // max
1093        0u64,                             // socialised
1094        64_000_000_000_000_000_000u128,   // expected: 2 * 32 ETH
1095    )]
1096    // Vault cap limits: deposit=200 ETH (6 entries), vault=64 (2), queue=192 (6), max=90
1097    // count_cap = 6; vault_cap = 2; queue = 6 → 2 entries
1098    #[case::vault_cap_limits(
1099        200_000_000_000_000_000_000u128,
1100        64_000_000_000_000_000_000u128,
1101        192_000_000_000_000_000_000u128,
1102        90u64,
1103        0u64,
1104        64_000_000_000_000_000_000u128,   // 2 * 32 ETH
1105    )]
1106    // Queue depth limits: deposit=200 ETH (6), vault=300 (9), queue=64 (2), max=90
1107    // count_cap = 6; vault_cap = 9; queue = 2 → 2 entries
1108    #[case::queue_depth_limits(
1109        200_000_000_000_000_000_000u128,
1110        300_000_000_000_000_000_000u128,
1111        64_000_000_000_000_000_000u128,
1112        90u64,
1113        0u64,
1114        64_000_000_000_000_000_000u128,   // 2 * 32 ETH
1115    )]
1116    // Max cap limits: deposit=200 ETH (6), vault=300 (9), queue=192 (6), max=3
1117    // count_cap = min(6, 3) = 3; vault_cap = 9; queue = 6 → 3 entries
1118    #[case::max_cap_limits(
1119        200_000_000_000_000_000_000u128,
1120        300_000_000_000_000_000_000u128,
1121        192_000_000_000_000_000_000u128,
1122        3u64,
1123        0u64,
1124        96_000_000_000_000_000_000u128,   // 3 * 32 ETH
1125    )]
1126    // Socialised max: deposit=10 ETH (<32, scaling=0), socialised=2, vault=200, queue=128, max=90
1127    // count_cap = min(0 + 2, 90) = 2; vault_cap = 6; queue = 4 → 2 entries
1128    #[case::socialised_max(
1129        10_000_000_000_000_000_000u128,
1130        200_000_000_000_000_000_000u128,
1131        128_000_000_000_000_000_000u128,
1132        90u64,
1133        2u64,
1134        64_000_000_000_000_000_000u128,   // 2 * 32 ETH
1135    )]
1136    // Small deposit, no socialised: deposit=10 ETH, socialised=0 → scaling=0, count_cap=0
1137    #[case::small_deposit_no_assignment(
1138        10_000_000_000_000_000_000u128,
1139        200_000_000_000_000_000_000u128,
1140        128_000_000_000_000_000_000u128,
1141        90u64,
1142        0u64,
1143        0u128
1144    )]
1145    // Vault below 32 ETH: deposit=100 ETH, vault=20, queue=128, max=90
1146    // vault_cap = 20/32 = 0 → 0 entries
1147    #[case::vault_below_32(
1148        100_000_000_000_000_000_000u128,
1149        20_000_000_000_000_000_000u128,
1150        128_000_000_000_000_000_000u128,
1151        90u64,
1152        0u64,
1153        0u128
1154    )]
1155    fn test_assign_constraint(
1156        #[case] deposit: u128,
1157        #[case] vault: u128,
1158        #[case] queue: u128,
1159        #[case] max: u64,
1160        #[case] socialised: u64,
1161        #[case] expected_assigned: u128,
1162    ) {
1163        let state = create_assign_state(
1164            U256::from(vault),
1165            U256::from(queue),
1166            U256::from(max),
1167            U256::from(socialised),
1168        );
1169        let assigned = state.calculate_assign_deposits(U256::from(deposit));
1170        assert_eq!(assigned, U256::from(expected_assigned));
1171    }
1172
1173    // ============ Live Post-Saturn Transaction Tests ============
1174
1175    /// State at block 24480104 (just before first post-Saturn deposit).
1176    /// Verified against on-chain data via cast calls at this block.
1177    fn create_state_at_block_24480104() -> RocketpoolState {
1178        RocketpoolState::new(
1179            U256::from_str_radix("489a96a246a2e92bbbd1", 16).unwrap(), // reth_supply
1180            U256::from_str_radix("540e645ee4119f4d8b9e", 16).unwrap(), // total_eth
1181            U256::from_str_radix("8dcfa9d0071987bb", 16).unwrap(),     // deposit_contract_balance
1182            U256::from_str_radix("c28d2e1d64f99ea24", 16).unwrap(),    // reth_contract_liquidity
1183            U256::from_str_radix("1c6bf52634000", 16).unwrap(),        // deposit_fee (0.05%)
1184            true,                                                      // deposits_enabled
1185            U256::from_str_radix("2386f26fc10000", 16).unwrap(),       // min_deposit_amount
1186            U256::from_str_radix("4f68ca6d8cd91c6000000", 16).unwrap(), // max_deposit_pool_size
1187            true,                                                      // deposit_assigning_enabled
1188            U256::from(90u64),                                         // deposit_assign_maximum
1189            U256::ZERO, // deposit_assign_socialised_maximum
1190            U256::from_str_radix("4a60532ad51bf000000", 16).unwrap(), /* megapool_queue_requested_total */
1191            U256::from(10_000_000_000_000_000u64), // target_reth_collateral_rate: 1%
1192        )
1193    }
1194
1195    /// Test against real v1.4 deposit transaction.
1196    /// Tx 0xe0f1db165b621cb1e50b629af9d47e064be464fbcc7f2bcba3df1d27dbb916be at block 24480105.
1197    /// User deposited 85 ETH and received 73382345660413064855 rETH (0.05% fee applied).
1198    ///
1199    /// On-chain, the 85 ETH went entirely to the rETH collateral buffer because
1200    /// the collateral shortfall (target ~3965 ETH vs ~224 ETH held) far exceeds 85 ETH.
1201    /// The vault balance (deposit_contract_balance) was unchanged at ~10.22 ETH,
1202    /// which is < 32 ETH so no queue entries were assigned.
1203    #[test]
1204    fn test_live_deposit_post_saturn() {
1205        let state = create_state_at_block_24480104();
1206
1207        let deposit_amount = BigUint::from(85_000_000_000_000_000_000u128);
1208        let res = state
1209            .get_amount_out(deposit_amount, &eth_token(), &reth_token())
1210            .unwrap();
1211
1212        // Output amount: exact match with on-chain result
1213        let expected_reth_out = BigUint::from(73_382_345_660_413_064_855u128);
1214        assert_eq!(res.amount, expected_reth_out);
1215
1216        // Post-state: now matches on-chain behavior.
1217        let new_state = res
1218            .new_state
1219            .as_any()
1220            .downcast_ref::<RocketpoolState>()
1221            .unwrap();
1222        assert_eq!(new_state.total_eth, state.total_eth);
1223        assert_eq!(new_state.reth_supply, state.reth_supply);
1224        // deposit_contract_balance unchanged — all 85 ETH went to rETH collateral buffer
1225        assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1226        // rETH contract liquidity increased by the full 85 ETH
1227        assert_eq!(
1228            new_state.reth_contract_liquidity,
1229            safe_add_u256(
1230                state.reth_contract_liquidity,
1231                U256::from(85_000_000_000_000_000_000u128)
1232            )
1233            .unwrap()
1234        );
1235        // Queue unchanged — vault had < 32 ETH, no assignment possible
1236        assert_eq!(new_state.megapool_queue_requested_total, state.megapool_queue_requested_total);
1237    }
1238
1239    /// Test against real v1.4 burn transaction.
1240    /// Tx 0x6e70e11475c158ca5f88a6d370dfef90eee6d696dd569c3eaab7d244c7b4a20f at block 24481338.
1241    /// User burned 2515686112138065226 rETH and received 2912504376202664754 ETH.
1242    ///
1243    /// Reuses `create_state_at_block_24480104` (~1200 blocks before the burn) because:
1244    ///   - `total_eth` and `reth_supply` are oracle-reported values (from rocketNetworkBalances)
1245    ///     updated only by oracle submissions (daily). Verified identical at both blocks:
1246    ///     getTotalETHBalance() = 396944271446898073504670, getTotalRETHSupply() =
1247    ///     342862039669683153255377.
1248    ///   - The burn amount (2.91 ETH) is sourced entirely from `reth_contract_liquidity` (224 ETH),
1249    ///     so `deposit_contract_balance` and `megapool_queue_requested_total` are not involved.
1250    #[test]
1251    fn test_live_burn_post_saturn() {
1252        let state = create_state_at_block_24480104();
1253
1254        let burn_amount = BigUint::from(2_515_686_112_138_065_226u128);
1255        let res = state
1256            .get_amount_out(burn_amount, &reth_token(), &eth_token())
1257            .unwrap();
1258
1259        // Output amount: exact match with on-chain getEthValue(burnAmount) at block 24481337
1260        let expected_eth_out = BigUint::from(2_912_504_376_202_664_754u128);
1261        assert_eq!(res.amount, expected_eth_out);
1262
1263        // Post-state: verify oracle values unchanged and liquidity decreased
1264        let new_state = res
1265            .new_state
1266            .as_any()
1267            .downcast_ref::<RocketpoolState>()
1268            .unwrap();
1269        assert_eq!(new_state.total_eth, state.total_eth);
1270        assert_eq!(new_state.reth_supply, state.reth_supply);
1271        // 2.91 ETH withdrawn entirely from reth_contract_liquidity (224 ETH available)
1272        assert_eq!(
1273            new_state.reth_contract_liquidity,
1274            safe_sub_u256(state.reth_contract_liquidity, U256::from(2_912_504_376_202_664_754u128))
1275                .unwrap()
1276        );
1277        assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
1278    }
1279}