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, SharedRouterFees},
27 },
28 EncodingOptions, FeeBreakdown, OrderQuote, QuoteStatus, SolveError, Transaction,
29};
30
31pub const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
33
34const MAX_SLIPPAGE_TOLERANCE_BPS: u64 = 2_000;
37
38const BPS_DENOMINATOR: u64 = 10_000;
40
41pub struct Encoder {
51 tycho_encoder: Option<Box<dyn TychoEncoder>>,
52 chain: Chain,
53 router_address: Option<Bytes>,
54 router_fees: SharedRouterFees,
55 exclusive_swap_signer: Option<ExclusiveSwapSigner>,
57 calldata_watermark: Option<Vec<u8>>,
61}
62
63impl TryFrom<&OrderQuote> for Solution {
69 type Error = SolveError;
70
71 fn try_from(quote: &OrderQuote) -> Result<Self, Self::Error> {
72 solution_from_quote(quote, quote.amount_out().clone())
73 }
74}
75
76fn solution_from_quote(
81 quote: &OrderQuote,
82 min_amount_out: BigUint,
83) -> Result<Solution, SolveError> {
84 if quote.status() != QuoteStatus::Success {
85 return Err(SolveError::FailedEncoding(format!(
86 "cannot convert quote with status {:?} to Solution",
87 quote.status()
88 )));
89 }
90
91 let route = quote.route().ok_or_else(|| {
92 SolveError::FailedEncoding("successful quote must have a route".to_string())
93 })?;
94
95 let token_in = route
96 .input_token()
97 .ok_or_else(|| SolveError::FailedEncoding("route has no input token".to_string()))?;
98 let token_out = route
99 .output_token()
100 .ok_or_else(|| SolveError::FailedEncoding("route has no output token".to_string()))?;
101
102 let token_map = route.tokens();
103 let lookup_token = |addr: &Bytes| {
104 token_map
105 .get(addr)
106 .cloned()
107 .ok_or_else(|| {
108 SolveError::FailedEncoding(format!(
109 "token {addr:?} not found in route's token map; \
110 algorithm must populate Route::with_tokens for every swap token"
111 ))
112 })
113 };
114 let swaps = route
115 .swaps()
116 .iter()
117 .map(|s| {
118 let token_in = lookup_token(s.token_in())?;
119 let token_out = lookup_token(s.token_out())?;
120 Ok(Swap::new(
121 s.protocol_component().clone(),
122 token_in,
123 token_out,
124 s.gas_estimate().clone(),
125 )
126 .with_split(*s.split())
127 .with_protocol_state(Arc::from(s.protocol_state().clone_box()))
128 .with_estimated_amount_in(s.amount_in().clone()))
129 })
130 .collect::<Result<Vec<_>, SolveError>>()?;
131
132 Ok(Solution::new(
133 quote.sender().clone(),
134 quote.receiver().clone(),
135 Bytes::from(token_in.as_ref()),
136 Bytes::from(token_out.as_ref()),
137 quote.amount_in().clone(),
138 quote.amount_out().clone(),
139 min_amount_out,
140 swaps,
141 ))
142}
143
144impl Encoder {
145 pub fn is_supported(chain: Chain) -> bool {
147 get_router_address(&chain).is_ok()
148 }
149
150 pub fn new(
161 chain: Chain,
162 swap_encoder_registry: SwapEncoderRegistry,
163 ) -> Result<Self, SolveError> {
164 let router_address = get_router_address(&chain).ok().cloned();
165 let tycho_encoder = router_address
166 .is_some()
167 .then(|| {
168 TychoRouterEncoderBuilder::new()
169 .chain(chain)
170 .swap_encoder_registry(swap_encoder_registry)
171 .build()
172 })
173 .transpose()?;
174 let exclusive_swap_signer = ExclusiveSwapSigner::from_env(chain.id())?;
175 Ok(Self {
176 tycho_encoder,
177 chain,
178 router_address,
179 router_fees: SharedRouterFees::default(),
180 exclusive_swap_signer,
181 calldata_watermark: None,
182 })
183 }
184
185 #[must_use]
189 pub fn with_calldata_watermark(mut self, watermark: impl Into<Vec<u8>>) -> Self {
190 self.calldata_watermark = Some(watermark.into());
191 self
192 }
193
194 #[must_use]
196 pub fn with_exclusive_swap_signer(mut self, signer: ExclusiveSwapSigner) -> Self {
197 self.exclusive_swap_signer = Some(signer);
198 self
199 }
200
201 pub fn router_address(&self) -> Option<&Bytes> {
204 self.router_address.as_ref()
205 }
206
207 pub fn chain(&self) -> Chain {
209 self.chain
210 }
211
212 pub fn router_fees(&self) -> SharedRouterFees {
217 self.router_fees.clone()
218 }
219
220 pub async fn encode(
229 &self,
230 mut quotes: Vec<OrderQuote>,
231 encoding_options: EncodingOptions,
232 ) -> Result<Vec<OrderQuote>, SolveError> {
233 let Some(tycho_encoder) = self.tycho_encoder.as_ref() else {
234 return Err(SolveError::EncodingUnavailable(format!(
235 "encoding is unavailable on chain '{}': no Tycho router is deployed. Fynd is \
236 running quote-only; contact ops to deploy the router/executor contracts.",
237 self.chain
238 )));
239 };
240
241 let slippage = encoding_options.slippage();
242 if slippage == 0.0 {
243 tracing::warn!("slippage is 0, transaction will likely revert");
244 } else if slippage > 0.5 {
245 tracing::warn!(slippage, "slippage exceeds 50%, possible misconfiguration");
246 }
247
248 let router_fees = self.router_fees.snapshot();
249 let mut to_encode: Vec<(usize, Solution, FeeBreakdown, FeeRates)> = Vec::new();
250
251 for (i, quote) in quotes.iter().enumerate() {
252 if quote.status() != QuoteStatus::Success {
253 continue;
254 }
255
256 let fee_client = encoding_options
260 .client_fee_params()
261 .map_or_else(|| quote.sender(), |f| f.receiver());
262 let fee_rates = router_fees.fees_for(fee_client);
263 let fee_breakdown = Self::calculate_fee_breakdown(
264 quote.amount_out(),
265 encoding_options
266 .client_fee_params()
267 .map_or(0, |f| f.bps()),
268 slippage,
269 fee_rates,
270 )?;
271 Self::check_slippage_guardrail(
272 biguint_to_u256(quote.amount_out()),
273 biguint_to_u256(fee_breakdown.min_amount_received()),
274 )?;
275
276 let solution = solution_from_quote(
277 quote,
278 fee_breakdown
279 .min_amount_received()
280 .clone(),
281 )?
282 .with_user_transfer_type(encoding_options.transfer_type().clone());
283 let solution = match &self.exclusive_swap_signer {
284 Some(signer) => Self::stamp_exclusive_swaps(solution, quote, signer)?,
285 None => {
286 if has_exclusive_leg(quote) {
289 return Err(SolveError::FailedEncoding(
290 "quote routes through an exclusive pool but no signing key is \
291 configured (set EXCLUSIVE_SWAP_CONTROLLER_KEY)"
292 .to_string(),
293 ));
294 }
295 solution
296 }
297 };
298 to_encode.push((i, solution, fee_breakdown, fee_rates));
299 }
300
301 let solutions: Vec<Solution> = to_encode
302 .iter()
303 .map(|(_, s, _, _)| s.clone())
304 .collect();
305 let encoded_solutions = tycho_encoder.encode_solutions(solutions)?;
306
307 for (encoded_solution, (idx, solution, fee_breakdown, fee_rates)) in encoded_solutions
308 .into_iter()
309 .zip(to_encode)
310 {
311 quotes[idx].set_gas_estimate(encoded_solution.estimated_gas().clone());
312 let (transaction, fee_breakdown) = self.encode_tycho_router_call(
313 encoded_solution,
314 &solution,
315 &encoding_options,
316 fee_breakdown,
317 fee_rates,
318 )?;
319 quotes[idx].set_transaction(transaction);
320 quotes[idx].set_fee_breakdown(fee_breakdown);
321 }
322
323 Ok(quotes)
324 }
325
326 fn stamp_exclusive_swaps(
332 solution: Solution,
333 quote: &OrderQuote,
334 signer: &ExclusiveSwapSigner,
335 ) -> Result<Solution, SolveError> {
336 let route = quote.route().ok_or_else(|| {
337 SolveError::FailedEncoding("successful quote must have a route".to_string())
338 })?;
339 let route_swaps = route.swaps();
340
341 if !route_swaps
343 .iter()
344 .any(|s| s.committed_amount_out().is_some())
345 {
346 return Ok(solution);
347 }
348
349 let swaps = solution
355 .swaps()
356 .iter()
357 .cloned()
358 .zip(route_swaps.iter())
359 .map(|(solution_swap, route_swap)| {
362 if route_swap
363 .committed_amount_out()
364 .is_some()
365 {
366 let user_data = signer.build_user_data(route_swap)?;
367 Ok(solution_swap.with_user_data(user_data))
368 } else {
369 Ok(solution_swap)
370 }
371 })
372 .collect::<Result<Vec<_>, SolveError>>()?;
373
374 Ok(solution.with_swaps(swaps))
375 }
376
377 fn encode_tycho_router_call(
387 &self,
388 encoded_solution: EncodedSolution,
389 solution: &Solution,
390 encoding_options: &EncodingOptions,
391 fee_breakdown: FeeBreakdown,
392 fee_rates: FeeRates,
393 ) -> Result<(Transaction, FeeBreakdown), EncodingError> {
394 let amount_in = biguint_to_u256(solution.amount_in());
395 let expected_amount_out = biguint_to_u256(solution.expected_amount_out());
396 let min_amount_out = biguint_to_u256(solution.min_amount_out());
397 let native_address = &self.chain.native_token().address;
398 let router_eth = Address::from_slice(ROUTER_ETH_ADDRESS.as_ref());
399 let to_router_address = |raw: Address| {
400 if raw.as_slice() == native_address.as_ref() {
401 router_eth
402 } else {
403 raw
404 }
405 };
406
407 let token_in = to_router_address(bytes_to_address(solution.token_in())?);
408 let token_out = to_router_address(bytes_to_address(solution.token_out())?);
409 let receiver = bytes_to_address(solution.receiver())?;
410
411 let (permit, permit2_sig) = if let Some(p) = encoding_options.permit() {
412 let d = p.details();
413 let permit = Some(PermitSingle {
414 details: SolPermitDetails {
415 token: bytes_to_address(d.token())?,
416 amount: U160::from(biguint_to_u256(d.amount())),
417 expiration: U48::from(biguint_to_u256(d.expiration())),
418 nonce: U48::from(biguint_to_u256(d.nonce())),
419 },
420 spender: bytes_to_address(p.spender())?,
421 sigDeadline: biguint_to_u256(p.sig_deadline()),
422 });
423 let sig = encoding_options
424 .permit2_signature()
425 .ok_or_else(|| {
426 EncodingError::FatalError("Signature must be provided for permit2".to_string())
427 })?
428 .to_vec();
429 (permit, sig)
430 } else {
431 (None, vec![])
432 };
433
434 let client_fee_params = if let Some(fee) = encoding_options.client_fee_params() {
435 let fee_units = fee_rates.client_fee_units(fee.bps());
438 let fee_units = u32::try_from(fee_units).map_err(|_| {
439 EncodingError::FatalError(format!(
440 "client fee ({} bps) scales to {fee_units} fee units, which overflows the \
441 router's uint32 clientFeeBps",
442 fee.bps()
443 ))
444 })?;
445 (
446 fee_units,
447 bytes_to_address(fee.receiver())?,
448 biguint_to_u256(fee.max_contribution()),
449 U256::from(fee.deadline()),
450 {
453 let mut sig = fee.signature().to_vec();
454 sig.resize(65, 0);
455 sig
456 },
457 )
458 } else {
459 (0u32, Address::ZERO, U256::ZERO, U256::MAX, vec![])
460 };
461
462 let fn_sig = encoded_solution.function_signature();
463 let swaps = encoded_solution.swaps();
464 let fee_breakdown = if encoding_options
465 .client_fee_params()
466 .is_some()
467 {
468 fee_breakdown.with_swaps_hash(keccak256(swaps).0)
469 } else {
470 fee_breakdown
471 };
472
473 let method_calldata = if fn_sig.contains("Permit2") {
474 let permit = permit.ok_or(EncodingError::FatalError(
475 "permit2 object must be set to use permit2".to_string(),
476 ))?;
477 if fn_sig.contains("splitSwap") {
478 (
479 amount_in,
480 token_in,
481 token_out,
482 expected_amount_out,
483 min_amount_out,
484 U256::from(encoded_solution.n_tokens()),
485 receiver,
486 client_fee_params,
487 permit,
488 permit2_sig,
489 swaps,
490 )
491 .abi_encode()
492 } else {
493 (
494 amount_in,
495 token_in,
496 token_out,
497 expected_amount_out,
498 min_amount_out,
499 receiver,
500 client_fee_params,
501 permit,
502 permit2_sig,
503 swaps,
504 )
505 .abi_encode()
506 }
507 } else if fn_sig.contains("splitSwap") {
508 (
509 amount_in,
510 token_in,
511 token_out,
512 expected_amount_out,
513 min_amount_out,
514 U256::from(encoded_solution.n_tokens()),
515 receiver,
516 client_fee_params,
517 swaps,
518 )
519 .abi_encode()
520 } else if fn_sig.contains("singleSwap") || fn_sig.contains("sequentialSwap") {
521 (
522 amount_in,
523 token_in,
524 token_out,
525 expected_amount_out,
526 min_amount_out,
527 receiver,
528 client_fee_params,
529 swaps,
530 )
531 .abi_encode()
532 } else {
533 return Err(EncodingError::FatalError(format!(
534 "unsupported function signature for Tycho router: {fn_sig}"
535 )));
536 };
537
538 let mut contract_interaction =
539 Self::encode_input(encoded_solution.function_signature(), method_calldata);
540 if let Some(watermark) = &self.calldata_watermark {
541 contract_interaction.extend_from_slice(watermark);
542 }
543
544 let value =
545 if token_in == router_eth { solution.amount_in().clone() } else { BigUint::ZERO };
546 let mut transaction = Transaction::new(
547 encoded_solution
548 .interacting_with()
549 .clone(),
550 value,
551 contract_interaction,
552 );
553 if encoding_options
554 .client_fee_params()
555 .is_some()
556 {
557 let offset = encoded_solution.client_fee_signature_offset();
558 transaction = transaction.with_client_fee_signature_offset(offset);
559 }
560 Ok((transaction, fee_breakdown))
561 }
562
563 fn check_slippage_guardrail(
569 expected_amount_out: U256,
570 min_amount_out: U256,
571 ) -> Result<(), EncodingError> {
572 let floor = expected_amount_out * U256::from(BPS_DENOMINATOR - MAX_SLIPPAGE_TOLERANCE_BPS) /
573 U256::from(BPS_DENOMINATOR);
574 if min_amount_out > expected_amount_out || min_amount_out < floor {
575 return Err(EncodingError::FatalError(format!(
576 "minimum amount out {min_amount_out} is outside the router's accepted range \
577 [{floor}, {expected_amount_out}] for the quoted output; reduce slippage or the \
578 client fee"
579 )));
580 }
581 Ok(())
582 }
583
584 fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
586 let mut hasher = Keccak256::new();
587 hasher.update(selector.as_bytes());
588 let selector_bytes = &hasher.finalize()[..4];
589 let mut call_data = selector_bytes.to_vec();
590 if encoded_args.len() > 32 &&
594 encoded_args[..32] ==
595 [0u8; 31]
596 .into_iter()
597 .chain([32].to_vec())
598 .collect::<Vec<u8>>()
599 {
600 encoded_args = encoded_args[32..].to_vec();
601 }
602 call_data.extend(encoded_args);
603 call_data
604 }
605
606 fn calculate_fee_breakdown(
617 swap_output: &BigUint,
618 client_fee_bps: u16,
619 slippage: f64,
620 fee_rates: FeeRates,
621 ) -> Result<FeeBreakdown, EncodingError> {
622 let max_fee_units = fee_rates.max_fee_units();
623 let scaled_client_fee = fee_rates.client_fee_units(client_fee_bps);
626 let fee_on_output = fee_rates.on_output() as u64;
627 let fee_on_client_fee = fee_rates.on_client_fee() as u64;
628
629 if scaled_client_fee + fee_on_output > max_fee_units {
630 return Err(EncodingError::FatalError(format!(
631 "client fee ({client_fee_bps} bps) plus router fee on output \
632 ({fee_on_output} fee units) exceed the {max_fee_units} fee-unit cap (100%); \
633 the router would revert"
634 )));
635 }
636 if fee_on_client_fee > max_fee_units {
637 return Err(EncodingError::FatalError(format!(
638 "router fee on client fee ({fee_on_client_fee} fee units) exceeds the \
639 {max_fee_units} fee-unit cap (100%); the router would revert"
640 )));
641 }
642
643 let mut router_fee_on_client = BigUint::ZERO;
644 let mut client_portion = BigUint::ZERO;
645
646 if scaled_client_fee > 0 {
647 let client_fee_numerator = swap_output * scaled_client_fee;
648 let total_client_fee = &client_fee_numerator / max_fee_units;
649
650 router_fee_on_client = client_fee_numerator * fee_on_client_fee /
651 BigUint::from(fee_rates.max_fee_units_squared());
652
653 client_portion = total_client_fee - &router_fee_on_client;
654 }
655
656 let router_fee_on_output = swap_output * fee_on_output / max_fee_units;
657 let total_router_fee = router_fee_on_client + router_fee_on_output;
658
659 let amount_after_fees = swap_output - &client_portion - &total_router_fee;
660
661 let precision = BigUint::from(1_000_000u64);
662 let slippage_amount =
663 &amount_after_fees * BigUint::from((slippage * 1_000_000.0) as u64) / &precision;
664
665 let min_amount_received = &amount_after_fees - &slippage_amount;
666
667 Ok(FeeBreakdown::new(
668 total_router_fee,
669 client_portion,
670 slippage_amount,
671 min_amount_received,
672 ))
673 }
674}
675
676impl From<EncodingError> for SolveError {
677 fn from(err: EncodingError) -> Self {
678 SolveError::FailedEncoding(err.to_string())
679 }
680}
681
682fn has_exclusive_leg(quote: &OrderQuote) -> bool {
685 quote.route().is_some_and(|route| {
686 route
687 .swaps()
688 .iter()
689 .any(|s| s.committed_amount_out().is_some())
690 })
691}
692
693#[cfg(test)]
694mod tests {
695 use alloy::primitives::{Address as EvmAddress, Bytes as EvmBytes};
696 use num_bigint::BigUint;
697 use rstest::rstest;
698 use rustc_hash::FxHashMap;
699 use tycho_execution::encoding::{
700 errors::EncodingError,
701 models::{EncodedSolution, Solution},
702 tycho_encoder::TychoEncoder,
703 };
704 use tycho_simulation::tycho_core::{
705 models::{token::Token, Address, Chain as SimChain},
706 Bytes,
707 };
708
709 use super::*;
710 use crate::{
711 algorithm::test_utils::{component, MockProtocolSim},
712 encoding::router_fees::RouterFees,
713 BlockInfo, OrderQuote, QuoteStatus,
714 };
715
716 fn make_token(addr: Address) -> Token {
717 Token {
718 address: addr,
719 symbol: "T".to_string(),
720 decimals: 18,
721 tax: Default::default(),
722 gas: vec![],
723 chain: SimChain::Ethereum,
724 quality: 100,
725 }
726 }
727
728 fn make_route_swap_addrs(token_in: Address, token_out: Address) -> crate::types::Swap {
729 let tin = make_token(token_in.clone());
730 let tout = make_token(token_out.clone());
731 let component_addr = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
733 crate::types::Swap::new(
734 component_addr.to_string(),
735 "uniswap_v2".to_string(),
736 token_in,
737 token_out,
738 BigUint::from(1000u64),
739 BigUint::from(990u64),
740 BigUint::from(50_000u64),
741 component(component_addr, &[tin, tout]),
742 Box::new(MockProtocolSim::default()),
743 )
744 }
745
746 fn make_route_with_tokens(pairs: &[(Address, Address)]) -> crate::types::Route {
749 let mut tokens = rustc_hash::FxHashMap::default();
750 let swaps = pairs
751 .iter()
752 .map(|(tin, tout)| {
753 tokens
754 .entry(tin.clone())
755 .or_insert_with(|| make_token(tin.clone()));
756 tokens
757 .entry(tout.clone())
758 .or_insert_with(|| make_token(tout.clone()));
759 make_route_swap_addrs(tin.clone(), tout.clone())
760 })
761 .collect();
762 crate::types::Route::new(swaps, tokens).expect("non-empty route")
763 }
764
765 fn make_address(byte: u8) -> Address {
766 Address::from([byte; 20])
767 }
768
769 fn make_order_quote(amount_out: u64) -> OrderQuote {
770 OrderQuote::new(
771 "test-order".to_string(),
772 QuoteStatus::Success,
773 BigUint::from(1000u64),
774 BigUint::from(amount_out),
775 BigUint::from(100_000u64),
776 BigUint::from(amount_out),
777 BlockInfo::new(1, "0x123".to_string(), 1000),
778 "test".to_string(),
779 Bytes::from(make_address(0xAA).as_ref()),
780 Bytes::from(make_address(0xAA).as_ref()),
781 "1".to_string(),
782 )
783 }
784
785 struct MockTychoEncoder;
786
787 impl TychoEncoder for MockTychoEncoder {
788 fn encode_solutions(
789 &self,
790 _solutions: Vec<Solution>,
791 ) -> Result<Vec<EncodedSolution>, EncodingError> {
792 Ok(vec![])
793 }
794
795 fn validate_solution(&self, _solution: &Solution) -> Result<(), EncodingError> {
796 Ok(())
797 }
798 }
799
800 fn mock_encoder(chain: Chain) -> Encoder {
801 let router_fees = SharedRouterFees::default();
802 router_fees.set(RouterFees::new(
803 FEE_SCALE,
804 100_000,
805 20_000_000,
806 rustc_hash::FxHashMap::default(),
807 ));
808 Encoder {
809 tycho_encoder: Some(Box::new(MockTychoEncoder)),
810 chain,
811 router_address: Some(Bytes::from([0u8; 20].as_ref())),
812 router_fees,
813 exclusive_swap_signer: None,
814 calldata_watermark: None,
815 }
816 }
817
818 #[test]
819 fn test_encoder_new_disabled_on_unsupported_chain() {
820 let registry =
824 tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry::new(Chain::Ethereum)
825 .add_default_encoders(None)
826 .expect("registry should build for Ethereum");
827 let encoder = Encoder::new(Chain::Starknet, registry)
828 .expect("new must not fail for a router-less chain");
829 assert!(
830 encoder.router_address().is_none(),
831 "expected disabled encoder, got a router address"
832 );
833 }
834
835 #[tokio::test]
836 async fn disabled_encoder_quotes_but_refuses_to_encode() {
837 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
839 .add_default_encoders(None)
840 .unwrap();
841 let encoder =
842 Encoder::new(Chain::Starknet, registry).expect("new must not fail when disabled");
843 assert!(encoder.router_address().is_none());
844
845 let err = encoder
846 .encode(vec![], EncodingOptions::new(0.01))
847 .await
848 .expect_err("encoding must fail on a router-less chain");
849 assert!(matches!(err, SolveError::EncodingUnavailable(_)));
850 }
851
852 #[test]
853 fn test_try_from_without_route_errors() {
854 let quote = make_order_quote(990);
855
856 let result = Solution::try_from("e);
857
858 assert!(result.is_err());
859 }
860
861 #[test]
862 fn test_try_from_non_success_errors() {
863 let quote = OrderQuote::new(
864 "test-order".to_string(),
865 QuoteStatus::NoRouteFound,
866 BigUint::from(1000u64),
867 BigUint::from(990u64),
868 BigUint::from(100_000u64),
869 BigUint::from(990u64),
870 BlockInfo::new(1, "0x123".to_string(), 1000),
871 "test".to_string(),
872 Bytes::from(make_address(0xAA).as_ref()),
873 Bytes::from(make_address(0xAA).as_ref()),
874 "1".to_string(),
875 );
876
877 let result = Solution::try_from("e);
878
879 assert!(result.is_err());
880 }
881
882 #[test]
883 fn test_try_from_maps_tokens_and_amounts() {
884 let quote = make_order_quote(990)
885 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
886
887 let solution = Solution::try_from("e).unwrap();
888
889 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
890 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x02).as_ref()));
891 assert_eq!(*solution.amount_in(), *quote.amount_in());
892 assert_eq!(*solution.expected_amount_out(), *quote.amount_out());
893 assert_eq!(*solution.min_amount_out(), *quote.amount_out());
895 assert_eq!(solution.swaps().len(), 1);
896 }
897
898 const CONTROLLER_KEY: &str =
899 "0x1111111111111111111111111111111111111111111111111111111111111111";
900
901 fn ekubo_signed_swap(committed: Option<u64>) -> crate::types::Swap {
902 let token_in = make_address(0x11);
903 let token_out = make_address(0x22);
904 let mut comp = component("ekubo-signed-pool", &[]);
905 comp.static_attributes
906 .insert("extension".to_string(), Bytes::from([0x55u8; 20].as_ref()));
907 comp.static_attributes
908 .insert("fee".to_string(), Bytes::from(0u64));
909 comp.static_attributes
910 .insert("pool_type_config".to_string(), Bytes::from(0u32));
911
912 let mut swap = crate::types::Swap::new(
913 "ekubo-signed-pool".to_string(),
914 "ekubo_v3".to_string(),
915 token_in,
916 token_out,
917 BigUint::from(1_000_000u64),
918 BigUint::from(1_000_000u64),
919 BigUint::from(50_000u64),
920 comp,
921 Box::new(MockProtocolSim::default()),
922 );
923 if let Some(committed) = committed {
924 swap.set_committed_amount_out(BigUint::from(committed));
925 }
926 swap
927 }
928
929 fn single_swap_route(swap: crate::types::Swap) -> crate::types::Route {
930 let tokens = FxHashMap::from_iter([
931 (swap.token_in().clone(), make_token(swap.token_in().clone())),
932 (swap.token_out().clone(), make_token(swap.token_out().clone())),
933 ]);
934 crate::types::Route::new(vec![swap], tokens).expect("non-empty route")
935 }
936
937 #[rstest]
938 #[case::exclusive_leg_signed(Some(990_000), true)]
939 #[case::public_leg_untouched(None, false)]
940 fn test_stamp_exclusive_swaps(#[case] committed: Option<u64>, #[case] signed: bool) {
941 let quote =
942 make_order_quote(990_000).with_route(single_swap_route(ekubo_signed_swap(committed)));
943 let signer = ExclusiveSwapSigner::new(CONTROLLER_KEY.parse().unwrap(), 1, 0, 120);
944
945 let solution =
946 Encoder::stamp_exclusive_swaps(Solution::try_from("e).unwrap(), "e, &signer)
947 .unwrap();
948
949 assert_eq!(
950 solution.swaps()[0]
951 .user_data()
952 .is_some(),
953 signed
954 );
955 }
956
957 #[tokio::test]
958 async fn test_encode_rejects_exclusive_leg_without_signer() {
959 let encoder = mock_encoder(Chain::Ethereum);
962 let quote = make_order_quote(990_000)
963 .with_route(single_swap_route(ekubo_signed_swap(Some(990_000))));
964
965 let result = encoder
966 .encode(vec![quote], EncodingOptions::new(0.01))
967 .await;
968
969 assert!(result.is_err(), "expected fail-fast error for unsigned exclusive leg");
970 }
971
972 #[test]
973 fn test_try_from_multi_hop_uses_boundary_swap_tokens() {
974 let quote = make_order_quote(990).with_route(make_route_with_tokens(&[
975 (make_address(0x01), make_address(0x02)),
976 (make_address(0x02), make_address(0x03)),
977 ]));
978
979 let solution = Solution::try_from("e).unwrap();
980
981 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
982 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x03).as_ref()));
983 assert_eq!(solution.swaps().len(), 2);
984 }
985
986 #[tokio::test]
987 async fn test_encode_skips_non_successful_solutions() {
988 let encoder = mock_encoder(Chain::Ethereum);
989 let quote = OrderQuote::new(
990 "test-order".to_string(),
991 QuoteStatus::NoRouteFound,
992 BigUint::from(1000u64),
993 BigUint::from(990u64),
994 BigUint::from(100_000u64),
995 BigUint::from(990u64),
996 BlockInfo::new(1, "0x123".to_string(), 1000),
997 "test".to_string(),
998 Bytes::from(make_address(0xAA).as_ref()),
999 Bytes::from(make_address(0xAA).as_ref()),
1000 "1".to_string(),
1001 );
1002
1003 let encoding_options = EncodingOptions::new(0.01);
1004
1005 let result = encoder
1006 .encode(vec![quote], encoding_options)
1007 .await
1008 .unwrap();
1009
1010 assert!(result[0].transaction().is_none());
1011 }
1012
1013 fn real_encoder() -> Encoder {
1014 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
1015 .add_default_encoders(None)
1016 .unwrap();
1017 let encoder = Encoder::new(Chain::Ethereum, registry).unwrap();
1018 encoder
1020 .router_fees()
1021 .set(RouterFees::new(FEE_SCALE, 100_000, 20_000_000, rustc_hash::FxHashMap::default()));
1022 encoder
1023 }
1024
1025 #[tokio::test]
1026 async fn test_encode_sets_transaction_on_successful_solution() {
1027 let encoder = real_encoder();
1028 let quote = make_order_quote(990)
1029 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1030
1031 let encoding_options = EncodingOptions::new(0.01);
1032
1033 let result = encoder
1034 .encode(vec![quote], encoding_options)
1035 .await
1036 .unwrap();
1037
1038 assert!(result[0].transaction().is_some());
1039 let tx = result[0].transaction().unwrap();
1040 assert!(!tx.data().is_empty());
1041 assert!(tx.data().len() > 4);
1043 }
1044
1045 type SingleSwapCalldata = (
1048 U256,
1049 EvmAddress,
1050 EvmAddress,
1051 U256,
1052 U256,
1053 EvmAddress,
1054 (u32, EvmAddress, U256, U256, EvmBytes),
1055 EvmBytes,
1056 );
1057
1058 #[tokio::test]
1059 async fn test_encode_calldata_amounts_and_client_fee_units() {
1060 let encoder = real_encoder();
1061 let quote = make_order_quote(1_000_000_000)
1062 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1063 let amount_in = quote.amount_in().clone();
1064 let amount_out = quote.amount_out().clone();
1065 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
1066
1067 let result = encoder
1068 .encode(vec![quote], opts)
1069 .await
1070 .unwrap();
1071
1072 let tx = result[0].transaction().unwrap();
1073 let breakdown = result[0].fee_breakdown().unwrap();
1074 let (encoded_amount_in, _, _, expected_amount_out, min_amount_out, _, client_fee, _) =
1075 <SingleSwapCalldata as SolValue>::abi_decode_params(&tx.data()[4..]).unwrap();
1076
1077 assert_eq!(encoded_amount_in, biguint_to_u256(&amount_in));
1078 assert_eq!(expected_amount_out, biguint_to_u256(&amount_out));
1080 assert_eq!(min_amount_out, biguint_to_u256(breakdown.min_amount_received()));
1081 assert!(min_amount_out < expected_amount_out);
1082 assert_eq!(client_fee.0, 1_000_000);
1084 }
1085
1086 #[tokio::test]
1087 async fn test_encode_rejects_slippage_beyond_router_guardrail() {
1088 let encoder = real_encoder();
1089 let quote = make_order_quote(1_000_000_000)
1090 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1091
1092 let err = encoder
1094 .encode(vec![quote], EncodingOptions::new(0.25))
1095 .await
1096 .expect_err("25% slippage must be rejected before it reaches the router");
1097
1098 assert!(
1099 err.to_string()
1100 .contains("outside the router's accepted range"),
1101 "got {err:?}"
1102 );
1103 }
1104
1105 #[tokio::test]
1106 async fn test_encode_with_client_fee_params() {
1107 let encoder = real_encoder();
1108 let quote = make_order_quote(990)
1109 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1110
1111 let fee = crate::ClientFeeParams::new(
1112 100,
1113 Bytes::from(make_address(0xBB).as_ref()),
1114 BigUint::from(0u64),
1115 1_893_456_000u64,
1116 Bytes::from(vec![0xAB; 65]),
1117 );
1118 let encoding_options = EncodingOptions::new(0.01).with_client_fee_params(fee);
1119
1120 let result = encoder
1121 .encode(vec![quote], encoding_options)
1122 .await
1123 .unwrap();
1124
1125 assert!(result[0].transaction().is_some());
1126 let tx = result[0].transaction().unwrap();
1127 assert!(!tx.data().is_empty());
1128 assert!(tx.data().len() > 4);
1130 }
1131
1132 #[tokio::test]
1133 async fn test_encode_without_client_fee_produces_transaction() {
1134 let encoder = real_encoder();
1135 let quote = make_order_quote(990)
1136 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1137
1138 let encoding_options = EncodingOptions::new(0.01);
1139
1140 let result = encoder
1141 .encode(vec![quote], encoding_options)
1142 .await
1143 .unwrap();
1144
1145 assert!(result[0].transaction().is_some());
1146 }
1147
1148 fn make_client_fee(bps: u16) -> crate::ClientFeeParams {
1151 crate::ClientFeeParams::new(
1152 bps,
1153 Bytes::from(make_address(0xBB).as_ref()),
1154 BigUint::from(0u64),
1155 1_893_456_000u64,
1156 Bytes::from(vec![]),
1157 )
1158 }
1159
1160 #[tokio::test]
1161 async fn test_encode_with_client_fee_returns_signature_offset() {
1162 let encoder = real_encoder();
1163 let quote = make_order_quote(990)
1164 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1165 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
1166
1167 let result = encoder
1168 .encode(vec![quote], opts)
1169 .await
1170 .unwrap();
1171
1172 let tx = result[0].transaction().unwrap();
1173 tx.client_fee_signature_offset()
1174 .expect("client_fee_signature_offset must be present with client fee");
1175 }
1176
1177 #[tokio::test]
1178 async fn test_encode_without_client_fee_has_no_signature_offset() {
1179 let encoder = real_encoder();
1180 let quote = make_order_quote(990)
1181 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1182 let opts = EncodingOptions::new(0.01);
1183
1184 let result = encoder
1185 .encode(vec![quote], opts)
1186 .await
1187 .unwrap();
1188
1189 let tx = result[0].transaction().unwrap();
1190 assert!(tx
1191 .client_fee_signature_offset()
1192 .is_none());
1193 }
1194
1195 #[tokio::test]
1196 async fn test_signature_offset_allows_patching() {
1197 let encoder = real_encoder();
1198 let real_sig = vec![0xFF; 65];
1199 let quote = make_order_quote(990)
1200 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1201 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
1202
1203 let result = encoder
1204 .encode(vec![quote], opts)
1205 .await
1206 .unwrap();
1207
1208 let tx = result[0].transaction().unwrap();
1209 let offset = tx
1210 .client_fee_signature_offset()
1211 .unwrap();
1212
1213 let mut calldata = tx.data().to_vec();
1214 calldata[offset..offset + 65].copy_from_slice(&real_sig);
1215 assert_eq!(&calldata[offset..offset + 65], &real_sig[..]);
1216 }
1217
1218 #[tokio::test]
1221 async fn test_encode_appends_calldata_watermark() {
1222 let encoder = real_encoder().with_calldata_watermark("fynd");
1223 let quote = make_order_quote(990)
1224 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1225
1226 let result = encoder
1227 .encode(vec![quote], EncodingOptions::new(0.01))
1228 .await
1229 .unwrap();
1230
1231 let tx = result[0].transaction().unwrap();
1232 assert!(
1233 tx.data().ends_with(b"fynd"),
1234 "calldata must end with the watermark bytes, got suffix {:?}",
1235 &tx.data()[tx.data().len().saturating_sub(4)..]
1236 );
1237 }
1238
1239 #[tokio::test]
1240 async fn test_encode_without_watermark_leaves_calldata_unchanged() {
1241 let make_quote = || {
1242 make_order_quote(990)
1243 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]))
1244 };
1245
1246 let plain = real_encoder()
1247 .encode(vec![make_quote()], EncodingOptions::new(0.01))
1248 .await
1249 .unwrap();
1250 let watermarked = real_encoder()
1251 .with_calldata_watermark("fynd")
1252 .encode(vec![make_quote()], EncodingOptions::new(0.01))
1253 .await
1254 .unwrap();
1255
1256 let plain_data = plain[0].transaction().unwrap().data();
1257 let watermarked_data = watermarked[0]
1258 .transaction()
1259 .unwrap()
1260 .data();
1261 assert_eq!(*plain_data, watermarked_data[..watermarked_data.len() - 4]);
1263 }
1264
1265 #[tokio::test]
1266 async fn test_watermarked_calldata_still_decodes() {
1267 let encoder = real_encoder().with_calldata_watermark("fynd");
1268 let quote = make_order_quote(1_000_000_000)
1269 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1270 let amount_in = quote.amount_in().clone();
1271 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
1272
1273 let result = encoder
1274 .encode(vec![quote], opts)
1275 .await
1276 .unwrap();
1277
1278 let tx = result[0].transaction().unwrap();
1279 let (encoded_amount_in, _, _, _, _, _, _, _) =
1282 <SingleSwapCalldata as SolValue>::abi_decode_params(&tx.data()[4..tx.data().len() - 4])
1283 .unwrap();
1284 assert_eq!(encoded_amount_in, biguint_to_u256(&amount_in));
1285 }
1286
1287 #[tokio::test]
1288 async fn test_signature_offset_unaffected_by_watermark() {
1289 let encoder = real_encoder().with_calldata_watermark("fynd");
1290 let real_sig = vec![0xFF; 65];
1291 let quote = make_order_quote(990)
1292 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1293 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
1294
1295 let result = encoder
1296 .encode(vec![quote], opts)
1297 .await
1298 .unwrap();
1299
1300 let tx = result[0].transaction().unwrap();
1301 let offset = tx
1302 .client_fee_signature_offset()
1303 .unwrap();
1304
1305 let mut calldata = tx.data().to_vec();
1306 calldata[offset..offset + 65].copy_from_slice(&real_sig);
1307 assert_eq!(&calldata[offset..offset + 65], &real_sig[..]);
1308 assert!(calldata.ends_with(b"fynd"));
1309 }
1310
1311 const FEE_SCALE: u64 = 100_000_000;
1315
1316 #[test]
1317 fn test_calculate_fee_breakdown() {
1318 let rates = FeeRates::new(100_000, 20_000_000, FEE_SCALE);
1320
1321 let breakdown =
1322 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 100, 0.0, rates)
1323 .unwrap();
1324
1325 assert_eq!(*breakdown.client_fee(), BigUint::from(8_000u64));
1328 assert_eq!(*breakdown.router_fee(), BigUint::from(3_000u64));
1329 assert_eq!(*breakdown.min_amount_received(), BigUint::from(989_000u64));
1330 }
1331
1332 #[test]
1333 fn test_calculate_fee_breakdown_zero_fees() {
1334 let rates = FeeRates::new(0, 0, FEE_SCALE);
1335
1336 let breakdown =
1337 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 0, 0.0, rates).unwrap();
1338
1339 assert_eq!(*breakdown.client_fee(), BigUint::ZERO);
1340 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
1341 assert_eq!(*breakdown.min_amount_received(), BigUint::from(1_000_000u64));
1342 }
1343
1344 #[test]
1345 fn test_calculate_fee_breakdown_fee_too_high() {
1346 let rates = FeeRates::new(1, 0, FEE_SCALE);
1348
1349 let result =
1350 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 10_000, 0.0, rates);
1351
1352 assert!(result.is_err());
1353 }
1354
1355 #[tokio::test]
1356 async fn test_encode_uses_custom_fees_for_client_fee_receiver() {
1357 let encoder = real_encoder();
1358 let custom =
1360 FxHashMap::from_iter([(Bytes::from(make_address(0xBB).as_ref()), (0u32, 0u32))]);
1361 encoder
1362 .router_fees()
1363 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
1364 let quote = make_order_quote(1_000_000_000)
1365 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1366 let opts = EncodingOptions::new(0.0).with_client_fee_params(make_client_fee(100));
1367
1368 let result = encoder
1369 .encode(vec![quote], opts)
1370 .await
1371 .unwrap();
1372
1373 let breakdown = result[0].fee_breakdown().unwrap();
1374 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
1375 assert_eq!(*breakdown.client_fee(), BigUint::from(10_000_000u64));
1377 }
1378
1379 #[tokio::test]
1380 async fn test_encode_falls_back_to_sender() {
1381 let encoder = real_encoder();
1382 let custom = FxHashMap::from_iter([(
1385 Bytes::from(make_address(0xAA).as_ref()),
1386 (0u32, 20_000_000u32),
1387 )]);
1388 encoder
1389 .router_fees()
1390 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
1391 let quote = make_order_quote(1_000_000_000)
1392 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1393
1394 let result = encoder
1395 .encode(vec![quote], EncodingOptions::new(0.0))
1396 .await
1397 .unwrap();
1398
1399 let breakdown = result[0].fee_breakdown().unwrap();
1400 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
1401 }
1402
1403 #[tokio::test]
1404 async fn test_encode_unknown_client() {
1405 let encoder = real_encoder();
1406 encoder
1407 .router_fees()
1408 .set(RouterFees::new(
1409 FEE_SCALE,
1410 1_000_000,
1411 20_000_000,
1412 rustc_hash::FxHashMap::default(),
1413 ));
1414 let quote = make_order_quote(1_000_000_000)
1415 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
1416
1417 let result = encoder
1418 .encode(vec![quote], EncodingOptions::new(0.0))
1419 .await
1420 .unwrap();
1421
1422 let breakdown = result[0].fee_breakdown().unwrap();
1423 assert_eq!(*breakdown.router_fee(), BigUint::from(10_000_000u64));
1425 }
1426}