Skip to main content

nautilus_blockchain/exchanges/parsing/uniswap_v3/
flash.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 alloy::{dyn_abi::SolType, primitives::Address, sol};
17use nautilus_model::defi::{PoolIdentifier, SharedDex, rpc::RpcLog};
18use ustr::Ustr;
19
20use crate::{
21    events::flash::FlashEvent,
22    hypersync::{
23        HypersyncLog,
24        log::{
25            extract_address_from_topic, extract_block_number, extract_log_index,
26            extract_transaction_hash, extract_transaction_index, validate_event_signature_hash,
27        },
28    },
29    rpc::log as rpc_log,
30};
31
32// Placeholder hash - will be calculated properly later
33const FLASH_EVENT_SIGNATURE_HASH: &str =
34    "bdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633";
35
36// Define sol macro for easier parsing of Flash event data
37// event Flash(address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1, uint256 paid0, uint256 paid1)
38sol! {
39    struct FlashEventData {
40        uint256 amount0;
41        uint256 amount1;
42        uint256 paid0;
43        uint256 paid1;
44    }
45}
46
47/// Parses a flash event from a Uniswap V3 log.
48///
49/// # Errors
50///
51/// Returns an error if the log parsing fails or if the event data is invalid.
52///
53/// # Panics
54///
55/// Panics if the contract address is not set in the log.
56pub fn parse_flash_event_hypersync(
57    dex: SharedDex,
58    log: &HypersyncLog,
59) -> anyhow::Result<FlashEvent> {
60    validate_event_signature_hash("FlashEvent", FLASH_EVENT_SIGNATURE_HASH, log)?;
61
62    let sender = extract_address_from_topic(log, 1, "sender")?;
63    let recipient = extract_address_from_topic(log, 2, "recipient")?;
64
65    if let Some(data) = &log.data {
66        let data_bytes = data.as_ref();
67
68        // Validate if data contains 4 parameters of 32 bytes each
69        if data_bytes.len() < 4 * 32 {
70            anyhow::bail!("Flash event data is too short");
71        }
72
73        // Decode the data using the FlashEventData struct
74        let decoded = match <FlashEventData as SolType>::abi_decode(data_bytes) {
75            Ok(decoded) => decoded,
76            Err(e) => anyhow::bail!("Failed to decode flash event data: {e}"),
77        };
78
79        let pool_address = Address::from_slice(
80            log.address
81                .clone()
82                .expect("Contract address should be set in logs")
83                .as_ref(),
84        );
85        let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
86
87        Ok(FlashEvent::new(
88            dex,
89            pool_identifier,
90            extract_block_number(log)?,
91            extract_transaction_hash(log)?,
92            extract_transaction_index(log)?,
93            extract_log_index(log)?,
94            sender,
95            recipient,
96            decoded.amount0,
97            decoded.amount1,
98            decoded.paid0,
99            decoded.paid1,
100        ))
101    } else {
102        anyhow::bail!("Missing data in flash event log");
103    }
104}
105
106/// Parses a flash event from an RPC log.
107///
108/// # Errors
109///
110/// Returns an error if the log parsing fails or if the event data is invalid.
111pub fn parse_flash_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<FlashEvent> {
112    rpc_log::validate_event_signature(log, FLASH_EVENT_SIGNATURE_HASH, "Flash")?;
113
114    let sender = rpc_log::extract_address_from_topic(log, 1, "sender")?;
115    let recipient = rpc_log::extract_address_from_topic(log, 2, "recipient")?;
116
117    let data_bytes = rpc_log::extract_data_bytes(log)?;
118
119    // Validate if data contains 4 parameters of 32 bytes each
120    if data_bytes.len() < 4 * 32 {
121        anyhow::bail!("Flash event data is too short");
122    }
123
124    // Decode the data using the FlashEventData struct
125    let decoded = match <FlashEventData as SolType>::abi_decode(&data_bytes) {
126        Ok(decoded) => decoded,
127        Err(e) => anyhow::bail!("Failed to decode flash event data: {e}"),
128    };
129
130    let pool_address = rpc_log::extract_address(log)?;
131    let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
132    Ok(FlashEvent::new(
133        dex,
134        pool_identifier,
135        rpc_log::extract_block_number(log)?,
136        rpc_log::extract_transaction_hash(log)?,
137        rpc_log::extract_transaction_index(log)?,
138        rpc_log::extract_log_index(log)?,
139        sender,
140        recipient,
141        decoded.amount0,
142        decoded.amount1,
143        decoded.paid0,
144        decoded.paid1,
145    ))
146}
147
148#[cfg(test)]
149mod tests {
150    use std::sync::Arc;
151
152    use alloy::primitives::U256;
153    use rstest::*;
154    use serde_json::json;
155
156    use super::*;
157    use crate::exchanges::arbitrum;
158
159    /// Real HyperSync log from Arbitrum Flash event at block 0xfe9d5ce (266982862)
160    /// Pool: 0x4cef551255ec96d89fec975446301b5c4e164c59
161    /// sender: 0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b
162    /// recipient: 0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b
163    #[fixture]
164    fn hypersync_log() -> HypersyncLog {
165        let log_json = json!({
166            "removed": null,
167            "log_index": "0x3b",
168            "transaction_index": "0x4",
169            "transaction_hash": "0x4d345a8cae1e39654904bb7ca04e552b0fc8728ed68a28563ea4b151b96262aa",
170            "block_hash": null,
171            "block_number": "0xfe9d5ce",
172            "address": "0x4CEf551255EC96d89feC975446301b5C4e164C59",
173            "data": "0x00000000000000000000000000000000000000000000002c55804c34816b99060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220c6ab9806365120000000000000000000000000000000000000000000000000000000000000000",
174            "topics": [
175                "0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633",
176                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b",
177                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
178            ]
179        });
180        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
181    }
182
183    /// Real RPC log from Arbitrum Flash event at block 0xfe9d5ce (266982862)
184    #[fixture]
185    fn rpc_log() -> RpcLog {
186        let log_json = json!({
187            "removed": false,
188            "logIndex": "0x3b",
189            "transactionIndex": "0x4",
190            "transactionHash": "0x4d345a8cae1e39654904bb7ca04e552b0fc8728ed68a28563ea4b151b96262aa",
191            "blockHash": "0xf10a01cbc75fccad0384a7447f37f06bfb01fbd08d7541a6e5f558ff9bc31ea4",
192            "blockNumber": "0xfe9d5ce",
193            "address": "0x4CEf551255EC96d89feC975446301b5C4e164C59",
194            "data": "0x00000000000000000000000000000000000000000000002c55804c34816b99060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220c6ab9806365120000000000000000000000000000000000000000000000000000000000000000",
195            "topics": [
196                "0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633",
197                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b",
198                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
199            ]
200        });
201        serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
202    }
203
204    #[rstest]
205    fn test_parse_flash_event_hypersync(hypersync_log: HypersyncLog) {
206        let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
207        let event = parse_flash_event_hypersync(dex, &hypersync_log).unwrap();
208
209        assert_eq!(
210            event.pool_identifier.to_string(),
211            "0x4CEf551255EC96d89feC975446301b5C4e164C59"
212        );
213        assert_eq!(
214            event.sender.to_string().to_lowercase(),
215            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
216        );
217        assert_eq!(
218            event.recipient.to_string().to_lowercase(),
219            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
220        );
221        let expected_amount0 = U256::from_str_radix("2c55804c34816b9906", 16).unwrap();
222        assert_eq!(event.amount0, expected_amount0);
223        assert_eq!(event.amount1, U256::ZERO);
224        let expected_paid0 = U256::from_str_radix("220c6ab980636512", 16).unwrap();
225        assert_eq!(event.paid0, expected_paid0);
226        assert_eq!(event.paid1, U256::ZERO);
227        assert_eq!(event.block_number, 266982862);
228    }
229
230    #[rstest]
231    fn test_parse_flash_event_rpc(rpc_log: RpcLog) {
232        let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
233        let event = parse_flash_event_rpc(dex, &rpc_log).unwrap();
234
235        assert_eq!(
236            event.pool_identifier.to_string(),
237            "0x4CEf551255EC96d89feC975446301b5C4e164C59"
238        );
239        assert_eq!(
240            event.sender.to_string().to_lowercase(),
241            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
242        );
243        assert_eq!(
244            event.recipient.to_string().to_lowercase(),
245            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
246        );
247        let expected_amount0 = U256::from_str_radix("2c55804c34816b9906", 16).unwrap();
248        assert_eq!(event.amount0, expected_amount0);
249        assert_eq!(event.amount1, U256::ZERO);
250        let expected_paid0 = U256::from_str_radix("220c6ab980636512", 16).unwrap();
251        assert_eq!(event.paid0, expected_paid0);
252        assert_eq!(event.paid1, U256::ZERO);
253        assert_eq!(event.block_number, 266982862);
254    }
255
256    #[rstest]
257    fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
258        let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
259        let event_hypersync =
260            parse_flash_event_hypersync(Arc::clone(&dex), &hypersync_log).unwrap();
261        let event_rpc = parse_flash_event_rpc(dex, &rpc_log).unwrap();
262
263        assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
264        assert_eq!(event_hypersync.sender, event_rpc.sender);
265        assert_eq!(event_hypersync.recipient, event_rpc.recipient);
266        assert_eq!(event_hypersync.amount0, event_rpc.amount0);
267        assert_eq!(event_hypersync.amount1, event_rpc.amount1);
268        assert_eq!(event_hypersync.paid0, event_rpc.paid0);
269        assert_eq!(event_hypersync.paid1, event_rpc.paid1);
270        assert_eq!(event_hypersync.block_number, event_rpc.block_number);
271        assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
272    }
273}