Skip to main content

tycho_common/simulation/
protocol_sim.rs

1use std::{any::Any, collections::HashMap, fmt, sync::Arc};
2
3use num_bigint::BigUint;
4
5use crate::{
6    dto::ProtocolStateDelta,
7    models::token::Token,
8    simulation::{
9        errors::{SimulationError, TransitionError},
10        indicatively_priced::IndicativelyPriced,
11        swap::{
12            self, LimitsParams, MarginalPriceParams, QuoteParams, SwapQuoter, TransitionParams,
13        },
14    },
15    Bytes,
16};
17
18/// The block a quote is expected to execute in.
19///
20/// This is **not** the block a state was decoded from: a quote produced against block `N` is
21/// normally submitted for block `N + 1`, and a quote produced against a still-open (partial /
22/// flashblock) view of block `N` is submitted for block `N` itself. Protocols whose pricing
23/// depends on the execution block — Aerodrome Slipstream's initial-vs-dynamic fee branch, for
24/// instance — resolve it from this type rather than from the block they last observed.
25#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
26#[non_exhaustive]
27pub struct BlockContext {
28    number: u64,
29    timestamp: u64,
30}
31
32impl BlockContext {
33    pub fn new(number: u64, timestamp: u64) -> Self {
34        Self { number, timestamp }
35    }
36
37    /// Height of the block the quote is expected to execute in.
38    pub fn number(&self) -> u64 {
39        self.number
40    }
41
42    /// Unix timestamp of the block the quote is expected to execute in.
43    ///
44    /// All flashblocks of one block share this value, matching `block.timestamp` on chain.
45    pub fn timestamp(&self) -> u64 {
46        self.timestamp
47    }
48}
49
50#[derive(Default, Debug, Clone)]
51pub struct Balances {
52    pub component_balances: HashMap<String, HashMap<Bytes, Bytes>>,
53    pub account_balances: HashMap<Bytes, HashMap<Bytes, Bytes>>,
54}
55
56/// Represents the result of getting the amount out of a trading pair.
57#[derive(Debug, Clone)]
58pub struct GetAmountOutResult {
59    /// The output amount
60    pub amount: BigUint,
61    /// The gas cost
62    pub gas: BigUint,
63    /// The new state after the swap
64    pub new_state: Box<dyn ProtocolSim>,
65}
66
67impl GetAmountOutResult {
68    /// Constructs a new GetAmountOutResult struct with the given amount and gas
69    pub fn new(amount: BigUint, gas: BigUint, new_state: Box<dyn ProtocolSim>) -> Self {
70        GetAmountOutResult { amount, gas, new_state }
71    }
72
73    /// Aggregates the given GetAmountOutResult struct to the current one.
74    /// It updates the amount with the other's amount and adds the other's gas to the current one's
75    /// gas.
76    pub fn aggregate(&mut self, other: &Self) {
77        self.amount = other.amount.clone();
78        self.gas += &other.gas;
79    }
80}
81
82impl fmt::Display for GetAmountOutResult {
83    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
84        write!(f, "amount = {}, gas = {}", self.amount, self.gas)
85    }
86}
87
88/// Represents a price as a fraction in the token_in -> token_out direction with units
89/// `[token_out/token_in]`.
90///
91/// A fraction struct is used for price to have flexibility in precision independent of the
92/// decimal precisions of the numerator and denominator tokens. This allows for:
93/// - Exact price representation without floating-point errors
94/// - Handling tokens with different decimal places without loss of precision
95///
96/// # Example
97/// If we want to represent that token A is worth 2.5 units of token B:
98///
99/// ```
100/// use num_bigint::BigUint;
101/// use tycho_common::simulation::protocol_sim::Price;
102///
103/// let numerator = BigUint::from(25u32); // Represents 25 units of token B
104/// let denominator = BigUint::from(10u32); // Represents 10 units of token A
105/// let price = Price::new(numerator, denominator);
106/// ```
107///
108/// If you want to define a limit price for a trade, where you expect to get at least 120 T1 for
109/// 50 T2:
110/// ```
111/// use num_bigint::BigUint;
112/// use tycho_common::simulation::protocol_sim::Price;
113///
114/// let min_amount_out = BigUint::from(120u32); // The minimum amount of T1 you expect
115/// let amount_in = BigUint::from(50u32); // The amount of T2 you are selling
116/// let limit_price = Price::new(min_amount_out, amount_in);
117/// ```
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct Price {
120    /// The amount of token_out (what you receive), including token decimals
121    pub numerator: BigUint,
122    /// The amount of token_in (what you pay), including token decimals
123    pub denominator: BigUint,
124}
125
126impl Price {
127    pub fn new(numerator: BigUint, denominator: BigUint) -> Self {
128        if denominator == BigUint::ZERO {
129            // Division by zero is not possible
130            panic!("Price denominator cannot be zero");
131        } else if numerator == BigUint::ZERO {
132            // Zero pool price is not valid in our context
133            panic!("Price numerator cannot be zero");
134        }
135        Self { numerator, denominator }
136    }
137}
138
139/// A point on the AMM price curve.
140///
141/// Collected during iterative numerical search algorithms.
142/// These points can be reused as bounds for subsequent searches, improving convergence speed.
143#[derive(Debug, Clone)]
144pub struct PricePoint {
145    /// The amount of token_in in atomic units (wei).
146    pub amount_in: BigUint,
147    /// The amount of token_out in atomic units (wei).
148    pub amount_out: BigUint,
149    /// The price in units of `[token_out/token_in]` scaled by decimals.
150    ///
151    /// Computed as `(amount_out / 10^token_out_decimals) / (amount_in / 10^token_in_decimals)`.
152    pub price: f64,
153}
154
155impl PricePoint {
156    pub fn new(amount_in: BigUint, amount_out: BigUint, price: f64) -> Self {
157        Self { amount_in, amount_out, price }
158    }
159}
160
161/// Represents a pool swap between two tokens at a given price on a pool.
162#[derive(Debug, Clone)]
163pub struct PoolSwap {
164    /// The amount of token_in sold to the pool
165    amount_in: BigUint,
166    /// The amount of token_out bought from the pool
167    amount_out: BigUint,
168    /// The new state of the pool after the swap
169    new_state: Box<dyn ProtocolSim>,
170    /// Optional price points that the pool was transitioned through while computing this swap.
171    /// Useful for providing good bounds for repeated calls.
172    price_points: Option<Vec<PricePoint>>,
173}
174
175impl PoolSwap {
176    pub fn new(
177        amount_in: BigUint,
178        amount_out: BigUint,
179        new_state: Box<dyn ProtocolSim>,
180        price_points: Option<Vec<PricePoint>>,
181    ) -> Self {
182        Self { amount_in, amount_out, new_state, price_points }
183    }
184
185    pub fn amount_in(&self) -> &BigUint {
186        &self.amount_in
187    }
188
189    pub fn amount_out(&self) -> &BigUint {
190        &self.amount_out
191    }
192
193    pub fn new_state(&self) -> &dyn ProtocolSim {
194        self.new_state.as_ref()
195    }
196
197    pub fn price_points(&self) -> &Option<Vec<PricePoint>> {
198        &self.price_points
199    }
200}
201
202/// Options on how to constrain the pool swap query.
203///
204/// All prices use units `[token_out/token_in]` with amounts in atomic units (wei). When selling
205/// token_in into a pool, prices decrease due to slippage.
206#[derive(Debug, Clone, PartialEq)]
207pub enum SwapConstraint {
208    /// Calculates the maximum trade while respecting a minimum trade price.
209    TradeLimitPrice {
210        /// The minimum acceptable trade price. The resulting `amount_out / amount_in >= limit`.
211        limit: Price,
212        /// Fraction to raise the acceptance threshold above `limit`. Loosens the search criteria
213        /// but will never allow violating the trade limit price itself.
214        tolerance: f64,
215        /// The minimum amount of token_in that must be used for this trade.
216        min_amount_in: Option<BigUint>,
217        /// The maximum amount of token_in that can be used for this trade.
218        max_amount_in: Option<BigUint>,
219    },
220
221    /// Calculates the swap required to move the pool's marginal price down to a target.
222    ///
223    /// # Edge Cases and Limitations
224    ///
225    /// Computing the exact amount to move a pool's marginal price to a target has several
226    /// challenges:
227    /// - The definition of marginal price varies between protocols. It is usually not an attribute
228    ///   of the pool but a consequence of its liquidity distribution and current state.
229    /// - For protocols with concentrated liquidity, the marginal price is discrete, meaning we
230    ///   can't always find an exact trade amount to reach the target price.
231    /// - Not all protocols support analytical solutions for this problem, requiring numerical
232    ///   methods.
233    PoolTargetPrice {
234        /// The target marginal price for the pool after the trade. The pool's price decreases
235        /// toward this target as token_in is sold into it.
236        target: Price,
237        /// Fraction above `target` considered acceptable. After trading, the pool's marginal
238        /// price will be in `[target, target * (1 + tolerance)]`.
239        tolerance: f64,
240        /// The lower bound for searching algorithms.
241        min_amount_in: Option<BigUint>,
242        /// The upper bound for searching algorithms.
243        max_amount_in: Option<BigUint>,
244    },
245}
246
247/// Represents the parameters for [ProtocolSim::query_pool_swap].
248#[derive(Debug, Clone, PartialEq)]
249pub struct QueryPoolSwapParams {
250    /// The token being sold (swapped into the pool)
251    token_in: Token,
252    /// The token being bought (swapped out of the pool)
253    token_out: Token,
254    /// Type of price constraint to be applied. See [SwapConstraint].
255    swap_constraint: SwapConstraint,
256}
257
258impl QueryPoolSwapParams {
259    pub fn new(token_in: Token, token_out: Token, swap_constraint: SwapConstraint) -> Self {
260        Self { token_in, token_out, swap_constraint }
261    }
262
263    /// Returns a reference to the input token (token being sold into the pool)
264    pub fn token_in(&self) -> &Token {
265        &self.token_in
266    }
267
268    /// Returns a reference to the output token (token being bought out of the pool)
269    pub fn token_out(&self) -> &Token {
270        &self.token_out
271    }
272
273    /// Returns a reference to the price constraint
274    pub fn swap_constraint(&self) -> &SwapConstraint {
275        &self.swap_constraint
276    }
277}
278
279/// ProtocolSim trait
280/// This trait defines the methods that a protocol state must implement in order to be used
281/// in the trade simulation.
282#[typetag::serde(tag = "protocol", content = "state")]
283pub trait ProtocolSim: fmt::Debug + Send + Sync + 'static {
284    /// Returns the fee of the protocol as ratio
285    ///
286    /// E.g. if the fee is 1%, the value returned would be 0.01.
287    ///
288    /// # Panics
289    ///
290    /// Currently panic for protocols with asymmetric fees (e.g. Rocketpool, Uniswap V4),
291    /// where a single fee value cannot represent the protocol's fee structure.
292    fn fee(&self) -> f64;
293
294    /// Returns the protocol's current spot buy price for `base` in units of `quote`.
295    ///
296    /// The returned price is the amount of `quote` required to buy exactly 1 unit of `base`,
297    /// accounting for the protocol fee (i.e. `price = pre_fee_price / (1.0 - fee)`)
298    /// and assuming zero slippage (i.e., a negligibly small trade size).
299    ///
300    /// # Arguments
301    /// * `base` - the token being priced (what you buy). For BTC/USDT, BTC is the base token.
302    /// * `quote` - the token used to price (pay) for `base`. For BTC/USDT, USDT is the quote token.
303    ///
304    /// # Examples
305    /// If the BTC/USDT is trading at 1000 with a 20% fee, this returns `1000 / (1.0 - 0.20) = 1250`
306    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
307
308    /// Returns the amount out given an amount in and input/output tokens.
309    ///
310    /// # Arguments
311    ///
312    /// * `amount_in` - The amount in of the input token.
313    /// * `token_in` - The input token ERC20 token.
314    /// * `token_out` - The output token ERC20 token.
315    ///
316    /// # Returns
317    ///
318    /// A `Result` containing a `GetAmountOutResult` struct on success or a
319    ///  `SimulationError` on failure.
320    fn get_amount_out(
321        &self,
322        amount_in: BigUint,
323        token_in: &Token,
324        token_out: &Token,
325    ) -> Result<GetAmountOutResult, SimulationError>;
326
327    /// Computes the maximum amount that can be traded between two tokens.
328    ///
329    /// This function calculates the maximum possible trade amount between two tokens,
330    /// taking into account the protocol's specific constraints and mechanics.
331    /// The implementation details vary by protocol - for example:
332    /// - For constant product AMMs (like Uniswap V2), this is based on available reserves
333    /// - For concentrated liquidity AMMs (like Uniswap V3), this considers liquidity across tick
334    ///   ranges
335    ///
336    /// Note: if there are no limits, the returned amount will be a "soft" limit,
337    ///       meaning that the actual amount traded could be higher but it's advised to not
338    ///       exceed it.
339    ///
340    /// # Arguments
341    /// * `sell_token` - The address of the token being sold
342    /// * `buy_token` - The address of the token being bought
343    ///
344    /// # Returns
345    /// * `Ok((BigUint, BigUint))` - A tuple containing:
346    ///   - First element: The maximum input amount (sell_token)
347    ///   - Second element: The maximum output amount (buy_token)
348    ///
349    /// For `let res = get_limits(...)`, the valid input domain for `get_amount_out` is `[0,
350    /// res.0]`.
351    ///
352    /// * `Err(SimulationError)` - If any unexpected error occurs
353    fn get_limits(
354        &self,
355        sell_token: Bytes,
356        buy_token: Bytes,
357    ) -> Result<(BigUint, BigUint), SimulationError>;
358
359    /// Decodes and applies a protocol state delta to the state
360    ///
361    /// Will error if the provided delta is missing any required attributes or if any of the
362    /// attribute values cannot be decoded.
363    ///
364    /// # Arguments
365    ///
366    /// * `delta` - A `ProtocolStateDelta` from the tycho indexer
367    ///
368    /// # Returns
369    ///
370    /// * `Result<(), TransitionError<String>>` - A `Result` containing `()` on success or a
371    ///   `TransitionError` on failure.
372    fn delta_transition(
373        &mut self,
374        delta: ProtocolStateDelta,
375        tokens: &HashMap<Bytes, Token>,
376        balances: &Balances,
377    ) -> Result<(), TransitionError>;
378
379    /// Calculates the swap volume required to achieve the provided goal when trading against this
380    /// pool.
381    ///
382    /// This method will branch towards different behaviors based on [SwapConstraint] enum. Please
383    /// refer to its documentation for further details on each behavior.
384    ///
385    /// In short, the current two options are:
386    /// - Maximize your trade while respecting a trade limit price:
387    ///   [SwapConstraint::TradeLimitPrice]
388    /// - Move the pool price to a target price: [SwapConstraint::PoolTargetPrice]
389    ///
390    /// # Arguments
391    ///
392    /// * `params` - A [QueryPoolSwapParams] struct containing the inputs for this method.
393    ///
394    /// # Returns
395    ///
396    /// * `Ok(PoolSwap)` - A `PoolSwap` struct containing the amounts to be traded and the state of
397    ///   the pool after trading.
398    /// * `Err(SimulationError)` - If:
399    ///   - The calculation encounters numerical issues
400    ///   - The method is not implemented for this protocol
401    #[allow(unused)]
402    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
403        Err(SimulationError::FatalError("query_pool_swap not implemented".into()))
404    }
405
406    /// Clones the protocol state as a trait object.
407    /// This allows the state to be cloned when it is being used as a `Box<dyn ProtocolSim>`.
408    fn clone_box(&self) -> Box<dyn ProtocolSim>;
409
410    /// Allows downcasting of the trait object to its underlying type.
411    fn as_any(&self) -> &dyn Any;
412
413    /// Allows downcasting of the trait object to its mutable underlying type.
414    fn as_any_mut(&mut self) -> &mut dyn Any;
415
416    /// Compares two protocol states for equality.
417    /// This method must be implemented to define how two protocol states are considered equal
418    /// (used for tests).
419    fn eq(&self, other: &dyn ProtocolSim) -> bool;
420
421    /// Cast as IndicativelyPriced. This is necessary for RFQ protocols
422    fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
423        Err(SimulationError::FatalError("Pool State does not implement IndicativelyPriced".into()))
424    }
425
426    /// Advances the state to the block a quote is expected to execute in.
427    ///
428    /// Returns `true` when quoting behavior changed and the state must be re-emitted to
429    /// consumers. The stream decoder calls this on every message — including messages in which
430    /// the pool itself did not change, since without that a state's notion of the execution
431    /// block would freeze at the pool's last update. Implementations must be idempotent for a
432    /// repeated block and must not use the value as a substitute for the block they were decoded
433    /// from: it describes the future, not the observed past.
434    fn apply_block(&mut self, _block: &BlockContext) -> bool {
435        false
436    }
437
438    /// Runs every later quote from this state under a pending block's state, held in
439    /// `overrides`.
440    ///
441    /// The payload type belongs to the simulation crate that builds it. The default ignores it,
442    /// for protocols that do not simulate pending state.
443    ///
444    /// # Errors
445    /// Returns an error when the protocol simulates pending state but `overrides` is not the
446    /// type it expects.
447    fn set_pending_overrides(
448        &mut self,
449        _overrides: Arc<dyn Any + Send + Sync>,
450    ) -> Result<(), SimulationError> {
451        Ok(())
452    }
453}
454
455impl Clone for Box<dyn ProtocolSim> {
456    fn clone(&self) -> Box<dyn ProtocolSim> {
457        self.clone_box()
458    }
459}
460
461impl<T> ProtocolSim for T
462where
463    T: SwapQuoter + Clone + Send + Sync + Eq + 'static,
464{
465    fn fee(&self) -> f64 {
466        self.quotable_pairs()
467            .iter()
468            .map(|(t0, t1)| {
469                let amount = BigUint::from(10u32).pow(t0.decimals);
470                if let Ok(params) = QuoteParams::fixed_in(&t0.address, &t1.address, amount) {
471                    self.fee(params)
472                        .map(|f| f.fee())
473                        .unwrap_or(f64::MAX)
474                } else {
475                    f64::MAX
476                }
477            })
478            .reduce(f64::min)
479            .unwrap_or(f64::MAX)
480    }
481
482    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
483        self.marginal_price(MarginalPriceParams::new(&base.address, &quote.address))
484            .map(|r| r.price())
485    }
486
487    fn get_amount_out(
488        &self,
489        amount_in: BigUint,
490        token_in: &Token,
491        token_out: &Token,
492    ) -> Result<GetAmountOutResult, SimulationError> {
493        #[allow(deprecated)]
494        self.quote(
495            QuoteParams::fixed_in(&token_in.address, &token_out.address, amount_in)?
496                .with_new_state(),
497        )
498        .map(|r| {
499            GetAmountOutResult::new(
500                r.amount_out().clone(),
501                r.gas().clone(),
502                r.new_state()
503                    .expect("quote includes new state")
504                    .to_protocol_sim(),
505            )
506        })
507    }
508
509    fn get_limits(
510        &self,
511        sell_token: Bytes,
512        buy_token: Bytes,
513    ) -> Result<(BigUint, BigUint), SimulationError> {
514        self.swap_limits(LimitsParams::new(&sell_token, &buy_token))
515            .map(|r| (r.range_in().upper().clone(), r.range_out().upper().clone()))
516    }
517
518    fn delta_transition(
519        &mut self,
520        delta: ProtocolStateDelta,
521        tokens: &HashMap<Bytes, Token>,
522        balances: &Balances,
523    ) -> Result<(), TransitionError> {
524        self.delta_transition(TransitionParams::new(delta, tokens, balances))
525            .map(|_| ())
526    }
527
528    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
529        let constraint = match params.swap_constraint.clone() {
530            SwapConstraint::TradeLimitPrice { limit, tolerance, min_amount_in, max_amount_in } => {
531                swap::SwapConstraint::TradeLimitPrice {
532                    limit,
533                    tolerance,
534                    min_amount_in,
535                    max_amount_in,
536                }
537            }
538            SwapConstraint::PoolTargetPrice { target, tolerance, min_amount_in, max_amount_in } => {
539                swap::SwapConstraint::PoolTargetPrice {
540                    target,
541                    tolerance,
542                    min_amount_in,
543                    max_amount_in,
544                }
545            }
546        };
547        #[allow(deprecated)]
548        self.query_swap(swap::QuerySwapParams::new(
549            &params.token_in.address,
550            &params.token_out.address,
551            constraint,
552        ))
553        .map(|r| {
554            PoolSwap::new(
555                r.amount_in().clone(),
556                r.amount_out().clone(),
557                r.new_state().unwrap().to_protocol_sim(),
558                r.price_points().as_ref().map(|points| {
559                    points
560                        .iter()
561                        .map(|p| {
562                            PricePoint::new(
563                                p.amount_in().clone(),
564                                p.amount_out().clone(),
565                                p.price(),
566                            )
567                        })
568                        .collect()
569                }),
570            )
571        })
572    }
573
574    fn clone_box(&self) -> Box<dyn ProtocolSim> {
575        #[allow(deprecated)]
576        self.to_protocol_sim()
577    }
578
579    fn as_any(&self) -> &dyn Any {
580        self
581    }
582
583    fn as_any_mut(&mut self) -> &mut dyn Any {
584        self
585    }
586
587    fn eq(&self, other: &dyn ProtocolSim) -> bool {
588        if let Some(other) = other.as_any().downcast_ref::<T>() {
589            self == other
590        } else {
591            false
592        }
593    }
594
595    fn typetag_name(&self) -> &'static str {
596        self.typetag_name()
597    }
598
599    fn typetag_deserialize(&self) {
600        self.typetag_deserialize()
601    }
602}
603
604#[cfg(test)]
605mod tests {
606
607    use super::*;
608
609    #[test]
610    fn serde() {
611        use serde::{Deserialize, Serialize};
612
613        #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
614        struct DummyProtocol {
615            reserve_0: u64,
616            reserve_1: u64,
617        }
618
619        #[typetag::serde]
620        impl ProtocolSim for DummyProtocol {
621            fn clone_box(&self) -> Box<dyn ProtocolSim> {
622                todo!()
623            }
624
625            fn as_any(&self) -> &dyn Any {
626                self
627            }
628
629            fn as_any_mut(&mut self) -> &mut dyn Any {
630                todo!()
631            }
632
633            fn eq(&self, other: &dyn ProtocolSim) -> bool {
634                if let Some(other) = other.as_any().downcast_ref::<Self>() {
635                    self.reserve_0 == other.reserve_0 && self.reserve_1 == other.reserve_1
636                } else {
637                    false
638                }
639            }
640
641            fn fee(&self) -> f64 {
642                todo!()
643            }
644            fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
645                todo!()
646            }
647            fn get_amount_out(
648                &self,
649                _amount_in: BigUint,
650                _token_in: &Token,
651                _token_out: &Token,
652            ) -> Result<GetAmountOutResult, SimulationError> {
653                todo!()
654            }
655            fn get_limits(
656                &self,
657                _sell_token: Bytes,
658                _buy_token: Bytes,
659            ) -> Result<(BigUint, BigUint), SimulationError> {
660                todo!()
661            }
662            fn delta_transition(
663                &mut self,
664                _delta: ProtocolStateDelta,
665                _tokens: &HashMap<Bytes, Token>,
666                _balances: &Balances,
667            ) -> Result<(), TransitionError> {
668                todo!()
669            }
670        }
671
672        let state = DummyProtocol { reserve_0: 1, reserve_1: 2 };
673
674        assert_eq!(serde_json::to_string(&state).unwrap(), r#"{"reserve_0":1,"reserve_1":2}"#);
675        assert_eq!(
676            serde_json::to_string(&state as &dyn ProtocolSim).unwrap(),
677            r#"{"protocol":"DummyProtocol","state":{"reserve_0":1,"reserve_1":2}}"#
678        );
679
680        let deserialized: Box<dyn ProtocolSim> = serde_json::from_str(
681            r#"{"protocol":"DummyProtocol","state":{"reserve_0":1,"reserve_1":2}}"#,
682        )
683        .unwrap();
684
685        assert!(deserialized.eq(&state));
686    }
687}