Skip to main content

tycho_simulation/evm/protocol/native_wrapper/
state.rs

1use std::{any::Any, collections::HashMap};
2
3use chrono::NaiveDateTime;
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use tycho_common::{
7    dto::ProtocolStateDelta,
8    models::{token::Token, Chain},
9    simulation::{
10        errors::{SimulationError, TransitionError},
11        protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
12    },
13    Bytes,
14};
15
16use crate::protocol::models::ProtocolComponent;
17
18pub const NATIVE_WRAPPER_ID: &str = "native_wrapper";
19const NATIVE_WRAPPER_PROTOCOL_SYSTEM: &str = "native_wrapper";
20const NATIVE_WRAPPER_PROTOCOL_TYPE: &str = "NativeWrapper";
21const WRAP_GAS: u64 = 7_000;
22const UNWRAP_GAS: u64 = 14_000;
23
24/// Stateless 1:1 bridge between a chain's native token and its wrapped
25/// counterpart (e.g. ETH ↔ WETH).
26///
27/// This component is auto-injected by `ProtocolStreamBuilder` so every
28/// consumer automatically sees the bridge without manual wiring.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct NativeWrapperState {
31    native_token: Token,
32    wrapped_token: Token,
33}
34
35impl NativeWrapperState {
36    pub fn new(chain: Chain) -> Self {
37        Self { native_token: chain.native_token(), wrapped_token: chain.wrapped_native_token() }
38    }
39
40    /// Builds the `ProtocolComponent` metadata for stream injection.
41    pub fn component(chain: Chain) -> ProtocolComponent {
42        let native = chain.native_token();
43        let wrapped = chain.wrapped_native_token();
44        ProtocolComponent::new(
45            Bytes::from(NATIVE_WRAPPER_ID.as_bytes()),
46            NATIVE_WRAPPER_PROTOCOL_SYSTEM.to_string(),
47            NATIVE_WRAPPER_PROTOCOL_TYPE.to_string(),
48            chain,
49            vec![native, wrapped],
50            vec![],
51            HashMap::new(),
52            Bytes::default(),
53            NaiveDateTime::default(),
54        )
55    }
56
57    fn validate_tokens(&self, token_in: &Bytes, token_out: &Bytes) -> Result<(), SimulationError> {
58        let valid_pair = (*token_in == self.native_token.address &&
59            *token_out == self.wrapped_token.address) ||
60            (*token_in == self.wrapped_token.address && *token_out == self.native_token.address);
61        if !valid_pair {
62            return Err(SimulationError::InvalidInput(
63                format!(
64                    "NativeWrapper only supports {} ↔ {}, got {} → {}",
65                    self.native_token.address, self.wrapped_token.address, token_in, token_out,
66                ),
67                None,
68            ));
69        }
70        Ok(())
71    }
72}
73
74#[typetag::serde]
75impl ProtocolSim for NativeWrapperState {
76    fn fee(&self) -> f64 {
77        0.0
78    }
79
80    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
81        self.validate_tokens(&base.address, &quote.address)?;
82        Ok(1.0)
83    }
84
85    fn get_amount_out(
86        &self,
87        amount_in: BigUint,
88        token_in: &Token,
89        token_out: &Token,
90    ) -> Result<GetAmountOutResult, SimulationError> {
91        self.validate_tokens(&token_in.address, &token_out.address)?;
92        let is_wrapping = token_in.address == self.native_token.address;
93        let gas = if is_wrapping { WRAP_GAS } else { UNWRAP_GAS };
94        Ok(GetAmountOutResult::new(amount_in, BigUint::from(gas), self.clone_box()))
95    }
96
97    fn get_limits(
98        &self,
99        sell_token: Bytes,
100        buy_token: Bytes,
101    ) -> Result<(BigUint, BigUint), SimulationError> {
102        self.validate_tokens(&sell_token, &buy_token)?;
103        Ok((BigUint::from(u128::MAX), BigUint::from(u128::MAX)))
104    }
105
106    fn delta_transition(
107        &mut self,
108        _delta: ProtocolStateDelta,
109        _tokens: &HashMap<Bytes, Token>,
110        _balances: &Balances,
111    ) -> Result<(), TransitionError> {
112        Ok(())
113    }
114
115    fn clone_box(&self) -> Box<dyn ProtocolSim> {
116        Box::new(self.clone())
117    }
118
119    fn as_any(&self) -> &dyn Any {
120        self
121    }
122
123    fn as_any_mut(&mut self) -> &mut dyn Any {
124        self
125    }
126
127    fn eq(&self, other: &dyn ProtocolSim) -> bool {
128        other
129            .as_any()
130            .downcast_ref::<NativeWrapperState>()
131            .is_some_and(|o| {
132                self.native_token == o.native_token && self.wrapped_token == o.wrapped_token
133            })
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    fn eth_state() -> NativeWrapperState {
142        NativeWrapperState::new(Chain::Ethereum)
143    }
144
145    fn native_token() -> Token {
146        Chain::Ethereum.native_token()
147    }
148
149    fn wrapped_token() -> Token {
150        Chain::Ethereum.wrapped_native_token()
151    }
152
153    #[test]
154    fn test_fee_is_zero() {
155        assert_eq!(eth_state().fee(), 0.0);
156    }
157
158    #[test]
159    fn test_spot_price_is_one() {
160        let state = eth_state();
161        let price = state
162            .spot_price(&native_token(), &wrapped_token())
163            .expect("valid pair");
164        assert_eq!(price, 1.0);
165
166        let price = state
167            .spot_price(&wrapped_token(), &native_token())
168            .expect("valid pair");
169        assert_eq!(price, 1.0);
170    }
171
172    #[test]
173    fn test_get_amount_out_wrapping() {
174        let state = eth_state();
175        let amount = BigUint::from(1_000_000u64);
176        let result = state
177            .get_amount_out(amount.clone(), &native_token(), &wrapped_token())
178            .expect("valid pair");
179        assert_eq!(result.amount, amount);
180        assert_eq!(result.gas, BigUint::from(WRAP_GAS));
181    }
182
183    #[test]
184    fn test_get_amount_out_unwrapping() {
185        let state = eth_state();
186        let amount = BigUint::from(1_000_000u64);
187        let result = state
188            .get_amount_out(amount.clone(), &wrapped_token(), &native_token())
189            .expect("valid pair");
190        assert_eq!(result.amount, amount);
191        assert_eq!(result.gas, BigUint::from(UNWRAP_GAS));
192    }
193
194    #[test]
195    fn test_get_amount_out_invalid_pair() {
196        let state = eth_state();
197        let bogus = Token { address: Bytes::from("0xdead"), ..native_token() };
198        let result = state.get_amount_out(BigUint::from(1u64), &bogus, &wrapped_token());
199        assert!(result.is_err());
200    }
201
202    #[test]
203    fn test_get_limits() {
204        let state = eth_state();
205        let (sell_limit, buy_limit) = state
206            .get_limits(native_token().address, wrapped_token().address)
207            .expect("valid pair");
208        assert_eq!(sell_limit, BigUint::from(u128::MAX));
209        assert_eq!(buy_limit, BigUint::from(u128::MAX));
210    }
211
212    #[test]
213    fn test_spot_price_invalid_pair() {
214        let state = eth_state();
215        let bogus = Token { address: Bytes::from("0xdead"), ..native_token() };
216        let result = state.spot_price(&bogus, &wrapped_token());
217        assert!(result.is_err());
218    }
219
220    #[test]
221    fn test_component_metadata() {
222        let component = NativeWrapperState::component(Chain::Ethereum);
223        assert_eq!(component.id, Bytes::from(NATIVE_WRAPPER_ID.as_bytes()));
224        assert_eq!(component.protocol_system, "native_wrapper");
225        assert_eq!(component.protocol_type_name, "NativeWrapper");
226    }
227}