tycho-execution 0.300.3

Provides tools for encoding and executing swaps against Tycho router and protocol executors.
Documentation
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;

import {
    SafeERC20,
    IERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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,
    SqrtRatio
} from "@ekubo/types/sqrtRatio.sol";
import {TransferManager} from "../TransferManager.sol";
import {ETH_ADDRESS} from "../../lib/NativeETH.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

contract EkuboExecutor is IExecutor, ILocker, IPayer, ICallback {
    error EkuboExecutor__AddressZero();
    error EkuboExecutor__InvalidDataLength();
    error EkuboExecutor__CoreOnly();
    error EkuboExecutor__UnknownCallback();

    ICore private immutable _core;
    address private immutable _mevResist;

    uint256 private constant _POOL_DATA_OFFSET = 56;
    uint256 private constant _HOP_BYTE_LEN = 52;

    bytes4 private constant _LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256)
    bytes4 private constant _PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address)

    uint256 private constant _SKIP_AHEAD = 0;

    using SafeERC20 for IERC20;

    constructor(address core, address mevResist) {
        _core = ICore(core);

        if (mevResist == address(0)) {
            revert EkuboExecutor__AddressZero();
        }
        _mevResist = mevResist;
    }

    function fundsExpectedAddress(
        bytes calldata /* data */
    )
        external
        view
        returns (address receiver)
    {
        return msg.sender;
    }

    function swap(uint256 amountIn, bytes calldata data, address receiver)
        external
        payable
    {
        if (data.length < 72) {
            revert EkuboExecutor__InvalidDataLength();
        }

        // amountIn must be at most type(int128).MAX
        _lock(
            abi.encodePacked(
                bytes16(uint128(amountIn)), bytes20(receiver), data
            )
        );
    }

    function handleCallback(bytes calldata raw)
        external
        returns (bytes memory)
    {
        verifyCallback(raw);

        bytes4 selector = bytes4(raw[:4]);

        if (selector == _LOCKED_SELECTOR) {
            _locked(raw[36:]);
        } else if (selector == _PAY_CALLBACK_SELECTOR) {
            // The paying is done in the Dispatcher using getCallbackTransferData
        } else {
            revert EkuboExecutor__UnknownCallback();
        }

        return "";
    }

    function verifyCallback(bytes calldata) public view coreOnly {}

    function locked(uint256) external coreOnly {
        _locked(msg.data[36:]);
    }

    function payCallback(
        uint256,
        address /*token*/
    )
        external
        coreOnly
    {
        // The paying is done in the Dispatcher using getCallbackTransferData. Nothing to do here
    }

    function _lock(bytes memory data) internal {
        // Prepend selector of lock() to calldata
        // We must use assembly here since the Ekubo Core's lock method expects the raw
        // bytes directly and not ABI-encoded bytes
        bytes memory callData = abi.encodePacked(bytes4(0xf83d08ba), data);

        // slither-disable-next-line low-level-calls
        (bool success, bytes memory result) = address(_core).call(callData);

        if (!success) {
            // slither-disable-next-line assembly
            assembly ("memory-safe") {
                revert(add(result, 32), mload(result))
            }
        }
    }

    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:56]));
        // Swap data uses ETH_ADDRESS for native ETH; translate to
        // address(0) for Ekubo protocol interaction (pool keys, pay,
        // withdraw).
        if (tokenIn == ETH_ADDRESS) tokenIn = address(0);

        address nextTokenIn = tokenIn;
        address nextTokenOut = address(0);

        uint256 hopsLength =
            (swapData.length - _POOL_DATA_OFFSET) / _HOP_BYTE_LEN;

        uint256 offset = _POOL_DATA_OFFSET;

        for (uint256 i = 0; i < hopsLength; i++) {
            nextTokenOut =
                address(bytes20(LibBytes.loadCalldata(swapData, offset)));
            if (nextTokenOut == ETH_ADDRESS) nextTokenOut = address(0);
            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 {
                // slither-disable-next-line calls-loop
                (delta0, delta1) = _core.swap_611415377(
                    pk, nextAmountIn, isToken1, sqrtRatioLimit, _SKIP_AHEAD
                );
            }

            nextTokenIn = nextTokenOut;
            nextAmountIn = -(isToken1 ? delta0 : delta1);

            offset += _HOP_BYTE_LEN;
        }

        _pay(tokenIn, tokenInDebtAmount);
        _core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn));
    }

    function _forward(address to, bytes memory data)
        internal
        returns (bytes memory result)
    {
        // Prepend forward(address) selector to the data
        // We must use assembly here since the Ekubo Core's lock method expects the raw
        // bytes directly and not ABI-encoded bytes
        bytes memory callData = abi.encodePacked(
            bytes4(0x101e8952), bytes32(uint256(uint160(to))), data
        );

        // slither-disable-next-line low-level-calls,calls-loop
        (bool success, bytes memory returnData) = address(_core).call(callData);

        // Assembly is necessary to be able to revert with arbitrary bytes memory
        if (!success) {
            // slither-disable-next-line assembly
            assembly ("memory-safe") {
                revert(add(returnData, 32), mload(returnData))
            }
        }

        return returnData;
    }

    function _pay(address token, uint128 amount) internal {
        if (token == NATIVE_TOKEN_ADDRESS) {
            SafeTransferLib.safeTransferETH(address(_core), amount);
        } else {
            bytes memory callData = abi.encodePacked(
                bytes4(0x0c11dedd), // pay(address) selector
                bytes32(uint256(uint160(token))),
                bytes16(amount)
            );

            // slither-disable-next-line low-level-calls
            (bool success, bytes memory result) = address(_core).call(callData);

            if (!success) {
                // slither-disable-next-line assembly
                assembly ("memory-safe") {
                    revert(add(result, 32), mload(result))
                }
            }
        }
    }

    // To receive withdrawals from Core
    receive() external payable {}

    modifier coreOnly() {
        if (msg.sender != address(_core)) revert EkuboExecutor__CoreOnly();
        _;
    }

    function getTransferData(bytes calldata data)
        external
        pure
        returns (
            TransferManager.TransferType transferType,
            address receiver,
            address tokenIn,
            address tokenOut,
            bool outputToRouter
        )
    {
        uint256 hopsLength =
            (data.length - _POOL_DATA_OFFSET + 36) / _HOP_BYTE_LEN;
        uint256 lastHopOffset = 20 + (hopsLength - 1) * _HOP_BYTE_LEN;
        tokenIn = address(bytes20(data[0:20]));
        tokenOut = address(bytes20(data[lastHopOffset:lastHopOffset + 20]));
        return (
            TransferManager.TransferType.None,
            address(0),
            tokenIn,
            tokenOut,
            false
        );
    }

    function getCallbackTransferData(
        bytes calldata data,
        address tokenIn,
        address /* caller */
    )
        external
        view
        returns (TransferManager.TransferType transferType, address receiver)
    {
        bytes4 selector = bytes4(data[:4]);
        if (selector == _PAY_CALLBACK_SELECTOR) {
            transferType = TransferManager.TransferType.Transfer;
            receiver = address(_core);
        } else {
            // _LOCKED_SELECTOR
            if (tokenIn == ETH_ADDRESS) {
                // ETH transfers are handled in the Executor, so we need to set the
                // transferType to TransferNativeInExecutor to update delta accounting.
                transferType =
                TransferManager.TransferType.TransferNativeInExecutor;
            } else {
                // Locked callback: no transfer needed for ERC20 tokens. This is
                // done in the Pay callback.
                transferType = TransferManager.TransferType.None;
            }
        }
    }
}