Skip to main content

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        block_overrides: None,
146    };
147
148    let res = vm_engine
149        .simulate(&params)
150        .map_err(|e| SimulationError::FatalError(format!("{method} simulate failed: {e}")))?;
151
152    Call::abi_decode_returns(res.result.as_ref())
153        .map_err(|e| SimulationError::FatalError(format!("{method} decode failed: {e}")))
154}
155
156#[cfg(test)]
157mod test {
158    use std::str::FromStr;
159
160    use tycho_client::feed::BlockHeader;
161    use tycho_common::{
162        models::{token::Token, Chain},
163        Bytes,
164    };
165
166    use crate::evm::{
167        engine_db::{
168            simulation_db::SimulationDB,
169            utils::{get_client, get_runtime},
170        },
171        protocol::erc4626::vm::decode_from_vm,
172        simulation::SimulationEngine,
173    };
174
175    #[test]
176    #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
177    fn test_decode_simulation_db() {
178        let usdc = Token::new(
179            &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
180            "usdc",
181            6,
182            0,
183            &[Some(20000)],
184            Chain::Ethereum,
185            100,
186        );
187        let sp_usdc = Token::new(
188            &Bytes::from_str("0x28B3a8fb53B741A8Fd78c0fb9A6B2393d896a43d").unwrap(),
189            "sp_usdc",
190            6,
191            0,
192            &[Some(2000)],
193            Chain::Ethereum,
194            100,
195        );
196
197        let block = BlockHeader {
198            number: 23881700,
199            hash: Bytes::from_str(
200                "0xb11cb57ba2620d0f31da3a3c531977707569b796003ba65c44eaca990e6f2957",
201            )
202            .unwrap(),
203            timestamp: 1764145355,
204            ..Default::default()
205        };
206        let mut db = SimulationDB::new(get_client(None).unwrap(), get_runtime().unwrap(), None);
207        db.set_block(Some(block));
208        let vm = SimulationEngine::new(db, false);
209
210        decode_from_vm(
211            &Bytes::from("0x28B3a8fb53B741A8Fd78c0fb9A6B2393d896a43d"),
212            &usdc,
213            &sp_usdc,
214            vm,
215        )
216        .expect("decoding failed");
217    }
218}