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::router_fees::{FeeRates, RouterFees, SharedRouterFees},
25 EncodingOptions, FeeBreakdown, OrderQuote, QuoteStatus, SolveError, Transaction,
26};
27
28pub const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
30
31pub struct Encoder {
41 tycho_encoder: Option<Box<dyn TychoEncoder>>,
42 chain: Chain,
43 router_address: Option<Bytes>,
44 router_fees: SharedRouterFees,
45}
46
47impl TryFrom<&OrderQuote> for Solution {
48 type Error = SolveError;
49
50 fn try_from(quote: &OrderQuote) -> Result<Self, Self::Error> {
51 if quote.status() != QuoteStatus::Success {
52 return Err(SolveError::FailedEncoding(format!(
53 "cannot convert quote with status {:?} to Solution",
54 quote.status()
55 )));
56 }
57
58 let route = quote.route().ok_or_else(|| {
59 SolveError::FailedEncoding("successful quote must have a route".to_string())
60 })?;
61
62 let token_in = route
63 .input_token()
64 .ok_or_else(|| SolveError::FailedEncoding("route has no input token".to_string()))?;
65 let token_out = route
66 .output_token()
67 .ok_or_else(|| SolveError::FailedEncoding("route has no output token".to_string()))?;
68
69 let token_map = route.tokens();
70 let lookup_token = |addr: &Bytes| {
71 token_map
72 .get(addr)
73 .cloned()
74 .ok_or_else(|| {
75 SolveError::FailedEncoding(format!(
76 "token {addr:?} not found in route's token map; \
77 algorithm must populate Route::with_tokens for every swap token"
78 ))
79 })
80 };
81 let swaps = route
82 .swaps()
83 .iter()
84 .map(|s| {
85 let token_in = lookup_token(s.token_in())?;
86 let token_out = lookup_token(s.token_out())?;
87 Ok(Swap::new(
88 s.protocol_component().clone(),
89 token_in,
90 token_out,
91 s.gas_estimate().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::<Result<Vec<_>, SolveError>>()?;
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 is_supported(chain: Chain) -> bool {
114 get_router_address(&chain).is_ok()
115 }
116
117 pub fn new(
128 chain: Chain,
129 swap_encoder_registry: SwapEncoderRegistry,
130 ) -> Result<Self, SolveError> {
131 let router_address = get_router_address(&chain).ok().cloned();
132 let tycho_encoder = router_address
133 .is_some()
134 .then(|| {
135 TychoRouterEncoderBuilder::new()
136 .chain(chain)
137 .swap_encoder_registry(swap_encoder_registry)
138 .build()
139 })
140 .transpose()?;
141 Ok(Self { tycho_encoder, chain, router_address, router_fees: SharedRouterFees::default() })
142 }
143
144 pub fn router_address(&self) -> Option<&Bytes> {
147 self.router_address.as_ref()
148 }
149
150 pub fn router_fees(&self) -> SharedRouterFees {
155 self.router_fees.clone()
156 }
157
158 pub async fn encode(
167 &self,
168 mut quotes: Vec<OrderQuote>,
169 encoding_options: EncodingOptions,
170 ) -> Result<Vec<OrderQuote>, SolveError> {
171 let Some(tycho_encoder) = self.tycho_encoder.as_ref() else {
172 return Err(SolveError::EncodingUnavailable(format!(
173 "encoding is unavailable on chain '{}': no Tycho router is deployed. Fynd is \
174 running quote-only; contact ops to deploy the router/executor contracts.",
175 self.chain
176 )));
177 };
178
179 let slippage = encoding_options.slippage();
180 if slippage == 0.0 {
181 tracing::warn!("slippage is 0, transaction will likely revert");
182 } else if slippage > 0.5 {
183 tracing::warn!(slippage, "slippage exceeds 50%, possible misconfiguration");
184 }
185
186 let mut to_encode: Vec<(usize, Solution)> = Vec::new();
187
188 for (i, quote) in quotes.iter().enumerate() {
189 if quote.status() != QuoteStatus::Success {
190 continue;
191 }
192
193 to_encode.push((
194 i,
195 Solution::try_from(quote)?
196 .with_user_transfer_type(encoding_options.transfer_type().clone()),
197 ));
198 }
199
200 let solutions: Vec<Solution> = to_encode
201 .iter()
202 .map(|(_, s)| s.clone())
203 .collect();
204 let encoded_solutions = tycho_encoder.encode_solutions(solutions)?;
205
206 let router_fees = self.router_fees.snapshot();
207 for (encoded_solution, (idx, solution)) in encoded_solutions
208 .into_iter()
209 .zip(to_encode)
210 {
211 quotes[idx].set_gas_estimate(encoded_solution.estimated_gas().clone());
212 let (transaction, fee_breakdown) = self.encode_tycho_router_call(
213 encoded_solution,
214 &solution,
215 &encoding_options,
216 &router_fees,
217 )?;
218 quotes[idx].set_transaction(transaction);
219 quotes[idx].set_fee_breakdown(fee_breakdown);
220 }
221
222 Ok(quotes)
223 }
224
225 fn encode_tycho_router_call(
234 &self,
235 encoded_solution: EncodedSolution,
236 solution: &Solution,
237 encoding_options: &EncodingOptions,
238 router_fees: &RouterFees,
239 ) -> Result<(Transaction, FeeBreakdown), EncodingError> {
240 let amount_in = biguint_to_u256(solution.amount_in());
241 let swap_output = solution.min_amount_out();
242 let fee_client = encoding_options
246 .client_fee_params()
247 .map_or_else(|| solution.sender(), |f| f.receiver());
248 let fee_breakdown = Self::calculate_fee_breakdown(
249 swap_output,
250 encoding_options
251 .client_fee_params()
252 .map_or(0, |f| f.bps()),
253 encoding_options.slippage(),
254 router_fees.fees_for(fee_client),
255 )?;
256 let min_amount_out = biguint_to_u256(fee_breakdown.min_amount_received());
257 let native_address = &self.chain.native_token().address;
258 let router_eth = Address::from_slice(ROUTER_ETH_ADDRESS.as_ref());
259 let to_router_address = |raw: Address| {
260 if raw.as_slice() == native_address.as_ref() {
261 router_eth
262 } else {
263 raw
264 }
265 };
266
267 let token_in = to_router_address(bytes_to_address(solution.token_in())?);
268 let token_out = to_router_address(bytes_to_address(solution.token_out())?);
269 let receiver = bytes_to_address(solution.receiver())?;
270
271 let (permit, permit2_sig) = if let Some(p) = encoding_options.permit() {
272 let d = p.details();
273 let permit = Some(PermitSingle {
274 details: SolPermitDetails {
275 token: bytes_to_address(d.token())?,
276 amount: U160::from(biguint_to_u256(d.amount())),
277 expiration: U48::from(biguint_to_u256(d.expiration())),
278 nonce: U48::from(biguint_to_u256(d.nonce())),
279 },
280 spender: bytes_to_address(p.spender())?,
281 sigDeadline: biguint_to_u256(p.sig_deadline()),
282 });
283 let sig = encoding_options
284 .permit2_signature()
285 .ok_or_else(|| {
286 EncodingError::FatalError("Signature must be provided for permit2".to_string())
287 })?
288 .to_vec();
289 (permit, sig)
290 } else {
291 (None, vec![])
292 };
293
294 let client_fee_params = if let Some(fee) = encoding_options.client_fee_params() {
295 (
296 fee.bps(),
297 bytes_to_address(fee.receiver())?,
298 biguint_to_u256(fee.max_contribution()),
299 U256::from(fee.deadline()),
300 {
303 let mut sig = fee.signature().to_vec();
304 sig.resize(65, 0);
305 sig
306 },
307 )
308 } else {
309 (0u16, Address::ZERO, U256::ZERO, U256::MAX, vec![])
310 };
311
312 let fn_sig = encoded_solution.function_signature();
313 let swaps = encoded_solution.swaps();
314 let fee_breakdown = if encoding_options
315 .client_fee_params()
316 .is_some()
317 {
318 fee_breakdown.with_swaps_hash(keccak256(swaps).0)
319 } else {
320 fee_breakdown
321 };
322
323 let method_calldata = if fn_sig.contains("Permit2") {
324 let permit = permit.ok_or(EncodingError::FatalError(
325 "permit2 object must be set to use permit2".to_string(),
326 ))?;
327 if fn_sig.contains("splitSwap") {
328 (
329 amount_in,
330 token_in,
331 token_out,
332 min_amount_out,
333 U256::from(encoded_solution.n_tokens()),
334 receiver,
335 client_fee_params,
336 permit,
337 permit2_sig,
338 swaps,
339 )
340 .abi_encode()
341 } else {
342 (
343 amount_in,
344 token_in,
345 token_out,
346 min_amount_out,
347 receiver,
348 client_fee_params,
349 permit,
350 permit2_sig,
351 swaps,
352 )
353 .abi_encode()
354 }
355 } else if fn_sig.contains("splitSwap") {
356 (
357 amount_in,
358 token_in,
359 token_out,
360 min_amount_out,
361 U256::from(encoded_solution.n_tokens()),
362 receiver,
363 client_fee_params,
364 swaps,
365 )
366 .abi_encode()
367 } else if fn_sig.contains("singleSwap") || fn_sig.contains("sequentialSwap") {
368 (amount_in, token_in, token_out, min_amount_out, receiver, client_fee_params, swaps)
369 .abi_encode()
370 } else {
371 return Err(EncodingError::FatalError(format!(
372 "unsupported function signature for Tycho router: {fn_sig}"
373 )));
374 };
375
376 let contract_interaction =
377 Self::encode_input(encoded_solution.function_signature(), method_calldata);
378
379 let value =
380 if token_in == router_eth { solution.amount_in().clone() } else { BigUint::ZERO };
381 let mut transaction = Transaction::new(
382 encoded_solution
383 .interacting_with()
384 .clone(),
385 value,
386 contract_interaction,
387 );
388 if encoding_options
389 .client_fee_params()
390 .is_some()
391 {
392 let offset = encoded_solution.client_fee_signature_offset();
393 transaction = transaction.with_client_fee_signature_offset(offset);
394 }
395 Ok((transaction, fee_breakdown))
396 }
397
398 fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
400 let mut hasher = Keccak256::new();
401 hasher.update(selector.as_bytes());
402 let selector_bytes = &hasher.finalize()[..4];
403 let mut call_data = selector_bytes.to_vec();
404 if encoded_args.len() > 32 &&
408 encoded_args[..32] ==
409 [0u8; 31]
410 .into_iter()
411 .chain([32].to_vec())
412 .collect::<Vec<u8>>()
413 {
414 encoded_args = encoded_args[32..].to_vec();
415 }
416 call_data.extend(encoded_args);
417 call_data
418 }
419
420 fn calculate_fee_breakdown(
431 swap_output: &BigUint,
432 client_fee_bps: u16,
433 slippage: f64,
434 fee_rates: FeeRates,
435 ) -> Result<FeeBreakdown, EncodingError> {
436 let max_fee_units = fee_rates.max_fee_units();
437 let scaled_client_fee = client_fee_bps as u64 * fee_rates.fee_units_per_bps();
440 let fee_on_output = fee_rates.on_output() as u64;
441 let fee_on_client_fee = fee_rates.on_client_fee() as u64;
442
443 if scaled_client_fee + fee_on_output > max_fee_units {
444 return Err(EncodingError::FatalError(format!(
445 "client fee ({client_fee_bps} bps) plus router fee on output \
446 ({fee_on_output} fee units) exceed the {max_fee_units} fee-unit cap (100%); \
447 the router would revert"
448 )));
449 }
450 if fee_on_client_fee > max_fee_units {
451 return Err(EncodingError::FatalError(format!(
452 "router fee on client fee ({fee_on_client_fee} fee units) exceeds the \
453 {max_fee_units} fee-unit cap (100%); the router would revert"
454 )));
455 }
456
457 let mut router_fee_on_client = BigUint::ZERO;
458 let mut client_portion = BigUint::ZERO;
459
460 if scaled_client_fee > 0 {
461 let client_fee_numerator = swap_output * scaled_client_fee;
462 let total_client_fee = &client_fee_numerator / max_fee_units;
463
464 router_fee_on_client = client_fee_numerator * fee_on_client_fee /
465 BigUint::from(fee_rates.max_fee_units_squared());
466
467 client_portion = total_client_fee - &router_fee_on_client;
468 }
469
470 let router_fee_on_output = swap_output * fee_on_output / max_fee_units;
471 let total_router_fee = router_fee_on_client + router_fee_on_output;
472
473 let amount_after_fees = swap_output - &client_portion - &total_router_fee;
474
475 let precision = BigUint::from(1_000_000u64);
476 let slippage_amount =
477 &amount_after_fees * BigUint::from((slippage * 1_000_000.0) as u64) / &precision;
478
479 let min_amount_received = &amount_after_fees - &slippage_amount;
480
481 Ok(FeeBreakdown::new(
482 total_router_fee,
483 client_portion,
484 slippage_amount,
485 min_amount_received,
486 ))
487 }
488}
489
490impl From<EncodingError> for SolveError {
491 fn from(err: EncodingError) -> Self {
492 SolveError::FailedEncoding(err.to_string())
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use std::collections::HashMap;
499
500 use num_bigint::BigUint;
501 use tycho_execution::encoding::{
502 errors::EncodingError,
503 models::{EncodedSolution, Solution},
504 tycho_encoder::TychoEncoder,
505 };
506 use tycho_simulation::tycho_core::{
507 models::{token::Token, Address, Chain as SimChain},
508 Bytes,
509 };
510
511 use super::*;
512 use crate::{
513 algorithm::test_utils::{component, MockProtocolSim},
514 BlockInfo, OrderQuote, QuoteStatus,
515 };
516
517 fn make_token(addr: Address) -> Token {
518 Token {
519 address: addr,
520 symbol: "T".to_string(),
521 decimals: 18,
522 tax: Default::default(),
523 gas: vec![],
524 chain: SimChain::Ethereum,
525 quality: 100,
526 }
527 }
528
529 fn make_route_swap_addrs(token_in: Address, token_out: Address) -> crate::types::Swap {
530 let tin = make_token(token_in.clone());
531 let tout = make_token(token_out.clone());
532 let pool_addr = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
534 crate::types::Swap::new(
535 pool_addr.to_string(),
536 "uniswap_v2".to_string(),
537 token_in,
538 token_out,
539 BigUint::from(1000u64),
540 BigUint::from(990u64),
541 BigUint::from(50_000u64),
542 component(pool_addr, &[tin, tout]),
543 Box::new(MockProtocolSim::default()),
544 )
545 }
546
547 fn make_route_with_tokens(pairs: &[(Address, Address)]) -> crate::types::Route {
550 let mut tokens = HashMap::new();
551 let swaps = pairs
552 .iter()
553 .map(|(tin, tout)| {
554 tokens
555 .entry(tin.clone())
556 .or_insert_with(|| make_token(tin.clone()));
557 tokens
558 .entry(tout.clone())
559 .or_insert_with(|| make_token(tout.clone()));
560 make_route_swap_addrs(tin.clone(), tout.clone())
561 })
562 .collect();
563 crate::types::Route::new(swaps, tokens).expect("non-empty route")
564 }
565
566 fn make_address(byte: u8) -> Address {
567 Address::from([byte; 20])
568 }
569
570 fn make_order_quote(amount_out: u64) -> OrderQuote {
571 OrderQuote::new(
572 "test-order".to_string(),
573 QuoteStatus::Success,
574 BigUint::from(1000u64),
575 BigUint::from(amount_out),
576 BigUint::from(100_000u64),
577 BigUint::from(amount_out),
578 BlockInfo::new(1, "0x123".to_string(), 1000),
579 "test".to_string(),
580 Bytes::from(make_address(0xAA).as_ref()),
581 Bytes::from(make_address(0xAA).as_ref()),
582 "1".to_string(),
583 )
584 }
585
586 struct MockTychoEncoder;
587
588 impl TychoEncoder for MockTychoEncoder {
589 fn encode_solutions(
590 &self,
591 _solutions: Vec<Solution>,
592 ) -> Result<Vec<EncodedSolution>, EncodingError> {
593 Ok(vec![])
594 }
595
596 fn validate_solution(&self, _solution: &Solution) -> Result<(), EncodingError> {
597 Ok(())
598 }
599 }
600
601 fn mock_encoder(chain: Chain) -> Encoder {
602 let router_fees = SharedRouterFees::default();
603 router_fees.set(RouterFees::new(FEE_SCALE, 100_000, 20_000_000, HashMap::new()));
604 Encoder {
605 tycho_encoder: Some(Box::new(MockTychoEncoder)),
606 chain,
607 router_address: Some(Bytes::from([0u8; 20].as_ref())),
608 router_fees,
609 }
610 }
611
612 #[test]
613 fn test_encoder_new_disabled_on_unsupported_chain() {
614 let registry =
618 tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry::new(Chain::Ethereum)
619 .add_default_encoders(None)
620 .expect("registry should build for Ethereum");
621 let encoder = Encoder::new(Chain::Starknet, registry)
622 .expect("new must not fail for a router-less chain");
623 assert!(
624 encoder.router_address().is_none(),
625 "expected disabled encoder, got a router address"
626 );
627 }
628
629 #[tokio::test]
630 async fn disabled_encoder_quotes_but_refuses_to_encode() {
631 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
633 .add_default_encoders(None)
634 .unwrap();
635 let encoder =
636 Encoder::new(Chain::Starknet, registry).expect("new must not fail when disabled");
637 assert!(encoder.router_address().is_none());
638
639 let err = encoder
640 .encode(vec![], EncodingOptions::new(0.01))
641 .await
642 .expect_err("encoding must fail on a router-less chain");
643 assert!(matches!(err, SolveError::EncodingUnavailable(_)));
644 }
645
646 #[test]
647 fn test_try_from_without_route_errors() {
648 let quote = make_order_quote(990);
649
650 let result = Solution::try_from("e);
651
652 assert!(result.is_err());
653 }
654
655 #[test]
656 fn test_try_from_non_success_errors() {
657 let quote = OrderQuote::new(
658 "test-order".to_string(),
659 QuoteStatus::NoRouteFound,
660 BigUint::from(1000u64),
661 BigUint::from(990u64),
662 BigUint::from(100_000u64),
663 BigUint::from(990u64),
664 BlockInfo::new(1, "0x123".to_string(), 1000),
665 "test".to_string(),
666 Bytes::from(make_address(0xAA).as_ref()),
667 Bytes::from(make_address(0xAA).as_ref()),
668 "1".to_string(),
669 );
670
671 let result = Solution::try_from("e);
672
673 assert!(result.is_err());
674 }
675
676 #[test]
677 fn test_try_from_maps_tokens_and_amounts() {
678 let quote = make_order_quote(990)
679 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
680
681 let solution = Solution::try_from("e).unwrap();
682
683 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
684 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x02).as_ref()));
685 assert_eq!(*solution.amount_in(), *quote.amount_in());
686 assert_eq!(*solution.min_amount_out(), *quote.amount_out());
687 assert_eq!(solution.swaps().len(), 1);
688 }
689
690 #[test]
691 fn test_try_from_multi_hop_uses_boundary_swap_tokens() {
692 let quote = make_order_quote(990).with_route(make_route_with_tokens(&[
693 (make_address(0x01), make_address(0x02)),
694 (make_address(0x02), make_address(0x03)),
695 ]));
696
697 let solution = Solution::try_from("e).unwrap();
698
699 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
700 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x03).as_ref()));
701 assert_eq!(solution.swaps().len(), 2);
702 }
703
704 #[tokio::test]
705 async fn test_encode_skips_non_successful_solutions() {
706 let encoder = mock_encoder(Chain::Ethereum);
707 let quote = OrderQuote::new(
708 "test-order".to_string(),
709 QuoteStatus::NoRouteFound,
710 BigUint::from(1000u64),
711 BigUint::from(990u64),
712 BigUint::from(100_000u64),
713 BigUint::from(990u64),
714 BlockInfo::new(1, "0x123".to_string(), 1000),
715 "test".to_string(),
716 Bytes::from(make_address(0xAA).as_ref()),
717 Bytes::from(make_address(0xAA).as_ref()),
718 "1".to_string(),
719 );
720
721 let encoding_options = EncodingOptions::new(0.01);
722
723 let result = encoder
724 .encode(vec![quote], encoding_options)
725 .await
726 .unwrap();
727
728 assert!(result[0].transaction().is_none());
729 }
730
731 fn real_encoder() -> Encoder {
732 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
733 .add_default_encoders(None)
734 .unwrap();
735 let encoder = Encoder::new(Chain::Ethereum, registry).unwrap();
736 encoder
738 .router_fees()
739 .set(RouterFees::new(FEE_SCALE, 100_000, 20_000_000, HashMap::new()));
740 encoder
741 }
742
743 #[tokio::test]
744 async fn test_encode_sets_transaction_on_successful_solution() {
745 let encoder = real_encoder();
746 let quote = make_order_quote(990)
747 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
748
749 let encoding_options = EncodingOptions::new(0.01);
750
751 let result = encoder
752 .encode(vec![quote], encoding_options)
753 .await
754 .unwrap();
755
756 assert!(result[0].transaction().is_some());
757 let tx = result[0].transaction().unwrap();
758 assert!(!tx.data().is_empty());
759 assert!(tx.data().len() > 4);
761 }
762
763 #[tokio::test]
764 async fn test_encode_with_client_fee_params() {
765 let encoder = real_encoder();
766 let quote = make_order_quote(990)
767 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
768
769 let fee = crate::ClientFeeParams::new(
770 100,
771 Bytes::from(make_address(0xBB).as_ref()),
772 BigUint::from(0u64),
773 1_893_456_000u64,
774 Bytes::from(vec![0xAB; 65]),
775 );
776 let encoding_options = EncodingOptions::new(0.01).with_client_fee_params(fee);
777
778 let result = encoder
779 .encode(vec![quote], encoding_options)
780 .await
781 .unwrap();
782
783 assert!(result[0].transaction().is_some());
784 let tx = result[0].transaction().unwrap();
785 assert!(!tx.data().is_empty());
786 assert!(tx.data().len() > 4);
788 }
789
790 #[tokio::test]
791 async fn test_encode_without_client_fee_produces_transaction() {
792 let encoder = real_encoder();
793 let quote = make_order_quote(990)
794 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
795
796 let encoding_options = EncodingOptions::new(0.01);
797
798 let result = encoder
799 .encode(vec![quote], encoding_options)
800 .await
801 .unwrap();
802
803 assert!(result[0].transaction().is_some());
804 }
805
806 fn make_client_fee(bps: u16) -> crate::ClientFeeParams {
809 crate::ClientFeeParams::new(
810 bps,
811 Bytes::from(make_address(0xBB).as_ref()),
812 BigUint::from(0u64),
813 1_893_456_000u64,
814 Bytes::from(vec![]),
815 )
816 }
817
818 #[tokio::test]
819 async fn test_encode_with_client_fee_returns_signature_offset() {
820 let encoder = real_encoder();
821 let quote = make_order_quote(990)
822 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
823 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
824
825 let result = encoder
826 .encode(vec![quote], opts)
827 .await
828 .unwrap();
829
830 let tx = result[0].transaction().unwrap();
831 tx.client_fee_signature_offset()
832 .expect("client_fee_signature_offset must be present with client fee");
833 }
834
835 #[tokio::test]
836 async fn test_encode_without_client_fee_has_no_signature_offset() {
837 let encoder = real_encoder();
838 let quote = make_order_quote(990)
839 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
840 let opts = EncodingOptions::new(0.01);
841
842 let result = encoder
843 .encode(vec![quote], opts)
844 .await
845 .unwrap();
846
847 let tx = result[0].transaction().unwrap();
848 assert!(tx
849 .client_fee_signature_offset()
850 .is_none());
851 }
852
853 #[tokio::test]
854 async fn test_signature_offset_allows_patching() {
855 let encoder = real_encoder();
856 let real_sig = vec![0xFF; 65];
857 let quote = make_order_quote(990)
858 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
859 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
860
861 let result = encoder
862 .encode(vec![quote], opts)
863 .await
864 .unwrap();
865
866 let tx = result[0].transaction().unwrap();
867 let offset = tx
868 .client_fee_signature_offset()
869 .unwrap();
870
871 let mut calldata = tx.data().to_vec();
872 calldata[offset..offset + 65].copy_from_slice(&real_sig);
873 assert_eq!(&calldata[offset..offset + 65], &real_sig[..]);
874 }
875
876 const FEE_SCALE: u64 = 100_000_000;
880
881 #[test]
882 fn test_calculate_fee_breakdown() {
883 let rates = FeeRates::new(100_000, 20_000_000, FEE_SCALE);
885
886 let breakdown =
887 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 100, 0.0, rates)
888 .unwrap();
889
890 assert_eq!(*breakdown.client_fee(), BigUint::from(8_000u64));
893 assert_eq!(*breakdown.router_fee(), BigUint::from(3_000u64));
894 assert_eq!(*breakdown.min_amount_received(), BigUint::from(989_000u64));
895 }
896
897 #[test]
898 fn test_calculate_fee_breakdown_zero_fees() {
899 let rates = FeeRates::new(0, 0, FEE_SCALE);
900
901 let breakdown =
902 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 0, 0.0, rates).unwrap();
903
904 assert_eq!(*breakdown.client_fee(), BigUint::ZERO);
905 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
906 assert_eq!(*breakdown.min_amount_received(), BigUint::from(1_000_000u64));
907 }
908
909 #[test]
910 fn test_calculate_fee_breakdown_fee_too_high() {
911 let rates = FeeRates::new(1, 0, FEE_SCALE);
913
914 let result =
915 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 10_000, 0.0, rates);
916
917 assert!(result.is_err());
918 }
919
920 #[tokio::test]
921 async fn test_encode_uses_custom_fees_for_client_fee_receiver() {
922 let encoder = real_encoder();
923 let custom = HashMap::from([(Bytes::from(make_address(0xBB).as_ref()), (0u32, 0u32))]);
925 encoder
926 .router_fees()
927 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
928 let quote = make_order_quote(1_000_000_000)
929 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
930 let opts = EncodingOptions::new(0.0).with_client_fee_params(make_client_fee(100));
931
932 let result = encoder
933 .encode(vec![quote], opts)
934 .await
935 .unwrap();
936
937 let breakdown = result[0].fee_breakdown().unwrap();
938 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
939 assert_eq!(*breakdown.client_fee(), BigUint::from(10_000_000u64));
941 }
942
943 #[tokio::test]
944 async fn test_encode_falls_back_to_sender() {
945 let encoder = real_encoder();
946 let custom =
949 HashMap::from([(Bytes::from(make_address(0xAA).as_ref()), (0u32, 20_000_000u32))]);
950 encoder
951 .router_fees()
952 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
953 let quote = make_order_quote(1_000_000_000)
954 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
955
956 let result = encoder
957 .encode(vec![quote], EncodingOptions::new(0.0))
958 .await
959 .unwrap();
960
961 let breakdown = result[0].fee_breakdown().unwrap();
962 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
963 }
964
965 #[tokio::test]
966 async fn test_encode_unknown_client() {
967 let encoder = real_encoder();
968 encoder
969 .router_fees()
970 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, HashMap::new()));
971 let quote = make_order_quote(1_000_000_000)
972 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
973
974 let result = encoder
975 .encode(vec![quote], EncodingOptions::new(0.0))
976 .await
977 .unwrap();
978
979 let breakdown = result[0].fee_breakdown().unwrap();
980 assert_eq!(*breakdown.router_fee(), BigUint::from(10_000_000u64));
982 }
983}