tycho_simulation/evm/protocol/uniswap_v3/
decoder.rs1use std::collections::HashMap;
2
3use alloy::primitives::U256;
4use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
5use tycho_common::{models::token::Token, Bytes};
6
7use super::{enums::FeeAmount, state::UniswapV3State};
8use crate::{
9 evm::protocol::utils::uniswap::{i24_be_bytes_to_i32, tick_list::TickInfo},
10 protocol::{
11 errors::InvalidSnapshotError,
12 models::{DecoderContext, TryFromWithBlock},
13 },
14};
15
16impl TryFromWithBlock<ComponentWithState, BlockHeader> for UniswapV3State {
17 type Error = InvalidSnapshotError;
18
19 async fn try_from_with_header(
22 snapshot: ComponentWithState,
23 _block: BlockHeader,
24 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
25 _all_tokens: &HashMap<Bytes, Token>,
26 _decoder_context: &DecoderContext,
27 ) -> Result<Self, Self::Error> {
28 let liq = snapshot
29 .state
30 .attributes
31 .get("liquidity")
32 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("liquidity".to_string()))?
33 .clone();
34
35 let liq_16_bytes = if liq.len() == 32 {
39 if liq == Bytes::zero(32) {
41 Bytes::from([0; 16])
42 } else {
43 return Err(InvalidSnapshotError::ValueError(format!(
44 "Liquidity bytes too long for {liq}, expected 16"
45 )));
46 }
47 } else {
48 liq
49 };
50
51 let liquidity = u128::from(liq_16_bytes);
52
53 let sqrt_price = U256::from_be_slice(
54 snapshot
55 .state
56 .attributes
57 .get("sqrt_price_x96")
58 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("sqrt_price".to_string()))?,
59 );
60
61 let fee_value = i32::from(
62 snapshot
63 .component
64 .static_attributes
65 .get("fee")
66 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("fee".to_string()))?
67 .clone(),
68 );
69 let fee = FeeAmount::try_from(fee_value)
70 .map_err(|_| InvalidSnapshotError::ValueError("Unsupported fee amount".to_string()))?;
71
72 let tick = snapshot
73 .state
74 .attributes
75 .get("tick")
76 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("tick".to_string()))?
77 .clone();
78
79 let ticks_4_bytes = if tick.len() == 32 {
83 if tick == Bytes::zero(32) {
85 Bytes::from([0; 4])
86 } else {
87 return Err(InvalidSnapshotError::ValueError(format!(
88 "Tick bytes too long for {tick}, expected 4"
89 )));
90 }
91 } else {
92 tick
93 };
94 let tick = i24_be_bytes_to_i32(&ticks_4_bytes);
95
96 let ticks: Result<Vec<_>, _> = snapshot
97 .state
98 .attributes
99 .iter()
100 .filter_map(|(key, value)| {
101 if key.starts_with("ticks/") {
102 Some(
103 key.split('/')
104 .nth(1)?
105 .parse::<i32>()
106 .map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
107 .and_then(|tick_index| {
108 TickInfo::new(tick_index, i128::from(value.clone())).map_err(
109 |err| InvalidSnapshotError::ValueError(err.to_string()),
110 )
111 }),
112 )
113 } else {
114 None
115 }
116 })
117 .collect();
118
119 let mut ticks = match ticks {
120 Ok(ticks) if !ticks.is_empty() => ticks
121 .into_iter()
122 .filter(|t| t.net_liquidity != 0)
123 .collect::<Vec<_>>(),
124 _ => return Err(InvalidSnapshotError::MissingAttribute("tick_liquidities".to_string())),
125 };
126
127 ticks.sort_by_key(|tick| tick.index);
128
129 UniswapV3State::new(liquidity, sqrt_price, fee, tick, ticks)
130 .map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use std::str::FromStr;
137
138 use chrono::DateTime;
139 use rstest::rstest;
140 use tycho_common::dto::{Chain, ChangeType, ProtocolComponent, ResponseProtocolState};
141
142 use super::*;
143
144 fn usv3_component() -> ProtocolComponent {
145 let creation_time = DateTime::from_timestamp(1622526000, 0)
146 .unwrap()
147 .naive_utc(); let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
151 static_attributes.insert("fee".to_string(), Bytes::from(3000_i32.to_be_bytes().to_vec()));
152
153 ProtocolComponent {
154 id: "State1".to_string(),
155 protocol_system: "system1".to_string(),
156 protocol_type_name: "typename1".to_string(),
157 chain: Chain::Ethereum,
158 tokens: Vec::new(),
159 contract_ids: Vec::new(),
160 static_attributes,
161 change: ChangeType::Creation,
162 creation_tx: Bytes::from_str("0x0000").unwrap(),
163 created_at: creation_time,
164 }
165 }
166
167 fn usv3_attributes() -> HashMap<String, Bytes> {
168 vec![
169 ("liquidity".to_string(), Bytes::from(100_u64.to_be_bytes().to_vec())),
170 ("sqrt_price_x96".to_string(), Bytes::from(200_u64.to_be_bytes().to_vec())),
171 ("tick".to_string(), Bytes::from(300_i32.to_be_bytes().to_vec())),
172 ("ticks/60/net_liquidity".to_string(), Bytes::from(400_i128.to_be_bytes().to_vec())),
173 ]
174 .into_iter()
175 .collect::<HashMap<String, Bytes>>()
176 }
177
178 fn header() -> BlockHeader {
179 BlockHeader {
180 number: 1,
181 hash: Bytes::from(vec![0; 32]),
182 parent_hash: Bytes::from(vec![0; 32]),
183 revert: false,
184 timestamp: 1,
185 }
186 }
187
188 #[tokio::test]
189 async fn test_usv3_try_from() {
190 let snapshot = ComponentWithState {
191 state: ResponseProtocolState {
192 component_id: "State1".to_owned(),
193 attributes: usv3_attributes(),
194 balances: HashMap::new(),
195 },
196 component: usv3_component(),
197 component_tvl: None,
198 entrypoints: Vec::new(),
199 };
200
201 let result = UniswapV3State::try_from_with_header(
202 snapshot,
203 header(),
204 &HashMap::new(),
205 &HashMap::new(),
206 &DecoderContext::new(),
207 )
208 .await;
209
210 assert!(result.is_ok());
211 let expected = UniswapV3State::new(
212 100,
213 U256::from(200),
214 FeeAmount::Medium,
215 300,
216 vec![TickInfo::new(60, 400).unwrap()],
217 )
218 .unwrap();
219 assert_eq!(result.unwrap(), expected);
220 }
221
222 #[tokio::test]
223 #[rstest]
224 #[case::missing_liquidity("liquidity")]
225 #[case::missing_sqrt_price("sqrt_price")]
226 #[case::missing_tick("tick")]
227 #[case::missing_tick_liquidity("tick_liquidities")]
228 #[case::missing_fee("fee")]
229 async fn test_usv3_try_from_invalid(#[case] missing_attribute: String) {
230 let mut attributes = usv3_attributes();
232 attributes.remove(&missing_attribute);
233
234 if missing_attribute == "tick_liquidities" {
235 attributes.remove("ticks/60/net_liquidity");
236 }
237
238 if missing_attribute == "sqrt_price" {
239 attributes.remove("sqrt_price_x96");
240 }
241
242 let mut component = usv3_component();
243 if missing_attribute == "fee" {
244 component
245 .static_attributes
246 .remove("fee");
247 }
248
249 let snapshot = ComponentWithState {
250 state: ResponseProtocolState {
251 component_id: "State1".to_owned(),
252 attributes,
253 balances: HashMap::new(),
254 },
255 component,
256 component_tvl: None,
257 entrypoints: Vec::new(),
258 };
259
260 let result = UniswapV3State::try_from_with_header(
261 snapshot,
262 header(),
263 &HashMap::new(),
264 &HashMap::new(),
265 &DecoderContext::new(),
266 )
267 .await;
268
269 assert!(result.is_err());
270 assert!(matches!(
271 result.err().unwrap(),
272 InvalidSnapshotError::MissingAttribute(attr) if attr == missing_attribute
273 ));
274 }
275
276 #[tokio::test]
277 async fn test_usv3_try_from_invalid_fee() {
278 let mut component = usv3_component();
280 component
281 .static_attributes
282 .insert("fee".to_string(), Bytes::from(4000_i32.to_be_bytes().to_vec()));
283
284 let snapshot = ComponentWithState {
285 state: ResponseProtocolState {
286 component_id: "State1".to_owned(),
287 attributes: usv3_attributes(),
288 balances: HashMap::new(),
289 },
290 component,
291 component_tvl: None,
292 entrypoints: Vec::new(),
293 };
294
295 let result = UniswapV3State::try_from_with_header(
296 snapshot,
297 header(),
298 &HashMap::new(),
299 &HashMap::new(),
300 &DecoderContext::new(),
301 )
302 .await;
303
304 assert!(result.is_err());
305 assert!(matches!(
306 result.err().unwrap(),
307 InvalidSnapshotError::ValueError(err) if err == *"Unsupported fee amount"
308 ));
309 }
310}