tycho_simulation/evm/protocol/uniswap_v4/
decoder.rs

1use std::collections::HashMap;
2
3use alloy::primitives::{Address, U256};
4use itertools::Itertools;
5use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
6use tycho_common::{models::token::Token, simulation::protocol_sim::ProtocolSim, Bytes};
7
8use super::state::UniswapV4State;
9use crate::{
10    evm::protocol::{
11        uniswap_v4::{
12            hooks::hook_handler_creator::{instantiate_hook_handler, HookCreationParams},
13            state::UniswapV4Fees,
14        },
15        utils::uniswap::{i24_be_bytes_to_i32, tick_list::TickInfo},
16    },
17    protocol::{
18        errors::InvalidSnapshotError,
19        models::{DecoderContext, TryFromWithBlock},
20    },
21};
22
23impl TryFromWithBlock<ComponentWithState, BlockHeader> for UniswapV4State {
24    type Error = InvalidSnapshotError;
25
26    /// Decodes a `ComponentWithState` into a `UniswapV4State`. Errors with a `InvalidSnapshotError`
27    /// if the snapshot is missing any required attributes.
28    async fn try_from_with_header(
29        snapshot: ComponentWithState,
30        _block: BlockHeader,
31        account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
32        all_tokens: &HashMap<Bytes, Token>,
33        decoder_context: &DecoderContext,
34    ) -> Result<Self, Self::Error> {
35        let liq = snapshot
36            .state
37            .attributes
38            .get("liquidity")
39            .ok_or_else(|| InvalidSnapshotError::MissingAttribute("liquidity".to_string()))?
40            .clone();
41
42        let liquidity = u128::from(liq);
43
44        let sqrt_price = U256::from_be_slice(
45            snapshot
46                .state
47                .attributes
48                .get("sqrt_price_x96")
49                .ok_or_else(|| InvalidSnapshotError::MissingAttribute("sqrt_price".to_string()))?,
50        );
51
52        let lp_fee = u32::from(
53            snapshot
54                .component
55                .static_attributes
56                .get("key_lp_fee")
57                .ok_or_else(|| InvalidSnapshotError::MissingAttribute("key_lp_fee".to_string()))?
58                .clone(),
59        );
60
61        let zero2one_protocol_fee = u32::from(
62            snapshot
63                .state
64                .attributes
65                .get("protocol_fees/zero2one")
66                .ok_or_else(|| {
67                    InvalidSnapshotError::MissingAttribute("protocol_fees/zero2one".to_string())
68                })?
69                .clone(),
70        );
71        let one2zero_protocol_fee = u32::from(
72            snapshot
73                .state
74                .attributes
75                .get("protocol_fees/one2zero")
76                .ok_or_else(|| {
77                    InvalidSnapshotError::MissingAttribute("protocol_fees/one2zero".to_string())
78                })?
79                .clone(),
80        );
81
82        let fees: UniswapV4Fees =
83            UniswapV4Fees::new(zero2one_protocol_fee, one2zero_protocol_fee, lp_fee);
84
85        let tick_spacing: i32 = i32::from(
86            snapshot
87                .component
88                .static_attributes
89                .get("tick_spacing")
90                .ok_or_else(|| InvalidSnapshotError::MissingAttribute("tick_spacing".to_string()))?
91                .clone(),
92        );
93
94        let tick = i24_be_bytes_to_i32(
95            snapshot
96                .state
97                .attributes
98                .get("tick")
99                .ok_or_else(|| InvalidSnapshotError::MissingAttribute("tick".to_string()))?,
100        );
101
102        let ticks: Result<Vec<_>, _> = snapshot
103            .state
104            .attributes
105            .iter()
106            .filter_map(|(key, value)| {
107                if key.starts_with("ticks/") {
108                    Some(
109                        key.split('/')
110                            .nth(1)?
111                            .parse::<i32>()
112                            .map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
113                            .and_then(|tick_index| {
114                                TickInfo::new(tick_index, i128::from(value.clone())).map_err(
115                                    |err| InvalidSnapshotError::ValueError(err.to_string()),
116                                )
117                            }),
118                    )
119                } else {
120                    None
121                }
122            })
123            .collect();
124
125        let hook_address = snapshot
126            .component
127            .static_attributes
128            .get("hooks");
129
130        let mut ticks = match ticks {
131            Ok(ticks) if !ticks.is_empty() => ticks
132                .into_iter()
133                .filter(|t| t.net_liquidity != 0)
134                .collect::<Vec<_>>(),
135            _ => {
136                // there might be pools where the liquidity is managed by the hook
137                if hook_address.is_some() {
138                    Vec::new()
139                } else {
140                    return Err(InvalidSnapshotError::MissingAttribute(
141                        "tick_liquidities".to_string(),
142                    ));
143                }
144            }
145        };
146
147        ticks.sort_by_key(|tick| tick.index);
148
149        let mut state = UniswapV4State::new(liquidity, sqrt_price, fees, tick, tick_spacing, ticks)
150            .map_err(|err| {
151                tracing::error!(
152                    pool_id = %snapshot.component.id,
153                    error = %err,
154                    "Failed to create UniswapV4State"
155                );
156                InvalidSnapshotError::ValueError(err.to_string())
157            })?;
158
159        if let Some(hook_address) = hook_address {
160            let hook_address = Address::from_slice(&hook_address.0);
161
162            // Merge state attributes into static_attributes for hook creation
163            let mut merged_attributes = snapshot
164                .component
165                .static_attributes
166                .clone();
167            merged_attributes.extend(snapshot.state.attributes.clone());
168
169            let hook_params = HookCreationParams::new(
170                hook_address,
171                account_balances,
172                all_tokens,
173                state.clone(),
174                &merged_attributes,
175                &snapshot.state.balances,
176                decoder_context.vm_traces,
177            );
178
179            let hook_handler = instantiate_hook_handler(&hook_address, hook_params)?;
180            state.set_hook_handler(hook_handler);
181        };
182
183        for tokens in snapshot
184            .component
185            .tokens
186            .iter()
187            .permutations(2)
188        {
189            let (t0, t1) = (tokens[0], tokens[1]);
190            let token_in = all_tokens.get(t0).ok_or_else(|| {
191                InvalidSnapshotError::ValueError("Failed to get token".to_string())
192            })?;
193            let token_out = all_tokens.get(t1).ok_or_else(|| {
194                InvalidSnapshotError::ValueError("Failed to get token".to_string())
195            })?;
196            state.spot_price(token_in, token_out)?;
197        }
198
199        Ok(state)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use std::str::FromStr;
206
207    use chrono::DateTime;
208    use rstest::rstest;
209    use tycho_common::dto::{Chain, ChangeType, ProtocolComponent, ResponseProtocolState};
210
211    use super::*;
212
213    fn usv4_component() -> ProtocolComponent {
214        let creation_time = DateTime::from_timestamp(1622526000, 0)
215            .unwrap()
216            .naive_utc();
217
218        let static_attributes: HashMap<String, Bytes> = HashMap::from([
219            ("key_lp_fee".to_string(), Bytes::from(500_i32.to_be_bytes().to_vec())),
220            ("tick_spacing".to_string(), Bytes::from(60_i32.to_be_bytes().to_vec())),
221        ]);
222
223        ProtocolComponent {
224            id: "State1".to_string(),
225            protocol_system: "system1".to_string(),
226            protocol_type_name: "typename1".to_string(),
227            chain: Chain::Ethereum,
228            tokens: Vec::new(),
229            contract_ids: Vec::new(),
230            static_attributes,
231            change: ChangeType::Creation,
232            creation_tx: Bytes::from_str("0x0000").unwrap(),
233            created_at: creation_time,
234        }
235    }
236
237    fn usv4_attributes() -> HashMap<String, Bytes> {
238        HashMap::from([
239            ("liquidity".to_string(), Bytes::from(100_u64.to_be_bytes().to_vec())),
240            ("tick".to_string(), Bytes::from(300_i32.to_be_bytes().to_vec())),
241            (
242                "sqrt_price_x96".to_string(),
243                Bytes::from(
244                    79228162514264337593543950336_u128
245                        .to_be_bytes()
246                        .to_vec(),
247                ),
248            ),
249            ("protocol_fees/zero2one".to_string(), Bytes::from(0_u32.to_be_bytes().to_vec())),
250            ("protocol_fees/one2zero".to_string(), Bytes::from(0_u32.to_be_bytes().to_vec())),
251            ("ticks/60/net_liquidity".to_string(), Bytes::from(400_i128.to_be_bytes().to_vec())),
252        ])
253    }
254    fn header() -> BlockHeader {
255        BlockHeader {
256            number: 1,
257            hash: Bytes::from(vec![0; 32]),
258            parent_hash: Bytes::from(vec![0; 32]),
259            revert: false,
260            timestamp: 1,
261        }
262    }
263
264    #[tokio::test]
265    async fn test_usv4_try_from() {
266        let snapshot = ComponentWithState {
267            state: ResponseProtocolState {
268                component_id: "State1".to_owned(),
269                attributes: usv4_attributes(),
270                balances: HashMap::new(),
271            },
272            component: usv4_component(),
273            component_tvl: None,
274            entrypoints: Vec::new(),
275        };
276
277        let result = UniswapV4State::try_from_with_header(
278            snapshot,
279            header(),
280            &HashMap::new(),
281            &HashMap::new(),
282            &DecoderContext::new(),
283        )
284        .await
285        .unwrap();
286
287        let fees = UniswapV4Fees::new(0, 0, 500);
288        let expected = UniswapV4State::new(
289            100,
290            U256::from(79228162514264337593543950336_u128),
291            fees,
292            300,
293            60,
294            vec![TickInfo::new(60, 400).unwrap()],
295        )
296        .unwrap();
297        assert_eq!(result, expected);
298    }
299
300    #[tokio::test]
301    #[rstest]
302    #[case::missing_liquidity("liquidity")]
303    #[case::missing_sqrt_price("sqrt_price")]
304    #[case::missing_tick("tick")]
305    #[case::missing_tick_liquidity("tick_liquidities")]
306    #[case::missing_fee("key_lp_fee")]
307    #[case::missing_fee("protocol_fees/one2zero")]
308    #[case::missing_fee("protocol_fees/zero2one")]
309    async fn test_usv4_try_from_invalid(#[case] missing_attribute: String) {
310        // remove missing attribute
311        let mut component = usv4_component();
312        let mut attributes = usv4_attributes();
313        attributes.remove(&missing_attribute);
314
315        if missing_attribute == "tick_liquidities" {
316            attributes.remove("ticks/60/net_liquidity");
317        }
318
319        if missing_attribute == "sqrt_price" {
320            attributes.remove("sqrt_price_x96");
321        }
322
323        if missing_attribute == "key_lp_fee" {
324            component
325                .static_attributes
326                .remove("key_lp_fee");
327        }
328
329        let snapshot = ComponentWithState {
330            state: ResponseProtocolState {
331                component_id: "State1".to_owned(),
332                attributes,
333                balances: HashMap::new(),
334            },
335            component,
336            component_tvl: None,
337            entrypoints: Vec::new(),
338        };
339
340        let result = UniswapV4State::try_from_with_header(
341            snapshot,
342            header(),
343            &HashMap::new(),
344            &HashMap::new(),
345            &DecoderContext::new(),
346        )
347        .await;
348
349        assert!(result.is_err());
350        assert!(matches!(
351            result.err().unwrap(),
352            InvalidSnapshotError::MissingAttribute(attr) if attr == missing_attribute
353        ));
354    }
355}