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 above `expected_amount_out` or further than
142    /// `MAX_SLIPPAGE_TOLERANCE_BPS` below it, which also excludes zero.
143    #[serde(with = "biguint_string")]
144    min_amount_out: BigUint,
145    /// List of swaps to fulfill the solution.
146    swaps: Vec<Swap>,
147    /// The transfer type to be used in this swap for user's funds (token in)
148    user_transfer_type: UserTransferType,
149}
150
151impl Solution {
152    #[allow(clippy::too_many_arguments)]
153    pub fn new(
154        sender: Bytes,
155        receiver: Bytes,
156        token_in: Bytes,
157        token_out: Bytes,
158        amount_in: BigUint,
159        expected_amount_out: BigUint,
160        min_amount_out: BigUint,
161        swaps: Vec<Swap>,
162    ) -> Self {
163        Self {
164            sender,
165            receiver,
166            token_in,
167            token_out,
168            amount_in,
169            expected_amount_out,
170            min_amount_out,
171            swaps,
172            user_transfer_type: UserTransferType::TransferFrom,
173        }
174    }
175    pub fn sender(&self) -> &Bytes {
176        &self.sender
177    }
178    pub fn receiver(&self) -> &Bytes {
179        &self.receiver
180    }
181
182    pub fn token_in(&self) -> &Bytes {
183        &self.token_in
184    }
185
186    pub fn amount_in(&self) -> &BigUint {
187        &self.amount_in
188    }
189
190    pub fn token_out(&self) -> &Bytes {
191        &self.token_out
192    }
193
194    pub fn expected_amount_out(&self) -> &BigUint {
195        &self.expected_amount_out
196    }
197
198    pub fn min_amount_out(&self) -> &BigUint {
199        &self.min_amount_out
200    }
201
202    pub fn swaps(&self) -> &[Swap] {
203        &self.swaps
204    }
205
206    pub fn user_transfer_type(&self) -> &UserTransferType {
207        &self.user_transfer_type
208    }
209
210    pub fn with_swaps(mut self, swaps: Vec<Swap>) -> Self {
211        self.swaps = swaps;
212        self
213    }
214
215    pub fn with_user_transfer_type(mut self, user_transfer_type: UserTransferType) -> Self {
216        self.user_transfer_type = user_transfer_type;
217        self
218    }
219}
220
221/// Represents a swap operation to be performed on a pool.
222#[derive(Clone, Debug, Deserialize, Serialize)]
223pub struct Swap {
224    /// Protocol component from tycho indexer
225    component: ProtocolComponent,
226    /// Token being input into the pool.
227    token_in: Token,
228    /// Token being output from the pool.
229    token_out: Token,
230    /// Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)
231    #[serde(default)]
232    split: f64,
233    /// Optional user data to be passed to encoding.
234    user_data: Option<Bytes>,
235    /// Optional protocol state used to perform the swap.
236    #[serde(skip)]
237    protocol_state: Option<Arc<dyn ProtocolSim>>,
238    /// Optional estimated amount in for this Swap. This is necessary for RFQ protocols. This value
239    /// is used to request the quote
240    estimated_amount_in: Option<BigUint>,
241    /// Estimated gas usage for this swap by simulation
242    estimated_gas: BigUint,
243}
244
245impl Swap {
246    pub fn new<T: Into<ProtocolComponent>>(
247        component: T,
248        token_in: Token,
249        token_out: Token,
250        estimated_gas: BigUint,
251    ) -> Self {
252        Self {
253            component: component.into(),
254            token_in,
255            token_out,
256            split: 0.0,
257            user_data: None,
258            protocol_state: None,
259            estimated_amount_in: None,
260            estimated_gas,
261        }
262    }
263
264    /// Sets the split value (percentage of the amount to be swapped)
265    pub fn with_split(mut self, split: f64) -> Self {
266        self.split = split;
267        self
268    }
269
270    /// Sets the user data to be passed to encoding
271    pub fn with_user_data(mut self, user_data: Bytes) -> Self {
272        self.user_data = Some(user_data);
273        self
274    }
275
276    /// Sets the protocol state used to perform the swap
277    pub fn with_protocol_state(mut self, protocol_state: Arc<dyn ProtocolSim>) -> Self {
278        self.protocol_state = Some(protocol_state);
279        self
280    }
281
282    /// Sets the estimated amount in for RFQ protocols
283    pub fn with_estimated_amount_in(mut self, estimated_amount_in: BigUint) -> Self {
284        self.estimated_amount_in = Some(estimated_amount_in);
285        self
286    }
287
288    pub fn component(&self) -> &ProtocolComponent {
289        &self.component
290    }
291
292    pub fn token_in(&self) -> &Token {
293        &self.token_in
294    }
295
296    pub fn token_out(&self) -> &Token {
297        &self.token_out
298    }
299
300    pub fn split(&self) -> f64 {
301        self.split
302    }
303
304    pub fn user_data(&self) -> &Option<Bytes> {
305        &self.user_data
306    }
307
308    pub fn protocol_state(&self) -> &Option<Arc<dyn ProtocolSim>> {
309        &self.protocol_state
310    }
311
312    pub fn estimated_amount_in(&self) -> &Option<BigUint> {
313        &self.estimated_amount_in
314    }
315
316    pub fn estimated_gas(&self) -> &BigUint {
317        &self.estimated_gas
318    }
319}
320
321impl PartialEq for Swap {
322    fn eq(&self, other: &Self) -> bool {
323        self.component() == other.component() &&
324            self.token_in().address == other.token_in().address &&
325            self.token_out().address == other.token_out().address &&
326            self.split() == other.split() &&
327            self.user_data() == other.user_data() &&
328            self.estimated_amount_in() == other.estimated_amount_in() &&
329            self.estimated_gas() == other.estimated_gas()
330    }
331}
332
333/// Represents a solution that has been encoded for execution.
334///
335/// # Fields
336/// * `swaps`: Encoded swaps to be executed.
337/// * `interacting_with`: Address of the contract to be called.
338/// * `function_signature`: The signature of the function to be called.
339/// * `n_tokens`: Number of tokens in the swap.
340/// * `estimated_gas`: Estimated gas usage for the encoded solution
341#[derive(Clone, Debug)]
342pub struct EncodedSolution {
343    /// Encoded swaps to be executed.
344    swaps: Vec<u8>,
345    /// Address of the contract to be called.
346    interacting_with: Bytes,
347    /// The signature of the function to be called.
348    function_signature: String,
349    /// Number of tokens in the swap.
350    n_tokens: usize,
351    /// Estimated gas usage for this solution
352    estimated_gas: BigUint,
353}
354
355impl EncodedSolution {
356    pub(crate) fn new(
357        swaps: Vec<u8>,
358        interacting_with: Bytes,
359        function_signature: String,
360        n_tokens: usize,
361        estimated_gas: BigUint,
362    ) -> Self {
363        Self { swaps, interacting_with, function_signature, n_tokens, estimated_gas }
364    }
365
366    pub fn swaps(&self) -> &[u8] {
367        &self.swaps
368    }
369
370    pub fn interacting_with(&self) -> &Bytes {
371        &self.interacting_with
372    }
373
374    pub fn function_signature(&self) -> &str {
375        &self.function_signature
376    }
377
378    pub fn n_tokens(&self) -> usize {
379        self.n_tokens
380    }
381
382    pub fn estimated_gas(&self) -> &BigUint {
383        &self.estimated_gas
384    }
385
386    /// Byte offset within TychoRouterV3 calldata where the client fee signature starts.
387    pub fn client_fee_signature_offset(&self) -> usize {
388        let name = self
389            .function_signature
390            .split('(')
391            .next()
392            .unwrap_or("");
393        let head_params = match name {
394            "singleSwap" |
395            "singleSwapUsingVault" |
396            "sequentialSwap" |
397            "sequentialSwapUsingVault" => 8,
398            "splitSwap" | "splitSwapUsingVault" => 9,
399            "singleSwapPermit2" | "sequentialSwapPermit2" => 15,
400            "splitSwapPermit2" => 16,
401            _ => 0,
402        };
403        // selector (4) + ABI head + offset to signature data within ClientFeeParams tuple
404        4 + head_params * 32 + 192
405    }
406}
407
408/// Represents a single permit for permit2.
409///
410/// # Fields
411/// * `details`: The details of the permit, such as token, amount, expiration, and nonce.
412/// * `spender`: The address authorized to spend the tokens.
413/// * `sig_deadline`: The deadline (as a timestamp) for the permit signature
414#[derive(Debug, Clone)]
415pub struct PermitSingle {
416    details: PermitDetails,
417    spender: Bytes,
418    sig_deadline: BigUint,
419}
420
421impl PermitSingle {
422    pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
423        Self { details, spender, sig_deadline }
424    }
425
426    pub fn details(&self) -> &PermitDetails {
427        &self.details
428    }
429
430    pub fn spender(&self) -> &Bytes {
431        &self.spender
432    }
433
434    pub fn sig_deadline(&self) -> &BigUint {
435        &self.sig_deadline
436    }
437}
438
439/// Details of a permit.
440///
441/// # Fields
442/// * `token`: The token address for which the permit is granted.
443/// * `amount`: The amount of tokens approved for spending.
444/// * `expiration`: The expiration time (as a timestamp) for the permit.
445/// * `nonce`: The unique nonce to prevent replay attacks.
446#[derive(Debug, Clone)]
447pub struct PermitDetails {
448    token: Bytes,
449    amount: BigUint,
450    expiration: BigUint,
451    nonce: BigUint,
452}
453
454impl PermitDetails {
455    pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
456        Self { token, amount, expiration, nonce }
457    }
458
459    pub fn token(&self) -> &Bytes {
460        &self.token
461    }
462
463    pub fn amount(&self) -> &BigUint {
464        &self.amount
465    }
466
467    pub fn expiration(&self) -> &BigUint {
468        &self.expiration
469    }
470
471    pub fn nonce(&self) -> &BigUint {
472        &self.nonce
473    }
474}
475
476impl PartialEq for PermitSingle {
477    fn eq(&self, other: &Self) -> bool {
478        self.details == other.details && self.spender == other.spender
479        // sig_deadline is intentionally ignored
480    }
481}
482
483impl PartialEq for PermitDetails {
484    fn eq(&self, other: &Self) -> bool {
485        self.token == other.token && self.amount == other.amount && self.nonce == other.nonce
486        // expiration is intentionally ignored
487    }
488}
489
490/// Necessary context for encoding a swap within a strategy.
491///
492/// # Fields
493///
494/// * `router_address`: Address of the router contract to be used for the swaps. Zero address if
495///   solution does not require router address.
496/// * `group_token_in`: Token to be used as the input for the group swap.
497/// * `group_token_out`: Token to be used as the output for the group swap.
498#[derive(Clone, Debug)]
499pub struct EncodingContext {
500    pub router_address: Option<Bytes>,
501    pub group_token_in: Bytes,
502    pub group_token_out: Bytes,
503}
504
505#[derive(PartialEq)]
506pub enum Strategy {
507    Single,
508    Sequential,
509    Split,
510}
511
512/// Creates a minimal `Token` from just an address, with zero-value defaults for other fields.
513/// Only available in tests and when the `test-utils` feature is enabled.
514#[cfg(any(test, feature = "test-utils"))]
515pub fn default_token(address: Bytes) -> Token {
516    Token::new(&address, "", 0, 0, &[Some(60_000u64)], Default::default(), 100)
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    struct MockProtocolComponent {
524        id: String,
525        protocol_system: String,
526    }
527
528    impl From<MockProtocolComponent> for ProtocolComponent {
529        fn from(component: MockProtocolComponent) -> Self {
530            ProtocolComponent {
531                id: component.id,
532                protocol_system: component.protocol_system,
533                tokens: vec![],
534                protocol_type_name: "".to_string(),
535                chain: Default::default(),
536                contract_addresses: vec![],
537                static_attributes: Default::default(),
538                change: Default::default(),
539                creation_tx: Default::default(),
540                created_at: Default::default(),
541            }
542        }
543    }
544
545    #[test]
546    fn test_swap_new() {
547        let component = MockProtocolComponent {
548            id: "i-am-an-id".to_string(),
549            protocol_system: "uniswap_v2".to_string(),
550        };
551        let user_data = Bytes::from("0x1234");
552        let swap = Swap::new(
553            component,
554            default_token(Bytes::from("0x12")),
555            default_token(Bytes::from("0x34")),
556            BigUint::ZERO,
557        )
558        .with_split(0.5)
559        .with_user_data(user_data.clone());
560
561        assert_eq!(swap.token_in().address, Bytes::from("0x12"));
562        assert_eq!(swap.token_out().address, Bytes::from("0x34"));
563        assert_eq!(swap.component().protocol_system, "uniswap_v2");
564        assert_eq!(swap.component().id, "i-am-an-id");
565        assert_eq!(swap.split(), 0.5);
566        assert_eq!(swap.user_data(), &Some(user_data));
567    }
568}