nautilus_blockchain/exchanges/parsing/uniswap_v3/
burn.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 alloy::{dyn_abi::SolType, primitives::Address, sol};
17use nautilus_model::defi::{PoolIdentifier, SharedDex, rpc::RpcLog};
18use ustr::Ustr;
19
20use crate::{
21    events::burn::BurnEvent,
22    hypersync::{
23        HypersyncLog,
24        helpers::{
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::helpers as rpc_helpers,
30};
31
32const BURN_EVENT_SIGNATURE_HASH: &str =
33    "0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c";
34
35// Define sol macro for easier parsing of Burn event data
36// It contains 3 parameters of 32 bytes each:
37// amount (uint128), amount0 (uint256), amount1 (uint256)
38sol! {
39    struct BurnEventData {
40        uint128 amount;
41        uint256 amount0;
42        uint256 amount1;
43    }
44}
45
46/// Parses a burn event from a HyperSync log.
47///
48/// # Errors
49///
50/// Returns an error if the log parsing fails or if the event data is invalid.
51///
52/// # Panics
53///
54/// Panics if the contract address is not set in the log.
55pub fn parse_burn_event_hypersync(dex: SharedDex, log: HypersyncLog) -> anyhow::Result<BurnEvent> {
56    validate_event_signature_hash("Burn", BURN_EVENT_SIGNATURE_HASH, &log)?;
57
58    let owner = extract_address_from_topic(&log, 1, "owner")?;
59
60    // Extract int24 tickLower from topic2 (stored as a 32-byte padded value)
61    let tick_lower = match log.topics.get(2).and_then(|t| t.as_ref()) {
62        Some(topic) => {
63            let tick_lower_bytes: [u8; 32] = topic.as_ref().try_into()?;
64            i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?)
65        }
66        None => anyhow::bail!("Missing tickLower in topic2 when parsing burn event"),
67    };
68
69    // Extract int24 tickUpper from topic3 (stored as a 32-byte padded value)
70    let tick_upper = match log.topics.get(3).and_then(|t| t.as_ref()) {
71        Some(topic) => {
72            let tick_upper_bytes: [u8; 32] = topic.as_ref().try_into()?;
73            i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?)
74        }
75        None => anyhow::bail!("Missing tickUpper in topic3 when parsing burn event"),
76    };
77
78    if let Some(data) = &log.data {
79        let data_bytes = data.as_ref();
80
81        // Validate if data contains 3 parameters of 32 bytes each
82        if data_bytes.len() < 3 * 32 {
83            anyhow::bail!("Burn event data is too short");
84        }
85
86        // Decode the data using the BurnEventData struct
87        let decoded = match <BurnEventData as SolType>::abi_decode(data_bytes) {
88            Ok(decoded) => decoded,
89            Err(e) => anyhow::bail!("Failed to decode burn event data: {e}"),
90        };
91
92        let pool_address = Address::from_slice(
93            log.address
94                .clone()
95                .expect("Contract address should be set in logs")
96                .as_ref(),
97        );
98        let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
99        Ok(BurnEvent::new(
100            dex,
101            pool_identifier,
102            extract_block_number(&log)?,
103            extract_transaction_hash(&log)?,
104            extract_transaction_index(&log)?,
105            extract_log_index(&log)?,
106            owner,
107            tick_lower,
108            tick_upper,
109            decoded.amount,
110            decoded.amount0,
111            decoded.amount1,
112        ))
113    } else {
114        Err(anyhow::anyhow!("Missing data in burn event log"))
115    }
116}
117
118/// Parses a burn event from an RPC log.
119///
120/// # Errors
121///
122/// Returns an error if the log parsing fails or if the event data is invalid.
123pub fn parse_burn_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<BurnEvent> {
124    rpc_helpers::validate_event_signature(log, BURN_EVENT_SIGNATURE_HASH, "Burn")?;
125
126    let owner = rpc_helpers::extract_address_from_topic(log, 1, "owner")?;
127
128    // Extract int24 tickLower from topic2 (stored as a 32-byte padded value)
129    let tick_lower_bytes = rpc_helpers::extract_topic_bytes(log, 2)?;
130    let tick_lower = i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?);
131
132    // Extract int24 tickUpper from topic3 (stored as a 32-byte padded value)
133    let tick_upper_bytes = rpc_helpers::extract_topic_bytes(log, 3)?;
134    let tick_upper = i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?);
135
136    let data_bytes = rpc_helpers::extract_data_bytes(log)?;
137
138    // Validate if data contains 3 parameters of 32 bytes each
139    if data_bytes.len() < 3 * 32 {
140        anyhow::bail!("Burn event data is too short");
141    }
142
143    // Decode the data using the BurnEventData struct
144    let decoded = match <BurnEventData as SolType>::abi_decode(&data_bytes) {
145        Ok(decoded) => decoded,
146        Err(e) => anyhow::bail!("Failed to decode burn event data: {e}"),
147    };
148
149    let pool_address = rpc_helpers::extract_address(log)?;
150    let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
151    Ok(BurnEvent::new(
152        dex,
153        pool_identifier,
154        rpc_helpers::extract_block_number(log)?,
155        rpc_helpers::extract_transaction_hash(log)?,
156        rpc_helpers::extract_transaction_index(log)?,
157        rpc_helpers::extract_log_index(log)?,
158        owner,
159        tick_lower,
160        tick_upper,
161        decoded.amount,
162        decoded.amount0,
163        decoded.amount1,
164    ))
165}
166
167#[cfg(test)]
168mod tests {
169    use alloy::primitives::U256;
170    use rstest::*;
171    use serde_json::json;
172
173    use super::*;
174    use crate::exchanges::arbitrum;
175
176    /// Real HyperSync log from Arbitrum Burn event at block 0x175a6484 (391799940)
177    /// Pool: 0xd13040d4fe917ee704158cfcb3338dcd2838b245
178    /// owner: 0xc36442b4a4522e871399cd717abdd847ab11fe88 (NonfungiblePositionManager)
179    /// tickLower: -139767 (0xfffdde09)
180    /// tickUpper: -139764 (0xfffdde0c)
181    #[fixture]
182    fn hypersync_log() -> HypersyncLog {
183        let log_json = json!({
184            "removed": null,
185            "log_index": "0xd",
186            "transaction_index": "0x5",
187            "transaction_hash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
188            "block_hash": null,
189            "block_number": "0x175a6484",
190            "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
191            "data": "0x00000000000000000000000000000000000000000002bc997c8ea58d3a8521ec0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065b33ff853688eb",
192            "topics": [
193                "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c",
194                "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
195                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
196                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
197            ]
198        });
199        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
200    }
201
202    /// Real RPC log from Arbitrum Burn event at block 0x175a6484 (391799940)
203    #[fixture]
204    fn rpc_log() -> RpcLog {
205        let log_json = json!({
206            "removed": false,
207            "logIndex": "0xd",
208            "transactionIndex": "0x5",
209            "transactionHash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
210            "blockHash": "0xe925eaa1f5178ceedfa24043a974edb928ddab7195600b6b99ff5403fbf13c8b",
211            "blockNumber": "0x175a6484",
212            "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
213            "data": "0x00000000000000000000000000000000000000000002bc997c8ea58d3a8521ec0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065b33ff853688eb",
214            "topics": [
215                "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c",
216                "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
217                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
218                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
219            ]
220        });
221        serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
222    }
223
224    #[rstest]
225    fn test_parse_burn_event_hypersync(hypersync_log: HypersyncLog) {
226        let dex = arbitrum::UNISWAP_V3.dex.clone();
227        let event = parse_burn_event_hypersync(dex, hypersync_log).unwrap();
228
229        assert_eq!(
230            event.pool_identifier.to_string(),
231            "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
232        );
233        assert_eq!(
234            event.owner.to_string().to_lowercase(),
235            "0xc36442b4a4522e871399cd717abdd847ab11fe88"
236        );
237        assert_eq!(event.tick_lower, -139767);
238        assert_eq!(event.tick_upper, -139764);
239        let expected_amount = u128::from_str_radix("2bc997c8ea58d3a8521ec", 16).unwrap();
240        assert_eq!(event.amount, expected_amount);
241        assert_eq!(event.amount0, U256::ZERO);
242        let expected_amount1 = U256::from_str_radix("65b33ff853688eb", 16).unwrap();
243        assert_eq!(event.amount1, expected_amount1);
244        assert_eq!(event.block_number, 391799940);
245    }
246
247    #[rstest]
248    fn test_parse_burn_event_rpc(rpc_log: RpcLog) {
249        let dex = arbitrum::UNISWAP_V3.dex.clone();
250        let event = parse_burn_event_rpc(dex, &rpc_log).unwrap();
251
252        assert_eq!(
253            event.pool_identifier.to_string(),
254            "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
255        );
256        assert_eq!(
257            event.owner.to_string().to_lowercase(),
258            "0xc36442b4a4522e871399cd717abdd847ab11fe88"
259        );
260        assert_eq!(event.tick_lower, -139767);
261        assert_eq!(event.tick_upper, -139764);
262        let expected_amount = u128::from_str_radix("2bc997c8ea58d3a8521ec", 16).unwrap();
263        assert_eq!(event.amount, expected_amount);
264        assert_eq!(event.amount0, U256::ZERO);
265        let expected_amount1 = U256::from_str_radix("65b33ff853688eb", 16).unwrap();
266        assert_eq!(event.amount1, expected_amount1);
267        assert_eq!(event.block_number, 391799940);
268    }
269
270    #[rstest]
271    fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
272        let dex = arbitrum::UNISWAP_V3.dex.clone();
273        let event_hypersync = parse_burn_event_hypersync(dex.clone(), hypersync_log).unwrap();
274        let event_rpc = parse_burn_event_rpc(dex, &rpc_log).unwrap();
275
276        assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
277        assert_eq!(event_hypersync.owner, event_rpc.owner);
278        assert_eq!(event_hypersync.tick_lower, event_rpc.tick_lower);
279        assert_eq!(event_hypersync.tick_upper, event_rpc.tick_upper);
280        assert_eq!(event_hypersync.amount, event_rpc.amount);
281        assert_eq!(event_hypersync.amount0, event_rpc.amount0);
282        assert_eq!(event_hypersync.amount1, event_rpc.amount1);
283        assert_eq!(event_hypersync.block_number, event_rpc.block_number);
284        assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
285    }
286}