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) -> Option<Self> {
37        let native_asset = chain.native_asset();
38        Some(Self {
39            native_token: native_asset.native_token().clone(),
40            wrapped_token: native_asset.wrapper()?.clone(),
41        })
42    }
43
44    /// Builds the `ProtocolComponent` metadata for stream injection.
45    pub fn component(&self) -> ProtocolComponent {
46        ProtocolComponent::new(
47            Bytes::from(NATIVE_WRAPPER_ID.as_bytes()),
48            NATIVE_WRAPPER_PROTOCOL_SYSTEM.to_string(),
49            NATIVE_WRAPPER_PROTOCOL_TYPE.to_string(),
50            self.native_token.chain,
51            vec![self.native_token.clone(), self.wrapped_token.clone()],
52            vec![],
53            HashMap::new(),
54            Bytes::default(),
55            NaiveDateTime::default(),
56        )
57    }
58
59    fn validate_tokens(&self, token_in: &Bytes, token_out: &Bytes) -> Result<(), SimulationError> {
60        let valid_pair = (*token_in == self.native_token.address &&
61            *token_out == self.wrapped_token.address) ||
62            (*token_in == self.wrapped_token.address && *token_out == self.native_token.address);
63        if !valid_pair {
64            return Err(SimulationError::InvalidInput(
65                format!(
66                    "NativeWrapper only supports {} ↔ {}, got {} → {}",
67                    self.native_token.address, self.wrapped_token.address, token_in, token_out,
68                ),
69                None,
70            ));
71        }
72        Ok(())
73    }
74}
75
76#[typetag::serde]
77impl ProtocolSim for NativeWrapperState {
78    fn fee(&self) -> f64 {
79        0.0
80    }
81
82    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
83        self.validate_tokens(&base.address, &quote.address)?;
84        Ok(1.0)
85    }
86
87    fn get_amount_out(
88        &self,
89        amount_in: BigUint,
90        token_in: &Token,
91        token_out: &Token,
92    ) -> Result<GetAmountOutResult, SimulationError> {
93        self.validate_tokens(&token_in.address, &token_out.address)?;
94        let is_wrapping = token_in.address == self.native_token.address;
95        let gas = if is_wrapping { WRAP_GAS } else { UNWRAP_GAS };
96        Ok(GetAmountOutResult::new(amount_in, BigUint::from(gas), self.clone_box()))
97    }
98
99    fn get_limits(
100        &self,
101        sell_token: Bytes,
102        buy_token: Bytes,
103    ) -> Result<(BigUint, BigUint), SimulationError> {
104        self.validate_tokens(&sell_token, &buy_token)?;
105        Ok((BigUint::from(u128::MAX), BigUint::from(u128::MAX)))
106    }
107
108    fn delta_transition(
109        &mut self,
110        _delta: ProtocolStateDelta,
111        _tokens: &HashMap<Bytes, Token>,
112        _balances: &Balances,
113    ) -> Result<(), TransitionError> {
114        Ok(())
115    }
116
117    fn clone_box(&self) -> Box<dyn ProtocolSim> {
118        Box::new(self.clone())
119    }
120
121    fn as_any(&self) -> &dyn Any {
122        self
123    }
124
125    fn as_any_mut(&mut self) -> &mut dyn Any {
126        self
127    }
128
129    fn eq(&self, other: &dyn ProtocolSim) -> bool {
130        other
131            .as_any()
132            .downcast_ref::<NativeWrapperState>()
133            .is_some_and(|o| {
134                self.native_token == o.native_token && self.wrapped_token == o.wrapped_token
135            })
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn eth_state() -> NativeWrapperState {
144        NativeWrapperState::new(Chain::Ethereum).expect("Ethereum should have a wrapper")
145    }
146
147    fn native_token() -> Token {
148        Chain::Ethereum.native_token()
149    }
150
151    fn wrapped_token() -> Token {
152        Chain::Ethereum
153            .wrapped_native_token()
154            .expect("Ethereum should have a wrapper")
155    }
156
157    #[test]
158    fn test_new_rejects_chains_without_wrapper_contracts() {
159        assert!(NativeWrapperState::new(Chain::Arc).is_none());
160        assert!(NativeWrapperState::new(Chain::Starknet).is_none());
161    }
162
163    #[test]
164    fn test_fee_is_zero() {
165        assert_eq!(eth_state().fee(), 0.0);
166    }
167
168    #[test]
169    fn test_spot_price_is_one() {
170        let state = eth_state();
171        let price = state
172            .spot_price(&native_token(), &wrapped_token())
173            .expect("valid pair");
174        assert_eq!(price, 1.0);
175
176        let price = state
177            .spot_price(&wrapped_token(), &native_token())
178            .expect("valid pair");
179        assert_eq!(price, 1.0);
180    }
181
182    #[test]
183    fn test_get_amount_out_wrapping() {
184        let state = eth_state();
185        let amount = BigUint::from(1_000_000u64);
186        let result = state
187            .get_amount_out(amount.clone(), &native_token(), &wrapped_token())
188            .expect("valid pair");
189        assert_eq!(result.amount, amount);
190        assert_eq!(result.gas, BigUint::from(WRAP_GAS));
191    }
192
193    #[test]
194    fn test_get_amount_out_unwrapping() {
195        let state = eth_state();
196        let amount = BigUint::from(1_000_000u64);
197        let result = state
198            .get_amount_out(amount.clone(), &wrapped_token(), &native_token())
199            .expect("valid pair");
200        assert_eq!(result.amount, amount);
201        assert_eq!(result.gas, BigUint::from(UNWRAP_GAS));
202    }
203
204    #[test]
205    fn test_get_amount_out_invalid_pair() {
206        let state = eth_state();
207        let bogus = Token { address: Bytes::from("0xdead"), ..native_token() };
208        let result = state.get_amount_out(BigUint::from(1u64), &bogus, &wrapped_token());
209        assert!(result.is_err());
210    }
211
212    #[test]
213    fn test_get_limits() {
214        let state = eth_state();
215        let (sell_limit, buy_limit) = state
216            .get_limits(native_token().address, wrapped_token().address)
217            .expect("valid pair");
218        assert_eq!(sell_limit, BigUint::from(u128::MAX));
219        assert_eq!(buy_limit, BigUint::from(u128::MAX));
220    }
221
222    #[test]
223    fn test_spot_price_invalid_pair() {
224        let state = eth_state();
225        let bogus = Token { address: Bytes::from("0xdead"), ..native_token() };
226        let result = state.spot_price(&bogus, &wrapped_token());
227        assert!(result.is_err());
228    }
229
230    #[test]
231    fn test_component_metadata() {
232        let component = eth_state().component();
233        assert_eq!(component.id, Bytes::from(NATIVE_WRAPPER_ID.as_bytes()));
234        assert_eq!(component.protocol_system, "native_wrapper");
235        assert_eq!(component.protocol_type_name, "NativeWrapper");
236    }
237}