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