feat: Separate encoding swaps from encoding txs
This way the user is responsible for encoding the Tycho Router method inputs that are used as guardrails in execution. Interface changes: - Create EncodedSolution - StrategyEncoder - don't need to know have permit2 or token_in_already_in_router as attributes anymore - encode_strategy returns EncodedSolution now (no method encoding done here now) - TychoEncoder - add encode_solution() method. This is the recommended method for users - needs to have permit2, token_in_already_in_router and router_address as attributes - permit creation is made in the router now Also: - create encoding_utils.rs - update all tests Took 2 hours 42 minutes Took 3 minutes Took 13 minutes
This commit is contained in:
@@ -20,6 +20,8 @@ pub enum EncodingError {
|
||||
FatalError(String),
|
||||
#[error("Recoverable error: {0}")]
|
||||
RecoverableError(String),
|
||||
#[error("Not implemented: {0}")]
|
||||
NotImplementedError(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for EncodingError {
|
||||
|
||||
@@ -19,7 +19,10 @@ use tycho_common::Bytes;
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::utils::{biguint_to_u256, bytes_to_address, encode_input, get_client, get_runtime},
|
||||
evm::{
|
||||
encoding_utils::encode_input,
|
||||
utils::{biguint_to_u256, bytes_to_address, get_client, get_runtime},
|
||||
},
|
||||
models::Chain,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ use tokio::{
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::utils::{encode_input, get_client, get_runtime},
|
||||
evm::{
|
||||
encoding_utils::encode_input,
|
||||
utils::{get_client, get_runtime},
|
||||
},
|
||||
};
|
||||
|
||||
/// A manager for checking if an approval is needed for interacting with a certain spender.
|
||||
|
||||
251
src/encoding/evm/encoding_utils.rs
Normal file
251
src/encoding/evm/encoding_utils.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use alloy_primitives::{Keccak256, U256};
|
||||
use alloy_sol_types::SolValue;
|
||||
use num_bigint::BigUint;
|
||||
use tycho_common::Bytes;
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::utils::{biguint_to_u256, bytes_to_address, get_min_amount_for_solution},
|
||||
models::{EncodedSolution, NativeAction, Solution, Transaction},
|
||||
};
|
||||
|
||||
/// 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> {
|
||||
let mut hasher = Keccak256::new();
|
||||
hasher.update(selector.as_bytes());
|
||||
let selector_bytes = &hasher.finalize()[..4];
|
||||
let mut call_data = selector_bytes.to_vec();
|
||||
// Remove extra prefix if present (32 bytes for dynamic data)
|
||||
// Alloy encoding is including a prefix for dynamic data indicating the offset or length
|
||||
// but at this point we don't want that
|
||||
if encoded_args.len() > 32 &&
|
||||
encoded_args[..32] ==
|
||||
[0u8; 31]
|
||||
.into_iter()
|
||||
.chain([32].to_vec())
|
||||
.collect::<Vec<u8>>()
|
||||
{
|
||||
encoded_args = encoded_args[32..].to_vec();
|
||||
}
|
||||
call_data.extend(encoded_args);
|
||||
call_data
|
||||
}
|
||||
|
||||
/// Encodes a transaction for the Tycho Router using one of its supported swap methods.
|
||||
///
|
||||
/// # Overview
|
||||
///
|
||||
/// This function provides an **example implementation** of how to encode a call to the Tycho
|
||||
/// Router. It handles all currently supported swap selectors such as:
|
||||
/// - `singleSwap`
|
||||
/// - `singleSwapPermit2`
|
||||
/// - `sequentialSwap`
|
||||
/// - `sequentialSwapPermit2`
|
||||
/// - `splitSwap`
|
||||
/// - `splitSwapPermit2`
|
||||
///
|
||||
/// The encoding includes handling of native asset wrapping/unwrapping, permit2 support,
|
||||
/// and proper input argument formatting based on the selector string.
|
||||
///
|
||||
/// # ⚠️ Important Responsibility Note
|
||||
///
|
||||
/// This function is intended as **an illustrative example only**. **Users must implement
|
||||
/// their own encoding logic** to ensure:
|
||||
/// - Full control of parameters passed to the router.
|
||||
/// - Proper validation and setting of critical inputs such as `minAmountOut`.
|
||||
///
|
||||
/// While Tycho is responsible for encoding the swap paths themselves, the input arguments
|
||||
/// to the router's methods act as **guardrails** for on-chain execution safety.
|
||||
/// Thus, the user must **take responsibility** for ensuring correctness of all input parameters,
|
||||
/// including `minAmountOut`, `receiver`, and permit2 logic.
|
||||
///
|
||||
/// # Min Amount Out
|
||||
///
|
||||
/// The `minAmountOut` calculation used here is just an example.
|
||||
/// You should ideally:
|
||||
/// - Query an external service (e.g., DEX aggregators, oracle, off-chain price feed).
|
||||
/// - Use your own strategy to determine an accurate and safe minimum acceptable output amount.
|
||||
///
|
||||
/// ⚠️ If `minAmountOut` is too low, your swap may be front-run or sandwiched, resulting in loss of
|
||||
/// funds.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `encoded_solution`: The solution already encoded by Tycho, including selector and swap path.
|
||||
/// - `solution`: The high-level solution including tokens, amounts, and receiver info.
|
||||
/// - `token_in_already_in_router`: Whether the input token is already present in the router.
|
||||
/// - `router_address`: The address of the Tycho Router contract.
|
||||
/// - `native_address`: The address used to represent the native token
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result<Transaction, EncodingError>` that either contains the full transaction data (to,
|
||||
/// value, data), or an error if the inputs are invalid.
|
||||
///
|
||||
/// # Errors
|
||||
/// - Returns `EncodingError::FatalError` if the selector is unsupported or required fields (e.g.,
|
||||
/// permit or signature) are missing.
|
||||
pub fn encode_tycho_router_call(
|
||||
encoded_solution: EncodedSolution,
|
||||
solution: &Solution,
|
||||
token_in_already_in_router: bool,
|
||||
router_address: Bytes,
|
||||
native_address: Bytes,
|
||||
) -> Result<Transaction, EncodingError> {
|
||||
let min_amount_out = get_min_amount_for_solution(solution.clone());
|
||||
let (mut unwrap, mut wrap) = (false, false);
|
||||
if let Some(action) = solution.native_action.clone() {
|
||||
match action {
|
||||
NativeAction::Wrap => wrap = true,
|
||||
NativeAction::Unwrap => unwrap = true,
|
||||
}
|
||||
}
|
||||
|
||||
let given_amount = biguint_to_u256(&solution.given_amount);
|
||||
let min_amount_out = biguint_to_u256(&min_amount_out);
|
||||
let given_token = bytes_to_address(&solution.given_token)?;
|
||||
let checked_token = bytes_to_address(&solution.checked_token)?;
|
||||
let receiver = bytes_to_address(&solution.receiver)?;
|
||||
let n_tokens = U256::from(encoded_solution.n_tokens);
|
||||
|
||||
let method_calldata = if encoded_solution
|
||||
.selector
|
||||
.contains("singleSwapPermit2")
|
||||
{
|
||||
(
|
||||
given_amount,
|
||||
given_token,
|
||||
checked_token,
|
||||
min_amount_out,
|
||||
wrap,
|
||||
unwrap,
|
||||
receiver,
|
||||
encoded_solution
|
||||
.permit
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"permit2 object must be set to use permit2".to_string(),
|
||||
))?,
|
||||
encoded_solution
|
||||
.signature
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"Signature must be set to use permit2".to_string(),
|
||||
))?
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
encoded_solution.swaps,
|
||||
)
|
||||
.abi_encode()
|
||||
} else if encoded_solution
|
||||
.selector
|
||||
.contains("singleSwap")
|
||||
{
|
||||
(
|
||||
given_amount,
|
||||
given_token,
|
||||
checked_token,
|
||||
min_amount_out,
|
||||
wrap,
|
||||
unwrap,
|
||||
receiver,
|
||||
!token_in_already_in_router,
|
||||
encoded_solution.swaps,
|
||||
)
|
||||
.abi_encode()
|
||||
} else if encoded_solution
|
||||
.selector
|
||||
.contains("sequentialSwapPermit2")
|
||||
{
|
||||
(
|
||||
given_amount,
|
||||
given_token,
|
||||
checked_token,
|
||||
min_amount_out,
|
||||
wrap,
|
||||
unwrap,
|
||||
receiver,
|
||||
encoded_solution
|
||||
.permit
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"permit2 object must be set to use permit2".to_string(),
|
||||
))?,
|
||||
encoded_solution
|
||||
.signature
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"Signature must be set to use permit2".to_string(),
|
||||
))?
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
encoded_solution.swaps,
|
||||
)
|
||||
.abi_encode()
|
||||
} else if encoded_solution
|
||||
.selector
|
||||
.contains("sequentialSwap")
|
||||
{
|
||||
(
|
||||
given_amount,
|
||||
given_token,
|
||||
checked_token,
|
||||
min_amount_out,
|
||||
wrap,
|
||||
unwrap,
|
||||
receiver,
|
||||
!token_in_already_in_router,
|
||||
encoded_solution.swaps,
|
||||
)
|
||||
.abi_encode()
|
||||
} else if encoded_solution
|
||||
.selector
|
||||
.contains("splitSwapPermit2")
|
||||
{
|
||||
(
|
||||
given_amount,
|
||||
given_token,
|
||||
checked_token,
|
||||
min_amount_out,
|
||||
wrap,
|
||||
unwrap,
|
||||
n_tokens,
|
||||
receiver,
|
||||
encoded_solution
|
||||
.permit
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"permit2 object must be set to use permit2".to_string(),
|
||||
))?,
|
||||
encoded_solution
|
||||
.signature
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"Signature must be set to use permit2".to_string(),
|
||||
))?
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
encoded_solution.swaps,
|
||||
)
|
||||
.abi_encode()
|
||||
} else if encoded_solution
|
||||
.selector
|
||||
.contains("splitSwap")
|
||||
{
|
||||
(
|
||||
given_amount,
|
||||
given_token,
|
||||
checked_token,
|
||||
min_amount_out,
|
||||
wrap,
|
||||
unwrap,
|
||||
n_tokens,
|
||||
receiver,
|
||||
!token_in_already_in_router,
|
||||
encoded_solution.swaps,
|
||||
)
|
||||
.abi_encode()
|
||||
} else {
|
||||
Err(EncodingError::FatalError("Invalid selector for Tycho router".to_string()))?
|
||||
};
|
||||
|
||||
let contract_interaction = encode_input(&encoded_solution.selector, method_calldata);
|
||||
let value = if solution.given_token == native_address {
|
||||
solution.given_amount.clone()
|
||||
} else {
|
||||
BigUint::ZERO
|
||||
};
|
||||
Ok(Transaction { to: router_address, value, data: contract_interaction })
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod approvals;
|
||||
mod constants;
|
||||
pub mod encoder_builders;
|
||||
pub mod encoding_utils;
|
||||
mod group_swaps;
|
||||
pub mod strategy_encoder;
|
||||
mod swap_encoder;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,14 +6,18 @@ use tycho_common::Bytes;
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::{
|
||||
approvals::permit2::Permit2,
|
||||
constants::{GROUPABLE_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS},
|
||||
encoding_utils::encode_tycho_router_call,
|
||||
group_swaps::group_swaps,
|
||||
strategy_encoder::strategy_encoders::{
|
||||
SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder,
|
||||
},
|
||||
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
|
||||
},
|
||||
models::{Chain, EncodingContext, NativeAction, Solution, Transaction, TransferType},
|
||||
models::{
|
||||
Chain, EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType,
|
||||
},
|
||||
strategy_encoder::StrategyEncoder,
|
||||
tycho_encoder::TychoEncoder,
|
||||
};
|
||||
@@ -26,6 +30,8 @@ use crate::encoding::{
|
||||
/// * `split_swap_strategy`: Encoder for split swaps
|
||||
/// * `native_address`: Address of the chain's native token
|
||||
/// * `wrapped_address`: Address of the chain's wrapped native token
|
||||
/// * `router_address`: Address of the Tycho router contract
|
||||
/// * `token_in_already_in_router`: Indicates if the token in is already in the router at swap time
|
||||
#[derive(Clone)]
|
||||
pub struct TychoRouterEncoder {
|
||||
single_swap_strategy: SingleSwapStrategyEncoder,
|
||||
@@ -33,6 +39,9 @@ pub struct TychoRouterEncoder {
|
||||
split_swap_strategy: SplitSwapStrategyEncoder,
|
||||
native_address: Bytes,
|
||||
wrapped_address: Bytes,
|
||||
router_address: Bytes,
|
||||
token_in_already_in_router: bool,
|
||||
permit2: Option<Permit2>,
|
||||
}
|
||||
|
||||
impl TychoRouterEncoder {
|
||||
@@ -45,78 +54,111 @@ impl TychoRouterEncoder {
|
||||
) -> Result<Self, EncodingError> {
|
||||
let native_address = chain.native_token()?;
|
||||
let wrapped_address = chain.wrapped_token()?;
|
||||
let permit2 = if let Some(swapper_pk) = swapper_pk.clone() {
|
||||
Some(Permit2::new(swapper_pk, chain.clone())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(TychoRouterEncoder {
|
||||
single_swap_strategy: SingleSwapStrategyEncoder::new(
|
||||
chain.clone(),
|
||||
swap_encoder_registry.clone(),
|
||||
swapper_pk.clone(),
|
||||
permit2.is_some(),
|
||||
router_address.clone(),
|
||||
token_in_already_in_router,
|
||||
)?,
|
||||
sequential_swap_strategy: SequentialSwapStrategyEncoder::new(
|
||||
chain.clone(),
|
||||
swap_encoder_registry.clone(),
|
||||
swapper_pk.clone(),
|
||||
permit2.is_some(),
|
||||
router_address.clone(),
|
||||
token_in_already_in_router,
|
||||
)?,
|
||||
split_swap_strategy: SplitSwapStrategyEncoder::new(
|
||||
chain,
|
||||
swap_encoder_registry,
|
||||
None,
|
||||
permit2.is_some(),
|
||||
router_address.clone(),
|
||||
token_in_already_in_router,
|
||||
)?,
|
||||
native_address,
|
||||
wrapped_address,
|
||||
router_address,
|
||||
token_in_already_in_router,
|
||||
permit2,
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_solution(&self, solution: &Solution) -> Result<EncodedSolution, EncodingError> {
|
||||
self.validate_solution(solution)?;
|
||||
let protocols: HashSet<String> = solution
|
||||
.clone()
|
||||
.swaps
|
||||
.into_iter()
|
||||
.map(|swap| swap.component.protocol_system)
|
||||
.collect();
|
||||
|
||||
let mut encoded_solution = if (solution.swaps.len() == 1) ||
|
||||
(protocols.len() == 1 &&
|
||||
protocols
|
||||
.iter()
|
||||
.any(|p| GROUPABLE_PROTOCOLS.contains(&p.as_str())))
|
||||
{
|
||||
self.single_swap_strategy
|
||||
.encode_strategy(solution.clone())?
|
||||
} else if solution
|
||||
.swaps
|
||||
.iter()
|
||||
.all(|swap| swap.split == 0.0)
|
||||
{
|
||||
self.sequential_swap_strategy
|
||||
.encode_strategy(solution.clone())?
|
||||
} else {
|
||||
self.split_swap_strategy
|
||||
.encode_strategy(solution.clone())?
|
||||
};
|
||||
|
||||
if let Some(permit2) = self.permit2.clone() {
|
||||
let (permit, signature) = permit2.get_permit(
|
||||
&self.router_address,
|
||||
&solution.sender,
|
||||
&solution.given_token,
|
||||
&solution.given_amount,
|
||||
)?;
|
||||
encoded_solution.permit = Some(permit);
|
||||
encoded_solution.signature = Some(signature);
|
||||
}
|
||||
Ok(encoded_solution)
|
||||
}
|
||||
}
|
||||
|
||||
impl TychoEncoder for TychoRouterEncoder {
|
||||
fn encode_solutions(
|
||||
&self,
|
||||
solutions: Vec<Solution>,
|
||||
) -> Result<Vec<EncodedSolution>, EncodingError> {
|
||||
let mut result: Vec<EncodedSolution> = Vec::new();
|
||||
for solution in solutions.iter() {
|
||||
let encoded_solution = self.encode_solution(solution)?;
|
||||
result.push(encoded_solution);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn encode_calldata(&self, solutions: Vec<Solution>) -> Result<Vec<Transaction>, EncodingError> {
|
||||
let mut transactions: Vec<Transaction> = Vec::new();
|
||||
for solution in solutions.iter() {
|
||||
self.validate_solution(solution)?;
|
||||
let encoded_solution = self.encode_solution(solution)?;
|
||||
|
||||
let protocols: HashSet<String> = solution
|
||||
.clone()
|
||||
.swaps
|
||||
.into_iter()
|
||||
.map(|swap| swap.component.protocol_system)
|
||||
.collect();
|
||||
let transaction = encode_tycho_router_call(
|
||||
encoded_solution,
|
||||
solution,
|
||||
self.token_in_already_in_router,
|
||||
self.router_address.clone(),
|
||||
self.native_address.clone(),
|
||||
)?;
|
||||
|
||||
let (contract_interaction, target_address) = if (solution.swaps.len() == 1) ||
|
||||
(protocols.len() == 1 &&
|
||||
protocols
|
||||
.iter()
|
||||
.any(|p| GROUPABLE_PROTOCOLS.contains(&p.as_str())))
|
||||
{
|
||||
self.single_swap_strategy
|
||||
.encode_strategy(solution.clone())?
|
||||
} else if solution
|
||||
.swaps
|
||||
.iter()
|
||||
.all(|swap| swap.split == 0.0)
|
||||
{
|
||||
self.sequential_swap_strategy
|
||||
.encode_strategy(solution.clone())?
|
||||
} else {
|
||||
self.split_swap_strategy
|
||||
.encode_strategy(solution.clone())?
|
||||
};
|
||||
|
||||
let value = if solution.given_token == self.native_address {
|
||||
solution.given_amount.clone()
|
||||
} else {
|
||||
BigUint::ZERO
|
||||
};
|
||||
|
||||
transactions.push(Transaction {
|
||||
value,
|
||||
data: contract_interaction,
|
||||
to: target_address,
|
||||
});
|
||||
transactions.push(transaction);
|
||||
}
|
||||
Ok(transactions)
|
||||
}
|
||||
@@ -300,6 +342,14 @@ impl TychoExecutorEncoder {
|
||||
}
|
||||
|
||||
impl TychoEncoder for TychoExecutorEncoder {
|
||||
fn encode_solutions(
|
||||
&self,
|
||||
_solutions: Vec<Solution>,
|
||||
) -> Result<Vec<EncodedSolution>, EncodingError> {
|
||||
Err(EncodingError::NotImplementedError(
|
||||
"Encoding solutions for TychoExecutorEncoder is not implemented".to_string(),
|
||||
))
|
||||
}
|
||||
fn encode_calldata(&self, solutions: Vec<Solution>) -> Result<Vec<Transaction>, EncodingError> {
|
||||
let mut transactions: Vec<Transaction> = Vec::new();
|
||||
let solution = solutions
|
||||
|
||||
@@ -10,7 +10,7 @@ use alloy::{
|
||||
providers::{ProviderBuilder, RootProvider},
|
||||
transports::BoxTransport,
|
||||
};
|
||||
use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8};
|
||||
use alloy_primitives::{aliases::U24, Address, U256, U8};
|
||||
use alloy_sol_types::SolValue;
|
||||
use num_bigint::BigUint;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -40,28 +40,6 @@ pub fn biguint_to_u256(value: &BigUint) -> U256 {
|
||||
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> {
|
||||
let mut hasher = Keccak256::new();
|
||||
hasher.update(selector.as_bytes());
|
||||
let selector_bytes = &hasher.finalize()[..4];
|
||||
let mut call_data = selector_bytes.to_vec();
|
||||
// Remove extra prefix if present (32 bytes for dynamic data)
|
||||
// Alloy encoding is including a prefix for dynamic data indicating the offset or length
|
||||
// but at this point we don't want that
|
||||
if encoded_args.len() > 32 &&
|
||||
encoded_args[..32] ==
|
||||
[0u8; 31]
|
||||
.into_iter()
|
||||
.chain([32].to_vec())
|
||||
.collect::<Vec<u8>>()
|
||||
{
|
||||
encoded_args = encoded_args[32..].to_vec();
|
||||
}
|
||||
call_data.extend(encoded_args);
|
||||
call_data
|
||||
}
|
||||
|
||||
/// Converts a decimal to a `U24` value. The percentage is a `f64` value between 0 and 1.
|
||||
/// MAX_UINT24 corresponds to 100%.
|
||||
pub fn percentage_to_uint24(decimal: f64) -> U24 {
|
||||
@@ -116,12 +94,6 @@ pub fn pad_to_fixed_size<const N: usize>(input: &[u8]) -> Result<[u8; N], Encodi
|
||||
Ok(padded)
|
||||
}
|
||||
|
||||
/// Encodes a function selector to a fixed size array of 4 bytes.
|
||||
pub fn encode_function_selector(selector: &str) -> FixedBytes<4> {
|
||||
let hash = keccak256(selector.as_bytes());
|
||||
FixedBytes::<4>::from([hash[0], hash[1], hash[2], hash[3]])
|
||||
}
|
||||
|
||||
/// Extracts a static attribute from a swap.
|
||||
pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>, EncodingError> {
|
||||
Ok(swap
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use alloy_primitives::PrimitiveSignature as Signature;
|
||||
use hex;
|
||||
use num_bigint::BigUint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -8,6 +9,7 @@ use tycho_common::{
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::approvals::permit2::PermitSingle,
|
||||
serde_primitives::{biguint_string, biguint_string_option},
|
||||
};
|
||||
|
||||
@@ -96,6 +98,22 @@ pub struct Transaction {
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Represents a solution that has been encoded for execution.
|
||||
///
|
||||
/// # Fields
|
||||
/// * `swaps`: Encoded swaps to be executed.
|
||||
/// * `selector`: The selector of the function to be called.
|
||||
/// * `n_tokens`: Number of tokens in the swap.
|
||||
/// * `permit`: Optional permit for the swap (if permit2 is enabled).
|
||||
/// * `signature`: Optional signature for the swap (if permit2 is enabled).
|
||||
pub struct EncodedSolution {
|
||||
pub swaps: Vec<u8>,
|
||||
pub selector: String,
|
||||
pub n_tokens: usize,
|
||||
pub permit: Option<PermitSingle>,
|
||||
pub signature: Option<Signature>,
|
||||
}
|
||||
|
||||
/// Represents necessary attributes for encoding an order.
|
||||
///
|
||||
/// # Fields
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use tycho_common::Bytes;
|
||||
|
||||
use crate::encoding::{errors::EncodingError, models::Solution, swap_encoder::SwapEncoder};
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
models::{EncodedSolution, Solution},
|
||||
swap_encoder::SwapEncoder,
|
||||
};
|
||||
|
||||
/// A trait that defines how to encode a `Solution` for execution.
|
||||
pub trait StrategyEncoder {
|
||||
@@ -13,11 +15,8 @@ pub trait StrategyEncoder {
|
||||
/// path
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<(Vec<u8>, Bytes, Option<String>), EncodingError>` - A tuple containing:
|
||||
/// - The encoded data as bytes
|
||||
/// - The address of the contract to call (router or executor)
|
||||
/// - Optionally, the function selector to use when calling the contract
|
||||
fn encode_strategy(&self, solution: Solution) -> Result<(Vec<u8>, Bytes), EncodingError>;
|
||||
/// * `Result<EncodedSwaps, EncodingError>`
|
||||
fn encode_strategy(&self, solution: Solution) -> Result<EncodedSolution, EncodingError>;
|
||||
|
||||
/// Retrieves the swap encoder for a specific protocol system.
|
||||
///
|
||||
|
||||
@@ -1,20 +1,75 @@
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
models::{Solution, Transaction},
|
||||
models::{EncodedSolution, Solution, Transaction},
|
||||
};
|
||||
|
||||
/// A high-level encoder that converts solutions into executable transactions. Allows for modularity
|
||||
/// in the encoding process.
|
||||
/// A high-level interface for encoding solutions into Tycho-compatible transactions or raw call
|
||||
/// data.
|
||||
///
|
||||
/// This trait is designed to abstract the encoding logic required to prepare swap transactions for
|
||||
/// the Tycho Router. It enables modular and customizable construction of transactions, allowing
|
||||
/// integrators to maintain full control over the execution constraints.
|
||||
///
|
||||
/// # User Responsibility
|
||||
///
|
||||
/// While this trait provides convenience methods, it is **strongly recommended** that users favor
|
||||
/// [`encode_solutions`] over [`encode_calldata`]. This is because:
|
||||
///
|
||||
/// - `encode_solutions` returns raw [`EncodedSolution`] objects, which include Tycho’s swap path
|
||||
/// encoding, but leave **function argument encoding entirely in the user’s hands**.
|
||||
/// - The function arguments to the router (e.g., `minAmountOut`, `receiver`, `unwrap`, `permit2`,
|
||||
/// etc.) are used as **guardrails** to ensure safe on-chain execution.
|
||||
/// - Automatically constructing full transactions via [`encode_calldata`] can obscure these
|
||||
/// important safeguards and may result in unexpected behavior or vulnerability to MEV.
|
||||
///
|
||||
/// Tycho is only responsible for generating the internal swap plan. **The user must encode the
|
||||
/// outer function call arguments themselves** and verify that they enforce correct and secure
|
||||
/// behavior.
|
||||
pub trait TychoEncoder {
|
||||
/// Encodes solutions into transactions that can be executed by the Tycho router.
|
||||
/// Encodes a list of [`Solution`]s into [`EncodedSolution`]s, which include the selector and
|
||||
/// internal swap call data.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `solutions` - Vector of solutions to encode, each potentially using different setups (swap
|
||||
/// paths, protocols, wrapping, etc.)
|
||||
/// This method gives users maximum flexibility and control. It **does not** produce full
|
||||
/// transaction objects. Users are responsible for:
|
||||
/// - Constructing the full calldata using their own encoding logic.
|
||||
/// - Managing execution-critical parameters like `minAmountOut`.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<Transaction>, EncodingError>` - Vector of executable transactions
|
||||
/// A vector of encoded solutions, each containing:
|
||||
/// - The Tycho method selector
|
||||
/// - The encoded swap path
|
||||
/// - Additional metadata (e.g., permit2 information)
|
||||
///
|
||||
/// # Recommendation
|
||||
/// Use this method if you care about execution safety and want to avoid surprises.
|
||||
fn encode_solutions(
|
||||
&self,
|
||||
solutions: Vec<Solution>,
|
||||
) -> Result<Vec<EncodedSolution>, EncodingError>;
|
||||
|
||||
/// Encodes a list of [`Solution`]s directly into executable transactions for the Tycho router.
|
||||
///
|
||||
/// This method wraps around Tycho’s example encoding logic (see [`encode_tycho_router_call`])
|
||||
/// and should only be used for **prototyping or development**.
|
||||
///
|
||||
/// # Warning
|
||||
/// This implementation uses default logic to construct the outer calldata (e.g., for setting
|
||||
/// `minAmountOut`). This might not be optimal or safe for production use.
|
||||
///
|
||||
/// To ensure correctness, **users should implement their own encoding pipeline** using
|
||||
/// [`encode_solutions`].
|
||||
///
|
||||
/// # Returns
|
||||
/// A vector of fully constructed [`Transaction`]s that can be submitted to a node or bundler.
|
||||
fn encode_calldata(&self, solutions: Vec<Solution>) -> Result<Vec<Transaction>, EncodingError>;
|
||||
|
||||
/// Performs solution-level validation and sanity checks.
|
||||
///
|
||||
/// This function can be used to verify whether a proposed solution is structurally sound and
|
||||
/// ready for encoding.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(())` if the solution is valid.
|
||||
/// - `Err(EncodingError)` if the solution is malformed or unsupported.
|
||||
fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user