Skip to main content

nautilus_blockchain/contracts/
uniswap_v3_swap.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::sync::Arc;
17
18use alloy::{
19    primitives::{Address, aliases::U24},
20    sol,
21    sol_types::SolCall,
22};
23
24use super::base::BaseContract;
25use crate::rpc::{error::BlockchainRpcClientError, http::BlockchainHttpRpcClient};
26
27// The original Uniswap V3 SwapRouter interface: `exactInputSingle` carries a `deadline`
28// parameter, unlike the later SwapRouter02 whose struct drops it.
29sol! {
30    #[sol(rpc)]
31    contract UniswapV3SwapRouter {
32        struct ExactInputSingleParams {
33            address tokenIn;
34            address tokenOut;
35            uint24 fee;
36            address recipient;
37            uint256 deadline;
38            uint256 amountIn;
39            uint256 amountOutMinimum;
40            uint160 sqrtPriceLimitX96;
41        }
42
43        function exactInputSingle(ExactInputSingleParams memory params) external payable returns (uint256 amountOut);
44    }
45}
46
47sol! {
48    #[sol(rpc)]
49    contract UniswapV3RouterState {
50        function factory() external view returns (address);
51        function WETH9() external view returns (address);
52    }
53
54    #[sol(rpc)]
55    contract UniswapV3Factory {
56        function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
57    }
58}
59
60/// Reads immutable deployment relationships used to authorize Uniswap V3 execution.
61#[derive(Debug)]
62pub struct UniswapV3Deployment {
63    base: BaseContract,
64}
65
66impl UniswapV3Deployment {
67    /// Creates a deployment reader with an optional per-request timeout.
68    #[must_use]
69    pub fn new(client: Arc<BlockchainHttpRpcClient>, rpc_timeout_secs: Option<u64>) -> Self {
70        Self {
71            base: BaseContract::new_with_multicall_limit_and_timeout(
72                client,
73                super::base::DEFAULT_MULTICALL_CALLS_PER_RPC_REQUEST,
74                rpc_timeout_secs,
75            ),
76        }
77    }
78
79    /// Reads the factory configured by the router.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if the RPC call fails or the result cannot be decoded as an address.
84    pub async fn router_factory(
85        &self,
86        router: &Address,
87    ) -> Result<Address, BlockchainRpcClientError> {
88        self.router_factory_with_block(router, None).await
89    }
90
91    async fn router_factory_with_block(
92        &self,
93        router: &Address,
94        block: Option<u64>,
95    ) -> Result<Address, BlockchainRpcClientError> {
96        let result = self
97            .base
98            .execute_call(
99                router,
100                &UniswapV3RouterState::factoryCall {}.abi_encode(),
101                block,
102            )
103            .await?;
104        UniswapV3RouterState::factoryCall::abi_decode_returns(&result)
105            .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
106    }
107
108    /// Reads the wrapped-native-token address configured by the router.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the RPC call fails or the result cannot be decoded as an address.
113    pub async fn router_weth9(
114        &self,
115        router: &Address,
116    ) -> Result<Address, BlockchainRpcClientError> {
117        self.router_weth9_with_block(router, None).await
118    }
119
120    async fn router_weth9_with_block(
121        &self,
122        router: &Address,
123        block: Option<u64>,
124    ) -> Result<Address, BlockchainRpcClientError> {
125        let result = self
126            .base
127            .execute_call(
128                router,
129                &UniswapV3RouterState::WETH9Call {}.abi_encode(),
130                block,
131            )
132            .await?;
133        UniswapV3RouterState::WETH9Call::abi_decode_returns(&result)
134            .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
135    }
136
137    /// Resolves the canonical pool registered by a factory for a token pair and fee tier.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the RPC call fails or the result cannot be decoded as an address.
142    pub async fn pool(
143        &self,
144        factory: &Address,
145        token_a: Address,
146        token_b: Address,
147        fee: U24,
148    ) -> Result<Address, BlockchainRpcClientError> {
149        self.pool_with_block(factory, token_a, token_b, fee, None)
150            .await
151    }
152
153    async fn pool_with_block(
154        &self,
155        factory: &Address,
156        token_a: Address,
157        token_b: Address,
158        fee: U24,
159        block: Option<u64>,
160    ) -> Result<Address, BlockchainRpcClientError> {
161        let call = UniswapV3Factory::getPoolCall {
162            tokenA: token_a,
163            tokenB: token_b,
164            fee,
165        };
166        let result = self
167            .base
168            .execute_call(factory, &call.abi_encode(), block)
169            .await?;
170        UniswapV3Factory::getPoolCall::abi_decode_returns(&result)
171            .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use alloy::{
178        primitives::{
179            U256, address,
180            aliases::{U24, U160},
181        },
182        sol_types::SolCall,
183    };
184    use nautilus_core::hex;
185    use rstest::rstest;
186
187    use super::*;
188
189    #[rstest]
190    fn exact_input_single_selector_matches_canonical_signature() {
191        let calldata = UniswapV3SwapRouter::exactInputSingleCall {
192            params: UniswapV3SwapRouter::ExactInputSingleParams {
193                tokenIn: address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
194                tokenOut: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
195                fee: U24::try_from(500u32).unwrap(),
196                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
197                deadline: U256::from(1_761_889_100u64),
198                amountIn: U256::from(1_000_000_000_000_000u64),
199                amountOutMinimum: U256::from(1_995_000u64),
200                sqrtPriceLimitX96: U160::ZERO,
201            },
202        }
203        .abi_encode();
204
205        // keccak256("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))")
206        assert_eq!(hex::encode(&calldata[..4]), "414bf389");
207    }
208
209    #[rstest]
210    fn exact_input_single_encodes_fields_in_order() {
211        let calldata = UniswapV3SwapRouter::exactInputSingleCall {
212            params: UniswapV3SwapRouter::ExactInputSingleParams {
213                tokenIn: address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
214                tokenOut: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
215                fee: U24::try_from(500u32).unwrap(),
216                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
217                deadline: U256::from(1_761_889_100u64),
218                amountIn: U256::from(1_000_000_000_000_000u64),
219                amountOutMinimum: U256::from(1_995_000u64),
220                sqrtPriceLimitX96: U160::ZERO,
221            },
222        }
223        .abi_encode();
224
225        let expected = concat!(
226            "414bf389",
227            "00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1",
228            "000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831",
229            "00000000000000000000000000000000000000000000000000000000000001f4",
230            "000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
231            "0000000000000000000000000000000000000000000000000000000069044b4c",
232            "00000000000000000000000000000000000000000000000000038d7ea4c68000",
233            "00000000000000000000000000000000000000000000000000000000001e70f8",
234            "0000000000000000000000000000000000000000000000000000000000000000",
235        );
236        assert_eq!(hex::encode(&calldata), expected);
237    }
238
239    #[rstest]
240    fn deployment_selectors_match_canonical_signatures() {
241        assert_eq!(
242            hex::encode(UniswapV3RouterState::factoryCall {}.abi_encode()),
243            "c45a0155"
244        );
245        assert_eq!(
246            hex::encode(UniswapV3RouterState::WETH9Call {}.abi_encode()),
247            "4aa4a4fc"
248        );
249        let calldata = UniswapV3Factory::getPoolCall {
250            tokenA: address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
251            tokenB: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
252            fee: U24::try_from(500u32).unwrap(),
253        }
254        .abi_encode();
255        assert_eq!(hex::encode(&calldata[..4]), "1698ee82");
256    }
257}