Skip to main content

tycho_execution/encoding/
models.rs

1use std::sync::Arc;
2
3#[cfg(feature = "evm")]
4use alloy::primitives::{Address, U256};
5use clap::ValueEnum;
6use num_bigint::BigUint;
7use serde::{Deserialize, Serialize};
8use tycho_common::{
9    models::{protocol::ProtocolComponent, token::Token},
10    simulation::protocol_sim::ProtocolSim,
11    Bytes,
12};
13
14use crate::encoding::serde_primitives::biguint_string;
15
16/// Specifies the method for transferring user funds into Tycho execution.
17///
18/// Options:
19///
20/// - `TransferFromPermit2`: Use Permit2 for token transfer.
21///     - You must manually approve the Permit2 contract and sign the permit object externally
22///       (outside `tycho-execution`).
23///
24/// - `TransferFrom`: Use standard ERC-20 approval and `transferFrom`.
25///     - You must approve the Tycho Router contract to spend your tokens via standard `approve()`
26///       calls.
27///
28/// - `UseVaultsFunds`: No transfer will be performed and the Vault's funds will be used
29///     - Assumes the tokens are already present in the Tycho Router.
30///     - The tokens must be deposited into the TychoRouterV3 before performing the swap
31#[derive(Clone, Debug, PartialEq, ValueEnum, Serialize, Deserialize, Default)]
32pub enum UserTransferType {
33    TransferFromPermit2,
34    #[default]
35    TransferFrom,
36    UseVaultsFunds,
37}
38
39/// Client fee parameters passed to the router, matching the Solidity `ClientFeeParams` struct.
40///
41/// The default value (all zeros) represents no fee. Clients are responsible for constructing
42/// and signing this struct; `tycho-execution` does not use it internally.
43#[derive(Clone, Debug, Default, Deserialize, Serialize)]
44pub struct ClientFeeParams {
45    /// Fee in fee units charged by the client (0–100_000_000, where 100_000_000 = 100%).
46    client_fee_bps: u32,
47    /// Address that identifies the client and receives any client fee.
48    client_fee_receiver: Bytes,
49    /// Maximum amount the client will contribute from their vault if the output falls below the
50    /// router's `minAmountOut` argument.
51    #[serde(with = "biguint_string")]
52    max_client_contribution: BigUint,
53    /// Deadline for the fee signature as a unix timestamp.
54    #[serde(with = "biguint_string")]
55    deadline: BigUint,
56    /// EIP-712 signature over the fee parameters and swap intent.
57    client_signature: Bytes,
58}
59
60impl ClientFeeParams {
61    /// Creates params that identify the client and charge a fee in basis points.
62    pub fn new(
63        client_fee_receiver: Bytes,
64        client_signature: Bytes,
65        deadline: BigUint,
66        client_fee_bps: u32,
67    ) -> Self {
68        Self {
69            client_fee_bps,
70            client_fee_receiver,
71            max_client_contribution: BigUint::ZERO,
72            deadline,
73            client_signature,
74        }
75    }
76
77    /// Creates params that identify the client for router fee discounts without charging a fee.
78    pub fn new_without_fee(
79        client_fee_receiver: Bytes,
80        client_signature: Bytes,
81        deadline: BigUint,
82    ) -> Self {
83        Self {
84            client_fee_bps: 0,
85            client_fee_receiver,
86            max_client_contribution: BigUint::ZERO,
87            deadline,
88            client_signature,
89        }
90    }
91
92    pub fn with_max_client_contribution(mut self, max_client_contribution: BigUint) -> Self {
93        self.max_client_contribution = max_client_contribution;
94        self
95    }
96}
97
98#[cfg(feature = "evm")]
99impl ClientFeeParams {
100    /// Converts into the ABI-encodable tuple matching the Solidity `ClientFeeParams` struct.
101    pub fn into_abi_params(self) -> (u32, Address, U256, U256, Vec<u8>) {
102        let receiver = if self.client_fee_receiver.is_empty() {
103            Address::ZERO
104        } else {
105            Address::from_slice(&self.client_fee_receiver)
106        };
107        (
108            self.client_fee_bps,
109            receiver,
110            U256::from_be_slice(
111                &self
112                    .max_client_contribution
113                    .to_bytes_be(),
114            ),
115            U256::from_be_slice(&self.deadline.to_bytes_be()),
116            self.client_signature.to_vec(),
117        )
118    }
119}
120
121/// Represents a solution containing details describing an order, and instructions for filling
122/// the order.
123#[derive(Clone, Debug, Deserialize, Serialize)]
124pub struct Solution {
125    /// Address of the sender.
126    sender: Bytes,
127    /// Address of the receiver.
128    receiver: Bytes,
129    /// The token being sold
130    token_in: Bytes,
131    /// Amount of the token in.
132    #[serde(with = "biguint_string")]
133    amount_in: BigUint,
134    /// The token being bought
135    token_out: Bytes,
136    /// Quoted output amount from simulation. Passed to the router as `expectedAmountOut`,
137    /// the baseline for positive slippage detection.
138    #[serde(with = "biguint_string")]
139    expected_amount_out: BigUint,
140    /// Smallest output the swap may return. Passed to the router as `minAmountOut`, the revert
141    /// guardrail. `TychoRouter` rejects a value of zero or above `expected_amount_out`.
142    #[serde(with = "biguint_string")]
143    min_amount_out: BigUint,
144    /// List of swaps to fulfill the solution.
145    swaps: Vec<Swap>,
146    /// The transfer type to be used in this swap for user's funds (token in)
147    user_transfer_type: UserTransferType,
148}
149
150impl Solution {
151    #[allow(clippy::too_many_arguments)]
152    pub fn new(
153        sender: Bytes,
154        receiver: Bytes,
155        token_in: Bytes,
156        token_out: Bytes,
157        amount_in: BigUint,
158        expected_amount_out: BigUint,
159        min_amount_out: BigUint,
160        swaps: Vec<Swap>,
161    ) -> Self {
162        Self {
163            sender,
164            receiver,
165            token_in,
166            token_out,
167            amount_in,
168            expected_amount_out,
169            min_amount_out,
170            swaps,
171            user_transfer_type: UserTransferType::TransferFrom,
172        }
173    }
174    pub fn sender(&self) -> &Bytes {
175        &self.sender
176    }
177    pub fn receiver(&self) -> &Bytes {
178        &self.receiver
179    }
180
181    pub fn token_in(&self) -> &Bytes {
182        &self.token_in
183    }
184
185    pub fn amount_in(&self) -> &BigUint {
186        &self.amount_in
187    }
188
189    pub fn token_out(&self) -> &Bytes {
190        &self.token_out
191    }
192
193    pub fn expected_amount_out(&self) -> &BigUint {
194        &self.expected_amount_out
195    }
196
197    pub fn min_amount_out(&self) -> &BigUint {
198        &self.min_amount_out
199    }
200
201    pub fn swaps(&self) -> &[Swap] {
202        &self.swaps
203    }
204
205    pub fn user_transfer_type(&self) -> &UserTransferType {
206        &self.user_transfer_type
207    }
208
209    pub fn with_swaps(mut self, swaps: Vec<Swap>) -> Self {
210        self.swaps = swaps;
211        self
212    }
213
214    pub fn with_user_transfer_type(mut self, user_transfer_type: UserTransferType) -> Self {
215        self.user_transfer_type = user_transfer_type;
216        self
217    }
218}
219
220/// Represents a swap operation to be performed on a pool.
221#[derive(Clone, Debug, Deserialize, Serialize)]
222pub struct Swap {
223    /// Protocol component from tycho indexer
224    component: ProtocolComponent,
225    /// Token being input into the pool.
226    token_in: Token,
227    /// Token being output from the pool.
228    token_out: Token,
229    /// Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)
230    #[serde(default)]
231    split: f64,
232    /// Optional user data to be passed to encoding.
233    user_data: Option<Bytes>,
234    /// Optional protocol state used to perform the swap.
235    #[serde(skip)]
236    protocol_state: Option<Arc<dyn ProtocolSim>>,
237    /// Optional estimated amount in for this Swap. This is necessary for RFQ protocols. This value
238    /// is used to request the quote
239    estimated_amount_in: Option<BigUint>,
240    /// Estimated gas usage for this swap by simulation
241    estimated_gas: BigUint,
242}
243
244impl Swap {
245    pub fn new<T: Into<ProtocolComponent>>(
246        component: T,
247        token_in: Token,
248        token_out: Token,
249        estimated_gas: BigUint,
250    ) -> Self {
251        Self {
252            component: component.into(),
253            token_in,
254            token_out,
255            split: 0.0,
256            user_data: None,
257            protocol_state: None,
258            estimated_amount_in: None,
259            estimated_gas,
260        }
261    }
262
263    /// Sets the split value (percentage of the amount to be swapped)
264    pub fn with_split(mut self, split: f64) -> Self {
265        self.split = split;
266        self
267    }
268
269    /// Sets the user data to be passed to encoding
270    pub fn with_user_data(mut self, user_data: Bytes) -> Self {
271        self.user_data = Some(user_data);
272        self
273    }
274
275    /// Sets the protocol state used to perform the swap
276    pub fn with_protocol_state(mut self, protocol_state: Arc<dyn ProtocolSim>) -> Self {
277        self.protocol_state = Some(protocol_state);
278        self
279    }
280
281    /// Sets the estimated amount in for RFQ protocols
282    pub fn with_estimated_amount_in(mut self, estimated_amount_in: BigUint) -> Self {
283        self.estimated_amount_in = Some(estimated_amount_in);
284        self
285    }
286
287    pub fn component(&self) -> &ProtocolComponent {
288        &self.component
289    }
290
291    pub fn token_in(&self) -> &Token {
292        &self.token_in
293    }
294
295    pub fn token_out(&self) -> &Token {
296        &self.token_out
297    }
298
299    pub fn split(&self) -> f64 {
300        self.split
301    }
302
303    pub fn user_data(&self) -> &Option<Bytes> {
304        &self.user_data
305    }
306
307    pub fn protocol_state(&self) -> &Option<Arc<dyn ProtocolSim>> {
308        &self.protocol_state
309    }
310
311    pub fn estimated_amount_in(&self) -> &Option<BigUint> {
312        &self.estimated_amount_in
313    }
314
315    pub fn estimated_gas(&self) -> &BigUint {
316        &self.estimated_gas
317    }
318}
319
320impl PartialEq for Swap {
321    fn eq(&self, other: &Self) -> bool {
322        self.component() == other.component() &&
323            self.token_in().address == other.token_in().address &&
324            self.token_out().address == other.token_out().address &&
325            self.split() == other.split() &&
326            self.user_data() == other.user_data() &&
327            self.estimated_amount_in() == other.estimated_amount_in() &&
328            self.estimated_gas() == other.estimated_gas()
329    }
330}
331
332/// Represents a solution that has been encoded for execution.
333///
334/// # Fields
335/// * `swaps`: Encoded swaps to be executed.
336/// * `interacting_with`: Address of the contract to be called.
337/// * `function_signature`: The signature of the function to be called.
338/// * `n_tokens`: Number of tokens in the swap.
339/// * `estimated_gas`: Estimated gas usage for the encoded solution
340#[derive(Clone, Debug)]
341pub struct EncodedSolution {
342    /// Encoded swaps to be executed.
343    swaps: Vec<u8>,
344    /// Address of the contract to be called.
345    interacting_with: Bytes,
346    /// The signature of the function to be called.
347    function_signature: String,
348    /// Number of tokens in the swap.
349    n_tokens: usize,
350    /// Estimated gas usage for this solution
351    estimated_gas: BigUint,
352}
353
354impl EncodedSolution {
355    pub(crate) fn new(
356        swaps: Vec<u8>,
357        interacting_with: Bytes,
358        function_signature: String,
359        n_tokens: usize,
360        estimated_gas: BigUint,
361    ) -> Self {
362        Self { swaps, interacting_with, function_signature, n_tokens, estimated_gas }
363    }
364
365    pub fn swaps(&self) -> &[u8] {
366        &self.swaps
367    }
368
369    pub fn interacting_with(&self) -> &Bytes {
370        &self.interacting_with
371    }
372
373    pub fn function_signature(&self) -> &str {
374        &self.function_signature
375    }
376
377    pub fn n_tokens(&self) -> usize {
378        self.n_tokens
379    }
380
381    pub fn estimated_gas(&self) -> &BigUint {
382        &self.estimated_gas
383    }
384
385    /// Byte offset within TychoRouterV3 calldata where the client fee signature starts.
386    pub fn client_fee_signature_offset(&self) -> usize {
387        let name = self
388            .function_signature
389            .split('(')
390            .next()
391            .unwrap_or("");
392        let head_params = match name {
393            "singleSwap" |
394            "singleSwapUsingVault" |
395            "sequentialSwap" |
396            "sequentialSwapUsingVault" => 8,
397            "splitSwap" | "splitSwapUsingVault" => 9,
398            "singleSwapPermit2" | "sequentialSwapPermit2" => 15,
399            "splitSwapPermit2" => 16,
400            _ => 0,
401        };
402        // selector (4) + ABI head + offset to signature data within ClientFeeParams tuple
403        4 + head_params * 32 + 192
404    }
405}
406
407/// Represents a single permit for permit2.
408///
409/// # Fields
410/// * `details`: The details of the permit, such as token, amount, expiration, and nonce.
411/// * `spender`: The address authorized to spend the tokens.
412/// * `sig_deadline`: The deadline (as a timestamp) for the permit signature
413#[derive(Debug, Clone)]
414pub struct PermitSingle {
415    details: PermitDetails,
416    spender: Bytes,
417    sig_deadline: BigUint,
418}
419
420impl PermitSingle {
421    pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
422        Self { details, spender, sig_deadline }
423    }
424
425    pub fn details(&self) -> &PermitDetails {
426        &self.details
427    }
428
429    pub fn spender(&self) -> &Bytes {
430        &self.spender
431    }
432
433    pub fn sig_deadline(&self) -> &BigUint {
434        &self.sig_deadline
435    }
436}
437
438/// Details of a permit.
439///
440/// # Fields
441/// * `token`: The token address for which the permit is granted.
442/// * `amount`: The amount of tokens approved for spending.
443/// * `expiration`: The expiration time (as a timestamp) for the permit.
444/// * `nonce`: The unique nonce to prevent replay attacks.
445#[derive(Debug, Clone)]
446pub struct PermitDetails {
447    token: Bytes,
448    amount: BigUint,
449    expiration: BigUint,
450    nonce: BigUint,
451}
452
453impl PermitDetails {
454    pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
455        Self { token, amount, expiration, nonce }
456    }
457
458    pub fn token(&self) -> &Bytes {
459        &self.token
460    }
461
462    pub fn amount(&self) -> &BigUint {
463        &self.amount
464    }
465
466    pub fn expiration(&self) -> &BigUint {
467        &self.expiration
468    }
469
470    pub fn nonce(&self) -> &BigUint {
471        &self.nonce
472    }
473}
474
475impl PartialEq for PermitSingle {
476    fn eq(&self, other: &Self) -> bool {
477        self.details == other.details && self.spender == other.spender
478        // sig_deadline is intentionally ignored
479    }
480}
481
482impl PartialEq for PermitDetails {
483    fn eq(&self, other: &Self) -> bool {
484        self.token == other.token && self.amount == other.amount && self.nonce == other.nonce
485        // expiration is intentionally ignored
486    }
487}
488
489/// Necessary context for encoding a swap within a strategy.
490///
491/// # Fields
492///
493/// * `router_address`: Address of the router contract to be used for the swaps. Zero address if
494///   solution does not require router address.
495/// * `group_token_in`: Token to be used as the input for the group swap.
496/// * `group_token_out`: Token to be used as the output for the group swap.
497#[derive(Clone, Debug)]
498pub struct EncodingContext {
499    pub router_address: Option<Bytes>,
500    pub group_token_in: Bytes,
501    pub group_token_out: Bytes,
502}
503
504#[derive(PartialEq)]
505pub enum Strategy {
506    Single,
507    Sequential,
508    Split,
509}
510
511/// Creates a minimal `Token` from just an address, with zero-value defaults for other fields.
512/// Only available in tests and when the `test-utils` feature is enabled.
513#[cfg(any(test, feature = "test-utils"))]
514pub fn default_token(address: Bytes) -> Token {
515    Token::new(&address, "", 0, 0, &[Some(60_000u64)], Default::default(), 100)
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    struct MockProtocolComponent {
523        id: String,
524        protocol_system: String,
525    }
526
527    impl From<MockProtocolComponent> for ProtocolComponent {
528        fn from(component: MockProtocolComponent) -> Self {
529            ProtocolComponent {
530                id: component.id,
531                protocol_system: component.protocol_system,
532                tokens: vec![],
533                protocol_type_name: "".to_string(),
534                chain: Default::default(),
535                contract_addresses: vec![],
536                static_attributes: Default::default(),
537                change: Default::default(),
538                creation_tx: Default::default(),
539                created_at: Default::default(),
540            }
541        }
542    }
543
544    #[test]
545    fn test_swap_new() {
546        let component = MockProtocolComponent {
547            id: "i-am-an-id".to_string(),
548            protocol_system: "uniswap_v2".to_string(),
549        };
550        let user_data = Bytes::from("0x1234");
551        let swap = Swap::new(
552            component,
553            default_token(Bytes::from("0x12")),
554            default_token(Bytes::from("0x34")),
555            BigUint::ZERO,
556        )
557        .with_split(0.5)
558        .with_user_data(user_data.clone());
559
560        assert_eq!(swap.token_in().address, Bytes::from("0x12"));
561        assert_eq!(swap.token_out().address, Bytes::from("0x34"));
562        assert_eq!(swap.component().protocol_system, "uniswap_v2");
563        assert_eq!(swap.component().id, "i-am-an-id");
564        assert_eq!(swap.split(), 0.5);
565        assert_eq!(swap.user_data(), &Some(user_data));
566    }
567}