tycho_simulation/evm/protocol/native_wrapper/
state.rs1use 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#[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 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, "e.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}