nautilus_blockchain/exchanges/ethereum/
uniswap_v3.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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::sync::LazyLock;
17
18use alloy::primitives::{Address, U256};
19use hypersync_client::simple_types::Log;
20use nautilus_model::defi::{
21    chain::chains,
22    dex::{AmmType, Dex},
23};
24
25use crate::{events::pool_created::PoolCreated, exchanges::extended::DexExtended};
26
27/// Uniswap V3 DEX on Ethereum.
28pub static UNISWAP_V3: LazyLock<DexExtended> = LazyLock::new(|| {
29    let mut dex = DexExtended::new(Dex::new(
30        chains::ETHEREUM.clone(),
31        "Uniswap V3",
32        "0x1F98431c8aD98523631AE4a59f267346ea31F984",
33        AmmType::CLAMM,
34        "PoolCreated(address,address,uint24,int24,address)",
35    ));
36    dex.set_pool_created_event_parsing(parse_pool_created_event);
37    dex
38});
39
40fn parse_pool_created_event(log: Log) -> anyhow::Result<PoolCreated> {
41    let block_number = log
42        .block_number
43        .expect("Block number should be set in logs");
44    let token = if let Some(topic) = log.topics.get(1).and_then(|t| t.as_ref()) {
45        // Address is stored in the last 20 bytes of the 32-byte topic
46        Address::from_slice(&topic.as_ref()[12..32])
47    } else {
48        anyhow::bail!("Missing token0 address in topic1");
49    };
50
51    let token1 = if let Some(topic) = log.topics.get(2).and_then(|t| t.as_ref()) {
52        Address::from_slice(&topic.as_ref()[12..32])
53    } else {
54        anyhow::bail!("Missing token1 address in topic2");
55    };
56
57    let fee = if let Some(topic) = log.topics.get(3).and_then(|t| t.as_ref()) {
58        U256::from_be_slice(topic.as_ref()).as_limbs()[0] as u32
59    } else {
60        anyhow::bail!("Missing fee in topic3");
61    };
62
63    if let Some(data) = log.data {
64        // Data contains: [tick_spacing (32 bytes), pool_address (32 bytes)]
65        let data_bytes = data.as_ref();
66
67        // Extract tick_spacing (first 32 bytes)
68        let tick_spacing_bytes: [u8; 32] = data_bytes[0..32].try_into()?;
69        let tick_spacing = u32::from_be_bytes(tick_spacing_bytes[28..32].try_into()?);
70
71        // Extract pool_address (next 32 bytes)
72        let pool_address_bytes: [u8; 32] = data_bytes[32..64].try_into()?;
73        let pool_address = Address::from_slice(&pool_address_bytes[12..32]);
74
75        Ok(PoolCreated::new(
76            block_number.into(),
77            token,
78            token1,
79            fee,
80            tick_spacing,
81            pool_address,
82        ))
83    } else {
84        Err(anyhow::anyhow!("Missing data in log"))
85    }
86}