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