Skip to main content

tycho_common/simulation/
protocol_sim.rs

1use std::{any::Any, collections::HashMap, fmt};
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    },
12    Bytes,
13};
14
15#[derive(Default)]
16pub struct Balances {
17    pub component_balances: HashMap<String, HashMap<Bytes, Bytes>>,
18    pub account_balances: HashMap<Bytes, HashMap<Bytes, Bytes>>,
19}
20
21/// GetAmountOutResult struct represents the result of getting the amount out of a trading pair
22///
23/// # Fields
24///
25/// * `amount`: BigUint, the amount of the trading pair
26/// * `gas`: BigUint, the gas of the trading pair
27#[derive(Debug)]
28pub struct GetAmountOutResult {
29    pub amount: BigUint,
30    pub gas: BigUint,
31    pub new_state: Box<dyn ProtocolSim>,
32}
33
34impl GetAmountOutResult {
35    /// Constructs a new GetAmountOutResult struct with the given amount and gas
36    pub fn new(amount: BigUint, gas: BigUint, new_state: Box<dyn ProtocolSim>) -> Self {
37        GetAmountOutResult { amount, gas, new_state }
38    }
39
40    /// Aggregates the given GetAmountOutResult struct to the current one.
41    /// It updates the amount with the other's amount and adds the other's gas to the current one's
42    /// gas.
43    pub fn aggregate(&mut self, other: &Self) {
44        self.amount = other.amount.clone();
45        self.gas += &other.gas;
46    }
47}
48
49impl fmt::Display for GetAmountOutResult {
50    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51        write!(f, "amount = {}, gas = {}", self.amount, self.gas)
52    }
53}
54
55/// Represents a price as a fraction in the token_in -> token_out direction with units
56/// `[token_out/token_in]`.
57///
58/// # Fields
59///
60/// * `numerator` - The amount of token_out (what you receive) in atomic units (wei)
61/// * `denominator` - The amount of token_in (what you pay) in atomic units (wei)
62///
63/// A fraction struct is used for price to have flexibility in precision independent of the
64/// decimal precisions of the numerator and denominator tokens. This allows for:
65/// - Exact price representation without floating-point errors
66/// - Handling tokens with different decimal places without loss of precision
67///
68/// # Example
69/// If we want to represent that token A is worth 2.5 units of token B:
70///
71/// ```
72/// use num_bigint::BigUint;
73/// use tycho_common::simulation::protocol_sim::Price;
74///
75/// let numerator = BigUint::from(25u32); // Represents 25 units of token B
76/// let denominator = BigUint::from(10u32); // Represents 10 units of token A
77/// let price = Price::new(numerator, denominator);
78/// ```
79///
80/// If you want to define a limit price for a trade, where you expect to get at least 120 T1 for
81/// 50 T2:
82/// ```
83/// use num_bigint::BigUint;
84/// use tycho_common::simulation::protocol_sim::Price;
85///
86/// let min_amount_out = BigUint::from(120u32); // The minimum amount of T1 you expect
87/// let amount_in = BigUint::from(50u32); // The amount of T2 you are selling
88/// let limit_price = Price::new(min_amount_out, amount_in);
89/// ```
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Price {
92    pub numerator: BigUint,
93    pub denominator: BigUint,
94}
95
96impl Price {
97    pub fn new(numerator: BigUint, denominator: BigUint) -> Self {
98        if denominator == BigUint::ZERO {
99            // Division by zero is not possible
100            panic!("Price denominator cannot be zero");
101        } else if numerator == BigUint::ZERO {
102            // Zero pool price is not valid in our context
103            panic!("Price numerator cannot be zero");
104        }
105        Self { numerator, denominator }
106    }
107}
108
109/// A point on the AMM price curve.
110///
111/// Collected during iterative numerical search algorithms.
112/// These points can be reused as bounds for subsequent searches, improving convergence speed.
113#[derive(Debug, Clone)]
114pub struct PricePoint {
115    /// The amount of token_in in atomic units (wei).
116    pub amount_in: BigUint,
117    /// The amount of token_out in atomic units (wei).
118    pub amount_out: BigUint,
119    /// The price in units of `[token_out/token_in]` scaled by decimals.
120    ///
121    /// Computed as `(amount_out / 10^token_out_decimals) / (amount_in / 10^token_in_decimals)`.
122    pub price: f64,
123}
124
125impl PricePoint {
126    pub fn new(amount_in: BigUint, amount_out: BigUint, price: f64) -> Self {
127        Self { amount_in, amount_out, price }
128    }
129}
130
131/// Represents a pool swap between two tokens at a given price on a pool.
132#[derive(Debug, Clone)]
133pub struct PoolSwap {
134    /// The amount of token_in sold to the pool
135    amount_in: BigUint,
136    /// The amount of token_out bought from the pool
137    amount_out: BigUint,
138    /// The new state of the pool after the swap
139    new_state: Box<dyn ProtocolSim>,
140    /// Optional price points that the pool was transitioned through while computing this swap.
141    /// Useful for providing good bounds for repeated calls.
142    price_points: Option<Vec<PricePoint>>,
143}
144
145impl PoolSwap {
146    pub fn new(
147        amount_in: BigUint,
148        amount_out: BigUint,
149        new_state: Box<dyn ProtocolSim>,
150        price_points: Option<Vec<PricePoint>>,
151    ) -> Self {
152        Self { amount_in, amount_out, new_state, price_points }
153    }
154
155    pub fn amount_in(&self) -> &BigUint {
156        &self.amount_in
157    }
158
159    pub fn amount_out(&self) -> &BigUint {
160        &self.amount_out
161    }
162
163    pub fn new_state(&self) -> &dyn ProtocolSim {
164        self.new_state.as_ref()
165    }
166
167    pub fn price_points(&self) -> &Option<Vec<PricePoint>> {
168        &self.price_points
169    }
170}
171
172/// Options on how to constrain the pool swap query.
173///
174/// All prices use units `[token_out/token_in]` with amounts in atomic units (wei). When selling
175/// token_in into a pool, prices decrease due to slippage.
176#[derive(Debug, Clone, PartialEq)]
177pub enum SwapConstraint {
178    /// Calculates the maximum trade while respecting a minimum trade price.
179    TradeLimitPrice {
180        /// The minimum acceptable trade price. The resulting `amount_out / amount_in >= limit`.
181        limit: Price,
182        /// Fraction to raise the acceptance threshold above `limit`. Loosens the search criteria
183        /// but will never allow violating the trade limit price itself.
184        tolerance: f64,
185        /// The minimum amount of token_in that must be used for this trade.
186        min_amount_in: Option<BigUint>,
187        /// The maximum amount of token_in that can be used for this trade.
188        max_amount_in: Option<BigUint>,
189    },
190
191    /// Calculates the swap required to move the pool's marginal price down to a target.
192    ///
193    /// # Edge Cases and Limitations
194    ///
195    /// Computing the exact amount to move a pool's marginal price to a target has several
196    /// challenges:
197    /// - The definition of marginal price varies between protocols. It is usually not an attribute
198    ///   of the pool but a consequence of its liquidity distribution and current state.
199    /// - For protocols with concentrated liquidity, the marginal price is discrete, meaning we
200    ///   can't always find an exact trade amount to reach the target price.
201    /// - Not all protocols support analytical solutions for this problem, requiring numerical
202    ///   methods.
203    PoolTargetPrice {
204        /// The target marginal price for the pool after the trade. The pool's price decreases
205        /// toward this target as token_in is sold into it.
206        target: Price,
207        /// Fraction above `target` considered acceptable. After trading, the pool's marginal
208        /// price will be in `[target, target * (1 + tolerance)]`.
209        tolerance: f64,
210        /// The lower bound for searching algorithms.
211        min_amount_in: Option<BigUint>,
212        /// The upper bound for searching algorithms.
213        max_amount_in: Option<BigUint>,
214    },
215}
216
217/// Represents the parameters for [ProtocolSim::query_pool_swap].
218///
219/// # Fields
220///
221/// * `token_in` - The token being sold (swapped into the pool)
222/// * `token_out` - The token being bought (swapped out of the pool)
223/// * `swap_constraint` - Type of price constraint to be applied. See [SwapConstraint].
224#[derive(Debug, Clone, PartialEq)]
225pub struct QueryPoolSwapParams {
226    token_in: Token,
227    token_out: Token,
228    swap_constraint: SwapConstraint,
229}
230
231impl QueryPoolSwapParams {
232    pub fn new(token_in: Token, token_out: Token, swap_constraint: SwapConstraint) -> Self {
233        Self { token_in, token_out, swap_constraint }
234    }
235
236    /// Returns a reference to the input token (token being sold into the pool)
237    pub fn token_in(&self) -> &Token {
238        &self.token_in
239    }
240
241    /// Returns a reference to the output token (token being bought out of the pool)
242    pub fn token_out(&self) -> &Token {
243        &self.token_out
244    }
245
246    /// Returns a reference to the price constraint
247    pub fn swap_constraint(&self) -> &SwapConstraint {
248        &self.swap_constraint
249    }
250}
251
252/// ProtocolSim trait
253/// This trait defines the methods that a protocol state must implement in order to be used
254/// in the trade simulation.
255#[typetag::serde(tag = "protocol", content = "state")]
256pub trait ProtocolSim: fmt::Debug + Send + Sync + 'static {
257    /// Returns the fee of the protocol as ratio
258    ///
259    /// E.g. if the fee is 1%, the value returned would be 0.01.
260    ///
261    /// # Panics
262    ///
263    /// Currently panic for protocols with asymmetric fees (e.g. Rocketpool, Uniswap V4),
264    /// where a single fee value cannot represent the protocol's fee structure.
265    fn fee(&self) -> f64;
266
267    /// Returns the protocol's current spot buy price for `base` in units of `quote`.
268    ///
269    /// The returned price is the amount of `quote` required to buy exactly 1 unit of `base`,
270    /// accounting for the protocol fee (i.e. `price = pre_fee_price / (1.0 - fee)`)
271    /// and assuming zero slippage (i.e., a negligibly small trade size).
272    ///
273    /// # Arguments
274    /// * `base` - the token being priced (what you buy). For BTC/USDT, BTC is the base token.
275    /// * `quote` - the token used to price (pay) for `base`. For BTC/USDT, USDT is the quote token.
276    ///
277    /// # Examples
278    /// If the BTC/USDT is trading at 1000 with a 20% fee, this returns `1000 / (1.0 - 0.20) = 1250`
279    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
280
281    /// Returns the amount out given an amount in and input/output tokens.
282    ///
283    /// # Arguments
284    ///
285    /// * `amount_in` - The amount in of the input token.
286    /// * `token_in` - The input token ERC20 token.
287    /// * `token_out` - The output token ERC20 token.
288    ///
289    /// # Returns
290    ///
291    /// A `Result` containing a `GetAmountOutResult` struct on success or a
292    ///  `SimulationError` on failure.
293    fn get_amount_out(
294        &self,
295        amount_in: BigUint,
296        token_in: &Token,
297        token_out: &Token,
298    ) -> Result<GetAmountOutResult, SimulationError>;
299
300    /// Computes the maximum amount that can be traded between two tokens.
301    ///
302    /// This function calculates the maximum possible trade amount between two tokens,
303    /// taking into account the protocol's specific constraints and mechanics.
304    /// The implementation details vary by protocol - for example:
305    /// - For constant product AMMs (like Uniswap V2), this is based on available reserves
306    /// - For concentrated liquidity AMMs (like Uniswap V3), this considers liquidity across tick
307    ///   ranges
308    ///
309    /// Note: if there are no limits, the returned amount will be a "soft" limit,
310    ///       meaning that the actual amount traded could be higher but it's advised to not
311    ///       exceed it.
312    ///
313    /// # Arguments
314    /// * `sell_token` - The address of the token being sold
315    /// * `buy_token` - The address of the token being bought
316    ///
317    /// # Returns
318    /// * `Ok((BigUint, BigUint))` - A tuple containing:
319    ///   - First element: The maximum input amount (sell_token)
320    ///   - Second element: The maximum output amount (buy_token)
321    ///
322    /// For `let res = get_limits(...)`, the valid input domain for `get_amount_out` is `[0,
323    /// res.0]`.
324    ///
325    /// * `Err(SimulationError)` - If any unexpected error occurs
326    fn get_limits(
327        &self,
328        sell_token: Bytes,
329        buy_token: Bytes,
330    ) -> Result<(BigUint, BigUint), SimulationError>;
331
332    /// Decodes and applies a protocol state delta to the state
333    ///
334    /// Will error if the provided delta is missing any required attributes or if any of the
335    /// attribute values cannot be decoded.
336    ///
337    /// # Arguments
338    ///
339    /// * `delta` - A `ProtocolStateDelta` from the tycho indexer
340    ///
341    /// # Returns
342    ///
343    /// * `Result<(), TransitionError<String>>` - A `Result` containing `()` on success or a
344    ///   `TransitionError` on failure.
345    fn delta_transition(
346        &mut self,
347        delta: ProtocolStateDelta,
348        tokens: &HashMap<Bytes, Token>,
349        balances: &Balances,
350    ) -> Result<(), TransitionError<String>>;
351
352    /// Calculates the swap volume required to achieve the provided goal when trading against this
353    /// pool.
354    ///
355    /// This method will branch towards different behaviors based on [SwapConstraint] enum. Please
356    /// refer to its documentation for further details on each behavior.
357    ///
358    /// In short, the current two options are:
359    /// - Maximize your trade while respecting a trade limit price:
360    ///   [SwapConstraint::TradeLimitPrice]
361    /// - Move the pool price to a target price: [SwapConstraint::PoolTargetPrice]
362    ///
363    /// # Arguments
364    ///
365    /// * `params` - A [QueryPoolSwapParams] struct containing the inputs for this method.
366    ///
367    /// # Returns
368    ///
369    /// * `Ok(PoolSwap)` - A `PoolSwap` struct containing the amounts to be traded and the state of
370    ///   the pool after trading.
371    /// * `Err(SimulationError)` - If:
372    ///   - The calculation encounters numerical issues
373    ///   - The method is not implemented for this protocol
374    #[allow(unused)]
375    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
376        Err(SimulationError::FatalError("query_pool_swap not implemented".into()))
377    }
378
379    /// Clones the protocol state as a trait object.
380    /// This allows the state to be cloned when it is being used as a `Box<dyn ProtocolSim>`.
381    fn clone_box(&self) -> Box<dyn ProtocolSim>;
382
383    /// Allows downcasting of the trait object to its underlying type.
384    fn as_any(&self) -> &dyn Any;
385
386    /// Allows downcasting of the trait object to its mutable underlying type.
387    fn as_any_mut(&mut self) -> &mut dyn Any;
388
389    /// Compares two protocol states for equality.
390    /// This method must be implemented to define how two protocol states are considered equal
391    /// (used for tests).
392    fn eq(&self, other: &dyn ProtocolSim) -> bool;
393
394    /// Cast as IndicativelyPriced. This is necessary for RFQ protocols
395    fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
396        Err(SimulationError::FatalError("Pool State does not implement IndicativelyPriced".into()))
397    }
398}
399
400impl Clone for Box<dyn ProtocolSim> {
401    fn clone(&self) -> Box<dyn ProtocolSim> {
402        self.clone_box()
403    }
404}
405
406#[cfg(test)]
407mod tests {
408
409    use super::*;
410
411    #[test]
412    fn serde() {
413        use serde::{Deserialize, Serialize};
414
415        #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
416        struct DummyProtocol {
417            reserve_0: u64,
418            reserve_1: u64,
419        }
420
421        #[typetag::serde]
422        impl ProtocolSim for DummyProtocol {
423            fn clone_box(&self) -> Box<dyn ProtocolSim> {
424                todo!()
425            }
426
427            fn as_any(&self) -> &dyn Any {
428                self
429            }
430
431            fn as_any_mut(&mut self) -> &mut dyn Any {
432                todo!()
433            }
434
435            fn eq(&self, other: &dyn ProtocolSim) -> bool {
436                if let Some(other) = other.as_any().downcast_ref::<Self>() {
437                    self.reserve_0 == other.reserve_0 && self.reserve_1 == other.reserve_1
438                } else {
439                    false
440                }
441            }
442
443            fn fee(&self) -> f64 {
444                todo!()
445            }
446            fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
447                todo!()
448            }
449            fn get_amount_out(
450                &self,
451                _amount_in: BigUint,
452                _token_in: &Token,
453                _token_out: &Token,
454            ) -> Result<GetAmountOutResult, SimulationError> {
455                todo!()
456            }
457            fn get_limits(
458                &self,
459                _sell_token: Bytes,
460                _buy_token: Bytes,
461            ) -> Result<(BigUint, BigUint), SimulationError> {
462                todo!()
463            }
464            fn delta_transition(
465                &mut self,
466                _delta: ProtocolStateDelta,
467                _tokens: &HashMap<Bytes, Token>,
468                _balances: &Balances,
469            ) -> Result<(), TransitionError<String>> {
470                todo!()
471            }
472        }
473
474        let state = DummyProtocol { reserve_0: 1, reserve_1: 2 };
475
476        assert_eq!(serde_json::to_string(&state).unwrap(), r#"{"reserve_0":1,"reserve_1":2}"#);
477        assert_eq!(
478            serde_json::to_string(&state as &dyn ProtocolSim).unwrap(),
479            r#"{"protocol":"DummyProtocol","state":{"reserve_0":1,"reserve_1":2}}"#
480        );
481
482        let deserialized: Box<dyn ProtocolSim> = serde_json::from_str(
483            r#"{"protocol":"DummyProtocol","state":{"reserve_0":1,"reserve_1":2}}"#,
484        )
485        .unwrap();
486
487        assert!(deserialized.eq(&state));
488    }
489}