From 3c982c5824602c3d2ddcd1990b4da380a723520b Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Thu, 20 Mar 2025 09:58:40 +0100 Subject: [PATCH 01/11] Ekubo integration --- .gitmodules | 3 + foundry/lib/ekubo/interfaces/ICore.sol | 16 ++ .../lib/ekubo/interfaces/IFlashAccountant.sol | 16 ++ foundry/lib/ekubo/math/constants.sol | 5 + foundry/lib/ekubo/types/poolKey.sol | 12 + foundry/lib/ekubo/types/sqrtRatio.sol | 9 + foundry/lib/solady | 1 + foundry/remappings.txt | 4 +- foundry/src/executors/EkuboExecutor.sol | 210 ++++++++++++++++++ foundry/test/executors/EkuboExecutor.t.sol | 121 ++++++++++ src/encoding/evm/constants.rs | 1 + .../evm/swap_encoder/swap_encoders.rs | 60 ++++- 12 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 foundry/lib/ekubo/interfaces/ICore.sol create mode 100644 foundry/lib/ekubo/interfaces/IFlashAccountant.sol create mode 100644 foundry/lib/ekubo/math/constants.sol create mode 100644 foundry/lib/ekubo/types/poolKey.sol create mode 100644 foundry/lib/ekubo/types/sqrtRatio.sol create mode 160000 foundry/lib/solady create mode 100644 foundry/src/executors/EkuboExecutor.sol create mode 100644 foundry/test/executors/EkuboExecutor.t.sol diff --git a/.gitmodules b/.gitmodules index eacb977..d9919b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "foundry/lib/v4-periphery"] path = foundry/lib/v4-periphery url = https://github.com/Uniswap/v4-periphery +[submodule "foundry/lib/solady"] + path = foundry/lib/solady + url = https://github.com/vectorized/solady diff --git a/foundry/lib/ekubo/interfaces/ICore.sol b/foundry/lib/ekubo/interfaces/ICore.sol new file mode 100644 index 0000000..2bc6b3d --- /dev/null +++ b/foundry/lib/ekubo/interfaces/ICore.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {IFlashAccountant} from "./IFlashAccountant.sol"; +import {PoolKey} from "../types/poolKey.sol"; +import {SqrtRatio} from "../types/sqrtRatio.sol"; + +interface ICore is IFlashAccountant { + function swap_611415377( + PoolKey memory poolKey, + int128 amount, + bool isToken1, + SqrtRatio sqrtRatioLimit, + uint256 skipAhead + ) external payable returns (int128 delta0, int128 delta1); +} \ No newline at end of file diff --git a/foundry/lib/ekubo/interfaces/IFlashAccountant.sol b/foundry/lib/ekubo/interfaces/IFlashAccountant.sol new file mode 100644 index 0000000..2524078 --- /dev/null +++ b/foundry/lib/ekubo/interfaces/IFlashAccountant.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +interface ILocker { + function locked(uint256 id) external; +} + +interface IPayer { + function payCallback(uint256 id, address token) external; +} + +interface IFlashAccountant { + // Withdraws a token amount from the accountant to the given recipient. + // The contract must be locked, as it tracks the withdrawn amount against the current locker's delta. + function withdraw(address token, address recipient, uint128 amount) external; +} diff --git a/foundry/lib/ekubo/math/constants.sol b/foundry/lib/ekubo/math/constants.sol new file mode 100644 index 0000000..cd08e3b --- /dev/null +++ b/foundry/lib/ekubo/math/constants.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +// We use this address to represent the native token within the protocol +address constant NATIVE_TOKEN_ADDRESS = address(0); diff --git a/foundry/lib/ekubo/types/poolKey.sol b/foundry/lib/ekubo/types/poolKey.sol new file mode 100644 index 0000000..b0301e2 --- /dev/null +++ b/foundry/lib/ekubo/types/poolKey.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +// address (20 bytes) | fee (8 bytes) | tickSpacing (4 bytes) +type Config is bytes32; + +// Each pool has its own state associated with this key +struct PoolKey { + address token0; + address token1; + Config config; +} diff --git a/foundry/lib/ekubo/types/sqrtRatio.sol b/foundry/lib/ekubo/types/sqrtRatio.sol new file mode 100644 index 0000000..27e5856 --- /dev/null +++ b/foundry/lib/ekubo/types/sqrtRatio.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +type SqrtRatio is uint96; + +uint96 constant MIN_SQRT_RATIO_RAW = 4611797791050542631; +SqrtRatio constant MIN_SQRT_RATIO = SqrtRatio.wrap(MIN_SQRT_RATIO_RAW); +uint96 constant MAX_SQRT_RATIO_RAW = 79227682466138141934206691491; +SqrtRatio constant MAX_SQRT_RATIO = SqrtRatio.wrap(MAX_SQRT_RATIO_RAW); diff --git a/foundry/lib/solady b/foundry/lib/solady new file mode 160000 index 0000000..c9e079c --- /dev/null +++ b/foundry/lib/solady @@ -0,0 +1 @@ +Subproject commit c9e079c0ca836dcc52777a1fa7227ef28e3537b3 diff --git a/foundry/remappings.txt b/foundry/remappings.txt index 04240ad..d10f28f 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -7,4 +7,6 @@ @uniswap/v3-updated/=lib/v3-updated/ @uniswap/v3-core/=lib/v3-core/ @uniswap/v4-core/=lib/v4-core/ -@uniswap/v4-periphery/=lib/v4-periphery/ \ No newline at end of file +@uniswap/v4-periphery/=lib/v4-periphery/ +@solady=lib/solady/src/ +@ekubo=lib/ekubo/ diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol new file mode 100644 index 0000000..b934f8c --- /dev/null +++ b/foundry/src/executors/EkuboExecutor.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IExecutor} from "@interfaces/IExecutor.sol"; +import {ICallback} from "@interfaces/ICallback.sol"; +import {ICore} from "@ekubo/interfaces/ICore.sol"; +import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol"; +import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol"; +import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol"; +import {LibBytes} from "@solady/utils/LibBytes.sol"; +import {Config, PoolKey} from "@ekubo/types/poolKey.sol"; +import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol"; + +contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { + error EkuboExecutor__InvalidDataLength(); + error EkuboExecutor__CoreOnly(); + error EkuboExecutor__UnknownCallback(); + + ICore immutable core; + + uint256 constant POOL_DATA_OFFSET = 56; + uint256 constant HOP_BYTE_LEN = 52; + + constructor(ICore _core) { + core = _core; + } + + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { + if (data.length < 92) revert EkuboExecutor__InvalidDataLength(); + + uint256 tokenOutOffset = data.length - HOP_BYTE_LEN; + address tokenOut = address(bytes20(LibBytes.loadCalldata(data, tokenOutOffset))); + + uint256 tokenOutBalanceBefore = _balanceOf(tokenOut); + + // amountIn must be at most type(int128).MAX + _lock(bytes.concat(bytes16(uint128(amountIn)), data)); + + uint256 tokenOutBalanceAfter = _balanceOf(tokenOut); + + // It would be better if we could somehow pass back the swapped amount from the lock but the interface doesn't offer that capability. + // Note that the current approach also prevents arbs that return less than their input because of arithmetic underflow. + calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; + } + + // We can't use the return value here since it won't get propagated (see Dispatcher.sol:_handleCallback) + function handleCallback(bytes calldata raw) + external + returns (bytes memory) + { + verifyCallback(raw); + + // Without selector and locker id + bytes calldata stripped = raw[36:]; + + bytes4 selector = bytes4(raw[:4]); + + if (selector == 0xb45a3c0e) { // Selector of locked(uint256) + _locked(stripped); + } else if (selector == 0x599d0714) { // Selector of payCallback(uint256,address) + _payCallback(stripped); + } else { + revert EkuboExecutor__UnknownCallback(); + } + + return ""; + } + + function verifyCallback(bytes calldata) public view coreOnly {} + + function locked(uint256) external coreOnly { + // Without selector and locker id + _locked(msg.data[36:]); + } + + function payCallback(uint256, address token) external coreOnly { + uint128 amount = uint128(bytes16(LibBytes.loadCalldata(msg.data, 68))); + + SafeTransferLib.safeTransfer(token, address(core), amount); + } + + function _balanceOf(address token) internal view returns (uint256 balance) { + balance = token == NATIVE_TOKEN_ADDRESS + ? address(this).balance + : IERC20(token).balanceOf(address(this)); + } + + function _lock(bytes memory data) internal { + address target = address(core); + + assembly ("memory-safe") { + let args := mload(0x40) + + // Selector of lock() + mstore(args, shl(224, 0xf83d08ba)) + + // We only copy the data, not the length, because the length is read from the calldata size + let len := mload(data) + mcopy(add(args, 4), add(data, 32), len) + + // If the call failed, pass through the revert + if iszero(call(gas(), target, 0, args, add(len, 36), 0, 0)) { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + + function _locked(bytes calldata swapData) internal { + // For partial swaps this is not equivalent to the given input amount + uint128 tokenInDebtAmount; + + int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16]))); + + address receiver = address(bytes20(swapData[16:36])); + address tokenIn = address(bytes20(swapData[36:POOL_DATA_OFFSET])); + + address nextTokenIn = tokenIn; + + uint256 hopsLength = (swapData.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN; + + uint256 offset = POOL_DATA_OFFSET; + + for (uint256 i = 0; i < hopsLength; i++) { + address nextTokenOut = address(bytes20(LibBytes.loadCalldata(swapData, offset))); + Config poolConfig = Config.wrap(LibBytes.loadCalldata(swapData, offset + 20)); + + ( + address token0, + address token1, + bool isToken1 + ) = nextTokenIn > nextTokenOut ? ( + nextTokenOut, + nextTokenIn, + true + ) : ( + nextTokenIn, + nextTokenOut, + false + ); + + (int128 delta0, int128 delta1) = core.swap_611415377( + PoolKey(token0, token1, poolConfig), + nextAmountIn, + isToken1, + isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO, + 0 + ); + + if (tokenInDebtAmount == 0) { + tokenInDebtAmount = uint128(isToken1 ? delta1 : delta0); + } + + nextTokenIn = nextTokenOut; + nextAmountIn = -(isToken1 ? delta0 : delta1); + + offset += HOP_BYTE_LEN; + } + + _pay(tokenIn, tokenInDebtAmount); + + core.withdraw( + nextTokenIn, + receiver, + uint128(nextAmountIn) + ); + } + + function _pay(address token, uint128 amount) internal { + address target = address(core); + + if (token == NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeTransferETH(target, amount); + } else { + assembly ("memory-safe") { + let free := mload(0x40) + // selector of pay(address) + mstore(free, shl(224, 0x0c11dedd)) + mstore(add(free, 4), token) + mstore(add(free, 36), shl(128, amount)) + + // if it failed, pass through revert + if iszero(call(gas(), target, 0, free, 52, 0, 0)) { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + } + + function _payCallback(bytes calldata payData) internal { + address token = address(bytes20(payData[0:20])); + uint128 amount = uint128(bytes16(payData[20:36])); + + SafeTransferLib.safeTransfer(address(core), token, amount); + } + + // To receive withdrawals from Core + receive() external payable {} + + modifier coreOnly() { + if (msg.sender != address(core)) revert EkuboExecutor__CoreOnly(); + _; + } +} diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol new file mode 100644 index 0000000..c323bdb --- /dev/null +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import {EkuboExecutor} 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"; +import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol"; +import {ICore} from "@ekubo/interfaces/ICore.sol"; + +contract EkuboExecutorTest is Test, Constants { + EkuboExecutor executor; + + IERC20 USDC = IERC20(USDC_ADDR); + IERC20 USDT = IERC20(USDT_ADDR); + + address constant CORE_ADDRESS = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444; + + bytes32 constant ORACLE_CONFIG = 0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000; + + function setUp() public { + uint256 forkBlock = 22082754; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + executor = new EkuboExecutor(ICore(payable(CORE_ADDRESS))); + } + + function testSingleSwapEth() public { + uint256 amountIn = 1 ether; + + deal(address(executor), amountIn); + + uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance; + uint256 ethBalanceBeforeExecutor = address(executor).balance; + + uint256 usdcBalanceBeforeCore = USDC.balanceOf(CORE_ADDRESS); + uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor)); + + bytes memory data = abi.encodePacked( + address(executor), // receiver + NATIVE_TOKEN_ADDRESS, // tokenIn + USDC_ADDR, // tokenOut + ORACLE_CONFIG // poolConfig + ); + + uint256 gasBefore = gasleft(); + uint256 amountOut = executor.swap(amountIn, data); + console.log(gasBefore - gasleft()); + + console.log(amountOut); + + assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); + assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); + + assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore - amountOut); + assertEq(USDC.balanceOf(address(executor)), usdcBalanceBeforeExecutor + amountOut); + } + + function testSingleSwapERC20() public { + uint256 amountIn = 1_000_000_000; + + deal(USDC_ADDR, address(executor), amountIn); + + uint256 usdcBalanceBeforeCore = USDC.balanceOf(CORE_ADDRESS); + uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor)); + + uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance; + uint256 ethBalanceBeforeExecutor = address(executor).balance; + + bytes memory data = abi.encodePacked( + address(executor), // receiver + USDC_ADDR, // tokenIn + NATIVE_TOKEN_ADDRESS, // tokenOut + ORACLE_CONFIG // config + ); + + uint256 gasBefore = gasleft(); + uint256 amountOut = executor.swap(amountIn, data); + console.log(gasBefore - gasleft()); + + console.log(amountOut); + + assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn); + assertEq(USDC.balanceOf(address(executor)), usdcBalanceBeforeExecutor - amountIn); + + assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut); + assertEq(address(executor).balance, ethBalanceBeforeExecutor + amountOut); + } + + function testMultiHopSwap() public { + uint256 amountIn = 1 ether; + + deal(address(executor), amountIn); + + uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance; + uint256 ethBalanceBeforeExecutor = address(executor).balance; + + uint256 usdtBalanceBeforeCore = USDT.balanceOf(CORE_ADDRESS); + uint256 usdtBalanceBeforeExecutor = USDT.balanceOf(address(executor)); + + bytes memory data = abi.encodePacked( + address(executor), // receiver + NATIVE_TOKEN_ADDRESS, // tokenIn + USDC_ADDR, // tokenOut of 1st swap + ORACLE_CONFIG, // config of 1st swap + USDT_ADDR, // tokenOut of 2nd swap + bytes32(0x00000000000000000000000000000000000000000001a36e2eb1c43200000032) // config of 2nd swap (0.0025% fee & 0.005% base pool) + ); + + uint256 gasBefore = gasleft(); + uint256 amountOut = executor.swap(amountIn, data); + console.log(gasBefore - gasleft()); + + console.log(amountOut); + + assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); + assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); + + assertEq(USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut); + assertEq(USDT.balanceOf(address(executor)), usdtBalanceBeforeExecutor + amountOut); + } +} diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index 8f93215..d742100 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -11,5 +11,6 @@ pub static GROUPABLE_PROTOCOLS: LazyLock> = LazyLock::new( let mut set = HashSet::new(); set.insert("uniswap_v4"); set.insert("balancer_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 473b250..f5c2752 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{ops::Add, str::FromStr}; use alloy_primitives::{Address, Bytes as AlloyBytes}; use alloy_sol_types::SolValue; @@ -258,6 +258,64 @@ impl SwapEncoder for BalancerV2SwapEncoder { } } +/// Encodes a swap on an Ekubo pool through the given executor address. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EkuboEncoder { + executor_address: String, +} + +impl SwapEncoder for EkuboEncoder { + fn new(executor_address: String) -> Self { + Self { executor_address } + } + + // TODO Exact out + fn encode_swap( + &self, + swap: Swap, + encoding_context: EncodingContext, + ) -> Result, EncodingError> { + let fee = u64::from_be_bytes(get_static_attribute(&swap, "fee")? + .try_into() + .map_err(|_| EncodingError::FatalError("fee should be an u64".to_string()))? + ); + + let tick_spacing = u32::from_be_bytes(get_static_attribute(&swap, "tick_spacing")? + .try_into() + .map_err(|_| EncodingError::FatalError("tick_spacing should be an u32".to_string()))? + ); + + let extension: Address = get_static_attribute(&swap, "fee")? + .as_slice() + .try_into() + .map_err(|_| EncodingError::FatalError("extension should be an address".to_string()))?; + + let mut encoded = vec![]; + + // TODO What if the token_in appears at the start of a route and later on again? + if encoding_context.group_token_in == swap.token_in { + encoded.extend(bytes_to_address(&encoding_context.receiver)?); + encoded.extend(bytes_to_address(&swap.token_in)?); + } + + encoded.extend(bytes_to_address(&swap.token_out)?); + encoded.extend((extension, fee, tick_spacing).abi_encode_packed()); + + Ok(encoded) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; From e954bc7e59cfe5400fded1de2e31ebc00aac6b3e Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:26:52 +0100 Subject: [PATCH 02/11] Fix attribute name --- src/encoding/evm/swap_encoder/swap_encoders.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index f5c2752..ae3a3c2 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,4 +1,4 @@ -use std::{ops::Add, str::FromStr}; +use std::str::FromStr; use alloy_primitives::{Address, Bytes as AlloyBytes}; use alloy_sol_types::SolValue; @@ -288,7 +288,7 @@ impl SwapEncoder for EkuboEncoder { .map_err(|_| EncodingError::FatalError("tick_spacing should be an u32".to_string()))? ); - let extension: Address = get_static_attribute(&swap, "fee")? + let extension: Address = get_static_attribute(&swap, "extension")? .as_slice() .try_into() .map_err(|_| EncodingError::FatalError("extension should be an address".to_string()))?; From af5742550f926b663dbe519aba674239a976015c Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:27:06 +0100 Subject: [PATCH 03/11] Add encoder tests --- .../evm/swap_encoder/swap_encoders.rs | 137 +++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index ae3a3c2..4fe9dac 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -263,11 +263,11 @@ impl SwapEncoder for BalancerV2SwapEncoder { /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct EkuboEncoder { +pub struct EkuboSwapEncoder { executor_address: String, } -impl SwapEncoder for EkuboEncoder { +impl SwapEncoder for EkuboSwapEncoder { fn new(executor_address: String) -> Self { Self { executor_address } } @@ -694,4 +694,137 @@ mod tests { )) ); } + + mod ekubo { + use super::*; + + #[test] + fn test_encode_swap_simple() { + let token_in = Bytes::from(Address::ZERO.as_slice()); + let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + + let static_attributes = HashMap::from([ + ("fee".to_string(), Bytes::from(0_u64)), + ("tick_spacing".to_string(), Bytes::from(0_u32)), + ("extension".to_string(), Bytes::from("0x51d02a5948496a67827242eabc5725531342527c")), // Oracle + ]); + + let component = ProtocolComponent { + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let encoding_context = EncodingContext { + receiver: "0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6".into(), // Random address + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + exact_out: false, + router_address: Bytes::default(), + }; + + let encoder = EkuboSwapEncoder::new(String::default()); + + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // receiver + "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6", + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + )) + ); + } + + #[test] + fn test_encode_swap_multi() { + let group_token_in = Bytes::from(Address::ZERO.as_slice()); + let group_token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + let intermediary_token = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + + let encoder = EkuboSwapEncoder::new(String::default()); + + let encoding_context = EncodingContext { + receiver: "0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6".into(), // Random address + group_token_in: group_token_in.clone(), + group_token_out: group_token_out.clone(), + exact_out: false, + router_address: Bytes::default(), + }; + + let first_swap = Swap { + component: ProtocolComponent { + static_attributes: HashMap::from([ + ("fee".to_string(), Bytes::from(0_u64)), + ("tick_spacing".to_string(), Bytes::from(0_u32)), + ("extension".to_string(), Bytes::from("0x51d02a5948496a67827242eabc5725531342527c")), // Oracle + ]), + ..Default::default() + }, + token_in: group_token_in.clone(), + token_out: intermediary_token.clone(), + split: 0f64, + }; + + let second_swap = Swap { + component: ProtocolComponent { + // 0.0025% fee & 0.005% base pool + static_attributes: HashMap::from([ + ("fee".to_string(), Bytes::from(461168601842738_u64)), + ("tick_spacing".to_string(), Bytes::from(50_u32)), + ("extension".to_string(), Bytes::zero(20)), + ]), + ..Default::default() + }, + token_in: intermediary_token.clone(), + token_out: group_token_out.clone(), + split: 0f64, + }; + + let first_encoded_swap = encoder + .encode_swap(first_swap, encoding_context.clone()) + .unwrap(); + + let second_encoded_swap = encoder + .encode_swap(second_swap, encoding_context) + .unwrap(); + + let combined_hex = format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap)); + + assert_eq!( + combined_hex, + String::from(concat!( + // 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 ce7362ef24ee9b26fa0bc4c1df95e3333bde0f49 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:17:20 +0100 Subject: [PATCH 04/11] Error on unsupported options --- src/encoding/evm/swap_encoder/swap_encoders.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 4fe9dac..68ba13a 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use alloy_primitives::{Address, Bytes as AlloyBytes}; use alloy_sol_types::SolValue; +use num_traits::Zero; use tycho_core::Bytes; use crate::encoding::{ @@ -272,12 +273,19 @@ impl SwapEncoder for EkuboSwapEncoder { Self { executor_address } } - // TODO Exact out fn encode_swap( &self, swap: Swap, encoding_context: EncodingContext, ) -> Result, EncodingError> { + if !swap.split.is_zero() { + return Err(EncodingError::InvalidInput("splits not implemented".to_string())); + } + + if encoding_context.exact_out { + return Err(EncodingError::InvalidInput("exact out swaps not implemented".to_string())); + } + let fee = u64::from_be_bytes(get_static_attribute(&swap, "fee")? .try_into() .map_err(|_| EncodingError::FatalError("fee should be an u64".to_string()))? @@ -295,7 +303,6 @@ impl SwapEncoder for EkuboSwapEncoder { let mut encoded = vec![]; - // TODO What if the token_in appears at the start of a route and later on again? if encoding_context.group_token_in == swap.token_in { encoded.extend(bytes_to_address(&encoding_context.receiver)?); encoded.extend(bytes_to_address(&swap.token_in)?); From 9e6af4b99f5ad98f0fa6398f6508ec16a5610757 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:18:15 +0100 Subject: [PATCH 05/11] cargo fmt & clippy --- .../evm/swap_encoder/swap_encoders.rs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 68ba13a..baccad3 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -286,14 +286,18 @@ impl SwapEncoder for EkuboSwapEncoder { return Err(EncodingError::InvalidInput("exact out swaps not implemented".to_string())); } - let fee = u64::from_be_bytes(get_static_attribute(&swap, "fee")? - .try_into() - .map_err(|_| EncodingError::FatalError("fee should be an u64".to_string()))? + let fee = u64::from_be_bytes( + get_static_attribute(&swap, "fee")? + .try_into() + .map_err(|_| EncodingError::FatalError("fee should be an u64".to_string()))?, ); - let tick_spacing = u32::from_be_bytes(get_static_attribute(&swap, "tick_spacing")? - .try_into() - .map_err(|_| EncodingError::FatalError("tick_spacing should be an u32".to_string()))? + let tick_spacing = u32::from_be_bytes( + get_static_attribute(&swap, "tick_spacing")? + .try_into() + .map_err(|_| { + EncodingError::FatalError("tick_spacing should be an u32".to_string()) + })?, ); let extension: Address = get_static_attribute(&swap, "extension")? @@ -713,13 +717,13 @@ mod tests { let static_attributes = HashMap::from([ ("fee".to_string(), Bytes::from(0_u64)), ("tick_spacing".to_string(), Bytes::from(0_u32)), - ("extension".to_string(), Bytes::from("0x51d02a5948496a67827242eabc5725531342527c")), // Oracle + ( + "extension".to_string(), + Bytes::from("0x51d02a5948496a67827242eabc5725531342527c"), + ), // Oracle ]); - let component = ProtocolComponent { - static_attributes, - ..Default::default() - }; + let component = ProtocolComponent { static_attributes, ..Default::default() }; let swap = Swap { component, @@ -780,7 +784,10 @@ mod tests { static_attributes: HashMap::from([ ("fee".to_string(), Bytes::from(0_u64)), ("tick_spacing".to_string(), Bytes::from(0_u32)), - ("extension".to_string(), Bytes::from("0x51d02a5948496a67827242eabc5725531342527c")), // Oracle + ( + "extension".to_string(), + Bytes::from("0x51d02a5948496a67827242eabc5725531342527c"), + ), // Oracle ]), ..Default::default() }, @@ -812,7 +819,8 @@ mod tests { .encode_swap(second_swap, encoding_context) .unwrap(); - let combined_hex = format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap)); + let combined_hex = + format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap)); assert_eq!( combined_hex, @@ -833,5 +841,4 @@ mod tests { ); } } - } From 92b5a8adb53fda1b31fb51ac46d6cb68c8ddf27a Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:15:50 +0100 Subject: [PATCH 06/11] Remove split check --- src/encoding/evm/swap_encoder/swap_encoders.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index baccad3..447be6d 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -278,10 +278,6 @@ impl SwapEncoder for EkuboSwapEncoder { swap: Swap, encoding_context: EncodingContext, ) -> Result, EncodingError> { - if !swap.split.is_zero() { - return Err(EncodingError::InvalidInput("splits not implemented".to_string())); - } - if encoding_context.exact_out { return Err(EncodingError::InvalidInput("exact out swaps not implemented".to_string())); } From 3e2e9308d48dc40028e2fe199a6ac334d3361a68 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:48:17 +0100 Subject: [PATCH 07/11] Fix slither lints --- foundry/src/executors/EkuboExecutor.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index b934f8c..fc21e6e 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -93,6 +93,7 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { function _lock(bytes memory data) internal { address target = address(core); + // slither-disable-next-line assembly assembly ("memory-safe") { let args := mload(0x40) @@ -113,7 +114,7 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { function _locked(bytes calldata swapData) internal { // For partial swaps this is not equivalent to the given input amount - uint128 tokenInDebtAmount; + uint128 tokenInDebtAmount = 0; int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16]))); @@ -177,6 +178,7 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { if (token == NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeTransferETH(target, amount); } else { + // slither-disable-next-line assembly assembly ("memory-safe") { let free := mload(0x40) // selector of pay(address) From 47d96c234776784cb42de31aa50fa75352b06efb Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:50:03 +0100 Subject: [PATCH 08/11] Add integration test --- foundry/test/executors/EkuboExecutor.t.sol | 44 ++++++++++----- .../evm/swap_encoder/swap_encoders.rs | 55 ++++++++++--------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol index c323bdb..d870352 100644 --- a/foundry/test/executors/EkuboExecutor.t.sol +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -9,6 +9,7 @@ import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol"; import {ICore} from "@ekubo/interfaces/ICore.sol"; contract EkuboExecutorTest is Test, Constants { + address constant EXECUTOR_ADDRESS = 0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6; // Same address as in swap_encoder.rs tests EkuboExecutor executor; IERC20 USDC = IERC20(USDC_ADDR); @@ -19,9 +20,14 @@ contract EkuboExecutorTest is Test, Constants { bytes32 constant ORACLE_CONFIG = 0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000; function setUp() public { - uint256 forkBlock = 22082754; - vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - executor = new EkuboExecutor(ICore(payable(CORE_ADDRESS))); + vm.createSelectFork(vm.rpcUrl("mainnet"), 22082754); + + deployCodeTo( + "executors/EkuboExecutor.sol", + abi.encode(CORE_ADDRESS), + EXECUTOR_ADDRESS + ); + executor = EkuboExecutor(payable(EXECUTOR_ADDRESS)); } function testSingleSwapEth() public { @@ -86,7 +92,8 @@ contract EkuboExecutorTest is Test, Constants { assertEq(address(executor).balance, ethBalanceBeforeExecutor + amountOut); } - function testMultiHopSwap() public { + // Expects input that encodes the same test case as swap_encoder::tests::ekubo::test_encode_swap_multi + function multiHopSwap(bytes memory data) internal { uint256 amountIn = 1 ether; deal(address(executor), amountIn); @@ -97,15 +104,6 @@ contract EkuboExecutorTest is Test, Constants { uint256 usdtBalanceBeforeCore = USDT.balanceOf(CORE_ADDRESS); uint256 usdtBalanceBeforeExecutor = USDT.balanceOf(address(executor)); - bytes memory data = abi.encodePacked( - address(executor), // receiver - NATIVE_TOKEN_ADDRESS, // tokenIn - USDC_ADDR, // tokenOut of 1st swap - ORACLE_CONFIG, // config of 1st swap - USDT_ADDR, // tokenOut of 2nd swap - bytes32(0x00000000000000000000000000000000000000000001a36e2eb1c43200000032) // config of 2nd swap (0.0025% fee & 0.005% base pool) - ); - uint256 gasBefore = gasleft(); uint256 amountOut = executor.swap(amountIn, data); console.log(gasBefore - gasleft()); @@ -118,4 +116,24 @@ contract EkuboExecutorTest is Test, Constants { assertEq(USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut); assertEq(USDT.balanceOf(address(executor)), usdtBalanceBeforeExecutor + amountOut); } + + // Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi + function testMultiHopSwap() public { + bytes memory data = abi.encodePacked( + address(executor), // receiver + NATIVE_TOKEN_ADDRESS, // tokenIn + USDC_ADDR, // tokenOut of 1st swap + ORACLE_CONFIG, // config of 1st swap + USDT_ADDR, // tokenOut of 2nd swap + bytes32(0x00000000000000000000000000000000000000000001a36e2eb1c43200000032) // config of 2nd swap (0.0025% fee & 0.005% base pool) + ); + multiHopSwap(data); + } + + // Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi + function testMultiHopSwapIntegration() public { + multiHopSwap( + hex"ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032" + ); + } } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 447be6d..0938088 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -2,7 +2,6 @@ use std::str::FromStr; use alloy_primitives::{Address, Bytes as AlloyBytes}; use alloy_sol_types::SolValue; -use num_traits::Zero; use tycho_core::Bytes; use crate::encoding::{ @@ -705,6 +704,8 @@ mod tests { mod ekubo { use super::*; + const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address + #[test] fn test_encode_swap_simple() { let token_in = Bytes::from(Address::ZERO.as_slice()); @@ -729,7 +730,7 @@ mod tests { }; let encoding_context = EncodingContext { - receiver: "0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6".into(), // Random address + receiver: RECEIVER.into(), group_token_in: token_in.clone(), group_token_out: token_out.clone(), exact_out: false, @@ -746,16 +747,15 @@ mod tests { assert_eq!( hex_swap, - String::from(concat!( - // receiver - "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6", - // group token in - "0000000000000000000000000000000000000000", - // token out 1st swap - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // pool config 1st swap - "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", - )) + RECEIVER.to_string() + + concat!( + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + ), ); } @@ -768,7 +768,7 @@ mod tests { let encoder = EkuboSwapEncoder::new(String::default()); let encoding_context = EncodingContext { - receiver: "0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6".into(), // Random address + receiver: RECEIVER.into(), group_token_in: group_token_in.clone(), group_token_out: group_token_out.clone(), exact_out: false, @@ -818,22 +818,23 @@ mod tests { let combined_hex = format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap)); + println!("{}", combined_hex); + assert_eq!( combined_hex, - String::from(concat!( - // 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", - )) + 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", + ), ); } } From 9fc99caf749e2410686b10ad92b0f3eb7146a3e5 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:57:43 +0100 Subject: [PATCH 09/11] forge fmt --- foundry/src/executors/EkuboExecutor.sol | 45 ++++++++++------------ foundry/test/executors/EkuboExecutor.t.sol | 37 +++++++++++++----- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index fc21e6e..1c0d07c 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -19,6 +19,9 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { ICore immutable core; + bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256) + bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address) + uint256 constant POOL_DATA_OFFSET = 56; uint256 constant HOP_BYTE_LEN = 52; @@ -34,7 +37,8 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { if (data.length < 92) revert EkuboExecutor__InvalidDataLength(); uint256 tokenOutOffset = data.length - HOP_BYTE_LEN; - address tokenOut = address(bytes20(LibBytes.loadCalldata(data, tokenOutOffset))); + address tokenOut = + address(bytes20(LibBytes.loadCalldata(data, tokenOutOffset))); uint256 tokenOutBalanceBefore = _balanceOf(tokenOut); @@ -60,9 +64,9 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { bytes4 selector = bytes4(raw[:4]); - if (selector == 0xb45a3c0e) { // Selector of locked(uint256) + if (selector == LOCKED_SELECTOR) { _locked(stripped); - } else if (selector == 0x599d0714) { // Selector of payCallback(uint256,address) + } else if (selector == PAY_CALLBACK_SELECTOR) { _payCallback(stripped); } else { revert EkuboExecutor__UnknownCallback(); @@ -84,7 +88,11 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { SafeTransferLib.safeTransfer(token, address(core), amount); } - function _balanceOf(address token) internal view returns (uint256 balance) { + function _balanceOf(address token) + internal + view + returns (uint256 balance) + { balance = token == NATIVE_TOKEN_ADDRESS ? address(this).balance : IERC20(token).balanceOf(address(this)); @@ -128,22 +136,15 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { uint256 offset = POOL_DATA_OFFSET; for (uint256 i = 0; i < hopsLength; i++) { - address nextTokenOut = address(bytes20(LibBytes.loadCalldata(swapData, offset))); - Config poolConfig = Config.wrap(LibBytes.loadCalldata(swapData, offset + 20)); + address nextTokenOut = + address(bytes20(LibBytes.loadCalldata(swapData, offset))); + Config poolConfig = + Config.wrap(LibBytes.loadCalldata(swapData, offset + 20)); - ( - address token0, - address token1, - bool isToken1 - ) = nextTokenIn > nextTokenOut ? ( - nextTokenOut, - nextTokenIn, - true - ) : ( - nextTokenIn, - nextTokenOut, - false - ); + (address token0, address token1, bool isToken1) = nextTokenIn + > nextTokenOut + ? (nextTokenOut, nextTokenIn, true) + : (nextTokenIn, nextTokenOut, false); (int128 delta0, int128 delta1) = core.swap_611415377( PoolKey(token0, token1, poolConfig), @@ -165,11 +166,7 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { _pay(tokenIn, tokenInDebtAmount); - core.withdraw( - nextTokenIn, - receiver, - uint128(nextAmountIn) - ); + core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn)); } function _pay(address token, uint128 amount) internal { diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol index d870352..0971b40 100644 --- a/foundry/test/executors/EkuboExecutor.t.sol +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -9,7 +9,8 @@ import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol"; import {ICore} from "@ekubo/interfaces/ICore.sol"; contract EkuboExecutorTest is Test, Constants { - address constant EXECUTOR_ADDRESS = 0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6; // Same address as in swap_encoder.rs tests + address constant EXECUTOR_ADDRESS = + 0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6; // Same address as in swap_encoder.rs tests EkuboExecutor executor; IERC20 USDC = IERC20(USDC_ADDR); @@ -17,7 +18,8 @@ contract EkuboExecutorTest is Test, Constants { address constant CORE_ADDRESS = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444; - bytes32 constant ORACLE_CONFIG = 0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000; + bytes32 constant ORACLE_CONFIG = + 0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000; function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), 22082754); @@ -57,8 +59,13 @@ contract EkuboExecutorTest is Test, Constants { assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); - assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore - amountOut); - assertEq(USDC.balanceOf(address(executor)), usdcBalanceBeforeExecutor + amountOut); + assertEq( + USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore - amountOut + ); + assertEq( + USDC.balanceOf(address(executor)), + usdcBalanceBeforeExecutor + amountOut + ); } function testSingleSwapERC20() public { @@ -86,10 +93,15 @@ contract EkuboExecutorTest is Test, Constants { console.log(amountOut); assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn); - assertEq(USDC.balanceOf(address(executor)), usdcBalanceBeforeExecutor - amountIn); + assertEq( + USDC.balanceOf(address(executor)), + usdcBalanceBeforeExecutor - amountIn + ); assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut); - assertEq(address(executor).balance, ethBalanceBeforeExecutor + amountOut); + assertEq( + address(executor).balance, ethBalanceBeforeExecutor + amountOut + ); } // Expects input that encodes the same test case as swap_encoder::tests::ekubo::test_encode_swap_multi @@ -113,8 +125,13 @@ contract EkuboExecutorTest is Test, Constants { assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); - assertEq(USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut); - assertEq(USDT.balanceOf(address(executor)), usdtBalanceBeforeExecutor + amountOut); + assertEq( + USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut + ); + assertEq( + USDT.balanceOf(address(executor)), + usdtBalanceBeforeExecutor + amountOut + ); } // Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi @@ -125,7 +142,9 @@ contract EkuboExecutorTest is Test, Constants { USDC_ADDR, // tokenOut of 1st swap ORACLE_CONFIG, // config of 1st swap USDT_ADDR, // tokenOut of 2nd swap - bytes32(0x00000000000000000000000000000000000000000001a36e2eb1c43200000032) // config of 2nd swap (0.0025% fee & 0.005% base pool) + bytes32( + 0x00000000000000000000000000000000000000000001a36e2eb1c43200000032 + ) // config of 2nd swap (0.0025% fee & 0.005% base pool) ); multiHopSwap(data); } From 95e1bf12906f468c862c5cfaef0450e6f84b6834 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Sat, 29 Mar 2025 10:29:21 +0100 Subject: [PATCH 10/11] Fix wrong argument order --- foundry/src/executors/EkuboExecutor.sol | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index 1c0d07c..7802742 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -82,10 +82,9 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { _locked(msg.data[36:]); } - function payCallback(uint256, address token) external coreOnly { - uint128 amount = uint128(bytes16(LibBytes.loadCalldata(msg.data, 68))); - - SafeTransferLib.safeTransfer(token, address(core), amount); + function payCallback(uint256, address /*token*/) external coreOnly { + // Without selector and locker id + _payCallback(msg.data[36:]); } function _balanceOf(address token) @@ -193,10 +192,10 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { } function _payCallback(bytes calldata payData) internal { - address token = address(bytes20(payData[0:20])); - uint128 amount = uint128(bytes16(payData[20:36])); + address token = address(bytes20(payData[12:32])); // This arg is abi-encoded + uint128 amount = uint128(bytes16(payData[32:48])); - SafeTransferLib.safeTransfer(address(core), token, amount); + SafeTransferLib.safeTransfer(token, address(core), amount); } // To receive withdrawals from Core From 68a9198e9c3f0bbb057ecdae61ab88acfe0b3db5 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Sat, 29 Mar 2025 10:32:22 +0100 Subject: [PATCH 11/11] forge fmt --- foundry/src/executors/EkuboExecutor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index 7802742..4f291fe 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -82,7 +82,7 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { _locked(msg.data[36:]); } - function payCallback(uint256, address /*token*/) external coreOnly { + function payCallback(uint256, address /*token*/ ) external coreOnly { // Without selector and locker id _payCallback(msg.data[36:]); }