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