Skip to main content

tycho_simulation/evm/protocol/fluid/
vm.rs

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; // maximum available swap amount
36        uint256 expandsTo; // maximum amount the available swap amount expands to
37        uint256 expandDuration; // duration for `available` to grow to `expandsTo`
38    }
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
61/// Attribute carrying the resolver's ABI-encoded `PoolWithReserves` output.
62pub const POOL_RESERVES_ADJUSTED_ATTRIBUTE: &str = "pool_reserves_adjusted";
63/// Attribute carrying the target block's timestamp as an eight-byte big-endian `u64`.
64pub const BLOCK_TIMESTAMP_ATTRIBUTE: &str = "block_timestamp";
65
66/// Encodes Fluid resolver output and its block timestamp as pending-state attributes.
67pub 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/// The pool state decoded from the resolver's `getPoolReservesAdjusted` return bytes.
78#[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/// State the resolver call runs against, overriding what the engine's database holds.
114/// `Default` overrides nothing and reads confirmed state.
115#[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
122/// Calls the reserves resolver for `pool` and returns the raw ABI-encoded return bytes.
123///
124/// Takes `overrides` by reference so one override set can be built once and reused across
125/// every pool priced against the same pending block.
126pub 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(&params)
154        .map_err(|e| SimulationError::FatalError(format!("{e}")))?;
155    Ok(res.result.to_vec())
156}
157
158/// Calls the reserves resolver for `pool` against confirmed state and decodes the result,
159/// stamping `sync_time` from the engine's current block.
160pub(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
183/// Decodes the resolver's ABI-encoded `PoolWithReserves` return bytes into pool state fields,
184/// stamping `sync_time` as the moment the reserves were observed.
185pub(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/// A `PoolWithReserves` sample with distinct values per field, so decode tests can detect
289/// any field-mapping mixup.
290#[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/// The `FluidPoolState` that decoding [`sample_pool_with_reserves`] must produce.
338#[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            &eth,
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    /// Returns the value pushed by `opcode` from the resolver execution environment.
484    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(&timestamp),
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, // PUSH1 0
616                0x54, // SLOAD
617                0x60, 0x00, // PUSH1 0
618                0x52, // MSTORE
619                0x60, 0x20, // PUSH1 32
620                0x60, 0x00, // PUSH1 0
621                0xf3, // RETURN
622            ],
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}