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 {IExecutor} from "@interfaces/IExecutor.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {IFeeCalculator} from "@interfaces/IFeeCalculator.sol";
import {FeeRecipient} from "../lib/FeeStructs.sol";
import {TransferManager} from "./TransferManager.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {
    SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
error Dispatcher__UnapprovedExecutor(address executor);
error Dispatcher__ExecutorIsTimelocked(address executor);
error Dispatcher__NonContractExecutor();
error Dispatcher__InvalidDataLength();
error Dispatcher__AddressZero();
error Dispatcher__UnsupportedSingleHopCycle(address token);
error Dispatcher__ExecutorAlreadyExists(address executor);
error Dispatcher__SwapReverted(address executor);
error Dispatcher__CallbackReverted(address executor);

/**
 * @title Dispatcher - Dispatch execution to external contracts
 * @dev Provides the ability to delegate execution of swaps to external
 *  contracts. This allows dynamically adding new supported protocols
 *  without needing to upgrade any contracts. External contracts will
 *  be called using delegatecall so they can share state with the main
 *  contract if needed.
 *
 *  Note: Executor contracts need to implement the IExecutor interface unless
 *  an alternate selector is specified.
 */
contract Dispatcher is TransferManager {
    using SafeERC20 for IERC20;

    mapping(address => uint256) public executorsActivationTimestamp;

    // keccak256("Dispatcher#CURRENTLY_SWAPPING_EXECUTOR_SLOT")
    uint256 private constant _CURRENTLY_SWAPPING_EXECUTOR_SLOT =
        0x098a7a3b47801589e8cdf9ec791b93ad44273246946c32ef1fc4dbe45390c80e;
    // keccak256("Dispatcher#IS_SPLIT_SWAP_SLOT")
    uint256 private constant _IS_SPLIT_SWAP_SLOT =
        0x7b3c4e5f6a8d9e0f1c2b3a4d5e6f7c8d9e0f1c2b3a4d5e6f7c8d9e0f1c2b3a4d;
    // keccak256("Dispatcher#IS_FIRST_SWAP_SLOT")
    uint256 private constant _IS_FIRST_SWAP_SLOT =
        0x8c47a7e3f4c2e1b5a6d9f0e8c7b3a2d1e4f5c6b7a8d9e0f1c2b3a4d5e6f7c8d9;
    // keccak256("Dispatcher#SWAP_INPUT_AMOUNT_SLOT")
    uint256 private constant _SWAP_INPUT_AMOUNT_SLOT =
        0xce9e2e8e50d57f2d688020ea7ab16e2039bcf4dc7175eba827e178586597bb39;
    // keccak256("Dispatcher#SWAP_INPUT_TOKEN_SLOT")
    uint256 private constant _SWAP_INPUT_TOKEN_SLOT =
        0x0c22c14aba48b0e26e3b58475c66f358c352532122e537c32e8184c0159e6e10;

    uint256 public constant DELAY_EXECUTOR_ACTIVATION = 3 days;

    event ExecutorSet(address indexed executor, uint256 timelockExpiresAt);
    event ExecutorRemoved(address indexed executor);

    constructor(address permit2_) TransferManager(permit2_) {}

    /**
     * @dev Adds an approved executor contract address if it is a contract.
     * @param target address of the executor contract
     */
    function _setExecutor(address target) internal {
        if (target.code.length == 0) {
            revert Dispatcher__NonContractExecutor();
        }

        // slither-disable-next-line timestamp
        if (executorsActivationTimestamp[target] != 0) {
            revert Dispatcher__ExecutorAlreadyExists(target);
        }

        uint256 timelockExpiresAt = block.timestamp + DELAY_EXECUTOR_ACTIVATION;
        executorsActivationTimestamp[target] = timelockExpiresAt;
        emit ExecutorSet(target, timelockExpiresAt);
    }

    /**
     * @dev Removes an approved executor contract address
     * @param target address of the executor contract
     */
    function _removeExecutor(address target) internal {
        delete executorsActivationTimestamp[target];
        emit ExecutorRemoved(target);
    }

    /**
     * @dev Calls an executor, assumes swap.protocolData contains
     *  protocol-specific data required by the executor.
     */
    // slither-disable-next-line delegatecall-loop,assembly,controlled-delegatecall,cyclomatic-complexity
    function _callSwapOnExecutor(
        address executor,
        uint256 amount,
        bytes calldata data,
        bool isFirstSwap,
        bool isSplitSwap,
        address receiver
    ) internal returns (uint256 amountOut) {
        _validateExecutor(executor);

        assembly {
            tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, executor)
            tstore(_IS_FIRST_SWAP_SLOT, isFirstSwap)
            tstore(_IS_SPLIT_SWAP_SLOT, isSplitSwap)
            tstore(_SWAP_INPUT_AMOUNT_SLOT, amount)
        }

        // slither-disable-next-line calls-loop
        (
            TransferManager.TransferType transferType,
            address transferReceiver,
            address tokenIn,
            address tokenOut,
            bool outputToRouter
        ) = IExecutor(executor).getTransferData(data);

        if (tokenIn == tokenOut) {
            // Single-hop cycles (such as those made possible via UniswapV4 flash
            // accounting) are not supported.
            revert Dispatcher__UnsupportedSingleHopCycle(tokenIn);
        }

        // Store tokenIn so it can be passed to getCallbackTransferData if a
        // callback occurs during the swap.
        assembly {
            tstore(_SWAP_INPUT_TOKEN_SLOT, tokenIn)
        }

        // Measure output before _transfer so cyclic handling is uniform
        // across callback and direct-transfer types.
        address measureAt = outputToRouter ? address(this) : receiver;
        uint256 balanceBeforeSwap = _balanceOf(tokenOut, measureAt);

        amount = _transfer(
            transferReceiver,
            transferType,
            tokenIn,
            amount,
            isFirstSwap,
            isSplitSwap,
            false
        );

        // slither-disable-next-line controlled-delegatecall,low-level-calls,calls-loop,reentrancy-balance
        (bool success, bytes memory result) = executor.delegatecall(
            abi.encodeWithSelector(
                IExecutor.swap.selector, amount, data, receiver
            )
        );

        // Clear transient storage in case no callback was performed
        assembly {
            tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, 0)
            tstore(_IS_FIRST_SWAP_SLOT, 0)
            tstore(_IS_SPLIT_SWAP_SLOT, 0)
            tstore(_SWAP_INPUT_AMOUNT_SLOT, 0)
            tstore(_SWAP_INPUT_TOKEN_SLOT, 0)
        }

        // Revoke any lingering allowance the protocol didn't consume.
        if (transferType == TransferType.ProtocolWillDebit) {
            _revokeUnconsumedApproval(tokenIn, transferReceiver);
        }

        if (!success) {
            // slither-disable-next-line incorrect-equality
            if (result.length == 0) {
                revert Dispatcher__SwapReverted(executor);
            }
            // Bubble the executor's revert payload byte-for-byte so callers
            // see the original Error(string)/Panic/custom-error
            assembly {
                revert(add(result, 0x20), mload(result))
            }
        }

        uint256 balanceAfterSwap = _balanceOf(tokenOut, measureAt);
        amountOut = balanceAfterSwap - balanceBeforeSwap;

        // Forward if output landed at router but needs to go elsewhere
        if (outputToRouter && receiver != address(this)) {
            amountOut = _transferOut(tokenOut, receiver, amountOut);
        }

        // Delta accounting if tokens stay at router
        if (receiver == address(this)) {
            // slither-disable-next-line calls-loop
            _updateDeltaAccounting(tokenOut, int256(amountOut));
        }
    }

    // slither-disable-next-line assembly
    function _callHandleCallbackOnExecutor(bytes calldata data, address caller)
        internal
        returns (bytes memory)
    {
        address executor;
        bool isFirstSwap;
        bool isSplitSwap;
        uint256 amount;
        address tokenIn;
        assembly {
            executor := tload(_CURRENTLY_SWAPPING_EXECUTOR_SLOT)
            isFirstSwap := tload(_IS_FIRST_SWAP_SLOT)
            isSplitSwap := tload(_IS_SPLIT_SWAP_SLOT)
            amount := tload(_SWAP_INPUT_AMOUNT_SLOT)
            tokenIn := tload(_SWAP_INPUT_TOKEN_SLOT)
        }

        _validateExecutor(executor);

        (TransferManager.TransferType transferType, address receiver) =
            ICallback(executor).getCallbackTransferData(data, tokenIn, caller);

        _transfer(
            receiver,
            transferType,
            tokenIn,
            amount,
            isFirstSwap,
            isSplitSwap,
            true
        );

        // slither-disable-next-line controlled-delegatecall,low-level-calls
        (bool success, bytes memory result) = executor.delegatecall(
            abi.encodeWithSelector(ICallback.handleCallback.selector, data)
        );

        if (!success) {
            // slither-disable-next-line incorrect-equality
            if (result.length == 0) {
                revert Dispatcher__CallbackReverted(executor);
            }
            // Bubble the executor's revert payload byte-for-byte so callers
            // see the original Error(string)/Panic/custom-error
            assembly {
                revert(add(result, 0x20), mload(result))
            }
        }

        // Revoke any lingering allowance the protocol didn't consume.
        if (transferType == TransferType.ProtocolWillDebit) {
            _revokeUnconsumedApproval(tokenIn, receiver);
        }

        // to prevent multiple callbacks
        assembly {
            tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, 0)
            tstore(_IS_FIRST_SWAP_SLOT, 0)
            tstore(_IS_SPLIT_SWAP_SLOT, 0)
        }

        // The final callback result should not be ABI encoded. That is why we are decoding here.
        // ABI encoding is very gas expensive and we want to avoid it if possible.
        // The result from `handleCallback` is always ABI encoded.
        bytes memory decodedResult = abi.decode(result, (bytes));
        return decodedResult;
    }

    function _callFundsExpectedAddress(address executor, bytes calldata data)
        internal
        view
        returns (address receiver)
    {
        _validateExecutor(executor);

        // slither-disable-next-line calls-loop
        receiver = IExecutor(executor).fundsExpectedAddress(data);
    }

    function _validateExecutor(address executor) private view {
        uint256 activationTimestamp = executorsActivationTimestamp[executor];

        // slither-disable-next-line incorrect-equality
        if (activationTimestamp == 0) {
            revert Dispatcher__UnapprovedExecutor(executor);
        }
        // slither-disable-next-line timestamp
        if (block.timestamp < activationTimestamp) {
            revert Dispatcher__ExecutorIsTimelocked(executor);
        }
    }

    function _callGetEffectiveRouterFeeOnOutput(
        address feeCalculator,
        address client
    ) internal view returns (uint16 routerFeeOnOutputBps) {
        // slither-disable-next-line calls-loop
        routerFeeOnOutputBps =
            IFeeCalculator(feeCalculator).getEffectiveRouterFeeOnOutput(client);
    }

    function _callCalculateFee(
        address feeCalculator,
        uint256 amountIn,
        uint16 clientFeeBps,
        address client
    )
        internal
        view
        returns (uint256 amountOut, FeeRecipient[] memory feeRecipients)
    {
        // slither-disable-next-line calls-loop
        (amountOut, feeRecipients) = IFeeCalculator(feeCalculator)
            .calculateFee(amountIn, client, clientFeeBps);
    }
}