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