1use std::time::Duration;
10
11use alloy::{
12 network::Ethereum,
13 primitives::{Address, U256},
14 providers::{ProviderBuilder, RootProvider},
15 sol,
16 sol_types::SolCall,
17};
18use tokio::time::{interval, MissedTickBehavior};
19use tracing::{info, warn};
20use tycho_simulation::tycho_common::Bytes;
21
22use crate::{
23 encoding::router_fees::{RouterFees, SharedRouterFees},
24 rpc,
25};
26
27sol! {
28 struct CustomFees {
30 bool hasCustomFeeOnOutput;
31 uint32 feeBpsOnOutput;
32 bool hasCustomFeeOnClientFee;
33 uint32 feeBpsOnClientFee;
34 }
35
36 interface ITychoRouter {
37 function getFeeCalculator() external view returns (address);
38 }
39
40 interface IFeeCalculator {
41 function MAX_BPS() external view returns (uint32);
42 function getRouterFeeOnOutput() external view returns (uint32);
43 function getRouterFeeOnClientFee() external view returns (uint32);
44 function getAllClientFees(uint256 start, uint256 count)
45 external view returns (address[] memory clients, CustomFees[] memory fees);
46 }
47}
48
49const CLIENT_FEE_PAGE_SIZE: usize = 500;
53
54#[derive(Debug, thiserror::Error)]
56pub enum RouterFeeFetchError {
57 #[error("invalid router fee fetcher configuration: {0}")]
59 Config(String),
60 #[error("{method} call to {contract} failed: {reason}")]
62 Call {
63 method: &'static str,
65 contract: Address,
67 reason: String,
69 },
70}
71
72pub struct RouterFeeFetcher {
74 provider: RootProvider<Ethereum>,
75 router_address: Address,
76 shared_fees: SharedRouterFees,
77 refresh_interval: Duration,
78}
79
80impl RouterFeeFetcher {
81 pub fn new(
88 rpc_url: &str,
89 router_address: &Bytes,
90 shared_fees: SharedRouterFees,
91 refresh_interval: Duration,
92 ) -> Result<Self, RouterFeeFetchError> {
93 let url = rpc_url.parse().map_err(|e| {
94 RouterFeeFetchError::Config(format!("invalid RPC URL {rpc_url:?}: {e}"))
95 })?;
96 Ok(Self {
97 provider: ProviderBuilder::default().connect_http(url),
98 router_address: rpc::to_address(router_address, "router address")
99 .map_err(RouterFeeFetchError::Config)?,
100 shared_fees,
101 refresh_interval,
102 })
103 }
104
105 pub async fn run(&self) {
110 let mut ticker = interval(self.refresh_interval);
111 ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
113
114 loop {
115 ticker.tick().await;
116
117 match self.fetch_fees().await {
118 Ok(fees) => {
119 info!(
120 custom_clients = fees.custom_client_count(),
121 "router fees refreshed from on-chain FeeCalculator"
122 );
123 self.shared_fees.set(fees);
124 }
125 Err(e) => {
126 warn!(
127 error = %e,
128 "failed to refresh router fees from chain; keeping previous values"
129 );
130 }
131 }
132 }
133 }
134
135 async fn fetch_fees(&self) -> Result<RouterFees, RouterFeeFetchError> {
141 let fee_calculator = self
142 .eth_call::<ITychoRouter::getFeeCalculatorCall>(
143 self.router_address,
144 "getFeeCalculator",
145 ITychoRouter::getFeeCalculatorCall {}.abi_encode(),
146 )
147 .await?;
148
149 let max_fee_units = self
150 .eth_call::<IFeeCalculator::MAX_BPSCall>(
151 fee_calculator,
152 "MAX_BPS",
153 IFeeCalculator::MAX_BPSCall {}.abi_encode(),
154 )
155 .await?;
156 if max_fee_units == 0 {
157 return Err(RouterFeeFetchError::Call {
158 method: "MAX_BPS",
159 contract: fee_calculator,
160 reason: "fee precision scale is zero".to_string(),
161 });
162 }
163
164 let default_fee_on_output = self
165 .eth_call::<IFeeCalculator::getRouterFeeOnOutputCall>(
166 fee_calculator,
167 "getRouterFeeOnOutput",
168 IFeeCalculator::getRouterFeeOnOutputCall {}.abi_encode(),
169 )
170 .await?;
171
172 let default_fee_on_client_fee = self
173 .eth_call::<IFeeCalculator::getRouterFeeOnClientFeeCall>(
174 fee_calculator,
175 "getRouterFeeOnClientFee",
176 IFeeCalculator::getRouterFeeOnClientFeeCall {}.abi_encode(),
177 )
178 .await?;
179
180 let mut custom_fees = rustc_hash::FxHashMap::default();
181 let mut start = 0usize;
182 loop {
183 let page = self
184 .eth_call::<IFeeCalculator::getAllClientFeesCall>(
185 fee_calculator,
186 "getAllClientFees",
187 IFeeCalculator::getAllClientFeesCall {
188 start: U256::from(start),
189 count: U256::from(CLIENT_FEE_PAGE_SIZE),
190 }
191 .abi_encode(),
192 )
193 .await?;
194
195 let page_len = page.clients.len();
196 for (client, fees) in page.clients.into_iter().zip(page.fees) {
197 let on_output = if fees.hasCustomFeeOnOutput {
200 fees.feeBpsOnOutput
201 } else {
202 default_fee_on_output
203 };
204 let on_client_fee = if fees.hasCustomFeeOnClientFee {
205 fees.feeBpsOnClientFee
206 } else {
207 default_fee_on_client_fee
208 };
209 custom_fees
210 .insert(Bytes::from(client.as_slice().to_vec()), (on_output, on_client_fee));
211 }
212
213 if page_len < CLIENT_FEE_PAGE_SIZE {
214 break;
215 }
216 start += CLIENT_FEE_PAGE_SIZE;
217 }
218
219 Ok(RouterFees::new(
220 max_fee_units as u64,
221 default_fee_on_output,
222 default_fee_on_client_fee,
223 custom_fees,
224 ))
225 }
226
227 async fn eth_call<C: SolCall>(
229 &self,
230 contract: Address,
231 method: &'static str,
232 calldata: Vec<u8>,
233 ) -> Result<C::Return, RouterFeeFetchError> {
234 rpc::eth_call::<C>(&self.provider, contract, calldata)
235 .await
236 .map_err(|reason| RouterFeeFetchError::Call { method, contract, reason })
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use std::str::FromStr;
243
244 use alloy::{
245 primitives::Bytes as AlloyBytes, rpc::client::RpcClient, transports::mock::Asserter,
246 };
247
248 use super::*;
249
250 const ROUTER: Address = Address::repeat_byte(0x11);
251 const CALCULATOR: Address = Address::repeat_byte(0x22);
252 const MAX_FEE_UNITS: u32 = 100_000_000;
254
255 fn fetcher_with(asserter: &Asserter) -> RouterFeeFetcher {
256 RouterFeeFetcher {
257 provider: RootProvider::new(RpcClient::mocked(asserter.clone())),
258 router_address: ROUTER,
259 shared_fees: SharedRouterFees::default(),
260 refresh_interval: Duration::from_secs(300),
261 }
262 }
263
264 fn push_return<C: SolCall>(asserter: &Asserter, ret: &C::Return) {
265 asserter.push_success(&AlloyBytes::from(C::abi_encode_returns(ret)));
266 }
267
268 fn push_defaults(asserter: &Asserter, fee_on_output: u32, fee_on_client_fee: u32) {
269 push_return::<ITychoRouter::getFeeCalculatorCall>(asserter, &CALCULATOR);
270 push_return::<IFeeCalculator::MAX_BPSCall>(asserter, &MAX_FEE_UNITS);
271 push_return::<IFeeCalculator::getRouterFeeOnOutputCall>(asserter, &fee_on_output);
272 push_return::<IFeeCalculator::getRouterFeeOnClientFeeCall>(asserter, &fee_on_client_fee);
273 }
274
275 fn custom_fees(on_output: Option<u32>, on_client_fee: Option<u32>) -> CustomFees {
276 CustomFees {
277 hasCustomFeeOnOutput: on_output.is_some(),
278 feeBpsOnOutput: on_output.unwrap_or(0),
279 hasCustomFeeOnClientFee: on_client_fee.is_some(),
280 feeBpsOnClientFee: on_client_fee.unwrap_or(0),
281 }
282 }
283
284 #[tokio::test]
285 async fn test_fetch_fees_defaults_and_custom_clients() {
286 let asserter = Asserter::new();
287 push_defaults(&asserter, 150_000, 25_000_000);
288 let client_a = Address::repeat_byte(0xAA);
289 let client_b = Address::repeat_byte(0xBB);
290 push_return::<IFeeCalculator::getAllClientFeesCall>(
291 &asserter,
292 &IFeeCalculator::getAllClientFeesReturn {
293 clients: vec![client_a, client_b],
294 fees: vec![custom_fees(Some(50_000), None), custom_fees(None, Some(10_000_000))],
295 },
296 );
297
298 let fees = fetcher_with(&asserter)
299 .fetch_fees()
300 .await
301 .unwrap();
302
303 let rates_a = fees.fees_for(&Bytes::from(client_a.as_slice().to_vec()));
304 assert_eq!(rates_a.on_output(), 50_000);
305 assert_eq!(rates_a.on_client_fee(), 25_000_000);
306 let rates_b = fees.fees_for(&Bytes::from(client_b.as_slice().to_vec()));
307 assert_eq!(rates_b.on_output(), 150_000);
308 assert_eq!(rates_b.on_client_fee(), 10_000_000);
309 let rates_unknown = fees.fees_for(&Bytes::from(vec![0xCC; 20]));
310 assert_eq!(rates_unknown.on_output(), 150_000);
311 assert_eq!(rates_unknown.on_client_fee(), 25_000_000);
312 assert_eq!(fees.max_fee_units(), MAX_FEE_UNITS as u64);
313 }
314
315 #[tokio::test]
316 async fn test_fetch_fees_rejects_zero_precision_scale() {
317 let asserter = Asserter::new();
318 push_return::<ITychoRouter::getFeeCalculatorCall>(&asserter, &CALCULATOR);
319 push_return::<IFeeCalculator::MAX_BPSCall>(&asserter, &0u32);
320
321 let err = fetcher_with(&asserter)
322 .fetch_fees()
323 .await
324 .unwrap_err();
325
326 assert!(err.to_string().contains("MAX_BPS"));
327 }
328
329 #[tokio::test]
330 async fn test_fetch_fees_paginates_until_partial_page() {
331 let asserter = Asserter::new();
332 push_defaults(&asserter, 100_000, 20_000_000);
333
334 let full_page: Vec<Address> = (0..CLIENT_FEE_PAGE_SIZE)
336 .map(|i| {
337 let mut bytes = [0u8; 20];
338 bytes[..8].copy_from_slice(&(i as u64).to_be_bytes());
339 bytes[19] = 1;
340 Address::from(bytes)
341 })
342 .collect();
343 push_return::<IFeeCalculator::getAllClientFeesCall>(
344 &asserter,
345 &IFeeCalculator::getAllClientFeesReturn {
346 clients: full_page.clone(),
347 fees: vec![custom_fees(Some(1), None); CLIENT_FEE_PAGE_SIZE],
348 },
349 );
350 let last_client = Address::repeat_byte(0xEE);
351 push_return::<IFeeCalculator::getAllClientFeesCall>(
352 &asserter,
353 &IFeeCalculator::getAllClientFeesReturn {
354 clients: vec![last_client],
355 fees: vec![custom_fees(Some(2), None)],
356 },
357 );
358
359 let fees = fetcher_with(&asserter)
360 .fetch_fees()
361 .await
362 .unwrap();
363
364 assert_eq!(fees.custom_client_count(), CLIENT_FEE_PAGE_SIZE + 1);
365 let last_rates = fees.fees_for(&Bytes::from(last_client.as_slice().to_vec()));
366 assert_eq!(last_rates.on_output(), 2);
367 }
368
369 #[tokio::test]
375 #[ignore = "hits a live mainnet RPC node"]
376 async fn test_fetch_fees_against_mainnet_router() {
377 let router = Bytes::from(
379 Address::from_str("0xdA892C989d07A18B5DD3F392d949f00dF15C5736")
380 .unwrap()
381 .as_slice(),
382 );
383 let rpc_url = std::env::var("RPC_URL")
384 .unwrap_or_else(|_| "https://ethereum-rpc.publicnode.com".to_string());
385
386 let fetcher =
387 RouterFeeFetcher::new(&rpc_url, &router, SharedRouterFees::default(), Duration::ZERO)
388 .unwrap();
389
390 let fees = fetcher
391 .fetch_fees()
392 .await
393 .expect("should read fees from the live mainnet FeeCalculator");
394
395 assert!(fees.max_fee_units() > 0, "max_fee_units must be non-zero");
398 let default_rates = fees.fees_for(&Bytes::from(vec![0u8; 20]));
399 assert_eq!(default_rates.max_fee_units(), fees.max_fee_units());
400
401 println!(
402 "mainnet router fees: max_fee_units={}, default_on_output={}, \
403 default_on_client_fee={}, custom_clients={}",
404 fees.max_fee_units(),
405 default_rates.on_output(),
406 default_rates.on_client_fee(),
407 fees.custom_client_count(),
408 );
409 }
410}