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<Self> {
100        Self::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)]
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 = match validate_address(factory) {
187            Ok(address) => address,
188            Err(e) => panic!(
189                "Invalid factory address for DEX {name} on chain {chain} for factory address {factory}: {e}"
190            ),
191        };
192        Self {
193            chain,
194            name,
195            factory: factory_address,
196            factory_creation_block,
197            pool_created_event: encoded_pool_created_event.into(),
198            initialize_event: None,
199            swap_created_event: encoded_swap_event.into(),
200            mint_created_event: encoded_mint_event.into(),
201            burn_created_event: encoded_burn_event.into(),
202            collect_created_event: encoded_collect_event.into(),
203            flash_created_event: None,
204            amm_type,
205            pairs: vec![],
206        }
207    }
208
209    /// Returns a unique identifier for this DEX, combining chain and protocol name.
210    pub fn id(&self) -> String {
211        format!("{}:{}", self.chain.name, self.name)
212    }
213
214    /// Sets the pool initialization event signature by hashing and encoding the provided event string.
215    pub fn set_initialize_event(&mut self, event: &str) {
216        let initialize_event_hash = keccak256(event.as_bytes());
217        let encoded_initialized_event = format!(
218            "0x{encoded_hash}",
219            encoded_hash = hex::encode(initialize_event_hash)
220        );
221        self.initialize_event = Some(encoded_initialized_event.into());
222    }
223
224    /// Sets the flash loan event signature by hashing and encoding the provided event string.
225    pub fn set_flash_event(&mut self, event: &str) {
226        let flash_event_hash = keccak256(event.as_bytes());
227        let encoded_flash_event = format!(
228            "0x{encoded_hash}",
229            encoded_hash = hex::encode(flash_event_hash)
230        );
231        self.flash_created_event = Some(encoded_flash_event.into());
232    }
233}
234
235impl Display for Dex {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        write!(f, "Dex(chain={}, name={})", self.chain, self.name)
238    }
239}
240
241impl From<Pool> for CurrencyPair {
242    fn from(p: Pool) -> Self {
243        let symbol = Symbol::from(format!("{}/{}", p.token0.symbol, p.token1.symbol));
244        let id = InstrumentId::new(symbol, Venue::from(p.dex.id()));
245
246        let size_precision = p.token0.decimals.min(FIXED_PRECISION);
247        let price_precision = p.token1.decimals.min(FIXED_PRECISION);
248
249        let price_increment = Price::new(10f64.powi(-(price_precision as i32)), price_precision);
250        let size_increment = Quantity::new(10f64.powi(-(size_precision as i32)), size_precision);
251
252        Self::new(
253            id,
254            symbol,
255            Currency::from(p.token0.symbol.as_str()),
256            Currency::from(p.token1.symbol.as_str()),
257            price_precision,
258            size_precision,
259            price_increment,
260            size_increment,
261            None,
262            None,
263            None,
264            None,
265            None,
266            None,
267            None,
268            None,
269            None,
270            None,
271            None,
272            None,
273            0.into(),
274            0.into(),
275        )
276    }
277}
278
279impl From<Pool> for InstrumentAny {
280    fn from(p: Pool) -> Self {
281        CurrencyPair::from(p).into_any()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use rstest::rstest;
288
289    use super::DexType;
290
291    #[rstest]
292    fn test_dex_type_from_dex_name_valid() {
293        // Test some known DEX names
294        assert!(DexType::from_dex_name("UniswapV3").is_some());
295        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
296        assert!(DexType::from_dex_name("BalancerV2").is_some());
297        assert!(DexType::from_dex_name("CamelotV3").is_some());
298
299        // Verify specific DEX type
300        let uniswap_v3 = DexType::from_dex_name("UniswapV3").unwrap();
301        assert_eq!(uniswap_v3, DexType::UniswapV3);
302
303        // Verify compound names
304        let aerodrome_slipstream = DexType::from_dex_name("AerodromeSlipstream").unwrap();
305        assert_eq!(aerodrome_slipstream, DexType::AerodromeSlipstream);
306
307        // Verify specialized names
308        let fluid_dex = DexType::from_dex_name("FluidDEX").unwrap();
309        assert_eq!(fluid_dex, DexType::FluidDEX);
310    }
311
312    #[rstest]
313    fn test_dex_type_from_dex_name_invalid() {
314        // Test unknown DEX names
315        assert!(DexType::from_dex_name("InvalidDEX").is_none());
316        assert!(DexType::from_dex_name("").is_none());
317        assert!(DexType::from_dex_name("NonExistentDEX").is_none());
318    }
319
320    #[rstest]
321    fn test_dex_type_from_dex_name_case_sensitive() {
322        // Test case sensitivity - should be case sensitive
323        assert!(DexType::from_dex_name("UniswapV3").is_some());
324        assert!(DexType::from_dex_name("uniswapv3").is_none()); // lowercase
325        assert!(DexType::from_dex_name("UNISWAPV3").is_none()); // uppercase
326        assert!(DexType::from_dex_name("UniSwapV3").is_none()); // mixed case
327
328        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
329        assert!(DexType::from_dex_name("sushiswapv2").is_none()); // lowercase
330    }
331
332    #[rstest]
333    fn test_dex_type_all_variants_mappable() {
334        // Test that all DEX variants can be mapped from their string representation
335        let all_dex_names = vec![
336            "AerodromeSlipstream",
337            "AerodromeV1",
338            "BalancerV2",
339            "BalancerV3",
340            "BaseSwapV2",
341            "BaseX",
342            "CamelotV3",
343            "CurveFinance",
344            "FluidDEX",
345            "MaverickV1",
346            "MaverickV2",
347            "PancakeSwapV3",
348            "SushiSwapV2",
349            "SushiSwapV3",
350            "UniswapV2",
351            "UniswapV3",
352            "UniswapV4",
353        ];
354
355        for dex_name in all_dex_names {
356            assert!(
357                DexType::from_dex_name(dex_name).is_some(),
358                "DEX name '{dex_name}' should be valid but was not found",
359            );
360        }
361    }
362
363    #[rstest]
364    fn test_dex_type_display() {
365        // Test that DexType variants display correctly (using strum::Display)
366        assert_eq!(DexType::UniswapV3.to_string(), "UniswapV3");
367        assert_eq!(DexType::SushiSwapV2.to_string(), "SushiSwapV2");
368        assert_eq!(
369            DexType::AerodromeSlipstream.to_string(),
370            "AerodromeSlipstream"
371        );
372        assert_eq!(DexType::FluidDEX.to_string(), "FluidDEX");
373    }
374}