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 use crate::evm::protocol::test_utils::try_decode_snapshot_with_defaults;
213
214 fn usv4_component() -> ProtocolComponent {
215 let creation_time = DateTime::from_timestamp(1622526000, 0)
216 .unwrap()
217 .naive_utc();
218
219 let static_attributes: HashMap<String, Bytes> = HashMap::from([
220 ("key_lp_fee".to_string(), Bytes::from(500_i32.to_be_bytes().to_vec())),
221 ("tick_spacing".to_string(), Bytes::from(60_i32.to_be_bytes().to_vec())),
222 ]);
223
224 ProtocolComponent {
225 id: "State1".to_string(),
226 protocol_system: "system1".to_string(),
227 protocol_type_name: "typename1".to_string(),
228 chain: Chain::Ethereum,
229 tokens: Vec::new(),
230 contract_ids: Vec::new(),
231 static_attributes,
232 change: ChangeType::Creation,
233 creation_tx: Bytes::from_str("0x0000").unwrap(),
234 created_at: creation_time,
235 }
236 }
237
238 fn usv4_attributes() -> HashMap<String, Bytes> {
239 HashMap::from([
240 ("liquidity".to_string(), Bytes::from(100_u64.to_be_bytes().to_vec())),
241 ("tick".to_string(), Bytes::from(300_i32.to_be_bytes().to_vec())),
242 (
243 "sqrt_price_x96".to_string(),
244 Bytes::from(
245 79228162514264337593543950336_u128
246 .to_be_bytes()
247 .to_vec(),
248 ),
249 ),
250 ("protocol_fees/zero2one".to_string(), Bytes::from(0_u32.to_be_bytes().to_vec())),
251 ("protocol_fees/one2zero".to_string(), Bytes::from(0_u32.to_be_bytes().to_vec())),
252 ("ticks/60/net_liquidity".to_string(), Bytes::from(400_i128.to_be_bytes().to_vec())),
253 ])
254 }
255
256 #[tokio::test]
257 async fn test_usv4_try_from() {
258 let snapshot = ComponentWithState {
259 state: ResponseProtocolState {
260 component_id: "State1".to_owned(),
261 attributes: usv4_attributes(),
262 balances: HashMap::new(),
263 },
264 component: usv4_component(),
265 component_tvl: None,
266 entrypoints: Vec::new(),
267 };
268
269 let result = try_decode_snapshot_with_defaults::<UniswapV4State>(snapshot)
270 .await
271 .unwrap();
272
273 let fees = UniswapV4Fees::new(0, 0, 500);
274 let expected = UniswapV4State::new(
275 100,
276 U256::from(79228162514264337593543950336_u128),
277 fees,
278 300,
279 60,
280 vec![TickInfo::new(60, 400).unwrap()],
281 )
282 .unwrap();
283 assert_eq!(result, expected);
284 }
285
286 #[tokio::test]
287 #[rstest]
288 #[case::missing_liquidity("liquidity")]
289 #[case::missing_sqrt_price("sqrt_price")]
290 #[case::missing_tick("tick")]
291 #[case::missing_tick_liquidity("tick_liquidities")]
292 #[case::missing_fee("key_lp_fee")]
293 #[case::missing_fee("protocol_fees/one2zero")]
294 #[case::missing_fee("protocol_fees/zero2one")]
295 async fn test_usv4_try_from_invalid(#[case] missing_attribute: String) {
296 let mut component = usv4_component();
298 let mut attributes = usv4_attributes();
299 attributes.remove(&missing_attribute);
300
301 if missing_attribute == "tick_liquidities" {
302 attributes.remove("ticks/60/net_liquidity");
303 }
304
305 if missing_attribute == "sqrt_price" {
306 attributes.remove("sqrt_price_x96");
307 }
308
309 if missing_attribute == "key_lp_fee" {
310 component
311 .static_attributes
312 .remove("key_lp_fee");
313 }
314
315 let snapshot = ComponentWithState {
316 state: ResponseProtocolState {
317 component_id: "State1".to_owned(),
318 attributes,
319 balances: HashMap::new(),
320 },
321 component,
322 component_tvl: None,
323 entrypoints: Vec::new(),
324 };
325
326 let result = try_decode_snapshot_with_defaults::<UniswapV4State>(snapshot).await;
327
328 assert!(result.is_err());
329 assert!(matches!(
330 result.err().unwrap(),
331 InvalidSnapshotError::MissingAttribute(attr) if attr == missing_attribute
332 ));
333 }
334}