Merge pull request #275 from propeller-heads/testing-sdk/tnl/ENG-5068-ekubo-encoding-side-fix

fix: Don't PLE-encode for Ekubo
This commit is contained in:
Tamara
2025-10-07 12:54:36 +02:00
committed by GitHub
6 changed files with 974 additions and 730 deletions

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

View File

@@ -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_grouped_swap");
(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);
}
}

View File

@@ -37,12 +37,12 @@ pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
set
});
// The protocols here are a subset of the ones defined in IN_TRANSFER_REQUIRED_PROTOCOLS. The in
// transfer needs to be performed inside the callback logic. This means, the tokens can not be sent
// directly from the previous pool into a pool of this protocol. The tokens need to be sent to the
// router and only then transferred into the pool. This is the case for uniswap v3 because of the
// callback logic. The only way for this to work it would be to call the second swap during the
// callback of the first swap. This is currently not supported.
/// The protocols here are a subset of the ones defined in IN_TRANSFER_REQUIRED_PROTOCOLS. The in
/// transfer needs to be performed inside the callback logic. This means, the tokens can not be sent
/// directly from the previous pool into a pool of this protocol. The tokens need to be sent to the
/// router and only then transferred into the pool. This is the case for uniswap v3 because of the
/// callback logic. The only way for this to work it would be to call the second swap during the
/// callback of the first swap. This is currently not supported.
pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new();
set.insert("uniswap_v3");
@@ -53,3 +53,10 @@ pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
set.insert("vm:balancer_v3");
set
});
/// These groupable protocols use simple concatenation instead of PLE when forming swap groups.
pub static NON_PLE_ENCODED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new();
set.insert("ekubo_v2");
set
});

View File

@@ -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)?,

View File

@@ -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,
&eth(),
None,
)
.unwrap()
.data;
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_single_ekubo_grouped_swap", hex_calldata.as_str());
}
#[test]
fn test_single_encoding_strategy_curve() {
// UWU ──(curve 2 crypto pool)──> WETH