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}