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), including token decimals
61/// * `denominator` - The amount of token_in (what you pay), including token decimals
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/// Represents a trade between two tokens at a given price on a pool.
110#[derive(Debug, Clone)]
111pub struct PoolSwap {
112    /// The amount of token_in sold to the pool
113    amount_in: BigUint,
114    /// The amount of token_out bought from the pool
115    amount_out: BigUint,
116    /// The new state of the pool after the swap
117    new_state: Box<dyn ProtocolSim>,
118    /// Optional price points that the pool was transitioned through while computing this swap.
119    /// The values are tuples of (amount_in, amount_out, price). This is useful for repeated calls
120    /// by providing good bounds for the next call.
121    price_points: Option<Vec<(BigUint, BigUint, f64)>>,
122}
123
124impl PoolSwap {
125    pub fn new(
126        amount_in: BigUint,
127        amount_out: BigUint,
128        new_state: Box<dyn ProtocolSim>,
129        price_points: Option<Vec<(BigUint, BigUint, f64)>>,
130    ) -> Self {
131        Self { amount_in, amount_out, new_state, price_points }
132    }
133
134    pub fn amount_in(&self) -> &BigUint {
135        &self.amount_in
136    }
137
138    pub fn amount_out(&self) -> &BigUint {
139        &self.amount_out
140    }
141
142    pub fn new_state(&self) -> &dyn ProtocolSim {
143        self.new_state.as_ref()
144    }
145
146    pub fn price_points(&self) -> &Option<Vec<(BigUint, BigUint, f64)>> {
147        &self.price_points
148    }
149}
150
151/// Options on how to constrain the pool swap query
152#[derive(Debug, Clone, PartialEq)]
153pub enum SwapConstraint {
154    /// This mode will calculate the maximum trade that this pool can execute while respecting a
155    /// trade limit price.
156    TradeLimitPrice {
157        /// The minimum acceptable price for the resulting trade, as a [Price] struct. The
158        /// resulting amount_out / amount_in must be >= trade_limit_price
159        limit: Price,
160        /// The tolerance as a fraction to be applied on top of (increasing) the trade
161        /// limit price, raising the acceptance threshold. This is used to loosen the acceptance
162        /// criteria for implementations of this method, but will never allow violating the trade
163        /// limit price itself.
164        tolerance: f64,
165        /// The minimum amount of token_in that must be used for this trade.
166        min_amount_in: Option<BigUint>,
167        /// The maximum amount of token_in that can be used for this trade.
168        max_amount_in: Option<BigUint>,
169    },
170
171    /// This mode will Calculate the amount of token_in required to move the pool's marginal price
172    /// down to a target price, and the amount of token_out received.
173    ///
174    /// # Edge Cases and Limitations
175    ///
176    /// Computing the exact amount to move a pool's marginal price to a target has several
177    /// challenges:
178    /// - The definition of marginal price varies between protocols. It is usually not an attribute
179    ///   of the pool but a consequence of its liquidity distribution and current state.
180    /// - For protocols with concentrated liquidity, the marginal price is discrete, meaning we
181    ///   can't always find an exact trade amount to reach the target price.
182    /// - Not all protocols support analytical solutions for this problem, requiring numerical
183    ///   methods.
184    PoolTargetPrice {
185        /// The marginal price we want the pool to be after the trade, as a [Price] struct. The
186        /// pool's price will move down to this level as token_in is sold into it
187        target: Price,
188        /// The tolerance as a fraction of the resulting pool marginal price. After trading, the
189        /// pool's  price will decrease to the interval `[target, target * (1 +
190        /// tolerance)]`.
191        tolerance: f64,
192        /// The lower bound for searching algorithms.
193        min_amount_in: Option<BigUint>,
194        /// The upper bound for searching algorithms.
195        max_amount_in: Option<BigUint>,
196    },
197}
198
199/// Represents the parameters for query_max_trade.
200///
201/// # Fields
202///
203/// * `token_in` - The token being sold (swapped into the pool)
204/// * `token_out` - The token being bought (swapped out of the pool)
205/// * `swap_constraint` - Type of price constraint to be applied. See [SwapConstraint].
206#[derive(Debug, Clone, PartialEq)]
207pub struct QueryPoolSwapParams {
208    token_in: Token,
209    token_out: Token,
210    swap_constraint: SwapConstraint,
211}
212
213impl QueryPoolSwapParams {
214    pub fn new(token_in: Token, token_out: Token, swap_constraint: SwapConstraint) -> Self {
215        Self { token_in, token_out, swap_constraint }
216    }
217
218    /// Returns a reference to the input token (token being sold into the pool)
219    pub fn token_in(&self) -> &Token {
220        &self.token_in
221    }
222
223    /// Returns a reference to the output token (token being bought out of the pool)
224    pub fn token_out(&self) -> &Token {
225        &self.token_out
226    }
227
228    /// Returns a reference to the price constraint
229    pub fn swap_constraint(&self) -> &SwapConstraint {
230        &self.swap_constraint
231    }
232}
233
234/// ProtocolSim trait
235/// This trait defines the methods that a protocol state must implement in order to be used
236/// in the trade simulation.
237pub trait ProtocolSim: fmt::Debug + Send + Sync + 'static {
238    /// Returns the fee of the protocol as ratio
239    ///
240    /// E.g. if the fee is 1%, the value returned would be 0.01.
241    fn fee(&self) -> f64;
242
243    /// Returns the protocol's current spot price of two tokens
244    ///
245    /// Currency pairs are meant to be compared against one another in
246    /// order to understand how much of the quote currency is required
247    /// to buy one unit of the base currency.
248    ///
249    /// E.g. if ETH/USD is trading at 1000, we need 1000 USD (quote)
250    /// to buy 1 ETH (base currency).
251    ///
252    /// # Arguments
253    ///
254    /// * `a` - Base Token: refers to the token that is the quantity of a pair. For the pair
255    ///   BTC/USDT, BTC would be the base asset.
256    /// * `b` - Quote Token: refers to the token that is the price of a pair. For the symbol
257    ///   BTC/USDT, USDT would be the quote asset.
258    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
259
260    /// Returns the amount out given an amount in and input/output tokens.
261    ///
262    /// # Arguments
263    ///
264    /// * `amount_in` - The amount in of the input token.
265    /// * `token_in` - The input token ERC20 token.
266    /// * `token_out` - The output token ERC20 token.
267    ///
268    /// # Returns
269    ///
270    /// A `Result` containing a `GetAmountOutResult` struct on success or a
271    ///  `SimulationError` on failure.
272    fn get_amount_out(
273        &self,
274        amount_in: BigUint,
275        token_in: &Token,
276        token_out: &Token,
277    ) -> Result<GetAmountOutResult, SimulationError>;
278
279    /// Computes the maximum amount that can be traded between two tokens.
280    ///
281    /// This function calculates the maximum possible trade amount between two tokens,
282    /// taking into account the protocol's specific constraints and mechanics.
283    /// The implementation details vary by protocol - for example:
284    /// - For constant product AMMs (like Uniswap V2), this is based on available reserves
285    /// - For concentrated liquidity AMMs (like Uniswap V3), this considers liquidity across tick
286    ///   ranges
287    ///
288    /// Note: if there are no limits, the returned amount will be a "soft" limit,
289    ///       meaning that the actual amount traded could be higher but it's advised to not
290    ///       exceed it.
291    ///
292    /// # Arguments
293    /// * `sell_token` - The address of the token being sold
294    /// * `buy_token` - The address of the token being bought
295    ///
296    /// # Returns
297    /// * `Ok((Option<BigUint>, Option<BigUint>))` - A tuple containing:
298    ///   - First element: The maximum input amount
299    ///   - Second element: The maximum output amount
300    ///
301    /// This means that for `let res = get_limits(...)` the amount input domain for `get_amount_out`
302    /// would be `[0, res.0]` and the amount input domain for `get_amount_in` would be `[0,
303    /// res.1]`
304    ///
305    /// * `Err(SimulationError)` - If any unexpected error occurs
306    fn get_limits(
307        &self,
308        sell_token: Bytes,
309        buy_token: Bytes,
310    ) -> Result<(BigUint, BigUint), SimulationError>;
311
312    /// Decodes and applies a protocol state delta to the state
313    ///
314    /// Will error if the provided delta is missing any required attributes or if any of the
315    /// attribute values cannot be decoded.
316    ///
317    /// # Arguments
318    ///
319    /// * `delta` - A `ProtocolStateDelta` from the tycho indexer
320    ///
321    /// # Returns
322    ///
323    /// * `Result<(), TransitionError<String>>` - A `Result` containing `()` on success or a
324    ///   `TransitionError` on failure.
325    fn delta_transition(
326        &mut self,
327        delta: ProtocolStateDelta,
328        tokens: &HashMap<Bytes, Token>,
329        balances: &Balances,
330    ) -> Result<(), TransitionError<String>>;
331
332    /// Calculates the swap volume required to achieve the provided goal when trading against this
333    /// pool.
334    ///
335    /// This method will branch towards different behaviors based on [SwapConstraint] enum. Please
336    /// refer to its documentation for further details on each behavior.
337    ///
338    /// In short, the current two options are:
339    /// - Maximize your trade while respecting a trade limit price:
340    ///   [SwapConstraint::TradeLimitPrice]
341    /// - Move the pool price to a target price: [SwapConstraint::PoolTargetPrice]
342    ///
343    /// # Arguments
344    ///
345    /// * `params` - A [QueryPoolSwapParams] struct containing the inputs for this method.
346    ///
347    /// # Returns
348    ///
349    /// * `Ok(Trade)` - A `Trade` struct containing the amounts to be traded and the state of the
350    ///   pool after trading.
351    /// * `Err(SimulationError)` - If:
352    ///   - The calculation encounters numerical issues
353    ///   - The method is not implemented for this protocol
354    #[allow(unused)]
355    fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
356        Err(SimulationError::FatalError("query_swap_size not implemented".into()))
357    }
358
359    /// Clones the protocol state as a trait object.
360    /// This allows the state to be cloned when it is being used as a `Box<dyn ProtocolSim>`.
361    fn clone_box(&self) -> Box<dyn ProtocolSim>;
362
363    /// Allows downcasting of the trait object to its underlying type.
364    fn as_any(&self) -> &dyn Any;
365
366    /// Allows downcasting of the trait object to its mutable underlying type.
367    fn as_any_mut(&mut self) -> &mut dyn Any;
368
369    /// Compares two protocol states for equality.
370    /// This method must be implemented to define how two protocol states are considered equal
371    /// (used for tests).
372    fn eq(&self, other: &dyn ProtocolSim) -> bool;
373
374    /// Cast as IndicativelyPriced. This is necessary for RFQ protocols
375    fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
376        Err(SimulationError::FatalError("Pool State does not implement IndicativelyPriced".into()))
377    }
378}
379
380impl Clone for Box<dyn ProtocolSim> {
381    fn clone(&self) -> Box<dyn ProtocolSim> {
382        self.clone_box()
383    }
384}