Skip to content

Commit 7e4ccd4

Browse files
committed
feat: add slipstreams decoder
# Conflicts: # src/evm/protocol/aerodrome_slipstreams/decoder.rs # src/evm/protocol/aerodrome_slipstreams/mod.rs # src/evm/protocol/aerodrome_slipstreams/state.rs
1 parent b30352b commit 7e4ccd4

File tree

8 files changed

+621
-636
lines changed

8 files changed

+621
-636
lines changed

src/evm/protocol/aerodrome_slipstreams/decoder.rs

Lines changed: 130 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@ use alloy::primitives::U256;
44
use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
55
use tycho_common::{models::token::Token, Bytes};
66

7-
use super::{enums::FeeAmount, state::UniswapV3State};
7+
use super::state::AerodromeSlipstreamsState;
88
use crate::{
9-
evm::protocol::utils::uniswap::{i24_be_bytes_to_i32, tick_list::TickInfo},
9+
evm::protocol::utils::{
10+
slipstreams::{dynamic_fee_module::DynamicFeeConfig, observations::Observation},
11+
uniswap::{i24_be_bytes_to_i32, tick_list::TickInfo},
12+
},
1013
protocol::{
1114
errors::InvalidSnapshotError,
1215
models::{DecoderContext, TryFromWithBlock},
1316
},
1417
};
1518

16-
impl TryFromWithBlock<ComponentWithState, BlockHeader> for UniswapV3State {
19+
impl TryFromWithBlock<ComponentWithState, BlockHeader> for AerodromeSlipstreamsState {
1720
type Error = InvalidSnapshotError;
1821

19-
/// Decodes a `ComponentWithState` into a `UniswapV3State`. Errors with a `InvalidSnapshotError`
20-
/// if the snapshot is missing any required attributes or if the fee amount is not supported.
22+
/// Decodes a `ComponentWithState` into a `AerodromeSlipstreamsState`. Errors with a
23+
/// `InvalidSnapshotError` if the snapshot is missing any required attributes or if the fee
24+
/// amount is not supported.
2125
async fn try_from_with_header(
2226
snapshot: ComponentWithState,
23-
_block: BlockHeader,
27+
block: BlockHeader,
2428
_account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
2529
_all_tokens: &HashMap<Bytes, Token>,
2630
_decoder_context: &DecoderContext,
@@ -58,16 +62,87 @@ impl TryFromWithBlock<ComponentWithState, BlockHeader> for UniswapV3State {
5862
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("sqrt_price".to_string()))?,
5963
);
6064

61-
let fee_value = i32::from(
65+
let observation_index = u16::from(
66+
snapshot
67+
.component
68+
.static_attributes
69+
.get("observationIndex")
70+
.ok_or_else(|| {
71+
InvalidSnapshotError::MissingAttribute("observationIndex".to_string())
72+
})?
73+
.clone(),
74+
);
75+
76+
let observation_cardinality = u16::from(
77+
snapshot
78+
.state
79+
.attributes
80+
.get("observationCardinality")
81+
.ok_or_else(|| {
82+
InvalidSnapshotError::MissingAttribute("observationCardinality".to_string())
83+
})?
84+
.clone(),
85+
);
86+
87+
let dfc_base_fee = u32::from(
88+
snapshot
89+
.component
90+
.static_attributes
91+
.get("dfc_baseFee")
92+
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("dfc_baseFee".to_string()))?
93+
.clone(),
94+
);
95+
96+
let dfc_scaling_factor = u64::from(
97+
snapshot
98+
.state
99+
.attributes
100+
.get("dfc_scalingFactor")
101+
.ok_or_else(|| {
102+
InvalidSnapshotError::MissingAttribute("dfc_scalingFactor".to_string())
103+
})?
104+
.clone(),
105+
);
106+
107+
let dfc_fee_cap = u32::from(
108+
snapshot
109+
.component
110+
.static_attributes
111+
.get("dfc_feeCap")
112+
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("dfc_feeCap".to_string()))?
113+
.clone(),
114+
);
115+
116+
let tick_spacing = snapshot
117+
.component
118+
.static_attributes
119+
.get("tick_spacing")
120+
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("tick_spacing".to_string()))?
121+
.clone();
122+
123+
let tick_spacing_4_bytes = if tick_spacing.len() == 32 {
124+
// Make sure it only happens for 0 values, otherwise error.
125+
if tick_spacing == Bytes::zero(32) {
126+
Bytes::from([0; 4])
127+
} else {
128+
return Err(InvalidSnapshotError::ValueError(format!(
129+
"Tick Spacing bytes too long for {tick_spacing}, expected 4"
130+
)));
131+
}
132+
} else {
133+
tick_spacing
134+
};
135+
136+
let tick_spacing = i24_be_bytes_to_i32(&tick_spacing_4_bytes);
137+
138+
let default_fee = u32::from(
62139
snapshot
63140
.component
64141
.static_attributes
65-
.get("fee")
66-
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("fee".to_string()))?
142+
.get("default_fee")
143+
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("default_fee".to_string()))?
67144
.clone(),
68145
);
69-
let fee = FeeAmount::try_from(fee_value)
70-
.map_err(|_| InvalidSnapshotError::ValueError("Unsupported fee amount".to_string()))?;
71146

