1use std::{collections::HashMap, fmt::Debug};
2
3use alloy::{core::sol, dyn_abi::SolType, primitives::U256, sol_types::SolCall};
4use revm::DatabaseRef;
5use tycho_common::{
6 models::{token::Token, Address},
7 simulation::errors::SimulationError,
8 Bytes,
9};
10
11use crate::evm::{
12 engine_db::engine_db_interface::EngineDatabaseInterface,
13 protocol::fluid::FluidV1,
14 simulation::{BlockEnvOverrides, SimulationEngine, SimulationParameters},
15};
16
17sol! {
18 struct CollateralReserves {
19 uint token0RealReserves;
20 uint token1RealReserves;
21 uint token0ImaginaryReserves;
22 uint token1ImaginaryReserves;
23 }
24
25 struct DebtReserves {
26 uint token0Debt;
27 uint token1Debt;
28 uint token0RealReserves;
29 uint token1RealReserves;
30 uint token0ImaginaryReserves;
31 uint token1ImaginaryReserves;
32 }
33
34 struct TokenLimit {
35 uint256 available; uint256 expandsTo; uint256 expandDuration; }
39
40 struct DexLimits {
41 TokenLimit withdrawableToken0;
42 TokenLimit withdrawableToken1;
43 TokenLimit borrowableToken0;
44 TokenLimit borrowableToken1;
45 }
46
47 struct PoolWithReserves {
48 address pool;
49 address token0;
50 address token1;
51 uint256 fee;
52 uint256 centerPrice;
53 CollateralReserves collateralReserves;
54 DebtReserves debtReserves;
55 DexLimits limits;
56 }
57
58 function getPoolReservesAdjusted(address pool_) public returns (PoolWithReserves memory poolReserves_);
59}
60
61pub const POOL_RESERVES_ADJUSTED_ATTRIBUTE: &str = "pool_reserves_adjusted";
63pub const BLOCK_TIMESTAMP_ATTRIBUTE: &str = "block_timestamp";
65
66pub fn pending_state_attributes(reserves: Vec<u8>, block_timestamp: u64) -> [(String, Bytes); 2] {
68 [
69 (POOL_RESERVES_ADJUSTED_ATTRIBUTE.to_string(), Bytes::from(reserves)),
70 (
71 BLOCK_TIMESTAMP_ATTRIBUTE.to_string(),
72 Bytes::from(block_timestamp.to_be_bytes().to_vec()),
73 ),
74 ]
75}
76
77#[derive(Debug, PartialEq)]
79pub(super) struct FluidPoolState {
80 pub(super) collateral_reserves: super::v1::CollateralReserves,
81 pub(super) debt_reserves: super::v1::DebtReserves,
82 pub(super) dex_limits: super::v1::DexLimits,
83 pub(super) center_price: U256,
84 pub(super) fee: U256,
85 pub(super) sync_time: u64,
86}
87
88pub fn decode_from_vm<D: EngineDatabaseInterface + Clone + Debug>(
89 pool: &Address,
90 token0: &Token,
91 token1: &Token,
92 resolver_address: &[u8],
93 vm: &SimulationEngine<D>,
94) -> Result<FluidV1, SimulationError>
95where
96 <D as DatabaseRef>::Error: Debug,
97 <D as EngineDatabaseInterface>::Error: Debug,
98{
99 let state = fetch_pool_state(pool, resolver_address, vm)?;
100 Ok(FluidV1::new(
101 pool,
102 token0,
103 token1,
104 state.collateral_reserves,
105 state.debt_reserves,
106 state.dex_limits,
107 state.center_price,
108 state.fee,
109 state.sync_time,
110 ))
111}
112
113#[derive(Debug, Clone, Default)]
116pub struct ResolverOverrides {
117 pub storage: Option<HashMap<alloy::primitives::Address, HashMap<U256, U256>>>,
118 pub native_balances: Option<HashMap<alloy::primitives::Address, U256>>,
119 pub block: Option<BlockEnvOverrides>,
120}
121
122pub fn call_resolver<D: EngineDatabaseInterface + Clone + Debug>(
127 pool: &Address,
128 resolver_address: &[u8],
129 engine: &SimulationEngine<D>,
130 overrides: &ResolverOverrides,
131) -> Result<Vec<u8>, SimulationError>
132where
133 <D as DatabaseRef>::Error: Debug,
134 <D as EngineDatabaseInterface>::Error: Debug,
135{
136 let reserves_call = getPoolReservesAdjustedCall {
137 pool_: alloy::primitives::Address::from_slice(pool.as_ref()),
138 };
139 let data = reserves_call.abi_encode();
140
141 let to = alloy::primitives::Address::from_slice(resolver_address);
142 let params = SimulationParameters {
143 caller: alloy::primitives::Address::ZERO,
144 to,
145 data,
146 overrides: overrides.storage.clone(),
147 block_overrides: overrides.block.clone(),
148 native_balance_overrides: overrides.native_balances.clone(),
149 ..Default::default()
150 };
151
152 let res = engine
153 .simulate(¶ms)
154 .map_err(|e| SimulationError::FatalError(format!("{e}")))?;
155 Ok(res.result.to_vec())
156}
157
158pub(super) fn fetch_pool_state<D: EngineDatabaseInterface + Clone + Debug>(
161 pool: &Address,
162 resolver_address: &[u8],
163 engine: &SimulationEngine<D>,
164) -> Result<FluidPoolState, SimulationError>
165where
166 <D as DatabaseRef>::Error: Debug,
167 <D as EngineDatabaseInterface>::Error: Debug,
168{
169 let bytes = call_resolver(pool, resolver_address, engine, &ResolverOverrides::default())?;
170 let sync_time = engine
171 .state
172 .get_current_block()
173 .ok_or_else(|| {
174 SimulationError::FatalError(format!(
175 "VM block not set while decoding state for FluidV1: 0x{:x}",
176 pool
177 ))
178 })?
179 .timestamp;
180 decode_reserves(&bytes, sync_time)
181}
182
183pub(super) fn decode_reserves(
186 bytes: &[u8],
187 sync_time: u64,
188) -> Result<FluidPoolState, SimulationError> {
189 let pool_w_reserves = PoolWithReserves::abi_decode(bytes).map_err(|e| {
190 SimulationError::FatalError(format!(
191 "Failed to decode pool reserves: {e} 0x{encoded}",
192 encoded = hex::encode(bytes)
193 ))
194 })?;
195 Ok(FluidPoolState {
196 collateral_reserves: super::v1::CollateralReserves {
197 token0_real_reserves: pool_w_reserves
198 .collateralReserves
199 .token0RealReserves,
200 token1_real_reserves: pool_w_reserves
201 .collateralReserves
202 .token1RealReserves,
203 token0_imaginary_reserves: pool_w_reserves
204 .collateralReserves
205 .token0ImaginaryReserves,
206 token1_imaginary_reserves: pool_w_reserves
207 .collateralReserves
208 .token1ImaginaryReserves,
209 },
210 debt_reserves: super::v1::DebtReserves {
211 token0_real_reserves: pool_w_reserves
212 .debtReserves
213 .token0RealReserves,
214 token1_real_reserves: pool_w_reserves
215 .debtReserves
216 .token1RealReserves,
217 token0_imaginary_reserves: pool_w_reserves
218 .debtReserves
219 .token0ImaginaryReserves,
220 token1_imaginary_reserves: pool_w_reserves
221 .debtReserves
222 .token1ImaginaryReserves,
223 },
224 dex_limits: super::v1::DexLimits {
225 borrowable_token0: super::v1::TokenLimit {
226 available: pool_w_reserves
227 .limits
228 .borrowableToken0
229 .available,
230 expands_to: pool_w_reserves
231 .limits
232 .borrowableToken0
233 .expandsTo,
234 expand_duration: pool_w_reserves
235 .limits
236 .borrowableToken0
237 .expandDuration,
238 },
239 borrowable_token1: super::v1::TokenLimit {
240 available: pool_w_reserves
241 .limits
242 .borrowableToken1
243 .available,
244 expands_to: pool_w_reserves
245 .limits
246 .borrowableToken1
247 .expandsTo,
248 expand_duration: pool_w_reserves
249 .limits
250 .borrowableToken1
251 .expandDuration,
252 },
253 withdrawable_token0: super::v1::TokenLimit {
254 available: pool_w_reserves
255 .limits
256 .withdrawableToken0
257 .available,
258 expands_to: pool_w_reserves
259 .limits
260 .withdrawableToken0
261 .expandsTo,
262 expand_duration: pool_w_reserves
263 .limits
264 .withdrawableToken0
265 .expandDuration,
266 },
267 withdrawable_token1: super::v1::TokenLimit {
268 available: pool_w_reserves
269 .limits
270 .withdrawableToken1
271 .available,
272 expands_to: pool_w_reserves
273 .limits
274 .withdrawableToken1
275 .expandsTo,
276 expand_duration: pool_w_reserves
277 .limits
278 .withdrawableToken1
279 .expandDuration,
280 },
281 },
282 center_price: pool_w_reserves.centerPrice,
283 fee: pool_w_reserves.fee,
284 sync_time,
285 })
286}
287
288#[cfg(test)]
291pub(super) fn sample_pool_with_reserves() -> PoolWithReserves {
292 PoolWithReserves {
293 pool: alloy::primitives::Address::ZERO,
294 token0: alloy::primitives::Address::ZERO,
295 token1: alloy::primitives::Address::ZERO,
296 fee: U256::from(41u64),
297 centerPrice: U256::from(42u64),
298 collateralReserves: CollateralReserves {
299 token0RealReserves: U256::from(1u64),
300 token1RealReserves: U256::from(2u64),
301 token0ImaginaryReserves: U256::from(3u64),
302 token1ImaginaryReserves: U256::from(4u64),
303 },
304 debtReserves: DebtReserves {
305 token0Debt: U256::from(11u64),
306 token1Debt: U256::from(12u64),
307 token0RealReserves: U256::from(13u64),
308 token1RealReserves: U256::from(14u64),
309 token0ImaginaryReserves: U256::from(15u64),
310 token1ImaginaryReserves: U256::from(16u64),
311 },
312 limits: DexLimits {
313 withdrawableToken0: TokenLimit {
314 available: U256::from(21u64),
315 expandsTo: U256::from(22u64),
316 expandDuration: U256::from(23u64),
317 },
318 withdrawableToken1: TokenLimit {
319 available: U256::from(24u64),
320 expandsTo: U256::from(25u64),
321 expandDuration: U256::from(26u64),
322 },
323 borrowableToken0: TokenLimit {
324 available: U256::from(27u64),
325 expandsTo: U256::from(28u64),
326 expandDuration: U256::from(29u64),
327 },
328 borrowableToken1: TokenLimit {
329 available: U256::from(30u64),
330 expandsTo: U256::from(31u64),
331 expandDuration: U256::from(32u64),
332 },
333 },
334 }
335}
336
337#[cfg(test)]
339pub(super) fn sample_pool_state(sync_time: u64) -> FluidPoolState {
340 FluidPoolState {
341 collateral_reserves: super::v1::CollateralReserves {
342 token0_real_reserves: U256::from(1u64),
343 token1_real_reserves: U256::from(2u64),
344 token0_imaginary_reserves: U256::from(3u64),
345 token1_imaginary_reserves: U256::from(4u64),
346 },
347 debt_reserves: super::v1::DebtReserves {
348 token0_real_reserves: U256::from(13u64),
349 token1_real_reserves: U256::from(14u64),
350 token0_imaginary_reserves: U256::from(15u64),
351 token1_imaginary_reserves: U256::from(16u64),
352 },
353 dex_limits: super::v1::DexLimits {
354 borrowable_token0: super::v1::TokenLimit {
355 available: U256::from(27u64),
356 expands_to: U256::from(28u64),
357 expand_duration: U256::from(29u64),
358 },
359 borrowable_token1: super::v1::TokenLimit {
360 available: U256::from(30u64),
361 expands_to: U256::from(31u64),
362 expand_duration: U256::from(32u64),
363 },
364 withdrawable_token0: super::v1::TokenLimit {
365 available: U256::from(21u64),
366 expands_to: U256::from(22u64),
367 expand_duration: U256::from(23u64),
368 },
369 withdrawable_token1: super::v1::TokenLimit {
370 available: U256::from(24u64),
371 expands_to: U256::from(25u64),
372 expand_duration: U256::from(26u64),
373 },
374 },
375 center_price: U256::from(42u64),
376 fee: U256::from(41u64),
377 sync_time,
378 }
379}
380
381#[cfg(test)]
382mod test {
383 use std::{collections::HashMap, str::FromStr};
384
385 use alloy::{primitives::U256, sol_types::SolValue};
386 use revm::state::{AccountInfo, Bytecode};
387 use tycho_client::feed::BlockHeader;
388 use tycho_common::{
389 models::{token::Token, Chain},
390 Bytes,
391 };
392
393 use crate::evm::{
394 engine_db::{
395 engine_db_interface::EngineDatabaseInterface,
396 simulation_db::SimulationDB,
397 utils::{get_client, get_runtime},
398 },
399 protocol::fluid::vm::{
400 call_resolver, decode_from_vm, decode_reserves, pending_state_attributes,
401 ResolverOverrides, BLOCK_TIMESTAMP_ATTRIBUTE, POOL_RESERVES_ADJUSTED_ATTRIBUTE,
402 },
403 simulation::{BlockEnvOverrides, SimulationEngine},
404 };
405
406 #[test]
407 #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
408 fn test_decode_simulation_db() {
409 let wsteth = Token::new(
410 &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
411 "wsteth",
412 18,
413 0,
414 &[Some(20000)],
415 Chain::Ethereum,
416 100,
417 );
418 let eth = Token::new(
419 &Bytes::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").unwrap(),
420 "ETH",
421 18,
422 0,
423 &[Some(2000)],
424 Chain::Ethereum,
425 100,
426 );
427
428 let block = BlockHeader {
429 number: 23526115,
430 hash: Bytes::from_str(
431 "0xfe5df4d77d2e4ce5660f2329084d5ef238b6671bdcf961ce0a510071af7a2275",
432 )
433 .unwrap(),
434 timestamp: 1759842947,
435 ..Default::default()
436 };
437 let mut db = SimulationDB::new(get_client(None).unwrap(), get_runtime().unwrap(), None);
438 db.set_block(Some(block));
439 let vm = SimulationEngine::new(db, false);
440
441 decode_from_vm(
442 &Bytes::from("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7"),
443 &wsteth,
444 ð,
445 &Bytes::from("0xC93876C0EEd99645DD53937b25433e311881A27C"),
446 &vm,
447 )
448 .expect("decoding failed");
449 }
450
451 #[test]
452 fn test_decode_reserves() {
453 let encoded = SolValue::abi_encode(&super::sample_pool_with_reserves());
454
455 let fields = decode_reserves(&encoded, 1_700_000_000).expect("decoding failed");
456
457 assert_eq!(fields, super::sample_pool_state(1_700_000_000));
458 }
459
460 const SELFBALANCE: u8 = 0x47;
461 const NUMBER: u8 = 0x43;
462 const TIMESTAMP: u8 = 0x42;
463
464 fn bytecode_reporting_engine(
465 resolver: alloy::primitives::Address,
466 bytecode: Vec<u8>,
467 ) -> SimulationEngine<SimulationDB<crate::evm::engine_db::simulation_db::EVMProvider>> {
468 let bytecode = Bytecode::new_raw(alloy::primitives::Bytes::from(bytecode));
469 let mut db = SimulationDB::new(get_client(None).unwrap(), get_runtime().unwrap(), None);
470 db.init_account(
471 resolver,
472 AccountInfo::new(U256::ZERO, 0, bytecode.hash_slow(), bytecode),
473 None,
474 true,
475 )
476 .expect("failed to init resolver account");
477 db.init_account(alloy::primitives::Address::ZERO, AccountInfo::default(), None, true)
478 .expect("failed to init caller account");
479 db.set_block(Some(BlockHeader { number: 1, timestamp: 2, ..Default::default() }));
480 SimulationEngine::new(db, false)
481 }
482
483 fn opcode_reporting_engine(
485 resolver: alloy::primitives::Address,
486 opcode: u8,
487 ) -> SimulationEngine<SimulationDB<crate::evm::engine_db::simulation_db::EVMProvider>> {
488 bytecode_reporting_engine(
489 resolver,
490 vec![opcode, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3],
491 )
492 }
493
494 #[test]
495 fn test_pending_state_attributes() {
496 let attributes = HashMap::from(pending_state_attributes(vec![1, 2, 3], 42));
497
498 assert_eq!(
499 attributes.get(POOL_RESERVES_ADJUSTED_ATTRIBUTE),
500 Some(&Bytes::from(vec![1, 2, 3]))
501 );
502 assert_eq!(
503 attributes.get(BLOCK_TIMESTAMP_ATTRIBUTE),
504 Some(&Bytes::from(42_u64.to_be_bytes().to_vec()))
505 );
506 }
507
508 #[test]
509 #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
510 fn test_call_resolver_applies_native_balance_override() {
511 let resolver = Bytes::from("0xC93876C0EEd99645DD53937b25433e311881A27C");
512 let resolver_address = alloy::primitives::Address::from_slice(resolver.as_ref());
513 let engine = opcode_reporting_engine(resolver_address, SELFBALANCE);
514 let pool = Bytes::from("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7");
515 let pending_balance = U256::from(4_200_000_000_000_000_000u64);
516
517 let confirmed = call_resolver(&pool, &resolver, &engine, &ResolverOverrides::default())
518 .expect("resolver call failed");
519 let pending = call_resolver(
520 &pool,
521 &resolver,
522 &engine,
523 &ResolverOverrides {
524 native_balances: Some(HashMap::from([(resolver_address, pending_balance)])),
525 ..Default::default()
526 },
527 )
528 .expect("resolver call failed");
529
530 assert_eq!(
531 U256::from_be_slice(&confirmed),
532 U256::ZERO,
533 "Without an override the resolver must see the confirmed balance."
534 );
535 assert_eq!(
536 U256::from_be_slice(&pending),
537 pending_balance,
538 "The overridden balance must reach the resolver's execution."
539 );
540 }
541
542 #[test]
543 #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
544 fn test_call_resolver_applies_block_env_overrides() {
545 let resolver = Bytes::from("0xC93876C0EEd99645DD53937b25433e311881A27C");
546 let resolver_address = alloy::primitives::Address::from_slice(resolver.as_ref());
547 let pool = Bytes::from("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7");
548 let overrides = ResolverOverrides {
549 block: Some(BlockEnvOverrides {
550 number: Some(23_526_115),
551 timestamp: Some(1_759_842_947),
552 }),
553 ..Default::default()
554 };
555
556 let number = call_resolver(
557 &pool,
558 &resolver,
559 &opcode_reporting_engine(resolver_address, NUMBER),
560 &overrides,
561 )
562 .expect("resolver call failed");
563 let timestamp = call_resolver(
564 &pool,
565 &resolver,
566 &opcode_reporting_engine(resolver_address, TIMESTAMP),
567 &overrides,
568 )
569 .expect("resolver call failed");
570
571 assert_eq!(
572 U256::from_be_slice(&number),
573 U256::from(23_526_115),
574 "The overridden block number must reach the resolver's execution."
575 );
576 assert_eq!(
577 U256::from_be_slice(×tamp),
578 U256::from(1_759_842_947),
579 "The overridden block timestamp must reach the resolver's execution."
580 );
581 }
582
583 #[test]
584 #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
585 fn test_call_resolver_without_overrides_reads_confirmed_block() {
586 let resolver = Bytes::from("0xC93876C0EEd99645DD53937b25433e311881A27C");
587 let resolver_address = alloy::primitives::Address::from_slice(resolver.as_ref());
588 let pool = Bytes::from("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7");
589
590 let number = call_resolver(
591 &pool,
592 &resolver,
593 &opcode_reporting_engine(resolver_address, NUMBER),
594 &ResolverOverrides::default(),
595 )
596 .expect("resolver call failed");
597
598 assert_eq!(
599 U256::from_be_slice(&number),
600 U256::ONE,
601 "Default overrides must leave the engine's confirmed block in place."
602 );
603 }
604
605 #[test]
606 #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
607 fn test_call_resolver_applies_storage_override() {
608 let resolver = Bytes::from("0xC93876C0EEd99645DD53937b25433e311881A27C");
609 let resolver_address = alloy::primitives::Address::from_slice(resolver.as_ref());
610 let pool = Bytes::from("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7");
611 let expected = U256::from(123);
612 let engine = bytecode_reporting_engine(
613 resolver_address,
614 vec![
615 0x60, 0x00, 0x54, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3, ],
623 );
624 let overrides = ResolverOverrides {
625 storage: Some(HashMap::from([(
626 resolver_address,
627 HashMap::from([(U256::ZERO, expected)]),
628 )])),
629 ..Default::default()
630 };
631
632 let result =
633 call_resolver(&pool, &resolver, &engine, &overrides).expect("resolver call failed");
634
635 assert_eq!(U256::from_be_slice(&result), expected);
636 }
637
638 #[test]
639 fn test_decode_reserves_truncated_bytes() {
640 let encoded = SolValue::abi_encode(&super::sample_pool_with_reserves());
641
642 let result = decode_reserves(&encoded[..encoded.len() - 32], 1_700_000_000);
643
644 assert!(result.is_err(), "truncated resolver bytes must fail to decode");
645 }
646}