From 58ce241c632feb6cc2014a511de9beca3cd6e471 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:02:37 +0200 Subject: [PATCH 01/10] Add support for Ekubo MEV-resist extension --- foundry/lib/ekubo/interfaces/ICore.sol | 6 +- .../lib/ekubo/interfaces/IFlashAccountant.sol | 12 +- foundry/lib/ekubo/types/poolKey.sol | 10 +- foundry/src/executors/EkuboExecutor.sol | 153 +++++++++++++----- foundry/test/TychoRouterTestSetup.sol | 9 +- foundry/test/executors/EkuboExecutor.t.sol | 86 ++++++++-- 6 files changed, 214 insertions(+), 62 deletions(-) diff --git a/foundry/lib/ekubo/interfaces/ICore.sol b/foundry/lib/ekubo/interfaces/ICore.sol index d2388e8..cb43b8d 100644 --- a/foundry/lib/ekubo/interfaces/ICore.sol +++ b/foundry/lib/ekubo/interfaces/ICore.sol @@ -2,15 +2,15 @@ pragma solidity ^0.8.26; import {IFlashAccountant} from "./IFlashAccountant.sol"; -import {EkuboPoolKey} from "../types/poolKey.sol"; +import {PoolKey} from "../types/poolKey.sol"; import {SqrtRatio} from "../types/sqrtRatio.sol"; interface ICore is IFlashAccountant { function swap_611415377( - EkuboPoolKey memory poolKey, + 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 index 2524078..5682e25 100644 --- a/foundry/lib/ekubo/interfaces/IFlashAccountant.sol +++ b/foundry/lib/ekubo/interfaces/IFlashAccountant.sol @@ -10,7 +10,17 @@ interface IPayer { } interface IFlashAccountant { + // Forward the lock from the current locker to the given address + // Any additional calldata is also passed through to the forwardee, with no additional encoding + // In addition, any data returned from IForwardee#forwarded is also returned from this function exactly as is, i.e. with no additional encoding or decoding + // Reverts are also bubbled up + function forward(address to) external; + // 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; + function withdraw( + address token, + address recipient, + uint128 amount + ) external; } diff --git a/foundry/lib/ekubo/types/poolKey.sol b/foundry/lib/ekubo/types/poolKey.sol index ad20db8..ad375a4 100644 --- a/foundry/lib/ekubo/types/poolKey.sol +++ b/foundry/lib/ekubo/types/poolKey.sol @@ -1,12 +1,20 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; +using {extension} for Config global; + // address (20 bytes) | fee (8 bytes) | tickSpacing (4 bytes) type Config is bytes32; // Each pool has its own state associated with this key -struct EkuboPoolKey { +struct PoolKey { address token0; address token1; Config config; } + +function extension(Config config) pure returns (address e) { + assembly ("memory-safe") { + e := shr(96, config) + } +} diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index 63a57b3..f149234 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -9,8 +9,8 @@ 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, EkuboPoolKey} from "@ekubo/types/poolKey.sol"; -import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol"; +import {Config, PoolKey} from "@ekubo/types/poolKey.sol"; +import {MAX_SQRT_RATIO, MIN_SQRT_RATIO, SqrtRatio} from "@ekubo/types/sqrtRatio.sol"; import {RestrictTransferFrom} from "../RestrictTransferFrom.sol"; import "@openzeppelin/contracts/utils/Address.sol"; @@ -26,6 +26,7 @@ contract EkuboExecutor is error EkuboExecutor__UnknownCallback(); ICore immutable core; + address immutable mevResist; uint256 constant POOL_DATA_OFFSET = 57; uint256 constant HOP_BYTE_LEN = 52; @@ -33,30 +34,34 @@ contract EkuboExecutor is bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256) bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address) + uint256 constant SKIP_AHEAD = 0; + using SafeERC20 for IERC20; - constructor(address _core, address _permit2) - RestrictTransferFrom(_permit2) - { + constructor( + address _core, + address _mevResist, + address _permit2 + ) RestrictTransferFrom(_permit2) { core = ICore(_core); + mevResist = _mevResist; } - function swap(uint256 amountIn, bytes calldata data) - external - payable - returns (uint256 calculatedAmount) - { + function swap( + uint256 amountIn, + bytes calldata data + ) external payable returns (uint256 calculatedAmount) { if (data.length < 92) 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)), data)) + ); } - function handleCallback(bytes calldata raw) - external - returns (bytes memory) - { + function handleCallback( + bytes calldata raw + ) external returns (bytes memory) { verifyCallback(raw); // Without selector and locker id @@ -89,15 +94,12 @@ contract EkuboExecutor is } } - function payCallback(uint256, address /*token*/ ) external coreOnly { + function payCallback(uint256, address /*token*/) external coreOnly { // Without selector and locker id _payCallback(msg.data[36:]); } - function _lock(bytes memory data) - internal - returns (uint128 swappedAmount) - { + function _lock(bytes memory data) internal returns (uint128 swappedAmount) { address target = address(core); // slither-disable-next-line assembly @@ -131,29 +133,56 @@ contract EkuboExecutor is address nextTokenIn = tokenIn; - uint256 hopsLength = (swapData.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN; + 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); - - // slither-disable-next-line calls-loop - (int128 delta0, int128 delta1) = core.swap_611415377( - EkuboPoolKey(token0, token1, poolConfig), - nextAmountIn, - isToken1, - isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO, - 0 + address nextTokenOut = address( + bytes20(LibBytes.loadCalldata(swapData, offset)) ); + Config poolConfig = Config.wrap( + LibBytes.loadCalldata(swapData, offset + 20) + ); + + ( + address token0, + address token1, + bool isToken1, + SqrtRatio sqrtRatioLimit + ) = nextTokenIn > nextTokenOut + ? (nextTokenOut, nextTokenIn, true, MAX_SQRT_RATIO) + : (nextTokenIn, nextTokenOut, false, MIN_SQRT_RATIO); + + PoolKey memory pk = PoolKey(token0, token1, poolConfig); + + int128 delta0; + int128 delta1; + + if (poolConfig.extension() == mevResist) { + (delta0, delta1) = abi.decode( + _forward( + mevResist, + abi.encode( + pk, + nextAmountIn, + isToken1, + sqrtRatioLimit, + SKIP_AHEAD + ) + ), + (int128, int128) + ); + } else { + (delta0, delta1) = core.swap_611415377( + pk, + nextAmountIn, + isToken1, + sqrtRatioLimit, + SKIP_AHEAD + ); + } nextTokenIn = nextTokenOut; nextAmountIn = -(isToken1 ? delta0 : delta1); @@ -166,9 +195,49 @@ contract EkuboExecutor is return nextAmountIn; } - function _pay(address token, uint128 amount, TransferType transferType) - internal - { + function _forward( + address to, + bytes memory data + ) internal returns (bytes memory result) { + address target = address(core); + + assembly ("memory-safe") { + // We will store result where the free memory pointer is now, ... + result := mload(0x40) + + // But first use it to store the calldata + + // Selector of forward(address) + mstore(result, shl(224, 0x101e8952)) + mstore(add(result, 4), to) + + // We only copy the data, not the length, because the length is read from the calldata size + let len := mload(data) + mcopy(add(result, 36), add(data, 32), len) + + // If the call failed, pass through the revert + if iszero(call(gas(), target, 0, result, add(36, len), 0, 0)) { + returndatacopy(result, 0, returndatasize()) + revert(result, returndatasize()) + } + + // Copy the entire return data into the space where the result is pointing + mstore(result, returndatasize()) + returndatacopy(add(result, 32), 0, returndatasize()) + + // Update the free memory pointer to be after the end of the data, aligned to the next 32 byte word + mstore( + 0x40, + and(add(add(result, add(32, returndatasize())), 31), not(31)) + ) + } + } + + function _pay( + address token, + uint128 amount, + TransferType transferType + ) internal { address target = address(core); if (token == NATIVE_TOKEN_ADDRESS) { diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 925fafa..da1291c 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; import "../src/executors/BalancerV2Executor.sol"; import "../src/executors/CurveExecutor.sol"; -import "../src/executors/EkuboExecutor.sol"; +import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol"; import "../src/executors/UniswapV2Executor.sol"; import "../src/executors/UniswapV3Executor.sol"; import "../src/executors/UniswapV4Executor.sol"; @@ -116,6 +116,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { bytes32 initCodePancakeV3 = PANCAKEV3_POOL_CODE_INIT_HASH; address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444; + address ekuboMevResist = 0x553a2EFc570c9e104942cEC6aC1c18118e54C091; IPoolManager poolManager = IPoolManager(poolManagerAddress); usv2Executor = @@ -127,7 +128,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS ); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); - ekuboExecutor = new EkuboExecutor(ekuboCore, PERMIT2_ADDRESS); + ekuboExecutor = new EkuboExecutor( + ekuboCore, + ekuboMevResist, + PERMIT2_ADDRESS + ); curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS); maverickv2Executor = new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol index 7fa3eb1..8168016 100644 --- a/foundry/test/executors/EkuboExecutor.t.sol +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -18,22 +18,29 @@ contract EkuboExecutorTest is Constants, TestUtils { IERC20 USDT = IERC20(USDT_ADDR); address constant CORE_ADDRESS = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444; + address constant MEV_RESIST_ADDRESS = + 0x553a2EFc570c9e104942cEC6aC1c18118e54C091; bytes32 constant ORACLE_CONFIG = 0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000; - function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet"), 22082754); + // 0.01% fee and 0.02% tick spacing + bytes32 constant MEV_RESIST_POOL_CONFIG = + 0x553a2EFc570c9e104942cEC6aC1c18118e54C09100068db8bac710cb000000c8; + + modifier setUpFork(uint256 blockNumber) { + vm.createSelectFork(vm.rpcUrl("mainnet"), blockNumber); deployCodeTo( "executors/EkuboExecutor.sol", - abi.encode(CORE_ADDRESS, PERMIT2_ADDRESS), + abi.encode(CORE_ADDRESS, MEV_RESIST_ADDRESS, PERMIT2_ADDRESS), EXECUTOR_ADDRESS ); executor = EkuboExecutor(payable(EXECUTOR_ADDRESS)); + _; } - function testSingleSwapEth() public { + function testSingleSwapEth() public setUpFork(22722989) { uint256 amountIn = 1 ether; deal(address(executor), amountIn); @@ -59,10 +66,14 @@ contract EkuboExecutorTest is Constants, TestUtils { console.log(amountOut); assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); - assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); + assertEq( + address(executor).balance, + ethBalanceBeforeExecutor - amountIn + ); assertEq( - USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore - amountOut + USDC.balanceOf(CORE_ADDRESS), + usdcBalanceBeforeCore - amountOut ); assertEq( USDC.balanceOf(address(executor)), @@ -70,7 +81,7 @@ contract EkuboExecutorTest is Constants, TestUtils { ); } - function testSingleSwapERC20() public { + function testSingleSwapERC20() public setUpFork(22722989) { uint256 amountIn = 1_000_000_000; deal(USDC_ADDR, address(executor), amountIn); @@ -95,7 +106,10 @@ contract EkuboExecutorTest is Constants, TestUtils { console.log(amountOut); - assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn); + assertEq( + USDC.balanceOf(CORE_ADDRESS), + usdcBalanceBeforeCore + amountIn + ); assertEq( USDC.balanceOf(address(executor)), usdcBalanceBeforeExecutor - amountIn @@ -103,7 +117,49 @@ contract EkuboExecutorTest is Constants, TestUtils { assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut); assertEq( - address(executor).balance, ethBalanceBeforeExecutor + amountOut + address(executor).balance, + ethBalanceBeforeExecutor + amountOut + ); + } + + function testMevResist() public setUpFork(22722989) { + 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( + uint8(RestrictTransferFrom.TransferType.Transfer), // transferNeeded (transfer from executor to core) + address(executor), // receiver + USDC_ADDR, // tokenIn + NATIVE_TOKEN_ADDRESS, // tokenOut + MEV_RESIST_POOL_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 ); } @@ -126,10 +182,14 @@ contract EkuboExecutorTest is Constants, TestUtils { console.log(amountOut); assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); - assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); + assertEq( + address(executor).balance, + ethBalanceBeforeExecutor - amountIn + ); assertEq( - USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut + USDT.balanceOf(CORE_ADDRESS), + usdtBalanceBeforeCore - amountOut ); assertEq( USDT.balanceOf(address(executor)), @@ -138,7 +198,7 @@ contract EkuboExecutorTest is Constants, TestUtils { } // Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi - function testMultiHopSwap() public { + function testMultiHopSwap() public setUpFork(22082754) { bytes memory data = abi.encodePacked( uint8(RestrictTransferFrom.TransferType.Transfer), // transferNeeded (transfer from executor to core) address(executor), // receiver @@ -154,7 +214,7 @@ contract EkuboExecutorTest is Constants, TestUtils { } // Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi - function testMultiHopSwapIntegration() public { + function testMultiHopSwapIntegration() public setUpFork(22082754) { multiHopSwap(loadCallDataFromFile("test_ekubo_encode_swap_multi")); } } From 72553bf98d432963851c07fba10b91f42ab7a3e6 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:34:31 +0200 Subject: [PATCH 02/10] Fix slither issues --- foundry/lib/ekubo/types/poolKey.sol | 1 + foundry/src/executors/EkuboExecutor.sol | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/foundry/lib/ekubo/types/poolKey.sol b/foundry/lib/ekubo/types/poolKey.sol index ad375a4..a3f2bc3 100644 --- a/foundry/lib/ekubo/types/poolKey.sol +++ b/foundry/lib/ekubo/types/poolKey.sol @@ -14,6 +14,7 @@ struct PoolKey { } function extension(Config config) pure returns (address e) { + // slither-disable-next-line assembly assembly ("memory-safe") { e := shr(96, config) } diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index f149234..e072810 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -21,6 +21,7 @@ contract EkuboExecutor is ICallback, RestrictTransferFrom { + error EkuboExecutor__AddressZero(); error EkuboExecutor__InvalidDataLength(); error EkuboExecutor__CoreOnly(); error EkuboExecutor__UnknownCallback(); @@ -44,6 +45,10 @@ contract EkuboExecutor is address _permit2 ) RestrictTransferFrom(_permit2) { core = ICore(_core); + + if (_mevResist == address(0)) { + revert EkuboExecutor__AddressZero(); + } mevResist = _mevResist; } @@ -175,6 +180,7 @@ contract EkuboExecutor is (int128, int128) ); } else { + // slither-disable-next-line calls-loop (delta0, delta1) = core.swap_611415377( pk, nextAmountIn, @@ -201,6 +207,7 @@ contract EkuboExecutor is ) internal returns (bytes memory result) { address target = address(core); + // slither-disable-next-line assembly assembly ("memory-safe") { // We will store result where the free memory pointer is now, ... result := mload(0x40) From b331a43392d3a55f30d3a3955b8acde24340fd27 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:22:34 +0200 Subject: [PATCH 03/10] forge fmt --- foundry/src/executors/EkuboExecutor.sol | 81 +++++++++++----------- foundry/test/TychoRouterTestSetup.sol | 7 +- foundry/test/executors/EkuboExecutor.t.sol | 32 +++------ 3 files changed, 49 insertions(+), 71 deletions(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index e072810..2718841 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -10,7 +10,11 @@ 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, SqrtRatio} from "@ekubo/types/sqrtRatio.sol"; +import { + MAX_SQRT_RATIO, + MIN_SQRT_RATIO, + SqrtRatio +} from "@ekubo/types/sqrtRatio.sol"; import {RestrictTransferFrom} from "../RestrictTransferFrom.sol"; import "@openzeppelin/contracts/utils/Address.sol"; @@ -39,11 +43,9 @@ contract EkuboExecutor is using SafeERC20 for IERC20; - constructor( - address _core, - address _mevResist, - address _permit2 - ) RestrictTransferFrom(_permit2) { + constructor(address _core, address _mevResist, address _permit2) + RestrictTransferFrom(_permit2) + { core = ICore(_core); if (_mevResist == address(0)) { @@ -52,21 +54,22 @@ contract EkuboExecutor is mevResist = _mevResist; } - function swap( - uint256 amountIn, - bytes calldata data - ) external payable returns (uint256 calculatedAmount) { + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { if (data.length < 92) 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)), data))); } - function handleCallback( - bytes calldata raw - ) external returns (bytes memory) { + function handleCallback(bytes calldata raw) + external + returns (bytes memory) + { verifyCallback(raw); // Without selector and locker id @@ -99,12 +102,15 @@ contract EkuboExecutor is } } - function payCallback(uint256, address /*token*/) external coreOnly { + function payCallback(uint256, address /*token*/ ) external coreOnly { // Without selector and locker id _payCallback(msg.data[36:]); } - function _lock(bytes memory data) internal returns (uint128 swappedAmount) { + function _lock(bytes memory data) + internal + returns (uint128 swappedAmount) + { address target = address(core); // slither-disable-next-line assembly @@ -138,18 +144,15 @@ contract EkuboExecutor is address nextTokenIn = tokenIn; - uint256 hopsLength = (swapData.length - POOL_DATA_OFFSET) / - HOP_BYTE_LEN; + 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 nextTokenOut = + address(bytes20(LibBytes.loadCalldata(swapData, offset))); + Config poolConfig = + Config.wrap(LibBytes.loadCalldata(swapData, offset + 20)); ( address token0, @@ -157,8 +160,8 @@ contract EkuboExecutor is bool isToken1, SqrtRatio sqrtRatioLimit ) = nextTokenIn > nextTokenOut - ? (nextTokenOut, nextTokenIn, true, MAX_SQRT_RATIO) - : (nextTokenIn, nextTokenOut, false, MIN_SQRT_RATIO); + ? (nextTokenOut, nextTokenIn, true, MAX_SQRT_RATIO) + : (nextTokenIn, nextTokenOut, false, MIN_SQRT_RATIO); PoolKey memory pk = PoolKey(token0, token1, poolConfig); @@ -182,11 +185,7 @@ contract EkuboExecutor is } else { // slither-disable-next-line calls-loop (delta0, delta1) = core.swap_611415377( - pk, - nextAmountIn, - isToken1, - sqrtRatioLimit, - SKIP_AHEAD + pk, nextAmountIn, isToken1, sqrtRatioLimit, SKIP_AHEAD ); } @@ -201,10 +200,10 @@ contract EkuboExecutor is return nextAmountIn; } - function _forward( - address to, - bytes memory data - ) internal returns (bytes memory result) { + function _forward(address to, bytes memory data) + internal + returns (bytes memory result) + { address target = address(core); // slither-disable-next-line assembly @@ -240,11 +239,9 @@ contract EkuboExecutor is } } - function _pay( - address token, - uint128 amount, - TransferType transferType - ) internal { + function _pay(address token, uint128 amount, TransferType transferType) + internal + { address target = address(core); if (token == NATIVE_TOKEN_ADDRESS) { diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index da1291c..8a68568 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -128,11 +128,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS ); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); - ekuboExecutor = new EkuboExecutor( - ekuboCore, - ekuboMevResist, - PERMIT2_ADDRESS - ); + ekuboExecutor = + new EkuboExecutor(ekuboCore, ekuboMevResist, PERMIT2_ADDRESS); curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS); maverickv2Executor = new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol index 8168016..6ddd395 100644 --- a/foundry/test/executors/EkuboExecutor.t.sol +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -66,14 +66,10 @@ contract EkuboExecutorTest is Constants, TestUtils { console.log(amountOut); assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); - assertEq( - address(executor).balance, - ethBalanceBeforeExecutor - amountIn - ); + assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); assertEq( - USDC.balanceOf(CORE_ADDRESS), - usdcBalanceBeforeCore - amountOut + USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore - amountOut ); assertEq( USDC.balanceOf(address(executor)), @@ -106,10 +102,7 @@ contract EkuboExecutorTest is Constants, TestUtils { console.log(amountOut); - assertEq( - USDC.balanceOf(CORE_ADDRESS), - usdcBalanceBeforeCore + amountIn - ); + assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn); assertEq( USDC.balanceOf(address(executor)), usdcBalanceBeforeExecutor - amountIn @@ -117,8 +110,7 @@ contract EkuboExecutorTest is Constants, TestUtils { assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut); assertEq( - address(executor).balance, - ethBalanceBeforeExecutor + amountOut + address(executor).balance, ethBalanceBeforeExecutor + amountOut ); } @@ -147,10 +139,7 @@ contract EkuboExecutorTest is Constants, TestUtils { console.log(amountOut); - assertEq( - USDC.balanceOf(CORE_ADDRESS), - usdcBalanceBeforeCore + amountIn - ); + assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn); assertEq( USDC.balanceOf(address(executor)), usdcBalanceBeforeExecutor - amountIn @@ -158,8 +147,7 @@ contract EkuboExecutorTest is Constants, TestUtils { assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut); assertEq( - address(executor).balance, - ethBalanceBeforeExecutor + amountOut + address(executor).balance, ethBalanceBeforeExecutor + amountOut ); } @@ -182,14 +170,10 @@ contract EkuboExecutorTest is Constants, TestUtils { console.log(amountOut); assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); - assertEq( - address(executor).balance, - ethBalanceBeforeExecutor - amountIn - ); + assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); assertEq( - USDT.balanceOf(CORE_ADDRESS), - usdtBalanceBeforeCore - amountOut + USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut ); assertEq( USDT.balanceOf(address(executor)), From 5aaac2855e7bb87de980450e5f05dc07f3611516 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:42:28 +0200 Subject: [PATCH 04/10] forge fmt --- foundry/test/TychoRouterTestSetup.sol | 109 +++++++++++--------------- 1 file changed, 46 insertions(+), 63 deletions(-) diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index ddcaec1..6f1193d 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -8,7 +8,10 @@ import {CurveExecutor} from "../src/executors/CurveExecutor.sol"; import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol"; import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol"; -import {UniswapV3Executor, IUniswapV3Pool} from "../src/executors/UniswapV3Executor.sol"; +import { + UniswapV3Executor, + IUniswapV3Pool +} from "../src/executors/UniswapV3Executor.sol"; import {UniswapV4Executor} from "../src/executors/UniswapV4Executor.sol"; // Test utilities and mocks @@ -38,10 +41,7 @@ contract TychoRouterExposed is TychoRouter { bool transferFromNeeded ) external { _tstoreTransferFromInfo( - tokenIn, - amountIn, - isPermit2, - transferFromNeeded + tokenIn, amountIn, isPermit2, transferFromNeeded ); } @@ -53,10 +53,10 @@ contract TychoRouterExposed is TychoRouter { return _splitSwap(amountIn, nTokens, swaps); } - function exposedSequentialSwap( - uint256 amountIn, - bytes calldata swaps - ) external returns (uint256) { + function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps) + external + returns (uint256) + { return _sequentialSwap(amountIn, swaps); } } @@ -100,8 +100,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER); tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER); tychoRouter.grantRole( - keccak256("EXECUTOR_SETTER_ROLE"), - EXECUTOR_SETTER + keccak256("EXECUTOR_SETTER_ROLE"), EXECUTOR_SETTER ); return tychoRouter; } @@ -118,34 +117,20 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { address ekuboMevResist = 0x553a2EFc570c9e104942cEC6aC1c18118e54C091; IPoolManager poolManager = IPoolManager(poolManagerAddress); - usv2Executor = new UniswapV2Executor( - factoryV2, - initCodeV2, - PERMIT2_ADDRESS, - 30 - ); - usv3Executor = new UniswapV3Executor( - factoryV3, - initCodeV3, - PERMIT2_ADDRESS - ); + usv2Executor = + new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS, 30); + usv3Executor = + new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS); usv4Executor = new UniswapV4Executor(poolManager, PERMIT2_ADDRESS); pancakev3Executor = new UniswapV3Executor( - factoryPancakeV3, - initCodePancakeV3, - PERMIT2_ADDRESS + factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS ); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); - ekuboExecutor = new EkuboExecutor( - ekuboCore, - ekuboMevResist, - PERMIT2_ADDRESS - ); + ekuboExecutor = + new EkuboExecutor(ekuboCore, ekuboMevResist, PERMIT2_ADDRESS); curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS); - maverickv2Executor = new MaverickV2Executor( - MAVERICK_V2_FACTORY, - PERMIT2_ADDRESS - ); + maverickv2Executor = + new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS); address[] memory executors = new address[](9); @@ -162,9 +147,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { return executors; } - function pleEncode( - bytes[] memory data - ) public pure returns (bytes memory encoded) { + function pleEncode(bytes[] memory data) + public + pure + returns (bytes memory encoded) + { for (uint256 i = 0; i < data.length; i++) { encoded = bytes.concat( encoded, @@ -173,17 +160,19 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { } } - function encodeSingleSwap( - address executor, - bytes memory protocolData - ) internal pure returns (bytes memory) { + function encodeSingleSwap(address executor, bytes memory protocolData) + internal + pure + returns (bytes memory) + { return abi.encodePacked(executor, protocolData); } - function encodeSequentialSwap( - address executor, - bytes memory protocolData - ) internal pure returns (bytes memory) { + function encodeSequentialSwap(address executor, bytes memory protocolData) + internal + pure + returns (bytes memory) + { return abi.encodePacked(executor, protocolData); } @@ -194,14 +183,9 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { address executor, bytes memory protocolData ) internal pure returns (bytes memory) { - return - abi.encodePacked( - tokenInIndex, - tokenOutIndex, - split, - executor, - protocolData - ); + return abi.encodePacked( + tokenInIndex, tokenOutIndex, split, executor, protocolData + ); } function encodeUniswapV2Swap( @@ -224,15 +208,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType ) internal view returns (bytes memory) { IUniswapV3Pool pool = IUniswapV3Pool(target); - return - abi.encodePacked( - tokenIn, - tokenOut, - pool.fee(), - receiver, - target, - zero2one, - transferType - ); + return abi.encodePacked( + tokenIn, + tokenOut, + pool.fee(), + receiver, + target, + zero2one, + transferType + ); } } From 312269dabba5b0d8f303f9aba47d9456e5cd5db7 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 27 Jun 2025 09:39:13 +0100 Subject: [PATCH 05/10] fix: Fix wrong log in CurveEncoder Took 5 minutes --- src/encoding/evm/swap_encoder/swap_encoders.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index cbf33e9..84ddecd 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -440,7 +440,7 @@ impl CurveSwapEncoder { .iter() .position(|&addr| addr == token_out) .ok_or(EncodingError::FatalError(format!( - "Token in address {token_in} not found in curve pool coins" + "Token in address {token_out} not found in curve pool coins" )))?; Ok((U8::from(i), U8::from(j))) } From 0458b910abc56b171920f505ca7fc1af6eb763b5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 27 Jun 2025 09:04:43 +0000 Subject: [PATCH 06/10] chore(release): 0.101.4 [skip ci] ## [0.101.4](https://github.com/propeller-heads/tycho-execution/compare/0.101.3...0.101.4) (2025-06-27) ### Bug Fixes * Fix wrong log in CurveEncoder ([312269d](https://github.com/propeller-heads/tycho-execution/commit/312269dabba5b0d8f303f9aba47d9456e5cd5db7)) --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4004d25..81825cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.101.4](https://github.com/propeller-heads/tycho-execution/compare/0.101.3...0.101.4) (2025-06-27) + + +### Bug Fixes + +* Fix wrong log in CurveEncoder ([312269d](https://github.com/propeller-heads/tycho-execution/commit/312269dabba5b0d8f303f9aba47d9456e5cd5db7)) + ## [0.101.3](https://github.com/propeller-heads/tycho-execution/compare/0.101.2...0.101.3) (2025-06-23) diff --git a/Cargo.lock b/Cargo.lock index 4d53c90..8501128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4658,7 +4658,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.101.3" +version = "0.101.4" dependencies = [ "alloy", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 13b398d..cba6b48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.101.3" +version = "0.101.4" edition = "2021" description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." repository = "https://github.com/propeller-heads/tycho-execution" From 0f679d6e0663aba881babb09319815723cdf68e5 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 27 Jun 2025 17:58:29 +0100 Subject: [PATCH 07/10] fix: Use native token curve address and not regular zero address Took 18 minutes Took 1 minute --- src/encoding/evm/swap_encoder/swap_encoders.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 84ddecd..c7e30e6 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -407,7 +407,10 @@ impl CurveSwapEncoder { // Some curve pools support both ETH and WETH as tokens. // They do the wrapping/unwrapping inside the pool fn normalize_token(&self, token: Address, coins: &[Address]) -> Result { - let native_token_address = bytes_to_address(&self.native_token_address)?; + let native_token_address = + Address::from_str(&self.native_token_curve_address).map_err(|_| { + EncodingError::FatalError("Invalid native token curve address".to_string()) + })?; let wrapped_native_token_address = bytes_to_address(&self.wrapped_native_token_address)?; if token == native_token_address && !coins.contains(&token) { Ok(wrapped_native_token_address) From 2f161231e9e69830f7ff8162046d04e419545d13 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 27 Jun 2025 17:05:44 +0000 Subject: [PATCH 08/10] chore(release): 0.101.5 [skip ci] ## [0.101.5](https://github.com/propeller-heads/tycho-execution/compare/0.101.4...0.101.5) (2025-06-27) ### Bug Fixes * Use native token curve address and not regular zero address ([0f679d6](https://github.com/propeller-heads/tycho-execution/commit/0f679d6e0663aba881babb09319815723cdf68e5)) --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81825cd..0b52b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.101.5](https://github.com/propeller-heads/tycho-execution/compare/0.101.4...0.101.5) (2025-06-27) + + +### Bug Fixes + +* Use native token curve address and not regular zero address ([0f679d6](https://github.com/propeller-heads/tycho-execution/commit/0f679d6e0663aba881babb09319815723cdf68e5)) + ## [0.101.4](https://github.com/propeller-heads/tycho-execution/compare/0.101.3...0.101.4) (2025-06-27) diff --git a/Cargo.lock b/Cargo.lock index 8501128..76029dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4658,7 +4658,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.101.4" +version = "0.101.5" dependencies = [ "alloy", "chrono", diff --git a/Cargo.toml b/Cargo.toml index cba6b48..97361f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.101.4" +version = "0.101.5" edition = "2021" description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." repository = "https://github.com/propeller-heads/tycho-execution" From 3fe9906e5babb4a31af45c1d48fda3a096802fe8 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 1 Jul 2025 16:00:51 +0100 Subject: [PATCH 09/10] feat: Deploy EkuboExecutor with MEV-resist Took 31 minutes --- config/executor_addresses.json | 2 +- foundry/scripts/deploy-executors.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/executor_addresses.json b/config/executor_addresses.json index fed287c..9790be0 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -7,7 +7,7 @@ "pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059", "uniswap_v4": "0xD11496EAb53A9521f0bC1e5c1098Ecb467103Ad9", "vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411", - "ekubo_v2": "0xcCF8e1E39e9ddfa88282fA6a7B31eBFB41a1ED7B", + "ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333", "vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165", "vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B", "vm:balancer_v3": "0x0000000000000000000000000000000000000000" diff --git a/foundry/scripts/deploy-executors.js b/foundry/scripts/deploy-executors.js index 82a9be7..82dfb9e 100644 --- a/foundry/scripts/deploy-executors.js +++ b/foundry/scripts/deploy-executors.js @@ -57,10 +57,11 @@ const executors_to_deploy = { }, // Args: Permit2 {exchange: "BalancerV2Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]}, - // Args: Ekubo core contract, Permit2 + // Args: Ekubo core contract, mev resist, Permit2 { exchange: "EkuboExecutor", args: [ "0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444", + "0x553a2EFc570c9e104942cEC6aC1c18118e54C091", "0x000000000022D473030F116dDEE9F6B43aC78BA3" ] }, From 62583e3419d9bc3f8419fa9f299f550cdedd4d12 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 1 Jul 2025 16:07:59 +0000 Subject: [PATCH 10/10] chore(release): 0.102.0 [skip ci] ## [0.102.0](https://github.com/propeller-heads/tycho-execution/compare/0.101.5...0.102.0) (2025-07-01) ### Features * Deploy EkuboExecutor with MEV-resist ([3fe9906](https://github.com/propeller-heads/tycho-execution/commit/3fe9906e5babb4a31af45c1d48fda3a096802fe8)) --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b52b8f..aa89aa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.102.0](https://github.com/propeller-heads/tycho-execution/compare/0.101.5...0.102.0) (2025-07-01) + + +### Features + +* Deploy EkuboExecutor with MEV-resist ([3fe9906](https://github.com/propeller-heads/tycho-execution/commit/3fe9906e5babb4a31af45c1d48fda3a096802fe8)) + ## [0.101.5](https://github.com/propeller-heads/tycho-execution/compare/0.101.4...0.101.5) (2025-06-27) diff --git a/Cargo.lock b/Cargo.lock index 76029dc..4e65e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4658,7 +4658,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.101.5" +version = "0.102.0" dependencies = [ "alloy", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 97361f1..d304d85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.101.5" +version = "0.102.0" edition = "2021" description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." repository = "https://github.com/propeller-heads/tycho-execution"