72147
let tick = snapshot
73148
.state
@@ -126,185 +201,53 @@ impl TryFromWithBlock<ComponentWithState, BlockHeader> for UniswapV3State {
126201

127202
ticks.sort_by_key(|tick| tick.index);
128203

129-
UniswapV3State::new(liquidity, sqrt_price, fee, tick, ticks)
130-
.map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
131-
}
132-
}
133-
134-
#[cfg(test)]
135-
mod 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(); //Sample timestamp
148-
149-
// Add a static attribute "fee"
150-
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-
// remove missing attribute
231-
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-
// set an invalid fee amount (100, 500, 3_000 and 10_000 are the only valid fee amounts)
279-
let mut component = usv3_component();
280-
component
281-
.static_attributes
282-
.insert("fee".to_string(), Bytes::from(4000_i32.to_be_bytes().to_vec()));
204+
let observations: Result<Vec<_>, _> = snapshot
205+
.state
206+
.attributes
207+
.iter()
208+
.filter_map(|(key, value)| {
209+
if key.starts_with("observations/") {
210+
Some(
211+
key.split('/')
212+
.nth(1)?
213+
.parse::<i32>()
214+
.map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
215+
.and_then(|observation_idx| {
216+
Observation::from_attribute(observation_idx, &value.clone())
217+
.map_err(|err| {
218+
InvalidSnapshotError::ValueError(err.to_string())
219+
})
220+
}),
221+
)
222+
} else {
223+
None
224+
}
225+
})
226+
.collect();
283227

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(),
228+
let mut observations = match observations {
229+
Ok(observations) if !observations.is_empty() => observations
230+
.into_iter()
231+
.filter(|t| t.initialized)
232+
.collect::<Vec<_>>(),
233+
_ => return Err(InvalidSnapshotError::MissingAttribute("observations".to_string())),
293234
};
294235

295-
let result = UniswapV3State::try_from_with_header(
296-
snapshot,
297-
header(),
298-
&HashMap::new(),
299-
&HashMap::new(),
300-
&DecoderContext::new(),
236+
observations.sort_by_key(|observation| observation.index);
237+
238+
AerodromeSlipstreamsState::new(
239+
block.timestamp,
240+
liquidity,
241+
sqrt_price,
242+
observation_index,
243+
observation_cardinality,
244+
default_fee,
245+
tick,
246+
tick_spacing,
247+
ticks,
248+
observations,
249+
DynamicFeeConfig::new(dfc_base_fee, dfc_fee_cap, dfc_scaling_factor),
301250
)
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-
));
251+
.map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
309252
}
310253
}
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
//! Uniswap V3 Decentralized Exchange
21
mod decoder;
3-
pub mod enums;
42
pub mod state;

0 commit comments

Comments
 (0)