fix: Don't PLE-encode for Ekubo
Our encoding uses PLE for subsequent swaps in a swap group. The EkuboExecutor assumes hard-coded hop length, which did not match the encoding side - leading to corrupted calldata in any swap after the first swap.
This commit is contained in:
1511
Cargo.lock
generated
1511
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -208,7 +208,11 @@ contract EkuboExecutorTest is Constants, TestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
|
||||
contract TychoRouterForEkuboTest is TychoRouterTestSetup {
|
||||
function getForkBlock() public view virtual override returns (uint256) {
|
||||
return 23518049;
|
||||
}
|
||||
|
||||
function testSingleEkuboIntegration() public {
|
||||
vm.stopPrank();
|
||||
|
||||
@@ -227,4 +231,28 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
|
||||
assertGe(balanceAfter - balanceBefore, 26173932);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testTwoEkuboIntegration() public {
|
||||
// Test multi-hop Ekubo swaps (grouped swap)
|
||||
//
|
||||
// USDE ──(EKUBO)──> USDC ──(EKUBO)──> USDT
|
||||
//
|
||||
deal(USDE_ADDR, ALICE, 1 ether);
|
||||
uint256 balanceBefore = IERC20(USDT_ADDR).balanceOf(ALICE);
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(USDE_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_single_ekubo_multi_hop");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(USDT_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 999804);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,3 +53,11 @@ pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
|
||||
set.insert("vm:balancer_v3");
|
||||
set
|
||||
});
|
||||
|
||||
/// These groupable protocols use simple concatenation when forming swap groups instead of PLE
|
||||
/// encoding for grouped protocol data.
|
||||
pub static NON_PLE_ENCODED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
let mut set = HashSet::new();
|
||||
set.insert("ekubo_v2");
|
||||
set
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ use tycho_common::{models::Chain, Bytes};
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::{
|
||||
constants::NON_PLE_ENCODED_PROTOCOLS,
|
||||
group_swaps::group_swaps,
|
||||
strategy_encoder::{
|
||||
strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator},
|
||||
@@ -139,8 +140,14 @@ impl StrategyEncoder for SingleSwapStrategyEncoder {
|
||||
}
|
||||
|
||||
if !grouped_protocol_data.is_empty() {
|
||||
if NON_PLE_ENCODED_PROTOCOLS.contains(grouped_swap.protocol_system.as_str()) {
|
||||
for protocol_data in grouped_protocol_data {
|
||||
initial_protocol_data.extend(protocol_data);
|
||||
}
|
||||
} else {
|
||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
||||
}
|
||||
}
|
||||
|
||||
let swap_data = self.encode_swap_header(
|
||||
Bytes::from_str(swap_encoder.executor_address())
|
||||
@@ -305,8 +312,14 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
|
||||
}
|
||||
|
||||
if !grouped_protocol_data.is_empty() {
|
||||
if NON_PLE_ENCODED_PROTOCOLS.contains(grouped_swap.protocol_system.as_str()) {
|
||||
for protocol_data in grouped_protocol_data {
|
||||
initial_protocol_data.extend(protocol_data);
|
||||
}
|
||||
} else {
|
||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
||||
}
|
||||
}
|
||||
|
||||
let swap_data = self.encode_swap_header(
|
||||
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
|
||||
@@ -511,8 +524,14 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
|
||||
}
|
||||
|
||||
if !grouped_protocol_data.is_empty() {
|
||||
if NON_PLE_ENCODED_PROTOCOLS.contains(grouped_swap.protocol_system.as_str()) {
|
||||
for protocol_data in grouped_protocol_data {
|
||||
initial_protocol_data.extend(protocol_data);
|
||||
}
|
||||
} else {
|
||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
||||
}
|
||||
}
|
||||
|
||||
let swap_data = self.encode_swap_header(
|
||||
get_token_position(&tokens, &grouped_swap.token_in)?,
|
||||
|
||||
@@ -406,6 +406,96 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_encoding_strategy_ekubo_grouped_swap() {
|
||||
// Test multi-hop Ekubo swap (grouped swaps)
|
||||
//
|
||||
// USDE ──(EKUBO)──> USDC ──(EKUBO)──> USDT
|
||||
|
||||
let usde = Bytes::from_str("0x4c9edd5852cd905f086c759e8383e09bff1e68b3").unwrap();
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
let usdt = Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap();
|
||||
|
||||
// First swap: USDE -> USDC
|
||||
let swap1 = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "a419f0ebb019eb85fdccd0200843752dd9cc31d0cb3127f3adb4ba37a092788f".to_string(),
|
||||
protocol_system: "ekubo_v2".to_string(),
|
||||
static_attributes: HashMap::from([
|
||||
("fee".to_string(), Bytes::from(922337203685478_u64)),
|
||||
("tick_spacing".to_string(), Bytes::from(100_u32)),
|
||||
(
|
||||
"extension".to_string(),
|
||||
Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usde.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// Second swap: USDC -> USDT
|
||||
let swap2 = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "ca5b3ef9770bb95940bd4e0bff5ead70a5973d904a8b370b52147820e61a2ff6".to_string(),
|
||||
protocol_system: "ekubo_v2".to_string(),
|
||||
static_attributes: HashMap::from([
|
||||
("fee".to_string(), Bytes::from(92233720368547_u64)),
|
||||
("tick_spacing".to_string(), Bytes::from(50_u32)),
|
||||
(
|
||||
"extension".to_string(),
|
||||
Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: usdt.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: usde,
|
||||
given_amount: BigUint::from_str("1_000000000000000000").unwrap(),
|
||||
checked_token: usdt,
|
||||
checked_amount: BigUint::from_str("1000").unwrap(),
|
||||
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
|
||||
receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
|
||||
swaps: vec![swap1, swap2],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.unwrap()[0]
|
||||
.clone();
|
||||
|
||||
let calldata = encode_tycho_router_call(
|
||||
eth_chain().id(),
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&UserTransferType::TransferFrom,
|
||||
ð(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_single_ekubo_multi_hop", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_encoding_strategy_curve() {
|
||||
// UWU ──(curve 2 crypto pool)──> WETH
|
||||
|
||||
Reference in New Issue
Block a user