From d9066d0a099d9c710b64a3ca39af729d83c91cf1 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 14 Apr 2025 23:08:05 -0400 Subject: [PATCH 1/5] feat: Use TokenTransfer optimization helper in Ekubo --- foundry/src/executors/EkuboExecutor.sol | 52 ++++++++++++------ foundry/test/TychoRouterIntegration.t.sol | 2 +- foundry/test/TychoRouterTestSetup.sol | 2 +- foundry/test/executors/EkuboExecutor.t.sol | 9 ++-- src/encoding/evm/constants.rs | 1 + .../evm/swap_encoder/swap_encoders.rs | 53 +++++++++++-------- 6 files changed, 75 insertions(+), 44 deletions(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index ad67205..e4de97b 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -11,21 +11,28 @@ import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol"; import {LibBytes} from "@solady/utils/LibBytes.sol"; import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol"; import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol"; +import {TokenTransfer} from "./TokenTransfer.sol"; -contract EkuboExecutor is IExecutor, ILocker, IPayer, ICallback { +contract EkuboExecutor is + IExecutor, + ILocker, + IPayer, + ICallback, + TokenTransfer +{ error EkuboExecutor__InvalidDataLength(); error EkuboExecutor__CoreOnly(); error EkuboExecutor__UnknownCallback(); ICore immutable core; - uint256 constant POOL_DATA_OFFSET = 56; + uint256 constant POOL_DATA_OFFSET = 77; uint256 constant HOP_BYTE_LEN = 52; bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256) bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address) - constructor(address _core) { + constructor(address _core, address _permit2) TokenTransfer(_permit2) { core = ICore(_core); } @@ -34,11 +41,16 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer, ICallback { payable returns (uint256 calculatedAmount) { - if (data.length < 92) revert EkuboExecutor__InvalidDataLength(); + if (data.length < 93) revert EkuboExecutor__InvalidDataLength(); // amountIn must be at most type(int128).MAX - calculatedAmount = - uint256(_lock(bytes.concat(bytes16(uint128(amountIn)), data))); + calculatedAmount = uint256( + _lock( + bytes.concat( + bytes16(uint128(amountIn)), bytes20(msg.sender), data + ) + ) + ); } function handleCallback(bytes calldata raw) @@ -113,9 +125,11 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer, ICallback { function _locked(bytes calldata swapData) internal returns (int128) { int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16]))); uint128 tokenInDebtAmount = uint128(nextAmountIn); + address sender = address(bytes20(swapData[16:36])); + uint8 transferType = uint8(swapData[36]); - address receiver = address(bytes20(swapData[16:36])); - address tokenIn = address(bytes20(swapData[36:POOL_DATA_OFFSET])); + address receiver = address(bytes20(swapData[37:57])); + address tokenIn = address(bytes20(swapData[57:77])); address nextTokenIn = tokenIn; @@ -149,14 +163,17 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer, ICallback { offset += HOP_BYTE_LEN; } - _pay(tokenIn, tokenInDebtAmount); - + _pay(tokenIn, tokenInDebtAmount, sender, transferType); core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn)); - return nextAmountIn; } - function _pay(address token, uint128 amount) internal { + function _pay( + address token, + uint128 amount, + address sender, + uint8 transferType + ) internal { address target = address(core); if (token == NATIVE_TOKEN_ADDRESS) { @@ -169,9 +186,11 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer, ICallback { mstore(free, shl(224, 0x0c11dedd)) mstore(add(free, 4), token) mstore(add(free, 36), shl(128, amount)) + mstore(add(free, 52), shl(96, sender)) + mstore(add(free, 72), shl(248, transferType)) - // if it failed, pass through revert - if iszero(call(gas(), target, 0, free, 52, 0, 0)) { + // 4 (selector) + 32 (token) + 16 (amount) + 20 (recipient) + 1 (transferType) = 73 + if iszero(call(gas(), target, 0, free, 132, 0, 0)) { returndatacopy(0, 0, returndatasize()) revert(0, returndatasize()) } @@ -182,8 +201,9 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer, ICallback { function _payCallback(bytes calldata payData) internal { address token = address(bytes20(payData[12:32])); // This arg is abi-encoded uint128 amount = uint128(bytes16(payData[32:48])); - - SafeTransferLib.safeTransfer(token, address(core), amount); + address sender = address(bytes20(payData[48:68])); + TransferType transferType = TransferType(uint8(payData[68])); + _transfer(token, sender, address(core), amount, transferType); } // To receive withdrawals from Core diff --git a/foundry/test/TychoRouterIntegration.t.sol b/foundry/test/TychoRouterIntegration.t.sol index 0f25861..053c780 100644 --- a/foundry/test/TychoRouterIntegration.t.sol +++ b/foundry/test/TychoRouterIntegration.t.sol @@ -159,7 +159,7 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); // Encoded solution generated using `test_split_encoding_strategy_ekubo` (bool success,) = address(tychoRouter).call{value: 1 ether}( - hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000003d7ebc40af7092e3f1c81f2e996cba5cae2090d7a4ad4f68d0b91cfd19687c881e50f3a00242828c0000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" + hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000078007600010000003d7ebc40af7092e3f1c81f2e996cba5cae2090d705a4ad4f68d0b91cfd19687c881e50f3a00242828c0000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c0000000000000000000000000000000000000000" ); uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 83b665f..c97cb56 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -107,7 +107,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper { factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS ); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); - ekuboExecutor = new EkuboExecutor(ekuboCore); + ekuboExecutor = new EkuboExecutor(ekuboCore, PERMIT2_ADDRESS); curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS); address[] memory executors = new address[](7); diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol index 0971b40..0d539ff 100644 --- a/foundry/test/executors/EkuboExecutor.t.sol +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.26; -import {EkuboExecutor} from "@src/executors/EkuboExecutor.sol"; +import {EkuboExecutor, TokenTransfer} from "@src/executors/EkuboExecutor.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Constants} from "../Constants.sol"; import {Test, console} from "forge-std/Test.sol"; @@ -26,7 +26,7 @@ contract EkuboExecutorTest is Test, Constants { deployCodeTo( "executors/EkuboExecutor.sol", - abi.encode(CORE_ADDRESS), + abi.encode(CORE_ADDRESS, PERMIT2_ADDRESS), EXECUTOR_ADDRESS ); executor = EkuboExecutor(payable(EXECUTOR_ADDRESS)); @@ -44,6 +44,7 @@ contract EkuboExecutorTest is Test, Constants { uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor)); bytes memory data = abi.encodePacked( + uint8(TokenTransfer.TransferType.TRANSFER), // transferType (transfer from executor to core) address(executor), // receiver NATIVE_TOKEN_ADDRESS, // tokenIn USDC_ADDR, // tokenOut @@ -80,6 +81,7 @@ contract EkuboExecutorTest is Test, Constants { uint256 ethBalanceBeforeExecutor = address(executor).balance; bytes memory data = abi.encodePacked( + uint8(TokenTransfer.TransferType.TRANSFER), // transferType (transfer from executor to core) address(executor), // receiver USDC_ADDR, // tokenIn NATIVE_TOKEN_ADDRESS, // tokenOut @@ -137,6 +139,7 @@ contract EkuboExecutorTest is Test, Constants { // Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi function testMultiHopSwap() public { bytes memory data = abi.encodePacked( + uint8(TokenTransfer.TransferType.TRANSFER), // transferType address(executor), // receiver NATIVE_TOKEN_ADDRESS, // tokenIn USDC_ADDR, // tokenOut of 1st swap @@ -152,7 +155,7 @@ contract EkuboExecutorTest is Test, Constants { // Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi function testMultiHopSwapIntegration() public { multiHopSwap( - hex"ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032" + hex"00ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032" ); } } diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index 4eadb35..3731047 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -24,6 +24,7 @@ pub static IN_TRANSFER_OPTIMIZABLE_PROTOCOLS: LazyLock> = let mut set = HashSet::new(); set.insert("uniswap_v2"); set.insert("uniswap_v3"); + set.insert("ekubo"); set }); diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 0ec212e..5581cc0 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -343,6 +343,7 @@ impl SwapEncoder for EkuboSwapEncoder { let mut encoded = vec![]; if encoding_context.group_token_in == swap.token_in { + encoded.extend((encoding_context.transfer_type as u8).to_be_bytes()); encoded.extend(bytes_to_address(&encoding_context.receiver)?); encoded.extend(bytes_to_address(&swap.token_in)?); } @@ -1032,15 +1033,18 @@ mod tests { assert_eq!( hex_swap, - RECEIVER.to_string() + - concat!( - // group token in - "0000000000000000000000000000000000000000", - // token out 1st swap - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // pool config 1st swap - "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", - ), + concat!( + // transfer type + "00", + // receiver + "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6", + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + ), ); } @@ -1107,22 +1111,25 @@ mod tests { format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap)); println!("{}", combined_hex); - assert_eq!( combined_hex, - RECEIVER.to_string() + - concat!( - // group token in - "0000000000000000000000000000000000000000", - // token out 1st swap - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // pool config 1st swap - "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", - // token out 2nd swap - "dac17f958d2ee523a2206206994597c13d831ec7", - // pool config 2nd swap - "00000000000000000000000000000000000000000001a36e2eb1c43200000032", - ), + // transfer type + concat!( + // transfer type + "00", + // receiver + "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6", + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + // token out 2nd swap + "dac17f958d2ee523a2206206994597c13d831ec7", + // pool config 2nd swap + "00000000000000000000000000000000000000000001a36e2eb1c43200000032", + ), ); } } From be65c890bb18440ab22f442e5fea3d242300e5e2 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 15 Apr 2025 14:28:30 -0400 Subject: [PATCH 2/5] fix: Calldata size for Ekubo pay callback - And doc clarification --- foundry/src/executors/EkuboExecutor.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index e4de97b..25d40ab 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -189,8 +189,8 @@ contract EkuboExecutor is mstore(add(free, 52), shl(96, sender)) mstore(add(free, 72), shl(248, transferType)) - // 4 (selector) + 32 (token) + 16 (amount) + 20 (recipient) + 1 (transferType) = 73 - if iszero(call(gas(), target, 0, free, 132, 0, 0)) { + // 4 (selector) + 32 (token) + 16 (amount) + 20 (sender) + 1 (transferType) = 73 + if iszero(call(gas(), target, 0, free, 73, 0, 0)) { returndatacopy(0, 0, returndatasize()) revert(0, returndatasize()) } From cf0300dd72f83e9e39bad62223f4b327760ccf22 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 15 Apr 2025 14:51:40 -0400 Subject: [PATCH 3/5] fix: TransferType renaming after rebase --- foundry/test/executors/EkuboExecutor.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol index 0d539ff..9a6a9be 100644 --- a/foundry/test/executors/EkuboExecutor.t.sol +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -44,7 +44,7 @@ contract EkuboExecutorTest is Test, Constants { uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor)); bytes memory data = abi.encodePacked( - uint8(TokenTransfer.TransferType.TRANSFER), // transferType (transfer from executor to core) + uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType (transfer from executor to core) address(executor), // receiver NATIVE_TOKEN_ADDRESS, // tokenIn USDC_ADDR, // tokenOut @@ -81,7 +81,7 @@ contract EkuboExecutorTest is Test, Constants { uint256 ethBalanceBeforeExecutor = address(executor).balance; bytes memory data = abi.encodePacked( - uint8(TokenTransfer.TransferType.TRANSFER), // transferType (transfer from executor to core) + uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType (transfer from executor to core) address(executor), // receiver USDC_ADDR, // tokenIn NATIVE_TOKEN_ADDRESS, // tokenOut @@ -139,7 +139,7 @@ contract EkuboExecutorTest is Test, Constants { // Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi function testMultiHopSwap() public { bytes memory data = abi.encodePacked( - uint8(TokenTransfer.TransferType.TRANSFER), // transferType + uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType address(executor), // receiver NATIVE_TOKEN_ADDRESS, // tokenIn USDC_ADDR, // tokenOut of 1st swap From 11886b3ac1438eed63841edf697fe9b122a75483 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 15 Apr 2025 16:13:40 -0400 Subject: [PATCH 4/5] fix: properly add ekubo_v2 to constants Mistakenly added "ekubo" --- src/encoding/evm/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index 3731047..ba0bce7 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -24,7 +24,7 @@ pub static IN_TRANSFER_OPTIMIZABLE_PROTOCOLS: LazyLock> = let mut set = HashSet::new(); set.insert("uniswap_v2"); set.insert("uniswap_v3"); - set.insert("ekubo"); + set.insert("ekubo_v2"); set }); From 860bba4b7a255a33ba4f70f8c6d4a8358e2b2f6a Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 15 Apr 2025 16:14:22 -0400 Subject: [PATCH 5/5] fix: Add slither ignore for loop call - This started being an issue only with slither upgrade in the CI. We accept the risk here, and have for some time. --- foundry/src/Dispatcher.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry/src/Dispatcher.sol b/foundry/src/Dispatcher.sol index 6daaaa8..483b3e6 100644 --- a/foundry/src/Dispatcher.sol +++ b/foundry/src/Dispatcher.sol @@ -66,7 +66,7 @@ contract Dispatcher { tstore(0, executor) } - // slither-disable-next-line controlled-delegatecall,low-level-calls + // slither-disable-next-line controlled-delegatecall,low-level-calls,calls-loop (bool success, bytes memory result) = executor.delegatecall( abi.encodeWithSelector(IExecutor.swap.selector, amount, data) );