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