Merge branch 'main' into encoder/hr/ENG-4093-bin

This commit is contained in:
Harsh Vardhan Roy
2025-02-06 22:41:23 +05:30
committed by GitHub
14 changed files with 143 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
# Tycho Execution # 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 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 custom code for each DEX, you get a simple, ready-to-use tool that generates the necessary data to execute trades. It's

BIN
banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 KiB

View File

@@ -12,6 +12,7 @@ use tokio::runtime::Runtime;
use crate::encoding::{errors::EncodingError, evm::utils::encode_input}; 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 { pub struct ProtocolApprovalsManager {
client: Arc<RootProvider<BoxTransport>>, client: Arc<RootProvider<BoxTransport>>,
runtime: Runtime, runtime: Runtime,
@@ -23,6 +24,9 @@ impl ProtocolApprovalsManager {
let client = runtime.block_on(get_client())?; let client = runtime.block_on(get_client())?;
Ok(Self { client, runtime }) 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( pub fn approval_needed(
&self, &self,
token: Address, 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<Arc<RootProvider<BoxTransport>>, EncodingError> { pub async fn get_client() -> Result<Arc<RootProvider<BoxTransport>>, EncodingError> {
dotenv().ok(); dotenv().ok();
let eth_rpc_url = env::var("ETH_RPC_URL") let eth_rpc_url = env::var("ETH_RPC_URL")

View File

@@ -10,6 +10,11 @@ use crate::encoding::{
strategy_encoder::{StrategyEncoder, StrategyEncoderRegistry}, 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 { pub struct EVMStrategyEncoderRegistry {
strategies: HashMap<String, Box<dyn StrategyEncoder>>, strategies: HashMap<String, Box<dyn StrategyEncoder>>,
} }

View File

@@ -21,7 +21,10 @@ use crate::encoding::{
swap_encoder::SwapEncoder, swap_encoder::SwapEncoder,
}; };
/// Encodes a solution using a specific strategy for execution on the EVM-compatible network.
pub trait EVMStrategyEncoder: StrategyEncoder { pub trait EVMStrategyEncoder: StrategyEncoder {
/// Encodes information necessary for performing a single swap against a given executor for
/// a protocol.
fn encode_swap_header( fn encode_swap_header(
&self, &self,
token_in: U8, token_in: U8,
@@ -40,11 +43,17 @@ pub trait EVMStrategyEncoder: StrategyEncoder {
encoded.extend(protocol_data); encoded.extend(protocol_data);
encoded encoded
} }
/// Encodes a selector string into its 4-byte representation.
fn encode_executor_selector(&self, selector: &str) -> FixedBytes<4> { fn encode_executor_selector(&self, selector: &str) -> FixedBytes<4> {
let hash = keccak256(selector.as_bytes()); let hash = keccak256(selector.as_bytes());
FixedBytes::<4>::from([hash[0], hash[1], hash[2], hash[3]]) 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<u8>>) -> Vec<u8> { fn ple_encode(&self, action_data_array: Vec<Vec<u8>>) -> Vec<u8> {
let mut encoded_action_data: Vec<u8> = Vec::new(); let mut encoded_action_data: Vec<u8> = 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 { pub struct SplitSwapStrategyEncoder {
swap_encoder_registry: SwapEncoderRegistry, swap_encoder_registry: SwapEncoderRegistry,
permit2: Permit2, 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> { fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> {
let mut swaps_by_token: HashMap<Bytes, Vec<&Swap>> = HashMap::new(); let mut swaps_by_token: HashMap<Bytes, Vec<&Swap>> = HashMap::new();
for swap in swaps { for swap in swaps {
@@ -151,6 +175,16 @@ impl SplitSwapStrategyEncoder {
Ok(()) 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( fn validate_swap_path(
&self, &self,
swaps: &[Swap], swaps: &[Swap],
@@ -222,7 +256,9 @@ impl SplitSwapStrategyEncoder {
} }
} }
} }
impl EVMStrategyEncoder for SplitSwapStrategyEncoder {} impl EVMStrategyEncoder for SplitSwapStrategyEncoder {}
impl StrategyEncoder for SplitSwapStrategyEncoder { impl StrategyEncoder for SplitSwapStrategyEncoder {
fn encode_strategy( fn encode_strategy(
&self, &self,
@@ -371,8 +407,11 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
} }
} }
/// This strategy encoder is used for solutions that are sent directly to the pool. /// This strategy encoder is used for solutions that are sent directly to the executor, bypassing
/// Only 1 solution with 1 swap is supported. /// the router. Only one solution with one swap is supported.
///
/// # Fields
/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders
pub struct ExecutorStrategyEncoder { pub struct ExecutorStrategyEncoder {
swap_encoder_registry: SwapEncoderRegistry, swap_encoder_registry: SwapEncoderRegistry,
} }
@@ -391,7 +430,7 @@ impl StrategyEncoder for ExecutorStrategyEncoder {
) -> Result<(Vec<u8>, Bytes), EncodingError> { ) -> Result<(Vec<u8>, Bytes), EncodingError> {
let router_address = solution.router_address.ok_or_else(|| { let router_address = solution.router_address.ok_or_else(|| {
EncodingError::InvalidInput( EncodingError::InvalidInput(
"Router address is required for straight to pool solutions".to_string(), "Router address is required for straight-to-executor solutions".to_string(),
) )
})?; })?;

View File

@@ -4,6 +4,7 @@ use crate::encoding::{
swap_encoder::SwapEncoder, swap_encoder::SwapEncoder,
}; };
/// Builds a `SwapEncoder` for the given protocol system and executor address.
pub struct SwapEncoderBuilder { pub struct SwapEncoderBuilder {
protocol_system: String, protocol_system: String,
executor_address: String, executor_address: String,

View File

@@ -5,12 +5,16 @@ use crate::encoding::{
swap_encoder::SwapEncoder, swap_encoder::SwapEncoder,
}; };
/// Registry containing all supported `SwapEncoders`.
#[derive(Clone)] #[derive(Clone)]
pub struct SwapEncoderRegistry { pub struct SwapEncoderRegistry {
/// A hashmap containing the protocol system as a key and the `SwapEncoder` as a value.
encoders: HashMap<String, Box<dyn SwapEncoder>>, encoders: HashMap<String, Box<dyn SwapEncoder>>,
} }
impl SwapEncoderRegistry { 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<Self, EncodingError> { pub fn new(executors_file_path: &str, blockchain: Chain) -> Result<Self, EncodingError> {
let config_str = fs::read_to_string(executors_file_path)?; let config_str = fs::read_to_string(executors_file_path)?;
let config: HashMap<String, HashMap<String, String>> = serde_json::from_str(&config_str)?; let config: HashMap<String, HashMap<String, String>> = serde_json::from_str(&config_str)?;

View File

@@ -12,6 +12,11 @@ use crate::encoding::{
swap_encoder::SwapEncoder, 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)] #[derive(Clone)]
pub struct UniswapV2SwapEncoder { pub struct UniswapV2SwapEncoder {
executor_address: String, 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)] #[derive(Clone)]
pub struct UniswapV3SwapEncoder { pub struct UniswapV3SwapEncoder {
executor_address: String, 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)] #[derive(Clone)]
pub struct BalancerV2SwapEncoder { pub struct BalancerV2SwapEncoder {
executor_address: String, executor_address: String,

View File

@@ -10,8 +10,17 @@ use crate::encoding::{
tycho_encoder::TychoEncoder, 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<S: StrategyEncoderRegistry> { pub struct EVMTychoEncoder<S: StrategyEncoderRegistry> {
strategy_selector: S, strategy_registry: S,
router_address: Bytes, router_address: Bytes,
native_address: Bytes, native_address: Bytes,
wrapped_address: Bytes, wrapped_address: Bytes,
@@ -19,7 +28,7 @@ pub struct EVMTychoEncoder<S: StrategyEncoderRegistry> {
impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> { impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
pub fn new( pub fn new(
strategy_selector: S, strategy_registry: S,
router_address: String, router_address: String,
chain: Chain, chain: Chain,
) -> Result<Self, EncodingError> { ) -> Result<Self, EncodingError> {
@@ -31,7 +40,7 @@ impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
)); ));
} }
Ok(EVMTychoEncoder { Ok(EVMTychoEncoder {
strategy_selector, strategy_registry,
router_address, router_address,
native_address: chain.native_token()?, native_address: chain.native_token()?,
wrapped_address: chain.wrapped_token()?, wrapped_address: chain.wrapped_token()?,
@@ -40,6 +49,15 @@ impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
} }
impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> { impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
/// 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> { fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> {
if solution.exact_out { if solution.exact_out {
return Err(EncodingError::FatalError( return Err(EncodingError::FatalError(
@@ -99,7 +117,7 @@ impl<S: StrategyEncoderRegistry> TychoEncoder<S> for EVMTychoEncoder<S> {
.unwrap_or(self.router_address.clone()); .unwrap_or(self.router_address.clone());
let strategy = self let strategy = self
.strategy_selector .strategy_registry
.get_encoder(solution)?; .get_encoder(solution)?;
let (contract_interaction, target_address) = let (contract_interaction, target_address) =
strategy.encode_strategy(solution.clone(), router_address)?; strategy.encode_strategy(solution.clone(), router_address)?;
@@ -187,9 +205,9 @@ mod tests {
} }
fn get_mocked_tycho_encoder() -> EVMTychoEncoder<MockStrategyRegistry> { fn get_mocked_tycho_encoder() -> EVMTychoEncoder<MockStrategyRegistry> {
let strategy_selector = MockStrategyRegistry::new(eth_chain(), "", None).unwrap(); let strategy_registry = MockStrategyRegistry::new(eth_chain(), "", None).unwrap();
EVMTychoEncoder::new( EVMTychoEncoder::new(
strategy_selector, strategy_registry,
"0x1234567890abcdef1234567890abcdef12345678".to_string(), "0x1234567890abcdef1234567890abcdef12345678".to_string(),
eth_chain(), eth_chain(),
) )

View File

@@ -16,11 +16,13 @@ pub fn bytes_to_address(address: &Bytes) -> Result<Address, EncodingError> {
} }
} }
/// Converts a general `BigUint` to an EVM-specific `U256` value.
pub fn biguint_to_u256(value: &BigUint) -> U256 { pub fn biguint_to_u256(value: &BigUint) -> U256 {
let bytes = value.to_bytes_be(); let bytes = value.to_bytes_be();
U256::from_be_slice(&bytes) 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<u8>) -> Vec<u8> { pub fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
let mut hasher = Keccak256::new(); let mut hasher = Keccak256::new();
hasher.update(selector.as_bytes()); hasher.update(selector.as_bytes());

View File

@@ -11,6 +11,9 @@ use crate::encoding::{
serde_primitives::{biguint_string, biguint_string_option}, 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)] #[derive(Clone, Default, Debug, Deserialize, Serialize)]
pub struct Solution { pub struct Solution {
/// Address of the sender. /// Address of the sender.
@@ -28,7 +31,7 @@ pub struct Solution {
/// supported. /// supported.
#[serde(default)] #[serde(default)]
pub exact_out: bool, 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<f64>, pub slippage: Option<f64>,
/// Expected amount of the bought token (exact in) or sold token (exact out). /// Expected amount of the bought token (exact in) or sold token (exact out).
#[serde(with = "biguint_string_option")] #[serde(with = "biguint_string_option")]
@@ -39,9 +42,9 @@ pub struct Solution {
pub check_amount: Option<BigUint>, pub check_amount: Option<BigUint>,
/// List of swaps to fulfill the solution. /// List of swaps to fulfill the solution.
pub swaps: Vec<Swap>, pub swaps: Vec<Swap>,
// If not set, then the Tycho Router will be used /// If not set, then the Tycho Router will be used
pub router_address: Option<Bytes>, pub router_address: Option<Bytes>,
// If set, the corresponding native action will be executed. /// If set, the corresponding native action will be executed.
pub native_action: Option<NativeAction>, pub native_action: Option<NativeAction>,
/// If set to true, the solution will be encoded to be sent directly to the Executor and /// 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 /// skip the router. The user is responsible for managing necessary approvals and token
@@ -50,6 +53,12 @@ pub struct Solution {
pub direct_execution: bool, 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)] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum NativeAction { pub enum NativeAction {
@@ -57,6 +66,9 @@ pub enum NativeAction {
Unwrap, Unwrap,
} }
/// Represents a swap operation to be performed on a pool.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Swap { pub struct Swap {
/// Protocol component from tycho indexer /// Protocol component from tycho indexer
@@ -65,7 +77,7 @@ pub struct Swap {
pub token_in: Bytes, pub token_in: Bytes,
/// Token being output from the pool. /// Token being output from the pool.
pub token_out: Bytes, 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)] #[serde(default)]
pub split: f64, 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)] #[derive(Clone, Debug)]
pub struct Transaction { pub struct Transaction {
// Address of the contract to call with the calldata
pub to: Bytes, pub to: Bytes,
// ETH value to be sent with the transaction.
pub value: BigUint, pub value: BigUint,
// Encoded calldata for the transaction.
pub data: Vec<u8>, pub data: Vec<u8>,
} }
/// 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 struct EncodingContext {
pub receiver: Bytes, pub receiver: Bytes,
pub exact_out: bool, pub exact_out: bool,

View File

@@ -6,6 +6,7 @@ use crate::encoding::{
swap_encoder::SwapEncoder, swap_encoder::SwapEncoder,
}; };
/// Encodes a solution using a specific strategy.
pub trait StrategyEncoder { pub trait StrategyEncoder {
fn encode_strategy( fn encode_strategy(
&self, &self,
@@ -17,6 +18,8 @@ pub trait StrategyEncoder {
fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box<dyn SwapEncoder>>; fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box<dyn SwapEncoder>>;
} }
/// 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 { pub trait StrategyEncoderRegistry {
fn new( fn new(
chain: Chain, chain: Chain,
@@ -25,6 +28,8 @@ pub trait StrategyEncoderRegistry {
) -> Result<Self, EncodingError> ) -> Result<Self, EncodingError>
where where
Self: Sized; Self: Sized;
/// Returns the strategy encoder that should be used to encode the given solution.
#[allow(clippy::borrowed_box)] #[allow(clippy::borrowed_box)]
fn get_encoder(&self, solution: &Solution) -> Result<&Box<dyn StrategyEncoder>, EncodingError>; fn get_encoder(&self, solution: &Solution) -> Result<&Box<dyn StrategyEncoder>, EncodingError>;
} }

View File

@@ -2,16 +2,24 @@ use crate::encoding::{
errors::EncodingError, errors::EncodingError,
models::{EncodingContext, Swap}, models::{EncodingContext, Swap},
}; };
/// This trait must be implemented in order to encode a single swap for a specific protocol.
pub trait SwapEncoder: Sync + Send { pub trait SwapEncoder: Sync + Send {
fn new(executor_address: String) -> Self fn new(executor_address: String) -> Self
where where
Self: Sized; Self: Sized;
/// Encodes a swap and its relevant context information into call data for a specific protocol.
fn encode_swap( fn encode_swap(
&self, &self,
swap: Swap, swap: Swap,
encoding_context: EncodingContext, encoding_context: EncodingContext,
) -> Result<Vec<u8>, EncodingError>; ) -> Result<Vec<u8>, EncodingError>;
/// The address of the executor that will be used to swap through a specific protocol.
fn executor_address(&self) -> &str; 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; fn executor_selector(&self) -> &str;
/// Clones the swap encoder as a trait object. /// Clones the swap encoder as a trait object.

View File

@@ -4,6 +4,8 @@ use crate::encoding::{
strategy_encoder::StrategyEncoderRegistry, 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<S: StrategyEncoderRegistry> { pub trait TychoEncoder<S: StrategyEncoderRegistry> {
fn encode_router_calldata( fn encode_router_calldata(
&self, &self,