tycho_simulation/evm/protocol/uniswap_v4/
decoder.rs1use 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 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 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 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 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}