diff --git a/README.md b/README.md index f440786..3910a29 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Tycho Execution -TODO: add banner +![img.png](banner.png) Tycho Execution makes it easy to trade on different DEXs by handling the complex encoding for you. Instead of creating custom code for each DEX, you get a simple, ready-to-use tool that generates the necessary data to execute trades. It's diff --git a/banner.png b/banner.png new file mode 100644 index 0000000..688bfe6 Binary files /dev/null and b/banner.png differ diff --git a/src/encoding/evm/approvals/protocol_approvals_manager.rs b/src/encoding/evm/approvals/protocol_approvals_manager.rs index 8d1d16f..9af2355 100644 --- a/src/encoding/evm/approvals/protocol_approvals_manager.rs +++ b/src/encoding/evm/approvals/protocol_approvals_manager.rs @@ -12,6 +12,7 @@ use tokio::runtime::Runtime; use crate::encoding::{errors::EncodingError, evm::utils::encode_input}; +/// A manager for checking if an approval is needed for interacting with a certain spender. pub struct ProtocolApprovalsManager { client: Arc>, runtime: Runtime, @@ -23,6 +24,9 @@ impl ProtocolApprovalsManager { let client = runtime.block_on(get_client())?; Ok(Self { client, runtime }) } + + /// Checks the current allowance for the given token, owner, and spender, and returns true + /// if the current allowance is zero. pub fn approval_needed( &self, token: Address, @@ -56,6 +60,7 @@ impl ProtocolApprovalsManager { } } +/// Gets the client used for interacting with the EVM-compatible network. pub async fn get_client() -> Result>, EncodingError> { dotenv().ok(); let eth_rpc_url = env::var("ETH_RPC_URL") diff --git a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs index ffdbf15..8ca10ce 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs @@ -10,6 +10,11 @@ use crate::encoding::{ strategy_encoder::{StrategyEncoder, StrategyEncoderRegistry}, }; +/// Contains all supported strategies to encode a solution. +/// +/// # Fields +/// * `strategies` - A hashmap containing the name of the strategy as a key and the strategy encoder +/// as a value. pub struct EVMStrategyEncoderRegistry { strategies: HashMap>, } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 21aeee1..af032a7 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -21,7 +21,10 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; +/// Encodes a solution using a specific strategy for execution on the EVM-compatible network. pub trait EVMStrategyEncoder: StrategyEncoder { + /// Encodes information necessary for performing a single swap against a given executor for + /// a protocol. fn encode_swap_header( &self, token_in: U8, @@ -40,11 +43,17 @@ pub trait EVMStrategyEncoder: StrategyEncoder { encoded.extend(protocol_data); encoded } + + /// Encodes a selector string into its 4-byte representation. fn encode_executor_selector(&self, selector: &str) -> FixedBytes<4> { let hash = keccak256(selector.as_bytes()); FixedBytes::<4>::from([hash[0], hash[1], hash[2], hash[3]]) } + /// Uses prefix-length encoding to efficient encode action data. + /// + /// Prefix-length encoding is a data encoding method where the beginning of a data segment + /// (the "prefix") contains information about the length of the following data. fn ple_encode(&self, action_data_array: Vec>) -> Vec { let mut encoded_action_data: Vec = Vec::new(); @@ -57,6 +66,13 @@ pub trait EVMStrategyEncoder: StrategyEncoder { } } +/// Represents the encoder for a swap strategy which supports single, sequential and split swaps. +/// +/// # Fields +/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders +/// * `permit2`: Permit2, responsible for managing permit2 operations and providing necessary +/// signatures and permit2 objects for calling the router +/// * `selector`: String, the selector for the swap function in the router contract pub struct SplitSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, permit2: Permit2, @@ -81,6 +97,14 @@ impl SplitSwapStrategyEncoder { }) } + /// Raises an error if the split percentages are invalid. + /// + /// Split percentages are considered valid if all the following conditions are met: + /// * Each split amount is < 1 (100%) + /// * There is exactly one 0% split for each token, and it's the last swap specified, signifying + /// to the router to send the remainder of the token to the designated protocol + /// * The sum of all non-remainder splits for each token is < 1 (100%) + /// * There are no negative split amounts fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> { let mut swaps_by_token: HashMap> = HashMap::new(); for swap in swaps { @@ -151,6 +175,16 @@ impl SplitSwapStrategyEncoder { Ok(()) } + /// Raises an error if swaps do not represent a valid path from the given token to the checked + /// token. + /// + /// A path is considered valid if all the following conditions are met: + /// * The checked token is reachable from the given token through the swap path + /// * There are no tokens which are unconnected from the main path + /// + /// If the given token is the native token and the native action is WRAP, it will be converted + /// to the wrapped token before validating the swap path. The same principle applies for the + /// checked token and the UNWRAP action. fn validate_swap_path( &self, swaps: &[Swap], @@ -222,7 +256,9 @@ impl SplitSwapStrategyEncoder { } } } + impl EVMStrategyEncoder for SplitSwapStrategyEncoder {} + impl StrategyEncoder for SplitSwapStrategyEncoder { fn encode_strategy( &self, @@ -371,8 +407,11 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } } -/// This strategy encoder is used for solutions that are sent directly to the pool. -/// Only 1 solution with 1 swap is supported. +/// This strategy encoder is used for solutions that are sent directly to the executor, bypassing +/// the router. Only one solution with one swap is supported. +/// +/// # Fields +/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders pub struct ExecutorStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, } @@ -391,7 +430,7 @@ impl StrategyEncoder for ExecutorStrategyEncoder { ) -> Result<(Vec, Bytes), EncodingError> { let router_address = solution.router_address.ok_or_else(|| { EncodingError::InvalidInput( - "Router address is required for straight to pool solutions".to_string(), + "Router address is required for straight-to-executor solutions".to_string(), ) })?; diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index d0b8eeb..4d99206 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -4,6 +4,7 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; +/// Builds a `SwapEncoder` for the given protocol system and executor address. pub struct SwapEncoderBuilder { protocol_system: String, executor_address: String, diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 49c40b0..a9bb22f 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -5,12 +5,16 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; +/// Registry containing all supported `SwapEncoders`. #[derive(Clone)] pub struct SwapEncoderRegistry { + /// A hashmap containing the protocol system as a key and the `SwapEncoder` as a value. encoders: HashMap>, } impl SwapEncoderRegistry { + /// Populates the registry with the `SwapEncoders` for the given blockchain by parsing the + /// executors' addresses in the file at the given path. pub fn new(executors_file_path: &str, blockchain: Chain) -> Result { let config_str = fs::read_to_string(executors_file_path)?; let config: HashMap> = serde_json::from_str(&config_str)?; diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index e3c573f..34c1203 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -12,6 +12,11 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; +/// Encodes a swap on a Uniswap V2 pool through the given executor address. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `executor_selector` - The selector of the swap function in the executor contract. #[derive(Clone)] pub struct UniswapV2SwapEncoder { executor_address: String, @@ -66,6 +71,11 @@ impl SwapEncoder for UniswapV2SwapEncoder { } } +/// Encodes a swap on a Uniswap V3 pool through the given executor address. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `executor_selector` - The selector of the swap function in the executor contract. #[derive(Clone)] pub struct UniswapV3SwapEncoder { executor_address: String, @@ -140,6 +150,11 @@ impl SwapEncoder for UniswapV3SwapEncoder { } } +/// Encodes a swap on a Balancer V2 pool through the given executor address. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `executor_selector` - The selector of the swap function in the executor contract. #[derive(Clone)] pub struct BalancerV2SwapEncoder { executor_address: String, diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs index 54c0f7a..a022d18 100644 --- a/src/encoding/evm/tycho_encoder.rs +++ b/src/encoding/evm/tycho_encoder.rs @@ -10,8 +10,17 @@ use crate::encoding::{ tycho_encoder::TychoEncoder, }; +/// Represents an encoder for a swap through the given router address using any strategy supported +/// by the strategy registry. +/// +/// # Fields +/// * `strategy_registry`: S, the strategy registry to use to select the best strategy to encode a +/// solution, based on its supported strategies and the solution attributes. +/// * `router_address`: Bytes, the address of the router to use to execute the swaps. +/// * `native_address`: Address of the chain's native token +/// * `wrapped_address`: Address of the chain's wrapped native token pub struct EVMTychoEncoder { - strategy_selector: S, + strategy_registry: S, router_address: Bytes, native_address: Bytes, wrapped_address: Bytes, @@ -19,7 +28,7 @@ pub struct EVMTychoEncoder { impl EVMTychoEncoder { pub fn new( - strategy_selector: S, + strategy_registry: S, router_address: String, chain: Chain, ) -> Result { @@ -31,7 +40,7 @@ impl EVMTychoEncoder { )); } Ok(EVMTychoEncoder { - strategy_selector, + strategy_registry, router_address, native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, @@ -40,6 +49,15 @@ impl EVMTychoEncoder { } impl EVMTychoEncoder { + /// Raises an `EncodingError` if the solution is not considered valid. + /// + /// A solution is considered valid if all the following conditions are met: + /// * The solution is not exact out. + /// * The solution has at least one swap. + /// * If the solution is wrapping, the given token is the chain's native token and the first + /// swap's input is the chain's wrapped token. + /// * If the solution is unwrapping, the checked token is the chain's native token and the last + /// swap's output is the chain's wrapped token. fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { if solution.exact_out { return Err(EncodingError::FatalError( @@ -99,7 +117,7 @@ impl TychoEncoder for EVMTychoEncoder { .unwrap_or(self.router_address.clone()); let strategy = self - .strategy_selector + .strategy_registry .get_encoder(solution)?; let (contract_interaction, target_address) = strategy.encode_strategy(solution.clone(), router_address)?; @@ -187,9 +205,9 @@ mod tests { } fn get_mocked_tycho_encoder() -> EVMTychoEncoder { - let strategy_selector = MockStrategyRegistry::new(eth_chain(), "", None).unwrap(); + let strategy_registry = MockStrategyRegistry::new(eth_chain(), "", None).unwrap(); EVMTychoEncoder::new( - strategy_selector, + strategy_registry, "0x1234567890abcdef1234567890abcdef12345678".to_string(), eth_chain(), ) diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index 05a13e0..5d3a70a 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -16,11 +16,13 @@ pub fn bytes_to_address(address: &Bytes) -> Result { } } +/// Converts a general `BigUint` to an EVM-specific `U256` value. pub fn biguint_to_u256(value: &BigUint) -> U256 { let bytes = value.to_bytes_be(); U256::from_be_slice(&bytes) } +/// Encodes the input data for a function call to the given function selector. pub fn encode_input(selector: &str, mut encoded_args: Vec) -> Vec { let mut hasher = Keccak256::new(); hasher.update(selector.as_bytes()); diff --git a/src/encoding/models.rs b/src/encoding/models.rs index c6b3432..0abe8e1 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -11,6 +11,9 @@ use crate::encoding::{ serde_primitives::{biguint_string, biguint_string_option}, }; + +/// Represents a solution containing details describing an order, and instructions for filling +/// the order. #[derive(Clone, Default, Debug, Deserialize, Serialize)] pub struct Solution { /// Address of the sender. @@ -28,7 +31,7 @@ pub struct Solution { /// supported. #[serde(default)] pub exact_out: bool, - // If set, it will be applied to expected_amount + /// If set, it will be applied to expected_amount pub slippage: Option, /// Expected amount of the bought token (exact in) or sold token (exact out). #[serde(with = "biguint_string_option")] @@ -39,9 +42,9 @@ pub struct Solution { pub check_amount: Option, /// List of swaps to fulfill the solution. pub swaps: Vec, - // If not set, then the Tycho Router will be used + /// If not set, then the Tycho Router will be used pub router_address: Option, - // If set, the corresponding native action will be executed. + /// If set, the corresponding native action will be executed. pub native_action: Option, /// If set to true, the solution will be encoded to be sent directly to the Executor and /// skip the router. The user is responsible for managing necessary approvals and token @@ -50,6 +53,12 @@ pub struct Solution { pub direct_execution: bool, } + +/// Represents an action to be performed on the native token either before or after the swap. +/// +/// `Wrap` means that the native token will be wrapped before the first swap, and `Unwrap` +/// means that the native token will be unwrapped after the last swap, before being sent to the +/// receiver. #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum NativeAction { @@ -57,6 +66,9 @@ pub enum NativeAction { Unwrap, } + + +/// Represents a swap operation to be performed on a pool. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Swap { /// Protocol component from tycho indexer @@ -65,7 +77,7 @@ pub struct Swap { pub token_in: Bytes, /// Token being output from the pool. pub token_out: Bytes, - /// Percentage of the amount to be swapped in this operation (for example, 0.5 means 50%) + /// Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%) #[serde(default)] pub split: f64, } @@ -81,16 +93,26 @@ impl Swap { } } +/// Represents a transaction to be executed. +/// +/// # Fields +/// * `to`: Address of the contract to call with the calldata +/// * `value`: Native token value to be sent with the transaction. +/// * `data`: Encoded calldata for the transaction. #[derive(Clone, Debug)] pub struct Transaction { - // Address of the contract to call with the calldata pub to: Bytes, - // ETH value to be sent with the transaction. pub value: BigUint, - // Encoded calldata for the transaction. pub data: Vec, } +/// Represents necessary attributes for encoding an order. +/// +/// # Fields +/// +/// * `receiver`: Address of the receiver of the out token after the swaps are completed. +/// * `exact_out`: true if the solution is a buy order, false if it is a sell order. +/// * `router_address`: Address of the router contract to be used for the swaps. pub struct EncodingContext { pub receiver: Bytes, pub exact_out: bool, diff --git a/src/encoding/strategy_encoder.rs b/src/encoding/strategy_encoder.rs index 64b8e9e..bb7ecf4 100644 --- a/src/encoding/strategy_encoder.rs +++ b/src/encoding/strategy_encoder.rs @@ -6,6 +6,7 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; +/// Encodes a solution using a specific strategy. pub trait StrategyEncoder { fn encode_strategy( &self, @@ -17,6 +18,8 @@ pub trait StrategyEncoder { fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box>; } +/// Contains the supported strategies to encode a solution, and chooses the best strategy to encode +/// a solution based on the solution's attributes. pub trait StrategyEncoderRegistry { fn new( chain: Chain, @@ -25,6 +28,8 @@ pub trait StrategyEncoderRegistry { ) -> Result where Self: Sized; + + /// Returns the strategy encoder that should be used to encode the given solution. #[allow(clippy::borrowed_box)] fn get_encoder(&self, solution: &Solution) -> Result<&Box, EncodingError>; } diff --git a/src/encoding/swap_encoder.rs b/src/encoding/swap_encoder.rs index 4247de5..a0a9d0f 100644 --- a/src/encoding/swap_encoder.rs +++ b/src/encoding/swap_encoder.rs @@ -2,16 +2,24 @@ use crate::encoding::{ errors::EncodingError, models::{EncodingContext, Swap}, }; + +/// This trait must be implemented in order to encode a single swap for a specific protocol. pub trait SwapEncoder: Sync + Send { fn new(executor_address: String) -> Self where Self: Sized; + + /// Encodes a swap and its relevant context information into call data for a specific protocol. fn encode_swap( &self, swap: Swap, encoding_context: EncodingContext, ) -> Result, EncodingError>; + + /// The address of the executor that will be used to swap through a specific protocol. fn executor_address(&self) -> &str; + + /// The selector of the executor function that will be called in order to perform a swap. fn executor_selector(&self) -> &str; /// Clones the swap encoder as a trait object. diff --git a/src/encoding/tycho_encoder.rs b/src/encoding/tycho_encoder.rs index 8ee054e..3be2277 100644 --- a/src/encoding/tycho_encoder.rs +++ b/src/encoding/tycho_encoder.rs @@ -4,6 +4,8 @@ use crate::encoding::{ strategy_encoder::StrategyEncoderRegistry, }; +/// An encoder must implement this trait in order to encode a solution into a Transaction for +/// execution using a Tycho router or related contracts. pub trait TychoEncoder { fn encode_router_calldata( &self,