1use std::sync::Arc;
2
3use alloy::{
4 primitives::{aliases::U48, keccak256, 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 ROUTER_ETH_ADDRESS,
17 },
18 models::{EncodedSolution, Solution, Swap},
19 tycho_encoder::TychoEncoder,
20};
21use tycho_simulation::tycho_common::{models::Chain, Bytes};
22
23use crate::{EncodingOptions, FeeBreakdown, OrderQuote, QuoteStatus, SolveError, Transaction};
24
25pub const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
27
28const ROUTER_FEE_ON_OUTPUT_BPS: u64 = 10;
30const ROUTER_FEE_ON_CLIENT_FEE_BPS: u64 = 2000;
32
33pub struct Encoder {
41 tycho_encoder: Box<dyn TychoEncoder>,
42 chain: Chain,
43 router_address: Bytes,
44}
45
46impl TryFrom<&OrderQuote> for Solution {
47 type Error = SolveError;
48
49 fn try_from(quote: &OrderQuote) -> Result<Self, Self::Error> {
50 if quote.status() != QuoteStatus::Success {
51 return Err(SolveError::FailedEncoding(format!(
52 "cannot convert quote with status {:?} to Solution",
53 quote.status()
54 )));
55 }
56
57 let route = quote.route().ok_or_else(|| {
58 SolveError::FailedEncoding("successful quote must have a route".to_string())
59 })?;
60
61 let token_in = route
62 .input_token()
63 .ok_or_else(|| SolveError::FailedEncoding("route has no input token".to_string()))?;
64 let token_out = route
65 .output_token()
66 .ok_or_else(|| SolveError::FailedEncoding("route has no output token".to_string()))?;
67
68 let token_map = route.tokens();
69 let lookup_token = |addr: &Bytes| {
70 token_map
71 .get(addr)
72 .cloned()
73 .ok_or_else(|| {
74 SolveError::FailedEncoding(format!(
75 "token {addr:?} not found in route's token map; \
76 algorithm must populate Route::with_tokens for every swap token"
77 ))
78 })
79 };
80 let swaps = route
81 .swaps()
82 .iter()
83 .map(|s| {
84 let token_in = lookup_token(s.token_in())?;
85 let token_out = lookup_token(s.token_out())?;
86 Ok(Swap::new(
87 s.protocol_component().clone(),
88 token_in,
89 token_out,
90 s.gas_estimate().clone(),
91 )
92 .with_split(*s.split())
93 .with_protocol_state(Arc::from(s.protocol_state().clone_box()))
94 .with_estimated_amount_in(s.amount_in().clone()))
95 })
96 .collect::<Result<Vec<_>, SolveError>>()?;
97
98 Ok(Solution::new(
99 quote.sender().clone(),
100 quote.receiver().clone(),
101 Bytes::from(token_in.as_ref()),
102 Bytes::from(token_out.as_ref()),
103 quote.amount_in().clone(),
104 quote.amount_out().clone(),
105 swaps,
106 ))
107 }
108}
109
110impl Encoder {
111 pub fn new(
120 chain: Chain,
121 swap_encoder_registry: SwapEncoderRegistry,
122 ) -> Result<Self, SolveError> {
123 let router_address = get_router_address(&chain)
124 .map_err(|e| SolveError::FailedEncoding(e.to_string()))?
125 .clone();
126 Ok(Self {
127 tycho_encoder: TychoRouterEncoderBuilder::new()
128 .chain(chain)
129 .swap_encoder_registry(swap_encoder_registry)
130 .build()?,
131 chain,
132 router_address,
133 })
134 }
135
136 pub fn router_address(&self) -> &Bytes {
138 &self.router_address
139 }
140
141 pub async fn encode(
150 &self,
151 mut quotes: Vec<OrderQuote>,
152 encoding_options: EncodingOptions,
153 ) -> Result<Vec<OrderQuote>, SolveError> {
154 let slippage = encoding_options.slippage();
155 if slippage == 0.0 {
156 tracing::warn!("slippage is 0, transaction will likely revert");
157 } else if slippage > 0.5 {
158 tracing::warn!(slippage, "slippage exceeds 50%, possible misconfiguration");
159 }
160
161 let mut to_encode: Vec<(usize, Solution)> = Vec::new();
162
163 for (i, quote) in quotes.iter().enumerate() {
164 if quote.status() != QuoteStatus::Success {
165 continue;
166 }
167
168 to_encode.push((
169 i,
170 Solution::try_from(quote)?
171 .with_user_transfer_type(encoding_options.transfer_type().clone()),
172 ));
173 }
174
175 let solutions: Vec<Solution> = to_encode
176 .iter()
177 .map(|(_, s)| s.clone())
178 .collect();
179 let encoded_solutions = self
180 .tycho_encoder
181 .encode_solutions(solutions)?;
182
183 for (encoded_solution, (idx, solution)) in encoded_solutions
184 .into_iter()
185 .zip(to_encode)
186 {
187 quotes[idx].set_gas_estimate(encoded_solution.estimated_gas().clone());
188 let (transaction, fee_breakdown) =
189 self.encode_tycho_router_call(encoded_solution, &solution, &encoding_options)?;
190 quotes[idx].set_transaction(transaction);
191 quotes[idx].set_fee_breakdown(fee_breakdown);
192 }
193
194 Ok(quotes)
195 }
196
197 fn encode_tycho_router_call(
206 &self,
207 encoded_solution: EncodedSolution,
208 solution: &Solution,
209 encoding_options: &EncodingOptions,
210 ) -> Result<(Transaction, FeeBreakdown), EncodingError> {
211 let amount_in = biguint_to_u256(solution.amount_in());
212 let swap_output = solution.min_amount_out();
213 let fee_breakdown = Self::calculate_fee_breakdown(
214 swap_output,
215 encoding_options
216 .client_fee_params()
217 .map_or(0, |f| f.bps()),
218 encoding_options.slippage(),
219 );
220 let min_amount_out = biguint_to_u256(fee_breakdown.min_amount_received());
221 let native_address = &self.chain.native_token().address;
222 let router_eth = Address::from_slice(ROUTER_ETH_ADDRESS.as_ref());
223 let to_router_address = |raw: Address| {
224 if raw.as_slice() == native_address.as_ref() {
225 router_eth
226 } else {
227 raw
228 }
229 };
230
231 let token_in = to_router_address(bytes_to_address(solution.token_in())?);
232 let token_out = to_router_address(bytes_to_address(solution.token_out())?);
233 let receiver = bytes_to_address(solution.receiver())?;
234
235 let (permit, permit2_sig) = if let Some(p) = encoding_options.permit() {
236 let d = p.details();
237 let permit = Some(PermitSingle {
238 details: SolPermitDetails {
239 token: bytes_to_address(d.token())?,
240 amount: U160::from(biguint_to_u256(d.amount())),
241 expiration: U48::from(biguint_to_u256(d.expiration())),
242 nonce: U48::from(biguint_to_u256(d.nonce())),
243 },
244 spender: bytes_to_address(p.spender())?,
245 sigDeadline: biguint_to_u256(p.sig_deadline()),
246 });
247 let sig = encoding_options
248 .permit2_signature()
249 .ok_or_else(|| {
250 EncodingError::FatalError("Signature must be provided for permit2".to_string())
251 })?
252 .to_vec();
253 (permit, sig)
254 } else {
255 (None, vec![])
256 };
257
258 let client_fee_params = if let Some(fee) = encoding_options.client_fee_params() {
259 (
260 fee.bps(),
261 bytes_to_address(fee.receiver())?,
262 biguint_to_u256(fee.max_contribution()),
263 U256::from(fee.deadline()),
264 {
267 let mut sig = fee.signature().to_vec();
268 sig.resize(65, 0);
269 sig
270 },
271 )
272 } else {
273 (0u16, Address::ZERO, U256::ZERO, U256::MAX, vec![])
274 };
275
276 let fn_sig = encoded_solution.function_signature();
277 let swaps = encoded_solution.swaps();
278 let fee_breakdown = if encoding_options
279 .client_fee_params()
280 .is_some()
281 {
282 fee_breakdown.with_swaps_hash(keccak256(swaps).0)
283 } else {
284 fee_breakdown
285 };
286
287 let method_calldata = if fn_sig.contains("Permit2") {
288 let permit = permit.ok_or(EncodingError::FatalError(
289 "permit2 object must be set to use permit2".to_string(),
290 ))?;
291 if fn_sig.contains("splitSwap") {
292 (
293 amount_in,
294 token_in,
295 token_out,
296 min_amount_out,
297 U256::from(encoded_solution.n_tokens()),
298 receiver,
299 client_fee_params,
300 permit,
301 permit2_sig,
302 swaps,
303 )
304 .abi_encode()
305 } else {
306 (
307 amount_in,
308 token_in,
309 token_out,
310 min_amount_out,
311 receiver,
312 client_fee_params,
313 permit,
314 permit2_sig,
315 swaps,
316 )
317 .abi_encode()
318 }
319 } else if fn_sig.contains("splitSwap") {
320 (
321 amount_in,
322 token_in,
323 token_out,
324 min_amount_out,
325 U256::from(encoded_solution.n_tokens()),
326 receiver,
327 client_fee_params,
328 swaps,
329 )
330 .abi_encode()
331 } else if fn_sig.contains("singleSwap") || fn_sig.contains("sequentialSwap") {
332 (amount_in, token_in, token_out, min_amount_out, receiver, client_fee_params, swaps)
333 .abi_encode()
334 } else {
335 return Err(EncodingError::FatalError(format!(
336 "unsupported function signature for Tycho router: {fn_sig}"
337 )));
338 };
339
340 let contract_interaction =
341 Self::encode_input(encoded_solution.function_signature(), method_calldata);
342
343 let value =
344 if token_in == router_eth { solution.amount_in().clone() } else { BigUint::ZERO };
345 let mut transaction = Transaction::new(
346 encoded_solution
347 .interacting_with()
348 .clone(),
349 value,
350 contract_interaction,
351 );
352 if encoding_options
353 .client_fee_params()
354 .is_some()
355 {
356 let offset = encoded_solution.client_fee_signature_offset();
357 transaction = transaction.with_client_fee_signature_offset(offset);
358 }
359 Ok((transaction, fee_breakdown))
360 }
361
362 fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
364 let mut hasher = Keccak256::new();
365 hasher.update(selector.as_bytes());
366 let selector_bytes = &hasher.finalize()[..4];
367 let mut call_data = selector_bytes.to_vec();
368 if encoded_args.len() > 32 &&
372 encoded_args[..32] ==
373 [0u8; 31]
374 .into_iter()
375 .chain([32].to_vec())
376 .collect::<Vec<u8>>()
377 {
378 encoded_args = encoded_args[32..].to_vec();
379 }
380 call_data.extend(encoded_args);
381 call_data
382 }
383
384 fn calculate_fee_breakdown(
389 swap_output: &BigUint,
390 client_fee_bps: u16,
391 slippage: f64,
392 ) -> FeeBreakdown {
393 let client_bps = client_fee_bps as u64;
394
395 let mut router_fee_on_client = BigUint::ZERO;
396 let mut client_portion = BigUint::ZERO;
397
398 if client_bps > 0 {
399 let fee_numerator = swap_output * client_bps;
400 let total_client_fee = &fee_numerator / 10_000u64;
401
402 router_fee_on_client = &fee_numerator * ROUTER_FEE_ON_CLIENT_FEE_BPS / 100_000_000u64;
403
404 client_portion = total_client_fee - &router_fee_on_client;
405 }
406
407 let router_fee_on_output = swap_output * ROUTER_FEE_ON_OUTPUT_BPS / 10_000u64;
408 let total_router_fee = router_fee_on_client + router_fee_on_output;
409
410 let amount_after_fees = swap_output - &client_portion - &total_router_fee;
411
412 let precision = BigUint::from(1_000_000u64);
413 let slippage_amount =
414 &amount_after_fees * BigUint::from((slippage * 1_000_000.0) as u64) / &precision;
415
416 let min_amount_received = &amount_after_fees - &slippage_amount;
417
418 FeeBreakdown::new(total_router_fee, client_portion, slippage_amount, min_amount_received)
419 }
420}
421
422impl From<EncodingError> for SolveError {
423 fn from(err: EncodingError) -> Self {
424 SolveError::FailedEncoding(err.to_string())
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use std::collections::HashMap;
431
432 use num_bigint::BigUint;
433 use tycho_execution::encoding::{
434 errors::EncodingError,
435 models::{EncodedSolution, Solution},
436 tycho_encoder::TychoEncoder,
437 };
438 use tycho_simulation::tycho_core::{
439 models::{token::Token, Address, Chain as SimChain},
440 Bytes,
441 };
442
443 use super::*;
444 use crate::{
445 algorithm::test_utils::{component, MockProtocolSim},
446 BlockInfo, OrderQuote, QuoteStatus,
447 };
448
449 fn make_token(addr: Address) -> Token {
450 Token {
451 address: addr,
452 symbol: "T".to_string(),
453 decimals: 18,
454 tax: Default::default(),
455 gas: vec![],
456 chain: SimChain::Ethereum,
457 quality: 100,
458 }
459 }
460
461 fn make_route_swap_addrs(token_in: Address, token_out: Address) -> crate::types::Swap {
462 let tin = make_token(token_in.clone());
463 let tout = make_token(token_out.clone());
464 let pool_addr = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
466 crate::types::Swap::new(
467 pool_addr.to_string(),
468 "uniswap_v2".to_string(),
469 token_in,
470 token_out,
471 BigUint::from(1000u64),
472 BigUint::from(990u64),
473 BigUint::from(50_000u64),
474 component(pool_addr, &[tin, tout]),
475 Box::new(MockProtocolSim::default()),
476 )
477 }
478
479 fn make_route_with_tokens(pairs: &[(Address, Address)]) -> crate::types::Route {
482 let mut tokens = HashMap::new();
483 let swaps = pairs
484 .iter()
485 .map(|(tin, tout)| {
486 tokens
487 .entry(tin.clone())
488 .or_insert_with(|| make_token(tin.clone()));
489 tokens
490 .entry(tout.clone())
491 .or_insert_with(|| make_token(tout.clone()));
492 make_route_swap_addrs(tin.clone(), tout.clone())
493 })
494 .collect();
495 crate::types::Route::new(swaps, tokens)
496 }
497
498 fn make_address(byte: u8) -> Address {
499 Address::from([byte; 20])
500 }
501
502 fn make_order_quote() -> OrderQuote {
503 OrderQuote::new(
504 "test-order".to_string(),
505 QuoteStatus::Success,
506 BigUint::from(1000u64),
507 BigUint::from(990u64),
508 BigUint::from(100_000u64),
509 BigUint::from(990u64),
510 BlockInfo::new(1, "0x123".to_string(), 1000),
511 "test".to_string(),
512 Bytes::from(make_address(0xAA).as_ref()),
513 Bytes::from(make_address(0xAA).as_ref()),
514 "1".to_string(),
515 )
516 }
517
518 struct MockTychoEncoder;
519
520 impl TychoEncoder for MockTychoEncoder {
521 fn encode_solutions(
522 &self,
523 _solutions: Vec<Solution>,
524 ) -> Result<Vec<EncodedSolution>, EncodingError> {
525 Ok(vec![])
526 }
527
528 fn validate_solution(&self, _solution: &Solution) -> Result<(), EncodingError> {
529 Ok(())
530 }
531 }
532
533 fn mock_encoder(chain: Chain) -> Encoder {
534 Encoder {
535 tycho_encoder: Box::new(MockTychoEncoder),
536 chain,
537 router_address: Bytes::from([0u8; 20].as_ref()),
538 }
539 }
540
541 #[test]
542 fn test_encoder_new_fails_on_unsupported_chain() {
543 let registry =
547 tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry::new(Chain::Ethereum)
548 .add_default_encoders(None)
549 .expect("registry should build for Ethereum");
550 let result = Encoder::new(Chain::Starknet, registry);
551 assert!(result.is_err(), "expected Err for chain without router address, got Ok");
552 }
553
554 #[test]
555 fn test_try_from_without_route_errors() {
556 let quote = make_order_quote();
557
558 let result = Solution::try_from("e);
559
560 assert!(result.is_err());
561 }
562
563 #[test]
564 fn test_try_from_non_success_errors() {
565 let quote = OrderQuote::new(
566 "test-order".to_string(),
567 QuoteStatus::NoRouteFound,
568 BigUint::from(1000u64),
569 BigUint::from(990u64),
570 BigUint::from(100_000u64),
571 BigUint::from(990u64),
572 BlockInfo::new(1, "0x123".to_string(), 1000),
573 "test".to_string(),
574 Bytes::from(make_address(0xAA).as_ref()),
575 Bytes::from(make_address(0xAA).as_ref()),
576 "1".to_string(),
577 );
578
579 let result = Solution::try_from("e);
580
581 assert!(result.is_err());
582 }
583
584 #[test]
585 fn test_try_from_maps_tokens_and_amounts() {
586 let quote = make_order_quote()
587 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
588
589 let solution = Solution::try_from("e).unwrap();
590
591 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
592 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x02).as_ref()));
593 assert_eq!(*solution.amount_in(), *quote.amount_in());
594 assert_eq!(*solution.min_amount_out(), *quote.amount_out());
595 assert_eq!(solution.swaps().len(), 1);
596 }
597
598 #[test]
599 fn test_try_from_multi_hop_uses_boundary_swap_tokens() {
600 let quote = make_order_quote().with_route(make_route_with_tokens(&[
601 (make_address(0x01), make_address(0x02)),
602 (make_address(0x02), make_address(0x03)),
603 ]));
604
605 let solution = Solution::try_from("e).unwrap();
606
607 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
608 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x03).as_ref()));
609 assert_eq!(solution.swaps().len(), 2);
610 }
611
612 #[tokio::test]
613 async fn test_encode_skips_non_successful_solutions() {
614 let encoder = mock_encoder(Chain::Ethereum);
615 let quote = OrderQuote::new(
616 "test-order".to_string(),
617 QuoteStatus::NoRouteFound,
618 BigUint::from(1000u64),
619 BigUint::from(990u64),
620 BigUint::from(100_000u64),
621 BigUint::from(990u64),
622 BlockInfo::new(1, "0x123".to_string(), 1000),
623 "test".to_string(),
624 Bytes::from(make_address(0xAA).as_ref()),
625 Bytes::from(make_address(0xAA).as_ref()),
626 "1".to_string(),
627 );
628
629 let encoding_options = EncodingOptions::new(0.01);
630
631 let result = encoder
632 .encode(vec![quote], encoding_options)
633 .await
634 .unwrap();
635
636 assert!(result[0].transaction().is_none());
637 }
638
639 fn real_encoder() -> Encoder {
640 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
641 .add_default_encoders(None)
642 .unwrap();
643 Encoder::new(Chain::Ethereum, registry).unwrap()
644 }
645
646 #[tokio::test]
647 async fn test_encode_sets_transaction_on_successful_solution() {
648 let encoder = real_encoder();
649 let quote = make_order_quote()
650 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
651
652 let encoding_options = EncodingOptions::new(0.01);
653
654 let result = encoder
655 .encode(vec![quote], encoding_options)
656 .await
657 .unwrap();
658
659 assert!(result[0].transaction().is_some());
660 let tx = result[0].transaction().unwrap();
661 assert!(!tx.data().is_empty());
662 assert!(tx.data().len() > 4);
664 }
665
666 #[tokio::test]
667 async fn test_encode_with_client_fee_params() {
668 let encoder = real_encoder();
669 let quote = make_order_quote()
670 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
671
672 let fee = crate::ClientFeeParams::new(
673 100,
674 Bytes::from(make_address(0xBB).as_ref()),
675 BigUint::from(0u64),
676 1_893_456_000u64,
677 Bytes::from(vec![0xAB; 65]),
678 );
679 let encoding_options = EncodingOptions::new(0.01).with_client_fee_params(fee);
680
681 let result = encoder
682 .encode(vec![quote], encoding_options)
683 .await
684 .unwrap();
685
686 assert!(result[0].transaction().is_some());
687 let tx = result[0].transaction().unwrap();
688 assert!(!tx.data().is_empty());
689 assert!(tx.data().len() > 4);
691 }
692
693 #[tokio::test]
694 async fn test_encode_without_client_fee_produces_transaction() {
695 let encoder = real_encoder();
696 let quote = make_order_quote()
697 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
698
699 let encoding_options = EncodingOptions::new(0.01);
700
701 let result = encoder
702 .encode(vec![quote], encoding_options)
703 .await
704 .unwrap();
705
706 assert!(result[0].transaction().is_some());
707 }
708
709 fn make_client_fee(bps: u16) -> crate::ClientFeeParams {
712 crate::ClientFeeParams::new(
713 bps,
714 Bytes::from(make_address(0xBB).as_ref()),
715 BigUint::from(0u64),
716 1_893_456_000u64,
717 Bytes::from(vec![]),
718 )
719 }
720
721 #[tokio::test]
722 async fn test_encode_with_client_fee_returns_signature_offset() {
723 let encoder = real_encoder();
724 let quote = make_order_quote()
725 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
726 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
727
728 let result = encoder
729 .encode(vec![quote], opts)
730 .await
731 .unwrap();
732
733 let tx = result[0].transaction().unwrap();
734 tx.client_fee_signature_offset()
735 .expect("client_fee_signature_offset must be present with client fee");
736 }
737
738 #[tokio::test]
739 async fn test_encode_without_client_fee_has_no_signature_offset() {
740 let encoder = real_encoder();
741 let quote = make_order_quote()
742 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
743 let opts = EncodingOptions::new(0.01);
744
745 let result = encoder
746 .encode(vec![quote], opts)
747 .await
748 .unwrap();
749
750 let tx = result[0].transaction().unwrap();
751 assert!(tx
752 .client_fee_signature_offset()
753 .is_none());
754 }
755
756 #[tokio::test]
757 async fn test_signature_offset_allows_patching() {
758 let encoder = real_encoder();
759 let real_sig = vec![0xFF; 65];
760 let quote = make_order_quote()
761 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
762 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
763
764 let result = encoder
765 .encode(vec![quote], opts)
766 .await
767 .unwrap();
768
769 let tx = result[0].transaction().unwrap();
770 let offset = tx
771 .client_fee_signature_offset()
772 .unwrap();
773
774 let mut calldata = tx.data().to_vec();
775 calldata[offset..offset + 65].copy_from_slice(&real_sig);
776 assert_eq!(&calldata[offset..offset + 65], &real_sig[..]);
777 }
778}