1use std::sync::Arc;
2
3use alloy::{
4 primitives::{aliases::U48, Address, Keccak256, U160, U256},
5 sol_types::SolValue,
6};
7use num_bigint::BigUint;
8use tycho_execution::encoding::{
9 errors::EncodingError,
10 evm::{
11 approvals::permit2::{PermitDetails as SolPermitDetails, PermitSingle},
12 encoder_builders::TychoRouterEncoderBuilder,
13 get_router_address,
14 swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
15 utils::{biguint_to_u256, bytes_to_address},
16 },
17 models::{EncodedSolution, Solution, Swap},
18 tycho_encoder::TychoEncoder,
19};
20use tycho_simulation::tycho_common::{models::Chain, Bytes};
21
22use crate::{EncodingOptions, FeeBreakdown, OrderQuote, QuoteStatus, SolveError, Transaction};
23
24pub const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
26
27const ROUTER_FEE_ON_OUTPUT_BPS: u64 = 10;
29const ROUTER_FEE_ON_CLIENT_FEE_BPS: u64 = 2000;
31
32pub struct Encoder {
40 tycho_encoder: Box<dyn TychoEncoder>,
41 chain: Chain,
42 router_address: Bytes,
43 encoding_rt: Option<Arc<tokio::runtime::Runtime>>,
47}
48
49impl Drop for Encoder {
50 fn drop(&mut self) {
51 if let Some(rt) = self.encoding_rt.take() {
55 if tokio::runtime::Handle::try_current().is_ok() {
56 std::thread::spawn(move || drop(rt));
57 }
58 }
59 }
60}
61
62impl TryFrom<&OrderQuote> for Solution {
63 type Error = SolveError;
64
65 fn try_from(quote: &OrderQuote) -> Result<Self, Self::Error> {
66 if quote.status() != QuoteStatus::Success {
67 return Err(SolveError::FailedEncoding(format!(
68 "cannot convert quote with status {:?} to Solution",
69 quote.status()
70 )));
71 }
72
73 let route = quote.route().ok_or_else(|| {
74 SolveError::FailedEncoding("successful quote must have a route".to_string())
75 })?;
76
77 let token_in = route
78 .input_token()
79 .ok_or_else(|| SolveError::FailedEncoding("route has no input token".to_string()))?;
80 let token_out = route
81 .output_token()
82 .ok_or_else(|| SolveError::FailedEncoding("route has no output token".to_string()))?;
83
84 let swaps = route
85 .swaps()
86 .iter()
87 .map(|s| {
88 Swap::new(
89 s.protocol_component().clone(),
90 s.token_in().clone(),
91 s.token_out().clone(),
92 )
93 .with_split(*s.split())
94 .with_protocol_state(Arc::from(s.protocol_state().clone_box()))
95 .with_estimated_amount_in(s.amount_in().clone())
96 })
97 .collect();
98
99 Ok(Solution::new(
100 quote.sender().clone(),
101 quote.receiver().clone(),
102 Bytes::from(token_in.as_ref()),
103 Bytes::from(token_out.as_ref()),
104 quote.amount_in().clone(),
105 quote.amount_out().clone(),
106 swaps,
107 ))
108 }
109}
110
111impl Encoder {
112 pub fn new(
121 chain: Chain,
122 swap_encoder_registry: SwapEncoderRegistry,
123 ) -> Result<Self, SolveError> {
124 let router_address = get_router_address(&chain)
125 .map_err(|e| SolveError::FailedEncoding(e.to_string()))?
126 .clone();
127 let encoding_rt = tokio::runtime::Builder::new_multi_thread()
128 .worker_threads(2)
129 .enable_all()
130 .build()
131 .map_err(|e| {
132 SolveError::FailedEncoding(format!("failed to create encoding runtime: {e}"))
133 })?;
134 Ok(Self {
135 tycho_encoder: TychoRouterEncoderBuilder::new()
136 .chain(chain)
137 .swap_encoder_registry(swap_encoder_registry)
138 .build()?,
139 chain,
140 router_address,
141 encoding_rt: Some(Arc::new(encoding_rt)),
142 })
143 }
144
145 pub fn router_address(&self) -> &Bytes {
147 &self.router_address
148 }
149
150 pub async fn encode(
159 &self,
160 mut quotes: Vec<OrderQuote>,
161 encoding_options: EncodingOptions,
162 ) -> Result<Vec<OrderQuote>, SolveError> {
163 let slippage = encoding_options.slippage();
164 if slippage == 0.0 {
165 tracing::warn!("slippage is 0, transaction will likely revert");
166 } else if slippage > 0.5 {
167 tracing::warn!(slippage, "slippage exceeds 50%, possible misconfiguration");
168 }
169
170 let mut to_encode: Vec<(usize, Solution)> = Vec::new();
171
172 for (i, quote) in quotes.iter().enumerate() {
173 if quote.status() != QuoteStatus::Success {
174 continue;
175 }
176
177 to_encode.push((
178 i,
179 Solution::try_from(quote)?
180 .with_user_transfer_type(encoding_options.transfer_type().clone()),
181 ));
182 }
183
184 let solutions: Vec<Solution> = to_encode
185 .iter()
186 .map(|(_, s)| s.clone())
187 .collect();
188 let rt = self
189 .encoding_rt
190 .as_ref()
191 .ok_or_else(|| SolveError::FailedEncoding("encoding runtime was dropped".into()))?;
192 let encoded_solutions = std::thread::scope(|scope| {
193 scope
194 .spawn(|| {
195 rt.block_on(async {
196 self.tycho_encoder
197 .encode_solutions(solutions)
198 })
199 })
200 .join()
201 .expect("encoding thread panicked")
202 })?;
203
204 for (encoded_solution, (idx, solution)) in encoded_solutions
205 .into_iter()
206 .zip(to_encode)
207 {
208 let (transaction, fee_breakdown) =
209 self.encode_tycho_router_call(encoded_solution, &solution, &encoding_options)?;
210 quotes[idx].set_transaction(transaction);
211 quotes[idx].set_fee_breakdown(fee_breakdown);
212 }
213
214 Ok(quotes)
215 }
216
217 fn encode_tycho_router_call(
226 &self,
227 encoded_solution: EncodedSolution,
228 solution: &Solution,
229 encoding_options: &EncodingOptions,
230 ) -> Result<(Transaction, FeeBreakdown), EncodingError> {
231 let amount_in = biguint_to_u256(solution.amount_in());
232 let swap_output = solution.min_amount_out();
233 let fee_breakdown = Self::calculate_fee_breakdown(
234 swap_output,
235 encoding_options
236 .client_fee_params()
237 .map_or(0, |f| f.bps()),
238 encoding_options.slippage(),
239 );
240 let min_amount_out = biguint_to_u256(fee_breakdown.min_amount_received());
241 let token_in = bytes_to_address(solution.token_in())?;
242 let token_out = bytes_to_address(solution.token_out())?;
243 let receiver = bytes_to_address(solution.receiver())?;
244
245 let (permit, permit2_sig) = if let Some(p) = encoding_options.permit() {
246 let d = p.details();
247 let permit = Some(PermitSingle {
248 details: SolPermitDetails {
249 token: bytes_to_address(d.token())?,
250 amount: U160::from(biguint_to_u256(d.amount())),
251 expiration: U48::from(biguint_to_u256(d.expiration())),
252 nonce: U48::from(biguint_to_u256(d.nonce())),
253 },
254 spender: bytes_to_address(p.spender())?,
255 sigDeadline: biguint_to_u256(p.sig_deadline()),
256 });
257 let sig = encoding_options
258 .permit2_signature()
259 .ok_or_else(|| {
260 EncodingError::FatalError("Signature must be provided for permit2".to_string())
261 })?
262 .to_vec();
263 (permit, sig)
264 } else {
265 (None, vec![])
266 };
267
268 let client_fee_params = if let Some(fee) = encoding_options.client_fee_params() {
269 (
270 fee.bps(),
271 bytes_to_address(fee.receiver())?,
272 biguint_to_u256(fee.max_contribution()),
273 U256::from(fee.deadline()),
274 fee.signature().to_vec(),
275 )
276 } else {
277 (0u16, Address::ZERO, U256::ZERO, U256::MAX, vec![])
278 };
279
280 let fn_sig = encoded_solution.function_signature();
281 let swaps = encoded_solution.swaps();
282
283 let method_calldata = if fn_sig.contains("Permit2") {
284 let permit = permit.ok_or(EncodingError::FatalError(
285 "permit2 object must be set to use permit2".to_string(),
286 ))?;
287 if fn_sig.contains("splitSwap") {
288 (
289 amount_in,
290 token_in,
291 token_out,
292 min_amount_out,
293 U256::from(encoded_solution.n_tokens()),
294 receiver,
295 client_fee_params,
296 permit,
297 permit2_sig,
298 swaps,
299 )
300 .abi_encode()
301 } else {
302 (
303 amount_in,
304 token_in,
305 token_out,
306 min_amount_out,
307 receiver,
308 client_fee_params,
309 permit,
310 permit2_sig,
311 swaps,
312 )
313 .abi_encode()
314 }
315 } else if fn_sig.contains("splitSwap") {
316 (
317 amount_in,
318 token_in,
319 token_out,
320 min_amount_out,
321 U256::from(encoded_solution.n_tokens()),
322 receiver,
323 client_fee_params,
324 swaps,
325 )
326 .abi_encode()
327 } else if fn_sig.contains("singleSwap") || fn_sig.contains("sequentialSwap") {
328 (amount_in, token_in, token_out, min_amount_out, receiver, client_fee_params, swaps)
329 .abi_encode()
330 } else {
331 return Err(EncodingError::FatalError(format!(
332 "unsupported function signature for Tycho router: {fn_sig}"
333 )));
334 };
335
336 let native_address = &self.chain.native_token().address;
337 let contract_interaction =
338 Self::encode_input(encoded_solution.function_signature(), method_calldata);
339 let value = if *solution.token_in() == *native_address {
340 solution.amount_in().clone()
341 } else {
342 BigUint::ZERO
343 };
344 let transaction = Transaction::new(
345 encoded_solution
346 .interacting_with()
347 .clone(),
348 value,
349 contract_interaction,
350 );
351 Ok((transaction, fee_breakdown))
352 }
353
354 fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
356 let mut hasher = Keccak256::new();
357 hasher.update(selector.as_bytes());
358 let selector_bytes = &hasher.finalize()[..4];
359 let mut call_data = selector_bytes.to_vec();
360 if encoded_args.len() > 32 &&
364 encoded_args[..32] ==
365 [0u8; 31]
366 .into_iter()
367 .chain([32].to_vec())
368 .collect::<Vec<u8>>()
369 {
370 encoded_args = encoded_args[32..].to_vec();
371 }
372 call_data.extend(encoded_args);
373 call_data
374 }
375
376 fn calculate_fee_breakdown(
381 swap_output: &BigUint,
382 client_fee_bps: u16,
383 slippage: f64,
384 ) -> FeeBreakdown {
385 let client_bps = client_fee_bps as u64;
386
387 let mut router_fee_on_client = BigUint::ZERO;
388 let mut client_portion = BigUint::ZERO;
389
390 if client_bps > 0 {
391 let fee_numerator = swap_output * client_bps;
392 let total_client_fee = &fee_numerator / 10_000u64;
393
394 router_fee_on_client = &fee_numerator * ROUTER_FEE_ON_CLIENT_FEE_BPS / 100_000_000u64;
395
396 client_portion = total_client_fee - &router_fee_on_client;
397 }
398
399 let router_fee_on_output = swap_output * ROUTER_FEE_ON_OUTPUT_BPS / 10_000u64;
400 let total_router_fee = router_fee_on_client + router_fee_on_output;
401
402 let amount_after_fees = swap_output - &client_portion - &total_router_fee;
403
404 let precision = BigUint::from(1_000_000u64);
405 let slippage_amount =
406 &amount_after_fees * BigUint::from((slippage * 1_000_000.0) as u64) / &precision;
407
408 let min_amount_received = &amount_after_fees - &slippage_amount;
409
410 FeeBreakdown::new(total_router_fee, client_portion, slippage_amount, min_amount_received)
411 }
412}
413
414impl From<EncodingError> for SolveError {
415 fn from(err: EncodingError) -> Self {
416 SolveError::FailedEncoding(err.to_string())
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use num_bigint::BigUint;
423 use tycho_execution::encoding::{
424 errors::EncodingError,
425 models::{EncodedSolution, Solution},
426 tycho_encoder::TychoEncoder,
427 };
428 use tycho_simulation::tycho_core::{
429 models::{token::Token, Address, Chain as SimChain},
430 Bytes,
431 };
432
433 use super::*;
434 use crate::{
435 algorithm::test_utils::{component, MockProtocolSim},
436 BlockInfo, OrderQuote, QuoteStatus,
437 };
438
439 fn make_route_swap_addrs(token_in: Address, token_out: Address) -> crate::types::Swap {
440 let make_token = |addr: Address| Token {
441 address: addr,
442 symbol: "T".to_string(),
443 decimals: 18,
444 tax: Default::default(),
445 gas: vec![],
446 chain: SimChain::Ethereum,
447 quality: 100,
448 };
449 let tin = make_token(token_in.clone());
450 let tout = make_token(token_out.clone());
451 let pool_addr = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
453 crate::types::Swap::new(
454 pool_addr.to_string(),
455 "uniswap_v2".to_string(),
456 token_in,
457 token_out,
458 BigUint::from(1000u64),
459 BigUint::from(990u64),
460 BigUint::from(50_000u64),
461 component(pool_addr, &[tin, tout]),
462 Box::new(MockProtocolSim::default()),
463 )
464 }
465
466 fn make_address(byte: u8) -> Address {
467 Address::from([byte; 20])
468 }
469
470 fn make_order_quote() -> OrderQuote {
471 OrderQuote::new(
472 "test-order".to_string(),
473 QuoteStatus::Success,
474 BigUint::from(1000u64),
475 BigUint::from(990u64),
476 BigUint::from(100_000u64),
477 BigUint::from(990u64),
478 BlockInfo::new(1, "0x123".to_string(), 1000),
479 "test".to_string(),
480 Bytes::from(make_address(0xAA).as_ref()),
481 Bytes::from(make_address(0xAA).as_ref()),
482 )
483 }
484
485 struct MockTychoEncoder;
486
487 impl TychoEncoder for MockTychoEncoder {
488 fn encode_solutions(
489 &self,
490 _solutions: Vec<Solution>,
491 ) -> Result<Vec<EncodedSolution>, EncodingError> {
492 Ok(vec![])
493 }
494
495 fn validate_solution(&self, _solution: &Solution) -> Result<(), EncodingError> {
496 Ok(())
497 }
498 }
499
500 fn mock_encoder(chain: Chain) -> Encoder {
501 Encoder {
502 tycho_encoder: Box::new(MockTychoEncoder),
503 chain,
504 router_address: Bytes::from([0u8; 20].as_ref()),
505 encoding_rt: Some(Arc::new(
506 tokio::runtime::Builder::new_multi_thread()
507 .worker_threads(1)
508 .enable_all()
509 .build()
510 .unwrap(),
511 )),
512 }
513 }
514
515 #[test]
516 fn test_encoder_new_fails_on_unsupported_chain() {
517 let registry =
521 tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry::new(Chain::Ethereum)
522 .add_default_encoders(None)
523 .expect("registry should build for Ethereum");
524 let result = Encoder::new(Chain::Arbitrum, registry);
525 assert!(result.is_err(), "expected Err for chain without router address, got Ok");
526 }
527
528 #[test]
529 fn test_try_from_without_route_errors() {
530 let quote = make_order_quote();
531
532 let result = Solution::try_from("e);
533
534 assert!(result.is_err());
535 }
536
537 #[test]
538 fn test_try_from_non_success_errors() {
539 let quote = OrderQuote::new(
540 "test-order".to_string(),
541 QuoteStatus::NoRouteFound,
542 BigUint::from(1000u64),
543 BigUint::from(990u64),
544 BigUint::from(100_000u64),
545 BigUint::from(990u64),
546 BlockInfo::new(1, "0x123".to_string(), 1000),
547 "test".to_string(),
548 Bytes::from(make_address(0xAA).as_ref()),
549 Bytes::from(make_address(0xAA).as_ref()),
550 );
551
552 let result = Solution::try_from("e);
553
554 assert!(result.is_err());
555 }
556
557 #[test]
558 fn test_try_from_maps_tokens_and_amounts() {
559 let quote =
560 make_order_quote().with_route(crate::types::Route::new(vec![make_route_swap_addrs(
561 make_address(0x01),
562 make_address(0x02),
563 )]));
564
565 let solution = Solution::try_from("e).unwrap();
566
567 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
568 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x02).as_ref()));
569 assert_eq!(*solution.amount_in(), *quote.amount_in());
570 assert_eq!(*solution.min_amount_out(), *quote.amount_out());
571 assert_eq!(solution.swaps().len(), 1);
572 }
573
574 #[test]
575 fn test_try_from_multi_hop_uses_boundary_swap_tokens() {
576 let quote = make_order_quote().with_route(crate::types::Route::new(vec![
577 make_route_swap_addrs(make_address(0x01), make_address(0x02)),
578 make_route_swap_addrs(make_address(0x02), make_address(0x03)),
579 ]));
580
581 let solution = Solution::try_from("e).unwrap();
582
583 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
584 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x03).as_ref()));
585 assert_eq!(solution.swaps().len(), 2);
586 }
587
588 #[tokio::test]
589 async fn test_encode_skips_non_successful_solutions() {
590 let encoder = mock_encoder(Chain::Ethereum);
591 let quote = OrderQuote::new(
592 "test-order".to_string(),
593 QuoteStatus::NoRouteFound,
594 BigUint::from(1000u64),
595 BigUint::from(990u64),
596 BigUint::from(100_000u64),
597 BigUint::from(990u64),
598 BlockInfo::new(1, "0x123".to_string(), 1000),
599 "test".to_string(),
600 Bytes::from(make_address(0xAA).as_ref()),
601 Bytes::from(make_address(0xAA).as_ref()),
602 );
603
604 let encoding_options = EncodingOptions::new(0.01);
605
606 let result = encoder
607 .encode(vec![quote], encoding_options)
608 .await
609 .unwrap();
610
611 assert!(result[0].transaction().is_none());
612 }
613
614 fn real_encoder() -> Encoder {
615 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
616 .add_default_encoders(None)
617 .unwrap();
618 Encoder::new(Chain::Ethereum, registry).unwrap()
619 }
620
621 #[tokio::test]
622 async fn test_encode_sets_transaction_on_successful_solution() {
623 let encoder = real_encoder();
624 let quote =
625 make_order_quote().with_route(crate::types::Route::new(vec![make_route_swap_addrs(
626 make_address(0x01),
627 make_address(0x02),
628 )]));
629
630 let encoding_options = EncodingOptions::new(0.01);
631
632 let result = encoder
633 .encode(vec![quote], encoding_options)
634 .await
635 .unwrap();
636
637 assert!(result[0].transaction().is_some());
638 let tx = result[0].transaction().unwrap();
639 assert!(!tx.data().is_empty());
640 assert!(tx.data().len() > 4);
642 }
643
644 #[tokio::test]
645 async fn test_encode_with_client_fee_params() {
646 let encoder = real_encoder();
647 let quote =
648 make_order_quote().with_route(crate::types::Route::new(vec![make_route_swap_addrs(
649 make_address(0x01),
650 make_address(0x02),
651 )]));
652
653 let fee = crate::ClientFeeParams::new(
654 100,
655 Bytes::from(make_address(0xBB).as_ref()),
656 BigUint::from(0u64),
657 1_893_456_000u64,
658 Bytes::from(vec![0xAB; 65]),
659 );
660 let encoding_options = EncodingOptions::new(0.01).with_client_fee_params(fee);
661
662 let result = encoder
663 .encode(vec![quote], encoding_options)
664 .await
665 .unwrap();
666
667 assert!(result[0].transaction().is_some());
668 let tx = result[0].transaction().unwrap();
669 assert!(!tx.data().is_empty());
670 assert!(tx.data().len() > 4);
672 }
673
674 #[tokio::test]
675 async fn test_encode_without_client_fee_produces_transaction() {
676 let encoder = real_encoder();
677 let quote =
678 make_order_quote().with_route(crate::types::Route::new(vec![make_route_swap_addrs(
679 make_address(0x01),
680 make_address(0x02),
681 )]));
682
683 let encoding_options = EncodingOptions::new(0.01);
684
685 let result = encoder
686 .encode(vec![quote], encoding_options)
687 .await
688 .unwrap();
689
690 assert!(result[0].transaction().is_some());
691 }
692}