Merge pull request #33 from propeller-heads/encoder/hr/ENG-4082-ExecutorEncoder

feat: update ExecutorEncoder and test module with contract
This commit is contained in:
Harsh Vardhan Roy
2025-01-30 21:19:43 +05:30
committed by GitHub
8 changed files with 136 additions and 25 deletions

4
Cargo.lock generated
View File

@@ -2941,7 +2941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.10.5", "itertools 0.13.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.96",
@@ -3011,7 +3011,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View File

@@ -80,7 +80,7 @@ contract UniswapV2ExecutorTest is UniswapV2ExecutorExposed, Test, Constants {
assertGe(amountOut, 0); assertGe(amountOut, 0);
} }
function testSwap() public { function testSwapUniswapV2() public {
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891; uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false; bool zeroForOne = false;
@@ -93,4 +93,31 @@ contract UniswapV2ExecutorTest is UniswapV2ExecutorExposed, Test, Constants {
uint256 finalBalance = DAI.balanceOf(BOB); uint256 finalBalance = DAI.balanceOf(BOB);
assertGe(finalBalance, amountOut); assertGe(finalBalance, amountOut);
} }
function testSwapExecutorEncoderData() public {
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100";
(IERC20 tokenIn, address target, address receiver, bool zeroForOne) =
uniswapV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR);
assertEq(target, 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
assertEq(receiver, 0x0000000000000000000000000000000000000001);
assertEq(zeroForOne, false);
}
function testSwapExecutorSwap() public {
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00";
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
uint256 finalBalance = DAI.balanceOf(BOB);
assertGe(finalBalance, amountOut);
}
} }

View File

@@ -30,16 +30,17 @@ impl<S: StrategySelector, A: UserApprovalsManager> RouterEncoder<S, A> for EVMRo
&self, &self,
solutions: Vec<Solution>, solutions: Vec<Solution>,
) -> Result<Vec<Transaction>, EncodingError> { ) -> Result<Vec<Transaction>, EncodingError> {
let _approvals_calldata = self.handle_approvals(&solutions)?; // TODO: where should we append this? let _approvals_calldata = self.handle_approvals(&solutions)?;
let mut transactions: Vec<Transaction> = Vec::new(); let mut transactions: Vec<Transaction> = Vec::new();
for solution in solutions.iter() { for solution in solutions.iter() {
let exact_out = solution.exact_out; let exact_out = solution.exact_out;
let straight_to_pool = solution.straight_to_pool; let straight_to_pool = solution.direct_execution;
let strategy = self let strategy = self
.strategy_selector .strategy_selector
.select_strategy(solution); .select_strategy(solution);
let method_calldata = strategy.encode_strategy((*solution).clone())?; let (method_calldata, target_address) =
strategy.encode_strategy((*solution).clone())?;
let contract_interaction = if straight_to_pool { let contract_interaction = if straight_to_pool {
method_calldata method_calldata
@@ -52,7 +53,11 @@ impl<S: StrategySelector, A: UserApprovalsManager> RouterEncoder<S, A> for EVMRo
} else { } else {
BigUint::ZERO BigUint::ZERO
}; };
transactions.push(Transaction { value, data: contract_interaction }); transactions.push(Transaction {
value,
data: contract_interaction,
to: target_address,
});
} }
Ok(transactions) Ok(transactions)
} }

View File

