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/// In the context of `swap_to_price` and `query_supply`, this represents the pool's price in
64/// the **token_out/token_in** direction
65///
66/// A fraction struct is used for price to have flexibility in precision independent of the
67/// decimal precisions of the numerator and denominator tokens. This allows for:
68/// - Exact price representation without floating-point errors
69/// - Handling tokens with different decimal places without loss of precision
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Price {
72    pub numerator: BigUint,
73    pub denominator: BigUint,
74}
75
76impl Price {
77    pub fn new(numerator: BigUint, denominator: BigUint) -> Self {
78        Self { numerator, denominator }
79    }
80}
81
82/// Represents a trade between two tokens at a given price on a pool.
83///
84/// # Fields
85///
86/// * `amount_in` - The amount of token_in (what you pay)
87/// * `amount_out` - The amount of token_out (what you receive)
88///
89/// The price of the trade is the ratio of amount_out to amount_in, i.e. amount_out / amount_in.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Trade {
92    pub amount_in: BigUint,
93    pub amount_out: BigUint,
94}
95
96impl Trade {
97    pub fn new(amount_in: BigUint, amount_out: BigUint) -> Self {
98        Self { amount_in, amount_out }
99    }
100}
101
102/// ProtocolSim trait
103/// This trait defines the methods that a protocol state must implement in order to be used
104/// in the trade simulation.
105pub trait ProtocolSim: fmt::Debug + Send + Sync + 'static {
106    /// Returns the fee of the protocol as ratio
107    ///
108    /// E.g. if the fee is 1%, the value returned would be 0.01.
109    fn fee(&self) -> f64;
110
111    /// Returns the protocol's current spot price of two tokens
112    ///
113    /// Currency pairs are meant to be compared against one another in
114    /// order to understand how much of the quote currency is required
115    /// to buy one unit of the base currency.
116    ///
117    /// E.g. if ETH/USD is trading at 1000, we need 1000 USD (quote)
118    /// to buy 1 ETH (base currency).
119    ///
120    /// # Arguments
121    ///
122    /// * `a` - Base Token: refers to the token that is the quantity of a pair. For the pair
123    ///   BTC/USDT, BTC would be the base asset.
124    /// * `b` - Quote Token: refers to the token that is the price of a pair. For the symbol
125    ///   BTC/USDT, USDT would be the quote asset.
126    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
127
128    /// Returns the amount out given an amount in and input/output tokens.
129    ///
130    /// # Arguments
131    ///
132    /// * `amount_in` - The amount in of the input token.
133    /// * `token_in` - The input token ERC20 token.
134    /// * `token_out` - The output token ERC20 token.
135    ///
136    /// # Returns
137    ///
138    /// A `Result` containing a `GetAmountOutResult` struct on success or a
139    ///  `SimulationError` on failure.
140    fn get_amount_out(
141        &self,
142        amount_in: BigUint,
143        token_in: &Token,
144        token_out: &Token,
145    ) -> Result<GetAmountOutResult, SimulationError>;
146
147    /// Computes the maximum amount that can be traded between two tokens.
148    ///
149    /// This function calculates the maximum possible trade amount between two tokens,
150    /// taking into account the protocol's specific constraints and mechanics.
151    /// The implementation details vary by protocol - for example:
152    /// - For constant product AMMs (like Uniswap V2), this is based on available reserves
153    /// - For concentrated liquidity AMMs (like Uniswap V3), this considers liquidity across tick
154    ///   ranges
155    ///
156    /// Note: if there are no limits, the returned amount will be a "soft" limit,
157    ///       meaning that the actual amount traded could be higher but it's advised to not
158    ///       exceed it.
159    ///
160    /// # Arguments
161    /// * `sell_token` - The address of the token being sold
162    /// * `buy_token` - The address of the token being bought
163    ///
164    /// # Returns
165    /// * `Ok((Option<BigUint>, Option<BigUint>))` - A tuple containing:
166    ///   - First element: The maximum input amount
167    ///   - Second element: The maximum output amount
168    ///
169    /// This means that for `let res = get_limits(...)` the amount input domain for `get_amount_out`
170    /// would be `[0, res.0]` and the amount input domain for `get_amount_in` would be `[0,
171    /// res.1]`
172    ///
173    /// * `Err(SimulationError)` - If any unexpected error occurs
174    fn get_limits(
175        &self,
176        sell_token: Bytes,
177        buy_token: Bytes,
178    ) -> Result<(BigUint, BigUint), SimulationError>;
179
180    /// Decodes and applies a protocol state delta to the state
181    ///
182    /// Will error if the provided delta is missing any required attributes or if any of the
183    /// attribute values cannot be decoded.
184    ///
185    /// # Arguments
186    ///
187    /// * `delta` - A `ProtocolStateDelta` from the tycho indexer
188    ///
189    /// # Returns
190    ///
191    /// * `Result<(), TransitionError<String>>` - A `Result` containing `()` on success or a
192    ///   `TransitionError` on failure.
193    fn delta_transition(
194        &mut self,
195        delta: ProtocolStateDelta,
196        tokens: &HashMap<Bytes, Token>,
197        balances: &Balances,
198    ) -> Result<(), TransitionError<String>>;
199
200    /// Calculates the amount of token_in required to move the pool's marginal price down to
201    /// a target price, and the amount of token_out received.
202    ///
203    /// # Arguments
204    ///
205    /// * `token_in` - The address of the token being sold (swapped into the pool)
206    /// * `token_out` - The address of the token being bought (swapped out of the pool)
207    /// * `target_price` - The target marginal price as a `Price` struct representing **token_out
208    ///   per token_in** (token_out/token_in) net of all fees:
209    ///   - `numerator`: Amount of token_out (what the pool offers)
210    ///   - `denominator`: Amount of token_in (what the pool wants)
211    ///   - The pool's price will move **down** to this level as token_in is sold into it
212    ///
213    /// # Returns
214    ///
215    /// * `Ok(Trade)` - A `Trade` struct containing the amount that needs to be swapped on the pool
216    ///   to move its price to target_price.
217    /// * `Err(SimulationError)` - If:
218    ///   - The calculation encounters numerical issues (overflow, division by zero, etc.)
219    ///   - The method is not implemented for this protocol
220    ///
221    /// # Edge Cases and Limitations
222    ///
223    /// ## Exact Price Achievement
224    ///
225    /// It is almost never possible to achieve the target price exactly, only within some margin
226    /// of tolerance. This is due to:
227    /// - **Discrete liquidity**: For concentrated liquidity protocols (e.g., Uniswap V3), liquidity
228    ///   is distributed across discrete price ticks, making exact price targeting impossible
229    /// - **Numerical precision**: Integer arithmetic and rounding may prevent exact price matching
230    /// - **Protocol constraints**: Some protocols have minimum trade sizes or other constraints
231    ///
232    /// ## Unreachable Prices
233    ///
234    /// If the target price is already below the current spot price (i.e., the price would need to
235    /// move in the wrong direction), implementations typically return a zero trade (`Trade` with
236    /// `amount_in = 0` and `amount_out = 0`).
237    #[allow(unused)]
238    fn swap_to_price(
239        &self,
240        token_in: &Bytes,
241        token_out: &Bytes,
242        target_price: Price,
243    ) -> Result<Trade, SimulationError> {
244        Err(SimulationError::FatalError("swap_to_price not implemented".into()))
245    }
246
247    /// Calculates the maximum amount of token_out (sell token) a pool can supply, and the
248    /// corresponding demanded amount of token_in (buy token), while respecting a minimum trade
249    /// price.
250    ///
251    /// # Arguments
252    ///
253    /// * `token_in` - The address of the token being bought by the pool (the buy token)
254    /// * `token_out` - The address of the token being sold by the pool (the sell token)
255    /// * `target_price` - The minimum acceptable price as a `Price` struct representing **token_out
256    ///   per token_in** (token_out/token_in) net of all fees:
257    ///   - `numerator`: Amount of token_out (what the pool offers)
258    ///   - `denominator`: Amount of token_in (what the pool wants)
259    ///   - The pool will supply token_out down to this price level
260    ///
261    /// # Returns
262    ///
263    /// * `Ok(Trade)` - A `Trade` struct containing the largest trade that can be executed on this
264    ///   pool while respecting the provided trace price
265    /// * `Err(SimulationError)` - If:
266    ///   - The calculation encounters numerical issues
267    ///   - The method is not implemented for this protocol
268    #[allow(unused)]
269    fn query_supply(
270        &self,
271        token_in: &Bytes,
272        token_out: &Bytes,
273        target_price: Price,
274    ) -> Result<Trade, SimulationError> {
275        Err(SimulationError::FatalError("query_supply not implemented".into()))
276    }
277
278    /// Clones the protocol state as a trait object.
279    /// This allows the state to be cloned when it is being used as a `Box<dyn ProtocolSim>`.
280    fn clone_box(&self) -> Box<dyn ProtocolSim>;
281
282    /// Allows downcasting of the trait object to its underlying type.
283    fn as_any(&self) -> &dyn Any;
284
285    /// Allows downcasting of the trait object to its mutable underlying type.
286    fn as_any_mut(&mut self) -> &mut dyn Any;
287
288    /// Compares two protocol states for equality.
289    /// This method must be implemented to define how two protocol states are considered equal
290    /// (used for tests).
291    fn eq(&self, other: &dyn ProtocolSim) -> bool;
292
293    /// Cast as IndicativelyPriced. This is necessary for RFQ protocols
294    fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
295        Err(SimulationError::FatalError("Pool State does not implement IndicativelyPriced".into()))
296    }
297}
298
299impl Clone for Box<dyn ProtocolSim> {
300    fn clone(&self) -> Box<dyn ProtocolSim> {
301        self.clone_box()
302    }
303}