nautilus_model/defi/
dex.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{borrow::Cow, fmt::Display, str::FromStr, sync::Arc};
17
18use alloy_primitives::{Address, keccak256};
19use serde::{Deserialize, Serialize};
20use strum::{Display, EnumIter, EnumString};
21
22use crate::{
23    defi::{amm::Pool, chain::Chain, validation::validate_address},
24    identifiers::{InstrumentId, Symbol, Venue},
25    instruments::{Instrument, any::InstrumentAny, currency_pair::CurrencyPair},
26    types::{currency::Currency, fixed::FIXED_PRECISION, price::Price, quantity::Quantity},
27};
28
29/// Represents different types of Automated Market Makers (AMMs) in DeFi protocols.
30#[derive(
31    Debug,
32    Clone,
33    Copy,
34    PartialEq,
35    Serialize,
36    Deserialize,
37    strum::EnumString,
38    strum::Display,
39    strum::EnumIter,
40)]
41#[cfg_attr(feature = "python", pyo3::pyclass(module = "nautilus_trader.model"))]
42#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
43#[non_exhaustive]
44pub enum AmmType {
45    /// Constant Product Automated Market Maker.
46    CPAMM,
47    /// Concentrated Liquidity Automated Market Maker.
48    CLAMM,
49    /// Concentrated liquidity AMM **with hooks** (e.g. upcoming Uniswap v4).
50    CLAMEnhanced,
51    /// Specialized Constant-Sum AMM for low-volatility assets (Curve-style “StableSwap”).
52    StableSwap,
53    /// AMM with customizable token weights (e.g., Balancer style).
54    WeightedPool,
55    /// Advanced pool type that can nest other pools (Balancer V3).
56    ComposablePool,
57}
58
59/// Represents different types of decentralized exchanges (DEXes) supported by Nautilus.
60#[derive(
61    Debug,
62    Clone,
63    Copy,
64    Hash,
65    PartialOrd,
66    PartialEq,
67    Ord,
68    Eq,
69    Display,
70    EnumIter,
71    EnumString,
72    Serialize,
73    Deserialize,
74)]
75#[cfg_attr(feature = "python", pyo3::pyclass(module = "nautilus_trader.model"))]
76#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
77pub enum DexType {
78    AerodromeSlipstream,
79    AerodromeV1,
80    BalancerV2,
81    BalancerV3,
82    BaseSwapV2,
83    BaseX,
84    CamelotV3,
85    CurveFinance,
86    FluidDEX,
87    MaverickV1,
88    MaverickV2,
89    PancakeSwapV3,
90    SushiSwapV2,
91    SushiSwapV3,
92    UniswapV2,
93    UniswapV3,
94    UniswapV4,
95}
96
97impl DexType {
98    /// Returns a reference to the `DexType` corresponding to the given dex name, or `None` if it is not found.
99    pub fn from_dex_name(dex_name: &str) -> Option<DexType> {
100        DexType::from_str(dex_name).ok()
101    }
102}
103
104/// Represents a decentralized exchange (DEX) in a blockchain ecosystem.
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106#[cfg_attr(feature = "python", pyo3::pyclass(module = "nautilus_trader.model"))]
107#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
108pub struct Dex {
109    /// The blockchain network where this DEX operates.
110    pub chain: Chain,
111    /// The variant of the DEX protocol.
112    pub name: DexType,
113    /// The blockchain address of the DEX factory contract.
114    pub factory: Address,
115    /// The block number at which the DEX factory contract was deployed.
116    pub factory_creation_block: u64,
117    /// The event signature or identifier used to detect pool creation events.
118    pub pool_created_event: Cow<'static, str>,
119    // Optional Initialize event signature emitted when pool is initialized.
120    pub initialize_event: Option<Cow<'static, str>>,
121    /// The event signature or identifier used to detect swap events.
122    pub swap_created_event: Cow<'static, str>,
123    /// The event signature or identifier used to detect mint events.
124    pub mint_created_event: Cow<'static, str>,
125    /// The event signature or identifier used to detect burn events.
126    pub burn_created_event: Cow<'static, str>,
127    /// The event signature or identifier used to detect collect fee events.
128    pub collect_created_event: Cow<'static, str>,
129    // Optional Flash event signature emitted when flash loan occurs.
130    pub flash_created_event: Option<Cow<'static, str>>,
131    /// The type of automated market maker (AMM) algorithm used by this DEX.
132    pub amm_type: AmmType,
133    /// Collection of liquidity pools managed by this DEX.
134    #[allow(dead_code, reason = "TBD")]
135    pairs: Vec<Pool>,
136}
137
138/// A thread-safe shared pointer to a `Dex`, enabling efficient reuse across multiple components.
139pub type SharedDex = Arc<Dex>;
140
141impl Dex {
142    /// Creates a new [`Dex`] instance with the specified properties.
143    ///
144    /// # Panics
145    ///
146    /// Panics if the provided factory address is invalid.
147    #[must_use]
148    #[allow(clippy::too_many_arguments)]
149    pub fn new(
150        chain: Chain,
151        name: DexType,
152        factory: &str,
153        factory_creation_block: u64,
154        amm_type: AmmType,
155        pool_created_event: &str,
156        swap_event: &str,
157        mint_event: &str,
158        burn_event: &str,
159        collect_event: &str,
160    ) -> Self {
161        let pool_created_event_hash = keccak256(pool_created_event.as_bytes());
162        let encoded_pool_created_event = format!(
163            "0x{encoded_hash}",
164            encoded_hash = hex::encode(pool_created_event_hash)
165        );
166        let swap_event_hash: alloy_primitives::FixedBytes<32> = keccak256(swap_event.as_bytes());
167        let encoded_swap_event = format!(
168            "0x{encoded_hash}",
169            encoded_hash = hex::encode(swap_event_hash)
170        );
171        let mint_event_hash = keccak256(mint_event.as_bytes());
172        let encoded_mint_event = format!(
173            "0x{encoded_hash}",
174            encoded_hash = hex::encode(mint_event_hash)
175        );
176        let burn_event_hash = keccak256(burn_event.as_bytes());
177        let encoded_burn_event = format!(
178            "0x{encoded_hash}",
179            encoded_hash = hex::encode(burn_event_hash)
180        );
181        let collect_event_hash = keccak256(collect_event.as_bytes());
182        let encoded_collect_event = format!(
183            "0x{encoded_hash}",
184            encoded_hash = hex::encode(collect_event_hash)
185        );
186        let factory_address = validate_address(factory).unwrap();
187        Self {
188            chain,
189            name,
190            factory: factory_address,
191            factory_creation_block,
192            pool_created_event: encoded_pool_created_event.into(),
193            initialize_event: None,
194            swap_created_event: encoded_swap_event.into(),
195            mint_created_event: encoded_mint_event.into(),
196            burn_created_event: encoded_burn_event.into(),
197            collect_created_event: encoded_collect_event.into(),
198            flash_created_event: None,
199            amm_type,
200            pairs: vec![],
201        }
202    }
203
204    /// Returns a unique identifier for this DEX, combining chain and protocol name.
205    pub fn id(&self) -> String {
206        format!("{}:{}", self.chain.name, self.name)
207    }
208
209    /// Sets the pool initialization event signature by hashing and encoding the provided event string.
210    pub fn set_initialize_event(&mut self, event: &str) {
211        let initialize_event_hash = keccak256(event.as_bytes());
212        let encoded_initialized_event = format!(
213            "0x{encoded_hash}",
214            encoded_hash = hex::encode(initialize_event_hash)
215        );
216        self.initialize_event = Some(encoded_initialized_event.into());
217    }
218
219    /// Sets the flash loan event signature by hashing and encoding the provided event string.
220    pub fn set_flash_event(&mut self, event: &str) {
221        let flash_event_hash = keccak256(event.as_bytes());
222        let encoded_flash_event = format!(
223            "0x{encoded_hash}",
224            encoded_hash = hex::encode(flash_event_hash)
225        );
226        self.flash_created_event = Some(encoded_flash_event.into());
227    }
228}
229
230impl Display for Dex {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        write!(f, "Dex(chain={}, name={})", self.chain, self.name)
233    }
234}
235
236impl From<Pool> for CurrencyPair {
237    fn from(p: Pool) -> Self {
238        let symbol = Symbol::from(format!("{}/{}", p.token0.symbol, p.token1.symbol));
239        let id = InstrumentId::new(symbol, Venue::from(p.dex.id()));
240
241        let size_precision = p.token0.decimals.min(FIXED_PRECISION);
242        let price_precision = p.token1.decimals.min(FIXED_PRECISION);
243
244        let price_increment = Price::new(10f64.powi(-(price_precision as i32)), price_precision);
245        let size_increment = Quantity::new(10f64.powi(-(size_precision as i32)), size_precision);
246
247        CurrencyPair::new(
248            id,
249            symbol,
250            Currency::from(p.token0.symbol.as_str()),
251            Currency::from(p.token1.symbol.as_str()),
252            price_precision,
253            size_precision,
254            price_increment,
255            size_increment,
256            None,
257            None,
258            None,
259            None,
260            None,
261            None,
262            None,
263            None,
264            None,
265            None,
266            None,
267            None,
268            0.into(),
269            0.into(),
270        )
271    }
272}
273
274impl From<Pool> for InstrumentAny {
275    fn from(p: Pool) -> Self {
276        CurrencyPair::from(p).into_any()
277    }
278}
279
280////////////////////////////////////////////////////////////////////////////////
281// Tests
282////////////////////////////////////////////////////////////////////////////////
283
284#[cfg(test)]
285mod tests {
286    use rstest::rstest;
287
288    use super::DexType;
289
290    #[rstest]
291    fn test_dex_type_from_dex_name_valid() {
292        // Test some known DEX names
293        assert!(DexType::from_dex_name("UniswapV3").is_some());
294        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
295        assert!(DexType::from_dex_name("BalancerV2").is_some());
296        assert!(DexType::from_dex_name("CamelotV3").is_some());
297
298        // Verify specific DEX type
299        let uniswap_v3 = DexType::from_dex_name("UniswapV3").unwrap();
300        assert_eq!(uniswap_v3, DexType::UniswapV3);
301
302        // Verify compound names
303        let aerodrome_slipstream = DexType::from_dex_name("AerodromeSlipstream").unwrap();
304        assert_eq!(aerodrome_slipstream, DexType::AerodromeSlipstream);
305
306        // Verify specialized names
307        let fluid_dex = DexType::from_dex_name("FluidDEX").unwrap();
308        assert_eq!(fluid_dex, DexType::FluidDEX);
309    }
310
311    #[rstest]
312    fn test_dex_type_from_dex_name_invalid() {
313        // Test unknown DEX names
314        assert!(DexType::from_dex_name("InvalidDEX").is_none());
315        assert!(DexType::from_dex_name("").is_none());
316        assert!(DexType::from_dex_name("NonExistentDEX").is_none());
317    }
318
319    #[rstest]
320    fn test_dex_type_from_dex_name_case_sensitive() {
321        // Test case sensitivity - should be case sensitive
322        assert!(DexType::from_dex_name("UniswapV3").is_some());
323        assert!(DexType::from_dex_name("uniswapv3").is_none()); // lowercase
324        assert!(DexType::from_dex_name("UNISWAPV3").is_none()); // uppercase
325        assert!(DexType::from_dex_name("UniSwapV3").is_none()); // mixed case
326
327        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
328        assert!(DexType::from_dex_name("sushiswapv2").is_none()); // lowercase
329    }
330
331    #[rstest]
332    fn test_dex_type_all_variants_mappable() {
333        // Test that all DEX variants can be mapped from their string representation
334        let all_dex_names = vec![
335            "AerodromeSlipstream",
336            "AerodromeV1",
337            "BalancerV2",
338            "BalancerV3",
339            "BaseSwapV2",
340            "BaseX",
341            "CamelotV3",
342            "CurveFinance",
343            "FluidDEX",
344            "MaverickV1",
345            "MaverickV2",
346            "PancakeSwapV3",
347            "SushiSwapV2",
348            "SushiSwapV3",
349            "UniswapV2",
350            "UniswapV3",
351            "UniswapV4",
352        ];
353
354        for dex_name in all_dex_names {
355            assert!(
356                DexType::from_dex_name(dex_name).is_some(),
357                "DEX name '{dex_name}' should be valid but was not found",
358            );
359        }
360    }
361
362    #[rstest]
363    fn test_dex_type_display() {
364        // Test that DexType variants display correctly (using strum::Display)
365        assert_eq!(DexType::UniswapV3.to_string(), "UniswapV3");
366        assert_eq!(DexType::SushiSwapV2.to_string(), "SushiSwapV2");
367        assert_eq!(
368            DexType::AerodromeSlipstream.to_string(),
369            "AerodromeSlipstream"
370        );
371        assert_eq!(DexType::FluidDEX.to_string(), "FluidDEX");
372    }
373}