Skip to main content

nautilus_blockchain/contracts/
uniswap_v3_pool.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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::{collections::HashMap, sync::Arc};
17
18use alloy::{
19    primitives::{Address, U256, keccak256},
20    sol,
21    sol_types::{SolCall, private::primitives::aliases::I24},
22};
23use nautilus_core::hex;
24use nautilus_model::{
25    defi::{
26        data::block::BlockPosition,
27        pool_analysis::{
28            position::PoolPosition,
29            snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
30        },
31        tick_map::tick::PoolTick,
32    },
33    identifiers::InstrumentId,
34};
35use thiserror::Error;
36
37use super::base::{BaseContract, ContractCall};
38use crate::rpc::{error::BlockchainRpcClientError, http::BlockchainHttpRpcClient};
39
40sol! {
41    #[sol(rpc)]
42    contract UniswapV3Pool {
43        /// Packed struct containing core pool state
44        struct Slot0Data {
45            uint160 sqrtPriceX96;
46            int24 tick;
47            uint16 observationIndex;
48            uint16 observationCardinality;
49            uint16 observationCardinalityNext;
50            uint8 feeProtocol;
51            bool unlocked;
52        }
53
54        /// Tick information
55        struct TickInfo {
56            uint128 liquidityGross;
57            int128 liquidityNet;
58            uint256 feeGrowthOutside0X128;
59            uint256 feeGrowthOutside1X128;
60            int56 tickCumulativeOutside;
61            uint160 secondsPerLiquidityOutsideX128;
62            uint32 secondsOutside;
63            bool initialized;
64        }
65
66        /// Position information
67        struct PositionInfo {
68            uint128 liquidity;
69            uint256 feeGrowthInside0LastX128;
70            uint256 feeGrowthInside1LastX128;
71            uint128 tokensOwed0;
72            uint128 tokensOwed1;
73        }
74
75        // Core state getters
76        function slot0() external view returns (Slot0Data memory);
77        function liquidity() external view returns (uint128);
78        function feeGrowthGlobal0X128() external view returns (uint256);
79        function feeGrowthGlobal1X128() external view returns (uint256);
80
81        // Tick and position getters
82        function ticks(int24 tick) external view returns (TickInfo memory);
83        function positions(bytes32 key) external view returns (PositionInfo memory);
84    }
85}
86
87/// Represents errors that can occur when interacting with UniswapV3Pool contract.
88#[derive(Debug, Error)]
89pub enum UniswapV3PoolError {
90    #[error("RPC error: {0}")]
91    RpcError(#[from] BlockchainRpcClientError),
92    #[error("Failed to decode {field} for pool {pool}: {reason} (raw data: {raw_data})")]
93    DecodingError {
94        field: String,
95        pool: Address,
96        reason: String,
97        raw_data: String,
98    },
99    #[error("Call failed for {field} at pool {pool}: {reason}")]
100    CallFailed {
101        field: String,
102        pool: Address,
103        reason: String,
104    },
105    #[error("Tick {tick} is not initialized in pool {pool}")]
106    TickNotInitialized { tick: i32, pool: Address },
107}
108
109/// Interface for interacting with UniswapV3Pool contracts on a blockchain.
110///
111/// This struct provides methods to query pool state including slot0, liquidity,
112/// fee growth, tick data, and position data. Supports both single calls and
113/// batch multicalls for efficiency.
114#[derive(Debug)]
115pub struct UniswapV3PoolContract {
116    /// The base contract providing common RPC execution functionality.
117    base: BaseContract,
118}
119
120impl UniswapV3PoolContract {
121    /// Creates a new UniswapV3Pool contract interface with the specified RPC client.
122    #[must_use]
123    pub fn new(client: Arc<BlockchainHttpRpcClient>) -> Self {
124        Self {
125            base: BaseContract::new(client),
126        }
127    }
128
129    /// Gets all global state in a single multicall.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if the multicall fails or any decoding fails.
134    pub async fn get_global_state(
135        &self,
136        pool_address: &Address,
137        block: Option<u64>,
138    ) -> Result<PoolState, UniswapV3PoolError> {
139        let calls = vec![
140            ContractCall {
141                target: *pool_address,
142                allow_failure: false,
143                call_data: UniswapV3Pool::slot0Call {}.abi_encode(),
144            },
145            ContractCall {
146                target: *pool_address,
147                allow_failure: false,
148                call_data: UniswapV3Pool::liquidityCall {}.abi_encode(),
149            },
150            ContractCall {
151                target: *pool_address,
152                allow_failure: false,
153                call_data: UniswapV3Pool::feeGrowthGlobal0X128Call {}.abi_encode(),
154            },
155            ContractCall {
156                target: *pool_address,
157                allow_failure: false,
158                call_data: UniswapV3Pool::feeGrowthGlobal1X128Call {}.abi_encode(),
159            },
160        ];
161
162        let results = self.base.execute_multicall(calls, block).await?;
163
164        if results.len() != 4 {
165            return Err(UniswapV3PoolError::CallFailed {
166                field: "global_state_multicall".to_string(),
167                pool: *pool_address,
168                reason: format!("Expected 4 results, received {}", results.len()),
169            });
170        }
171
172        // Decode slot0
173        let slot0 =
174            UniswapV3Pool::slot0Call::abi_decode_returns(&results[0].returnData).map_err(|e| {
175                UniswapV3PoolError::DecodingError {
176                    field: "slot0".to_string(),
177                    pool: *pool_address,
178                    reason: e.to_string(),
179                    raw_data: hex::encode(&results[0].returnData),
180                }
181            })?;
182
183        // Decode liquidity
184        let liquidity = UniswapV3Pool::liquidityCall::abi_decode_returns(&results[1].returnData)
185            .map_err(|e| UniswapV3PoolError::DecodingError {
186                field: "liquidity".to_string(),
187                pool: *pool_address,
188                reason: e.to_string(),
189                raw_data: hex::encode(&results[1].returnData),
190            })?;
191
192        // Decode feeGrowthGlobal0X128
193        let fee_growth_0 =
194            UniswapV3Pool::feeGrowthGlobal0X128Call::abi_decode_returns(&results[2].returnData)
195                .map_err(|e| UniswapV3PoolError::DecodingError {
196                    field: "feeGrowthGlobal0X128".to_string(),
197                    pool: *pool_address,
198                    reason: e.to_string(),
199                    raw_data: hex::encode(&results[2].returnData),
200                })?;
201
202        // Decode feeGrowthGlobal1X128
203        let fee_growth_1 =
204            UniswapV3Pool::feeGrowthGlobal1X128Call::abi_decode_returns(&results[3].returnData)
205                .map_err(|e| UniswapV3PoolError::DecodingError {
206                    field: "feeGrowthGlobal1X128".to_string(),
207                    pool: *pool_address,
208                    reason: e.to_string(),
209                    raw_data: hex::encode(&results[3].returnData),
210                })?;
211
212        Ok(PoolState {
213            current_tick: slot0.tick.as_i32(),
214            price_sqrt_ratio_x96: slot0.sqrtPriceX96,
215            liquidity,
216            protocol_fees_token0: U256::ZERO,
217            protocol_fees_token1: U256::ZERO,
218            fee_protocol: slot0.feeProtocol,
219            fee_growth_global_0: fee_growth_0,
220            fee_growth_global_1: fee_growth_1,
221        })
222    }
223
224    /// Gets tick data for a specific tick.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the RPC call fails or decoding fails.
229    pub async fn get_tick(
230        &self,
231        pool_address: &Address,
232        tick: i32,
233        block: Option<u64>,
234    ) -> Result<PoolTick, UniswapV3PoolError> {
235        let tick_i24 = I24::try_from(tick).map_err(|_| UniswapV3PoolError::CallFailed {
236            field: "tick".to_string(),
237            pool: *pool_address,
238            reason: format!("Tick {tick} out of range for int24"),
239        })?;
240
241        let call_data = UniswapV3Pool::ticksCall { tick: tick_i24 }.abi_encode();
242        let raw_response = self
243            .base
244            .execute_call(pool_address, &call_data, block)
245            .await?;
246
247        let tick_info =
248            UniswapV3Pool::ticksCall::abi_decode_returns(&raw_response).map_err(|e| {
249                UniswapV3PoolError::DecodingError {
250                    field: format!("ticks({tick})"),
251                    pool: *pool_address,
252                    reason: e.to_string(),
253                    raw_data: hex::encode(&raw_response),
254                }
255            })?;
256
257        Ok(PoolTick::new(
258            tick,
259            tick_info.liquidityGross,
260            tick_info.liquidityNet,
261            tick_info.feeGrowthOutside0X128,
262            tick_info.feeGrowthOutside1X128,
263            tick_info.initialized,
264            0, // last_updated_block - not available from RPC
265        ))
266    }
267
268    /// Gets tick data for multiple ticks in a single multicall.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if the multicall fails or if any tick decoding fails.
273    /// Uninitialized ticks are silently skipped (not included in the result HashMap).
274    pub async fn batch_get_ticks(
275        &self,
276        pool_address: &Address,
277        ticks: &[i32],
278        block: Option<u64>,
279    ) -> Result<HashMap<i32, PoolTick>, UniswapV3PoolError> {
280        let calls: Vec<ContractCall> = ticks
281            .iter()
282            .filter_map(|&tick| {
283                I24::try_from(tick).ok().map(|tick_i24| ContractCall {
284                    target: *pool_address,
285                    allow_failure: true,
286                    call_data: UniswapV3Pool::ticksCall { tick: tick_i24 }.abi_encode(),
287                })
288            })
289            .collect();
290
291        let results = self.base.execute_multicall(calls, block).await?;
292
293        let mut tick_infos = HashMap::with_capacity(ticks.len());
294        for (i, &tick_value) in ticks.iter().enumerate() {
295            if i >= results.len() {
296                break;
297            }
298
299            let result = &results[i];
300            if !result.success {
301                // Skip uninitialized ticks
302                continue;
303            }
304
305            let tick_info = UniswapV3Pool::ticksCall::abi_decode_returns(&result.returnData)
306                .map_err(|e| UniswapV3PoolError::DecodingError {
307                    field: format!("ticks({tick_value})"),
308                    pool: *pool_address,
309                    reason: e.to_string(),
310                    raw_data: hex::encode(&result.returnData),
311                })?;
312
313            tick_infos.insert(
314                tick_value,
315                PoolTick::new(
316                    tick_value,
317                    tick_info.liquidityGross,
318                    tick_info.liquidityNet,
319                    tick_info.feeGrowthOutside0X128,
320                    tick_info.feeGrowthOutside1X128,
321                    tick_info.initialized,
322                    0, // last_updated_block - not available from RPC
323                ),
324            );
325        }
326
327        Ok(tick_infos)
328    }
329
330    /// Computes the position key used by Uniswap V3.
331    ///
332    /// The key is: keccak256(abi.encodePacked(owner, tickLower, tickUpper))
333    #[must_use]
334    pub fn compute_position_key(owner: &Address, tick_lower: i32, tick_upper: i32) -> [u8; 32] {
335        // Pack: address (20 bytes) + int24 (3 bytes) + int24 (3 bytes) = 26 bytes total
336        let mut packed = Vec::with_capacity(26);
337
338        // Add owner address (20 bytes)
339        packed.extend_from_slice(owner.as_slice());
340
341        // Add tick_lower as int24 (3 bytes, big-endian, sign-extended)
342        let tick_lower_bytes = tick_lower.to_be_bytes();
343        packed.extend_from_slice(&tick_lower_bytes[1..4]);
344
345        // Add tick_upper as int24 (3 bytes, big-endian, sign-extended)
346        let tick_upper_bytes = tick_upper.to_be_bytes();
347        packed.extend_from_slice(&tick_upper_bytes[1..4]);
348
349        keccak256(&packed).into()
350    }
351
352    /// Gets position data for multiple positions in a single multicall.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if the multicall fails. Individual position failures are
357    /// captured in the Result values of the returned Vec.
358    pub async fn batch_get_positions(
359        &self,
360        pool_address: &Address,
361        positions: &[(Address, i32, i32)],
362        block: Option<u64>,
363    ) -> Result<Vec<PoolPosition>, UniswapV3PoolError> {
364        let calls: Vec<ContractCall> = positions
365            .iter()
366            .map(|(owner, tick_lower, tick_upper)| {
367                let position_key = Self::compute_position_key(owner, *tick_lower, *tick_upper);
368                ContractCall {
369                    target: *pool_address,
370                    allow_failure: true,
371                    call_data: UniswapV3Pool::positionsCall {
372                        key: position_key.into(),
373                    }
374                    .abi_encode(),
375                }
376            })
377            .collect();
378
379        let results = self.base.execute_multicall(calls, block).await?;
380
381        let position_infos: Vec<PoolPosition> = positions
382            .iter()
383            .enumerate()
384            .filter_map(|(i, (owner, tick_lower, tick_upper))| {
385                if i >= results.len() {
386                    return None;
387                }
388
389                let result = &results[i];
390                if !result.success {
391                    return None;
392                }
393
394                UniswapV3Pool::positionsCall::abi_decode_returns(&result.returnData)
395                    .ok()
396                    .map(|info| PoolPosition {
397                        owner: *owner,
398                        tick_lower: *tick_lower,
399                        tick_upper: *tick_upper,
400                        liquidity: info.liquidity,
401                        fee_growth_inside_0_last: info.feeGrowthInside0LastX128,
402                        fee_growth_inside_1_last: info.feeGrowthInside1LastX128,
403                        tokens_owed_0: info.tokensOwed0,
404                        tokens_owed_1: info.tokensOwed1,
405                        total_amount0_deposited: U256::ZERO,
406                        total_amount1_deposited: U256::ZERO,
407                        total_amount0_collected: 0,
408                        total_amount1_collected: 0,
409                    })
410            })
411            .collect();
412
413        Ok(position_infos)
414    }
415
416    /// Fetches a complete pool snapshot directly from on-chain state.
417    ///
418    /// Retrieves global state, tick data, and position data from the blockchain
419    /// and constructs a `PoolSnapshot` representing the current on-chain state.
420    /// This snapshot can be compared against profiler state for validation.
421    ///
422    /// # Errors
423    ///
424    /// Returns error if any RPC calls fail or data cannot be decoded.
425    pub async fn fetch_snapshot(
426        &self,
427        pool_address: &Address,
428        instrument_id: InstrumentId,
429        tick_values: &[i32],
430        position_keys: &[(Address, i32, i32)],
431        block_position: BlockPosition,
432    ) -> Result<PoolSnapshot, UniswapV3PoolError> {
433        // Fetch all data at the specified block
434        let block = Some(block_position.number);
435        let global_state = self.get_global_state(pool_address, block).await?;
436        let ticks_map = self
437            .batch_get_ticks(pool_address, tick_values, block)
438            .await?;
439        let positions = self
440            .batch_get_positions(pool_address, position_keys, block)
441            .await?;
442
443        Ok(PoolSnapshot::new(
444            instrument_id,
445            global_state,
446            positions,
447            ticks_map.into_values().collect(),
448            PoolAnalytics::default(),
449            block_position,
450        ))
451    }
452}