From 4e34f45bf420a0d6ab2fe1717d9a94495d6daf12 Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:28:11 +0700 Subject: [PATCH 1/2] Remove balanceOf checks in EkuboExecutor & remove ICallback impl --- foundry/src/TychoRouter.sol | 14 +- foundry/src/executors/EkuboExecutor.sol | 168 ++++++++---------------- 2 files changed, 66 insertions(+), 116 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index d3e7d0f..2de02f7 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -557,9 +557,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { address executor = address(0xA612f60d3C49E5f13f0e067b14E0eD6656F3F279); // slither-disable-next-line controlled-delegatecall,low-level-calls - (bool success, bytes memory result) = executor.delegatecall( - abi.encodeWithSelector(ICallback.handleCallback.selector, msg.data) - ); + (bool success, bytes memory result) = executor.delegatecall(msg.data); if (!success) { revert( @@ -570,15 +568,19 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { ) ); } + + // slither-disable-next-line assembly + assembly ("memory-safe") { + // Propagate the swappedAmount + return(add(result, 32), 16) + } } function payCallback(uint256, address /*token*/ ) external { address executor = address(0xA612f60d3C49E5f13f0e067b14E0eD6656F3F279); // slither-disable-next-line controlled-delegatecall,low-level-calls - (bool success, bytes memory result) = executor.delegatecall( - abi.encodeWithSelector(ICallback.handleCallback.selector, msg.data) - ); + (bool success, bytes memory result) = executor.delegatecall(msg.data); if (!success) { revert( diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index ade58c1..eb5b7d4 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -3,7 +3,6 @@ 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"; @@ -12,17 +11,14 @@ 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"; -contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { +contract EkuboExecutor is IExecutor, ILocker, IPayer { error EkuboExecutor__InvalidDataLength(); error EkuboExecutor__CoreOnly(); error EkuboExecutor__UnknownCallback(); 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 POOL_DATA_OFFSET = 92; uint256 constant HOP_BYTE_LEN = 52; constructor(address _core) { @@ -36,68 +32,67 @@ 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))); - - 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; + calculatedAmount = uint256(_lock(bytes.concat(bytes16(uint128(amountIn)), data))); } - // 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 == LOCKED_SELECTOR) { - _locked(stripped); - } else if (selector == PAY_CALLBACK_SELECTOR) { - _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:]); + int128 nextAmountIn = int128(uint128(bytes16(msg.data[36:52]))); + uint128 tokenInDebtAmount = uint128(nextAmountIn); + + address receiver = address(bytes20(msg.data[52:72])); + address tokenIn = address(bytes20(msg.data[72:POOL_DATA_OFFSET])); + + address nextTokenIn = tokenIn; + + uint256 hopsLength = (msg.data.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(msg.data, offset))); + Config poolConfig = + Config.wrap(LibBytes.loadCalldata(msg.data, 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 + ); + + nextTokenIn = nextTokenOut; + nextAmountIn = -(isToken1 ? delta0 : delta1); + + offset += HOP_BYTE_LEN; + } + + _pay(tokenIn, tokenInDebtAmount); + + core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn)); + + // slither-disable-next-line assembly + assembly ("memory-safe") { + mstore(0, nextAmountIn) + return(0x10, 16) + } } - function payCallback(uint256, address /*token*/ ) external coreOnly { - // Without selector and locker id - _payCallback(msg.data[36:]); + function payCallback(uint256, address token) external coreOnly { + uint128 amount = uint128(bytes16(msg.data[68:84])); + + 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 { + function _lock(bytes memory data) internal returns (uint128 swappedAmount) { address target = address(core); // slither-disable-next-line assembly @@ -116,52 +111,12 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { returndatacopy(0, 0, returndatasize()) revert(0, returndatasize()) } + + returndatacopy(0, 0, 16) + swappedAmount := shr(128, mload(0)) } } - function _locked(bytes calldata swapData) internal { - int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16]))); - uint128 tokenInDebtAmount = uint128(nextAmountIn); - - 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( - EkuboPoolKey(token0, token1, poolConfig), - nextAmountIn, - isToken1, - isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO, - 0 - ); - - 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); @@ -185,13 +140,6 @@ contract EkuboExecutor is IExecutor, ICallback, ILocker, IPayer { } } - 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); - } - // To receive withdrawals from Core receive() external payable {} From 5540503a59cd340d4a93d23e6855b48712285eed Mon Sep 17 00:00:00 2001 From: die-herdplatte <173669014+die-herdplatte@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:42:10 +0700 Subject: [PATCH 2/2] forge fmt --- foundry/src/executors/EkuboExecutor.sol | 8 ++++++-- foundry/test/TychoRouter.t.sol | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol index eb5b7d4..564b9d2 100644 --- a/foundry/src/executors/EkuboExecutor.sol +++ b/foundry/src/executors/EkuboExecutor.sol @@ -33,7 +33,8 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer { 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 locked(uint256) external coreOnly { @@ -92,7 +93,10 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer { SafeTransferLib.safeTransfer(token, address(core), amount); } - 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 diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 4fde00a..a54712b 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1020,7 +1020,10 @@ contract TychoRouterTest is TychoRouterTestSetup { // TEMPORARY while the Ekubo executor address is hardcoded in TychoRouter // This allows us to change the code at that address to be the testing executor code - vm.etch(0xA612f60d3C49E5f13f0e067b14E0eD6656F3F279, 0x2a07706473244BC757E10F2a9E86fB532828afe3.code); + vm.etch( + 0xA612f60d3C49E5f13f0e067b14E0eD6656F3F279, + 0x2a07706473244BC757E10F2a9E86fB532828afe3.code + ); deal(ALICE, 1 ether); uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE);