use std::{any::Any, collections::HashMap};
use alloy::primitives::U256;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use tycho_common::{
dto::ProtocolStateDelta,
models::token::Token,
simulation::{
errors::{SimulationError, TransitionError},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
use tycho_ethereum::BytesCodec;
use crate::evm::protocol::{
rocketpool::ETH_ADDRESS,
safe_math::{safe_add_u256, safe_sub_u256},
u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
utils::solidity_math::mul_div,
};
const DEPOSIT_FEE_BASE: u128 = 1_000_000_000_000_000_000;
const FULL_DEPOSIT_VALUE: u128 = 32_000_000_000_000_000_000u128;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RocketpoolState {
pub reth_supply: U256,
pub total_eth: U256,
pub deposit_contract_balance: U256,
pub reth_contract_liquidity: U256,
pub deposit_fee: U256,
pub deposits_enabled: bool,
pub min_deposit_amount: U256,
pub max_deposit_pool_size: U256,
pub deposit_assigning_enabled: bool,
pub deposit_assign_maximum: U256,
pub deposit_assign_socialised_maximum: U256,
pub megapool_queue_requested_total: U256,
pub target_reth_collateral_rate: U256,
}
impl RocketpoolState {
#[allow(clippy::too_many_arguments)]
pub fn new(
reth_supply: U256,
total_eth: U256,
deposit_contract_balance: U256,
reth_contract_liquidity: U256,
deposit_fee: U256,
deposits_enabled: bool,
min_deposit_amount: U256,
max_deposit_pool_size: U256,
deposit_assigning_enabled: bool,
deposit_assign_maximum: U256,
deposit_assign_socialised_maximum: U256,
megapool_queue_requested_total: U256,
target_reth_collateral_rate: U256,
) -> Self {
Self {
reth_supply,
total_eth,
deposit_contract_balance,
reth_contract_liquidity,
deposit_fee,
deposits_enabled,
min_deposit_amount,
max_deposit_pool_size,
deposit_assigning_enabled,
deposit_assign_maximum,
deposit_assign_socialised_maximum,
megapool_queue_requested_total,
target_reth_collateral_rate,
}
}
fn get_reth_value(&self, eth_amount: U256) -> Result<U256, SimulationError> {
let fee = mul_div(eth_amount, self.deposit_fee, U256::from(DEPOSIT_FEE_BASE))?;
let net_eth = safe_sub_u256(eth_amount, fee)?;
mul_div(net_eth, self.reth_supply, self.total_eth)
}
fn get_eth_value(&self, reth_amount: U256) -> Result<U256, SimulationError> {
mul_div(reth_amount, self.total_eth, self.reth_supply)
}
fn is_depositing_eth(token_in: &Bytes) -> bool {
token_in.as_ref() == ETH_ADDRESS
}
fn assert_deposits_enabled(&self) -> Result<(), SimulationError> {
if !self.deposits_enabled {
Err(SimulationError::RecoverableError(
"Deposits are currently disabled in Rocketpool".to_string(),
))
} else {
Ok(())
}
}
fn get_max_deposit_capacity(&self) -> Result<U256, SimulationError> {
if self.deposit_assigning_enabled {
safe_add_u256(self.max_deposit_pool_size, self.megapool_queue_requested_total)
} else {
Ok(self.max_deposit_pool_size)
}
}
fn get_deposit_pool_excess_balance(&self) -> Result<U256, SimulationError> {
if self.megapool_queue_requested_total >= self.deposit_contract_balance {
Ok(U256::ZERO)
} else {
safe_sub_u256(self.deposit_contract_balance, self.megapool_queue_requested_total)
}
}
fn get_total_available_for_withdrawal(&self) -> Result<U256, SimulationError> {
let deposit_pool_excess = self.get_deposit_pool_excess_balance()?;
safe_add_u256(self.reth_contract_liquidity, deposit_pool_excess)
}
fn compute_deposit_routing(
&self,
deposit_amount: U256,
) -> Result<(U256, U256), SimulationError> {
let target_collateral = mul_div(
self.total_eth,
self.target_reth_collateral_rate,
U256::from(DEPOSIT_FEE_BASE),
)?;
let shortfall = target_collateral.saturating_sub(self.reth_contract_liquidity);
let to_reth = deposit_amount.min(shortfall);
let to_vault = deposit_amount - to_reth;
Ok((to_reth, to_vault))
}
fn calculate_assign_deposits(&self, deposit_amount: U256) -> U256 {
if !self.deposit_assigning_enabled ||
self.megapool_queue_requested_total
.is_zero()
{
return U256::ZERO;
}
let full_deposit_value = U256::from(FULL_DEPOSIT_VALUE);
let scaling_count = deposit_amount / full_deposit_value;
let count_cap = (self.deposit_assign_socialised_maximum + scaling_count)
.min(self.deposit_assign_maximum);
let vault_cap = self.deposit_contract_balance / full_deposit_value;
let queue_entries = self.megapool_queue_requested_total / full_deposit_value;
let entries = count_cap
.min(vault_cap)
.min(queue_entries);
entries * full_deposit_value
}
}
#[typetag::serde]
impl ProtocolSim for RocketpoolState {
fn fee(&self) -> f64 {
unimplemented!("Rocketpool has asymmetric fees; use spot_price or get_amount_out instead")
}
fn spot_price(&self, _base: &Token, quote: &Token) -> Result<f64, SimulationError> {
let is_depositing_eth = RocketpoolState::is_depositing_eth("e.address);
let amount = U256::from(1e18);
let base_per_quote = if is_depositing_eth {
self.assert_deposits_enabled()?;
self.get_reth_value(amount)?
} else {
self.get_eth_value(amount)?
};
let base_per_quote = u256_to_f64(base_per_quote)? / 1e18;
Ok(1.0 / base_per_quote)
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
_token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let amount_in = biguint_to_u256(&amount_in);
let is_depositing_eth = RocketpoolState::is_depositing_eth(&token_in.address);
let amount_out = if is_depositing_eth {
self.assert_deposits_enabled()?;
if amount_in < self.min_deposit_amount {
return Err(SimulationError::InvalidInput(
format!(
"Deposit amount {} is less than the minimum deposit of {}",
amount_in, self.min_deposit_amount
),
None,
));
}
let capacity_needed = safe_add_u256(self.deposit_contract_balance, amount_in)?;
let max_capacity = self.get_max_deposit_capacity()?;
if capacity_needed > max_capacity {
return Err(SimulationError::InvalidInput(
format!(
"Deposit would exceed maximum pool size (capacity needed: {}, max: {})",
capacity_needed, max_capacity
),
None,
));
}
self.get_reth_value(amount_in)?
} else {
let eth_out = self.get_eth_value(amount_in)?;
let total_available = self.get_total_available_for_withdrawal()?;
if eth_out > total_available {
return Err(SimulationError::RecoverableError(format!(
"Withdrawal {} exceeds available liquidity {}",
eth_out, total_available
)));
}
eth_out
};
let mut new_state = self.clone();
if is_depositing_eth {
let (to_reth, to_vault) = new_state.compute_deposit_routing(amount_in)?;
new_state.reth_contract_liquidity =
safe_add_u256(new_state.reth_contract_liquidity, to_reth)?;
new_state.deposit_contract_balance =
safe_add_u256(new_state.deposit_contract_balance, to_vault)?;
let eth_assigned = new_state.calculate_assign_deposits(amount_in);
if eth_assigned > U256::ZERO {
new_state.deposit_contract_balance =
safe_sub_u256(new_state.deposit_contract_balance, eth_assigned)?;
new_state.megapool_queue_requested_total =
safe_sub_u256(new_state.megapool_queue_requested_total, eth_assigned)?;
}
} else {
let needed_from_deposit_pool =
amount_out.saturating_sub(new_state.reth_contract_liquidity);
new_state.reth_contract_liquidity = new_state
.reth_contract_liquidity
.saturating_sub(amount_out);
new_state.deposit_contract_balance =
safe_sub_u256(new_state.deposit_contract_balance, needed_from_deposit_pool)?;
}
let gas_used = if is_depositing_eth { 209_000u32 } else { 134_000u32 };
Ok(GetAmountOutResult::new(
u256_to_biguint(amount_out),
BigUint::from(gas_used),
Box::new(new_state),
))
}
fn get_limits(
&self,
sell_token: Bytes,
_buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
let is_depositing_eth = Self::is_depositing_eth(&sell_token);
if is_depositing_eth {
let max_capacity = self.get_max_deposit_capacity()?;
let max_eth_sell = safe_sub_u256(max_capacity, self.deposit_contract_balance)?;
let max_reth_buy = self.get_reth_value(max_eth_sell)?;
Ok((u256_to_biguint(max_eth_sell), u256_to_biguint(max_reth_buy)))
} else {
let max_eth_buy = self.get_total_available_for_withdrawal()?;
let max_reth_sell = mul_div(max_eth_buy, self.reth_supply, self.total_eth)?;
Ok((u256_to_biguint(max_reth_sell), u256_to_biguint(max_eth_buy)))
}
}
fn delta_transition(
&mut self,
delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
self.total_eth = delta
.updated_attributes
.get("total_eth")
.map_or(self.total_eth, U256::from_bytes);
self.reth_supply = delta
.updated_attributes
.get("reth_supply")
.map_or(self.reth_supply, U256::from_bytes);
self.deposit_contract_balance = delta
.updated_attributes
.get("deposit_contract_balance")
.map_or(self.deposit_contract_balance, U256::from_bytes);
self.reth_contract_liquidity = delta
.updated_attributes
.get("reth_contract_liquidity")
.map_or(self.reth_contract_liquidity, U256::from_bytes);
self.deposits_enabled = delta
.updated_attributes
.get("deposits_enabled")
.map_or(self.deposits_enabled, |val| !U256::from_bytes(val).is_zero());
self.deposit_assigning_enabled = delta
.updated_attributes
.get("deposit_assigning_enabled")
.map_or(self.deposit_assigning_enabled, |val| !U256::from_bytes(val).is_zero());
self.deposit_fee = delta
.updated_attributes
.get("deposit_fee")
.map_or(self.deposit_fee, U256::from_bytes);
self.min_deposit_amount = delta
.updated_attributes
.get("min_deposit_amount")
.map_or(self.min_deposit_amount, U256::from_bytes);
self.max_deposit_pool_size = delta
.updated_attributes
.get("max_deposit_pool_size")
.map_or(self.max_deposit_pool_size, U256::from_bytes);
self.deposit_assign_maximum = delta
.updated_attributes
.get("deposit_assign_maximum")
.map_or(self.deposit_assign_maximum, U256::from_bytes);
self.deposit_assign_socialised_maximum = delta
.updated_attributes
.get("deposit_assign_socialised_maximum")
.map_or(self.deposit_assign_socialised_maximum, U256::from_bytes);
self.megapool_queue_requested_total = delta
.updated_attributes
.get("megapool_queue_requested_total")
.map_or(self.megapool_queue_requested_total, U256::from_bytes);
self.target_reth_collateral_rate = delta
.updated_attributes
.get("target_reth_collateral_rate")
.map_or(self.target_reth_collateral_rate, U256::from_bytes);
Ok(())
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
self == other_state
} else {
false
}
}
fn query_pool_swap(
&self,
params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
crate::evm::query_pool_swap::query_pool_swap(self, params)
}
}
#[cfg(test)]
mod tests {
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use approx::assert_ulps_eq;
use num_bigint::BigUint;
use rstest::rstest;
use tycho_common::{
dto::ProtocolStateDelta,
hex_bytes::Bytes,
models::{token::Token, Chain},
simulation::{
errors::SimulationError,
protocol_sim::{Balances, ProtocolSim},
},
};
use super::*;
fn create_state() -> RocketpoolState {
RocketpoolState::new(
U256::from(100e18), U256::from(200e18),
U256::from(50e18), U256::ZERO, U256::from(400_000_000_000_000_000u64), true, U256::ZERO, U256::from(1000e18), false, U256::ZERO, U256::ZERO, U256::ZERO, U256::from(10_000_000_000_000_000u64), )
}
fn eth_token() -> Token {
Token::new(&Bytes::from(ETH_ADDRESS), "ETH", 18, 0, &[Some(100_000)], Chain::Ethereum, 100)
}
fn reth_token() -> Token {
Token::new(
&Bytes::from_str("0xae78736Cd615f374D3085123A210448E74Fc6393").unwrap(),
"rETH",
18,
0,
&[Some(100_000)],
Chain::Ethereum,
100,
)
}
#[test]
fn test_max_capacity_assign_disabled() {
let state = create_state();
assert_eq!(
state
.get_max_deposit_capacity()
.unwrap(),
U256::from(1000e18)
);
}
#[test]
fn test_max_capacity_assign_enabled_empty_queue() {
let mut state = create_state();
state.deposit_assigning_enabled = true;
assert_eq!(
state
.get_max_deposit_capacity()
.unwrap(),
U256::from(1000e18)
);
}
#[test]
fn test_max_capacity_assign_enabled_with_queue() {
let mut state = create_state();
state.deposit_assigning_enabled = true;
state.megapool_queue_requested_total = U256::from(500e18);
assert_eq!(
state
.get_max_deposit_capacity()
.unwrap(),
U256::from(1500e18)
);
}
#[rstest]
#[case::all_to_reth(
U256::ZERO, // reth_contract_liquidity
U256::from(10_000_000_000_000_000u64), // target_reth_collateral_rate: 1%
1_000_000_000_000_000_000u128, // deposit: 1 ETH
1_000_000_000_000_000_000u128, // expected to_reth
0u128, // expected to_vault
)]
#[case::split(
U256::ZERO,
U256::from(10_000_000_000_000_000u64),
10_000_000_000_000_000_000u128, // deposit: 10 ETH
2_000_000_000_000_000_000u128, // to_reth: 2 ETH (shortfall)
8_000_000_000_000_000_000u128, // to_vault: 8 ETH
)]
#[case::all_to_vault(
U256::from(10_000_000_000_000_000_000u128), // liquidity: 10 ETH > 2 ETH target
U256::from(10_000_000_000_000_000u64),
5_000_000_000_000_000_000u128, // deposit: 5 ETH
0u128,
5_000_000_000_000_000_000u128,
)]
#[case::zero_collateral_rate(
U256::ZERO,
U256::ZERO, // target_reth_collateral_rate: 0%
5_000_000_000_000_000_000u128,
0u128,
5_000_000_000_000_000_000u128,
)]
fn test_deposit_routing(
#[case] reth_contract_liquidity: U256,
#[case] target_reth_collateral_rate: U256,
#[case] deposit: u128,
#[case] expected_to_reth: u128,
#[case] expected_to_vault: u128,
) {
let mut state = create_state();
state.reth_contract_liquidity = reth_contract_liquidity;
state.target_reth_collateral_rate = target_reth_collateral_rate;
let (to_reth, to_vault) = state
.compute_deposit_routing(U256::from(deposit))
.unwrap();
assert_eq!(to_reth, U256::from(expected_to_reth));
assert_eq!(to_vault, U256::from(expected_to_vault));
}
#[test]
fn test_delta_transition_basic() {
let mut state = create_state();
let attributes: HashMap<String, Bytes> = [
("total_eth", U256::from(300u64)),
("reth_supply", U256::from(150u64)),
("deposit_contract_balance", U256::from(100u64)),
("reth_contract_liquidity", U256::from(20u64)),
]
.into_iter()
.map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
.collect();
let delta = ProtocolStateDelta {
component_id: "Rocketpool".to_owned(),
updated_attributes: attributes,
deleted_attributes: HashSet::new(),
};
state
.delta_transition(delta, &HashMap::new(), &Balances::default())
.unwrap();
assert_eq!(state.total_eth, U256::from(300u64));
assert_eq!(state.reth_supply, U256::from(150u64));
assert_eq!(state.deposit_contract_balance, U256::from(100u64));
assert_eq!(state.reth_contract_liquidity, U256::from(20u64));
}
#[test]
fn test_delta_transition_megapool_fields() {
let mut state = create_state();
let attributes: HashMap<String, Bytes> = [
("megapool_queue_requested_total", U256::from(1000u64)),
("target_reth_collateral_rate", U256::from(20_000_000_000_000_000u64)),
]
.into_iter()
.map(|(k, v)| (k.to_string(), Bytes::from(v.to_be_bytes_vec())))
.collect();
let delta = ProtocolStateDelta {
component_id: "Rocketpool".to_owned(),
updated_attributes: attributes,
deleted_attributes: HashSet::new(),
};
state
.delta_transition(delta, &HashMap::new(), &Balances::default())
.unwrap();
assert_eq!(state.megapool_queue_requested_total, U256::from(1000u64));
assert_eq!(state.target_reth_collateral_rate, U256::from(20_000_000_000_000_000u64));
}
#[test]
fn test_spot_price_deposit() {
let state = create_state();
let price = state
.spot_price(ð_token(), &reth_token())
.unwrap();
assert_ulps_eq!(price, 0.5);
}
#[test]
fn test_spot_price_withdraw() {
let state = create_state();
let price = state
.spot_price(&reth_token(), ð_token())
.unwrap();
assert_ulps_eq!(price, 1.0 / 0.3);
}
#[test]
fn test_live_spot_price_withdrawal() {
let state = create_state_at_block_24480104();
let price = state
.spot_price(ð_token(), &reth_token())
.unwrap();
let on_chain_eth_value = 1_157_737_589_816_937_166f64;
let expected = 1e18 / on_chain_eth_value;
assert_ulps_eq!(price, expected, max_ulps = 10);
}
#[test]
fn test_live_spot_price_deposit() {
use crate::evm::protocol::utils::add_fee_markup;
let state = create_state_at_block_24480104();
let price = state
.spot_price(&reth_token(), ð_token())
.unwrap();
let on_chain_reth_value = 863_753_590_447_141_981f64;
let rate_without_fee = 1e18 / on_chain_reth_value;
let fee = 500_000_000_000_000f64 / DEPOSIT_FEE_BASE as f64; let expected = add_fee_markup(rate_without_fee, fee);
assert_ulps_eq!(price, expected, max_ulps = 10);
}
#[test]
fn test_fee_panics() {
let state = create_state();
let result = std::panic::catch_unwind(|| state.fee());
assert!(result.is_err());
}
#[test]
fn test_limits_deposit() {
let state = create_state();
let (max_sell, max_buy) = state
.get_limits(eth_token().address, reth_token().address)
.unwrap();
assert_eq!(max_sell, BigUint::from(950_000_000_000_000_000_000u128));
assert_eq!(max_buy, BigUint::from(285_000_000_000_000_000_000u128));
}
#[test]
fn test_limits_withdrawal() {
let state = create_state();
let (max_sell, max_buy) = state
.get_limits(reth_token().address, eth_token().address)
.unwrap();
assert_eq!(max_buy, BigUint::from(50_000_000_000_000_000_000u128));
assert_eq!(max_sell, BigUint::from(25_000_000_000_000_000_000u128));
}
#[test]
fn test_limits_with_megapool_queue() {
let mut state = create_state();
state.max_deposit_pool_size = U256::from(100e18);
state.deposit_assigning_enabled = true;
state.megapool_queue_requested_total = U256::from(62e18);
let (max_sell, _) = state
.get_limits(eth_token().address, reth_token().address)
.unwrap();
assert_eq!(max_sell, BigUint::from(112_000_000_000_000_000_000u128));
}
#[test]
fn test_limits_deposit_boundary_accepted() {
let mut state = create_state();
state.deposit_assigning_enabled = true;
state.megapool_queue_requested_total = U256::from(64e18);
let (max_sell, _) = state
.get_limits(eth_token().address, reth_token().address)
.unwrap();
let res = state.get_amount_out(max_sell.clone(), ð_token(), &reth_token());
assert!(res.is_ok(), "max_sell should be accepted");
let over = max_sell + BigUint::from(1u64);
let res = state.get_amount_out(over, ð_token(), &reth_token());
assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
}
#[test]
fn test_limits_withdrawal_boundary_accepted() {
let mut state = create_state();
state.reth_contract_liquidity = U256::from(30e18);
let (max_sell, _) = state
.get_limits(reth_token().address, eth_token().address)
.unwrap();
let res = state.get_amount_out(max_sell.clone(), &reth_token(), ð_token());
assert!(res.is_ok(), "max_sell should be accepted");
let over = max_sell + BigUint::from(1u64);
let res = state.get_amount_out(over, &reth_token(), ð_token());
assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
}
#[test]
fn test_deposit_eth() {
let state = create_state();
let res = state
.get_amount_out(
BigUint::from(10_000_000_000_000_000_000u128),
ð_token(),
&reth_token(),
)
.unwrap();
assert_eq!(res.amount, BigUint::from(3_000_000_000_000_000_000u128));
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
assert_eq!(new_state.reth_contract_liquidity, U256::from(2e18));
}
#[test]
fn test_withdraw_reth() {
let state = create_state();
let res = state
.get_amount_out(
BigUint::from(10_000_000_000_000_000_000u128),
&reth_token(),
ð_token(),
)
.unwrap();
assert_eq!(res.amount, BigUint::from(20_000_000_000_000_000_000u128));
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
}
#[test]
fn test_deposit_disabled() {
let mut state = create_state();
state.deposits_enabled = false;
let res = state.get_amount_out(BigUint::from(10u64), ð_token(), &reth_token());
assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
}
#[test]
fn test_deposit_below_minimum() {
let mut state = create_state();
state.min_deposit_amount = U256::from(100u64);
let res = state.get_amount_out(BigUint::from(50u64), ð_token(), &reth_token());
assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
}
#[test]
fn test_deposit_exceeds_max_pool() {
let mut state = create_state();
state.max_deposit_pool_size = U256::from(60e18);
let res = state.get_amount_out(
BigUint::from(20_000_000_000_000_000_000u128),
ð_token(),
&reth_token(),
);
assert!(matches!(res, Err(SimulationError::InvalidInput(_, _))));
}
#[test]
fn test_withdrawal_insufficient_liquidity() {
let state = create_state();
let res = state.get_amount_out(
BigUint::from(30_000_000_000_000_000_000u128),
&reth_token(),
ð_token(),
);
assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
}
#[test]
fn test_withdrawal_limited_by_queue() {
let mut state = create_state();
state.deposit_contract_balance = U256::from(100e18);
state.megapool_queue_requested_total = U256::from(62e18);
let res = state.get_amount_out(
BigUint::from(20_000_000_000_000_000_000u128),
&reth_token(),
ð_token(),
);
assert!(matches!(res, Err(SimulationError::RecoverableError(_))));
let res = state
.get_amount_out(
BigUint::from(15_000_000_000_000_000_000u128),
&reth_token(),
ð_token(),
)
.unwrap();
assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
}
#[test]
fn test_withdrawal_uses_both_pools() {
let mut state = create_state();
state.reth_contract_liquidity = U256::from(10e18);
state.deposit_contract_balance = U256::from(50e18);
let res = state
.get_amount_out(
BigUint::from(15_000_000_000_000_000_000u128),
&reth_token(),
ð_token(),
)
.unwrap();
assert_eq!(res.amount, BigUint::from(30_000_000_000_000_000_000u128));
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.reth_contract_liquidity, U256::ZERO);
assert_eq!(new_state.deposit_contract_balance, U256::from(30e18));
}
#[test]
fn test_assign_deposits_with_queue() {
let mut state = create_state();
state.deposit_contract_balance = U256::from(200e18);
state.reth_contract_liquidity = U256::from(10e18); state.deposit_assigning_enabled = true;
state.deposit_assign_maximum = U256::from(90u64);
state.megapool_queue_requested_total = U256::from(128e18);
let res = state
.get_amount_out(
BigUint::from(100_000_000_000_000_000_000u128),
ð_token(),
&reth_token(),
)
.unwrap();
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.deposit_contract_balance, U256::from(204e18));
assert_eq!(new_state.megapool_queue_requested_total, U256::from(32e18));
}
#[test]
fn test_assign_deposits_empty_queue() {
let mut state = create_state();
state.deposit_assigning_enabled = true;
let res = state
.get_amount_out(
BigUint::from(10_000_000_000_000_000_000u128),
ð_token(),
&reth_token(),
)
.unwrap();
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
}
#[test]
fn test_assign_deposits_disabled() {
let mut state = create_state();
state.deposit_assigning_enabled = false;
state.megapool_queue_requested_total = U256::from(100e18);
let res = state
.get_amount_out(
BigUint::from(10_000_000_000_000_000_000u128),
ð_token(),
&reth_token(),
)
.unwrap();
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.deposit_contract_balance, U256::from(58e18));
}
#[test]
fn test_deposit_split_routing_with_assignment() {
let mut state = create_state();
state.reth_contract_liquidity = U256::ZERO;
state.deposit_contract_balance = U256::from(50e18);
state.deposit_assigning_enabled = true;
state.deposit_assign_maximum = U256::from(90u64);
state.megapool_queue_requested_total = U256::from(96e18); state.target_reth_collateral_rate = U256::from(100_000_000_000_000_000u64);
let res = state
.get_amount_out(
BigUint::from(100_000_000_000_000_000_000u128),
ð_token(),
&reth_token(),
)
.unwrap();
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.reth_contract_liquidity, U256::from(20e18));
assert_eq!(new_state.deposit_contract_balance, U256::from(34e18));
assert_eq!(new_state.megapool_queue_requested_total, U256::ZERO);
}
fn create_assign_state(
deposit_contract_balance: U256,
megapool_queue_requested_total: U256,
deposit_assign_maximum: U256,
deposit_assign_socialised_maximum: U256,
) -> RocketpoolState {
let mut state = create_state();
state.deposit_assigning_enabled = true;
state.deposit_contract_balance = deposit_contract_balance;
state.megapool_queue_requested_total = megapool_queue_requested_total;
state.deposit_assign_maximum = deposit_assign_maximum;
state.deposit_assign_socialised_maximum = deposit_assign_socialised_maximum;
state.reth_contract_liquidity = U256::from(10_000_000_000_000_000_000u128);
state
}
#[rstest]
#[case::count_cap_limits(
64_000_000_000_000_000_000u128, // deposit
300_000_000_000_000_000_000u128, // vault
128_000_000_000_000_000_000u128, // queue
90u64, // max
0u64, // socialised
64_000_000_000_000_000_000u128, // expected: 2 * 32 ETH
)]
#[case::vault_cap_limits(
200_000_000_000_000_000_000u128,
64_000_000_000_000_000_000u128,
192_000_000_000_000_000_000u128,
90u64,
0u64,
64_000_000_000_000_000_000u128, // 2 * 32 ETH
)]
#[case::queue_depth_limits(
200_000_000_000_000_000_000u128,
300_000_000_000_000_000_000u128,
64_000_000_000_000_000_000u128,
90u64,
0u64,
64_000_000_000_000_000_000u128, // 2 * 32 ETH
)]
#[case::max_cap_limits(
200_000_000_000_000_000_000u128,
300_000_000_000_000_000_000u128,
192_000_000_000_000_000_000u128,
3u64,
0u64,
96_000_000_000_000_000_000u128, // 3 * 32 ETH
)]
#[case::socialised_max(
10_000_000_000_000_000_000u128,
200_000_000_000_000_000_000u128,
128_000_000_000_000_000_000u128,
90u64,
2u64,
64_000_000_000_000_000_000u128, // 2 * 32 ETH
)]
#[case::small_deposit_no_assignment(
10_000_000_000_000_000_000u128,
200_000_000_000_000_000_000u128,
128_000_000_000_000_000_000u128,
90u64,
0u64,
0u128
)]
#[case::vault_below_32(
100_000_000_000_000_000_000u128,
20_000_000_000_000_000_000u128,
128_000_000_000_000_000_000u128,
90u64,
0u64,
0u128
)]
fn test_assign_constraint(
#[case] deposit: u128,
#[case] vault: u128,
#[case] queue: u128,
#[case] max: u64,
#[case] socialised: u64,
#[case] expected_assigned: u128,
) {
let state = create_assign_state(
U256::from(vault),
U256::from(queue),
U256::from(max),
U256::from(socialised),
);
let assigned = state.calculate_assign_deposits(U256::from(deposit));
assert_eq!(assigned, U256::from(expected_assigned));
}
fn create_state_at_block_24480104() -> RocketpoolState {
RocketpoolState::new(
U256::from_str_radix("489a96a246a2e92bbbd1", 16).unwrap(), U256::from_str_radix("540e645ee4119f4d8b9e", 16).unwrap(), U256::from_str_radix("8dcfa9d0071987bb", 16).unwrap(), U256::from_str_radix("c28d2e1d64f99ea24", 16).unwrap(), U256::from_str_radix("1c6bf52634000", 16).unwrap(), true, U256::from_str_radix("2386f26fc10000", 16).unwrap(), U256::from_str_radix("4f68ca6d8cd91c6000000", 16).unwrap(), true, U256::from(90u64), U256::ZERO, U256::from_str_radix("4a60532ad51bf000000", 16).unwrap(),
U256::from(10_000_000_000_000_000u64), )
}
#[test]
fn test_live_deposit_post_saturn() {
let state = create_state_at_block_24480104();
let deposit_amount = BigUint::from(85_000_000_000_000_000_000u128);
let res = state
.get_amount_out(deposit_amount, ð_token(), &reth_token())
.unwrap();
let expected_reth_out = BigUint::from(73_382_345_660_413_064_855u128);
assert_eq!(res.amount, expected_reth_out);
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.total_eth, state.total_eth);
assert_eq!(new_state.reth_supply, state.reth_supply);
assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
assert_eq!(
new_state.reth_contract_liquidity,
safe_add_u256(
state.reth_contract_liquidity,
U256::from(85_000_000_000_000_000_000u128)
)
.unwrap()
);
assert_eq!(new_state.megapool_queue_requested_total, state.megapool_queue_requested_total);
}
#[test]
fn test_live_burn_post_saturn() {
let state = create_state_at_block_24480104();
let burn_amount = BigUint::from(2_515_686_112_138_065_226u128);
let res = state
.get_amount_out(burn_amount, &reth_token(), ð_token())
.unwrap();
let expected_eth_out = BigUint::from(2_912_504_376_202_664_754u128);
assert_eq!(res.amount, expected_eth_out);
let new_state = res
.new_state
.as_any()
.downcast_ref::<RocketpoolState>()
.unwrap();
assert_eq!(new_state.total_eth, state.total_eth);
assert_eq!(new_state.reth_supply, state.reth_supply);
assert_eq!(
new_state.reth_contract_liquidity,
safe_sub_u256(state.reth_contract_liquidity, U256::from(2_912_504_376_202_664_754u128))
.unwrap()
);
assert_eq!(new_state.deposit_contract_balance, state.deposit_contract_balance);
}
}