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 {
|
function testSingleEkuboIntegration() public {
|
||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
|
|
||||||
@@ -227,4 +231,28 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
|
|||||||
assertGe(balanceAfter - balanceBefore, 26173932);
|
assertGe(balanceAfter - balanceBefore, 26173932);
|
||||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
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.insert("vm:balancer_v3");
|
||||||
set
|
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::{
|
use crate::encoding::{
|
||||||
errors::EncodingError,
|
errors::EncodingError,
|
||||||
evm::{
|
evm::{
|
||||||
|
constants::NON_PLE_ENCODED_PROTOCOLS,
|
||||||
group_swaps::group_swaps,
|
group_swaps::group_swaps,
|
||||||
strategy_encoder::{
|
strategy_encoder::{
|
||||||
strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator},
|
strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator},
|
||||||
@@ -139,7 +140,13 @@ impl StrategyEncoder for SingleSwapStrategyEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !grouped_protocol_data.is_empty() {
|
if !grouped_protocol_data.is_empty() {
|
||||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
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(
|
let swap_data = self.encode_swap_header(
|
||||||
@@ -305,7 +312,13 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !grouped_protocol_data.is_empty() {
|
if !grouped_protocol_data.is_empty() {
|
||||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
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(
|
let swap_data = self.encode_swap_header(
|
||||||
@@ -511,7 +524,13 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !grouped_protocol_data.is_empty() {
|
if !grouped_protocol_data.is_empty() {
|
||||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
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(
|
let swap_data = self.encode_swap_header(
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn test_single_encoding_strategy_curve() {
|
fn test_single_encoding_strategy_curve() {
|
||||||
// UWU ──(curve 2 crypto pool)──> WETH
|
// UWU ──(curve 2 crypto pool)──> WETH
|
||||||
|
|||||||
Reference in New Issue
Block a user