1use std::{any::Any, collections::HashMap};
2
3use num_bigint::BigUint;
4use num_traits::Zero;
5use tycho_common::{
6 dto::ProtocolStateDelta,
7 models::token::Token,
8 simulation::{
9 errors::{SimulationError, TransitionError},
10 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
11 },
12 Bytes,
13};
14
15use crate::evm::protocol::{
16 safe_math::{safe_div_u256, safe_mul_u256},
17 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
18};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum LidoPoolType {
22 StEth,
23 WStEth,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum StakingStatus {
28 Limited = 0,
29 Paused = 1,
30 Unlimited = 2,
31}
32
33impl StakingStatus {
34 pub fn as_str_name(&self) -> &'static str {
35 match self {
36 StakingStatus::Limited => "Limited",
37 StakingStatus::Paused => "Paused",
38 StakingStatus::Unlimited => "Unlimited",
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct StakeLimitState {
46 pub staking_status: StakingStatus,
47 pub staking_limit: BigUint,
48}
49
50impl StakeLimitState {
51 fn get_limit(&self) -> BigUint {
52 match self.staking_status {
54 StakingStatus::Limited => self.staking_limit.clone(),
55 StakingStatus::Paused => BigUint::zero(),
56 StakingStatus::Unlimited => BigUint::from(u128::MAX),
57 }
58 }
59}
60
61pub const DEFAULT_GAS: u64 = 60000;
62
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct LidoState {
65 pub pool_type: LidoPoolType,
66 pub total_shares: BigUint,
67 pub total_pooled_eth: BigUint,
68 pub total_wrapped_st_eth: Option<BigUint>,
69 pub id: Bytes,
70 pub native_address: Bytes,
71 pub stake_limits_state: StakeLimitState,
72 pub tokens: [Bytes; 2],
73 pub token_to_track_total_pooled_eth: Bytes,
74}
75
76impl LidoState {
77 fn steth_swap(&self, amount_in: BigUint) -> Result<GetAmountOutResult, SimulationError> {
78 let shares = safe_div_u256(
79 safe_mul_u256(biguint_to_u256(&amount_in), biguint_to_u256(&self.total_shares))?,
80 biguint_to_u256(&self.total_pooled_eth),
81 )?;
82
83 let amount_out = safe_div_u256(
84 safe_mul_u256(shares, biguint_to_u256(&self.total_pooled_eth))?,
85 biguint_to_u256(&self.total_shares),
86 )?;
87
88 Ok(GetAmountOutResult {
89 amount: u256_to_biguint(amount_out),
90 gas: BigUint::from(DEFAULT_GAS),
91 new_state: Box::new(Self {
92 pool_type: self.pool_type.clone(),
93 total_shares: self.total_shares.clone() + u256_to_biguint(shares),
94 total_pooled_eth: self.total_pooled_eth.clone() + amount_in,
95 total_wrapped_st_eth: None,
96 id: self.id.clone(),
97 native_address: self.native_address.clone(),
98 stake_limits_state: self.stake_limits_state.clone(),
99 tokens: self.tokens.clone(),
100 token_to_track_total_pooled_eth: self
101 .token_to_track_total_pooled_eth
102 .clone(),
103 }),
104 })
105 }
106
107 fn wrap_steth(&self, amount_in: BigUint) -> Result<GetAmountOutResult, SimulationError> {
108 if amount_in.is_zero() {
109 return Err(SimulationError::InvalidInput("Cannot wrap 0 stETH ".to_string(), None))
110 }
111
112 let amount_out = u256_to_biguint(safe_div_u256(
113 safe_mul_u256(biguint_to_u256(&amount_in), biguint_to_u256(&self.total_shares))?,
114 biguint_to_u256(&self.total_pooled_eth),
115 )?);
116
117 let new_total_wrapped_st_eth = self
118 .total_wrapped_st_eth
119 .as_ref()
120 .expect("total_wrapped_st_eth must be present for wrapped staked ETH pool") +
121 &amount_out;
122
123 Ok(GetAmountOutResult {
124 amount: amount_out.clone(),
125 gas: BigUint::from(DEFAULT_GAS),
126 new_state: Box::new(Self {
127 pool_type: self.pool_type.clone(),
128 total_shares: self.total_shares.clone(),
129 total_pooled_eth: self.total_pooled_eth.clone(),
130 total_wrapped_st_eth: Some(new_total_wrapped_st_eth),
131 id: self.id.clone(),
132 native_address: self.native_address.clone(),
133 stake_limits_state: self.stake_limits_state.clone(),
134 tokens: self.tokens.clone(),
135 token_to_track_total_pooled_eth: self
136 .token_to_track_total_pooled_eth
137 .clone(),
138 }),
139 })
140 }
141
142 fn unwrap_steth(&self, amount_in: BigUint) -> Result<GetAmountOutResult, SimulationError> {
143 if amount_in.is_zero() {
144 return Err(SimulationError::InvalidInput("Cannot unwrap 0 wstETH ".to_string(), None))
145 }
146
147 let amount_out = u256_to_biguint(safe_div_u256(
148 safe_mul_u256(biguint_to_u256(&amount_in), biguint_to_u256(&self.total_pooled_eth))?,
149 biguint_to_u256(&self.total_shares),
150 )?);
151
152 let new_total_wrapped_st_eth = self
153 .total_wrapped_st_eth
154 .as_ref()
155 .expect("total_wrapped_st_eth must be present for wrapped staked ETH pool") -
156 &amount_in;
157
158 Ok(GetAmountOutResult {
159 amount: amount_out.clone(),
160 gas: BigUint::from(DEFAULT_GAS),
161 new_state: Box::new(Self {
162 pool_type: self.pool_type.clone(),
163 total_shares: self.total_shares.clone(),
164 total_pooled_eth: self.total_pooled_eth.clone(),
165 total_wrapped_st_eth: Some(new_total_wrapped_st_eth),
166 id: self.id.clone(),
167 native_address: self.native_address.clone(),
168 stake_limits_state: self.stake_limits_state.clone(),
169 tokens: self.tokens.clone(),
170 token_to_track_total_pooled_eth: self
171 .token_to_track_total_pooled_eth
172 .clone(),
173 }),
174 })
175 }
176
177 fn zero2one(&self, sell_token: &Bytes, buy_token: &Bytes) -> Result<bool, SimulationError> {
178 let second_token = self
179 .tokens
180 .iter()
181 .find(|t| **t != self.token_to_track_total_pooled_eth)
182 .expect("No second token found");
183
184 if buy_token == second_token && *sell_token == self.token_to_track_total_pooled_eth {
185 Ok(true)
186 } else if *buy_token == self.token_to_track_total_pooled_eth && sell_token == second_token {
187 Ok(false)
188 } else {
189 Err(SimulationError::InvalidInput(
190 format!(
191 "Invalid combination of tokens for type {:?}: {:?}, {:?}",
192 self.pool_type, buy_token, sell_token
193 ),
194 None,
195 ))
196 }
197 }
198
199 fn st_eth_limits(
200 &self,
201 sell_token: Bytes,
202 buy_token: Bytes,
203 ) -> Result<(BigUint, BigUint), SimulationError> {
204 if self.zero2one(&sell_token, &buy_token)? {
205 let limit = self.stake_limits_state.get_limit();
206
207 let shares = safe_div_u256(
208 safe_mul_u256(biguint_to_u256(&limit), biguint_to_u256(&self.total_shares))?,
209 biguint_to_u256(&self.total_pooled_eth),
210 )?;
211
212 let amount_out = safe_div_u256(
213 safe_mul_u256(shares, biguint_to_u256(&self.total_pooled_eth))?,
214 biguint_to_u256(&self.total_shares),
215 )?;
216
217 Ok((limit.clone(), u256_to_biguint(amount_out)))
218 } else {
219 Ok((BigUint::zero(), BigUint::zero()))
220 }
221 }
222
223 fn wst_eth_limits(
224 &self,
225 sell_token: Bytes,
226 buy_token: Bytes,
227 ) -> Result<(BigUint, BigUint), SimulationError> {
228 if !self.zero2one(&sell_token, &buy_token)? {
229 let total_wrapped_eth = self
232 .total_wrapped_st_eth
233 .clone()
234 .expect("total_wrapped_st_eth must be present for wrapped staked ETH pool");
235
236 let amount_out = u256_to_biguint(safe_div_u256(
237 safe_mul_u256(
238 biguint_to_u256(&total_wrapped_eth),
239 biguint_to_u256(&self.total_pooled_eth),
240 )?,
241 biguint_to_u256(&self.total_shares),
242 )?);
243 Ok((
244 self.total_wrapped_st_eth
245 .clone()
246 .expect("total_wrapped_st_eth must be present for wrapped staked ETH pool"),
247 amount_out,
248 ))
249 } else {
250 let limit_for_wrapping = &self.total_shares -
253 self.total_wrapped_st_eth
254 .as_ref()
255 .expect("total_wrapped_st_eth must be present for wrapped staked ETH pool");
256
257 let amount_in = u256_to_biguint(safe_div_u256(
258 safe_mul_u256(
259 biguint_to_u256(&limit_for_wrapping),
260 biguint_to_u256(&self.total_shares),
261 )?,
262 biguint_to_u256(&self.total_pooled_eth),
263 )?);
264
265 Ok((amount_in, limit_for_wrapping))
266 }
267 }
268
269 fn st_eth_delta_transition(
270 &mut self,
271 delta: ProtocolStateDelta,
272 ) -> Result<(), TransitionError<String>> {
273 self.total_shares = BigUint::from_bytes_be(
274 delta
275 .updated_attributes
276 .get("total_shares")
277 .ok_or(TransitionError::MissingAttribute(
278 "total_shares field is missing".to_owned(),
279 ))?,
280 );
281
282 let staking_status = delta
283 .updated_attributes
284 .get("staking_status")
285 .ok_or(TransitionError::MissingAttribute(
286 "Staking_status field is missing".to_owned(),
287 ))?;
288
289 let staking_status_parsed = if let Ok(status_as_str) = std::str::from_utf8(staking_status) {
290 match status_as_str {
291 "Limited" => StakingStatus::Limited,
292 "Paused" => StakingStatus::Paused,
293 "Unlimited" => StakingStatus::Unlimited,
294 _ => {
295 return Err(TransitionError::DecodeError(
296 "status_as_str parsed to invalid status".to_owned(),
297 ))
298 }
299 }
300 } else {
301 return Err(TransitionError::DecodeError("status_as_str cannot be parsed".to_owned()))
302 };
303
304 let staking_limit = delta
305 .updated_attributes
306 .get("staking_limit")
307 .ok_or(TransitionError::MissingAttribute(
308 "Staking_limit field is missing".to_owned(),
309 ))?;
310
311 self.stake_limits_state = StakeLimitState {
312 staking_status: staking_status_parsed,
313 staking_limit: BigUint::from_bytes_be(staking_limit),
314 };
315 Ok(())
316 }
317
318 fn st_eth_balance_transition(&mut self, balances: &HashMap<Bytes, Bytes>) {
319 for (token, balance) in balances.iter() {
320 if token == &self.token_to_track_total_pooled_eth {
321 self.total_pooled_eth = BigUint::from_bytes_be(balance)
322 }
323 }
324 }
325
326 fn wst_eth_delta_transition(
327 &mut self,
328 delta: ProtocolStateDelta,
329 ) -> Result<(), TransitionError<String>> {
330 self.total_shares = BigUint::from_bytes_be(
331 delta
332 .updated_attributes
333 .get("total_shares")
334 .ok_or(TransitionError::MissingAttribute(
335 "total_shares field is missing".to_owned(),
336 ))?,
337 );
338 self.total_wrapped_st_eth = Some(BigUint::from_bytes_be(
339 delta
340 .updated_attributes
341 .get("total_wstETH")
342 .ok_or(TransitionError::MissingAttribute(
343 "total_wstETH field is missing".to_owned(),
344 ))?,
345 ));
346
347 Ok(())
348 }
349
350 fn wst_eth_balance_transition(&mut self, balances: &HashMap<Bytes, Bytes>) {
351 for (token, balance) in balances.iter() {
352 if token == &self.token_to_track_total_pooled_eth {
353 self.total_pooled_eth = BigUint::from_bytes_be(balance)
354 }
355 }
356 }
357}
358
359impl ProtocolSim for LidoState {
360 fn fee(&self) -> f64 {
361 0.0
363 }
364
365 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
366 match self.pool_type {
367 LidoPoolType::StEth => {
368 if self.zero2one(&base.address, "e.address)? {
369 let total_shares_f64 = u256_to_f64(biguint_to_u256(&self.total_shares))?;
370 let total_pooled_eth_f64 =
371 u256_to_f64(biguint_to_u256(&self.total_pooled_eth))?;
372
373 Ok(total_pooled_eth_f64 / total_shares_f64 * total_shares_f64 /
374 total_pooled_eth_f64)
375 } else {
376 Err(SimulationError::InvalidInput(
377 format!(
378 "Spot_price: Invalid combination of tokens for type {:?}: {:?}, {:?}",
379 self.pool_type, base, quote
380 ),
381 None,
382 ))
383 }
384 }
385 LidoPoolType::WStEth => {
386 if self.zero2one(&base.address, "e.address)? {
387 let total_shares_f64 = u256_to_f64(biguint_to_u256(&self.total_shares))?;
388 let total_pooled_eth_f64 =
389 u256_to_f64(biguint_to_u256(&self.total_pooled_eth))?;
390
391 Ok(total_shares_f64 / total_pooled_eth_f64)
392 } else {
393 let total_shares_f64 = u256_to_f64(biguint_to_u256(&self.total_shares))?;
394 let total_pooled_eth_f64 =
395 u256_to_f64(biguint_to_u256(&self.total_pooled_eth))?;
396
397 Ok(total_pooled_eth_f64 / total_shares_f64)
398 }
399 }
400 }
401 }
402
403 fn get_amount_out(
404 &self,
405 amount_in: BigUint,
406 token_in: &Token,
407 token_out: &Token,
408 ) -> Result<GetAmountOutResult, SimulationError> {
409 match self.pool_type {
413 LidoPoolType::StEth => {
414 if self.zero2one(&token_in.address, &token_out.address)? {
415 Ok(self.steth_swap(amount_in)?)
416 } else {
417 Err(SimulationError::InvalidInput(
418 format!(
419 "Invalid combination of tokens for type {:?}: {:?}, {:?}",
420 self.pool_type, token_in, token_out
421 ),
422 None,
423 ))
424 }
425 }
426 LidoPoolType::WStEth => {
427 if self.zero2one(&token_in.address, &token_out.address)? {
428 self.wrap_steth(amount_in)
429 } else {
430 self.unwrap_steth(amount_in)
431 }
432 }
433 }
434 }
435
436 fn get_limits(
437 &self,
438 sell_token: Bytes,
439 buy_token: Bytes,
440 ) -> Result<(BigUint, BigUint), SimulationError> {
441 match self.pool_type {
446 LidoPoolType::StEth => self.st_eth_limits(sell_token, buy_token),
447 LidoPoolType::WStEth => self.wst_eth_limits(sell_token, buy_token),
448 }
449 }
450
451 fn delta_transition(
452 &mut self,
453 delta: ProtocolStateDelta,
454 _tokens: &HashMap<Bytes, Token>,
455 balances: &Balances,
456 ) -> Result<(), TransitionError<String>> {
457 for (component_id, balances) in balances.component_balances.iter() {
458 if Bytes::from(component_id.as_str()) == self.id {
459 match self.pool_type {
460 LidoPoolType::StEth => self.st_eth_balance_transition(balances),
461 LidoPoolType::WStEth => self.wst_eth_balance_transition(balances),
462 }
463 } else {
464 return Err(TransitionError::DecodeError(format!(
465 "Invalid component id in balances: {:?}",
466 component_id,
467 )))
468 }
469 }
470
471 if Bytes::from(delta.component_id.as_str()) == self.id {
472 match self.pool_type {
473 LidoPoolType::StEth => self.st_eth_delta_transition(delta),
474 LidoPoolType::WStEth => self.wst_eth_delta_transition(delta),
475 }
476 } else {
477 Err(TransitionError::DecodeError(format!(
478 "Invalid component id in delta: {:?}",
479 delta.component_id,
480 )))
481 }
482 }
483
484 fn clone_box(&self) -> Box<dyn ProtocolSim> {
485 Box::new(self.clone())
486 }
487
488 fn as_any(&self) -> &dyn Any {
489 self
490 }
491
492 fn as_any_mut(&mut self) -> &mut dyn Any {
493 self
494 }
495
496 fn eq(&self, other: &dyn ProtocolSim) -> bool {
497 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
498 self == other_state
499 } else {
500 false
501 }
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use std::{collections::HashSet, str::FromStr};
508
509 use num_bigint::BigUint;
510 use rstest::rstest;
511 use tycho_common::{
512 hex_bytes::Bytes,
513 models::{token::Token, Chain},
514 };
515
516 use super::*;
517
518 const ST_ETH_ADDRESS_PROXY: &str = "0xae7ab96520de3a18e5e111b5eaab095312d7fe84";
519 const WST_ETH_ADDRESS: &str = "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0";
520 const ETH_ADDRESS: &str = "0x0000000000000000000000000000000000000000";
521
522 fn from_hex_str_to_biguint(input: &str) -> BigUint {
523 let bytes = hex::decode(input).unwrap();
524 BigUint::from_bytes_be(&bytes)
525 }
526
527 fn lido_state_steth() -> LidoState {
528 let total_shares_start = from_hex_str_to_biguint(
529 "00000000000000000000000000000000000000000005dc41ec2e3ba19cf3ea6d",
530 );
531 let total_pooled_eth_start = from_hex_str_to_biguint("072409d75ebf50c5534125");
532 let staking_limit = from_hex_str_to_biguint("1fc3842bd1f071c00000");
533
534 LidoState {
535 pool_type: LidoPoolType::StEth,
536 total_shares: total_shares_start.clone(),
537 total_pooled_eth: total_pooled_eth_start.clone(),
538 total_wrapped_st_eth: None,
539 id: ST_ETH_ADDRESS_PROXY.into(),
540 native_address: ETH_ADDRESS.into(),
541 stake_limits_state: StakeLimitState {
542 staking_status: crate::evm::protocol::lido::state::StakingStatus::Limited,
543 staking_limit,
544 },
545 tokens: [
546 Bytes::from("0x0000000000000000000000000000000000000000"),
547 Bytes::from("0xae7ab96520de3a18e5e111b5eaab095312d7fe84"),
548 ],
549 token_to_track_total_pooled_eth: Bytes::from(ETH_ADDRESS),
550 }
551 }
552
553 fn lido_state_wsteth() -> LidoState {
554 let total_shares_start = from_hex_str_to_biguint(
555 "00000000000000000000000000000000000000000005dc41ec2e3ba19cf3ea6d",
556 );
557 let total_pooled_eth_start = from_hex_str_to_biguint("072409d75ebf50c5534125");
558 let total_wsteth_start = from_hex_str_to_biguint(
559 "00000000000000000000000000000000000000000002be110f2a220611513da6",
560 );
561
562 LidoState {
563 pool_type: LidoPoolType::WStEth,
564 total_shares: total_shares_start,
565 total_pooled_eth: total_pooled_eth_start.clone(),
566 total_wrapped_st_eth: Some(total_wsteth_start),
567 id: WST_ETH_ADDRESS.into(),
568 native_address: ETH_ADDRESS.into(),
569 stake_limits_state: StakeLimitState {
570 staking_status: crate::evm::protocol::lido::state::StakingStatus::Limited,
571 staking_limit: BigUint::zero(),
572 },
573 tokens: [
574 Bytes::from("0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0"),
575 Bytes::from("0xae7ab96520de3a18e5e111b5eaab095312d7fe84"),
576 ],
577 token_to_track_total_pooled_eth: Bytes::from(ST_ETH_ADDRESS_PROXY),
578 }
579 }
580
581 fn bytes_st_eth() -> Bytes {
582 Bytes::from(ST_ETH_ADDRESS_PROXY)
583 }
584
585 fn bytes_wst_eth() -> Bytes {
586 Bytes::from(WST_ETH_ADDRESS)
587 }
588
589 fn bytes_eth() -> Bytes {
590 Bytes::from(ETH_ADDRESS)
591 }
592
593 fn token_st_eth() -> Token {
594 Token::new(
595 &Bytes::from_str(ST_ETH_ADDRESS_PROXY).unwrap(),
596 "stETH",
597 18,
598 0,
599 &[Some(44000)],
600 Chain::Ethereum,
601 10,
602 )
603 }
604
605 fn token_wst_eth() -> Token {
606 Token::new(
607 &Bytes::from_str(WST_ETH_ADDRESS).unwrap(),
608 "wstETH",
609 18,
610 0,
611 &[Some(44000)],
612 Chain::Ethereum,
613 10,
614 )
615 }
616
617 fn token_eth() -> Token {
618 Token::new(
619 &Bytes::from_str(ETH_ADDRESS).unwrap(),
620 "ETH",
621 18,
622 0,
623 &[Some(44000)],
624 Chain::Ethereum,
625 100,
626 )
627 }
628
629 #[test]
630 fn test_lido_get_amount_out() {
631 let token_eth = token_eth();
637 let token_st_eth = token_st_eth();
638 let state = lido_state_steth();
639
640 let amount_in = BigUint::from_str("9001102957532401").unwrap();
641 let res = state
642 .get_amount_out(amount_in.clone(), &token_eth, &token_st_eth)
643 .unwrap();
644
645 let exp = BigUint::from_str("9001102957532400").unwrap(); assert_eq!(res.amount, exp);
647
648 let total_shares_after = from_hex_str_to_biguint(
649 "00000000000000000000000000000000000000000005dc41ec487a31b7865d5e",
650 );
651
652 let total_pooled_eth_after = from_hex_str_to_biguint("072409d77eb9c55db21616");
653
654 let new_state = res
655 .new_state
656 .as_any()
657 .downcast_ref::<LidoState>()
658 .unwrap();
659 assert_eq!(new_state.total_shares, total_shares_after);
660 assert_eq!(new_state.total_pooled_eth, total_pooled_eth_after);
661 }
662
663 #[test]
664 fn test_lido_wrapping_get_amount_out() {
665 let token_st_eth = token_st_eth();
672 let token_wst_eth = token_wst_eth();
673 let mut state = lido_state_wsteth();
674
675 let total_wsteth_start = from_hex_str_to_biguint(
676 "00000000000000000000000000000000000000000002be10e0f61dc56f6f85dc",
677 );
678 state.total_wrapped_st_eth = Some(total_wsteth_start);
679
680 let total_pooled_eth = from_hex_str_to_biguint("072409d88cbb5e48a01616");
681 state.total_pooled_eth = total_pooled_eth;
682
683 let total_shares = from_hex_str_to_biguint(
684 "00000000000000000000000000000000000000000005dc41ed2611c5fd46f034",
685 );
686 state.total_shares = total_shares;
687
688 let amount_in = BigUint::from_str("20711588703656141053").unwrap();
689 let res = state
690 .get_amount_out(amount_in.clone(), &token_st_eth, &token_wst_eth)
691 .unwrap();
692 let exp = BigUint::from_str("16997846311821787517").unwrap();
693 assert_eq!(res.amount, exp);
694
695 let total_wsteth_after = from_hex_str_to_biguint(
696 "00000000000000000000000000000000000000000002be11ccda98eef241c759",
697 );
698 let new_state = res
699 .new_state
700 .as_any()
701 .downcast_ref::<LidoState>()
702 .unwrap();
703 assert_eq!(new_state.total_wrapped_st_eth, Some(total_wsteth_after));
704
705 assert!(state
706 .get_amount_out(BigUint::zero(), &token_st_eth, &token_wst_eth)
707 .is_err());
708 }
709
710 #[test]
711
712 fn test_lido_unwrapping_get_amount_out() {
713 let token_st_eth = token_st_eth();
720 let token_wst_eth = token_wst_eth();
721 let mut state = lido_state_wsteth();
722
723 let total_wsteth_start = from_hex_str_to_biguint(
724 "00000000000000000000000000000000000000000002be110f2a220611513da6",
725 );
726 state.total_wrapped_st_eth = Some(total_wsteth_start);
727
728 let amount_in = BigUint::from_str("3329290700173981642").unwrap();
729 let res = state
730 .get_amount_out(amount_in.clone(), &token_wst_eth, &token_st_eth)
731 .unwrap();
732 let exp = BigUint::from_str("4056684499432944068").unwrap();
733 assert_eq!(res.amount, exp);
734
735 let total_wsteth_after = from_hex_str_to_biguint(
736 "00000000000000000000000000000000000000000002be10e0f61dc56f6f85dc",
737 );
738
739 let new_state = res
740 .new_state
741 .as_any()
742 .downcast_ref::<LidoState>()
743 .unwrap();
744
745 assert_eq!(new_state.total_wrapped_st_eth, Some(total_wsteth_after));
746 }
747
748 #[test]
749 fn test_lido_spot_price() {
750 let token_st_eth = token_st_eth();
751 let token_wst_eth = token_wst_eth();
752 let token_eth = token_eth();
753
754 let st_state = lido_state_steth();
755 let wst_state = lido_state_wsteth();
756
757 let res = st_state
758 .spot_price(&token_eth, &token_st_eth)
759 .unwrap();
760 let exp = 1.0000000000000002;
761 assert_eq!(res, exp);
762
763 let res = st_state.spot_price(&token_st_eth, &token_wst_eth);
764 assert!(res.is_err());
765
766 let res = wst_state
767 .spot_price(&token_st_eth, &token_wst_eth)
768 .unwrap();
769 assert_eq!(res, 0.8206925386086495);
770
771 let res = wst_state
772 .spot_price(&token_wst_eth, &token_st_eth)
773 .unwrap();
774 assert_eq!(res, 1.2184831139019945);
775
776 let res = wst_state.spot_price(&token_eth, &token_st_eth);
777 assert!(res.is_err());
778 }
779
780 #[test]
781 fn test_lido_get_limits() {
782 let token_st_eth = bytes_st_eth();
783 let token_wst_eth = bytes_wst_eth();
784 let token_eth = bytes_eth();
785
786 let st_state = lido_state_steth();
787 let wst_state = lido_state_wsteth();
788
789 let res = st_state
790 .get_limits(token_eth.clone(), token_st_eth.clone())
791 .unwrap();
792 let exp = (
793 st_state
794 .stake_limits_state
795 .staking_limit
796 .clone(),
797 BigUint::from_str("149999999999999999999999").unwrap(),
798 );
799 assert_eq!(res, exp);
800
801 let res = st_state
802 .get_limits(token_st_eth.clone(), token_eth.clone())
803 .unwrap();
804 let exp = (BigUint::zero(), BigUint::zero());
805 assert_eq!(res, exp);
806
807 let res = st_state.get_limits(token_wst_eth.clone(), token_eth.clone());
808 assert!(res.is_err());
809
810 let res = wst_state
811 .get_limits(token_st_eth.clone(), token_wst_eth.clone())
812 .unwrap();
813 let allowed_to_wrap = wst_state.total_shares.clone() -
814 wst_state
815 .total_wrapped_st_eth
816 .clone()
817 .unwrap();
818 let exp = (BigUint::from_str("3093477275082723426591391").unwrap(), allowed_to_wrap);
819
820 assert_eq!(res, exp);
821
822 let res = wst_state
823 .get_limits(token_wst_eth.clone(), token_st_eth.clone())
824 .unwrap();
825 let total_wrapped = wst_state
826 .total_wrapped_st_eth
827 .clone()
828 .unwrap();
829 let exp = (total_wrapped, BigUint::from_str("4039778360807033131920717").unwrap());
830
831 assert_eq!(res, exp);
832
833 let res = wst_state.get_limits(token_wst_eth.clone(), token_eth.clone());
834 assert!(res.is_err());
835 }
836
837 #[test]
838 fn test_lido_st_delta_transition() {
839 let mut st_state = lido_state_steth();
840
841 let total_shares_after =
842 "0x00000000000000000000000000000000000000000005d9d75ae42b4ba9c04d1a";
843 let staking_status_after = "0x4c696d69746564";
844 let staking_limit_after = "0x1fc3842bd1f071c00000";
845
846 let mut updated_attributes: HashMap<String, Bytes> = HashMap::new();
847 updated_attributes.insert("total_shares".to_owned(), Bytes::from(total_shares_after));
848 updated_attributes.insert("staking_status".to_owned(), Bytes::from(staking_status_after));
849 updated_attributes.insert("staking_limit".to_owned(), Bytes::from(staking_limit_after));
850
851 let staking_state_delta = ProtocolStateDelta {
852 component_id: ST_ETH_ADDRESS_PROXY.to_owned(),
853 updated_attributes,
854 deleted_attributes: HashSet::new(),
855 };
856
857 let mut component_balances = HashMap::new();
858 let mut component_balances_inner = HashMap::new();
859 component_balances_inner.insert(
860 Bytes::from_str(ETH_ADDRESS).unwrap(),
861 Bytes::from_str("0x072409d88cbb5e48a01616").unwrap(),
862 );
863 component_balances.insert(ST_ETH_ADDRESS_PROXY.to_owned(), component_balances_inner);
864
865 let balances = Balances { component_balances, account_balances: HashMap::new() };
866
867 st_state
868 .delta_transition(staking_state_delta.clone(), &HashMap::new(), &balances)
869 .unwrap();
870
871 let exp = LidoState {
872 pool_type: LidoPoolType::StEth,
873 total_shares: BigUint::from_bytes_be(
874 &hex::decode("00000000000000000000000000000000000000000005d9d75ae42b4ba9c04d1a")
875 .unwrap(),
876 ),
877 total_pooled_eth: from_hex_str_to_biguint("072409d88cbb5e48a01616"),
878 total_wrapped_st_eth: None,
879 id: ST_ETH_ADDRESS_PROXY.into(),
880 native_address: ETH_ADDRESS.into(),
881 stake_limits_state: StakeLimitState {
882 staking_status: crate::evm::protocol::lido::state::StakingStatus::Limited,
883 staking_limit: BigUint::from_bytes_be(
884 &hex::decode("1fc3842bd1f071c00000").unwrap(),
885 ),
886 },
887 tokens: [
888 Bytes::from("0x0000000000000000000000000000000000000000"),
889 Bytes::from("0xae7ab96520de3a18e5e111b5eaab095312d7fe84"),
890 ],
891 token_to_track_total_pooled_eth: Bytes::from(ETH_ADDRESS),
892 };
893 assert_eq!(st_state, exp);
894 }
895
896 #[rstest]
897 #[case::missing_total_shares("total_shares")]
898 #[case::missing_staking_status("staking_status")]
899 #[case::missing_staking_limit("staking_limit")]
900 fn test_lido_st_delta_transition_missing_arg(#[case] missing_attribute: &str) {
901 let mut st_state = lido_state_steth();
902
903 let total_shares_after =
904 "0x00000000000000000000000000000000000000000005d9d75ae42b4ba9c04d1a";
905 let staking_status_after = "0x4c696d69746564";
906 let staking_limit_after = "0x1fc3842bd1f071c00000";
907
908 let mut updated_attributes: HashMap<String, Bytes> = HashMap::new();
909 updated_attributes.insert("total_shares".to_owned(), Bytes::from(total_shares_after));
910 updated_attributes.insert("staking_status".to_owned(), Bytes::from(staking_status_after));
911 updated_attributes.insert("staking_limit".to_owned(), Bytes::from(staking_limit_after));
912
913 let mut staking_state_delta = ProtocolStateDelta {
914 component_id: ST_ETH_ADDRESS_PROXY.to_owned(),
915 updated_attributes,
916 deleted_attributes: HashSet::new(),
917 };
918
919 let balances =
920 Balances { component_balances: HashMap::new(), account_balances: HashMap::new() };
921
922 staking_state_delta
923 .updated_attributes
924 .remove(missing_attribute);
925
926 assert!(st_state
927 .delta_transition(staking_state_delta.clone(), &HashMap::new(), &balances)
928 .is_err());
929 }
930
931 #[test]
932 fn test_lido_wst_delta_transition() {
933 let mut wst_state = lido_state_wsteth();
934
935 let total_shares_after =
936 "0x00000000000000000000000000000000000000000005d9d75ae42b4ba9c04d1a";
937 let total_ws_eth_after =
938 "0x00000000000000000000000000000000000000000002ba6f7b9af3c7a7b749e2";
939
940 let mut updated_attributes: HashMap<String, Bytes> = HashMap::new();
941 updated_attributes.insert("total_shares".to_owned(), Bytes::from(total_shares_after));
942 updated_attributes.insert("total_wstETH".to_owned(), Bytes::from(total_ws_eth_after));
943
944 let staking_state_delta = ProtocolStateDelta {
945 component_id: WST_ETH_ADDRESS.to_owned(),
946 updated_attributes,
947 deleted_attributes: HashSet::new(),
948 };
949
950 let mut component_balances = HashMap::new();
951 let mut component_balances_inner = HashMap::new();
952 component_balances_inner.insert(
953 Bytes::from_str(ST_ETH_ADDRESS_PROXY).unwrap(),
954 Bytes::from_str("0x072409d88cbb5e48a01616").unwrap(),
955 );
956 component_balances.insert(WST_ETH_ADDRESS.to_owned(), component_balances_inner);
957
958 let balances = Balances { component_balances, account_balances: HashMap::new() };
959
960 wst_state
961 .delta_transition(staking_state_delta, &HashMap::new(), &balances)
962 .unwrap();
963
964 let exp = LidoState {
965 pool_type: LidoPoolType::WStEth,
966 total_shares: BigUint::from_bytes_be(
967 &hex::decode("00000000000000000000000000000000000000000005d9d75ae42b4ba9c04d1a")
968 .unwrap(),
969 ),
970 total_pooled_eth: from_hex_str_to_biguint("072409d88cbb5e48a01616"),
971 total_wrapped_st_eth: Some(BigUint::from_bytes_be(
972 &hex::decode("00000000000000000000000000000000000000000002ba6f7b9af3c7a7b749e2")
973 .unwrap(),
974 )),
975 id: WST_ETH_ADDRESS.into(),
976 native_address: ETH_ADDRESS.into(),
977 stake_limits_state: wst_state.stake_limits_state.clone(),
978 tokens: [
979 Bytes::from("0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0"),
980 Bytes::from("0xae7ab96520de3a18e5e111b5eaab095312d7fe84"),
981 ],
982 token_to_track_total_pooled_eth: Bytes::from(ST_ETH_ADDRESS_PROXY),
983 };
984
985 assert_eq!(wst_state, exp);
986 }
987
988 #[rstest]
989 #[case::missing_total_shares("total_shares")]
990 #[case::missing_total_ws_eth("total_wstETH")]
991 fn test_lido_wst_delta_transition_missing_arg(#[case] missing_attribute: &str) {
992 let mut wst_state = lido_state_wsteth();
993
994 let total_shares_after =
995 "0x00000000000000000000000000000000000000000005d9d75ae42b4ba9c04d1a";
996 let total_ws_eth_after =
997 "0x00000000000000000000000000000000000000000002ba6f7b9af3c7a7b749e2";
998
999 let mut updated_attributes: HashMap<String, Bytes> = HashMap::new();
1000 updated_attributes.insert("total_shares".to_owned(), Bytes::from(total_shares_after));
1001 updated_attributes.insert("total_wstETH".to_owned(), Bytes::from(total_ws_eth_after));
1002
1003 let mut staking_state_delta = ProtocolStateDelta {
1004 component_id: WST_ETH_ADDRESS.to_owned(),
1005 updated_attributes,
1006 deleted_attributes: HashSet::new(),
1007 };
1008
1009 let balances =
1010 Balances { component_balances: HashMap::new(), account_balances: HashMap::new() };
1011
1012 staking_state_delta
1013 .updated_attributes
1014 .remove(missing_attribute);
1015
1016 assert!(wst_state
1017 .delta_transition(staking_state_delta.clone(), &HashMap::new(), &balances)
1018 .is_err());
1019 }
1020}