Skip to main content

tycho_simulation/evm/protocol/erc4626/
state.rs

1use std::{any::Any, collections::HashMap, fmt::Debug};
2
3use alloy::primitives::U256;
4use num_bigint::{BigUint, ToBigUint};
5use serde::{Deserialize, Serialize};
6use tracing::trace;
7use tycho_common::{
8    dto::ProtocolStateDelta,
9    models::token::Token,
10    simulation::{
11        errors::{SimulationError, TransitionError},
12        protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
13    },
14    Bytes,
15};
16
17use crate::evm::{
18    engine_db::{create_engine, SHARED_TYCHO_DB},
19    protocol::{
20        erc4626::vm,
21        u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
22    },
23};
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct ERC4626State {
27    pool_address: Bytes,
28    asset_token: Token,
29    share_token: Token,
30    asset_price: U256,
31    share_price: U256,
32    max_deposit: U256,
33    max_redeem: U256,
34}
35
36impl ERC4626State {
37    pub fn new(
38        pool_address: &Bytes,
39        asset_token: &Token,
40        share_token: &Token,
41        asset_price: U256,
42        share_price: U256,
43        max_deposit: U256,
44        max_redeem: U256,
45    ) -> Self {
46        Self {
47            pool_address: pool_address.clone(),
48            asset_token: asset_token.clone(),
49            share_token: share_token.clone(),
50            asset_price,
51            share_price,
52            max_deposit,
53            max_redeem,
54        }
55    }
56}
57
58#[typetag::serde]
59impl ProtocolSim for ERC4626State {
60    fn fee(&self) -> f64 {
61        0f64
62    }
63
64    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
65        let share_unit = U256::from(10).pow(U256::from(self.share_token.decimals));
66        let asset_unit = U256::from(10).pow(U256::from(self.asset_token.decimals));
67
68        let one_share_in_asset = u256_to_f64(self.share_price)? / u256_to_f64(asset_unit)?;
69        let one_asset_in_share = u256_to_f64(self.asset_price)? / u256_to_f64(share_unit)?;
70
71        if base.address == self.share_token.address && quote.address == self.asset_token.address {
72            return Ok(one_share_in_asset); // 1 share → asset
73        }
74
75        if base.address == self.asset_token.address && quote.address == self.share_token.address {
76            return Ok(one_asset_in_share); // 1 asset → share
77        }
78
79        Err(SimulationError::FatalError("invalid pair".into()))
80    }
81
82    fn get_amount_out(
83        &self,
84        amount_in: BigUint,
85        token_in: &Token,
86        token_out: &Token,
87    ) -> Result<GetAmountOutResult, SimulationError> {
88        let amount_in = biguint_to_u256(&amount_in);
89        if token_in.address == self.asset_token.address &&
90            token_out.address == self.share_token.address
91        {
92            // asset → share: this corresponds to an ERC4626.deposit operation.
93            // The user deposits underlying assets and receives vault shares.
94            Ok(GetAmountOutResult {
95                amount: u256_to_biguint(
96                    amount_in * self.asset_price /
97                        U256::from(10).pow(U256::from(self.asset_token.decimals)),
98                ),
99                gas: 86107.to_biguint().expect("infallible"),
100                new_state: self.clone_box(),
101            })
102        } else if token_in.address == self.share_token.address &&
103            token_out.address == self.asset_token.address
104        {
105            // share → asset: this corresponds to an ERC4626.redeem operation.
106            // The user burns vault shares and receives underlying assets.
107            Ok(GetAmountOutResult {
108                amount: u256_to_biguint(
109                    amount_in * self.share_price /
110                        U256::from(10).pow(U256::from(self.share_token.decimals)),
111                ),
112                gas: 74977.to_biguint().expect("infallible"),
113                new_state: self.clone_box(),
114            })
115        } else {
116            Err(SimulationError::FatalError(format!(
117                "Invalid token pair: {}, {}",
118                token_in.address, token_out.address
119            )))
120        }
121    }
122
123    fn get_limits(
124        &self,
125        sell_token: Bytes,
126        buy_token: Bytes,
127    ) -> Result<(BigUint, BigUint), SimulationError> {
128        let share_scale = BigUint::from(10u32).pow(self.share_token.decimals);
129        let asset_scale = BigUint::from(10u32).pow(self.asset_token.decimals);
130
131        if sell_token == self.share_token.address && buy_token == self.asset_token.address {
132            // asset_out_raw = shares_raw * share_price_raw / 10^share_decimals
133            let buy_raw = (&u256_to_biguint(self.max_redeem) * &u256_to_biguint(self.share_price)) /
134                &share_scale;
135            return Ok((u256_to_biguint(self.max_redeem), buy_raw));
136        }
137
138        if sell_token == self.asset_token.address && buy_token == self.share_token.address {
139            // share_out_raw = asset_raw * asset_price_raw / 10^asset_decimals
140            let buy_raw = (&u256_to_biguint(self.max_deposit) * &u256_to_biguint(self.asset_price)) /
141                &asset_scale;
142
143            return Ok((u256_to_biguint(self.max_deposit), buy_raw));
144        }
145
146        Err(SimulationError::FatalError(format!(
147            "Invalid token pair: {}, {}",
148            sell_token, buy_token
149        )))
150    }
151
152    fn delta_transition(
153        &mut self,
154        _delta: ProtocolStateDelta,
155        _tokens: &HashMap<Bytes, Token>,
156        _balances: &Balances,
157    ) -> Result<(), TransitionError> {
158        let engine =
159            create_engine(SHARED_TYCHO_DB.clone(), false).expect("Failed to create engine");
160
161        let state =
162            vm::decode_from_vm(&self.pool_address, &self.asset_token, &self.share_token, engine)?;
163        trace!(?state, "Calling delta transition for {}", &self.pool_address);
164
165        self.asset_price = state.asset_price;
166        self.share_price = state.share_price;
167        self.max_deposit = state.max_deposit;
168        self.max_redeem = state.max_redeem;
169        Ok(())
170    }
171
172    fn clone_box(&self) -> Box<dyn ProtocolSim> {
173        Box::new(self.clone())
174    }
175
176    fn as_any(&self) -> &dyn Any {
177        self
178    }
179
180    fn as_any_mut(&mut self) -> &mut dyn Any {
181        self
182    }
183
184    fn eq(&self, other: &dyn ProtocolSim) -> bool {
185        if let Some(other_state) = other
186            .as_any()
187            .downcast_ref::<ERC4626State>()
188        {
189            self.pool_address == other_state.pool_address &&
190                self.asset_token == other_state.asset_token &&
191                self.share_token == other_state.share_token &&
192                self.asset_price == other_state.asset_price &&
193                self.share_price == other_state.share_price &&
194                self.max_deposit == other_state.max_deposit &&
195                self.max_redeem == other_state.max_redeem
196        } else {
197            false
198        }
199    }
200
201    fn query_pool_swap(
202        &self,
203        params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
204    ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
205        crate::evm::query_pool_swap::query_pool_swap(self, params)
206    }
207}