tycho_simulation/evm/protocol/erc4626/
vm.rs

1use std::{collections::HashMap, fmt::Debug};
2
3use alloy::{
4    core::sol,
5    primitives::{Address as AlloyAddress, U256},
6    sol_types::SolCall,
7};
8use revm::{primitives::KECCAK_EMPTY, state::AccountInfo, DatabaseRef};
9use tycho_common::{
10    models::{token::Token, Address},
11    simulation::errors::SimulationError,
12};
13use tycho_ethereum::BytesCodec;
14
15use crate::evm::{
16    engine_db::engine_db_interface::EngineDatabaseInterface,
17    protocol::{
18        erc4626::state::ERC4626State,
19        vm::{
20            constants::{EXTERNAL_ACCOUNT, MAX_BALANCE},
21            erc20_token::{Overwrites, TokenProxyOverwriteFactory},
22        },
23    },
24    simulation::{SimulationEngine, SimulationParameters},
25};
26
27sol! {
28    function convertToShares(uint256 assets) public returns (uint256);
29    function convertToAssets(uint256 shares) public returns (uint256);
30    function maxDeposit(address caller) external returns (uint256);
31    function maxWithdraw(address caller) external returns (uint256);
32    function totalSupply() external view returns (uint256);
33}
34
35pub fn decode_from_vm<D: EngineDatabaseInterface + Clone + Debug>(
36    pool: &Address,
37    asset_token: &Token,
38    share_token: &Token,
39    vm_engine: SimulationEngine<D>,
40) -> Result<ERC4626State, SimulationError>
41where
42    <D as DatabaseRef>::Error: Debug,
43    <D as EngineDatabaseInterface>::Error: Debug,
44{
45    let total_supply = simulate_and_decode_call(
46        &vm_engine,
47        pool,
48        AlloyAddress::ZERO,
49        totalSupplyCall {},
50        None,
51        "totalSupply",
52    )?;
53
54    let share_price = simulate_and_decode_call(
55        &vm_engine,
56        pool,
57        AlloyAddress::ZERO,
58        convertToAssetsCall { shares: U256::from(10).pow(U256::from(share_token.decimals)) },
59        None,
60        "convertToAssets",
61    )?;
62
63    let asset_price = simulate_and_decode_call(
64        &vm_engine,
65        pool,
66        AlloyAddress::ZERO,
67        convertToSharesCall { assets: U256::from(10).pow(U256::from(asset_token.decimals)) },
68        None,
69        "convertToShares",
70    )?;
71
72    vm_engine
73        .state
74        .init_account(
75            *EXTERNAL_ACCOUNT,
76            AccountInfo { balance: *MAX_BALANCE, nonce: 0, code_hash: KECCAK_EMPTY, code: None },
77            None,
78            false,
79        )
80        .map_err(|err| {
81            SimulationError::FatalError(format!(
82                "Failed to decode from vm: Failed to init external account: {err:?}"
83            ))
84        })?;
85
86    let mut factory = TokenProxyOverwriteFactory::new(
87        AlloyAddress::from_slice(asset_token.address.as_ref()),
88        None,
89    );
90    factory.set_balance(*MAX_BALANCE, *EXTERNAL_ACCOUNT);
91    let token_overwrites = factory.get_overwrites();
92
93    let caller = AlloyAddress::from_slice(&*EXTERNAL_ACCOUNT.0);
94
95    // Assume the caller has sufficient tokens. In simulation we only care about
96    // the protocol limits, not the caller’s actual token balance.
97    let max_deposit = simulate_and_decode_call(
98        &vm_engine,
99        pool,
100        caller,
101        maxDepositCall { caller },
102        Some(token_overwrites.clone()),
103        "maxDeposit",
104    )?;
105
106    // Use the vault's totalSupply as the upper bound for maxRedeem.
107    // This represents the maximum amount of shares that can be burned, since
108    // a user cannot redeem more shares than the total supply of the vault.
109    Ok(ERC4626State::new(
110        pool,
111        asset_token,
112        share_token,
113        asset_price,
114        share_price,
115        max_deposit,
116        total_supply,
117    ))
118}
119
120fn simulate_and_decode_call<D, Call, Ret>(
121    vm_engine: &SimulationEngine<D>,
122    pool: &Address,
123    caller: AlloyAddress,
124    call: Call,
125    overrides: Option<HashMap<AlloyAddress, Overwrites>>,
126    method: &str,
127) -> Result<Ret, SimulationError>
128where
129    D: EngineDatabaseInterface + Clone + Debug,
130    <D as DatabaseRef>::Error: Debug,
131    <D as EngineDatabaseInterface>::Error: Debug,
132    Call: SolCall<Return = Ret>,
133{
134    let data = call.abi_encode();
135    let to = AlloyAddress::from_bytes(pool);
136
137    let params = SimulationParameters {
138        caller,
139        to,
140        data,
141        value: U256::ZERO,
142        overrides,
143        gas_limit: None,
144        transient_storage: None,
145    };
146
147    let res = vm_engine
148        .simulate(&params)
149        .map_err(|e| SimulationError::FatalError(format!("{method} simulate failed: {e}")))?;
150
151    Call::abi_decode_returns(res.result.as_ref())
152        .map_err(|e| SimulationError::FatalError(format!("{method} decode failed: {e}")))
153}
154
155#[cfg(test)]
156mod test {
157    use std::str::FromStr;
158
159    use tycho_client::feed::BlockHeader;
160    use tycho_common::{
161        models::{token::Token, Chain},
162        Bytes,
163    };
164
165    use crate::evm::{
166        engine_db::{
167            simulation_db::SimulationDB,
168            utils::{get_client, get_runtime},
169        },
170        protocol::erc4626::vm::decode_from_vm,
171        simulation::SimulationEngine,
172    };
173
174    #[test]
175    #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
176    fn test_decode_simulation_db() {
177        let usdc = Token::new(
178            &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
179            "usdc",
180            6,
181            0,
182            &[Some(20000)],
183            Chain::Ethereum,
184            100,
185        );
186        let sp_usdc = Token::new(
187            &Bytes::from_str("0x28B3a8fb53B741A8Fd78c0fb9A6B2393d896a43d").unwrap(),
188            "sp_usdc",
189            6,
190            0,
191            &[Some(2000)],
192            Chain::Ethereum,
193            100,
194        );
195
196        let block = BlockHeader {
197            number: 23881700,
198            hash: Bytes::from_str(
199                "0xb11cb57ba2620d0f31da3a3c531977707569b796003ba65c44eaca990e6f2957",
200            )
201            .unwrap(),
202            timestamp: 1764145355,
203            ..Default::default()
204        };
205        let mut db = SimulationDB::new(get_client(None).unwrap(), get_runtime().unwrap(), None);
206        db.set_block(Some(block));
207        let vm = SimulationEngine::new(db, false);
208
209        decode_from_vm(
210            &Bytes::from("0x28B3a8fb53B741A8Fd78c0fb9A6B2393d896a43d"),
211            &usdc,
212            &sp_usdc,
213            vm,
214        )
215        .expect("decoding failed");
216    }
217}