@@ -1,5 +1,8 @@
use std::str::FromStr;
use alloy_primitives::Address; use alloy_primitives::Address;
use alloy_sol_types::SolValue; use alloy_sol_types::SolValue;
use tycho_core::Bytes;
use crate::encoding::{ use crate::encoding::{
errors::EncodingError, errors::EncodingError,
@@ -27,7 +30,7 @@ pub trait EVMStrategyEncoder: StrategyEncoder {
pub struct SplitSwapStrategyEncoder {} pub struct SplitSwapStrategyEncoder {}
impl EVMStrategyEncoder for SplitSwapStrategyEncoder {} impl EVMStrategyEncoder for SplitSwapStrategyEncoder {}
impl StrategyEncoder for SplitSwapStrategyEncoder { impl StrategyEncoder for SplitSwapStrategyEncoder {
fn encode_strategy(&self, _solution: Solution) -> Result<Vec<u8>, EncodingError> { fn encode_strategy(&self, _solution: Solution) -> Result<(Vec<u8>, Bytes), EncodingError> {
todo!() todo!()
} }
fn selector(&self, _exact_out: bool) -> &str { fn selector(&self, _exact_out: bool) -> &str {
@@ -37,10 +40,10 @@ 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 pool.
/// Only 1 solution with 1 swap is supported. /// Only 1 solution with 1 swap is supported.
pub struct StraightToPoolStrategyEncoder {} pub struct ExecutorStrategyEncoder {}
impl EVMStrategyEncoder for StraightToPoolStrategyEncoder {} impl EVMStrategyEncoder for ExecutorStrategyEncoder {}
impl StrategyEncoder for StraightToPoolStrategyEncoder { impl StrategyEncoder for ExecutorStrategyEncoder {
fn encode_strategy(&self, solution: Solution) -> Result<Vec<u8>, EncodingError> { fn encode_strategy(&self, solution: Solution) -> Result<(Vec<u8>, Bytes), EncodingError> {
if solution.router_address.is_none() { if solution.router_address.is_none() {
return Err(EncodingError::InvalidInput( return Err(EncodingError::InvalidInput(
"Router address is required for straight to pool solutions".to_string(), "Router address is required for straight to pool solutions".to_string(),
@@ -68,10 +71,85 @@ impl StrategyEncoder for StraightToPoolStrategyEncoder {
router_address, router_address,
}; };
let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context)?; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context)?;
// TODO: here we need to pass also the address of the executor to be used let executor_address = Bytes::from_str(swap_encoder.executor_address())
Ok(protocol_data) .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?;
Ok((protocol_data, executor_address))
} }
fn selector(&self, _exact_out: bool) -> &str { fn selector(&self, _exact_out: bool) -> &str {
unimplemented!(); "swap(uint256, bytes)"
}
}
#[cfg(test)]
mod tests {
use alloy::hex::encode;
use num_bigint::BigUint;
use tycho_core::{dto::ProtocolComponent, Bytes};
use super::*;
use crate::encoding::models::Swap;
#[test]
fn test_executor_strategy_encode() {
let encoder = ExecutorStrategyEncoder {};
let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f");
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
given_token: token_in,
given_amount: BigUint::from(1000000000000000000u64),
expected_amount: BigUint::from(1000000000000000000u64),
checked_token: token_out,
check_amount: None,
sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
// The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(),
swaps: vec![swap],
direct_execution: true,
router_address: Some(Bytes::zero(20)),
slippage: None,
native_action: None,
};
let (protocol_data, executor_address) = encoder
.encode_strategy(solution)
.unwrap();
let hex_protocol_data = encode(&protocol_data);
assert_eq!(
executor_address,
Bytes::from_str("0x5c2f5a71f67c01775180adc06909288b4c329308").unwrap()
);
assert_eq!(
hex_protocol_data,
String::from(concat!(
// in token
"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
// component id
"a478c2975ab1ea89e8196811f51a7b7ade33eb11",
// receiver
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
// zero for one
"00",
))
);
}
#[test]
fn test_selector() {
let encoder = ExecutorStrategyEncoder {};
assert_eq!(encoder.selector(false), "swap(uint256, bytes)");
} }
} }

View File

@@ -1,5 +1,5 @@
use crate::encoding::{ use crate::encoding::{
evm::strategy_encoder::encoder::{SplitSwapStrategyEncoder, StraightToPoolStrategyEncoder}, evm::strategy_encoder::encoder::{ExecutorStrategyEncoder, SplitSwapStrategyEncoder},
models::Solution, models::Solution,
strategy_encoder::{StrategyEncoder, StrategySelector}, strategy_encoder::{StrategyEncoder, StrategySelector},
}; };
@@ -8,8 +8,8 @@ pub struct EVMStrategySelector;
impl StrategySelector for EVMStrategySelector { impl StrategySelector for EVMStrategySelector {
fn select_strategy(&self, solution: &Solution) -> Box<dyn StrategyEncoder> { fn select_strategy(&self, solution: &Solution) -> Box<dyn StrategyEncoder> {
if solution.straight_to_pool { if solution.direct_execution {
Box::new(StraightToPoolStrategyEncoder {}) Box::new(ExecutorStrategyEncoder {})
} else { } else {
Box::new(SplitSwapStrategyEncoder {}) Box::new(SplitSwapStrategyEncoder {})
} }

View File

@@ -46,7 +46,6 @@ impl SwapEncoder for UniswapV2SwapEncoder {
component_id, component_id,
bytes_to_address(&encoding_context.receiver)?, bytes_to_address(&encoding_context.receiver)?,
zero_to_one, zero_to_one,
encoding_context.exact_out,
); );
Ok(args.abi_encode_packed()) Ok(args.abi_encode_packed())
@@ -211,8 +210,6 @@ mod tests {
"0000000000000000000000000000000000000001", "0000000000000000000000000000000000000001",
// zero for one // zero for one
"00", "00",
// exact out
"00",
)) ))
); );
} }

View File

@@ -11,7 +11,7 @@ pub struct Solution {
/// Amount of the given token. /// Amount of the given token.
pub given_amount: BigUint, pub given_amount: BigUint,
/// The token being bought (exact in) or sold (exact out). /// The token being bought (exact in) or sold (exact out).
checked_token: Bytes, pub checked_token: Bytes,
/// 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).
pub expected_amount: BigUint, pub expected_amount: BigUint,
/// Minimum amount to be checked for the solution to be valid. /// Minimum amount to be checked for the solution to be valid.
@@ -23,10 +23,10 @@ pub struct Solution {
pub receiver: Bytes, pub receiver: Bytes,
/// List of swaps to fulfill the solution. /// List of swaps to fulfill the solution.
pub swaps: Vec<Swap>, pub swaps: Vec<Swap>,
/// If set to true, the solution will be encoded to be sent directly to the SwapExecutor 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
/// transfers. /// transfers.
pub straight_to_pool: bool, pub direct_execution: bool,
// if not set, then the Propeller Router will be used // if not set, then the Propeller Router will be used
pub router_address: Option<Bytes>, pub router_address: Option<Bytes>,
// if set, it will be applied to check_amount // if set, it will be applied to check_amount
@@ -60,6 +60,8 @@ pub struct Transaction {
pub data: Vec<u8>, pub data: Vec<u8>,
// ETH value to be sent with the transaction. // ETH value to be sent with the transaction.
pub value: BigUint, pub value: BigUint,
// Address of the contract to call with the calldata
pub to: Bytes,
} }
#[allow(dead_code)] #[allow(dead_code)]

View File

@@ -1,8 +1,10 @@
use tycho_core::Bytes;
use crate::encoding::{errors::EncodingError, models::Solution}; use crate::encoding::{errors::EncodingError, models::Solution};
#[allow(dead_code)] #[allow(dead_code)]
pub trait StrategyEncoder { pub trait StrategyEncoder {
fn encode_strategy(&self, to_encode: Solution) -> Result<Vec<u8>, EncodingError>; fn encode_strategy(&self, to_encode: Solution) -> Result<(Vec<u8>, Bytes), EncodingError>;
fn selector(&self, exact_out: bool) -> &str; fn selector(&self, exact_out: bool) -> &str;
} }