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::{
24 encoding::{
25 exclusive_swap::ExclusiveSwapSigner,
26 router_fees::{FeeRates, RouterFees, SharedRouterFees},
27 },
28 EncodingOptions, FeeBreakdown, OrderQuote, QuoteStatus, SolveError, Transaction,
29};
30
31pub const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
33
34pub struct Encoder {
44 tycho_encoder: Option<Box<dyn TychoEncoder>>,
45 chain: Chain,
46 router_address: Option<Bytes>,
47 router_fees: SharedRouterFees,
48 exclusive_swap_signer: Option<ExclusiveSwapSigner>,
50}
51
52impl TryFrom<&OrderQuote> for Solution {
53 type Error = SolveError;
54
55 fn try_from(quote: &OrderQuote) -> Result<Self, Self::Error> {
56 if quote.status() != QuoteStatus::Success {
57 return Err(SolveError::FailedEncoding(format!(
58 "cannot convert quote with status {:?} to Solution",
59 quote.status()
60 )));
61 }
62
63 let route = quote.route().ok_or_else(|| {
64 SolveError::FailedEncoding("successful quote must have a route".to_string())
65 })?;
66
67 let token_in = route
68 .input_token()
69 .ok_or_else(|| SolveError::FailedEncoding("route has no input token".to_string()))?;
70 let token_out = route
71 .output_token()
72 .ok_or_else(|| SolveError::FailedEncoding("route has no output token".to_string()))?;
73
74 let token_map = route.tokens();
75 let lookup_token = |addr: &Bytes| {
76 token_map
77 .get(addr)
78 .cloned()
79 .ok_or_else(|| {
80 SolveError::FailedEncoding(format!(
81 "token {addr:?} not found in route's token map; \
82 algorithm must populate Route::with_tokens for every swap token"
83 ))
84 })
85 };
86 let swaps = route
87 .swaps()
88 .iter()
89 .map(|s| {
90 let token_in = lookup_token(s.token_in())?;
91 let token_out = lookup_token(s.token_out())?;
92 Ok(Swap::new(
93 s.protocol_component().clone(),
94 token_in,
95 token_out,
96 s.gas_estimate().clone(),
97 )
98 .with_split(*s.split())
99 .with_protocol_state(Arc::from(s.protocol_state().clone_box()))
100 .with_estimated_amount_in(s.amount_in().clone()))
101 })
102 .collect::<Result<Vec<_>, SolveError>>()?;
103
104 Ok(Solution::new(
105 quote.sender().clone(),
106 quote.receiver().clone(),
107 Bytes::from(token_in.as_ref()),
108 Bytes::from(token_out.as_ref()),
109 quote.amount_in().clone(),
110 quote.amount_out().clone(),
111 swaps,
112 ))
113 }
114}
115
116impl Encoder {
117 pub fn is_supported(chain: Chain) -> bool {
119 get_router_address(&chain).is_ok()
120 }
121
122 pub fn new(
133 chain: Chain,
134 swap_encoder_registry: SwapEncoderRegistry,
135 ) -> Result<Self, SolveError> {
136 let router_address = get_router_address(&chain).ok().cloned();
137 let tycho_encoder = router_address
138 .is_some()
139 .then(|| {
140 TychoRouterEncoderBuilder::new()
141 .chain(chain)
142 .swap_encoder_registry(swap_encoder_registry)
143 .build()
144 })
145 .transpose()?;
146 let exclusive_swap_signer = ExclusiveSwapSigner::from_env(chain.id())?;
147 Ok(Self {
148 tycho_encoder,
149 chain,
150 router_address,
151 router_fees: SharedRouterFees::default(),
152 exclusive_swap_signer,
153 })
154 }
155
156 #[must_use]
158 pub fn with_exclusive_swap_signer(mut self, signer: ExclusiveSwapSigner) -> Self {
159 self.exclusive_swap_signer = Some(signer);
160 self
161 }
162
163 pub fn router_address(&self) -> Option<&Bytes> {
166 self.router_address.as_ref()
167 }
168
169 pub fn router_fees(&self) -> SharedRouterFees {
174 self.router_fees.clone()
175 }
176
177 pub async fn encode(
186 &self,
187 mut quotes: Vec<OrderQuote>,
188 encoding_options: EncodingOptions,
189 ) -> Result<Vec<OrderQuote>, SolveError> {
190 let Some(tycho_encoder) = self.tycho_encoder.as_ref() else {
191 return Err(SolveError::EncodingUnavailable(format!(
192 "encoding is unavailable on chain '{}': no Tycho router is deployed. Fynd is \
193 running quote-only; contact ops to deploy the router/executor contracts.",
194 self.chain
195 )));
196 };
197
198 let slippage = encoding_options.slippage();
199 if slippage == 0.0 {
200 tracing::warn!("slippage is 0, transaction will likely revert");
201 } else if slippage > 0.5 {
202 tracing::warn!(slippage, "slippage exceeds 50%, possible misconfiguration");
203 }
204
205 let mut to_encode: Vec<(usize, Solution)> = Vec::new();
206
207 for (i, quote) in quotes.iter().enumerate() {
208 if quote.status() != QuoteStatus::Success {
209 continue;
210 }
211
212 let solution = Solution::try_from(quote)?
213 .with_user_transfer_type(encoding_options.transfer_type().clone());
214 let solution = match &self.exclusive_swap_signer {
215 Some(signer) => Self::stamp_exclusive_swaps(solution, quote, signer)?,
216 None => {
217 if has_exclusive_leg(quote) {
220 return Err(SolveError::FailedEncoding(
221 "quote routes through an exclusive pool but no signing key is \
222 configured (set EXCLUSIVE_SWAP_CONTROLLER_KEY)"
223 .to_string(),
224 ));
225 }
226 solution
227 }
228 };
229 to_encode.push((i, solution));
230 }
231
232 let solutions: Vec<Solution> = to_encode
233 .iter()
234 .map(|(_, s)| s.clone())
235 .collect();
236 let encoded_solutions = tycho_encoder.encode_solutions(solutions)?;
237
238 let router_fees = self.router_fees.snapshot();
239 for (encoded_solution, (idx, solution)) in encoded_solutions
240 .into_iter()
241 .zip(to_encode)
242 {
243 quotes[idx].set_gas_estimate(encoded_solution.estimated_gas().clone());
244 let (transaction, fee_breakdown) = self.encode_tycho_router_call(
245 encoded_solution,
246 &solution,
247 &encoding_options,
248 &router_fees,
249 )?;
250 quotes[idx].set_transaction(transaction);
251 quotes[idx].set_fee_breakdown(fee_breakdown);
252 }
253
254 Ok(quotes)
255 }
256
257 fn stamp_exclusive_swaps(
263 solution: Solution,
264 quote: &OrderQuote,
265 signer: &ExclusiveSwapSigner,
266 ) -> Result<Solution, SolveError> {
267 let route = quote.route().ok_or_else(|| {
268 SolveError::FailedEncoding("successful quote must have a route".to_string())
269 })?;
270 let route_swaps = route.swaps();
271
272 if !route_swaps
274 .iter()
275 .any(|s| s.committed_amount_out().is_some())
276 {
277 return Ok(solution);
278 }
279
280 let swaps = solution
285 .swaps()
286 .iter()
287 .cloned()
288 .zip(route_swaps.iter())
289 .map(|(solution_swap, route_swap)| {
292 if route_swap
293 .committed_amount_out()
294 .is_some()
295 {
296 let user_data = signer.build_user_data(route_swap)?;
297 Ok(solution_swap.with_user_data(user_data))
298 } else {
299 Ok(solution_swap)
300 }
301 })
302 .collect::<Result<Vec<_>, SolveError>>()?;
303
304 Ok(solution.with_swaps(swaps))
305 }
306
307 fn encode_tycho_router_call(
316 &self,
317 encoded_solution: EncodedSolution,
318 solution: &Solution,
319 encoding_options: &EncodingOptions,
320 router_fees: &RouterFees,
321 ) -> Result<(Transaction, FeeBreakdown), EncodingError> {
322 let amount_in = biguint_to_u256(solution.amount_in());
323 let swap_output = solution.min_amount_out();
324 let fee_client = encoding_options
328 .client_fee_params()
329 .map_or_else(|| solution.sender(), |f| f.receiver());
330 let fee_breakdown = Self::calculate_fee_breakdown(
331 swap_output,
332 encoding_options
333 .client_fee_params()
334 .map_or(0, |f| f.bps()),
335 encoding_options.slippage(),
336 router_fees.fees_for(fee_client),
337 )?;
338 let min_amount_out = biguint_to_u256(fee_breakdown.min_amount_received());
339 let native_address = &self.chain.native_token().address;
340 let router_eth = Address::from_slice(ROUTER_ETH_ADDRESS.as_ref());
341 let to_router_address = |raw: Address| {
342 if raw.as_slice() == native_address.as_ref() {
343 router_eth
344 } else {
345 raw
346 }
347 };
348
349 let token_in = to_router_address(bytes_to_address(solution.token_in())?);
350 let token_out = to_router_address(bytes_to_address(solution.token_out())?);
351 let receiver = bytes_to_address(solution.receiver())?;
352
353 let (permit, permit2_sig) = if let Some(p) = encoding_options.permit() {
354 let d = p.details();
355 let permit = Some(PermitSingle {
356 details: SolPermitDetails {
357 token: bytes_to_address(d.token())?,
358 amount: U160::from(biguint_to_u256(d.amount())),
359 expiration: U48::from(biguint_to_u256(d.expiration())),
360 nonce: U48::from(biguint_to_u256(d.nonce())),
361 },
362 spender: bytes_to_address(p.spender())?,
363 sigDeadline: biguint_to_u256(p.sig_deadline()),
364 });
365 let sig = encoding_options
366 .permit2_signature()
367 .ok_or_else(|| {
368 EncodingError::FatalError("Signature must be provided for permit2".to_string())
369 })?
370 .to_vec();
371 (permit, sig)
372 } else {
373 (None, vec![])
374 };
375
376 let client_fee_params = if let Some(fee) = encoding_options.client_fee_params() {
377 (
378 fee.bps(),
379 bytes_to_address(fee.receiver())?,
380 biguint_to_u256(fee.max_contribution()),
381 U256::from(fee.deadline()),
382 {
385 let mut sig = fee.signature().to_vec();
386 sig.resize(65, 0);
387 sig
388 },
389 )
390 } else {
391 (0u16, Address::ZERO, U256::ZERO, U256::MAX, vec![])
392 };
393
394 let fn_sig = encoded_solution.function_signature();
395 let swaps = encoded_solution.swaps();
396 let fee_breakdown = if encoding_options
397 .client_fee_params()
398 .is_some()
399 {
400 fee_breakdown.with_swaps_hash(keccak256(swaps).0)
401 } else {
402 fee_breakdown
403 };
404
405 let method_calldata = if fn_sig.contains("Permit2") {
406 let permit = permit.ok_or(EncodingError::FatalError(
407 "permit2 object must be set to use permit2".to_string(),
408 ))?;
409 if fn_sig.contains("splitSwap") {
410 (
411 amount_in,
412 token_in,
413 token_out,
414 min_amount_out,
415 U256::from(encoded_solution.n_tokens()),
416 receiver,
417 client_fee_params,
418 permit,
419 permit2_sig,
420 swaps,
421 )
422 .abi_encode()
423 } else {
424 (
425 amount_in,
426 token_in,
427 token_out,
428 min_amount_out,
429 receiver,
430 client_fee_params,
431 permit,
432 permit2_sig,
433 swaps,
434 )
435 .abi_encode()
436 }
437 } else if fn_sig.contains("splitSwap") {
438 (
439 amount_in,
440 token_in,
441 token_out,
442 min_amount_out,
443 U256::from(encoded_solution.n_tokens()),
444 receiver,
445 client_fee_params,
446 swaps,
447 )
448 .abi_encode()
449 } else if fn_sig.contains("singleSwap") || fn_sig.contains("sequentialSwap") {
450 (amount_in, token_in, token_out, min_amount_out, receiver, client_fee_params, swaps)
451 .abi_encode()
452 } else {
453 return Err(EncodingError::FatalError(format!(
454 "unsupported function signature for Tycho router: {fn_sig}"
455 )));
456 };
457
458 let contract_interaction =
459 Self::encode_input(encoded_solution.function_signature(), method_calldata);
460
461 let value =
462 if token_in == router_eth { solution.amount_in().clone() } else { BigUint::ZERO };
463 let mut transaction = Transaction::new(
464 encoded_solution
465 .interacting_with()
466 .clone(),
467 value,
468 contract_interaction,
469 );
470 if encoding_options
471 .client_fee_params()
472 .is_some()
473 {
474 let offset = encoded_solution.client_fee_signature_offset();
475 transaction = transaction.with_client_fee_signature_offset(offset);
476 }
477 Ok((transaction, fee_breakdown))
478 }
479
480 fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
482 let mut hasher = Keccak256::new();
483 hasher.update(selector.as_bytes());
484 let selector_bytes = &hasher.finalize()[..4];
485 let mut call_data = selector_bytes.to_vec();
486 if encoded_args.len() > 32 &&
490 encoded_args[..32] ==
491 [0u8; 31]
492 .into_iter()
493 .chain([32].to_vec())
494 .collect::<Vec<u8>>()
495 {
496 encoded_args = encoded_args[32..].to_vec();
497 }
498 call_data.extend(encoded_args);
499 call_data
500 }
501
502 fn calculate_fee_breakdown(
513 swap_output: &BigUint,
514 client_fee_bps: u16,
515 slippage: f64,
516 fee_rates: FeeRates,
517 ) -> Result<FeeBreakdown, EncodingError> {
518 let max_fee_units = fee_rates.max_fee_units();
519 let scaled_client_fee = client_fee_bps as u64 * fee_rates.fee_units_per_bps();
522 let fee_on_output = fee_rates.on_output() as u64;
523 let fee_on_client_fee = fee_rates.on_client_fee() as u64;
524
525 if scaled_client_fee + fee_on_output > max_fee_units {
526 return Err(EncodingError::FatalError(format!(
527 "client fee ({client_fee_bps} bps) plus router fee on output \
528 ({fee_on_output} fee units) exceed the {max_fee_units} fee-unit cap (100%); \
529 the router would revert"
530 )));
531 }
532 if fee_on_client_fee > max_fee_units {
533 return Err(EncodingError::FatalError(format!(
534 "router fee on client fee ({fee_on_client_fee} fee units) exceeds the \
535 {max_fee_units} fee-unit cap (100%); the router would revert"
536 )));
537 }
538
539 let mut router_fee_on_client = BigUint::ZERO;
540 let mut client_portion = BigUint::ZERO;
541
542 if scaled_client_fee > 0 {
543 let client_fee_numerator = swap_output * scaled_client_fee;
544 let total_client_fee = &client_fee_numerator / max_fee_units;
545
546 router_fee_on_client = client_fee_numerator * fee_on_client_fee /
547 BigUint::from(fee_rates.max_fee_units_squared());
548
549 client_portion = total_client_fee - &router_fee_on_client;
550 }
551
552 let router_fee_on_output = swap_output * fee_on_output / max_fee_units;
553 let total_router_fee = router_fee_on_client + router_fee_on_output;
554
555 let amount_after_fees = swap_output - &client_portion - &total_router_fee;
556
557 let precision = BigUint::from(1_000_000u64);
558 let slippage_amount =
559 &amount_after_fees * BigUint::from((slippage * 1_000_000.0) as u64) / &precision;
560
561 let min_amount_received = &amount_after_fees - &slippage_amount;
562
563 Ok(FeeBreakdown::new(
564 total_router_fee,
565 client_portion,
566 slippage_amount,
567 min_amount_received,
568 ))
569 }
570}
571
572impl From<EncodingError> for SolveError {
573 fn from(err: EncodingError) -> Self {
574 SolveError::FailedEncoding(err.to_string())
575 }
576}
577
578fn has_exclusive_leg(quote: &OrderQuote) -> bool {
581 quote.route().is_some_and(|route| {
582 route
583 .swaps()
584 .iter()
585 .any(|s| s.committed_amount_out().is_some())
586 })
587}
588
589#[cfg(test)]
590mod tests {
591 use std::collections::HashMap;
592
593 use num_bigint::BigUint;
594 use rstest::rstest;
595 use tycho_execution::encoding::{
596 errors::EncodingError,
597 models::{EncodedSolution, Solution},
598 tycho_encoder::TychoEncoder,
599 };
600 use tycho_simulation::tycho_core::{
601 models::{token::Token, Address, Chain as SimChain},
602 Bytes,
603 };
604
605 use super::*;
606 use crate::{
607 algorithm::test_utils::{component, MockProtocolSim},
608 BlockInfo, OrderQuote, QuoteStatus,
609 };
610
611 fn make_token(addr: Address) -> Token {
612 Token {
613 address: addr,
614 symbol: "T".to_string(),
615 decimals: 18,
616 tax: Default::default(),
617 gas: vec![],
618 chain: SimChain::Ethereum,
619 quality: 100,
620 }
621 }
622
623 fn make_route_swap_addrs(token_in: Address, token_out: Address) -> crate::types::Swap {
624 let tin = make_token(token_in.clone());
625 let tout = make_token(token_out.clone());
626 let pool_addr = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
628 crate::types::Swap::new(
629 pool_addr.to_string(),
630 "uniswap_v2".to_string(),
631 token_in,
632 token_out,
633 BigUint::from(1000u64),
634 BigUint::from(990u64),
635 BigUint::from(50_000u64),
636 component(pool_addr, &[tin, tout]),
637 Box::new(MockProtocolSim::default()),
638 )
639 }
640
641 fn make_route_with_tokens(pairs: &[(Address, Address)]) -> crate::types::Route {
644 let mut tokens = HashMap::new();
645 let swaps = pairs
646 .iter()
647 .map(|(tin, tout)| {
648 tokens
649 .entry(tin.clone())
650 .or_insert_with(|| make_token(tin.clone()));
651 tokens
652 .entry(tout.clone())
653 .or_insert_with(|| make_token(tout.clone()));
654 make_route_swap_addrs(tin.clone(), tout.clone())
655 })
656 .collect();
657 crate::types::Route::new(swaps, tokens).expect("non-empty route")
658 }
659
660 fn make_address(byte: u8) -> Address {
661 Address::from([byte; 20])
662 }
663
664 fn make_order_quote(amount_out: u64) -> OrderQuote {
665 OrderQuote::new(
666 "test-order".to_string(),
667 QuoteStatus::Success,
668 BigUint::from(1000u64),
669 BigUint::from(amount_out),
670 BigUint::from(100_000u64),
671 BigUint::from(amount_out),
672 BlockInfo::new(1, "0x123".to_string(), 1000),
673 "test".to_string(),
674 Bytes::from(make_address(0xAA).as_ref()),
675 Bytes::from(make_address(0xAA).as_ref()),
676 "1".to_string(),
677 )
678 }
679
680 struct MockTychoEncoder;
681
682 impl TychoEncoder for MockTychoEncoder {
683 fn encode_solutions(
684 &self,
685 _solutions: Vec<Solution>,
686 ) -> Result<Vec<EncodedSolution>, EncodingError> {
687 Ok(vec![])
688 }
689
690 fn validate_solution(&self, _solution: &Solution) -> Result<(), EncodingError> {
691 Ok(())
692 }
693 }
694
695 fn mock_encoder(chain: Chain) -> Encoder {
696 let router_fees = SharedRouterFees::default();
697 router_fees.set(RouterFees::new(FEE_SCALE, 100_000, 20_000_000, HashMap::new()));
698 Encoder {
699 tycho_encoder: Some(Box::new(MockTychoEncoder)),
700 chain,
701 router_address: Some(Bytes::from([0u8; 20].as_ref())),
702 router_fees,
703 exclusive_swap_signer: None,
704 }
705 }
706
707 #[test]
708 fn test_encoder_new_disabled_on_unsupported_chain() {
709 let registry =
713 tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry::new(Chain::Ethereum)
714 .add_default_encoders(None)
715 .expect("registry should build for Ethereum");
716 let encoder = Encoder::new(Chain::Starknet, registry)
717 .expect("new must not fail for a router-less chain");
718 assert!(
719 encoder.router_address().is_none(),
720 "expected disabled encoder, got a router address"
721 );
722 }
723
724 #[tokio::test]
725 async fn disabled_encoder_quotes_but_refuses_to_encode() {
726 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
728 .add_default_encoders(None)
729 .unwrap();
730 let encoder =
731 Encoder::new(Chain::Starknet, registry).expect("new must not fail when disabled");
732 assert!(encoder.router_address().is_none());
733
734 let err = encoder
735 .encode(vec![], EncodingOptions::new(0.01))
736 .await
737 .expect_err("encoding must fail on a router-less chain");
738 assert!(matches!(err, SolveError::EncodingUnavailable(_)));
739 }
740
741 #[test]
742 fn test_try_from_without_route_errors() {
743 let quote = make_order_quote(990);
744
745 let result = Solution::try_from("e);
746
747 assert!(result.is_err());
748 }
749
750 #[test]
751 fn test_try_from_non_success_errors() {
752 let quote = OrderQuote::new(
753 "test-order".to_string(),
754 QuoteStatus::NoRouteFound,
755 BigUint::from(1000u64),
756 BigUint::from(990u64),
757 BigUint::from(100_000u64),
758 BigUint::from(990u64),
759 BlockInfo::new(1, "0x123".to_string(), 1000),
760 "test".to_string(),
761 Bytes::from(make_address(0xAA).as_ref()),
762 Bytes::from(make_address(0xAA).as_ref()),
763 "1".to_string(),
764 );
765
766 let result = Solution::try_from("e);
767
768 assert!(result.is_err());
769 }
770
771 #[test]
772 fn test_try_from_maps_tokens_and_amounts() {
773 let quote = make_order_quote(990)
774 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
775
776 let solution = Solution::try_from("e).unwrap();
777
778 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
779 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x02).as_ref()));
780 assert_eq!(*solution.amount_in(), *quote.amount_in());
781 assert_eq!(*solution.min_amount_out(), *quote.amount_out());
782 assert_eq!(solution.swaps().len(), 1);
783 }
784
785 const CONTROLLER_KEY: &str =
786 "0x1111111111111111111111111111111111111111111111111111111111111111";
787
788 fn ekubo_signed_swap(committed: Option<u64>) -> crate::types::Swap {
789 let token_in = make_address(0x11);
790 let token_out = make_address(0x22);
791 let mut comp = component("ekubo-signed-pool", &[]);
792 comp.static_attributes
793 .insert("extension".to_string(), Bytes::from([0x55u8; 20].as_ref()));
794 comp.static_attributes
795 .insert("fee".to_string(), Bytes::from(0u64));
796 comp.static_attributes
797 .insert("pool_type_config".to_string(), Bytes::from(0u32));
798
799 let mut swap = crate::types::Swap::new(
800 "ekubo-signed-pool".to_string(),
801 "ekubo_v3".to_string(),
802 token_in,
803 token_out,
804 BigUint::from(1_000_000u64),
805 BigUint::from(1_000_000u64),
806 BigUint::from(50_000u64),
807 comp,
808 Box::new(MockProtocolSim::default()),
809 );
810 if let Some(committed) = committed {
811 swap.set_committed_amount_out(BigUint::from(committed));
812 }
813 swap
814 }
815
816 fn single_swap_route(swap: crate::types::Swap) -> crate::types::Route {
817 let tokens = HashMap::from([
818 (swap.token_in().clone(), make_token(swap.token_in().clone())),
819 (swap.token_out().clone(), make_token(swap.token_out().clone())),
820 ]);
821 crate::types::Route::new(vec![swap], tokens).expect("non-empty route")
822 }
823
824 #[rstest]
825 #[case::exclusive_leg_signed(Some(990_000), true)]
826 #[case::public_leg_untouched(None, false)]
827 fn test_stamp_exclusive_swaps(#[case] committed: Option<u64>, #[case] signed: bool) {
828 let quote =
829 make_order_quote(990_000).with_route(single_swap_route(ekubo_signed_swap(committed)));
830 let signer = ExclusiveSwapSigner::new(CONTROLLER_KEY.parse().unwrap(), 1, 0, 120);
831
832 let solution =
833 Encoder::stamp_exclusive_swaps(Solution::try_from("e).unwrap(), "e, &signer)
834 .unwrap();
835
836 assert_eq!(
837 solution.swaps()[0]
838 .user_data()
839 .is_some(),
840 signed
841 );
842 }
843
844 #[tokio::test]
845 async fn test_encode_rejects_exclusive_leg_without_signer() {
846 let encoder = mock_encoder(Chain::Ethereum);
849 let quote = make_order_quote(990_000)
850 .with_route(single_swap_route(ekubo_signed_swap(Some(990_000))));
851
852 let result = encoder
853 .encode(vec![quote], EncodingOptions::new(0.01))
854 .await;
855
856 assert!(result.is_err(), "expected fail-fast error for unsigned exclusive leg");
857 }
858
859 #[test]
860 fn test_try_from_multi_hop_uses_boundary_swap_tokens() {
861 let quote = make_order_quote(990).with_route(make_route_with_tokens(&[
862 (make_address(0x01), make_address(0x02)),
863 (make_address(0x02), make_address(0x03)),
864 ]));
865
866 let solution = Solution::try_from("e).unwrap();
867
868 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
869 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x03).as_ref()));
870 assert_eq!(solution.swaps().len(), 2);
871 }
872
873 #[tokio::test]
874 async fn test_encode_skips_non_successful_solutions() {
875 let encoder = mock_encoder(Chain::Ethereum);
876 let quote = OrderQuote::new(
877 "test-order".to_string(),
878 QuoteStatus::NoRouteFound,
879 BigUint::from(1000u64),
880 BigUint::from(990u64),
881 BigUint::from(100_000u64),
882 BigUint::from(990u64),
883 BlockInfo::new(1, "0x123".to_string(), 1000),
884 "test".to_string(),
885 Bytes::from(make_address(0xAA).as_ref()),
886 Bytes::from(make_address(0xAA).as_ref()),
887 "1".to_string(),
888 );
889
890 let encoding_options = EncodingOptions::new(0.01);
891
892 let result = encoder
893 .encode(vec![quote], encoding_options)
894 .await
895 .unwrap();
896
897 assert!(result[0].transaction().is_none());
898 }
899
900 fn real_encoder() -> Encoder {
901 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
902 .add_default_encoders(None)
903 .unwrap();
904 let encoder = Encoder::new(Chain::Ethereum, registry).unwrap();
905 encoder
907 .router_fees()
908 .set(RouterFees::new(FEE_SCALE, 100_000, 20_000_000, HashMap::new()));
909 encoder
910 }
911
912 #[tokio::test]
913 async fn test_encode_sets_transaction_on_successful_solution() {
914 let encoder = real_encoder();
915 let quote = make_order_quote(990)
916 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
917
918 let encoding_options = EncodingOptions::new(0.01);
919
920 let result = encoder
921 .encode(vec![quote], encoding_options)
922 .await
923 .unwrap();
924
925 assert!(result[0].transaction().is_some());
926 let tx = result[0].transaction().unwrap();
927 assert!(!tx.data().is_empty());
928 assert!(tx.data().len() > 4);
930 }
931
932 #[tokio::test]
933 async fn test_encode_with_client_fee_params() {
934 let encoder = real_encoder();
935 let quote = make_order_quote(990)
936 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
937
938 let fee = crate::ClientFeeParams::new(
939 100,
940 Bytes::from(make_address(0xBB).as_ref()),
941 BigUint::from(0u64),
942 1_893_456_000u64,
943 Bytes::from(vec![0xAB; 65]),
944 );
945 let encoding_options = EncodingOptions::new(0.01).with_client_fee_params(fee);
946
947 let result = encoder
948 .encode(vec![quote], encoding_options)
949 .await
950 .unwrap();
951
952 assert!(result[0].transaction().is_some());
953 let tx = result[0].transaction().unwrap();
954 assert!(!tx.data().is_empty());
955 assert!(tx.data().len() > 4);
957 }
958
959 #[tokio::test]
960 async fn test_encode_without_client_fee_produces_transaction() {
961 let encoder = real_encoder();
962 let quote = make_order_quote(990)
963 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
964
965 let encoding_options = EncodingOptions::new(0.01);
966
967 let result = encoder
968 .encode(vec![quote], encoding_options)
969 .await
970 .unwrap();
971
972 assert!(result[0].transaction().is_some());
973 }
974
975 fn make_client_fee(bps: u16) -> crate::ClientFeeParams {
978 crate::ClientFeeParams::new(
979 bps,
980 Bytes::from(make_address(0xBB).as_ref()),
981 BigUint::from(0u64),
982 1_893_456_000u64,
983 Bytes::from(vec![]),
984 )
985 }
986
987 #[tokio::test]
988 async fn test_encode_with_client_fee_returns_signature_offset() {
989 let encoder = real_encoder();
990 let quote = make_order_quote(990)
991 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
992 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
993
994 let result = encoder
995 .encode(vec![quote], opts)
996 .await
997 .unwrap();
998
999 let tx = result[0].transaction().unwrap();
1000 tx.client_fee_signature_offset()
1001 .expect("client_fee_signature_offset must be present with client fee");
1002 }
1003
1004 #[tokio::test]
1005 async fn test_encode_without_client_fee_has_no_signature_offset() {
1006 let encoder = real_encoder();
1007 let quote = make_order_quote(990)
1008 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1009 let opts = EncodingOptions::new(0.01);
1010
1011 let result = encoder
1012 .encode(vec![quote], opts)
1013 .await
1014 .unwrap();
1015
1016 let tx = result[0].transaction().unwrap();
1017 assert!(tx
1018 .client_fee_signature_offset()
1019 .is_none());
1020 }
1021
1022 #[tokio::test]
1023 async fn test_signature_offset_allows_patching() {
1024 let encoder = real_encoder();
1025 let real_sig = vec![0xFF; 65];
1026 let quote = make_order_quote(990)
1027 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1028 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
1029
1030 let result = encoder
1031 .encode(vec![quote], opts)
1032 .await
1033 .unwrap();
1034
1035 let tx = result[0].transaction().unwrap();
1036 let offset = tx
1037 .client_fee_signature_offset()
1038 .unwrap();
1039
1040 let mut calldata = tx.data().to_vec();
1041 calldata[offset..offset + 65].copy_from_slice(&real_sig);
1042 assert_eq!(&calldata[offset..offset + 65], &real_sig[..]);
1043 }
1044
1045 const FEE_SCALE: u64 = 100_000_000;
1049
1050 #[test]
1051 fn test_calculate_fee_breakdown() {
1052 let rates = FeeRates::new(100_000, 20_000_000, FEE_SCALE);
1054
1055 let breakdown =
1056 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 100, 0.0, rates)
1057 .unwrap();
1058
1059 assert_eq!(*breakdown.client_fee(), BigUint::from(8_000u64));
1062 assert_eq!(*breakdown.router_fee(), BigUint::from(3_000u64));
1063 assert_eq!(*breakdown.min_amount_received(), BigUint::from(989_000u64));
1064 }
1065
1066 #[test]
1067 fn test_calculate_fee_breakdown_zero_fees() {
1068 let rates = FeeRates::new(0, 0, FEE_SCALE);
1069
1070 let breakdown =
1071 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 0, 0.0, rates).unwrap();
1072
1073 assert_eq!(*breakdown.client_fee(), BigUint::ZERO);
1074 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
1075 assert_eq!(*breakdown.min_amount_received(), BigUint::from(1_000_000u64));
1076 }
1077
1078 #[test]
1079 fn test_calculate_fee_breakdown_fee_too_high() {
1080 let rates = FeeRates::new(1, 0, FEE_SCALE);
1082
1083 let result =
1084 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 10_000, 0.0, rates);
1085
1086 assert!(result.is_err());
1087 }
1088
1089 #[tokio::test]
1090 async fn test_encode_uses_custom_fees_for_client_fee_receiver() {
1091 let encoder = real_encoder();
1092 let custom = HashMap::from([(Bytes::from(make_address(0xBB).as_ref()), (0u32, 0u32))]);
1094 encoder
1095 .router_fees()
1096 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
1097 let quote = make_order_quote(1_000_000_000)
1098 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1099 let opts = EncodingOptions::new(0.0).with_client_fee_params(make_client_fee(100));
1100
1101 let result = encoder
1102 .encode(vec![quote], opts)
1103 .await
1104 .unwrap();
1105
1106 let breakdown = result[0].fee_breakdown().unwrap();
1107 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
1108 assert_eq!(*breakdown.client_fee(), BigUint::from(10_000_000u64));
1110 }
1111
1112 #[tokio::test]
1113 async fn test_encode_falls_back_to_sender() {
1114 let encoder = real_encoder();
1115 let custom =
1118 HashMap::from([(Bytes::from(make_address(0xAA).as_ref()), (0u32, 20_000_000u32))]);
1119 encoder
1120 .router_fees()
1121 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
1122 let quote = make_order_quote(1_000_000_000)
1123 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1124
1125 let result = encoder
1126 .encode(vec![quote], EncodingOptions::new(0.0))
1127 .await
1128 .unwrap();
1129
1130 let breakdown = result[0].fee_breakdown().unwrap();
1131 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
1132 }
1133
1134 #[tokio::test]
1135 async fn test_encode_unknown_client() {
1136 let encoder = real_encoder();
1137 encoder
1138 .router_fees()
1139 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, HashMap::new()));
1140 let quote = make_order_quote(1_000_000_000)
1141 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1142
1143 let result = encoder
1144 .encode(vec![quote], EncodingOptions::new(0.0))
1145 .await
1146 .unwrap();
1147
1148 let breakdown = result[0].fee_breakdown().unwrap();
1149 assert_eq!(*breakdown.router_fee(), BigUint::from(10_000_000u64));
1151 }
1152}