tycho_simulation/evm/protocol/lido/
state.rs

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// see here https://github.com/lidofinance/core/blob/cca04b42123735714d8c60a73c2f7af949e989db/contracts/0.4.24/lib/StakeLimitUtils.sol#L38
44#[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        // https://github.com/lidofinance/core/blob/cca04b42123735714d8c60a73c2f7af949e989db/contracts/0.4.24/lib/StakeLimitUtils.sol#L98
53        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            //amount of wsteth, wstETH -> stETH
230
231            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            // total_shares - wstETH, stETH -> wstETH
251
252            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        // there is no fee when swapping
362        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, &quote.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, &quote.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        // check the pool type and the token in and token out
410        // if it's stETH type and the token out is ETH this is not allowed
411        // call the corresponding swap method
412        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        // If it's the stETH type:
442        //   - and the buy token is ETH, the limits are 0
443        //   - sell token is ETH: use the StakeLimitState
444        // If it's wstETH: rely on the total supply
445        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        // total pooled eth: 0x072409d75ebf50c5534125, 8632667470434094430765349
632        // total shares: 0x00000000000000000000000000000000000000000005dc41ec2e3ba19cf3ea6d
633        // tx 0x1953b525c8640c2709e984ebc28bb1f2180dd72759bb2aac7413e94b602b0d53
634        // total_shares_after: 0x00000000000000000000000000000000000000000005dc41ec487a31b7865d5e
635        // total pooled eth after: 0x072409d77eb9c55db21616
636        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(); // diff in total pooled eth; rounding error
646        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        // total pooled eth: 072409d88cbb5e48a01616
666        // total shares: 00000000000000000000000000000000000000000005dc41ed2611c5fd46f034
667        // tx 0xce9418dd0e74cdf738362bee9428da73c047049c34f28ff8a18b19f047c27c53
668        // ws eth before 00000000000000000000000000000000000000000002be10e0f61dc56f6f85dc
669        // ws eth after 00000000000000000000000000000000000000000002be11ccda98eef241c759
670
671        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        // total pooled eth: 0x072409d75ebf50c5534125, 8632667470434094430765349
714        // total shares: 0x00000000000000000000000000000000000000000005dc41ec2e3ba19cf3ea6d
715        // tx 0xa49316d76b7cf2ba9f81c7b84868faaa6306eef5a15f194f55b3675bce89367a
716        // ws eth after 0x00000000000000000000000000000000000000000002be10e0f61dc56f6f85dc
717        // ws eth before 0x00000000000000000000000000000000000000000002be110f2a220611513da6
718
719        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}